Files
sex/infer.scm
alex-eg f5eee71eb7 declare the closure environment in c99
`max_align_t' is C11 and the union goes into every unit, so a program
with no closure in it stopped building under -std=c99. `unify' also
bound a rigid variable one way round only.
2026-09-30 23:51:55 +03:00

645 lines
26 KiB
Scheme

;;; Type inference, layer 0: the type representation and unification.
;;;
;;; Nothing in the compiler calls this unit yet. It is the ground floor
;;; of the pass described in Type-inference.org -- built and tested on
;;; its own before a single form is routed through it.
;;;
;;; Two representations meet here. *Surface* types are the forms the
;;; rest of the compiler passes around -- `int', `(* const char)',
;;; `(¤ int 16)', `(fn ((int)) int)'. They are what the reader
;;; produces, what the C writer consumes and what `type-match' compares
;;; with `equal?', and they are hopeless for unification. The *IR*
;;; below is the other one: mutable cells, so that solving a type
;;; variable is a side effect rather than a substitution rebuilt at
;;; every step.
;;;
;;; `parse-type' and `unparse-type' are the boundary between the two,
;;; and they carry the whole compatibility burden: `unparse-type' must
;;; produce the exact spelling `type-match' compares against, or the
;;; reflection macros break by silently falling into their `else'
;;; branch. That is what the round-trip test in tests/infer.scm is for,
;;; and why it is driven by every type spelling that appears in the
;;; repository.
(import
scheme
(scheme base)
(chicken base)
matchable
srfi-1
srfi-69
types
utils)
;;; ---------------------------------------------------------------
;;; The IR
;;; ---------------------------------------------------------------
;;; A type variable is a mutable cell. `ref' is #f while unsolved and
;;; the type it stands for once bound -- union-find, with the path
;;; compression done in `resolve'.
;;;
;;; `classes' is the list of type classes the variable must satisfy
;;; (`numeric', and one day `ord'); see "constraints" below. `rigid?'
;;; marks a variable that must not unify with anything but itself --
;;; unused until a `fn' grows type parameters, and five lines now
;;; against an IR change later.
(define-record-type <tvar>
(%make-tvar id ref classes rigid?)
tvar?
(id tvar-id)
(ref tvar-ref tvar-ref-set!)
(classes tvar-classes tvar-classes-set!)
(rigid? tvar-rigid?))
;;; A primitive or otherwise nominal type. `name' is the list of words
;;; making it up, so `int', `(unsigned int)' and `(long long)' are all
;;; one node, and so is a name we have never parsed a declaration for
;;; (`size-t', `GLuint'). The two cases are told apart by
;;; `c-primitive?', which is what keeps a constraint over an unparsed C
;;; typedef from being an error.
(define-record-type <prim>
(make-prim name quals)
prim-type?
(name prim-name)
(quals prim-quals))
(define-record-type <ptr>
(make-ptr target quals)
ptr-type?
(target ptr-target)
(quals ptr-quals))
;;; `size' is an integer, or #f for `(¤ int)' -- an array of unwritten
;;; length.
(define-record-type <array>
(make-array-type elt size)
array-type?
(elt array-elt)
(size array-size))
(define-record-type <fn>
(make-fn-type ret args variadic?)
fn-type?
(ret fn-ret)
(args fn-args)
(variadic? fn-variadic?))
;;; struct / union / enum. Nominal: two of them are the same type when
;;; they are the same kind and the same name. `spelling' is the surface
;;; form it was written as, kept verbatim so that an aggregate defined
;;; inline in a type position round-trips unchanged.
(define-record-type <agg>
(make-agg kind name spelling quals)
agg-type?
(kind agg-kind)
(name agg-name)
(spelling agg-spelling)
(quals agg-quals))
;;; A typedef. Transparent to unification -- it unifies as whatever it
;;; expands to -- and opaque to printing, so a diagnostic and a
;;; generated declaration both say `size-t' rather than `unsigned long'.
(define-record-type <alias>
(make-alias name expansion quals)
alias-type?
(name alias-name)
(expansion alias-expansion)
(quals alias-quals))
;;; `?'. Sex has full C interop, so `printf', `SDL-CreateWindow' and
;;; `size-t' arrive from headers nobody parsed. Rather than reject
;;; every real program, the lattice gets a top element: `?' is
;;; consistent with every type and constrains nothing.
(define-record-type <unknown>
(%make-unknown)
unknown-type?)
(define the-unknown-type (%make-unknown))
(define tvar-counter 0)
(define (fresh-tvar . classes)
(set! tvar-counter (+ tvar-counter 1))
(%make-tvar tvar-counter #f (if (null? classes) (list) (car classes)) #f))
(define (fresh-rigid-tvar . classes)
(set! tvar-counter (+ tvar-counter 1))
(%make-tvar tvar-counter #f (if (null? classes) (list) (car classes)) #t))
;;; Follow a bound variable to what it stands for, compressing the path
;;; on the way out. Every procedure that looks at a type's shape starts
;;; here.
(define (resolve type)
(if (and (tvar? type) (tvar-ref type))
(let ((target (resolve (tvar-ref type))))
(tvar-ref-set! type target)
target)
type))
;;; ...and through any typedef as well, for the places that care what a
;;; type *is* rather than what it is called.
(define (underlying type)
(let ((t (resolve type)))
(if (alias-type? t)
(underlying (alias-expansion t))
t)))
(define (type-quals type)
(cond ((prim-type? type) (prim-quals type))
((ptr-type? type) (ptr-quals type))
((agg-type? type) (agg-quals type))
((alias-type? type) (alias-quals type))
(else (list))))
;;; Array-to-pointer and function-to-function-pointer, for the
;;; positions where C decays: a call argument, an operand of `+', the
;;; subscripted half of `(¤ a i)'.
(define (decay type)
(let ((t (underlying type)))
(cond ((array-type? t) (make-ptr (array-elt t) (list)))
((fn-type? t) (make-ptr t (list)))
(else (resolve type)))))
(define (free-tvars type)
(let collect ((t type) (acc (list)))
(let ((t (resolve t)))
(cond ((tvar? t) (if (memq t acc) acc (cons t acc)))
((ptr-type? t) (collect (ptr-target t) acc))
((array-type? t) (collect (array-elt t) acc))
((alias-type? t) (collect (alias-expansion t) acc))
((fn-type? t) (fold collect (collect (fn-ret t) acc) (fn-args t)))
(else acc)))))
;;; ---------------------------------------------------------------
;;; Surface -> IR
;;; ---------------------------------------------------------------
(define +qualifiers+ '(const volatile restrict))
(define (qualifier? word) (memq word +qualifiers+))
;;; `(const char)' written as `((const char))' is the same type: a
;;; sublist that merely groups. The C writer unwraps these too.
(define (maybe-unwrap type)
(if (and (list? type) (= 1 (length type)))
(car type)
type))
(define (parse-type surface)
(cond
((symbol? surface) (parse-words (list surface) (list) surface))
((not (pair? surface)) (sex-error surface "not a type" surface))
((eq? (car surface) '¤) (parse-array surface))
((eq? (car surface) 'fn) (parse-fn surface))
((memq '* surface) (parse-pointer-chain surface))
(else (parse-words surface (list) surface))))
;;; A `*'-free run of words: qualifiers, then whatever they qualify.
;;; `form' is only carried along so a complaint can say where it was
;;; written.
(define (parse-words words quals form)
(cond
((null? words) (sex-error form "type is nothing but qualifiers" form))
((qualifier? (car words))
(parse-words (cdr words) (cons (car words) quals) form))
;; A single sublist left: grouping parens, as in (* (const struct s))
((and (null? (cdr words)) (pair? (car words)))
(with-quals (parse-type (car words)) (reverse quals)))
((memq (car words) '(struct union enum)) (parse-agg words (reverse quals)))
((eq? (car words) '¤) (parse-array words))
((eq? (car words) 'fn) (parse-fn words))
((memq '* words) (parse-pointer-chain (append (reverse quals) words)))
(else (parse-name words (reverse quals) form))))
;;; A name, one word or several: `int', `size-t', `(unsigned int)'.
(define (parse-name words quals form)
(cond
((not (every symbol? words)) (sex-error form "malformed type" form))
;; The type-level wildcard. It is a fresh variable wherever it
;; appears, which is what makes partial types -- `(* _)', `(¤ _ 4)'
;; -- fall out for free rather than needing their own grammar.
((equal? words '(_)) (fresh-tvar))
((and (null? (cdr words)) (get-underlying-type (car words)))
=> (lambda (target)
(make-alias (car words) (parse-type target) quals)))
(else (make-prim words quals))))
;;; ([pub] struct name), (struct name (fields ...)), (struct (fields ...))
(define (parse-agg words quals)
(let* ((kind (car words))
(name (and (pair? (cdr words)) (symbol? (cadr words)) (cadr words))))
(make-agg kind name words quals)))
;;; (¤ elt ... size) -- the size is the last element when it is an
;;; integer, and absent otherwise. The element words are unwrapped the
;;; way the C writer unwraps them, so `[int 16]' and `[(int) 16]' are
;;; one type.
(define (parse-array surface)
(let* ((rest (cdr surface))
(sized? (and (pair? rest) (integer? (last rest))))
(size (and sized? (last rest)))
(words (if sized? (drop-right rest 1) rest)))
(when (null? words)
(sex-error surface "array type without an element type" surface))
(make-array-type (parse-type (maybe-unwrap words)) size)))
;;; (fn ((int) (float)) void). Argument entries are types, not named
;;; parameters -- a `fn' in type position has no room for names.
(define (parse-fn surface)
(match surface
(('fn (? list? arglist) ret)
(let* ((variadic? (and (pair? arglist) (variadic-marker? (last arglist))))
(entries (if variadic? (drop-right arglist 1) arglist)))
(make-fn-type (parse-type ret)
(map (lambda (entry) (parse-type (maybe-unwrap entry)))
entries)
variadic?)))
(else (sex-error surface "malformed function type" surface))))
;;; `...' in an arglist, written bare or wrapped the way every other
;;; entry is.
(define (variadic-marker? entry)
(or (eq? entry '...) (equal? entry '(...))))
;;; Pointer chains are written flat and read right to left: the last
;;; `*'-separated run is the pointed-to type, and each run before it
;;; qualifies one level of indirection. `(const * const char)' is a
;;; const pointer to a const char.
(define (parse-pointer-chain words)
(let* ((segments (list-split words '*))
(base (last segments))
(levels (reverse (drop-right segments 1))))
(when (null? base)
(sex-error words "pointer to nothing" words))
(fold (lambda (level acc)
(unless (every qualifier? level)
(sex-error words "only qualifiers may sit between two `*'" words))
(make-ptr acc level))
(parse-words (maybe-unwrap-segment base) (list) words)
levels)))
(define (maybe-unwrap-segment segment)
(let ((s (maybe-unwrap segment)))
(if (list? s) s (list s))))
;;; Re-qualify a parsed type, for the grouping case `(const (struct s))'
;;; where the qualifier is read before the thing it qualifies.
(define (with-quals type quals)
(if (null? quals)
type
(cond ((prim-type? type) (make-prim (prim-name type)
(append quals (prim-quals type))))
((ptr-type? type) (make-ptr (ptr-target type)
(append quals (ptr-quals type))))
((agg-type? type) (make-agg (agg-kind type) (agg-name type)
(agg-spelling type)
(append quals (agg-quals type))))
((alias-type? type) (make-alias (alias-name type)
(alias-expansion type)
(append quals (alias-quals type))))
(else type))))
;;; ---------------------------------------------------------------
;;; IR -> surface
;;; ---------------------------------------------------------------
;;; Every result here has to be the spelling the rest of the compiler
;;; already writes by hand, since `type-match' compares with `equal?'
;;; and a near miss is silent.
(define (unparse-type type)
(let ((t (resolve type)))
(cond
((tvar? t) '_)
((unknown-type? t) '?)
((alias-type? t) (qualify (alias-quals t) (list (alias-name t))))
((prim-type? t) (qualify (prim-quals t) (prim-name t)))
((agg-type? t) (qualify (agg-quals t) (agg-spelling t)))
((ptr-type? t)
(append (ptr-quals t) (list '*) (as-words (unparse-type (ptr-target t)))))
((array-type? t)
(let ((elt (as-words (unparse-type (array-elt t)))))
(append (list '¤)
(if (and (pair? elt) (eq? (car elt) '¤)) (list elt) elt)
(if (array-size t) (list (array-size t)) (list)))))
((fn-type? t)
(list 'fn
(append (map (lambda (arg) (as-arg (unparse-type arg))) (fn-args t))
(if (fn-variadic? t) (list '(...)) (list)))
(unparse-type (fn-ret t))))
(else (error "unparse-type: not a type" t)))))
;;; A one-word type is written bare, anything longer as a list --
;;; `int', but `(const int)' and `(struct point)'.
(define (qualify quals words)
(let ((all (append quals words)))
(if (and (null? quals) (= 1 (length all)))
(car all)
all)))
;;; An argument in a `fn' type is written as a list even when it is one
;;; word -- `((int) (float))' -- so only an atom needs wrapping.
(define (as-arg surface)
(if (pair? surface) surface (list surface)))
;;; Splice a type into a surrounding word list, the way `(* const char)'
;;; and `[* const char]' splice theirs. An array keeps its parentheses:
;;; `(¤ ¤ char 4)' would read back as something else entirely.
(define (as-words surface)
(cond ((not (pair? surface)) (list surface))
((memq (car surface) '(¤ fn)) (list surface))
(else surface)))
;;; ---------------------------------------------------------------
;;; Constraints
;;; ---------------------------------------------------------------
;;; `(numeric a)' is already a type class, so it is written as one from
;;; the start: one representation, one table, one entailment check. A
;;; trait bound `(ord (struct circle))' is the same shape, discharged
;;; the same way, and reported by the same procedure -- which is the
;;; whole reason to build it this way while there is only one kind of
;;; constraint to build.
;;;
;;; `default' is the type an unresolved constraint falls back to, the
;;; way Haskell defaults `Num a' to Integer. `test' is how the built-in
;;; classes say "every arithmetic type" without enumerating twenty
;;; spellings as instances; a user trait has no test and lives entirely
;;; in the instance table. `strict?' marks a class that must not be
;;; guessed at: static dispatch needs a real instance, so `?' fails it.
(define-record-type <type-class>
(%make-type-class name default test strict?)
type-class?
(name type-class-name)
(default type-class-default)
(test type-class-test)
(strict? type-class-strict?))
(define +classes+ (make-hash-table))
(define +instances+ (make-hash-table))
(define (register-class! name default test strict?)
(hash-table-set! +classes+ name (%make-type-class name default test strict?)))
(define (get-class name)
(or (hash-table-ref/default +classes+ name #f)
(error "no such type class" name)))
;;; Instances key on the *resolved* type, so `(impl show for size-t)'
;;; and `(impl show for unsigned long)' collide rather than quietly
;;; coexisting as two instances of one C type.
(define (instance-key type)
(unparse-type (underlying type)))
(define (add-instance! class-name type)
(hash-table-set! +instances+ (cons class-name (instance-key type)) #t))
(define (has-instance? class-name type)
(hash-table-exists? +instances+ (cons class-name (instance-key type))))
;;; #t, #f, or 'unknown -- and the third answer is the important one.
;;; A C name we never parsed a declaration for might well be numeric;
;;; saying #f there would reject working programs, and saying #t would
;;; invent knowledge. 'unknown means "do not constrain, do not
;;; complain".
(define (entails? class-name type)
(let ((cls (get-class class-name))
(t (underlying type)))
(cond
((tvar? t) 'unknown)
;; `?' is consistent with every type, but it entails nothing:
;; there is no instance to select and no name to mangle.
((unknown-type? t) (if (type-class-strict? cls) #f 'unknown))
((has-instance? class-name t) #t)
((type-class-test cls) => (lambda (test) (test t)))
(else #f))))
(define +integer-words+ '(char short int long signed unsigned bool _Bool))
(define +float-words+ '(float double))
(define +known-words+ (append '(void) +integer-words+ +float-words+))
;;; A prim built only out of words we recognise. Anything else is a
;;; name from a header, and we have no opinion about it.
(define (c-primitive? t)
(and (prim-type? t)
(every (lambda (word) (memq word +known-words+)) (prim-name t))))
(define (void-type? t)
(and (prim-type? t) (equal? (prim-name t) '(void))))
(define (arithmetic-type? t)
(cond ((and (agg-type? t) (eq? (agg-kind t) 'enum)) #t) ; an enum is an integer
((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
((void-type? t) #f)
(else #t)))
(define (integral-type? t)
(cond ((and (agg-type? t) (eq? (agg-kind t) 'enum)) #t)
((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
((void-type? t) #f)
((any (lambda (word) (memq word +float-words+)) (prim-name t)) #f)
(else #t)))
(define (floating-type? t)
(cond ((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
(else (and (any (lambda (word) (memq word +float-words+)) (prim-name t))
#t))))
(define (scalar-type? t)
(cond ((ptr-type? t) #t)
((array-type? t) #t) ; decays to one
((fn-type? t) #t) ; likewise
(else (arithmetic-type? t))))
;;; The built-ins. They are ordinary classes, registered the same way a
;;; trait will be -- that is the point.
(register-class! 'numeric 'int arithmetic-type? #f)
(register-class! 'integral 'int integral-type? #f)
(register-class! 'floating 'double floating-type? #f)
(register-class! 'scalar #f scalar-type? #f)
;;; A constraint that survives to the end of a function is defaulted:
;;; `(numeric a)' with nothing else known is an `int'. A *strict*
;;; class has no default and no business guessing, so an unresolved one
;;; is an error -- the rule is worth stating while there is only one
;;; kind of constraint to state it about.
(define (default-tvar! v form)
(let ((strict (find (lambda (c) (type-class-strict? (get-class c)))
(tvar-classes v))))
(cond
(strict (sex-error form "unresolved constraint" (list strict (unparse-type v))))
((find (lambda (c) (type-class-default (get-class c))) (tvar-classes v))
=> (lambda (c)
(tvar-ref-set! v (parse-type (type-class-default (get-class c))))
#t))
(else #f))))
;;; Default every variable still open in TYPE. Returns #t when none is
;;; left unsolved, so a caller can tell "inferred" from "give up and
;;; ask for the type in writing".
(define (default-type-variables! type form)
(fold (lambda (v ok) (and (default-tvar! v form) ok))
#t
(free-tvars type)))
(define (check-classes classes type form)
(for-each
(lambda (c)
(when (eq? #f (entails? c type))
(sex-error form "type does not satisfy a constraint"
(list c (unparse-type type)))))
classes))
;;; ---------------------------------------------------------------
;;; Unification
;;; ---------------------------------------------------------------
;;; Consistency in the gradual-typing sense rather than equality: `?'
;;; succeeds against anything and binds nothing, which is what keeps
;;; the pass from rejecting every program that includes a C header.
;;;
;;; FORM is carried only so a failure can say where it was written.
(define (unify t1 t2 form)
(let ((a (resolve t1))
(b (resolve t2)))
(cond
((eq? a b) #t)
((unknown-type? a) #t)
((unknown-type? b) #t)
;; Whichever side is free takes the binding, so that a rigid
;; variable is solved *to* rather than solved, in either order.
;; Both rigid and distinct is the mismatch `eq?' above let through.
((and (tvar? a) (not (tvar-rigid? a))) (bind-tvar! a b form))
((and (tvar? b) (not (tvar-rigid? b))) (bind-tvar! b a form))
((or (tvar? a) (tvar? b)) (type-mismatch a b form))
;; A typedef unifies as what it stands for. Its name survives in
;; whichever side is printed later, since neither side is rebuilt.
((alias-type? a) (unify (alias-expansion a) b form))
((alias-type? b) (unify a (alias-expansion b) form))
((and (prim-type? a) (prim-type? b))
(check-quals a b form)
(or (equal? (prim-name a) (prim-name b))
(type-mismatch a b form)))
((and (ptr-type? a) (ptr-type? b))
(check-quals a b form)
(unify (ptr-target a) (ptr-target b) form))
((and (array-type? a) (array-type? b))
;; One of them may be `(¤ int)': an unwritten length constrains
;; nothing, the way it does not in C either.
(when (and (array-size a) (array-size b)
(not (= (array-size a) (array-size b))))
(type-mismatch a b form))
(unify (array-elt a) (array-elt b) form))
((and (fn-type? a) (fn-type? b))
(unless (and (= (length (fn-args a)) (length (fn-args b)))
(eq? (fn-variadic? a) (fn-variadic? b)))
(type-mismatch a b form))
(unify (fn-ret a) (fn-ret b) form)
(for-each (lambda (x y) (unify x y form)) (fn-args a) (fn-args b))
#t)
((and (agg-type? a) (agg-type? b))
(check-quals a b form)
(or (and (eq? (agg-kind a) (agg-kind b))
(if (and (agg-name a) (agg-name b))
(eq? (agg-name a) (agg-name b))
(equal? (agg-spelling a) (agg-spelling b))))
(type-mismatch a b form)))
(else (type-mismatch a b form)))))
(define (type-mismatch a b form)
(sex-error form "type mismatch: expected"
(unparse-type a) 'got (unparse-type b)))
;;; Qualifiers are compared, and a mismatch is a warning rather than a
;;; failure: C's const-correctness is not this pass's fight yet, and
;;; making it one would reject programs that compile today.
(define (check-quals a b form)
(let ((qa (type-quals a))
(qb (type-quals b)))
(unless (lset= eq? qa qb)
(sex-warning form "qualifiers differ between"
(unparse-type a) "and" (unparse-type b)))))
(define (bind-tvar! v t form)
(cond
;; Without recursive types this cannot trigger. It is four lines,
;; and the alternative to having it is a hang.
((occurs? v t) (sex-error form "recursive type" (unparse-type v)))
;; A rigid variable is a type *parameter*: inside a generic body it
;; stands for one specific unknown type and must not be solved.
((tvar-rigid? v) (type-mismatch v t form))
(else
(when (tvar? t)
(tvar-classes-set! t (lset-union eq? (tvar-classes t) (tvar-classes v))))
(tvar-ref-set! v t)
(unless (tvar? t)
(check-classes (tvar-classes v) t form))
#t)))
(define (occurs? v type)
(let ((t (resolve type)))
(cond ((eq? v t) #t)
((ptr-type? t) (occurs? v (ptr-target t)))
((array-type? t) (occurs? v (array-elt t)))
((alias-type? t) (occurs? v (alias-expansion t)))
((fn-type? t) (or (occurs? v (fn-ret t))
(any (lambda (a) (occurs? v a)) (fn-args t))))
(else #f))))
;;; ---------------------------------------------------------------
;;; Type schemes
;;; ---------------------------------------------------------------
;;; Nothing generalizes yet -- every `fn' in Sex carries a written
;;; signature and there is no polymorphism to abstract over. These are
;;; here because they are ten lines on top of unification and because
;;; they are exactly what a `fn' with type parameters needs, and
;;; because a scheme without a constraint list is the wrong shape for
;;; every bounded generic. `(forall vars constraints type)' it is,
;;; from the start.
(define-record-type <scheme>
(make-scheme vars constraints type)
scheme?
(vars scheme-vars)
(constraints scheme-constraints)
(type scheme-type))
;;; Quantify over everything free in TYPE that is not also free in the
;;; environment, carrying each variable's class constraints along as
;;; the scheme's context.
(define (generalize type env-tvars)
(let ((vars (lset-difference eq? (free-tvars type) env-tvars)))
(make-scheme vars
(append-map (lambda (v)
(map (lambda (c) (cons c v)) (tvar-classes v)))
vars)
type)))
(define (instantiate scheme)
(let ((subst (map (lambda (v) (cons v (fresh-tvar (tvar-classes v))))
(scheme-vars scheme))))
(substitute (scheme-type scheme) subst)))
;;; Structural copy with the variables in SUBST replaced. Copying is
;;; how a generic body must be handled anyway -- `form-type' is keyed
;;; by cons cell, one form one type, so an instantiation gets fresh
;;; cells rather than a second type for the same cell.
(define (substitute type subst)
(let ((t (resolve type)))
(cond
((tvar? t) (let ((hit (assq t subst))) (if hit (cdr hit) t)))
((ptr-type? t) (make-ptr (substitute (ptr-target t) subst) (ptr-quals t)))
((array-type? t) (make-array-type (substitute (array-elt t) subst)
(array-size t)))
((alias-type? t) (make-alias (alias-name t)
(substitute (alias-expansion t) subst)
(alias-quals t)))
((fn-type? t) (make-fn-type (substitute (fn-ret t) subst)
(map (lambda (a) (substitute a subst))
(fn-args t))
(fn-variadic? t)))
(else t))))