// Codegen: EX module graph → single ESM bundle. // Emits the $ex runtime header, then each module's functions/lets/classes/tests, // then the entry runner (main call or test runner). import type * as A from "./ast.js"; import type { CheckedModule, Resolution, FunSig, TypeDeclInfo, SchemaSpec } from "./checker.js"; import { moduleInfoFromNative } from "./checker.js"; import type { NativeFun } from "./builtins.js"; import { runtimeSource } from "./runtime.js"; export interface ModuleBundle { program: A.Program; checked: CheckedModule; } export interface CodegenOptions { /** module id of the entry module */ entry: string; /** "run": call main(); "test": run tests */ mode: "run" | "test"; /** js module id (js:name) -> import specifier; emitted as top-level dynamic imports */ jsImports?: Map; } const JS_RESERVED = new Set([ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield", "let", "static", "await", "async", "get", "set", "of", "undefined", "NaN", "Infinity", "arguments", "eval", ]); function jsName(name: string): string { return JS_RESERVED.has(name) ? name + "_" : name; } function modId(name: string): string { return name.replace(/[^A-Za-z0-9]/g, "_"); } export function emitBundle(modules: Map, opts: CodegenOptions): string { const out: string[] = []; out.push(runtimeSource()); for (const [id, bundle] of modules) { if (id === opts.entry) continue; out.push(`\n// ===== module ${id} =====\n`); out.push(new Codegen(id, bundle).emit()); } out.push(`\n// ===== module ${opts.entry} (entry) =====\n`); const entry = modules.get(opts.entry); if (!entry) throw new Error(`entry module '${opts.entry}' not found`); out.push(new Codegen(opts.entry, entry).emit()); if (opts.jsImports && opts.jsImports.size > 0) { out.push(`\n// ===== js imports =====\n`); for (const [id, spec] of opts.jsImports) { out.push(`const $ex_js_${modId(id)} = await import(${JSON.stringify(spec)});`); } } if (opts.mode === "test") { out.push(`\nawait $ex.test.main();\n`); } else { out.push(` const _exMain = ${fnRef(opts.entry, "main")}; if (typeof _exMain === "function") { const _exCode = _exMain(process.argv.slice(2)); if (_exCode && typeof _exCode.then === "function") { _exCode.then((c) => { if (typeof c === "number") process.exitCode = c; }); } else if (typeof _exCode === "number") { process.exitCode = _exCode; } } `); } return out.join("\n"); } function fnRef(moduleId: string, name: string): string { return `$m_${modId(moduleId)}_${jsName(name)}`; } class Codegen { private tmpCount = 0; constructor( private moduleId: string, private bundle: ModuleBundle, ) {} private get types() { return this.bundle.checked.types; } private get resolutions() { return this.bundle.checked.resolutions; } private get funs() { return this.bundle.checked.funs; } private get decls() { return this.bundle.checked.decls; } emit(): string { const out: string[] = []; for (const d of this.bundle.program.decls) { const s = this.genDecl(d); if (s) out.push(s); } for (const t of this.bundle.program.tests) { out.push(`$ex.test.run(${JSON.stringify(t.name)}, async () => {`); out.push(this.genBlockBody(t.body, " ").join("\n")); out.push(`});\n`); } return out.join("\n"); } private genDecl(d: A.Decl): string { switch (d.kind) { case "fun": return this.genFun(d); case "let": { const name = fnRef(this.moduleId, d.name); const mut = d.mut ? "let" : "const"; return `${mut} ${name} = ${this.genExpr(d.init)};\n`; } case "struct": case "enum": case "type": case "contract": case "schema": return ""; case "class": return this.genClass(d); } } // ---------- functions ---------- private genFun(d: A.FunDecl): string { const name = fnRef(this.moduleId, d.name); const sig = this.funs.get(d.name); const isAsync = d.async; const fallible = sig?.ret.k === "result"; this.ctxStack.push({ fallible, async: isAsync, inTry: false }); const body = this.wrapFallible(this.genFunBody(d.body, " ")); this.ctxStack.pop(); return `${isAsync ? "async " : ""}function ${name}(${d.params.map((p) => jsName(p.name)).join(", ")}) {\n${body}\n}\n`; } private genClass(d: A.ClassDecl): string { const name = fnRef(this.moduleId, d.name); const fields = d.fields.map((f) => { const init = f.init ? this.genExpr(f.init) : "undefined"; return `${f.exported ? "" : "#"}${jsName(f.name)} = ${init};`; }); const methods = d.methods.map((m) => { const sig = this.decls.get(d.name)!.methods!.find((x) => x.name === m.name)!; const isAsync = m.async; const fallible = sig.ret.k === "result"; this.ctxStack.push({ fallible, async: isAsync, inTry: false }); const body = this.wrapFallible(this.genFunBody(m.body, " ")); this.ctxStack.pop(); const mname = this.classMethodName(d, m); return `${isAsync ? "async " : ""}${mname}(${m.params.map((p) => jsName(p.name)).join(", ")}) {\n${body}\n }`; }); const wrappers = d.methods.map((m) => { const params = m.params.map((p) => jsName(p.name)).join(", "); const isAsync = m.async; return `${isAsync ? "async " : ""}function ${fnRef(this.moduleId, m.name)}(_ex_self${params ? `, ${params}` : ""}) { return _ex_self.${jsName(m.name)}(${params}); }`; }); return `class ${name} {\n ${fields.join("\n ")}\n ${methods.join("\n ")}\n}\n${wrappers.join("\n")}\n`; } private classMethodName(d: A.ClassDecl, m: A.FunDecl): string { const field = d.fields.find((f) => f.name === m.name); void field; return jsName(m.name); } private ctxStack: { fallible: boolean; async: boolean; inTry: boolean }[] = []; private get ctx() { return this.ctxStack[this.ctxStack.length - 1] ?? { fallible: false, async: false, inTry: false }; } private tmp(): string { return `_ex_t${this.tmpCount++}`; } // ---------- bodies & blocks ---------- private genFunBody(body: A.FunBody, ind: string): string { if (body.kind === "expr") { if (body.expr.kind === "raise") return `${ind}throw new $ex.Err(${this.genExpr(body.expr.value)});`; const v = this.genExpr(body.expr); const wrap = this.ctx.fallible && this.types.get(body.expr)?.k !== "result"; return `${ind}return ${wrap ? `$ex.ok(${v})` : v};`; } let stmts = body.stmts; const last = stmts[stmts.length - 1]; if (last?.kind === "expr") { stmts = [...stmts.slice(0, -1), { ...last, kind: "return", value: last.expr }]; } else if (last?.kind === "match") { const matchExpr = this.genMatchExpr(last as unknown as Extract); const wrap = this.ctx.fallible && this.types.get(last as unknown as A.Expr)?.k !== "result"; const tail = `${ind}return ${wrap ? `$ex.ok(${matchExpr})` : matchExpr};`; return `${this.genBlockBody({ ...body, stmts: stmts.slice(0, -1) }, ind).join("\n")}${stmts.length > 1 ? "\n" : ""}${tail}`; } return this.genBlockBody({ ...body, stmts }, ind).join("\n"); } private wrapFallible(body: string): string { if (!this.ctx.fallible) return body; return `try {\n${body}\n} catch (_ex_e) {\n return $ex.err($ex.errMessage(_ex_e));\n}`; } private genBlockBody(block: A.Block, ind: string): string[] { const lines: string[] = []; for (const s of block.stmts) lines.push(...this.genStmt(s, ind)); return lines; } private genStmt(s: A.Stmt, ind: string): string[] { switch (s.kind) { case "let": { const mut = s.mut ? "let" : "const"; const init = s.init ? this.genExpr(s.init) : "undefined"; return [`${ind}${mut} ${jsName(s.name)} = ${init};`]; } case "return": { const v = s.value ? this.genExpr(s.value) : "undefined"; const wrap = this.ctx.fallible && s.value && this.types.get(s.value)?.k !== "result" && !this.ctx.inTry; return [`${ind}return ${wrap ? `$ex.ok(${v})` : v};`]; } case "raise": return [`${ind}throw new $ex.Err(${this.genExpr(s.value)});`]; case "expr": return [`${ind}${this.genExpr(s.expr)};`]; case "if": return this.genIfStmt(s, ind); case "iflet": return this.genIfLet(s, ind); case "match": return this.genMatchStmt(s, ind); case "for": return this.genFor(s, ind); case "while": { const lines = [`${ind}while (${this.genExpr(s.cond)}) {`]; lines.push(...this.genBlockBody(s.body, ind + " ")); lines.push(`${ind}}`); return lines; } case "try": { this.ctxStack.push({ ...this.ctx, inTry: true }); const body = this.genBlockBody(s.body, ind + " ").join("\n"); this.ctxStack.pop(); const lines = [`${ind}try {`, body, `${ind}} catch (_ex_e) {`]; lines.push(`${ind} const ${jsName(s.catchVar)} = $ex.errMessage(_ex_e);`); lines.push(...this.genBlockBody(s.handler, ind + " ")); lines.push(`${ind}}`); return lines; } case "with": { const r = this.tmp(); const body = `${ind}${r} = ${this.genExpr(s.init)};\n${ind}try {`; const inner = this.genBlockBody(s.body, ind + " ").join("\n"); const close = `${ind}} finally { if (${r} && typeof ${r}.close === "function") ${r}.close(); }`; return [`${ind}let ${r} = null;`, body, inner, close]; } } } private isOptionalish(te: A.TypeExpr): boolean { return te.kind === "optional"; } // ---------- if ---------- private genIfStmt(s: Extract, ind: string): string[] { const lines = [`${ind}if (${this.genExpr(s.cond)}) {`]; lines.push(...this.genBlockBody(s.then, ind + " ")); lines.push(`${ind}}`); if (s.else) { lines.push(`${ind}else {`); lines.push(...this.genBlockBody(s.else, ind + " ")); lines.push(`${ind}}`); } return lines; } private genIfLet(s: Extract, ind: string): string[] { const t = this.tmp(); const lines = [`${ind}const ${t} = ${this.genExpr(s.value)};`]; const check = this.patternCheck(s.pattern, t); lines.push(`${ind}if (${check}) {`); lines.push(...this.patternBind(s.pattern, t, ind + " ")); lines.push(...this.genBlockBody(s.then, ind + " ")); lines.push(`${ind}}`); if (s.else) { lines.push(`${ind}else {`); lines.push(...this.genBlockBody(s.else, ind + " ")); lines.push(`${ind}}`); } return lines; } // ---------- match ---------- private genMatchStmt(s: Extract, ind: string): string[] { const t = this.tmp(); const lines = [`${ind}const ${t} = ${this.genExpr(s.value)};`]; s.arms.forEach((arm, i) => { const check = this.patternCheck(arm.pattern, t); lines.push(`${ind}${i === 0 ? "if" : "else if"} (${check}) {`); lines.push(...this.patternBind(arm.pattern, t, ind + " ")); const body = arm.body.kind === "block" ? this.genBlockBody(arm.body, ind + " ") : arm.body.kind === "raise" ? [`${ind} throw new $ex.Err(${this.genExpr(arm.body.value)});`] : [`${ind} ${this.genExpr(arm.body)};`]; lines.push(...body); lines.push(`${ind}}`); }); lines.push(`${ind}else {`); lines.push(`${ind} throw new Error("exhaustive match reached its fall-through — this is a compiler bug");`); lines.push(`${ind}}`); return lines; } // ---------- patterns ---------- private patternCheck(p: A.Pattern, t: string): string { switch (p.kind) { case "wild": case "ident": return "true"; case "literal": return `${t} === ${this.patternLiteral(p)}`; case "some": return `${t} !== undefined`; case "none": return `${t} === undefined`; case "ok": return `${t} !== undefined && ${t}.ok === true`; case "err": return `${t} !== undefined && ${t}.ok === false`; case "variant": { const v = this.variantFields(p.name); if (v && v.fields.length === 0) return `${t} !== undefined && ${t}.tag === ${JSON.stringify(p.name)}`; return `${t} !== undefined && ${t}.tag === ${JSON.stringify(p.name)}`; } case "is": { return this.typeCheck(t, p.type); } case "tuple": return `${t} !== undefined && Array.isArray(${t}) && ${t}.length === ${p.items.length}`; case "alt": return p.items.map((it) => this.patternCheck(it, t)).join(" || "); } } private patternLiteral(p: Extract): string { return typeof p.value === "string" ? JSON.stringify(p.value) : String(p.value); } private patternBind(p: A.Pattern, t: string, ind: string): string[] { switch (p.kind) { case "wild": case "literal": case "none": case "is": return []; case "ident": return [`${ind}const ${jsName(p.name)} = ${t};`]; case "some": return this.patternBind(p.inner, t, ind); case "ok": return this.patternBind(p.inner, `${t}.value`, ind); case "err": return p.inner ? this.patternBind(p.inner, `${t}.error`, ind) : []; case "variant": { if (!p.args) return []; const v = this.variantFields(p.name); const lines: string[] = []; p.args.forEach((a, i) => { const field = v?.fields[i]; lines.push(...this.patternBind(a, `${t}.${jsName(field ? field.name : "v" + i)}`, ind)); }); return lines; } case "tuple": return [`${ind}const [${p.items.map((_, i) => this.tmpBind(i)).join(", ")}] = ${t};`]; case "alt": return []; } } private tmpBind(i: number): string { return `_ex_b${i}`; } private variantFields(name: string): { name: string; fields: { name: string }[] } | null { for (const decl of this.decls.values()) { const v = decl.variants?.find((x) => x.name === name); if (v) return { name, fields: v.fields }; } return null; } // ---------- for ---------- private genFor(s: Extract, ind: string): string[] { const iter = this.genExpr(s.iterable); const targetType = this.types.get(s.iterable); if (targetType?.k === "map") { const lines = [`${ind}for (const [${jsName(this.patternIdent0(s.pattern))}, ${jsName(this.patternIdent1(s.pattern))}] of ${iter}) {`]; lines.push(...this.genBlockBody(s.body, ind + " ")); lines.push(`${ind}}`); return lines; } if (s.pattern.kind === "tuple") { const names = s.pattern.items.map((it) => (it.kind === "ident" ? jsName(it.name) : "_ex_b")); const lines = [`${ind}for (const [${names.join(", ")}] of ${iter}) {`]; lines.push(...this.genBlockBody(s.body, ind + " ")); lines.push(`${ind}}`); return lines; } const name = s.pattern.kind === "ident" ? jsName(s.pattern.name) : "_ex_v"; const lines = [`${ind}for (const ${name} of ${iter}) {`]; lines.push(...this.genBlockBody(s.body, ind + " ")); lines.push(`${ind}}`); return lines; } private patternIdent0(p: A.Pattern): string { if (p.kind === "tuple" && p.items[0]?.kind === "ident") return p.items[0].name; return "k"; } private patternIdent1(p: A.Pattern): string { if (p.kind === "tuple" && p.items[1]?.kind === "ident") return p.items[1].name; return "v"; } // ---------- type checks (is) ---------- private typeOfExpr(te: A.TypeExpr): string { return this.typeCheck("_ex_v", te); } private typeCheck(t: string, te: A.TypeExpr): string { switch (te.kind) { case "named": { const n = te.name; if (n === "Int") return `typeof ${t} === "number" && Number.isInteger(${t})`; if (n === "Float") return `typeof ${t} === "number"`; if (n === "String") return `typeof ${t} === "string"`; if (n === "Bool") return `typeof ${t} === "boolean"`; if (n === "Char") return `typeof ${t} === "string" && ${t}.length === 1`; if (n === "Bytes") return `${t} instanceof Uint8Array`; if (n === "Any") return `true`; if (n === "Void") return `${t} === undefined`; if (n === "Err") return `${t} instanceof $ex.Err`; if (n === "List") return `Array.isArray(${t})`; if (n === "Map") return `${t} instanceof Map`; if (n === "Set") return `${t} instanceof Set`; const decl = this.decls.get(n); if (decl?.kind === "enum") { return decl.variants!.map((v) => `${t} === ${JSON.stringify(v.name)}`).join(" || ") || "false"; } if (decl?.kind === "union") { return decl.variants!.map((v) => `${t} !== undefined && ${t}.tag === ${JSON.stringify(v.name)}`).join(" || ") || "false"; } if (decl?.kind === "struct" || decl?.kind === "schema" || decl?.kind === "class") { return `${t} !== undefined && typeof ${t} === "object" && !Array.isArray(${t})`; } return `true`; } case "optional": return `${t} === undefined || (${this.typeCheck(t, te.inner)})`; case "result": return `${t} !== undefined && typeof ${t} === "object" && (${t}.ok === true || ${t}.ok === false)`; case "list": return `Array.isArray(${t})`; case "map": return `${t} instanceof Map`; case "set": return `${t} instanceof Set`; case "tuple": return `Array.isArray(${t}) && ${t}.length === ${te.items.length}`; case "fun": return `typeof ${t} === "function"`; } } // ---------- expressions ---------- genExpr(e: A.Expr): string { switch (e.kind) { case "int": return String(e.value); case "float": return String(e.value); case "char": return JSON.stringify(e.value); case "bool": return String(e.value); case "string": { let out = ""; for (const p of e.parts) { if (p.kind === "text") out += JSON.stringify(p.text).slice(1, -1); else out += "${" + `$ex.string(${this.genExpr(p.expr)})` + "}"; } return "`" + out + "`"; } case "none": case "undefined": return "undefined"; case "some": return this.genExpr(e.value); case "ident": return this.genIdent(e); case "unary": return e.op === "not" ? `!(${this.genExpr(e.operand)})` : `-(${this.genExpr(e.operand)})`; case "binary": return this.genBinary(e); case "is": return this.typeCheck(this.genExpr(e.left), e.type); case "range": return `$ex.range(${this.genExpr(e.start)}, ${this.genExpr(e.end)}, ${e.inclusive})`; case "assign": return this.genAssign(e); case "raise": return `throw new $ex.Err(${this.genExpr(e.value)})`; case "call": return this.genCall(e); case "index": return this.genIndex(e); case "member": return this.genMember(e); case "optaccess": { const t = this.tmp(); return `(() => { const ${t} = ${this.genExpr(e.target)}; return ${t} === undefined ? undefined : ${t}.${jsName(e.name)}; })()`; } case "propagate": return this.genPropagate(e); case "if": return this.genIfExpr(e); case "match": return this.genMatchExpr(e); case "lambda": { const isAsync = e.async; this.ctxStack.push({ fallible: false, async: isAsync, inTry: false }); const body = this.genFunBody(e.body, " "); this.ctxStack.pop(); return `(${isAsync ? "async " : ""}(${e.params.map((p) => jsName(p.name)).join(", ")}) => {\n${body}\n })`; } case "await": return `await ${this.genExpr(e.target)}`; case "list": return `[${e.items.map((i) => this.genExpr(i)).join(", ")}]`; case "map": { return `new Map([${e.entries.map((en) => `[${JSON.stringify(en.key)}, ${this.genExpr(en.value)}]`).join(", ")}])`; } case "tuple": return `[${e.items.map((i) => this.genExpr(i)).join(", ")}]`; case "struct": return this.genStructLiteral(e); case "ok": return `$ex.ok(${e.value ? this.genExpr(e.value) : "undefined"})`; case "err": return `$ex.err(${this.genExpr(e.value)})`; } } private genIdent(e: Extract): string { const res = this.resolutions.get(e); if (e.name === "this") return "this"; if (!res) return jsName(e.name); switch (res.kind) { case "var": if (res.moduleId) return fnRef(res.moduleId, e.name); return jsName(e.name); case "fun": return fnRef(res.moduleId, res.sig.name); case "native": { const f = res.fun; const args = f.params.map((_, i) => `_ex_a${i}`); return `(${args.join(", ")}) => ${f.gen(args, { temp: this.tempFn(), async: this.ctx.async })}`; } case "struct": return "null"; case "variantRef": return JSON.stringify(res.name); case "enumval": return JSON.stringify(e.name); case "variantCtor": return `((${res.decl.variants!.find((v) => v.name === res.name)!.fields.map((f) => jsName(f.name)).join(", ")}) => ({ tag: ${JSON.stringify(res.name)}, ${res.decl.variants!.find((v) => v.name === res.name)!.fields.map((f) => `${jsName(f.name)}`).join(", ")} }))`; case "nativeConst": return res.name === "pi" ? "Math.PI" : res.name; case "module": { if (res.moduleId.startsWith("js/")) return `$ex_js_${modId(res.moduleId)}`; return `{ $module: ${JSON.stringify(res.moduleId)} }`; } case "all": case "race": case "timeout": return e.name; default: return jsName(e.name); } } private tempFn() { return (expr: string, fn: (name: string) => string) => { const t = this.tmp(); return `(() => { const ${t} = ${expr}; return ${fn(t)}; })()`; }; } private genBinary(e: Extract): string { const l = this.genExpr(e.left); const r = this.genExpr(e.right); switch (e.op) { case "&&": return `(${l} && ${r})`; case "||": return `(${l} || ${r})`; case "or": { const lt = this.types.get(e.left); const t = this.tmp(); if (e.right.kind === "raise") { const raiseVal = this.genExpr(e.right.value); if (lt?.k === "result") { return `(() => { const ${t} = ${l}; if (${t} && ${t}.ok === true) return ${t}.value; throw new $ex.Err(${raiseVal}); })()`; } return `(() => { const ${t} = ${l}; if (${t} !== undefined) return ${t}; throw new $ex.Err(${raiseVal}); })()`; } if (lt?.k === "result") { return `(() => { const ${t} = ${l}; return ${t} && ${t}.ok === true ? ${t}.value : ${r}; })()`; } return `(() => { const ${t} = ${l}; return ${t} !== undefined ? ${t} : ${r}; })()`; } case "+": case "-": case "*": case "%": return `(${l} ${e.op} ${r})`; case "/": { const lt = this.types.get(e.left); const rt = this.types.get(e.right); const lInt = lt?.k === "prim" && lt.name === "Int"; const rInt = rt?.k === "prim" && rt.name === "Int"; const anyT = lt?.k === "any" || rt?.k === "any"; if (!anyT && (lInt || rInt)) return `Math.trunc((${l}) / (${r}))`; return `(${l} / ${r})`; } case "==": case "!=": { const lt = this.types.get(e.left); const rt = this.types.get(e.right); const deep = this.needsDeepEq(lt) || this.needsDeepEq(rt); return deep ? `${e.op === "==" ? "$ex.eq" : "!$ex.eq"}(${l}, ${r})` : `(${l} ${e.op === "==" ? "===" : "!=="} ${r})`; } default: return `(${l} ${e.op} ${r})`; } } private needsDeepEq(t: ReturnType): boolean { if (!t) return false; switch (t.k) { case "list": case "map": case "set": case "bytes": case "struct": case "variant": case "optional": case "result": case "tuple": case "any": return true; default: return false; } } private genAssign(e: Extract): string { const v = this.genExpr(e.value); const target = e.target; if (target.kind === "index") { const tt = this.types.get(target.target); const base = this.genExpr(target.target); const idx = this.genExpr(target.index); if (tt?.k === "map") { if (e.op === "=") return `(${base}.set(${idx}, ${v}), ${base}.get(${idx}))`; const op = e.op.replace(/=/, ""); return `(${base}.set(${idx}, ${base}.get(${idx}) ${op} ${v}) , ${base}.get(${idx}))`; } if (tt?.k === "bytes") return `(${base}[${idx}] = ${v}, ${v})`; const op = e.op === "=" ? "" : e.op.replace(/=/, ""); return `(${base}[${idx}] ${op}= ${v}, ${base}[${idx}])`; } if (target.kind === "member") { const base = this.genExpr(target.target); const field = this.fieldRef(target); const op = e.op === "=" ? "" : e.op.replace(/=/, ""); return `(${base}.${field} ${op}= ${v}, ${base}.${field})`; } if (target.kind === "ident") { const res = this.resolutions.get(target); const name = res?.kind === "var" && res.moduleId ? fnRef(res.moduleId, target.name) : jsName(target.name); const op = e.op === "=" ? "" : e.op.replace(/=/, ""); return `(${name} ${op}= ${v}, ${name})`; } return v; } private fieldRef(m: Extract): string { const mt = this.types.get(m.target); const name = m.name; if (mt?.k === "struct" && mt.kind === "class") { const decl = this.decls.get(mt.name); const f = decl?.classFields?.find((x) => x.name === name); if (f && !f.exported) return `#${jsName(name)}`; } return jsName(name); } private genIndex(e: Extract): string { const base = this.genExpr(e.target); const idx = this.genExpr(e.index); const tt = this.types.get(e.target); if (tt?.k === "map") return `${base}.get(${idx})`; if (tt?.k === "prim" && tt.name === "String") return `${base}.charAt(${idx})`; return `${base}[${idx}]`; } private genMember(e: Extract): string { const res = this.resolutions.get(e); if (res?.kind === "enumval") return JSON.stringify(e.name); if (res?.kind === "variantRef") return JSON.stringify(res.name); if (res?.kind === "module") { if (res.moduleId.startsWith("js/")) return `$ex_js_${modId(res.moduleId)}`; return `{ $module: ${JSON.stringify(res.moduleId)} }`; } const base = this.genExpr(e.target); if (res?.kind === "nativeConst") return this.nativeConstGen(res); if (res?.kind === "fun") { if (res.sig.native) { const f = res.sig.native; const args = f.params.map((_, i) => `_ex_a${i}`); return `(${args.join(", ")}) => ${f.gen(args, { temp: this.tempFn(), async: this.ctx.async })}`; } return fnRef(res.moduleId, res.sig.name); } const mt = this.types.get(e.target); if (mt?.k === "struct" && mt.kind === "class") { const decl = this.decls.get(mt.name); const f = decl?.classFields?.find((x) => x.name === e.name); if (f && !f.exported) return `${base}.#${jsName(e.name)}`; } return `${base}.${jsName(e.name)}`; } private nativeConstGen(res: Extract): string { if (res.name === "pi") return "Math.PI"; return res.name; } private genPropagate(e: Extract): string { const t = this.tmp(); const expr = this.genExpr(e.target); const tt = this.types.get(e.target); if (tt?.k === "result") { if (this.ctx.inTry) { return `(() => { const ${t} = ${expr}; if (${t} && ${t}.ok !== true) throw ${t}; return ${t}.value; })()`; } return `(() => { const ${t} = ${expr}; if (${t} && ${t}.ok !== true) return ${t}; return ${t}.value; })()`; } if (tt?.k === "optional") { if (this.ctx.inTry) { return `(() => { const ${t} = ${expr}; if (${t} === undefined) throw $ex.err("expected a value but found none"); return ${t}; })()`; } return `(() => { const ${t} = ${expr}; if (${t} === undefined) return $ex.err("expected a value but found none"); return ${t}; })()`; } if (tt?.k === "any") { return `(() => { const ${t} = ${expr}; if (${t} && ${t}.ok === true) return ${t}.value; if (${t} && ${t}.ok === false) { if (${this.ctx.inTry ? "true" : "false"}) throw ${t}; return ${t}; } return ${t}; })()`; } return expr; } private genIfExpr(e: Extract): string { const cond = this.genExpr(e.cond); const then = `(() => {\n${this.genBlockBody(e.then, " ").join("\n")}\n })()`; const elseNode = e.else; const els = elseNode === null ? "undefined" : elseNode.kind === "block" ? `(() => {\n${this.genBlockBody(elseNode, " ").join("\n")}\n })()` : this.genExpr(elseNode); return `(${cond} ? ${then} : ${els})`; } private genMatchExpr(e: Extract): string { const t = this.tmp(); const lines = [`(() => {`, ` const ${t} = ${this.genExpr(e.value)};`]; for (const arm of e.arms) { lines.push(` if (${this.patternCheck(arm.pattern, t)}) {`); lines.push(...this.patternBind(arm.pattern, t, " ").map((l) => ` ${l}`)); if (arm.body.kind === "block") { lines.push(...this.genBlockBody(arm.body, " ")); } else if (arm.body.kind === "raise") { lines.push(` throw new $ex.Err(${this.genExpr(arm.body.value)});`); } else { lines.push(` return ${this.genExpr(arm.body)};`); } lines.push(` }`); } lines.push(` throw new Error("exhaustive match reached its fall-through — this is a compiler bug");`); lines.push(`})()`); return lines.join("\n"); } private genCall(e: Extract): string { const res = this.resolutions.get(e); if (res?.kind === "fun") { const args = this.callArgs(e, res); return `${fnRef(res.moduleId, res.sig.name)}(${args.join(", ")})`; } if (res?.kind === "native") { const args = this.nativeArgs(e, res.fun, res.shiftReceiver === true); return res.fun.gen(args, { temp: this.tempFn(), async: this.ctx.async }); } if (res?.kind === "variantCtor") { const v = res.decl.variants!.find((x) => x.name === res.name)!; const props = v.fields.map((f, i) => `${jsName(f.name)}: ${this.genExpr(e.args[i]!)}`).join(", "); return `({ tag: ${JSON.stringify(res.name)}${props ? `, ${props}` : ""} })`; } if (res?.kind === "struct") { const fields = res.decl.fields!; const props = fields.map((f, i) => `${jsName(f.name)}: ${this.genExpr(e.args[i]!)}`).join(", "); return `({ ${props} })`; } if (res?.kind === "schemaMethod") { const spec = this.schemaLiteral(res.decl.schemaSpec!); const arg = this.genExpr(e.args[0]!); return res.method === "parse" ? `$ex.schemaParse(${spec}, ${arg})` : `$ex.schemaFrom(${spec}, ${arg})`; } if (res?.kind === "all" || res?.kind === "race") { const args = e.args.map((a) => this.genExpr(a)); const ret = this.types.get(e); const fallible = ret?.k === "result"; if (!fallible) { const kw = res.kind === "all" ? "Promise.all" : "Promise.race"; return `${kw}([${args.join(", ")}])`; } return `$ex.${res.kind}([${args.join(", ")}])`; } if (res?.kind === "timeout") { const ms = this.genExpr(e.args[0]!); const fut = this.genExpr(e.args[1]!); return `$ex.timeout(${ms}, ${fut})`; } // arbitrary call const callee = this.genExpr(e.callee); return `${callee}(${e.args.map((a) => this.genExpr(a)).join(", ")})`; } private callArgs(e: Extract, res: Extract): string[] { const callee = e.callee; if (callee.kind === "member" && callee.target.kind === "ident") { const targetRes = this.resolutions.get(callee.target); if (targetRes?.kind !== "module") { return [this.genExpr(callee.target), ...e.args.map((a) => this.genExpr(a))]; } } return e.args.map((a) => this.genExpr(a)); } private nativeArgs(e: Extract, fun: NativeFun, shiftReceiver: boolean): string[] { if (shiftReceiver && e.callee.kind === "member") { return [this.genExpr(e.callee.target), ...e.args.map((a) => this.genExpr(a))]; } return e.args.map((a) => this.genExpr(a)); } private genStructLiteral(e: Extract): string { const decl = this.decls.get(e.name); if (decl?.kind === "class") { const props = e.entries.map((en) => `${jsName(en.name)}: ${this.genExpr(en.value)}`).join(", "); return `Object.assign(new ${fnRef(this.moduleId, e.name)}(), { ${props} })`; } if (e.positional) { const decl = this.decls.get(e.name); const fields = decl?.fields ?? []; const props = e.entries.map((en, i) => `${jsName(fields[i]?.name ?? en.name)}: ${this.genExpr(en.value)}`).join(", "); return `({ ${props} })`; } const props = e.entries.map((en) => `${jsName(en.name)}: ${this.genExpr(en.value)}`).join(", "); return `({ ${props} })`; } private schemaLiteral(spec: SchemaSpec): string { const fields = spec.fields .map((f) => { const type = this.schemaTypeLiteral(f.type); const def = f.default !== undefined ? `, default: ${JSON.stringify(f.default)}` : ""; return `{ name: ${JSON.stringify(f.name)}, type: ${type}, required: ${f.required}${def} }`; }) .join(", "); return `$ex.schema([${fields}])`; } private schemaTypeLiteral(t: SchemaSpec["fields"][number]["type"]): string { switch (t.k) { case "str": return `{ k: "str"${t.minLen !== undefined ? `, minLen: ${t.minLen}` : ""}${t.maxLen !== undefined ? `, maxLen: ${t.maxLen}` : ""}${t.pattern !== undefined ? `, pattern: ${JSON.stringify(t.pattern)}` : ""} }`; case "int": return `{ k: "int"${t.min !== undefined ? `, min: ${t.min}` : ""}${t.max !== undefined ? `, max: ${t.max}` : ""} }`; case "float": return `{ k: "float"${t.min !== undefined ? `, min: ${t.min}` : ""}${t.max !== undefined ? `, max: ${t.max}` : ""} }`; case "bool": return `{ k: "bool" }`; case "bytes": return `{ k: "bytes" }`; case "opt": return `{ k: "opt", inner: ${this.schemaTypeLiteral(t.inner)} }`; case "list": return `{ k: "list", elem: ${this.schemaTypeLiteral(t.elem)} }`; case "any": return `{ k: "any" }`; case "format": return `{ k: "format", name: ${JSON.stringify(t.name)} }`; case "enum": return `{ k: "enum", variants: [${t.variants.map((v) => JSON.stringify(v)).join(", ")}] }`; case "ref": if (Array.isArray(t.fields)) { return `{ k: "ref", name: ${JSON.stringify(t.name)}, fields: [${(t.fields as { name: string; type: SchemaSpec["fields"][number]["type"] }[]).map((f) => `{ name: ${JSON.stringify(f.name)}, type: ${this.schemaTypeLiteral(f.type)} }`).join(", ")}] }`; } return `{ k: "ref", name: ${JSON.stringify(t.name)}, fields: [${(t.fields as string[]).map((v) => JSON.stringify(v)).join(", ")}] }`; } } }