Skip to content

Commit ae039a7

Browse files
committed
Implement typed keyword argument stages and &key support
This commit introduces first-class keyword arguments into Coalton's function type system and syntax. Keyword arguments are all-optional. A keyword with a default has visible binder type T. A keyword without a default has visible binder type (Optional T). Omitted no-default keywords are treated as None, making omission and explicit None equivalent for those keys. Some amount of row polymphism is supported: calling a function that has more keyword arguments than supplied (e.g., through high-order gymnastics) will automatically eta-expand into a call with all arguments supplied. Type representation: add keyword-stage and entry nodes to parser and typechecker type ASTs, with pretty-printer support and parser support for keyword stages in declared types. Unification now understands keyword stages via closed-row matching logic. Environment metadata and related plumbing are extended so keyword parameters survive inference, diagnostics, and documentation paths. Syntax: add &key support for named definitions and anonymous fn argument lists; add keyword argument parsing at call sites. Enforce ordering and validity rules (keywords after positional arguments, no duplicates, malformed pairs rejected). Add targeted diagnostics for invalid placement, duplicate binders, malformed call syntax, unknown keys, and declaration/definition keyword-stage mismatch. Compilation strategy: preserve a simple runtime model by lowering keyword stages to ordinary positional calls in canonical keyword order. Defaulted visible binders are computed from hidden Optional physical parameters (Some provided value, None uses default expression). No-default binders flow as Optional directly. This keeps runtime/codegen on standard lambdas and applications. Tests: extend parser/type inference/runtime coverage for keyword syntax and behavior, including nested keyword-lambda regression cases; update user and internals docs with keyword-stage syntax/semantics and lowering rationale. Verified with full test run (make test).
1 parent 7ffbd50 commit ae039a7

25 files changed

+2174
-407
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ coverage-report/
1313
benchmarks/benchmarks-game/pidigits
1414

1515
bench.json
16+
system-index.txt

docs/internals/internals.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,20 @@ Parsing of expressions also occurs in this file.
6363

6464
Some constructions are de-sugared directly in parsing. For example, there is no `node-rec`; it is desugared into a `node-let` immediately.
6565

66+
Keyword arguments are parsed here as explicit AST data:
67+
68+
- function/`fn` parameters may include `&key` entries
69+
- call sites may include keyword argument pairs
70+
- parser-level validation handles malformed placement, missing values, and duplicates
71+
6672
### Intermediate representation for types
6773

6874
File: `src/parser/types.lisp`
6975

7076
The IR for types is a relatively straightforward data structure, which are substructures of `ty`. This file also contains the parser for said data structure
7177

78+
Function types can include a keyword stage, represented as `(&key ...)` in type syntax.
79+
7280
### Transformations of the AST
7381

7482
File: `src/parser/renamer.lisp`
@@ -110,6 +118,18 @@ Type checking relies mainly on:
110118

111119
There are other modules to handle more specific things, but the bulk of the "type calculus" is defined above.
112120

121+
Keyword arguments are represented in the typechecker by a dedicated keyword-stage type node (`keyword-stage-ty`) with keyword entries (`keyword-ty-entry`).
122+
123+
Unification currently treats keyword rows as closed: key sets must match exactly, and corresponding entry types are unified by key name.
124+
125+
During inference, keyword stages are lowered to ordinary positional arguments before codegen:
126+
127+
- defaulted key `k : T` is passed physically as `(Optional T)` and defaulted in the callee
128+
- no-default key `k : Optional T` is passed physically as `(Optional T)` directly
129+
- omitted keys lower to `None`
130+
131+
After this lowering, codegen sees only ordinary lambda/application shapes.
132+
113133
### Environment
114134

115135
File: `src/typechecker/environment.lisp`

