import type * as A from "./ast.js"; import type { Span } from "./lexer.js"; import type { Diagnostic } from "./diagnostics.js"; import { registerSource } from "./diagnostics.js"; import * as T from "./types.js"; import type { Type, FieldInfo } from "./types.js"; import { ANY, BOOL, BYTES, CHAR, ERR, FLOAT, INT, RANGE, STRING, VOID, assignable, freshTypeParams, fun, instantiate, list, mapType, optional, prim, result, set as setType, tuple, unify } from "./types.js"; import { findBuiltinMethod, findNativeConst, INT_PARSE, FLOAT_PARSE, NATIVE_MODULES, BYTES_FROM_LIST, BUILTIN_FUNS, type NativeFun, type NativeModule } from "./builtins.js"; // ---------- module info ---------- export interface FunSig { name: string; typeParams: string[]; params: { name: string; type: Type }[]; ret: Type; async: boolean; file: string; decl?: A.FunDecl; native?: NativeFun; exported: boolean; classOf?: string; moduleId: string; } export interface TypeDeclInfo { kind: "struct" | "enum" | "union" | "contract" | "schema" | "class"; name: string; typeParams: string[]; fields?: FieldInfo[]; variants?: { name: string; fields: FieldInfo[]; span?: Span }[]; methods?: FunSig[]; classFields?: { name: string; type: Type; mut: boolean; exported: boolean }[]; schemaSpec?: SchemaSpec; decl?: A.Decl; file: string; } export interface SchemaSpec { fields: { name: string; type: SchemaFieldType; required: boolean; default?: unknown; }[]; } export type SchemaFieldType = | { k: "str"; minLen?: number; maxLen?: number; pattern?: string } | { k: "int"; min?: number; max?: number } | { k: "float"; min?: number; max?: number } | { k: "bool" } | { k: "bytes" } | { k: "opt"; inner: SchemaFieldType } | { k: "list"; elem: SchemaFieldType } | { k: "any" } | { k: "format"; name: string } | { k: "enum"; variants: string[] } | { k: "ref"; name: string; kind: "struct" | "union" | "enum" | "schema"; fields: { name: string; type: SchemaFieldType }[] | string[] }; export interface ExportedItem { name: string; kind: "fun" | "const" | "type"; sig?: FunSig; type?: Type; } export interface ModuleInfo { id: string; kind: "ex" | "native" | "js"; jsSpecifier?: string; name: string; exports: ExportedItem[]; file?: string; } // ---------- resolutions (consumed by codegen) ---------- export type Resolution = | { kind: "var"; moduleId?: string } | { kind: "fun"; moduleId: string; sig: FunSig } | { kind: "native"; fun: NativeFun; shiftReceiver?: boolean } | { kind: "nativeConst"; name: string; moduleId: string } | { kind: "module"; moduleId: string } | { kind: "enumval" } | { kind: "intParse" } | { kind: "floatParse" } | { kind: "schemaMethod"; decl: TypeDeclInfo; method: "parse" | "from" } | { kind: "struct"; moduleId: string; decl: TypeDeclInfo } | { kind: "variantCtor"; decl: TypeDeclInfo; name: string } | { kind: "variantRef"; of: string; name: string } | { kind: "member"; field: FieldInfo } | { kind: "classField"; field: { name: string; type: Type; mut: boolean; exported: boolean } } | { kind: "method"; moduleId: string; sig: FunSig; hasReceiver: boolean } | { kind: "builtinMethod"; fun: NativeFun } | { kind: "all" | "race" | "timeout" }; export interface CheckedModule { moduleId: string; file: string; program: A.Program; exports: ExportedItem[]; types: Map; resolutions: Map; diagnostics: Diagnostic[]; decls: Map; funs: Map; topLets: Map; importedModules: Map; schemaSpecs: Map; } export interface CheckerOptions { moduleId: string; file: string; source: string; program: A.Program; resolveImport: (ref: A.ModuleRef) => ModuleInfo | null; resolveChecked: (moduleId: string) => CheckedModule | null; } interface Scope { vars: Map; narrows: Map; } interface FuncCtx { fallible: boolean; explicitFallible: boolean; async: boolean; inTry: boolean; retType: Type | null; implicitFallible: boolean; } const FORMAT_TYPES = new Set(["Email", "Url", "Uuid"]); const BUILTIN_CONCURRENCY = new Set(["all", "race", "timeout"]); export function checkModule(opts: CheckerOptions): CheckedModule { return new Checker(opts).run(); } class Checker { private diags: Diagnostic[] = []; private types = new Map(); private resolutions = new Map(); private scopes: Scope[] = []; private decls = new Map(); private funs = new Map(); private topLets = new Map(); private modules = new Map(); private exports: ExportedItem[] = []; private funcCtx: FuncCtx | null = null; private droppedResultFired = false; private inTest = false; private schemaSpecs = new Map(); constructor(private opts: CheckerOptions) {} run(): CheckedModule { const { program } = this.opts; registerSource(this.opts.file, this.opts.source); for (const imp of program.imports) this.processImport(imp); if (program.tests.length > 0) this.autoImportStdTest(); for (const d of program.decls) this.collectDecl(d); for (const d of program.decls) this.checkDecl(d); for (const t of program.tests) this.checkTest(t); return { moduleId: this.opts.moduleId, file: this.opts.file, program, exports: this.exports, types: this.types, resolutions: this.resolutions, diagnostics: this.diags, decls: this.decls, funs: this.funs, topLets: this.topLets, importedModules: this.modules, schemaSpecs: this.schemaSpecs, }; } // ---------- imports ---------- private processImport(imp: A.ImportDecl) { const info = this.opts.resolveImport(imp.module); if (!info) { this.error("E3004", `module '${modulePathText(imp.module)}' was not found`, imp.span, "Check the path. For std modules, use 'std.*'. For packages, add the dependency to project.xan. For JS, use 'js:name'."); return; } if (imp.names.length > 0) { for (const n of imp.names) { const item = info.exports.find((e) => e.name === n.name); if (!item) { this.error("E3004", `'${n.name}' is not exported by '${modulePathText(imp.module)}'`, imp.span, `The module exports: ${info.exports.map((e) => e.name).join(", ") || "(nothing)"}.`); continue; } const localName = n.alias ?? n.name; if (item.kind === "fun") { this.funs.set(localName, item.sig!); } else if (item.kind === "type") { const declInfo = this.declFromModule(info, item); if (declInfo) this.decls.set(localName, declInfo); } else if (item.kind === "const") { this.fromImportedConsts.set(localName, { moduleId: info.id, name: n.name }); } } return; } const alias = imp.alias ?? moduleAlias(imp.module); this.modules.set(alias, info); this.modules.set(info.id, info); } private fromImportedConsts = new Map(); private declFromModule(info: ModuleInfo, item: ExportedItem): TypeDeclInfo | null { if (info.kind === "ex") { const cm = this.opts.resolveChecked(info.id); return cm?.decls.get(item.name) ?? null; } return null; } private autoImportStdTest() { const mod = NATIVE_MODULES["std.test"]!; const info = moduleInfoFromNative(mod); this.modules.set("test", info); this.modules.set(info.id, info); } // ---------- declaration collection ---------- private collectDecl(d: A.Decl) { switch (d.kind) { case "fun": this.collectFun(d); break; case "let": break; case "struct": this.decls.set(d.name, { kind: "struct", name: d.name, typeParams: [], fields: [], file: d.file, decl: d }); break; case "enum": this.decls.set(d.name, { kind: "enum", name: d.name, typeParams: [], variants: d.variants.map((v) => ({ name: v, fields: [] })), file: d.file, decl: d }); break; case "type": this.decls.set(d.name, { kind: "union", name: d.name, typeParams: [], variants: [], file: d.file, decl: d }); break; case "contract": this.decls.set(d.name, { kind: "contract", name: d.name, typeParams: d.typeParams, fields: [], file: d.file, decl: d }); break; case "schema": this.decls.set(d.name, { kind: "schema", name: d.name, typeParams: [], fields: [], file: d.file, decl: d }); break; case "class": this.decls.set(d.name, { kind: "class", name: d.name, typeParams: [], fields: [], methods: [], classFields: [], file: d.file, decl: d }); break; } } private collectFun(d: A.FunDecl) { if (this.funs.has(d.name)) { this.error("E3002", `'${d.name}' is already declared in this module`, d.span, "Rename one of the declarations."); return; } const sig: FunSig = { name: d.name, typeParams: [], params: d.params.map((p) => ({ name: p.name, type: this.typeOf(p.type) })), ret: d.retType ? this.typeOf(d.retType) : VOID, async: d.async, file: d.file, decl: d, exported: d.exported, moduleId: this.opts.moduleId, }; if (!d.retType) sig.ret = this.preTypeFun(d); this.funs.set(d.name, sig); if (d.exported) this.exports.push({ name: d.name, kind: "fun", sig }); } /** shallow pre-type inference so callers see a function's return type before its body is checked */ private preTypeFun(d: A.FunDecl): Type { const memo = new Map(); const rec = (decl: A.FunDecl, stack: Set): Type => { if (stack.has(decl)) return { k: "unknown" }; const cached = memo.get(decl); if (cached) return cached; stack.add(decl); const t = this.preTypeBody(decl.body, stack); stack.delete(decl); memo.set(decl, t); return t; }; return rec(d, new Set()); } private preTypeBody(body: A.FunBody, stack: Set): Type { if (body.kind === "expr") return this.preTypeExpr(body.expr, stack); if (body.stmts.length === 0) return VOID; const last = body.stmts[body.stmts.length - 1]!; if (last.kind === "expr") return this.preTypeExpr(last.expr, stack); if (last.kind === "return") return last.value ? this.preTypeExpr(last.value, stack) : VOID; return VOID; } private preTypeExpr(e: A.Expr, stack: Set): Type { switch (e.kind) { case "int": return INT; case "float": return FLOAT; case "string": case "char": return e.kind === "char" ? CHAR : STRING; case "bool": return BOOL; case "undefined": case "none": return optional({ k: "unknown" }); case "some": return optional(this.preTypeExpr(e.value, stack)); case "ok": return result(e.value ? this.preTypeExpr(e.value, stack) : VOID); case "err": return result(ANY); case "ident": { const f = this.funs.get(e.name); if (f) return f.ret; return { k: "unknown" }; } case "unary": if (e.op === "not") return BOOL; return this.preTypeExpr(e.operand, stack); case "binary": { const l = this.preTypeExpr(e.left, stack); const r = this.preTypeExpr(e.right, stack); if (e.op === "or") { if (l.k === "optional") return r; if (l.k === "result") return r; return BOOL; } if (e.op === "==" || e.op === "!=" || e.op === "<" || e.op === "<=" || e.op === ">" || e.op === ">=" || e.op === "&&" || e.op === "||") return BOOL; if (e.op === "+") { if (l.k === "prim" && l.name === "String") return STRING; if (r.k === "prim" && r.name === "String") return STRING; } if (l.k === "prim" && r.k === "prim" && (l.name === "Float" || r.name === "Float")) return FLOAT; return INT; } case "range": return RANGE; case "if": { const thenT = this.preTypeBlock(e.then, stack); const elseT = e.else ? (e.else.kind === "block" ? this.preTypeBlock(e.else, stack) : this.preTypeExpr(e.else, stack)) : VOID; return thenT.k === "unknown" ? elseT : thenT; } case "match": { if (e.arms.length === 0) return VOID; const t = e.arms[0]!.body.kind === "block" ? this.preTypeBlock(e.arms[0]!.body, stack) : this.preTypeExpr(e.arms[0]!.body, stack); return t; } case "call": { if (e.callee.kind === "ident") { const f = this.funs.get(e.callee.name); if (f) return f.ret; if (BUILTIN_CONCURRENCY.has(e.callee.name)) { if (e.callee.name === "timeout") { return e.args[1] ? this.preTypeExpr(e.args[1]!, stack) : { k: "unknown" }; } const ts = e.args.map((a) => this.preTypeExpr(a, stack)); if (e.callee.name === "race") return ts[0] ?? { k: "unknown" }; return tuple(ts); } } return { k: "unknown" }; } case "propagate": return this.preTypeExpr(e.target, stack); case "await": return this.preTypeExpr(e.target, stack); case "lambda": return e.retType ? this.typeOf(e.retType) : { k: "unknown" }; case "list": return e.items.length ? list(this.preTypeExpr(e.items[0]!, stack)) : list({ k: "unknown" }); case "map": return mapType(STRING, { k: "unknown" }); case "tuple": return tuple(e.items.map((i) => this.preTypeExpr(i, stack))); case "member": return { k: "unknown" }; case "index": return { k: "unknown" }; case "struct": return { k: "unknown" }; case "optaccess": return optional({ k: "unknown" }); case "assign": return this.preTypeExpr(e.value, stack); case "is": return BOOL; case "raise": return { k: "unknown" }; } } private preTypeBlock(b: A.Block, stack: Set): Type { if (b.stmts.length === 0) return VOID; const last = b.stmts[b.stmts.length - 1]!; if (last.kind === "expr") return this.preTypeExpr(last.expr, stack); if (last.kind === "return") return last.value ? this.preTypeExpr(last.value, stack) : VOID; return VOID; } // ---------- type expression → Type ---------- private typeOf(te: A.TypeExpr): Type { switch (te.kind) { case "named": { const name = te.name; if (name === "Int" || name === "Float" || name === "Bool" || name === "String" || name === "Char") return prim(name); if (name === "Void") return VOID; if (name === "Any") return ANY; if (name === "Err") return ERR; if (name === "Bytes") return BYTES; if (name === "List") return list(te.args[0] ? this.typeOf(te.args[0]) : ANY); if (name === "Map") return mapType(te.args[0] ? this.typeOf(te.args[0]) : ANY, te.args[1] ? this.typeOf(te.args[1]) : ANY); if (name === "Set") return setType(te.args[0] ? this.typeOf(te.args[0]) : ANY); const info = this.decls.get(name); if (info) { if (te.args.length === 0) return this.declType(info); return this.instantiateDecl(info, te.args.map((a) => this.typeOf(a))); } this.error("E3004", `unknown type '${name}'`, te.span, "Known types: Int, Float, Bool, String, Char, Bytes, Any, Void, Err, List, Map, Set, and the types declared in this module and its imports."); return { k: "unknown" }; } case "optional": return optional(this.typeOf(te.inner)); case "result": return result(this.typeOf(te.inner)); case "list": return list(this.typeOf(te.elem)); case "map": return mapType(this.typeOf(te.key), this.typeOf(te.value)); case "set": return setType(this.typeOf(te.elem)); case "tuple": return tuple(te.items.map((i) => this.typeOf(i))); case "fun": return fun(te.params.map((p) => this.typeOf(p.type)), this.typeOf(te.ret)); } } private declType(info: TypeDeclInfo): Type { switch (info.kind) { case "struct": case "schema": case "contract": return { k: "struct", name: info.name, fields: info.fields ?? [], kind: info.kind, typeArgs: [] }; case "enum": return { k: "enum", name: info.name, variants: info.variants!.map((v) => v.name) }; case "union": return { k: "union", name: info.name, variants: info.variants ?? [] }; case "class": return { k: "struct", name: info.name, fields: info.fields ?? [], kind: "class", typeArgs: [] }; } } private instantiateDecl(info: TypeDeclInfo, args: Type[]): Type { if (info.kind === "contract" && info.typeParams.length) { const tp = new Map(); info.typeParams.forEach((_, i) => tp.set(i, args[i] ?? ANY)); const fields = info.fields!.map((f) => ({ name: f.name, type: instantiate(f.type, tp) })); return { k: "struct", name: info.name, fields, kind: "contract", typeArgs: args }; } return this.declType(info); } // ---------- declaration checking ---------- private checkDecl(d: A.Decl) { switch (d.kind) { case "fun": { const sig = this.funs.get(d.name)!; sig.params = d.params.map((p) => ({ name: p.name, type: this.typeOf(p.type) })); if (d.retType) sig.ret = this.typeOf(d.retType); this.checkFunBody(d); break; } case "let": { const initType = this.checkExpr(d.init); const declared = d.type ? this.typeOf(d.type) : null; if (declared && !assignable(initType, declared)) { this.assignabilityError(initType, declared, d.span, `'${d.name}' is declared as ${T.typeToString(declared)} but the initializer has type ${T.typeToString(initType)}.`); } const finalType = declared ?? initType; if (d.exported) this.exports.push({ name: d.name, kind: "const", type: finalType }); this.topLets.set(d.name, { type: finalType, mut: d.mut }); break; } case "struct": { const info = this.decls.get(d.name)!; info.fields = d.fields.map((f) => ({ name: f.name, type: this.typeOf(f.type) })); this.checkDuplicateFields(d.fields.map((f) => f.name), d.span); if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); break; } case "enum": { const info = this.decls.get(d.name)!; info.variants = d.variants.map((v) => ({ name: v, fields: [] })); if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); break; } case "type": { const info = this.decls.get(d.name)!; info.variants = d.variants.map((v) => ({ name: v.name, fields: v.params ? v.params.map((p) => ({ name: p.name, type: this.typeOf(p.type) })) : [], span: v.span, })); if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); break; } case "contract": { const info = this.decls.get(d.name)!; info.fields = d.methods.map((m) => ({ name: m.name, type: fun(m.params.map((p) => this.typeOf(p.type)), this.typeOf(m.retType)), })); if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); break; } case "schema": { const info = this.decls.get(d.name)!; const fields: FieldInfo[] = []; const spec: SchemaSpec = { fields: [] }; for (const f of d.fields) { const t = this.typeOf(f.type); fields.push({ name: f.name, type: t }); const v = this.schemaFieldType(f); spec.fields.push({ name: f.name, type: v.type, required: v.required, default: v.default }); } info.fields = fields; info.schemaSpec = spec; this.schemaSpecs.set(d.name, spec); if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); break; } case "class": { const info = this.decls.get(d.name)!; const fields: FieldInfo[] = []; const classFields = d.fields.map((f) => { const t = this.typeOf(f.type); fields.push({ name: f.name, type: t }); return { name: f.name, type: t, mut: f.mut, exported: f.exported }; }); info.classFields = classFields; const methods: FunSig[] = []; for (const m of d.methods) { const sig: FunSig = { name: m.name, typeParams: [], params: m.params.map((p) => ({ name: p.name, type: this.typeOf(p.type) })), ret: m.retType ? this.typeOf(m.retType) : VOID, async: m.async, file: m.file, decl: m, exported: m.exported, classOf: d.name, moduleId: this.opts.moduleId, }; if (!m.retType) sig.ret = this.preTypeFun(m); methods.push(sig); if (m.exported) this.exports.push({ name: `${d.name}.${m.name}`, kind: "fun", sig }); } info.methods = methods; info.fields = fields; if (d.exported) this.exports.push({ name: d.name, kind: "type", type: this.declType(info) }); for (const m of d.methods) this.checkClassMethod(d, m); break; } } } private checkDuplicateFields(names: string[], span: Span) { const seen = new Set(); for (const n of names) { if (seen.has(n)) this.error("E3002", `duplicate field '${n}'`, span, "Rename one of the fields."); seen.add(n); } } // ---------- schema ---------- private schemaFieldType(f: A.SchemaField): { type: SchemaFieldType; required: boolean; default?: unknown } { const cons = new Map(); let pattern: string | undefined; let defaultExpr: A.Expr | undefined; for (const c of f.constraints) { if (c.name === "default") { defaultExpr = c.value; continue; } const val = c.value; if (val.kind === "int") cons.set(c.name, val.value); else if (val.kind === "float") cons.set(c.name, val.value); else if (val.kind === "unary" && val.op === "-" && val.operand.kind === "int") cons.set(c.name, -val.operand.value); else if (val.kind === "string") { const text = val.parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); cons.set(c.name, text); } else if (val.kind === "bool") cons.set(c.name, val.value ? 1 : 0); else this.error("E2005", `constraint '${c.name}' must be a literal value`, c.span, "Constraints are checked at compile time, so they must be literals."); } const validNames = ["min", "max", "minLen", "maxLen", "pattern", "default"]; for (const k of cons.keys()) { if (!validNames.includes(k)) { this.error("E2005", `unknown constraint '${k}'`, f.span, `Valid constraints: ${validNames.join(", ")}.`); } } const defaultVal = defaultExpr ? this.literalValue(defaultExpr) : undefined; const base = this.schemaBaseType(f.type); const st = f.type.kind === "optional" ? ({ k: "opt", inner: base } as SchemaFieldType) : base; const min = cons.get("min") as number | undefined; const max = cons.get("max") as number | undefined; const minLen = cons.get("minLen") as number | undefined; const maxLen = cons.get("maxLen") as number | undefined; const pat = cons.get("pattern") as string | undefined; if (pat !== undefined) pattern = pat; const applied = this.applyConstraints(st, { min, max, minLen, maxLen, pattern: pat }); const req = !(f.type.kind === "optional") && defaultExpr === undefined; return { type: applied, required: req, default: defaultVal }; } private applyConstraints(st: SchemaFieldType, c: { min?: number; max?: number; minLen?: number; maxLen?: number; pattern?: string }): SchemaFieldType { switch (st.k) { case "str": return { k: "str", minLen: c.minLen, maxLen: c.maxLen, pattern: c.pattern }; case "int": return { k: "int", min: c.min, max: c.max }; case "float": return { k: "float", min: c.min, max: c.max }; case "opt": return { k: "opt", inner: this.applyConstraints(st.inner, c) }; default: return st; } } private schemaBaseType(te: A.TypeExpr): SchemaFieldType { switch (te.kind) { case "named": { const n = te.name; if (n === "String") return { k: "str" }; if (n === "Int") return { k: "int" }; if (n === "Float") return { k: "float" }; if (n === "Bool") return { k: "bool" }; if (n === "Bytes") return { k: "bytes" }; if (n === "Any") return { k: "any" }; if (FORMAT_TYPES.has(n)) return { k: "format", name: n }; const info = this.decls.get(n); if (info?.kind === "enum") return { k: "enum", variants: info.variants!.map((v) => v.name) }; if (info) { const kind: "struct" | "union" | "enum" | "schema" = info.kind === "contract" || info.kind === "class" ? "struct" : info.kind; const ref: Extract = { k: "ref", name: n, kind, fields: [] }; if (info.kind === "union") ref.fields = info.variants!.map((v) => v.name); else if (info.fields) ref.fields = info.fields.map((f) => ({ name: f.name, type: this.schemaBaseTypeFromType(f.type) })); return ref; } return { k: "any" }; } case "optional": return { k: "opt", inner: this.schemaBaseType(te.inner) }; case "list": return { k: "list", elem: this.schemaBaseType(te.elem) }; default: return { k: "any" }; } } private schemaBaseTypeFromType(t: Type): SchemaFieldType { switch (t.k) { case "prim": if (t.name === "String") return { k: "str" }; if (t.name === "Int") return { k: "int" }; if (t.name === "Float") return { k: "float" }; return { k: "bool" }; case "optional": return { k: "opt", inner: this.schemaBaseTypeFromType(t.inner) }; case "list": return { k: "list", elem: this.schemaBaseTypeFromType(t.elem) }; case "enum": return { k: "enum", variants: t.variants }; default: return { k: "any" }; } } private literalValue(e: A.Expr): unknown { switch (e.kind) { case "int": case "float": return e.value; case "bool": return e.value; case "char": return e.value; case "string": return e.parts.map((p) => (p.kind === "text" ? p.text : "")).join(""); case "unary": if (e.op === "-" && (e.operand.kind === "int" || e.operand.kind === "float")) return -e.operand.value; return undefined; case "list": return e.items.map((i) => this.literalValue(i)); case "none": return undefined; default: return undefined; } } // ---------- function bodies ---------- private checkFunBody(d: A.FunDecl) { const sig = this.funs.get(d.name)!; const declared = d.retType ? this.typeOf(d.retType) : null; const explicitFallible = d.retType?.kind === "result"; this.scopes.push({ vars: new Map(), narrows: new Map() }); const ctx: FuncCtx = { fallible: explicitFallible, explicitFallible, async: d.async, inTry: false, retType: declared, implicitFallible: false, }; for (const p of d.params) this.scopes[this.scopes.length - 1]!.vars.set(p.name, { type: this.typeOf(p.type), mut: false }); this.funcCtx = ctx; const bodyType = this.checkFunBodyExpr(d.body); this.funcCtx = null; this.scopes.pop(); this.finishFunBody(sig, bodyType, ctx, d.body.span); } private checkClassMethod(cls: A.ClassDecl, m: A.FunDecl) { const info = this.decls.get(cls.name)!; const sig = info.methods!.find((x) => x.name === m.name)!; const declared = m.retType ? this.typeOf(m.retType) : null; const explicitFallible = m.retType?.kind === "result"; this.scopes.push({ vars: new Map(), narrows: new Map() }); const scope = this.scopes[this.scopes.length - 1]!; scope.vars.set("this", { type: this.declType(info), mut: false }); for (const p of m.params) scope.vars.set(p.name, { type: this.typeOf(p.type), mut: false }); const ctx: FuncCtx = { fallible: explicitFallible, explicitFallible, async: m.async, inTry: false, retType: declared, implicitFallible: false, }; this.funcCtx = ctx; const bodyType = this.checkFunBodyExpr(m.body); this.funcCtx = null; this.scopes.pop(); this.finishFunBody(sig, bodyType, ctx, m.body.span); } /** reconcile the body's actual type with the function's (possibly fallible) declared return type */ private finishFunBody(sig: FunSig, bodyType: Type, ctx: FuncCtx, bodySpan: Span) { const declared = ctx.retType; const explicitFallible = ctx.explicitFallible; if (explicitFallible) { const declaredT = declared!; const valueT = declaredT.k === "result" ? declaredT.inner : declaredT; const ok = assignable(bodyType, declaredT) || assignable(bodyType, valueT); if (!ok && bodyType.k !== "unknown" && !ctx.implicitFallible) { this.assignabilityError(bodyType, valueT, bodySpan, `This function is declared '-> ${T.typeToString(declaredT)}' so its body must produce a ${T.typeToString(valueT)} or a ${T.typeToString(declaredT)}.`); } sig.ret = declaredT; } else if (ctx.implicitFallible) { if (declared && !assignable(bodyType, declared)) { this.assignabilityError(bodyType, declared, bodySpan, `This function returns ${T.typeToString(declared)}, but its body produces ${T.typeToString(bodyType)}.`); } sig.ret = result(declared ?? bodyType); } else { if (declared) { if (!assignable(bodyType, declared)) { if (bodyType.k === "result") { this.resultDroppedError(bodySpan, `This function's body produces a Result, but its declared return type is ${T.typeToString(declared)}. Handle it with '?', 'or', 'match', or '.require', or declare the function '-> ...!'`); } else { this.assignabilityError(bodyType, declared, bodySpan, `This function is declared '-> ${T.typeToString(declared)}' but its body produces ${T.typeToString(bodyType)}.`); } } } sig.ret = declared ?? bodyType; } } private checkFunBodyExpr(body: A.FunBody): Type { if (body.kind === "expr") return this.checkExpr(body.expr); return this.checkBlock(body, true); } private checkTest(t: A.TestDecl) { this.scopes.push({ vars: new Map(), narrows: new Map() }); this.inTest = true; this.funcCtx = { fallible: false, explicitFallible: false, async: false, inTry: false, retType: VOID, implicitFallible: false }; this.checkBlock(t.body, false); this.funcCtx = null; this.inTest = false; this.scopes.pop(); } private checkBlock(block: A.Block, isBody: boolean): Type { this.scopes.push({ vars: new Map(), narrows: new Map() }); let lastType: Type = VOID; for (let i = 0; i < block.stmts.length; i++) { const s = block.stmts[i]!; const isLast = i === block.stmts.length - 1; lastType = this.checkStmt(s, isLast && isBody); } this.scopes.pop(); return lastType; } private checkStmt(s: A.Stmt, isLastExpr: boolean): Type { switch (s.kind) { case "let": { if (!s.init) { this.error("E2005", `'${s.name}' needs an initializer`, s.span, "In EX, 'let' always requires an initial value."); return VOID; } const initType = this.checkExpr(s.init); const declared = s.type ? this.typeOf(s.type) : null; if (declared && !assignable(initType, declared)) { this.assignabilityError(initType, declared, s.span, `'${s.name}' is declared as ${T.typeToString(declared)} but the initializer has type ${T.typeToString(initType)}.`); } const finalType = declared ?? initType; this.scopes[this.scopes.length - 1]!.vars.set(s.name, { type: finalType, mut: s.mut }); return VOID; } case "expr": { const t = this.checkExpr(s.expr); if (t.k === "result" && !isLastExpr) { this.resultDroppedError(s.span, "A Result value was dropped without being handled. The function may fail, and you're ignoring that."); } return t; } case "return": { const ctx = this.funcCtx; if (!ctx) { this.error("E2003", "'return' is only valid inside a function", s.span); return VOID; } if (s.value) { const t = this.checkExpr(s.value); if (ctx.fallible) { const inner = ctx.retType && ctx.retType.k === "result" ? ctx.retType.inner : ctx.retType; if (t.k === "result") { if (ctx.retType && !assignable(t, ctx.retType)) this.assignabilityError(t, ctx.retType, s.span, `This function returns ${T.typeToString(ctx.retType)}.`); } else if (inner && !assignable(t, inner)) { this.assignabilityError(t, inner, s.span, `This function returns ${T.typeToString(inner)} (via '!'), but this value has type ${T.typeToString(t)}.`); } } else if (ctx.inTry) { // value returned as-is } else { if (t.k === "result") { this.resultDroppedError(s.span, `This function is not fallible, but this return value is a Result. Handle it with '?', 'or', 'match', or '.require', or declare the function '-> ...!'`); } else if (ctx.retType && !assignable(t, ctx.retType)) { this.assignabilityError(t, ctx.retType, s.span, `This function returns ${T.typeToString(ctx.retType)}, but this value has type ${T.typeToString(t)}.`); } } return t; } return VOID; } case "raise": { const ctx = this.funcCtx; if (!ctx) this.error("E2003", "'raise' is only valid inside a function", s.span); else if (!ctx.fallible && !ctx.inTry) { this.error("E4003", "'raise' needs a fallible function", s.span, "This function is not declared with '!' (and is not inside 'try'), so there is nowhere for the failure to go.", [{ title: "Declare the function as fallible", code: `fun f() -> T! { ... }`, }]); } else if (!ctx.explicitFallible && !ctx.inTry) { ctx.implicitFallible = true; ctx.fallible = true; } const t = this.checkExpr(s.value); if (t.k !== "prim" && t.k !== "any" && t.k !== "unknown") { this.assignabilityError(t, STRING, s.span, "The message passed to 'raise' must be a String."); } return ctx && ctx.fallible ? { k: "unknown" } : VOID; } case "if": { const condType = this.checkExpr(s.cond); this.expectBool(condType, s.cond, "the condition of 'if'"); const narrows = this.narrowsFromCond(s.cond); this.scopes.push({ vars: new Map(), narrows }); const thenType = this.checkBlock(s.then, false); this.scopes.pop(); let elseType: Type = VOID; if (s.else) { this.scopes.push({ vars: new Map(), narrows: new Map() }); elseType = this.checkBlock(s.else, false); this.scopes.pop(); } return this.commonType(thenType, elseType, s.span); } case "iflet": { const valueType = this.checkExpr(s.value); this.checkPatternBindings(s.pattern, valueType, s.span); this.scopes.push({ vars: new Map(), narrows: new Map() }); this.bindPattern(s.pattern, valueType); const thenType = this.checkBlock(s.then, false); this.scopes.pop(); if (s.else) { this.scopes.push({ vars: new Map(), narrows: new Map() }); const elseType = this.checkBlock(s.else, false); this.scopes.pop(); return this.commonType(thenType, elseType, s.span); } return thenType; } case "match": return this.checkMatchStmt(s.value, s.arms, s.span); case "for": { const itType = this.checkExpr(s.iterable); const itemType = this.iterableItemType(itType, s.iterable, s.span); this.scopes.push({ vars: new Map(), narrows: new Map() }); this.bindPattern(s.pattern, itemType); const bodyType = this.checkBlock(s.body, false); this.scopes.pop(); return bodyType; } case "while": { const condType = this.checkExpr(s.cond); this.expectBool(condType, s.cond, "the condition of 'while'"); this.checkBlock(s.body, false); return VOID; } case "try": { this.scopes.push({ vars: new Map(), narrows: new Map() }); const prev = this.funcCtx!; this.funcCtx = { ...prev, inTry: true }; this.checkBlock(s.body, false); this.funcCtx = prev; this.scopes.pop(); this.scopes.push({ vars: new Map(), narrows: new Map() }); this.scopes[this.scopes.length - 1]!.vars.set(s.catchVar, { type: ANY, mut: false }); this.checkBlock(s.handler, false); this.scopes.pop(); return VOID; } case "with": { const initType = this.checkExpr(s.init); this.scopes.push({ vars: new Map(), narrows: new Map() }); this.scopes[this.scopes.length - 1]!.vars.set(s.name, { type: initType, mut: false }); this.checkBlock(s.body, false); this.scopes.pop(); return VOID; } } } private narrowsFromCond(cond: A.Expr): Map { const narrows = new Map(); if (cond.kind === "is" && cond.left.kind === "ident") { const t = this.typeTestType(cond.type); if (t) narrows.set(cond.left.name, t); } if (cond.kind === "binary" && cond.op === "&&" && cond.left.kind === "is" && cond.left.left.kind === "ident") { const t = this.typeTestType(cond.left.type); if (t) narrows.set(cond.left.left.name, t); } return narrows; } private typeTestType(te: A.TypeExpr): Type | null { if (te.kind === "named") { const n = te.name; if (n === "Int" || n === "Float" || n === "Bool" || n === "String" || n === "Char") return prim(n); if (n === "Bytes") return BYTES; if (n === "Any") return ANY; if (n === "List") return list(te.args[0] ? this.typeOf(te.args[0]) : ANY); if (n === "Map") return mapType(ANY, ANY); if (n === "Set") return setType(ANY); const info = this.decls.get(n); if (info) return this.declType(info); const v = this.findVariantName(n); if (v) return { k: "variant", of: v.decl.name, name: n, fields: v.fields }; const e = this.findEnumMember(n); if (e) return { k: "enumval", of: e.decl.name, name: n }; } return null; } private expectBool(t: Type, expr: A.Expr, what: string) { if (t.k === "any" || t.k === "unknown") return; if (t.k === "prim" && t.name === "Bool") return; if (t.k === "optional") { this.error("E1008", `${what} must be a Bool`, expr.span, `This value has type ${T.typeToString(t)} — it may be missing. Check presence first or use 'or':`, [{ title: "Provide a fallback", code: `(${exprText(expr)} or false)`, }]); return; } if (t.k === "result") { this.error("E1009", `${what} must be a Bool`, expr.span, `This value is a Result — handle it with '?', 'or', 'match', or '.require' first.`); return; } this.assignabilityError(t, BOOL, expr.span, `${what} must be a Bool, but this value has type ${T.typeToString(t)}.`); } private iterableItemType(t: Type, expr: A.Expr, span: Span): Type { switch (t.k) { case "list": return t.elem; case "set": return t.elem; case "range": return INT; case "map": return tuple([t.key, t.value]); case "prim": if (t.name === "String") return CHAR; break; case "bytes": return INT; case "any": return ANY; case "optional": this.error("E1007", "cannot iterate over an optional value", span, `This value has type ${T.typeToString(t)} — it may be missing. Check presence first:`, [{ title: "Check it first", code: `if let xs = ${exprText(expr)} { for x in xs { ... } }`, }]); break; case "result": this.error("E1009", "cannot iterate over a Result", span, `Handle the failure first with '?', 'or', 'match', or '.require'.`); break; default: this.error("E1001", "cannot iterate over this value", span, `Its type is ${T.typeToString(t)}. Iteration works over List, Set, Map, ranges, String, and Bytes.`); } return ANY; } // ---------- match ---------- private checkMatchStmt(value: A.Expr, arms: A.MatchArm[], span: Span): Type { const valueType = this.checkExpr(value); this.checkPatternBindingsForArms(arms, valueType, span); let common: Type | null = null; for (const arm of arms) { const armType = this.checkArm(arm, valueType, value); common = common === null ? armType : this.commonType(common, armType, arm.span); } this.checkExhaustive(valueType, arms, span); return common ?? VOID; } private checkPatternBindingsForArms(arms: A.MatchArm[], valueType: Type, span: Span) { const handlesResult = arms.some((a) => this.patternCovers(a.pattern, "result")); if (valueType.k === "result" && !handlesResult) { this.resultDroppedError(span, "A Result value was matched without covering both 'Ok' and 'Err' — the failure case would be dropped."); this.droppedResultFired = true; } for (const arm of arms) this.checkPatternBindings(arm.pattern, valueType, arm.span); } private checkArm(arm: A.MatchArm, valueType: Type, value: A.Expr): Type { this.scopes.push({ vars: new Map(), narrows: new Map() }); if (value.kind === "ident") { const narrowed = this.patternNarrowType(arm.pattern, valueType); if (narrowed) this.scopes[this.scopes.length - 1]!.narrows.set(value.name, narrowed); } this.bindPattern(arm.pattern, valueType); const bodyType = arm.body.kind === "block" ? this.checkBlock(arm.body, false) : this.checkExpr(arm.body); this.scopes.pop(); return bodyType; } private patternNarrowType(p: A.Pattern, scrutinee: Type): Type | null { switch (p.kind) { case "variant": { if (scrutinee.k === "union") { const v = scrutinee.variants.find((x) => x.name === p.name); if (v) return { k: "variant", of: scrutinee.name, name: p.name, fields: v.fields }; } if (scrutinee.k === "enum") return scrutinee; return null; } case "is": { const t = this.typeTestType(p.type); if (scrutinee.k === "any" && t) return t; return null; } case "some": if (scrutinee.k === "optional") return scrutinee.inner; return null; case "ok": if (scrutinee.k === "result") return scrutinee.inner; return null; case "err": if (scrutinee.k === "result") return ERR; return null; case "alt": { let t: Type | null = null; for (const it of p.items) { const n = this.patternNarrowType(it, scrutinee); if (n) t = t ? this.commonType(t, n, p.span) : n; } return t; } default: return null; } } private bindPattern(p: A.Pattern, scrutinee: Type) { const scope = this.scopes[this.scopes.length - 1]!; switch (p.kind) { case "ident": { if (!this.isKnownVariant(p.name, scrutinee)) scope.vars.set(p.name, { type: scrutinee, mut: false }); return; } case "wild": return; case "some": this.bindPattern(p.inner, scrutinee.k === "optional" ? scrutinee.inner : scrutinee.k === "any" ? ANY : scrutinee); return; case "none": return; case "ok": this.bindPattern(p.inner, scrutinee.k === "result" ? scrutinee.inner : scrutinee.k === "any" ? ANY : scrutinee); return; case "err": if (p.inner) this.bindPattern(p.inner, ERR); return; case "variant": { if (scrutinee.k === "union") { const v = scrutinee.variants.find((x) => x.name === p.name); if (v && p.args) { if (p.args.length !== v.fields.length) { this.error("E5002", `variant '${p.name}' has ${v.fields.length} field(s), but the pattern provides ${p.args.length}`, p.span); } v.fields.forEach((f, i) => { const arg = p.args![i]; if (arg) this.bindPattern(arg, f.type); }); } } else if (scrutinee.k === "any" && p.args) { for (const arg of p.args) this.bindPattern(arg, ANY); } return; } case "is": return; case "tuple": { if (scrutinee.k === "tuple") { p.items.forEach((it, i) => this.bindPattern(it, scrutinee.items[i] ?? ANY)); } else if (scrutinee.k === "list") { p.items.forEach((it) => this.bindPattern(it, scrutinee.elem)); } else if (scrutinee.k === "any") { p.items.forEach((it) => this.bindPattern(it, ANY)); } else { this.error("E1001", "cannot destructure this value with a tuple pattern", p.span, `Its type is ${T.typeToString(scrutinee)}.`); } return; } case "literal": return; case "alt": for (const it of p.items) this.bindPattern(it, scrutinee); return; } } private isKnownVariant(name: string, scrutinee: Type): boolean { if (scrutinee.k === "union") return scrutinee.variants.some((v) => v.name === name); if (scrutinee.k === "enum") return scrutinee.variants.includes(name); return false; } private checkPatternBindings(p: A.Pattern, scrutinee: Type, span: Span) { const incompatible = (what: string) => { this.error("E5002", what, span, `The value has type ${T.typeToString(scrutinee)}. Use a pattern that matches its shape, or match a different value.`); }; switch (p.kind) { case "ok": case "err": if (scrutinee.k !== "result" && scrutinee.k !== "any") incompatible(`This pattern expects a Result (Ok/Err), but the value is not one.`); break; case "some": case "none": if (scrutinee.k !== "optional" && scrutinee.k !== "any") incompatible(`This pattern expects an optional value (Some/None), but the value is not optional.`); break; case "variant": if (scrutinee.k === "union") { const v = scrutinee.variants.find((x) => x.name === p.name); if (!v) { this.error("E5002", `'${p.name}' is not a variant of this union`, p.span, `The variants are: ${scrutinee.variants.map((x) => x.name).join(", ")}.`); } } else if (scrutinee.k === "enum") { if (!scrutinee.variants.includes(p.name)) { this.error("E5002", `'${p.name}' is not a member of this enum`, p.span, `The members are: ${scrutinee.variants.join(", ")}.`); } } else if (scrutinee.k !== "any") { incompatible(`This pattern matches variants, but the value has type ${T.typeToString(scrutinee)}.`); } break; case "is": { if (!this.typeTestType(p.type)) this.error("E3004", "unknown type in type test", p.span); break; } default: break; } if (p.kind === "alt") for (const it of p.items) this.checkPatternBindings(it, scrutinee, span); } private patternCovers(p: A.Pattern, kind: "union" | "enum" | "bool" | "optional" | "result"): boolean { switch (p.kind) { case "wild": return true; case "ident": return !this.isVariantNamePattern(p); case "alt": return p.items.some((i) => this.patternCovers(i, kind)); case "some": return kind === "optional"; case "none": return kind === "optional"; case "ok": return kind === "result"; case "err": return kind === "result"; case "variant": return true; case "literal": return kind === "bool"; case "is": return true; default: return false; } } private isVariantNamePattern(p: A.Pattern): boolean { if (p.kind !== "ident") return false; return this.decls.has(p.name); } private checkExhaustive(scrutinee: Type, arms: A.MatchArm[], span: Span) { if (scrutinee.k === "any" || scrutinee.k === "unknown") return; if (scrutinee.k === "result" && this.droppedResultFired) { this.droppedResultFired = false; return; } if (arms.some((a) => this.patternCovers(a.pattern, "union"))) return; const covered = new Set(); for (const a of arms) { const p = a.pattern; switch (scrutinee.k) { case "union": case "enum": if (p.kind === "variant") covered.add(p.name); else if (p.kind === "ident" && this.isVariantNamePattern(p)) covered.add(p.name); break; case "optional": if (p.kind === "some") covered.add("some"); if (p.kind === "none") covered.add("none"); break; case "result": if (p.kind === "ok") covered.add("ok"); if (p.kind === "err") covered.add("err"); break; case "prim": if (scrutinee.name === "Bool" && p.kind === "literal") covered.add(String(p.value)); break; default: return; } } const missing: string[] = []; switch (scrutinee.k) { case "union": for (const v of scrutinee.variants) if (!covered.has(v.name)) missing.push(v.name); break; case "enum": for (const v of scrutinee.variants) if (!covered.has(v)) missing.push(v); break; case "optional": if (!covered.has("some")) missing.push("Some(..)"); if (!covered.has("none")) missing.push("None"); break; case "result": if (!covered.has("ok")) missing.push("Ok(..)"); if (!covered.has("err")) missing.push("Err(..)"); break; case "prim": if (scrutinee.name === "Bool") { if (!covered.has("true")) missing.push("true"); if (!covered.has("false")) missing.push("false"); } break; } if (missing.length > 0) { this.error("E5001", "match is not exhaustive", span, `The value can be ${missing.join(", ")} — but the match doesn't cover it. Either add the missing arm(s) or a '_' wildcard.`, [{ title: "Cover all cases", code: `match value {\n ${missing.map((m) => `${m} -> ...`).join("\n ")}\n _ -> ...\n }`, }]); } } // ---------- expressions ---------- checkExpr(e: A.Expr, expected?: Type): Type { const t = this.checkExprInner(e, expected); this.types.set(e, t); return t; } private checkExprInner(e: A.Expr, expected?: Type): Type { switch (e.kind) { case "raise": { const ctx = this.funcCtx; if (!ctx) this.error("E2003", "'raise' is only valid inside a function", e.span); else if (!ctx.fallible && !ctx.inTry) { this.error("E4003", "'raise' needs a fallible function", e.span, "This function is not declared with '!' (and is not inside 'try'), so there is nowhere for the failure to go.", [{ title: "Declare the function as fallible", code: "fun f() -> T! { ... }", }]); } else if (!ctx.explicitFallible && !ctx.inTry) { ctx.implicitFallible = true; ctx.fallible = true; } const t = this.checkExpr(e.value); if (t.k !== "prim" && t.k !== "any" && t.k !== "unknown") { this.assignabilityError(t, STRING, e.span, "The message passed to 'raise' must be a String."); } return expected ?? ({ k: "unknown" } as Type); } case "int": return INT; case "float": return FLOAT; case "string": for (const p of e.parts) if (p.kind === "expr") this.checkExpr(p.expr); return STRING; case "char": return CHAR; case "bool": return BOOL; case "undefined": case "none": return optional({ k: "unknown" }); case "ident": { for (let i = this.scopes.length - 1; i >= 0; i--) { const sc = this.scopes[i]!; const v = sc.vars.get(e.name); if (v) { this.resolutions.set(e, { kind: "var" }); return sc.narrows.get(e.name) ?? v.type; } } const fn = this.funs.get(e.name); if (fn) { this.resolutions.set(e, { kind: "fun", moduleId: fn.moduleId, sig: fn }); return fun(fn.params.map((p) => p.type), fn.ret, fn.async); } const builtin = BUILTIN_FUNS[e.name]; if (builtin) { this.resolutions.set(e, { kind: "native", fun: builtin }); return builtin.ret; } if (this.inTest) { const testMod = this.modules.get("test"); const item = testMod?.exports.find((x) => x.name === e.name && x.kind === "fun" && x.sig); if (item?.sig?.native) { this.resolutions.set(e, { kind: "native", fun: item.sig.native }); return fun(item.sig.params.map((p) => p.type), item.sig.ret, item.sig.async); } } if (BUILTIN_CONCURRENCY.has(e.name)) { this.resolutions.set(e, { kind: e.name as "all" }); return ANY; } const decl = this.decls.get(e.name); if (decl) { this.resolutions.set(e, { kind: "struct", moduleId: this.opts.moduleId, decl }); return this.declType(decl); } if (e.name === "Bytes") { this.resolutions.set(e, { kind: "native", fun: BYTES_FROM_LIST }); return BYTES; } const v = this.findVariantName(e.name); if (v) { this.resolutions.set(e, { kind: "variantCtor", decl: v.decl, name: e.name }); return { k: "variant", of: v.decl.name, name: e.name, fields: v.fields }; } const importedConst = this.fromImportedConsts.get(e.name); if (importedConst) { this.resolutions.set(e, { kind: "nativeConst", name: importedConst.name, moduleId: importedConst.moduleId }); return this.nativeConstType(importedConst.moduleId, importedConst.name, e.span); } const module = this.modules.get(e.name); if (module) { this.resolutions.set(e, { kind: "module", moduleId: module.id }); return { k: "module", moduleId: module.id }; } const topLet = this.topLets.get(e.name); if (topLet) { this.resolutions.set(e, { kind: "var", moduleId: this.opts.moduleId }); return topLet.type; } const didYouMean = this.suggestName(e.name); this.error("E3001", `'${e.name}' is not defined here`, e.span, `Nothing named '${e.name}' exists in this scope.${didYouMean ? ` Did you mean '${didYouMean}'?` : ""}`); return { k: "unknown" }; } case "unary": { const t = this.checkExpr(e.operand); if (e.op === "not") { if (t.k === "prim" && t.name === "Bool") return BOOL; if (t.k === "any" || t.k === "unknown") return BOOL; this.assignabilityError(t, BOOL, e.span, `'not' needs a Bool, but this value has type ${T.typeToString(t)}.`); return BOOL; } if (t.k === "prim" && (t.name === "Int" || t.name === "Float")) return t; if (t.k === "any" || t.k === "unknown") return ANY; this.assignabilityError(t, INT, e.span, `'-' needs a number, but this value has type ${T.typeToString(t)}.`); return INT; } case "binary": return this.checkBinary(e); case "is": { const t = this.checkExpr(e.left); const tt = this.typeTestType(e.type); if (!tt) this.error("E3004", "unknown type in type test", e.type.span); return BOOL; } case "range": { const st = this.checkExpr(e.start); const en = this.checkExpr(e.end); for (const [t, name] of [[st, "start"], [en, "end"]] as const) { if (t.k === "any" || t.k === "unknown") continue; if (!(t.k === "prim" && (t.name === "Int" || t.name === "Float"))) { this.assignabilityError(t, INT, e.span, `The ${name} of a range must be an Int.`); } } return RANGE; } case "assign": { const lvalue = this.assignTargetType(e.target); const valueType = this.checkExpr(e.value); if (e.op !== "=") { if (!(lvalue.k === "prim" && (lvalue.name === "Int" || lvalue.name === "Float")) && lvalue.k !== "any") { this.assignabilityError(lvalue, INT, e.span, `'${e.op}' needs a number, but this has type ${T.typeToString(lvalue)}.`); } this.checkAssignTarget(e.target); return lvalue; } if (!assignable(valueType, lvalue)) { this.assignabilityError(valueType, lvalue, e.span, `Cannot assign a ${T.typeToString(valueType)} to a ${T.typeToString(lvalue)}.`); } this.checkAssignTarget(e.target); return lvalue; } case "call": return this.checkCall(e); case "index": return this.checkIndex(e); case "member": return this.checkMember(e); case "optaccess": { const targetType = this.checkExpr(e.target); const inner = targetType.k === "optional" ? targetType.inner : targetType.k === "any" ? ANY : null; if (inner === null) { this.error("E1008", "'?.' on a value that cannot be missing", e.span, `This value has type ${T.typeToString(targetType)}. '?.' is for optional values ('?').`); return { k: "unknown" }; } const memberType = this.memberType(inner, e.name, e.span, e); return optional(memberType); } case "propagate": { const ctx = this.funcCtx; const t = this.checkExpr(e.target); if (t.k !== "result") { if (t.k === "any") { this.error("E4002", "cannot propagate an 'Any' value", e.span, "An 'Any' value may or may not be a Result; narrow it with 'is' or validate it with a schema first."); return ANY; } if (t.k === "unknown") return t; this.error("E4002", `'?' only works on Results`, e.span, `This value has type ${T.typeToString(t)} — it cannot fail, so there is nothing to propagate.`, [{ title: "Remove the '?'", code: exprText(e.target), }]); return t; } if (!ctx || (!ctx.fallible && !ctx.inTry)) { this.error("E4002", "'?' needs a fallible function", e.span, "'?' propagates failures, so it is only valid inside a function declared with '!' — or inside 'try' at the JavaScript boundary.", [{ title: "Declare the function as fallible", code: `fun f() -> T! { ... }`, }]); } else if (ctx && !ctx.inTry && !ctx.explicitFallible) { ctx.implicitFallible = true; ctx.fallible = true; } return t.inner; } case "if": { const condType = this.checkExpr(e.cond); this.expectBool(condType, e.cond, "the condition of 'if'"); const narrows = this.narrowsFromCond(e.cond); this.scopes.push({ vars: new Map(), narrows }); const thenType = this.checkBlock(e.then, false); this.scopes.pop(); let elseType: Type = VOID; if (e.else) { this.scopes.push({ vars: new Map(), narrows: new Map() }); elseType = e.else.kind === "block" ? this.checkBlock(e.else, false) : this.checkExpr(e.else); this.scopes.pop(); } return this.commonType(thenType, elseType, e.span); } case "match": return this.checkMatchExpr(e); case "lambda": return this.checkLambda(e, e.async, expected); case "await": { const ctx = this.funcCtx; if (!ctx || !ctx.async) { this.error("E6001", "'await' needs an async function", e.span, "'await' is only valid inside an 'async fun' or an 'async' lambda.", [{ title: "Make the function async", code: `async fun f() { ... }`, }]); } const t = this.checkExpr(e.target); if (t.k === "fun") { this.error("E6002", "nothing to await here", e.span, "This value is a function. Call it first: `await f()`."); } return t; } case "list": { if (e.items.length === 0) return list({ k: "unknown" }); const itemTypes = e.items.map((i) => this.checkExpr(i)); return list(this.commonListType(itemTypes, e.span)); } case "map": { const valueTypes = e.entries.map((en) => this.checkExpr(en.value)); return mapType(STRING, this.commonListType(valueTypes, e.span)); } case "tuple": return tuple(e.items.map((i) => this.checkExpr(i))); case "struct": { const decl = this.decls.get(e.name); if (!decl || (decl.kind !== "struct" && decl.kind !== "schema" && decl.kind !== "class")) { if (decl?.kind === "enum") this.error("E1005", `'${e.name}' is an enum — construct members with '${e.name}.Member'`, e.span); else if (decl?.kind === "union") this.error("E1005", `'${e.name}' is a union — construct a variant: '${decl.variants![0]?.name}(...)'.`, e.span); else this.error("E3001", `'${e.name}' is not a constructible type`, e.span); for (const en of e.entries) this.checkExpr(en.value); return { k: "unknown" }; } const fields = decl.fields!; const used = new Set(); for (const en of e.entries) { const f = fields.find((x) => x.name === en.name); if (!f) { this.error("E1004", `'${e.name}' has no field '${en.name}'`, en.span, `Its fields are: ${fields.map((x) => x.name).join(", ") || "(none)"}.`); this.checkExpr(en.value); continue; } const vt = this.checkExpr(en.value); if (!assignable(vt, f.type)) { this.assignabilityError(vt, f.type, en.span, `Field '${en.name}' expects ${T.typeToString(f.type)}, but this value has type ${T.typeToString(vt)}.`); } used.add(en.name); } const missing = fields.filter((f) => !used.has(f.name)); if (missing.length > 0) { this.error("E1006", `missing field(s) for '${e.name}': ${missing.map((m) => m.name).join(", ")}`, e.span, "Struct construction must provide every field."); } this.resolutions.set(e, { kind: "struct", moduleId: this.opts.moduleId, decl }); return { k: "struct", name: e.name, fields, kind: decl.kind === "schema" ? "schema" : decl.kind === "class" ? "class" : "struct", typeArgs: [] }; } case "ok": { const v = e.value ? this.checkExpr(e.value) : VOID; return result(v); } case "err": { const v = this.checkExpr(e.value); if (!(v.k === "prim" && v.name === "String") && v.k !== "any" && v.k !== "unknown") { this.assignabilityError(v, STRING, e.span, "The message passed to Err(...) must be a String."); } return result(ANY); } case "some": { const v = this.checkExpr(e.value); return optional(v); } } } private nativeConstType(moduleId: string, name: string, span: Span): Type { const c = findNativeConst(moduleId, name); if (c) return c.type; this.error("E3001", `'${name}' is not a constant in this module`, span); return { k: "unknown" }; } private checkLambda(e: Extract, isAsync: boolean, expected?: Type): Type { let expectedParams: Type[] | null = null; let expectedRet: Type | null = null; if (expected?.k === "fun") { expectedParams = expected.params; expectedRet = expected.ret; } const params = e.params.map((p, i) => { if (p.type.kind === "named" && p.type.name === "_") return expectedParams?.[i] ?? ANY; return this.typeOf(p.type); }); const ret = e.retType ? this.typeOf(e.retType) : expectedRet ?? null; this.scopes.push({ vars: new Map(), narrows: new Map() }); const prev = this.funcCtx; this.funcCtx = { fallible: e.retType?.kind === "result", explicitFallible: e.retType?.kind === "result", async: isAsync, inTry: false, retType: ret, implicitFallible: false, }; params.forEach((pt, i) => this.scopes[this.scopes.length - 1]!.vars.set(e.params[i]!.name, { type: pt, mut: false })); const bodyType = this.checkFunBodyExpr(e.body); const ctx = this.funcCtx; this.funcCtx = prev; this.scopes.pop(); const finalRet = ret ?? (ctx!.implicitFallible ? result(bodyType) : bodyType); return fun(params, finalRet, isAsync); } private checkBinary(e: A.Expr): Type { if (e.kind !== "binary") return { k: "unknown" }; const { op } = e; if (op === "or") { const leftType = this.checkExpr(e.left); const rightType = this.checkExpr(e.right); if (leftType.k === "optional") return this.commonType(leftType.inner, rightType, e.span); if (leftType.k === "result") return this.commonType(leftType.inner, rightType, e.span); if (leftType.k === "prim" && leftType.name === "Bool") { if (rightType.k === "prim" && rightType.name === "Bool") return BOOL; if (rightType.k === "any") return BOOL; this.assignabilityError(rightType, BOOL, e.right.span, "Both sides of 'or' must be Bool when the left side is Bool."); return BOOL; } if (leftType.k === "any") return rightType; if (leftType.k === "unknown" || rightType.k === "unknown") return leftType; this.error("E1008", "'or' needs an optional or fallible left side", e.span, `The left side has type ${T.typeToString(leftType)} — it always has a value, so there is nothing to fall back to.`, [{ title: "Remove the fallback", code: exprText(e.left), }]); return leftType; } if (op === "&&" || op === "||") { const l = this.checkExpr(e.left); this.expectBool(l, e.left, `the left side of '${op}'`); const r = this.checkExpr(e.right); this.expectBool(r, e.right, `the right side of '${op}'`); return BOOL; } if (op === "==" || op === "!=") { const l = this.checkExpr(e.left); const r = this.checkExpr(e.right); this.checkComparable(l, r, e.span); return BOOL; } if (op === "<" || op === "<=" || op === ">" || op === ">=") { const l = this.checkExpr(e.left); const r = this.checkExpr(e.right); for (const [t, side] of [[l, "left"], [r, "right"]] as const) { if (t.k === "any" || t.k === "unknown") continue; if (!(t.k === "prim" && (t.name === "Int" || t.name === "Float"))) { this.assignabilityError(t, INT, e.span, `The ${side} side of '${op}' must be a number.`); } } return BOOL; } const l = this.checkExpr(e.left); const r = this.checkExpr(e.right); if (op === "+") { if (l.k === "prim" && l.name === "String") { if (r.k === "prim" && (r.name === "String" || r.name === "Char")) return STRING; if (r.k === "any" || r.k === "unknown") return STRING; this.assignabilityError(r, STRING, e.right.span, `'String + ${T.typeToString(r)}' — to build a string, use interpolation: "value: {expr}".`); return STRING; } if (r.k === "prim" && r.name === "String") { if (l.k === "prim" && l.name === "Char") return STRING; if (l.k === "any" || l.k === "unknown") return STRING; this.assignabilityError(l, STRING, e.left.span, `'${T.typeToString(l)} + String' — to build a string, use interpolation: "value: {expr}".`); return STRING; } } const num = (t: Type): boolean => t.k === "any" || t.k === "unknown" || (t.k === "prim" && (t.name === "Int" || t.name === "Float")); if (!num(l) || !num(r)) { const bad = !num(l) ? l : r; this.assignabilityError(bad, INT, e.span, `'${op}' works on numbers, but this value has type ${T.typeToString(bad)}.`); return INT; } if (l.k === "prim" && r.k === "prim" && (l.name === "Float" || r.name === "Float")) return FLOAT; return INT; } private checkComparable(l: Type, r: Type, span: Span) { if (l.k === "any" || r.k === "any" || l.k === "unknown" || r.k === "unknown") return; if (assignable(l, r) || assignable(r, l)) return; if (l.k === "optional" && r.k === "optional" && (assignable(l.inner, r.inner) || assignable(r.inner, l.inner))) return; this.error("E1001", `cannot compare ${T.typeToString(l)} with ${T.typeToString(r)}`, span, "These types are not comparable. Equality works on values of the same type."); } private checkAssignTarget(target: A.Expr) { if (target.kind === "ident") { for (let i = this.scopes.length - 1; i >= 0; i--) { const sc = this.scopes[i]!; if (sc.narrows.has(target.name)) continue; const v = sc.vars.get(target.name); if (v) { if (!v.mut) { this.error("E1008", `'${target.name}' is immutable`, target.span, "It was declared with 'let', so it cannot be reassigned.", [{ title: "Declare it mutable", code: `let mut ${target.name} = ...`, }]); } return; } } const tl = this.topLets.get(target.name); if (tl) { if (!tl.mut) this.error("E1008", `'${target.name}' is immutable`, target.span, "It was declared with 'let', so it cannot be reassigned.", [{ title: "Declare it mutable", code: `let mut ${target.name} = ...` }]); return; } this.error("E1008", `'${target.name}' cannot be assigned`, target.span, "It is not a variable in this scope."); return; } if (target.kind === "member") { const t = this.types.get(target.target); if (t?.k === "struct" && t.kind === "class") { const decl = this.decls.get(t.name); const f = decl?.classFields?.find((x) => x.name === target.name); if (f && !f.mut) this.error("E1008", `field '${target.name}' is immutable`, target.span, "Declare it 'mut' in the class to allow assignment."); return; } if (t?.k === "struct") { this.error("E1008", "struct fields are immutable", target.span, "Structs are immutable records. Rebuild the value or use a 'class' for mutable state."); return; } this.error("E1008", "this cannot be assigned", target.span); return; } if (target.kind === "index") { const t = this.types.get(target.target); if (t?.k === "list" || t?.k === "map" || t?.k === "bytes") { if (target.target.kind === "ident") { const v = this.lookupVar(target.target.name); if (v && !v.mut) this.error("E1008", `'${target.target.name}' is immutable`, target.span, "Declare it 'let mut' to mutate the collection in place."); } return; } this.error("E1008", "this cannot be assigned", target.span); return; } this.error("E1008", "this cannot be assigned", target.span); } private lookupVar(name: string): { type: Type; mut: boolean } | null { for (let i = this.scopes.length - 1; i >= 0; i--) { const v = this.scopes[i]!.vars.get(name); if (v) return v; } return this.topLets.get(name) ?? null; } private assignTargetType(target: A.Expr): Type { if (target.kind === "index") { const t = this.checkExpr(target.target); if (t.k === "map") return t.value; if (t.k === "list") return t.elem; if (t.k === "bytes") return INT; } return this.checkExpr(target); } private checkIndex(e: A.Expr): Type { if (e.kind !== "index") return { k: "unknown" }; const targetType = this.checkExpr(e.target); const idxType = this.checkExpr(e.index); if (targetType.k === "map") { if (idxType.k !== "any" && idxType.k !== "unknown" && !assignable(idxType, targetType.key)) { this.assignabilityError(idxType, targetType.key, e.index.span, `Map indexes must be ${T.typeToString(targetType.key)}.`); } return optional(targetType.value); } if (idxType.k !== "any" && idxType.k !== "unknown" && !(idxType.k === "prim" && idxType.name === "Int")) { this.assignabilityError(idxType, INT, e.index.span, "Indexes must be Int."); } switch (targetType.k) { case "list": return targetType.elem; case "tuple": { if (e.index.kind === "int") { const i = e.index.value; if (i < 0 || i >= targetType.items.length) { this.error("E1001", `index ${i} is out of bounds for this tuple`, e.span, `The tuple has ${targetType.items.length} element(s).`); return { k: "unknown" }; } return targetType.items[i]!; } this.error("E1001", "tuple indexes must be literal integers", e.span); return { k: "unknown" }; } case "prim": if (targetType.name === "String") return optional(CHAR); break; case "bytes": return INT; case "any": return ANY; default: this.error("E1001", "cannot index this value", e.span, `Its type is ${T.typeToString(targetType)}. Indexing works on List, Map, tuples, String, and Bytes.`); return { k: "unknown" }; } return { k: "unknown" }; } private checkMember(e: A.Expr): Type { if (e.kind !== "member") return { k: "unknown" }; const targetType = this.checkExpr(e.target); const t = this.memberType(targetType, e.name, e.span, e); this.types.set(e, t); return t; } private memberType(targetType: Type, name: string, span: Span, node: A.Expr): Type { if (targetType.k === "module") { const info = this.modules.get(targetType.moduleId); if (info?.kind === "native") { const item = info.exports.find((x) => x.name === name); if (item?.kind === "const") { this.resolutions.set(node, { kind: "nativeConst", name, moduleId: info.id }); return item.type ?? ANY; } if (item?.kind === "fun" && item.sig?.native) { this.resolutions.set(node, { kind: "native", fun: item.sig.native }); return this.nativeFunType(item.sig.native); } this.error("E3001", `'${name}' is not exported by '${info.name}'`, span, `It exports: ${info.exports.map((x) => x.name).join(", ")}.`); return { k: "unknown" }; } if (info?.kind === "ex") { const item = info.exports.find((x) => x.name === name); if (item?.kind === "const") { this.resolutions.set(node, { kind: "module", moduleId: info.id }); return item.type ?? ANY; } this.error("E3001", `'${name}' is not exported by '${info.name}'`, span, `It exports: ${info.exports.map((x) => x.name).join(", ")}.`); return { k: "unknown" }; } return ANY; } switch (targetType.k) { case "struct": { if (targetType.kind === "class") { const decl = this.decls.get(targetType.name); const f = decl?.classFields?.find((x) => x.name === name); if (f) { if (!f.exported) this.error("E1004", `'${name}' is private`, span, "Class fields are private by default. Add 'export' to expose it, or use a method."); this.resolutions.set(node, { kind: "classField", field: f }); return f.type; } const m = decl?.methods?.find((x) => x.name === name); if (m) { this.resolutions.set(node, { kind: "method", moduleId: this.opts.moduleId, sig: m, hasReceiver: false }); return fun(m.params.map((p) => p.type), m.ret, m.async); } this.error("E1004", `'${targetType.name}' has no member '${name}'`, span, `It has: ${[...(decl?.methods?.map((m) => m.name) ?? []), ...(decl?.classFields?.filter((f) => f.exported).map((f) => f.name) ?? [])].join(", ") || "(nothing public)"}.`); return { k: "unknown" }; } const f = targetType.fields.find((x) => x.name === name); if (f) { this.resolutions.set(node, { kind: "member", field: f }); return f.type; } this.error("E1004", `'${targetType.name}' has no field '${name}'`, span, `Its fields are: ${targetType.fields.map((x) => x.name).join(", ") || "(none)"}.`); return { k: "unknown" }; } case "enum": { const v = targetType.variants.find((x) => x === name); if (!v) { this.error("E1004", `'${name}' is not a member of enum '${targetType.name}'`, span, `Its members are: ${targetType.variants.join(", ")}.`); return { k: "unknown" }; } this.resolutions.set(node, { kind: "enumval" }); return { k: "enumval", of: targetType.name, name }; } case "union": { const v = targetType.variants.find((x) => x.name === name); if (!v) { this.error("E1004", `'${name}' is not a variant of '${targetType.name}'`, span, `Its variants are: ${targetType.variants.map((x) => x.name).join(", ")}.`); return { k: "unknown" }; } this.resolutions.set(node, { kind: "variantRef", of: targetType.name, name }); return { k: "variant", of: targetType.name, name, fields: v.fields }; } case "optional": { this.error("E1007", `'${name}' on an optional value`, span, `This value has type ${T.typeToString(targetType)} — it may be missing. Use '?.' to access members safely:`, [{ title: "Use optional access", code: `value?.${name}`, }]); return { k: "unknown" }; } case "result": { this.error("E1009", `'${name}' on a Result`, span, "This value may be a failure. Handle it first with '?', 'match', 'or', or '.require'.", [{ title: "Unwrap it", code: `value.require("...")`, }]); return { k: "unknown" }; } default: { const m = findBuiltinMethod(targetType, name); if (m) { this.resolutions.set(node, { kind: "builtinMethod", fun: m }); return this.nativeFunType(m); } this.error("E1004", `'${name}' is not a member of ${T.typeToString(targetType)}`, span, `If it's a method from a std module, import it first (e.g. 'import std.string').`); return { k: "unknown" }; } } } private nativeFunType(m: NativeFun): Type { const { typeParams, map } = freshTypeParams(m.typeParams); return fun(m.params.map((p) => instantiate(p, map)), instantiate(m.ret, map)); } // ---------- calls ---------- private checkCall(e: A.Expr): Type { if (e.kind !== "call") return { k: "unknown" }; const callee = e.callee; if (callee.kind === "member" || callee.kind === "optaccess") { const isOpt = callee.kind === "optaccess"; // type-name member calls: Int.parse, SchemaName.parse/from, Enum.Member if (callee.kind === "member" && callee.target.kind === "ident") { const tn = callee.target.name; if (tn === "Int" || tn === "Float") { const result = this.checkNativeCall(e, tn === "Int" ? INT_PARSE : FLOAT_PARSE); return isOpt ? optional(result) : result; } const decl = this.decls.get(tn); if (decl?.kind === "schema" && (callee.name === "parse" || callee.name === "from")) { return this.checkSchemaCall(e, decl, callee.name); } } const targetType = this.checkExpr(callee.target); const inner = isOpt && targetType.k === "optional" ? targetType.inner : targetType; if (isOpt && targetType.k !== "optional" && targetType.k !== "any") { this.error("E1008", "'?.' on a value that cannot be missing", callee.span, `This value has type ${T.typeToString(targetType)}.`); } const memberRes = this.resolveMemberCall(inner, callee.name, e, callee.span); let resultType: Type; switch (memberRes.kind) { case "notfound": return { k: "unknown" }; case "module": { resultType = this.checkModuleCall(memberRes.info, callee.name, e); break; } case "native": { resultType = this.checkNativeCall(e, memberRes.fun, true); break; } case "fun": { resultType = this.checkSigCall(e, memberRes.sig, memberRes.moduleId, true); break; } case "member": { if (memberRes.field.type.k === "fun") { resultType = this.checkArbitraryCall(e, memberRes.field.type); } else { this.error("E1001", `'${callee.name}' is not a function`, e.span, `It's a field of type ${T.typeToString(memberRes.field.type)}.`); for (const a of e.args) this.checkExpr(a); resultType = { k: "unknown" }; } break; } case "classField": { if (memberRes.field.type.k === "fun") { resultType = this.checkArbitraryCall(e, memberRes.field.type); } else { this.error("E1001", `'${callee.name}' is not a function`, e.span); for (const a of e.args) this.checkExpr(a); resultType = { k: "unknown" }; } break; } case "method": { resultType = this.checkSigCall(e, memberRes.sig, memberRes.moduleId, !memberRes.hasReceiver); break; } case "builtinMethod": { resultType = this.checkNativeCall(e, memberRes.fun, true); break; } case "variant": { resultType = this.checkVariantCall(e, memberRes.decl, memberRes.name); break; } } return isOpt ? optional(resultType) : resultType; } if (callee.kind === "ident") { const calleeType = this.checkExpr(callee); const res = this.resolutions.get(callee); if (res?.kind === "all" || res?.kind === "race" || res?.kind === "timeout") { return this.checkConcurrencyBuiltin(e, res.kind); } if (res?.kind === "fun") return this.checkSigCall(e, res.sig, res.moduleId, false); if (res?.kind === "native") { if (res.fun.builtin) return this.checkBuiltinResultCall(e, res.fun); return this.checkNativeCall(e, res.fun); } if (res?.kind === "struct") return this.checkStructCall(e, res.decl); if (res?.kind === "variantCtor") return this.checkVariantCall(e, res.decl, res.name); if (res?.kind === "module") { this.error("E1001", `module '${callee.name}' is not callable`, e.span, `Access its members with '.' — e.g. \`${callee.name}.someFunction(...)\`.`); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } const v = this.findVariantName(callee.name); if (v) return this.checkVariantCall(e, v.decl, callee.name); if (calleeType.k === "fun") return this.checkArbitraryCall(e, calleeType); if (calleeType.k === "any" || calleeType.k === "unknown") { for (const a of e.args) this.checkExpr(a); return ANY; } this.error("E1001", "this is not callable", e.span, `It has type ${T.typeToString(calleeType)}. Only functions are callable.`); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } const calleeType = this.checkExpr(callee); if (calleeType.k === "fun") return this.checkArbitraryCall(e, calleeType); if (calleeType.k === "any" || calleeType.k === "unknown") { for (const a of e.args) this.checkExpr(a); return ANY; } this.error("E1001", "this is not callable", e.span, `It has type ${T.typeToString(calleeType)}. Only functions are callable.`); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } private checkConcurrencyBuiltin(e: A.Expr, kind: "all" | "race" | "timeout"): Type { if (e.kind !== "call") return { k: "unknown" }; if (kind === "timeout") { if (e.args.length !== 2) { this.error("E6003", "timeout takes two arguments", e.span, "Usage: timeout(milliseconds, future)."); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } const msType = this.checkExpr(e.args[0]!); if (msType.k !== "any" && msType.k !== "unknown" && !(msType.k === "prim" && msType.name === "Int")) { this.assignabilityError(msType, INT, e.args[0]!.span, "The first argument of timeout must be milliseconds (Int)."); } return this.checkExpr(e.args[1]!); } if (kind === "race") { if (e.args.length === 0) { this.error("E6003", "race needs at least one future", e.span); return { k: "unknown" }; } const types = e.args.map((a) => this.checkExpr(a)); if (types.some((t) => t.k === "result")) { if (!types.every((t) => t.k === "result")) { this.error("E6003", "race: all futures must be fallible together", e.span, "Mixing results and plain values would let a failure be dropped."); } return result(this.commonListType(types.map((t) => (t.k === "result" ? t.inner : t)), e.span)); } return this.commonListType(types, e.span); } if (e.args.length === 0) { this.error("E6003", "all needs at least one future", e.span); return { k: "unknown" }; } const types = e.args.map((a) => this.checkExpr(a)); let t: Type; if (types.some((x) => x.k === "result")) { if (!types.every((x) => x.k === "result")) { this.error("E6003", "all: all futures must be fallible together", e.span, "Mixing results and plain values would let a failure be dropped."); } t = result(tuple(types.map((x) => (x.k === "result" ? x.inner : x)))); } else { t = tuple(types); } this.resolutions.set(e, { kind }); return t; } private checkSchemaCall(e: A.Expr, decl: TypeDeclInfo, method: "parse" | "from"): Type { if (e.kind !== "call") return { k: "unknown" }; if (e.args.length !== 1) { this.error("E1001", `${decl.name}.${method} takes exactly one argument`, e.span); for (const a of e.args) this.checkExpr(a); return result(this.declType(decl)); } const expected = method === "parse" ? STRING : ANY; const t = this.checkExpr(e.args[0]!); if (t.k !== "any" && t.k !== "unknown" && !assignable(t, expected)) { this.assignabilityError(t, expected, e.args[0]!.span, `${decl.name}.${method} expects ${T.typeToString(expected)}, but got ${T.typeToString(t)}.`); } this.resolutions.set(e, { kind: "schemaMethod", decl, method }); return result(this.declType(decl)); } private checkStructCall(e: A.Expr, decl: TypeDeclInfo): Type { if (e.kind !== "call") return { k: "unknown" }; if (decl.kind === "enum" || decl.kind === "union") return this.checkCallTypeNameError(e, decl); const fields = decl.fields!; if (e.args.length !== fields.length) { this.error("E1001", `${decl.name} expects ${fields.length} field(s) but got ${e.args.length}`, e.span, `Prefer named construction: ${decl.name} { ${fields.map((f) => f.name).join(", ")} }.`); for (const a of e.args) this.checkExpr(a); return this.declType(decl); } fields.forEach((f, i) => { const t = this.checkExpr(e.args[i]!); if (!assignable(t, f.type)) { this.assignabilityError(t, f.type, e.args[i]!.span, `Field '${f.name}' expects ${T.typeToString(f.type)}, but this value has type ${T.typeToString(t)}.`); } }); return this.declType(decl); } private checkCallTypeNameError(e: A.Expr, decl: TypeDeclInfo): Type { if (e.kind !== "call") return { k: "unknown" }; if (decl.kind === "enum") this.error("E1005", `'${decl.name}' is an enum — construct members with '${decl.name}.Member'`, e.span); else this.error("E1005", `'${decl.name}' is a union — construct a variant: '${decl.variants![0]?.name}(...)'.`, e.span); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } private checkArbitraryCall(e: A.Expr, fnType: Type): Type { if (e.kind !== "call") return { k: "unknown" }; if (fnType.k !== "fun") return { k: "unknown" }; if (e.args.length !== fnType.params.length) { this.error("E1001", `expected ${fnType.params.length} argument(s) but got ${e.args.length}`, e.span); } e.args.forEach((a, i) => { const t = this.checkExpr(a, fnType.params[i]); const p = fnType.params[i]; if (p && !assignable(t, p)) { this.assignabilityError(t, p, a.span, `Argument ${i + 1} expects ${T.typeToString(p)}, but got ${T.typeToString(t)}.`); } }); return fnType.ret; } private findVariantDecl(name: string): TypeDeclInfo | null { for (const d of this.decls.values()) { if (d.kind === "union" && d.variants!.some((v) => v.name === name)) return d; } return null; } private findVariantName(name: string): { decl: TypeDeclInfo; fields: FieldInfo[] } | null { for (const d of this.decls.values()) { if (d.kind === "union") { const v = d.variants!.find((x) => x.name === name); if (v) return { decl: d, fields: v.fields }; } } return null; } private findEnumMember(name: string): { decl: TypeDeclInfo } | null { for (const d of this.decls.values()) { if (d.kind === "enum" && d.variants!.some((v) => v.name === name)) return { decl: d }; } return null; } private resolveMemberCall( targetType: Type, name: string, node: A.Expr, span: Span, ): | { kind: "notfound" } | { kind: "module"; info: ModuleInfo } | { kind: "native"; fun: NativeFun } | { kind: "fun"; sig: FunSig; moduleId: string } | { kind: "member"; field: FieldInfo } | { kind: "classField"; field: { name: string; type: Type; mut: boolean; exported: boolean } } | { kind: "method"; sig: FunSig; moduleId: string; hasReceiver: boolean } | { kind: "builtinMethod"; fun: NativeFun } | { kind: "variant"; decl: TypeDeclInfo; name: string } { if (targetType.k === "module") { const info = this.modules.get(targetType.moduleId); if (info) return { kind: "module", info }; return { kind: "notfound" }; } if (targetType.k === "union") { const v = targetType.variants.find((x) => x.name === name); if (!v) { this.error("E1004", `'${name}' is not a variant of '${targetType.name}'`, span, `Its variants are: ${targetType.variants.map((x) => x.name).join(", ")}.`); return { kind: "notfound" }; } const decl = this.decls.get(targetType.name); if (decl) return { kind: "variant", decl, name }; return { kind: "notfound" }; } if (targetType.k === "struct") { if (targetType.kind === "class") { const decl = this.decls.get(targetType.name); const f = decl?.classFields?.find((x) => x.name === name); if (f) { if (!f.exported && f.type.k !== "fun") { this.error("E1004", `'${name}' is private`, span, "Class fields are private by default. Add 'export' to expose it, or use a method."); } return { kind: "classField", field: f }; } const m = decl?.methods?.find((x) => x.name === name); if (m) return { kind: "method", sig: m, moduleId: this.opts.moduleId, hasReceiver: false }; this.error("E1004", `'${targetType.name}' has no method '${name}'`, span, `It has: ${decl?.methods?.map((m) => m.name).join(", ") ?? ""}`); return { kind: "notfound" }; } const f = targetType.fields.find((x) => x.name === name); if (f) return { kind: "member", field: f }; const sugar = this.findMethodSugar(targetType, name); if (sugar) return sugar; this.error("E1004", `'${targetType.name}' has no field '${name}'`, span, `Its fields are: ${targetType.fields.map((x) => x.name).join(", ") || "(none)"}.`); return { kind: "notfound" }; } const builtin = findBuiltinMethod(targetType, name); if (builtin) return { kind: "builtinMethod", fun: builtin }; const sugar = this.findMethodSugar(targetType, name); if (sugar) return sugar; this.error("E1004", `'${name}' is not a method of ${T.typeToString(targetType)}`, span, `If it's a method from a std module, import it first (e.g. 'import std.string').`); return { kind: "notfound" }; } private findMethodSugar( targetType: Type, name: string, ): { kind: "fun"; sig: FunSig; moduleId: string } | null { const local = this.funs.get(name); if (local && local.params.length > 0 && this.receiverMatches(local.params[0]!.type, targetType)) { return { kind: "fun", sig: local, moduleId: this.opts.moduleId }; } for (const info of this.modules.values()) { if (info.kind === "native") { const exp = info.exports.find((x) => x.name === name && x.kind === "fun"); if (exp?.sig?.native && exp.sig.params.length > 0 && this.receiverMatches(exp.sig.params[0]!.type, targetType)) { return { kind: "fun", sig: exp.sig, moduleId: info.id }; } continue; } if (info.kind === "ex") { const exp = info.exports.find((x) => x.name === name && x.kind === "fun"); if (exp?.sig && exp.sig.params.length > 0 && this.receiverMatches(exp.sig.params[0]!.type, targetType)) { return { kind: "fun", sig: exp.sig, moduleId: info.id }; } } } return null; } private receiverMatches(paramType: Type, receiver: Type): boolean { if (paramType.k === "typeparam") return true; if (paramType.k === "list" && receiver.k === "list") return true; if (paramType.k === "map" && receiver.k === "map") return true; return assignable(receiver, paramType); } private checkModuleCall(info: ModuleInfo, name: string, e: A.Expr): Type { if (e.kind !== "call") return { k: "unknown" }; if (info.kind === "js") { for (const a of e.args) this.checkExpr(a); return ANY; } if (info.kind === "native") { const item = info.exports.find((x) => x.name === name); if (!item) { this.error("E3001", `'${name}' is not exported by '${info.name}'`, e.span, `It exports: ${info.exports.map((x) => x.name).join(", ")}.`); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } if (item.kind === "fun" && item.sig?.native) return this.checkNativeCall(e, item.sig.native); this.error("E1001", `'${name}' is a constant, not a function`, e.span); for (const a of e.args) this.checkExpr(a); return item.type ?? { k: "unknown" }; } const item = info.exports.find((x) => x.name === name); if (!item) { this.error("E3001", `'${name}' is not exported by '${info.name}'`, e.span, `It exports: ${info.exports.map((x) => x.name).join(", ")}.`); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } if (item.kind === "fun" && item.sig) return this.checkSigCall(e, item.sig, info.id, false); if (item.kind === "const") { this.error("E1001", `'${name}' is a constant, not a function`, e.span); for (const a of e.args) this.checkExpr(a); return item.type ?? { k: "unknown" }; } this.error("E1001", `'${name}' is not a function`, e.span); for (const a of e.args) this.checkExpr(a); return { k: "unknown" }; } private checkSigCall(e: A.Expr, sig: FunSig, moduleId: string, shiftReceiver: boolean): Type { if (e.kind !== "call") return { k: "unknown" }; const { typeParams, map } = freshTypeParams(sig.typeParams); const params = sig.params.map((p) => ({ name: p.name, type: instantiate(p.type, map) })); const declaredRet = instantiate(sig.ret, map); const expectedParams = shiftReceiver ? params.slice(1) : params; if (e.args.length !== expectedParams.length) { this.error("E1001", `'${sig.name}' expects ${expectedParams.length} argument(s) but got ${e.args.length}`, e.span); } const deferredLambdas: { arg: A.Expr; index: number }[] = []; e.args.forEach((a, i) => { if (a.kind === "lambda") { deferredLambdas.push({ arg: a, index: i }); return; } const expected = expectedParams[i]?.type; const t = this.checkExpr(a, expected); if (expected && !assignable(t, expected)) { this.assignabilityError(t, expected, a.span, `Argument ${i + 1} ('${sig.params[i + (shiftReceiver ? 1 : 0)]?.name ?? "?"}') expects ${T.typeToString(expected)}, but got ${T.typeToString(t)}.`); } if (expected) unify(expected, t, map); }); for (const { arg, index } of deferredLambdas) { const expected = instantiate(expectedParams[index]?.type ?? ANY, map); const lt = this.checkExpr(arg, expected); if (expected) unify(expected, lt, map); } const ret = instantiate(declaredRet, map); this.resolutions.set(e, { kind: "fun", moduleId, sig }); return ret; } private checkNativeCall(e: A.Expr, fun: NativeFun, shiftReceiver = false): Type { if (e.kind !== "call") return { k: "unknown" }; const { typeParams, map } = freshTypeParams(fun.typeParams); const allParams = fun.params.map((p) => instantiate(p, map)); const expectedParams = shiftReceiver ? allParams.slice(1) : allParams; const declaredRet = instantiate(fun.ret, map); if (e.args.length !== expectedParams.length) { this.error("E1001", `'${fun.name}' expects ${expectedParams.length} argument(s) but got ${e.args.length}`, e.span); } const deferredLambdas: { arg: A.Expr; index: number }[] = []; e.args.forEach((a, i) => { if (a.kind === "lambda") { deferredLambdas.push({ arg: a, index: i }); return; } const expected = expectedParams[i]; const t = this.checkExpr(a, expected); if (expected && !assignable(t, expected)) { this.assignabilityError(t, expected, a.span, `Argument ${i + 1} expects ${T.typeToString(expected)}, but got ${T.typeToString(t)}.`); } if (expected) unify(expected, t, map); }); for (const { arg, index } of deferredLambdas) { const expected = instantiate(expectedParams[index] ?? ANY, map); const lt = this.checkExpr(arg, expected); if (expected) unify(expected, lt, map); } const ret = instantiate(declaredRet, map); this.resolutions.set(e, { kind: "native", fun, shiftReceiver }); return ret; } private checkBuiltinResultCall(e: A.Expr, fun: NativeFun): Type { if (e.kind !== "call") return { k: "unknown" }; if (e.args.length !== 1) { this.error("E1001", `'${fun.name}' expects 1 argument but got ${e.args.length}`, e.span); } const argType = e.args[0] ? this.checkExpr(e.args[0]) : ({ k: "unknown" } as Type); const ret = fun.builtin === "ok" ? result(argType) : result(ERR); this.resolutions.set(e, { kind: "native", fun, shiftReceiver: false }); return ret; } private checkVariantCall(e: A.Expr, decl: TypeDeclInfo, name: string): Type { if (e.kind !== "call") return { k: "unknown" }; const v = decl.variants!.find((x) => x.name === name); if (!v) return { k: "unknown" }; if (e.args.length !== v.fields.length) { this.error("E1001", `variant '${name}' expects ${v.fields.length} argument(s) but got ${e.args.length}`, e.span, `Its fields are: ${v.fields.map((f) => f.name).join(", ") || "(none)"}.`); } v.fields.forEach((f, i) => { const t = this.checkExpr(e.args[i]!); if (!assignable(t, f.type)) { this.assignabilityError(t, f.type, e.args[i]!.span, `Field '${f.name}' expects ${T.typeToString(f.type)}, but this value has type ${T.typeToString(t)}.`); } }); this.resolutions.set(e, { kind: "variantCtor", decl, name }); return { k: "variant", of: decl.name, name, fields: v.fields }; } private checkMatchExpr(e: A.Expr): Type { if (e.kind !== "match") return { k: "unknown" }; const valueType = this.checkExpr(e.value); this.checkPatternBindingsForArms(e.arms, valueType, e.span); let common: Type | null = null; for (const arm of e.arms) { const armType = this.checkArm(arm, valueType, e.value); common = common === null ? armType : this.commonType(common, armType, arm.span); } this.checkExhaustive(valueType, e.arms, e.span); return common ?? VOID; } // ---------- helpers ---------- private commonListType(types: Type[], span: Span): Type { let t = types[0]!; for (let i = 1; i < types.length; i++) t = this.commonType(t, types[i]!, span); return t; } private commonType(a: Type, b: Type, span: Span): Type { if (a.k === "unknown") return b; if (b.k === "unknown") return a; if (a.k === "any" || b.k === "any") return ANY; if (assignable(a, b) && assignable(b, a)) return a; if (assignable(b, a)) return a; if (assignable(a, b)) return b; if (a.k === "optional" && b.k === "optional") return optional(this.commonType(a.inner, b.inner, span)); if (a.k === "optional" && b.k !== "optional") { if (assignable(b, a.inner)) return a; return optional(this.commonType(a.inner, b, span)); } if (b.k === "optional" && a.k !== "optional") { if (assignable(a, b.inner)) return b; return optional(this.commonType(a, b.inner, span)); } if (a.k === "result" && b.k !== "result") return result(this.commonType(a.inner, b, span)); if (b.k === "result" && a.k !== "result") return result(this.commonType(a, b.inner, span)); if (a.k === "prim" && b.k === "prim" && a.name === "Int" && b.name === "Float") return FLOAT; if (a.k === "prim" && b.k === "prim" && a.name === "Float" && b.name === "Int") return FLOAT; if (a.k === "variant" && b.k === "variant" && a.of === b.of) { const decl = this.decls.get(a.of); if (decl?.kind === "union") return this.declType(decl); } if (a.k === "enumval" && b.k === "enumval" && a.of === b.of) { const decl = this.decls.get(a.of); if (decl?.kind === "enum") return this.declType(decl); } if (a.k === "variant" && b.k === "union" && b.name === a.of) return b; if (a.k === "union" && b.k === "variant" && a.name === b.of) return a; this.error("E1001", "branches have incompatible types", span, `One branch produces ${T.typeToString(a)} and another produces ${T.typeToString(b)}. Make them agree.`); return a; } private assignabilityError(actual: Type, expected: Type, span: Span, why: string) { if (actual.k === "unknown" || expected.k === "unknown") return; const fixes: { title: string; code: string }[] = []; if (actual.k === "optional") { fixes.push( { title: "Check that it exists first", code: `if let value = value { use(value) }` }, { title: "Provide a fallback", code: `value or "fallback"` }, { title: "Change the expected type to optional", code: `...: ${T.typeToString(optional(expected))}` }, ); } if (actual.k === "result") { fixes.push( { title: "Propagate the failure", code: `value?` }, { title: "Match on it", code: `match value {\n Ok(v) -> ...\n Err(e) -> ...\n }` }, { title: "Provide a fallback", code: `value or "fallback"` }, { title: "Unwrap with a checked message", code: `value.require("...")` }, ); } this.diags.push({ severity: "error", code: expected.k === "optional" ? "E1007" : expected.k === "result" ? "E1009" : "E1001", message: `cannot use a ${T.typeToString(actual)} as a ${T.typeToString(expected)}`, span, detail: why, fixes, }); } private resultDroppedError(span: Span, why: string) { this.diags.push({ severity: "error", code: "E4001", message: "a Result was dropped without being handled", span, detail: why, fixes: [ { title: "Propagate it", code: `value?` }, { title: "Handle both cases", code: `match value {\n Ok(v) -> ...\n Err(e) -> ...\n }` }, { title: "Fall back", code: `value or "fallback"` }, { title: "Unwrap (crash with a good message)", code: `value.require("...")` }, ], }); } private error(code: string, message: string, span: Span, detail?: string, fixes?: { title: string; code: string }[]) { this.diags.push({ severity: "error", code, message, span, detail, fixes }); } private suggestName(name: string): string | null { const all = new Set([...this.funs.keys(), ...this.decls.keys(), ...this.topLets.keys(), ...this.modules.keys()]); for (const s of this.scopes) for (const k of s.vars.keys()) all.add(k); let best: string | null = null; let bestDist = 3; for (const k of all) { const d = levenshtein(name, k); if (d < bestDist) { bestDist = d; best = k; } } return best; } } function levenshtein(a: string, b: string): number { const n = b.length; const dp: number[] = Array.from({ length: n + 1 }, (_, j) => j); for (let i = 1; i <= a.length; i++) { let prev = dp[0]!; dp[0] = i; for (let j = 1; j <= n; j++) { const tmp = dp[j]!; dp[j] = Math.min(dp[j]! + 1, dp[j - 1]! + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1)); prev = tmp; } } return dp[n]!; } type NativeModuleLike = NativeModule; /** convert a native stdlib module into a ModuleInfo the checker can consume */ export function moduleInfoFromNative(m: NativeModuleLike): ModuleInfo { const exports: ExportedItem[] = []; for (const [name, exp] of Object.entries(m.exports)) { if ("params" in exp && exp.ret) { exports.push({ name, kind: "fun", sig: { name, typeParams: exp.typeParams ?? [], params: (exp.params ?? []).map((p, i) => ({ name: `a${i}`, type: p })), ret: exp.ret, async: false, file: "", exported: true, native: exp as NativeFun, moduleId: m.id, }, }); } else { exports.push({ name, kind: "const", type: "type" in exp ? exp.type : ANY }); } } return { id: m.id, kind: "native", name: m.name, exports, file: "" }; } function modulePathText(ref: A.ModuleRef): string { switch (ref.kind) { case "std": return "std." + ref.segments.join("."); case "pkg": return "pkg:" + ref.name; case "js": return "js:" + ref.name; case "rel": return `"${ref.path}"`; } } function moduleAlias(ref: A.ModuleRef): string { switch (ref.kind) { case "std": return ref.segments[ref.segments.length - 1]!; case "pkg": return ref.name; case "js": return ref.name.split("/").pop()!; case "rel": { const base = ref.path.split("/").pop()!; return base.replace(/\.xan$/, ""); } } } function exprText(e: A.Expr): string { switch (e.kind) { case "ident": return e.name; case "int": case "float": return String(e.value); case "string": return `"${e.parts.map((p) => (p.kind === "text" ? p.text : "")).join("")}"`; case "char": return `'${e.value}'`; case "bool": return String(e.value); default: return "value"; } }