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:
@@ -320,6 +320,73 @@ compiles."
|
||||
(test-assert "no captures means no capture record"
|
||||
(not (emits? "(fn f () (closure () int) (return (closure () int () (return 7))))"
|
||||
"_captures {")))
|
||||
;; `(name expr)' names a capture and gives what it holds, so the
|
||||
;; expression is evaluated once, where the closure is written
|
||||
(test-assert "a named capture takes its type from the expression"
|
||||
(emits? "(struct p ((x int) (y int)))
|
||||
(fn f ((s (struct p))) (closure () int)
|
||||
(return (closure () int ((sum (+ (. s x) (. s y)))) (return sum))))"
|
||||
"int sum;"))
|
||||
(test-assert "and the constructor is handed the expression"
|
||||
(emits? "(struct p ((x int) (y int)))
|
||||
(fn f ((s (struct p))) (closure () int)
|
||||
(return (closure () int ((sum (+ (. s x) (. s y)))) (return sum))))"
|
||||
"_make(s.x + s.y)"))
|
||||
(test-assert "capturing a pointer is how by-reference is spelled"
|
||||
(emits? "(struct p ((x int)))
|
||||
(fn f ((s (* (struct p)))) (closure () int)
|
||||
(return (closure () int ((q s)) (return (-> q x)))))"
|
||||
"struct p* q;"))
|
||||
;; A bare function is a closure that captures nothing, so it
|
||||
;; converts wherever one is expected -- the pointer goes in the
|
||||
;; environment and one thunk per signature reads it back out
|
||||
(test-assert "a named function in a var initializer"
|
||||
(emits? "(fn g ((n int)) int (return n))
|
||||
(fn f () void (var c (closure ((int)) int) g))"
|
||||
"ƛint_int_fromfn(g)"))
|
||||
(test-assert "a lambda, which is a bare function too"
|
||||
(emits? "(fn f () void (var c (closure ((int)) int)
|
||||
(lambda ((n int)) int (return n))))"
|
||||
"ƛint_int_fromfn(λ0_f)"))
|
||||
(test-assert "an argument, against the parameter that signature wrote"
|
||||
(emits? "(fn g ((n int)) int (return n))
|
||||
(fn h ((c (closure ((int)) int))) int (return (c 1)))
|
||||
(fn f () int (return (h g)))"
|
||||
"h(ƛint_int_fromfn(g))"))
|
||||
(test-assert "a return, against the declared return type"
|
||||
(emits? "(fn g ((n int)) int (return n))
|
||||
(fn f () (closure ((int)) int) (return g))"
|
||||
"return ƛint_int_fromfn(g)"))
|
||||
(test-assert "the thunk reads the pointer out of the environment"
|
||||
(emits? "(fn g ((n float)) int (return 1))
|
||||
(fn f () void (var c (closure ((float)) int) g))"
|
||||
"return ƛcaptures->f(ƛa0)"))
|
||||
;; a signature that does not match is left alone, and C rejects it
|
||||
(test-assert "a function of the wrong signature does not convert"
|
||||
(not (emits? "(fn g ((n float)) int (return 1))
|
||||
(fn f () void (var c (closure ((int)) int) g))"
|
||||
"_fromfn(g)")))
|
||||
;; a closure is lifted into the function it was written in, so
|
||||
;; there has to be one
|
||||
(test-assert "a closure at toplevel is refused"
|
||||
(reports? "(var c (closure () int) (closure () int () (return 1)))"
|
||||
"only be written inside a function"))
|
||||
(test-assert "and so is one in a struct field"
|
||||
(reports? "(struct s ((f (closure () int) (closure () int () (return 1)))))"
|
||||
"only be written inside a function"))
|
||||
;; A block opens a scope, so what it declares ends with it
|
||||
(test-assert "a name shadowed in a block does not escape it"
|
||||
(emits? "(fn mk () (closure () int) (return (closure () int () (return 1))))
|
||||
(fn f () int (var c (closure () int) (mk))
|
||||
(do (var c int 9) (g c))
|
||||
(return (c)))"
|
||||
"ƛvoid_int_call(c)"))
|
||||
(test-assert "and the shadowing declaration is what the block sees"
|
||||
(emits? "(fn mk () (closure () int) (return (closure () int () (return 1))))
|
||||
(fn f () int (var c (closure () int) (mk))
|
||||
(do (var c int 9) (g c))
|
||||
(return (c)))"
|
||||
"g(c)"))
|
||||
;; The receiver is written twice, so a name used as an argument
|
||||
;; must not be mistaken for a call of its own
|
||||
(test-assert "a closure passed as an argument stays a value"
|
||||
@@ -327,6 +394,136 @@ compiles."
|
||||
(fn f ((c (closure ((int)) int))) int (return (g c)))"
|
||||
"g(c)")))
|
||||
|
||||
;; a macro body reads types as lists, so the srfi-1 accessors are in
|
||||
;; scope beside the type database
|
||||
(test-group "macro list accessors"
|
||||
(test-assert "third reads an array's length"
|
||||
(emits? "(defmacro (len t) (third t))
|
||||
(fn f () int (return (len (¤ int 7))))"
|
||||
"return 7;"))
|
||||
(test-assert "second reads a tag"
|
||||
(emits? "(defmacro (tag t) (symbol->string (second t)))
|
||||
(fn f () void (g (tag (struct point))))"
|
||||
"g(\"point\")")))
|
||||
|
||||
;; `type-of' hands a macro the type of an *expression*, where
|
||||
;; `get-name-type' only answers for a name. The macro is expanded
|
||||
;; during the walk rather than before it, so the scope is still live.
|
||||
(test-group "type-of"
|
||||
(test-assert "a local, from its declaration"
|
||||
(emits? "(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
|
||||
(fn f () int (var n int 0) (return (t n)))"
|
||||
"return 1;"))
|
||||
(test-assert "an expression, not just a name"
|
||||
(emits? "(defmacro (t x) (type-match (type-of x) (double 1) (else 0)))
|
||||
(fn f () int (var d double 0.0) (return (t (+ d 1))))"
|
||||
"return 1;"))
|
||||
(test-assert "a call, through the callee's signature"
|
||||
(emits? "(fn g () float (return 1.0))
|
||||
(defmacro (t x) (type-match (type-of x) (float 1) (else 0)))
|
||||
(fn f () int (return (t (g))))"
|
||||
"return 1;"))
|
||||
;; a macro is shown the written spelling, not the generated struct
|
||||
(test-assert "a closure, spelled the way it was written"
|
||||
(emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(defmacro (t x) (type-match (type-of x) ((closure _ _) 1) (else 0)))
|
||||
(fn f () int (var c _ (mk)) (return (t c)))"
|
||||
"return 1;"))
|
||||
(test-assert "and calling one has the closure's return type"
|
||||
(emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
|
||||
(fn f () int (var c _ (mk)) (return (t (c 1))))"
|
||||
"return 1;"))
|
||||
;; outside an expansion there is no scope to ask about
|
||||
(test-assert "a name the walk has not reached is unknown"
|
||||
(emits? "(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
|
||||
(fn f () int (return (t nope)))"
|
||||
"return 0;")))
|
||||
|
||||
;; `_' as a type is written out from what the initializer says. The
|
||||
;; answer comes from declarations and from the signature a call names,
|
||||
;; never from unification -- a partial type would need one.
|
||||
(test-group "wildcard types"
|
||||
(test-assert "an integer literal"
|
||||
(emits? (in-fn "(var x _ 42)") "int x = 42"))
|
||||
(test-assert "a float literal"
|
||||
(emits? (in-fn "(var x _ 3.5)") "double x = 3.5"))
|
||||
(test-assert "a string literal"
|
||||
(emits? (in-fn "(var x _ \"hi\")") "const char * x"))
|
||||
(test-assert "a call, through the name table"
|
||||
(emits? "(fn g ((a int)) float (return 1.0))
|
||||
(fn f () void (var x _ (g 1)))"
|
||||
"float x = g(1)"))
|
||||
(test-assert "a struct member"
|
||||
(emits? "(struct p ((a int) (b float)))
|
||||
(fn f ((s (struct p))) void (var x _ (. s b)))"
|
||||
"float x = s.b"))
|
||||
(test-assert "an address, which composes"
|
||||
(emits? "(struct p ((a int)))
|
||||
(fn f ((s (struct p))) void (var x _ (& s)))"
|
||||
"struct p* x = &s"))
|
||||
(test-assert "a comparison is a bool"
|
||||
(emits? (in-fn "(var x _ (< a b))") "bool x = a < b"))
|
||||
;; a wildcard inside a spelling is solved in place, leaving the rest
|
||||
;; of the written type alone -- this is what needs the unifier
|
||||
(test-assert "a wildcard inside a pointer"
|
||||
(emits? "(struct p ((a int)))
|
||||
(fn f ((s (struct p))) void (var x (* _) (& s)))"
|
||||
"struct p* x = &s"))
|
||||
(test-assert "a wildcard inside an array"
|
||||
(emits? (in-fn "(var t (¤ _ 3) #((¤ int 3) : 1 2 3))") "int t[3]"))
|
||||
;; a compound literal carries its own type, where a brace
|
||||
;; initializer has none and takes one from its context
|
||||
(test-assert "a compound literal answers a bare wildcard"
|
||||
(emits? "(struct p ((a int) (b int)))
|
||||
(fn f () void (var x _ #((struct p) : 1 2)))"
|
||||
"struct p x = (struct p){1, 2}"))
|
||||
(test-assert "a brace initializer cannot"
|
||||
(reports? (in-fn "(var x _ #(1 2))") "cannot infer the type"))
|
||||
;; ...but its elements still solve the hole in an array type
|
||||
(test-assert "elements solve an array's element type"
|
||||
(emits? (in-fn "(var t (¤ _ 4) #(0 1 4 9))") "int t[4] = {0, 1, 4, 9}"))
|
||||
(test-assert "including when there are fewer than the length"
|
||||
(emits? (in-fn "(var t (¤ _ 10) #(1 2))") "int t[10] = {1, 2}"))
|
||||
(test-assert "and they have to agree with each other"
|
||||
(reports? (in-fn "(var t (¤ _ 2) #(1 \"s\"))") "type mismatch"))
|
||||
;; a closure type reaches the solver as the struct that stands for
|
||||
;; it, which is the spelling `parse-type' knows
|
||||
(test-assert "a closure, from the signature that produced it"
|
||||
(emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(fn f () void (var c _ (mk)))"
|
||||
"struct ƛint_int c = mk()"))
|
||||
(test-assert "and it is callable once inferred"
|
||||
(emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(fn f () int (var c _ (mk)) (return (c 1)))"
|
||||
"ƛint_int_call(c, 1)"))
|
||||
;; C's usual arithmetic conversions, far enough to answer `_'
|
||||
(test-assert "floating beats integral"
|
||||
(emits? (in-fn "(var d double 1.0) (var x _ (+ a d))") "double x = a + d"))
|
||||
(test-assert "the wider integer wins"
|
||||
(emits? (in-fn "(var l long 1) (var x _ (+ a l))") "long x = a + l"))
|
||||
(test-assert "double beats float"
|
||||
(emits? (in-fn "(var g float 1.0) (var d double 1.0) (var x _ (+ g d))")
|
||||
"double x = g + d"))
|
||||
(test-assert "and same-width operands stay put"
|
||||
(emits? (in-fn "(var g float 1.0) (var x _ (+ g g))") "float x = g + g"))
|
||||
(test-assert "a pointer operand makes it pointer arithmetic"
|
||||
(emits? "(struct p ((a int)))
|
||||
(fn f ((s (struct p))) void (var x _ (+ (& s) 1)))"
|
||||
"struct p* x = &s + 1"))
|
||||
;; a written type that cannot match what the initializer gives
|
||||
(test-assert "a mismatch is reported, not papered over"
|
||||
(reports? (in-fn "(var p (* _) 42)") "type mismatch"))
|
||||
;; a wildcard that cannot be answered is an error, not a guess
|
||||
(test-assert "with no initializer there is nothing to infer from"
|
||||
(reports? (in-fn "(var x _)") "cannot infer the type"))
|
||||
(test-assert "nor from a name the compiler never saw declared"
|
||||
(reports? (in-fn "(var x _ (never-declared))") "cannot infer the type")))
|
||||
|
||||
;; `(car res)' on the expansion assumed it was a pair, so a macro
|
||||
;; computing a value rather than building a form crashed the compiler.
|
||||
(test-group "macro expanding to an atom"
|
||||
@@ -356,7 +553,23 @@ compiles."
|
||||
"b (void)"))
|
||||
(test-assert "and ($) is nothing at all"
|
||||
(emits? "(defmacro (quiet) (list '$)) (quiet) (fn f () int (return 1))"
|
||||
"return 1;")))
|
||||
"return 1;"))
|
||||
;; without `$' a list is one form, so a head that is itself a form
|
||||
;; stays a call rather than becoming two statements
|
||||
(test-assert "a computed callee stays one form"
|
||||
(emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(defmacro (apply-it x) `((mk) ,x))
|
||||
(fn f () int (return (apply-it 5)))"
|
||||
"ƛint_int_call(mk(), 5)"))
|
||||
;; spliced, it would have become two forms in the `return' -- the
|
||||
;; comma operator, and the wrong answer
|
||||
(test-assert "rather than two forms in its context"
|
||||
(not (emits? "(fn mk () (closure ((int)) int)
|
||||
(return (closure ((b int)) int () (return b))))
|
||||
(defmacro (apply-it x) `((mk) ,x))
|
||||
(fn f () int (return (apply-it 5)))"
|
||||
"return mk(), 5"))))
|
||||
|
||||
;; A unary expression parenthesised its operand rather than itself, so
|
||||
;; the parens landed inside: `*(p).x', which C reads as `*(p.x)'.
|
||||
|
||||
Reference in New Issue
Block a user