forked from alex-eg/sex
implement type inference
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.
This commit is contained in:
@@ -6,7 +6,11 @@
|
||||
"From an array: 1 2 3"
|
||||
"Index evaluated once: 21 i 1"
|
||||
"Through a struct member: 8"
|
||||
"Nested: 33")
|
||||
"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
|
||||
@@ -22,6 +26,14 @@
|
||||
|
||||
(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)))))
|
||||
@@ -71,10 +83,39 @@
|
||||
(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))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
(input)
|
||||
(output "direct: 120"
|
||||
"fac: 120 3628800"
|
||||
"fib: 55 6765")
|
||||
"fib: 55 6765"
|
||||
"applied on the spot: 120")
|
||||
(return 0)
|
||||
|
||||
;;; A fixed point built out of closures, which is the hardest thing to
|
||||
@@ -52,4 +53,9 @@
|
||||
(var fib (closure ((int)) int) (fix (& fib-knot)))
|
||||
(printf "fac: %d %d\n" (fac 5) (fac 10))
|
||||
(printf "fib: %d %d\n" (fib 10) (fib 20))
|
||||
|
||||
;; the combinator's result invoked where it is returned, with no
|
||||
;; intervening `var' -- the receiver's type is the return type of the
|
||||
;; signature it came from, which is what the name table records
|
||||
(printf "applied on the spot: %d\n" ((fix (& fac-knot)) 5))
|
||||
(return 0))
|
||||
|
||||
178
tests/sex-programs/inference.sex
Normal file
178
tests/sex-programs/inference.sex
Normal file
@@ -0,0 +1,178 @@
|
||||
(input)
|
||||
(output "15"
|
||||
"42 0.25"
|
||||
"(5, 7)"
|
||||
"(1, 2)"
|
||||
"3"
|
||||
"(5, 7)"
|
||||
"0 1 4 9 "
|
||||
"4"
|
||||
"42"
|
||||
"<closure of 0: 10>"
|
||||
"42"
|
||||
"2 1"
|
||||
"Hello from Sex!")
|
||||
(return 0)
|
||||
|
||||
;;; Both halves of inference, from the two sides that read the same
|
||||
;;; answers: `_' in a type means "work it out", and `type-of' hands a
|
||||
;;; macro the type of an expression.
|
||||
;;;
|
||||
;;; This was written as a draft before either existed, to be read
|
||||
;;; before it was built. It is registered now.
|
||||
;;;
|
||||
;;; Two features, one mechanism:
|
||||
;;;
|
||||
;;; `_' in a type means "work it out", and
|
||||
;;; `type-of' hands a macro the type of an expression.
|
||||
;;;
|
||||
;;; Both are the same solved constraint store, read from two sides.
|
||||
|
||||
(include stdio.h)
|
||||
(include string.h)
|
||||
|
||||
;;; `(include string.h)' is for the C compiler; it tells Sex nothing.
|
||||
;;; A signature has to be written before `_' can be resolved from a
|
||||
;;; call to `strlen' -- without one the call's type is `?', and a `_'
|
||||
;;; that resolves to `?' is an error, not a silent int.
|
||||
(extern fn strlen ((s (* const char))) size-t)
|
||||
|
||||
(struct point ((x int) (y int)))
|
||||
|
||||
(fn midpoint ((a (* const struct point)) (b (* const struct point))) (struct point)
|
||||
(var m (struct point))
|
||||
;; No `_' here: `m' has no initializer to infer from. Inference fills
|
||||
;; in a type, it does not invent one.
|
||||
(= (. m x) (/ (+ (-> a x) (-> b x)) 2))
|
||||
(= (. m y) (/ (+ (-> a y) (-> b y)) 2))
|
||||
(return m))
|
||||
|
||||
;;; The closure's type is written once, in the signature; `_' reads it
|
||||
;;; from there at every use.
|
||||
(fn make-adder ((n int)) (closure ((int)) int)
|
||||
(return (closure ((b int)) int (n)
|
||||
(return (+ n b)))))
|
||||
|
||||
;;; A macro that asks what it was handed.
|
||||
;;;
|
||||
;;; `type-of' returns a *surface* type -- the same spelling the type
|
||||
;;; database hands to `map-fields' -- so it composes with the
|
||||
;;; `type-match' that already exists, and dispatch over a user struct
|
||||
;;; costs nothing extra.
|
||||
(defmacro (print x)
|
||||
(type-match (type-of x)
|
||||
(int `(printf "%d\n" ,x))
|
||||
(size-t `(printf "%zu\n" ,x))
|
||||
(double `(printf "%g\n" ,x))
|
||||
((* const char) `(printf "%s\n" ,x))
|
||||
;; NOTE: ,x twice -- a macro that duplicates its argument still has
|
||||
;; to think about evaluating it twice. Inference does not fix that.
|
||||
((struct point) `(printf "(%d, %d)\n" (. ,x x) (. ,x y)))
|
||||
;; a closure is a type like any other, so it dispatches like one --
|
||||
;; and `_' saves a clause per signature
|
||||
((closure _ int) `(printf "<closure of 0: %d>\n" (,x 0)))
|
||||
(else (error "print: don't know how to print" (type-of x)))))
|
||||
|
||||
;;; The temporary's type is the thing the macro could not write down
|
||||
;;; before. Either spelling works -- `_' is the lazier one, and it is
|
||||
;;; inferred in the expansion's own scope.
|
||||
(defmacro (swap a b)
|
||||
`(do (var tmp _ ,a)
|
||||
(= ,a ,b)
|
||||
(= ,b tmp)))
|
||||
|
||||
(pub fn main () int
|
||||
;; Written out, for contrast with everything below it.
|
||||
(var greeting (* const char) "Hello from Sex!")
|
||||
|
||||
;; size-t, from the signature above -- not int, and not a guess.
|
||||
(var n _ (strlen greeting))
|
||||
(printf "%zu\n" n)
|
||||
|
||||
;; Literals carry a constraint, not a type: the int-ish one defaults
|
||||
;; to int, and the mixed division joins to double the way C does.
|
||||
(var count _ (+ 20 22))
|
||||
(var half _ (/ 1.0 4))
|
||||
(printf "%d %g\n" count half)
|
||||
|
||||
;; An aggregate initializer has no type of its own, so the type flows
|
||||
;; in and has to be written. `(var origin _ #(3 4))' is an error --
|
||||
;; there is nothing to infer from.
|
||||
(var origin (struct point) #(3 4))
|
||||
(var corner (struct point) #(7 10))
|
||||
|
||||
;; A compound literal is the way out of that rule: the `:' is where an
|
||||
;; initializer stops needing a type from its context, so `_' has
|
||||
;; something to read after all.
|
||||
(var centre _ #((struct point) : 5 7))
|
||||
(print centre)
|
||||
|
||||
;; ...and the same designated, which names fields instead of counting
|
||||
;; positions.
|
||||
(var offset _ #((struct point) : .x 1 .y 2))
|
||||
(print offset)
|
||||
|
||||
;; A partial type: "a pointer to something". The something arrives
|
||||
;; from the initializer. This is why the wildcard lives in the type
|
||||
;; grammar rather than beside it -- it composes.
|
||||
(var p (* _) (& origin))
|
||||
|
||||
;; Member access reads the same type database the macros do.
|
||||
(var x _ (-> p x))
|
||||
(printf "%d\n" x)
|
||||
|
||||
;; A call into a function Sex has actually parsed: the return type is
|
||||
;; the whole answer, and `print' then dispatches on it.
|
||||
(var mid _ (midpoint (& origin) (& corner)))
|
||||
(print mid)
|
||||
|
||||
;; The loop variable, which is where `_' earns its keep most often.
|
||||
(for (var i _ 0) (< i 4) (++ i)
|
||||
(printf "%d " (* i i)))
|
||||
(printf "\n")
|
||||
|
||||
;; Four of something: the element type is fixed by the initializer,
|
||||
;; the count by the type. Subscripting gives the element type back.
|
||||
(var squares [_ 4] #(0 1 4 9))
|
||||
(print [squares 2])
|
||||
|
||||
;; A closure's type comes from the signature that produced it, and
|
||||
;; calling one needs that type and nothing else.
|
||||
(var add-10 _ (make-adder 10))
|
||||
(print (add-10 32))
|
||||
(print add-10)
|
||||
|
||||
;; ...including where it is returned, with no name in between.
|
||||
(print ((make-adder 20) 22))
|
||||
|
||||
;; A macro writing a declaration it could not have written before.
|
||||
(var a _ 1)
|
||||
(var b _ 2)
|
||||
(swap a b)
|
||||
(printf "%d %d\n" a b)
|
||||
|
||||
(print greeting)
|
||||
(return 0))
|
||||
|
||||
;;; Open questions this draft raises, to settle before Layer 2 ships:
|
||||
;;;
|
||||
;;; 1. SETTLED. `type-match' took `_' on the pattern side, so
|
||||
;;; `(closure _ int)' above is one clause rather than one per
|
||||
;;; signature, and `(* _)' and `(¤ _ _)' say "any pointer" and "any
|
||||
;;; array". A `_' written last takes the rest, since a type's words
|
||||
;;; are spread and not nested: `(* const char)' is three elements.
|
||||
;;; Nothing destructures -- a macro body is Scheme and a type is a
|
||||
;;; list, so `(caddr (type-of x))' reads an array's length.
|
||||
;;;
|
||||
;;; 2. SETTLED, allowed. `[_ 4]' against `#(0 1 4 9)' unifies each
|
||||
;;; element with the hole, so the element type comes from the
|
||||
;;; literals and the length stays as written -- `[_ 10]' with two
|
||||
;;; initializers is still ten. Elements that disagree are a type
|
||||
;;; mismatch. A bare `_' is still refused: `#(0 1 4 9)' has no type
|
||||
;;; of its own, only elements.
|
||||
;;;
|
||||
;;; 3. POSTPONED to the standard library design. `(extern fn strlen
|
||||
;;; ...)' above duplicates string.h, which is the same bargain every
|
||||
;;; FFI makes, but it is where "no C header parsing" starts costing
|
||||
;;; the user something. A `sex/libc' module of prototypes is the
|
||||
;;; obvious answer and belongs with the rest of the stdlib.
|
||||
83
tests/sex-programs/wildcards.sex
Normal file
83
tests/sex-programs/wildcards.sex
Normal file
@@ -0,0 +1,83 @@
|
||||
(input)
|
||||
(output "literals: 42 3.5 hello"
|
||||
"calls: 12"
|
||||
"members: 1 2.5"
|
||||
"pointers: 1 2.5"
|
||||
"arrays: 30"
|
||||
"loop: 0 1 2"
|
||||
"shadowed: 9 then 42"
|
||||
"partial: 1 2.5"
|
||||
"joined: 43.5 84 49 1"
|
||||
"from elements: 4 9 0")
|
||||
(return 0)
|
||||
|
||||
;;; `_' as a type means "work it out from the initializer". What the
|
||||
;;; pass can answer comes from declarations -- Sex writes a type at
|
||||
;;; every binding site -- and from the signature of whatever a call
|
||||
;;; names. A partial type like `(* _)' is solved by unifying what was
|
||||
;;; written against what the initializer gives, so only the wildcard
|
||||
;;; inside the spelling is filled in.
|
||||
|
||||
(include stdio.h)
|
||||
|
||||
(struct point ((x int) (y float)))
|
||||
|
||||
(fn area ((w int) (h int)) int
|
||||
(return (* w h)))
|
||||
|
||||
(pub fn main () int
|
||||
(var n _ 42)
|
||||
(var f _ 3.5)
|
||||
(var s _ "hello")
|
||||
(printf "literals: %d %g %s\n" n f s)
|
||||
|
||||
(var a _ (area 3 4))
|
||||
(printf "calls: %d\n" a)
|
||||
|
||||
(var p (struct point) #((struct point) : .x 1 .y 2.5))
|
||||
(var px _ (. p x))
|
||||
(var py _ (. p y))
|
||||
(printf "members: %d %g\n" px py)
|
||||
|
||||
(var pp _ (& p))
|
||||
(printf "pointers: %d %g\n" (-> pp x) (-> pp y))
|
||||
|
||||
(var table (¤ int 3))
|
||||
(= (¤ table 0) 10)
|
||||
(= (¤ table 1) 20)
|
||||
(var first _ (¤ table 0))
|
||||
(var second _ (¤ table 1))
|
||||
(printf "arrays: %d\n" (+ first second))
|
||||
|
||||
;; a for opens a scope, and its initializer is declared inside it
|
||||
(printf "loop:")
|
||||
(for (var i _ 0) (< i 3) (++ i)
|
||||
(printf " %d" i))
|
||||
(printf "\n")
|
||||
|
||||
;; a block's declarations end with it
|
||||
(do (var n _ 9)
|
||||
(printf "shadowed: %d then " n))
|
||||
(printf "%d\n" n)
|
||||
|
||||
;; a wildcard inside a written type: only it is solved
|
||||
(var pp2 (* _) (& p))
|
||||
(printf "partial: %d %g\n" (-> pp2 x) (-> pp2 y))
|
||||
|
||||
;; C's usual arithmetic conversions, far enough to answer `_'
|
||||
(var d double 1.5)
|
||||
(var l long 7)
|
||||
(var g float 0.5)
|
||||
(var mixed _ (+ n d))
|
||||
(var same _ (+ n n))
|
||||
(var wider _ (+ n l))
|
||||
(var single _ (+ g g))
|
||||
(printf "joined: %g %d %ld %g\n" mixed same wider single)
|
||||
|
||||
;; a brace initializer has no type of its own, but its elements solve
|
||||
;; the hole in the array type around it -- and the length stays as
|
||||
;; written, whether or not every slot is initialized
|
||||
(var squares (¤ _ 4) #(0 1 4 9))
|
||||
(var sparse (¤ _ 8) #(0 1))
|
||||
(printf "from elements: %d %d %d\n" (¤ squares 2) (¤ squares 3) (¤ sparse 7))
|
||||
(return 0))
|
||||
Reference in New Issue
Block a user