Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 29s
Phase 3 cont. Adds:
* go-classify-literal-string — heuristic detection of literal kind
from the value-string (parser strips lexer's kind tag; flagged for
follow-up to extend AST shape).
* go-synth-literal — :ty-untyped-int / -float / -imag / -string.
* go-synth-binop — arithmetic, bitwise, comparison, logical ops with
untyped-constant unification:
untyped-int + untyped-float → untyped-float
untyped + typed → typed
comparison ops → bool
logical ops → bool
* go-untyped? + go-type-assignable? — pluggable assignability that
swaps in where structural equality used to gate go-check. Untyped
int assignable to any numeric type; untyped float assignable to
float/complex; untyped string to string.
**Canonical Go pitfall handled correctly**: `var x float64 = 42 / 7`
parses to a binop, synth produces :ty-untyped-int (since BOTH operands
are untyped, the int division stays in the int domain), and check
against float64 returns :ok via assignability. Wrong implementations
that float-coerce eagerly would give 6.0; the right behaviour is
"compute 6 as int, then convert to float64 = 6.0".
Verified by test "binop: 42 / 7 assignable to float64 (canonical
pitfall)" and the type-only test "binop: 42 / 7 — untyped int".
Sister-plan static-types-bidirectional diary updated with the
**pluggable-assignable-predicate** kit-API proposal:
(check-with assignable? CTX EXPR EXPECTED)
Each consumer plugs in its own variance discipline (Go untyped-flow,
TS structural subtyping, Rust lifetime-aware identity) without
rewriting synth or the judgment skeleton.
types 28/28, total 333/333.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
314 lines
11 KiB
Plaintext
314 lines
11 KiB
Plaintext
;; lib/go/types.sx — Go bidirectional type checker.
|
|
;;
|
|
;; Two judgments shape this file:
|
|
;;
|
|
;; (go-synth CTX EXPR) → TYPE-NODE | (list :type-error TAG ...)
|
|
;; Given a context and an expression, produce a type.
|
|
;;
|
|
;; (go-check CTX EXPR EXPECTED) → :ok | (list :type-error TAG ...)
|
|
;; Given a context, expression, and expected type, verify compatibility.
|
|
;;
|
|
;; The two judgments are mutually recursive. Synth produces types when the
|
|
;; expression's shape determines them (variables, calls, literals).
|
|
;; Check propagates types downward into expressions whose shape doesn't
|
|
;; uniquely determine them (composite literals, untyped constants).
|
|
;;
|
|
;; Type representations reuse the parser's :ty-* AST nodes from
|
|
;; lib/go/parse.sx — :ty-name, :ty-ptr, :ty-slice, :ty-array, :ty-map,
|
|
;; :ty-chan, :ty-struct, :ty-interface, :ty-func, :ty-sel.
|
|
;;
|
|
;; Context: an association list of (NAME TYPE) bindings. Per-block scope
|
|
;; via a fresh extension on entry.
|
|
;;
|
|
;; **Independent implementation.** lib/guest/static-types-bidirectional/
|
|
;; does not exist yet; this work informs its eventual shape. Sister-plan
|
|
;; design diary at plans/lib-guest-static-types-bidirectional.md tracks
|
|
;; the chiselling insights as Phase 3 progresses.
|
|
|
|
;; ── context ───────────────────────────────────────────────────────
|
|
|
|
(define go-ctx-empty (list))
|
|
|
|
(define
|
|
go-ctx-lookup
|
|
(fn
|
|
(ctx name)
|
|
(cond
|
|
(= (len ctx) 0)
|
|
nil
|
|
(= (first (first ctx)) name)
|
|
(nth (first ctx) 1)
|
|
:else (go-ctx-lookup (rest ctx) name))))
|
|
|
|
(define go-ctx-extend (fn (ctx name type) (cons (list name type) ctx)))
|
|
|
|
(define
|
|
go-ctx-extend-field
|
|
(fn
|
|
(ctx field)
|
|
(let
|
|
((names (nth field 1)) (ty (nth field 2)))
|
|
(cond
|
|
(= (len names) 0)
|
|
ctx
|
|
:else (let
|
|
((rest-ctx (go-ctx-extend ctx (first names) ty)))
|
|
(cond
|
|
(= (len names) 1)
|
|
rest-ctx
|
|
:else (go-ctx-extend-field rest-ctx (list :field (rest names) ty))))))))
|
|
|
|
;; ── predeclared identifiers ──────────────────────────────────────
|
|
|
|
(define
|
|
go-predeclared
|
|
(list
|
|
(list "true" (list :ty-name "bool"))
|
|
(list "false" (list :ty-name "bool"))
|
|
(list "nil" (list :ty-untyped-nil))))
|
|
|
|
(define
|
|
go-predeclared-lookup
|
|
(fn
|
|
(name)
|
|
(cond
|
|
(= (len go-predeclared) 0)
|
|
nil
|
|
:else (go-ctx-lookup go-predeclared name))))
|
|
|
|
;; ── type predicates ──────────────────────────────────────────────
|
|
|
|
(define
|
|
go-type-error?
|
|
(fn
|
|
(x)
|
|
(and
|
|
(list? x)
|
|
(not (= (len x) 0))
|
|
(= (first x) :type-error))))
|
|
|
|
(define go-type-equal? (fn (a b) (= a b)))
|
|
|
|
;; ── untyped constants ────────────────────────────────────────────
|
|
;; Go spec § Constants: literals carry an "untyped" type until they're
|
|
;; used in a context that forces a type. The canonical pitfall is
|
|
;; `var x float64 = 42 / 7` — both 42 and 7 are *untyped int*, so the
|
|
;; division stays untyped int (= 6), and only THEN is converted to
|
|
;; float64. (Wrong implementations float-coerce first, getting 6.0 from
|
|
;; what was meant to round.) The :ty-untyped-* tags below model this.
|
|
|
|
(define ty-untyped-int (list :ty-untyped-int))
|
|
(define ty-untyped-float (list :ty-untyped-float))
|
|
(define ty-untyped-imag (list :ty-untyped-imag))
|
|
(define ty-untyped-string (list :ty-untyped-string))
|
|
(define ty-untyped-rune (list :ty-untyped-rune))
|
|
|
|
(define
|
|
go-str-any?
|
|
(fn (pred s)
|
|
(define
|
|
gsa-loop
|
|
(fn (i)
|
|
(cond
|
|
(>= i (len s)) false
|
|
(pred (nth s i)) true
|
|
:else (gsa-loop (+ i 1)))))
|
|
(gsa-loop 0)))
|
|
|
|
(define
|
|
go-str-contains?
|
|
(fn (s ch) (go-str-any? (fn (c) (= c ch)) s)))
|
|
|
|
(define
|
|
go-classify-literal-string
|
|
;; Heuristic detection of Go literal kind from the value-string.
|
|
;; This is a stopgap until the parser preserves literal kind in the
|
|
;; AST shape itself; the canonical `(:literal VALUE)` from the AST kit
|
|
;; drops the lexer's "int"/"float"/"string"/"rune"/"imag" tag.
|
|
;; Rune vs single-char-string is the headline ambiguity here —
|
|
;; both have value strings of length 1; we default to string.
|
|
(fn (v)
|
|
(cond
|
|
(or (not (string? v)) (= (len v) 0)) :string
|
|
(or (and (>= (nth v 0) "0") (<= (nth v 0) "9"))
|
|
(and (= (nth v 0) ".") (>= (len v) 2)
|
|
(>= (nth v 1) "0") (<= (nth v 1) "9")))
|
|
(cond
|
|
(= (nth v (- (len v) 1)) "i") :imag
|
|
(go-str-contains? v ".") :float
|
|
(and (or (go-str-contains? v "e") (go-str-contains? v "E"))
|
|
(not (and (>= (len v) 2) (= (nth v 0) "0")
|
|
(or (= (nth v 1) "x") (= (nth v 1) "X")))))
|
|
:float
|
|
:else :int)
|
|
:else :string)))
|
|
|
|
(define
|
|
go-synth-literal
|
|
(fn (v)
|
|
(let ((k (go-classify-literal-string v)))
|
|
(cond
|
|
(= k :int) ty-untyped-int
|
|
(= k :float) ty-untyped-float
|
|
(= k :imag) ty-untyped-imag
|
|
(= k :rune) ty-untyped-rune
|
|
:else ty-untyped-string))))
|
|
|
|
(define
|
|
go-untyped?
|
|
(fn (t)
|
|
(and (list? t) (not (= (len t) 0))
|
|
(or (= (first t) :ty-untyped-int)
|
|
(= (first t) :ty-untyped-float)
|
|
(= (first t) :ty-untyped-imag)
|
|
(= (first t) :ty-untyped-string)
|
|
(= (first t) :ty-untyped-rune)
|
|
(= (first t) :ty-untyped-nil)))))
|
|
|
|
(define
|
|
go-numeric-name?
|
|
;; Built-in numeric type names per Go spec § Numeric types.
|
|
(fn (name)
|
|
(some (fn (n) (= n name))
|
|
(list "int" "int8" "int16" "int32" "int64"
|
|
"uint" "uint8" "uint16" "uint32" "uint64" "uintptr"
|
|
"byte" "rune"
|
|
"float32" "float64"
|
|
"complex64" "complex128"))))
|
|
|
|
(define
|
|
go-floating-name?
|
|
(fn (name)
|
|
(or (= name "float32") (= name "float64"))))
|
|
|
|
(define
|
|
go-complex-name?
|
|
(fn (name)
|
|
(or (= name "complex64") (= name "complex128"))))
|
|
|
|
(define
|
|
go-type-assignable?
|
|
;; Can a value of type GOT be assigned to a slot of type EXPECTED?
|
|
;; Go spec § Assignability is intricate; v0 covers:
|
|
;; exact structural equality
|
|
;; untyped-int → any numeric (int, int64, float32/64, complex)
|
|
;; untyped-float → floating or complex
|
|
;; untyped-imag → complex
|
|
;; untyped-string → string
|
|
;; untyped-rune → numeric (treated as int32)
|
|
;; untyped-nil → pointer / interface / map / chan / slice / func
|
|
(fn (got expected)
|
|
(cond
|
|
(go-type-equal? got expected) true
|
|
(and (list? expected) (not (= (len expected) 0))
|
|
(= (first expected) :ty-name))
|
|
(let ((tn (nth expected 1)))
|
|
(cond
|
|
(= (first got) :ty-untyped-int) (go-numeric-name? tn)
|
|
(= (first got) :ty-untyped-float)
|
|
(or (go-floating-name? tn) (go-complex-name? tn))
|
|
(= (first got) :ty-untyped-imag) (go-complex-name? tn)
|
|
(= (first got) :ty-untyped-rune) (go-numeric-name? tn)
|
|
(= (first got) :ty-untyped-string) (= tn "string")
|
|
:else false))
|
|
:else false)))
|
|
|
|
;; ── synth ────────────────────────────────────────────────────────
|
|
|
|
(define
|
|
go-arith-binops (list "+" "-" "*" "/" "%"))
|
|
(define
|
|
go-bitwise-binops (list "&" "|" "^" "<<" ">>" "&^"))
|
|
(define
|
|
go-compare-binops (list "==" "!=" "<" "<=" ">" ">="))
|
|
(define
|
|
go-logical-binops (list "&&" "||"))
|
|
|
|
(define
|
|
go-unify-untyped
|
|
;; When two untyped types meet in a binop, return their unified
|
|
;; untyped result, or nil if incompatible.
|
|
(fn (a b)
|
|
(cond
|
|
(go-type-equal? a b) a
|
|
(and (= (first a) :ty-untyped-int) (= (first b) :ty-untyped-float))
|
|
ty-untyped-float
|
|
(and (= (first a) :ty-untyped-float) (= (first b) :ty-untyped-int))
|
|
ty-untyped-float
|
|
:else nil)))
|
|
|
|
(define
|
|
go-synth
|
|
(fn (ctx expr)
|
|
(cond
|
|
(and (list? expr) (= (first expr) :literal))
|
|
(go-synth-literal (nth expr 1))
|
|
(and (list? expr) (= (first expr) :var))
|
|
(let ((name (nth expr 1)))
|
|
(let ((pre (go-predeclared-lookup name)))
|
|
(cond
|
|
(not (= pre nil)) pre
|
|
:else
|
|
(let ((t (go-ctx-lookup ctx name)))
|
|
(cond
|
|
(= t nil) (list :type-error :unbound name)
|
|
:else t)))))
|
|
;; (:app (:var OP) [LHS RHS]) — binary operator
|
|
(and (list? expr) (= (first expr) :app)
|
|
(list? (nth expr 1)) (= (first (nth expr 1)) :var)
|
|
(= (len (nth expr 2)) 2))
|
|
(let ((op (nth (nth expr 1) 1))
|
|
(args (nth expr 2)))
|
|
(go-synth-binop ctx op (first args) (nth args 1)))
|
|
:else (list :type-error :unsupported-synth expr))))
|
|
|
|
(define
|
|
go-synth-binop
|
|
(fn (ctx op lhs rhs)
|
|
(let ((lt (go-synth ctx lhs)) (rt (go-synth ctx rhs)))
|
|
(cond
|
|
(go-type-error? lt) lt
|
|
(go-type-error? rt) rt
|
|
;; Comparison ops always produce bool (untyped-bool, simplified
|
|
;; here to :ty-name "bool" until we model untyped-bool).
|
|
(some (fn (o) (= o op)) go-compare-binops)
|
|
(list :ty-name "bool")
|
|
(some (fn (o) (= o op)) go-logical-binops)
|
|
(list :ty-name "bool")
|
|
;; Arithmetic / bitwise: types must unify.
|
|
(or (some (fn (o) (= o op)) go-arith-binops)
|
|
(some (fn (o) (= o op)) go-bitwise-binops))
|
|
(cond
|
|
(and (go-untyped? lt) (go-untyped? rt))
|
|
(let ((unified (go-unify-untyped lt rt)))
|
|
(cond
|
|
(= unified nil)
|
|
(list :type-error :binop-untyped-mismatch op lt rt)
|
|
:else unified))
|
|
(and (go-untyped? lt) (not (go-untyped? rt)))
|
|
(cond
|
|
(go-type-assignable? lt rt) rt
|
|
:else (list :type-error :binop-mismatch op lt rt))
|
|
(and (not (go-untyped? lt)) (go-untyped? rt))
|
|
(cond
|
|
(go-type-assignable? rt lt) lt
|
|
:else (list :type-error :binop-mismatch op lt rt))
|
|
(go-type-equal? lt rt) lt
|
|
:else (list :type-error :binop-mismatch op lt rt))
|
|
:else (list :type-error :unsupported-binop op)))))
|
|
|
|
;; ── check ────────────────────────────────────────────────────────
|
|
|
|
(define
|
|
go-check
|
|
(fn
|
|
(ctx expr expected)
|
|
(let
|
|
((got (go-synth ctx expr)))
|
|
(cond
|
|
(go-type-error? got)
|
|
got
|
|
(go-type-assignable? got expected)
|
|
:ok :else
|
|
(list :type-error :mismatch expected got)))))
|