Two things out of one mechanism. `_' as a type means "work it out from
the initializer", so (var n _ (strlen s)) stops needing size-t spelled
out; `type-of' hands a macro the type of an expression, so a macro can
dispatch on what it was handed rather than on what was declared. Both
read the same answers from two sides.
Algorithm W's core, intra-procedural, with the extensions C forces:
- an unknown type, since (include stdio.h) brings in names we never
parsed. Unification is consistency rather than equality, so
anything touching an unparsed declaration stops constraining
instead of rejecting a program that compiled yesterday;
- the usual arithmetic conversions, since `+' is not a function of
one type;
- checking mode for initializers, since #(0 0) has no type of its own
and takes one from its context. #(T : ...) is the way out of that.
What it wanted on the way:
- what type a *name* has, which neither the typedef nor the tag
database recorded. One table serves functions and variables, since
a function type already has a surface spelling;
- a scope chain, so a (var c int 9) inside a do ends with the block;
- form-type, keyed by cons cell, so one form has one type;
- macros expanded during the walk rather than before it, so type-of
is answered in the scope the macro was written in.
Closures take the same machinery: a receiver whose type comes from a
call, captures written (name expr) and typed from the expression, and
conversion from a bare function wherever a closure is expected.
type-match grew `_' on the pattern side, since (closure ((int)) int)
and (closure ((float)) int) were separate clauses for one case.
122 lines
4.2 KiB
Scheme
122 lines
4.2 KiB
Scheme
(input)
|
|
(output "Adders: 15 25"
|
|
"Two captures: 47"
|
|
"No captures: 7"
|
|
"Through a parameter: 110"
|
|
"From an array: 1 2 3"
|
|
"Index evaluated once: 21 i 1"
|
|
"Through a struct member: 8"
|
|
"Shadowed in a block: 9 then 15"
|
|
"Nested: 33"
|
|
"Named capture: 7"
|
|
"Captured pointer: 11 then 12"
|
|
"From a bare fn: 20 42 7")
|
|
(return 0)
|
|
|
|
;;; A closure is a code pointer beside its captures, so what this
|
|
;;; checks is that the captures survive the lifting -- that two
|
|
;;; closures of one shape keep their own environments, that a closure
|
|
;;; outlives the call that built it, and that calling one through a
|
|
;;; parameter, an array element or a struct member resolves the same
|
|
;;; way as through a local.
|
|
;;;
|
|
;;; A receiver is also an ordinary expression: `[table (++ i)]' has to
|
|
;;; evaluate its index exactly once, the way it would for an array of
|
|
;;; function pointers.
|
|
|
|
(include stdio.h)
|
|
|
|
(fn double-it ((n int)) int
|
|
(return (* n 2)))
|
|
|
|
;;; a bare function is a closure that captures nothing, so it converts
|
|
;;; wherever one is expected -- here a declared return type
|
|
(fn as-closure () (closure ((int)) int)
|
|
(return double-it))
|
|
|
|
(fn make-adder ((n int)) (closure ((int)) int)
|
|
(return (closure ((b int)) int (n)
|
|
(return (+ n b)))))
|
|
|
|
(fn make-affine ((k int) (b int)) (closure ((int)) int)
|
|
(return (closure ((x int)) int (k b)
|
|
(return (+ (* k x) b)))))
|
|
|
|
(fn make-const-7 () (closure () int)
|
|
(return (closure () int ()
|
|
(return 7))))
|
|
|
|
;;; A closure arriving as a parameter: its type is written, so the call
|
|
;;; resolves without knowing where it came from
|
|
(fn apply-twice ((f (closure ((int)) int)) (x int)) int
|
|
(return (f (f x))))
|
|
|
|
(struct handlers ((on-tick (closure ((int)) int))))
|
|
|
|
(pub fn main () int
|
|
(var add-10 (closure ((int)) int) (make-adder 10))
|
|
(var add-20 (closure ((int)) int) (make-adder 20))
|
|
(printf "Adders: %d %d\n" (add-10 5) (add-20 5))
|
|
|
|
(var affine (closure ((int)) int) (make-affine 5 2))
|
|
(printf "Two captures: %d\n" (affine 9))
|
|
|
|
(var seven (closure () int) (make-const-7))
|
|
(printf "No captures: %d\n" (seven))
|
|
|
|
(printf "Through a parameter: %d\n" (apply-twice (make-adder 50) 10))
|
|
|
|
(var table (¤ (closure ((int)) int) 3))
|
|
(var i int 0)
|
|
(for (= i 0) (< i 3) (++ i)
|
|
(= (¤ table i) (make-adder i)))
|
|
(printf "From an array: %d %d %d\n"
|
|
((¤ table 0) 1) ((¤ table 1) 1) ((¤ table 2) 1))
|
|
|
|
;; the index must be evaluated once, so `i' ends at 1 and not 2 --
|
|
;; read in a separate statement, since reading and bumping it in one
|
|
;; printf would be unsequenced whatever the closure did
|
|
(= i 0)
|
|
(var once int ([table (++ i)] 20))
|
|
(printf "Index evaluated once: %d i %d\n" once i)
|
|
|
|
(var h (struct handlers) #((struct handlers) : .on-tick (make-adder 5)))
|
|
(printf "Through a struct member: %d\n" ((. h on-tick) 3))
|
|
|
|
;; a block opens a scope: the inner `add-10' ends with it, and the
|
|
;; call after it is the closure again
|
|
(do (var add-10 int 9)
|
|
(printf "Shadowed in a block: %d then " add-10))
|
|
(printf "%d\n" (add-10 5))
|
|
|
|
;; a closure built inside a closure, capturing that one's capture
|
|
(var outer (closure ((int)) int)
|
|
(closure ((x int)) int ()
|
|
(var inner (closure ((int)) int) (make-adder x))
|
|
(return (inner 3))))
|
|
(printf "Nested: %d\n" (outer 30))
|
|
|
|
;; a capture can name what it holds rather than borrow a variable's
|
|
;; name, and the expression is evaluated where the closure is written
|
|
(var pt (struct handlers))
|
|
(var sum-once (closure () int)
|
|
(closure () int ((sum (+ 3 4))) (return sum)))
|
|
(printf "Named capture: %d\n" (sum-once))
|
|
|
|
;; capturing a pointer is how by-reference is spelled; the caller owns
|
|
;; what it points at
|
|
(var counter int 11)
|
|
(var peek (closure () int)
|
|
(closure () int ((at (& counter))) (return (* at))))
|
|
(printf "Captured pointer: %d then " (peek))
|
|
(++ counter)
|
|
(printf "%d\n" (peek))
|
|
|
|
;; ...and in an initializer, as an argument, and as a return
|
|
(var from-fn (closure ((int)) int) double-it)
|
|
(printf "From a bare fn: %d %d %d\n"
|
|
(apply-twice from-fn 5)
|
|
((as-closure) 21)
|
|
(apply-twice (lambda ((n int)) int (return (+ n 1))) 5))
|
|
(return 0))
|