docs/intro-to-coalton.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,47 @@ Here is an example of using a curried function to transform a list.
197197
(map (+ 2) nums)) ;; 4 5 6 7
198198
```
199199

200+
### Keyword Arguments
201+
202+
Coalton also supports keyword argument stages on functions.
203+
204+
```lisp
205+
(coalton-toplevel
206+
(define (run-job x &key (timeout 1000) tag)
207+
(match tag
208+
((Some _) (+ x timeout))
209+
((None) x))))
210+
```
211+
212+
Rules:
213+
214+
- Keyword arguments must come after positional arguments at call sites.
215+
- All keyword arguments are omittable.
216+
- A keyword with a default (for example, `(timeout 1000)`) has binder type `T`.
217+
- A keyword without a default (for example, `tag`) has binder type `(Optional T)`.
218+
- Omitting a no-default key is equivalent to passing `None`.
219+
220+
Example calls:
221+
222+
```lisp
223+
(coalton-toplevel
224+
(define a (run-job 10))
225+
(define b (run-job 10 :timeout 2000))
226+
(define c (run-job 10 :tag (Some "prod"))))
227+
```
228+
229+
Keyword information is also expressible in explicit types:
230+
231+
```lisp
232+
(coalton-toplevel
233+
(declare run-job
234+
(Integer -> (&key :timeout Integer
235+
:tag (Optional String))
236+
-> Integer)))
237+
```
238+
239+
Currying still applies to positional arguments. Once a call reaches a keyword stage, that stage is consumed immediately (using provided keys and defaults/omissions). If you want to intentionally produce a keyword-taking closure after fixing positionals, use eta-expansion with `fn`.
240+
200241
### Pipelining Syntax and Function Composition
201242

202243
There are convenient syntaxes for composing functions with the `pipe` and `nest` macros.

src/codegen/typecheck-node.lisp

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@
1111

1212
(in-package #:coalton-impl/codegen/typecheck-node)
1313

14+
(defun erase-keyword-stages (type)
15+
"Convert keyword stages in TYPE into physical Optional-arrow form."
16+
(declare (type tc:ty type)
17+
(values tc:ty &optional))
18+
(typecase type
19+
(tc:keyword-stage-ty
20+
(loop :with out := (erase-keyword-stages (tc:keyword-stage-ty-to type))
21+
:for entry :in (reverse (tc:keyword-stage-ty-entries type))
22+
:do (setf out
23+
(tc:make-function-type
24+
(erase-keyword-stages
25+
(tc:keyword-entry-physical-type
26+
(tc:keyword-ty-entry-type entry)))
27+
out))
28+
:finally (return out)))
29+
(tc:tapp
30+
(tc:make-tapp
31+
:from (erase-keyword-stages (tc:tapp-from type))
32+
:to (erase-keyword-stages (tc:tapp-to type))))
33+
(t
34+
type)))
35+
36+
(defun codegen-unify (subs type1 type2)
37+
(declare (type tc:substitution-list subs)
38+
(type tc:ty type1 type2)
39+
(values tc:substitution-list &optional))
40+
(tc:unify subs
41+
(erase-keyword-stages type1)
42+
(erase-keyword-stages type2)))
43+
1444
(defgeneric typecheck-node (expr env)
1545
(:documentation "Check that EXPR is valid. Currently only verifies
1646
that applied functions match their arguments.")
@@ -37,8 +67,8 @@
3767
(loop :for arg :in (node-application-rands expr)
3868
:for arg-ty := (typecheck-node arg env) :do
3969
(progn
40-
(setf subs (tc:unify subs (tc:function-type-from type) arg-ty))
41-
(setf subs (tc:unify subs arg-ty (tc:function-type-from type)))
70+
(setf subs (codegen-unify subs (tc:function-type-from type) arg-ty))
71+
(setf subs (codegen-unify subs arg-ty (tc:function-type-from type)))
4272
(setf type (tc:function-type-to type))))
4373
(node-type expr)))
4474

@@ -53,8 +83,8 @@
5383
(loop :for arg :in (node-direct-application-rands expr)
5484
:for arg-ty := (typecheck-node arg env) :do
5585
(progn
56-
(setf subs (tc:unify subs (tc:function-type-from type) arg-ty))
57-
(setf subs (tc:unify subs arg-ty (tc:function-type-from type)))
86+
(setf subs (codegen-unify subs (tc:function-type-from type) arg-ty))
87+
(setf subs (codegen-unify subs arg-ty (tc:function-type-from type)))
5888
(setf type (tc:function-type-to type))))
5989
(node-type expr)))
6090

@@ -71,8 +101,8 @@
71101
(setf type (tc:function-type-to type))))
72102

73103
(let ((subexpr-ty (typecheck-node (node-abstraction-subexpr expr) env)))
74-
(setf subs (tc:unify subs type subexpr-ty))
75-
(setf subs (tc:unify subs subexpr-ty type))
104+
(setf subs (codegen-unify subs type subexpr-ty))
105+
(setf subs (codegen-unify subs subexpr-ty type))
76106
(node-type expr))))
77107

78108
(:method ((expr node-let) env)
@@ -84,17 +114,17 @@
84114
(let ((subexpr-ty (typecheck-node (node-let-subexpr expr) env))
85115

86116
(subs nil))
87-
(setf subs (tc:unify subs subexpr-ty (node-type expr)))
88-
(setf subs (tc:unify subs (node-type expr) subexpr-ty))
117+
(setf subs (codegen-unify subs subexpr-ty (node-type expr)))
118+
(setf subs (codegen-unify subs (node-type expr) subexpr-ty))
89119
subexpr-ty))
90120

91121
(:method ((expr node-locally) env)
92122
(declare (type tc:environment env)
93123
(values tc:ty))
94124
(let ((subexpr-ty (typecheck-node (node-locally-subexpr expr) env))
95125
(subs nil))
96-
(setf subs (tc:unify subs subexpr-ty (node-type expr)))
97-
(setf subs (tc:unify subs (node-type expr) subexpr-ty))
126+
(setf subs (codegen-unify subs subexpr-ty (node-type expr)))
127+
(setf subs (codegen-unify subs (node-type expr) subexpr-ty))
98128
subexpr-ty))
99129

100130
(:method ((expr node-lisp) env)
@@ -123,8 +153,8 @@
123153
(loop :for branch :in (node-match-branches expr)
124154
:for subexpr-ty := (typecheck-node branch env) :do
125155
(progn
126-
(setf subs (tc:unify subs type subexpr-ty))
127-
(setf subs (tc:unify subs subexpr-ty type))))
156+
(setf subs (codegen-unify subs type subexpr-ty))
157+
(setf subs (codegen-unify subs subexpr-ty type))))
128158
type))
129159

130160
(:method ((expr catch-branch) env)
@@ -142,8 +172,8 @@
142172
(loop :for branch :in (node-catch-branches expr)
143173
:for subexpr-ty := (typecheck-node branch env) :do
144174
(progn
145-
(setf subs (tc:unify subs type subexpr-ty))
146-
(setf subs (tc:unify subs subexpr-ty type))))
175+
(setf subs (codegen-unify subs type subexpr-ty))
176+
(setf subs (codegen-unify subs subexpr-ty type))))
147177
type))
148178

149179
(:method ((expr node-resumable) env)
@@ -156,8 +186,8 @@
156186
(loop :for branch :in (node-resumable-branches expr)
157187
:for subexpr-ty := (typecheck-node branch env) :do
158188
(progn
159-
(setf subs (tc:unify subs type subexpr-ty))
160-
(setf subs (tc:unify subs subexpr-ty type))))
189+
(setf subs (codegen-unify subs type subexpr-ty))
190+
(setf subs (codegen-unify subs subexpr-ty type))))
161191
type))
162192

163193
(:method ((expr resumable-branch) env)
@@ -205,8 +235,8 @@
205235
(let ((last-node (car (last (node-seq-nodes expr))))
206236

207237
(subs nil))
208-
(setf subs (tc:unify subs (node-type expr) (node-type last-node)))
209-
(setf subs (tc:unify subs (node-type last-node) (node-type expr)))
238+
(setf subs (codegen-unify subs (node-type expr) (node-type last-node)))
239+
(setf subs (codegen-unify subs (node-type last-node) (node-type expr)))
210240
(node-type last-node)))
211241

212242
(:method ((expr node-return-from) env)
@@ -230,7 +260,7 @@
230260
(:method ((expr node-block) env)
231261
(declare (type tc:environment env)
232262
(values tc:ty))
233-
(tc:unify
263+
(codegen-unify
234264
nil
235265
(node-type expr)
236266
(typecheck-node (node-block-body expr) env))
@@ -246,7 +276,7 @@
246276
(declare (type tc:environment env)
247277
(values tc:ty))
248278
(typecheck-node (node-dynamic-extent-node expr) env)
249-
(tc:unify
279+
(codegen-unify
250280
nil
251281
(node-type expr)
252282
(typecheck-node (node-dynamic-extent-body expr) env))
@@ -256,7 +286,7 @@
256286
(declare (type tc:environment env)
257287
(values tc:ty))
258288
(typecheck-node (node-bind-expr expr) env)
259-
(tc:unify
289+
(codegen-unify
260290
nil
261291
(node-type expr)
262292
(typecheck-node (node-bind-body expr) env))
@@ -274,14 +304,14 @@
274304
(length (node-values-nodes expr))))
275305
(loop :for subnode :in (node-values-nodes expr)
276306
:for comp-ty :in components
277-
:do (tc:unify nil (typecheck-node subnode env) comp-ty))
307+
:do (codegen-unify nil (typecheck-node subnode env) comp-ty))
278308
(node-type expr)))
279309

280310
(:method ((expr node-mv-call) env)
281311
(declare (type tc:environment env)
282312
(values tc:ty))
283313
(let ((sub-ty (typecheck-node (node-mv-call-expr expr) env)))
284-
(tc:unify nil (node-type expr) sub-ty)
314+
(codegen-unify nil (node-type expr) sub-ty)
285315
(node-type expr)))
286316

287317
(:method ((expr node-values-bind) env)
@@ -296,7 +326,7 @@
296326
(length components)
297327
(length (node-values-bind-vars expr))))
298328
(typecheck-node (node-values-bind-expr expr) env)
299-
(tc:unify nil (node-type expr) (typecheck-node (node-values-bind-body expr) env))
329+
(codegen-unify nil (node-type expr) (typecheck-node (node-values-bind-body expr) env))
300330
(node-type expr)))
301331

302332
(:method ((expr node-values-match) env)
@@ -308,6 +338,6 @@
308338
(loop :for branch :in (node-values-match-branches expr)
309339
:for subexpr-ty := (typecheck-node branch env) :do
310340
(progn
311-
(setf subs (tc:unify subs type subexpr-ty))
312-
(setf subs (tc:unify subs subexpr-ty type))))
341+
(setf subs (codegen-unify subs type subexpr-ty))
342+
(setf subs (codegen-unify subs subexpr-ty type))))
313343
type)))

src/doc/markdown.lisp

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,21 +240,42 @@
240240
(object-aname ty)
241241
(html-entities:encode-entities (object-name ty))))))
242242

243+
(defun write-keyword-stage-head (stream stage)
244+
(declare (type tc:keyword-stage-ty stage)
245+
(values null &optional))
246+
(write-string "(&key" stream)
247+
(dolist (entry (tc:keyword-stage-ty-entries stage))
248+
(write-string " " stream)
249+
(write-string (html-entities:encode-entities
250+
(format nil "~S" (tc:keyword-ty-entry-keyword entry)))
251+
stream)
252+
(write-string " " stream)
253+
(write-string (to-markdown (tc:keyword-ty-entry-type entry)) stream))
254+
(write-string ")" stream)
255+
nil)
256+
243257
(defmethod to-markdown ((ty tc:tapp))
244258
(with-output-to-string (stream)
245259
(cond
246260
((tc:function-type-p ty) ;; Print function types
247261
(write-string "(" stream)
248262
(write-string (to-markdown (tc:tapp-to (tc:tapp-from ty))) stream)
249-
(write-string (html-entities:encode-entities "") stream)
250-
;; Avoid printing extra parentheses on curried functions
251-
(labels ((print-subfunction (to)
263+
;; Avoid printing extra parentheses on curried functions.
264+
;; A function tail may be a keyword stage, not only a tapp.
265+
(labels ((write-arrow ()
266+
(write-string (html-entities:encode-entities "") stream))
267+
(print-subfunction (to)
252268
(cond
253-
((tc:function-type-p to)
269+
((typep to 'tc:keyword-stage-ty)
270+
(write-arrow)
271+
(write-keyword-stage-head stream to)
272+
(print-subfunction (tc:keyword-stage-ty-to to)))
273+
((and (typep to 'tc:tapp) (tc:function-type-p to))
274+
(write-arrow)
254275
(write-string (to-markdown (tc:tapp-to (tc:tapp-from to))) stream)
255-
(write-string (html-entities:encode-entities "") stream)
256276
(print-subfunction (tc:tapp-to to)))
257277
(t
278+
(write-arrow)
258279
(write-string (to-markdown to) stream)))))
259280
(print-subfunction (tc:tapp-to ty)))
260281
(write-string ")" stream))
@@ -277,12 +298,20 @@
277298
(write-string (to-markdown arg) stream))
278299
(write-string ")" stream))
279300
(t
280-
(write-string "(" stream)
281-
(write-string (to-markdown (tc:tapp-from ty)) stream)
282-
(write-string " " stream)
301+
(write-string "(" stream)
302+
(write-string (to-markdown (tc:tapp-from ty)) stream)
303+
(write-string " " stream)
283304
(write-string (to-markdown (tc:tapp-to ty)) stream)
284305
(write-string ")" stream))))))))
285306

307+
(defmethod to-markdown ((ty tc:keyword-stage-ty))
308+
(with-output-to-string (stream)
309+
(write-string "(" stream)
310+
(write-keyword-stage-head stream ty)
311+
(write-string (html-entities:encode-entities "") stream)
312+
(write-string (to-markdown (tc:keyword-stage-ty-to ty)) stream)
313+
(write-string ")" stream)))
314+
286315
(defmethod to-markdown ((object tc:qualified-ty))
287316
(let ((preds (tc:qualified-ty-predicates object))
288317
(qual-type (tc:qualified-ty-type object)))

0 commit comments

Comments
 (0)