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:
2026-09-29 23:52:08 +03:00
parent 53f92727a5
commit 1f445f0f9b
15 changed files with 1126 additions and 100 deletions

View File

@@ -21,6 +21,12 @@
(define +type-db+ (make-hash-table)) ; typedefs and defines
(define +tag-db+ (make-hash-table)) ; struct, union and enums
;;; What type a *name* has, which neither of the two above records:
;;;
;;; (fn sum ((a int) (b int)) int ...) -> (fn ((int) (int)) int)
;;; (var origin (struct point) ...) -> (struct point)
(define +name-db+ (make-hash-table))
(define (strip-pub form)
(if (eq? (car form) 'pub) (cdr form) form))
@@ -74,6 +80,34 @@
(hash-table-set! +type-db+ name
(list 'define name (cddr (strip-pub form)))))
;;; `(type-of x)' inside a macro body: the type of the expression the
;;; macro was handed, where `get-name-type' only answers for a name.
;;; The walker that can answer it lives in `semen', which is compiled
;;; after this, so it installs itself here for the length of one
;;; expansion. Outside one there is no scope to ask about, and the
;;; answer is #f.
(define current-type-of (make-parameter (lambda (form) #f)))
(define (type-of form) ((current-type-of) form))
(define (add-name-type! name type)
(hash-table-set! +name-db+ name type))
;;; #f for a name never declared, which is what `printf' looks like
;;; until something parses stdio.h. Not an error here; the caller
;;; decides.
(define (get-name-type name)
(hash-table-ref/default +name-db+ name #f))
;;; What `(make-adder 10)' has for a type: `make-adder's return type,
;;; or #f when NAME is not a function with a signature on record
(define (get-return-type name)
(let ((type (get-name-type name)))
(and (pair? type)
(eq? 'fn (car type))
(= 3 (length type))
(third type))))
(define (get-tag-info name)
(hash-table-ref/default +tag-db+ name #f))
@@ -142,21 +176,47 @@
;;; (int ...)
;;; ((* const char) ...)
;;; ([int 10] ...)
;;; ((* _) ...) ; a pointer to anything
;;; ((¤ _ _) ...) ; an array of anything, any length
;;; (else ...))
;;;
;;; A type is a form, not an atom, so this compares with equal? rather
;;; than dispatching like `case'. Patterns are literal types and are not
;;; evaluated; `else' is optional and the whole thing is #f when nothing
;;; matches and there is no else.
;;; A type is a form, not an atom, so patterns are matched structurally
;;; rather than dispatched on like `case'. They are literal types and
;;; are not evaluated; `else' is optional and the whole thing is #f when
;;; nothing matches and there is no else.
;;;
;;; `_' in a pattern matches anything in that position, the same thing
;;; it means in a type. Without it every spelling has to be enumerated:
;;; `(closure ((int)) int)' and `(closure ((float)) int)' are separate
;;; clauses for what is one case.
;;;
;;; A `_' written last takes everything that remains, because a type's
;;; words are spread rather than nested -- `(* const char)' is three
;;; elements, so `(* _)' has to cover two of them to mean "a pointer to
;;; anything".
;;;
;;; Nothing destructures: a macro body is ordinary Scheme and a type is
;;; a list, so `(caddr (type-of x))' already reads the length out of
;;; `(¤ int 4)'.
(define-syntax type-match
(syntax-rules (else)
((_ type) #f)
((_ type (else body ...)) (begin body ...))
((_ type (pattern body ...) clause ...)
(if (equal? type 'pattern)
(if (type-pattern-matches? 'pattern type)
(begin body ...)
(type-match type clause ...)))))
(define (type-pattern-matches? pattern type)
(cond
((eq? pattern '_) #t)
((and (pair? pattern) (pair? type))
(if (and (eq? (car pattern) '_) (null? (cdr pattern)))
#t ; a trailing `_' takes the rest
(and (type-pattern-matches? (car pattern) (car type))
(type-pattern-matches? (cdr pattern) (cdr type)))))
(else (equal? pattern type))))
;;; Map function to each field/value of a structure/union/enum
;;; For enums, field-type is the type of the enum (since C 23)
;;; (map-fields type-name