edict-lang 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/dist/ast/nodes.d.ts +248 -0
  4. package/dist/ast/nodes.d.ts.map +1 -0
  5. package/dist/ast/nodes.js +95 -0
  6. package/dist/ast/nodes.js.map +1 -0
  7. package/dist/ast/types.d.ts +78 -0
  8. package/dist/ast/types.d.ts.map +1 -0
  9. package/dist/ast/types.js +7 -0
  10. package/dist/ast/types.js.map +1 -0
  11. package/dist/check.d.ts +19 -0
  12. package/dist/check.d.ts.map +1 -0
  13. package/dist/check.js +49 -0
  14. package/dist/check.js.map +1 -0
  15. package/dist/checker/check.d.ts +7 -0
  16. package/dist/checker/check.d.ts.map +1 -0
  17. package/dist/checker/check.js +548 -0
  18. package/dist/checker/check.js.map +1 -0
  19. package/dist/checker/type-env.d.ts +24 -0
  20. package/dist/checker/type-env.d.ts.map +1 -0
  21. package/dist/checker/type-env.js +67 -0
  22. package/dist/checker/type-env.js.map +1 -0
  23. package/dist/checker/types-equal.d.ts +18 -0
  24. package/dist/checker/types-equal.d.ts.map +1 -0
  25. package/dist/checker/types-equal.js +79 -0
  26. package/dist/checker/types-equal.js.map +1 -0
  27. package/dist/codegen/builtins.d.ts +27 -0
  28. package/dist/codegen/builtins.d.ts.map +1 -0
  29. package/dist/codegen/builtins.js +54 -0
  30. package/dist/codegen/builtins.js.map +1 -0
  31. package/dist/codegen/codegen.d.ts +32 -0
  32. package/dist/codegen/codegen.d.ts.map +1 -0
  33. package/dist/codegen/codegen.js +1192 -0
  34. package/dist/codegen/codegen.js.map +1 -0
  35. package/dist/codegen/runner.d.ts +16 -0
  36. package/dist/codegen/runner.d.ts.map +1 -0
  37. package/dist/codegen/runner.js +82 -0
  38. package/dist/codegen/runner.js.map +1 -0
  39. package/dist/codegen/string-table.d.ts +35 -0
  40. package/dist/codegen/string-table.d.ts.map +1 -0
  41. package/dist/codegen/string-table.js +62 -0
  42. package/dist/codegen/string-table.js.map +1 -0
  43. package/dist/compile.d.ts +18 -0
  44. package/dist/compile.d.ts.map +1 -0
  45. package/dist/compile.js +40 -0
  46. package/dist/compile.js.map +1 -0
  47. package/dist/contracts/translate.d.ts +39 -0
  48. package/dist/contracts/translate.d.ts.map +1 -0
  49. package/dist/contracts/translate.js +372 -0
  50. package/dist/contracts/translate.js.map +1 -0
  51. package/dist/contracts/verify.d.ts +8 -0
  52. package/dist/contracts/verify.d.ts.map +1 -0
  53. package/dist/contracts/verify.js +400 -0
  54. package/dist/contracts/verify.js.map +1 -0
  55. package/dist/contracts/z3-context.d.ts +14 -0
  56. package/dist/contracts/z3-context.d.ts.map +1 -0
  57. package/dist/contracts/z3-context.js +24 -0
  58. package/dist/contracts/z3-context.js.map +1 -0
  59. package/dist/effects/call-graph.d.ts +21 -0
  60. package/dist/effects/call-graph.d.ts.map +1 -0
  61. package/dist/effects/call-graph.js +135 -0
  62. package/dist/effects/call-graph.js.map +1 -0
  63. package/dist/effects/effect-check.d.ts +13 -0
  64. package/dist/effects/effect-check.d.ts.map +1 -0
  65. package/dist/effects/effect-check.js +48 -0
  66. package/dist/effects/effect-check.js.map +1 -0
  67. package/dist/errors/structured-errors.d.ts +193 -0
  68. package/dist/errors/structured-errors.d.ts.map +1 -0
  69. package/dist/errors/structured-errors.js +96 -0
  70. package/dist/errors/structured-errors.js.map +1 -0
  71. package/dist/index.d.ts +34 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +37 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/mcp/create-server.d.ts +3 -0
  76. package/dist/mcp/create-server.d.ts.map +1 -0
  77. package/dist/mcp/create-server.js +157 -0
  78. package/dist/mcp/create-server.js.map +1 -0
  79. package/dist/mcp/handlers.d.ts +44 -0
  80. package/dist/mcp/handlers.d.ts.map +1 -0
  81. package/dist/mcp/handlers.js +112 -0
  82. package/dist/mcp/handlers.js.map +1 -0
  83. package/dist/mcp/server.d.ts +3 -0
  84. package/dist/mcp/server.d.ts.map +1 -0
  85. package/dist/mcp/server.js +129 -0
  86. package/dist/mcp/server.js.map +1 -0
  87. package/dist/resolver/levenshtein.d.ts +12 -0
  88. package/dist/resolver/levenshtein.d.ts.map +1 -0
  89. package/dist/resolver/levenshtein.js +54 -0
  90. package/dist/resolver/levenshtein.js.map +1 -0
  91. package/dist/resolver/resolve.d.ts +7 -0
  92. package/dist/resolver/resolve.d.ts.map +1 -0
  93. package/dist/resolver/resolve.js +393 -0
  94. package/dist/resolver/resolve.js.map +1 -0
  95. package/dist/resolver/scope.d.ts +34 -0
  96. package/dist/resolver/scope.d.ts.map +1 -0
  97. package/dist/resolver/scope.js +51 -0
  98. package/dist/resolver/scope.js.map +1 -0
  99. package/dist/validator/id-tracker.d.ts +14 -0
  100. package/dist/validator/id-tracker.d.ts.map +1 -0
  101. package/dist/validator/id-tracker.js +28 -0
  102. package/dist/validator/id-tracker.js.map +1 -0
  103. package/dist/validator/node-validators.d.ts +6 -0
  104. package/dist/validator/node-validators.d.ts.map +1 -0
  105. package/dist/validator/node-validators.js +808 -0
  106. package/dist/validator/node-validators.js.map +1 -0
  107. package/dist/validator/validate.d.ts +17 -0
  108. package/dist/validator/validate.d.ts.map +1 -0
  109. package/dist/validator/validate.js +27 -0
  110. package/dist/validator/validate.js.map +1 -0
  111. package/package.json +75 -0
@@ -0,0 +1,1192 @@
1
+ // =============================================================================
2
+ // WASM Code Generator — compile(module) → CompileResult
3
+ // =============================================================================
4
+ // Transforms a validated Edict module AST into WASM bytecode via binaryen.
5
+ // Handles: Int/Float/Bool/String literals, binops, unops, calls, if/else,
6
+ // let bindings, blocks, and the `print` builtin.
7
+ import binaryen from "binaryen";
8
+ import { StringTable } from "./string-table.js";
9
+ import { BUILTIN_FUNCTIONS } from "./builtins.js";
10
+ // =============================================================================
11
+ // Edict → WASM type mapping
12
+ // =============================================================================
13
+ function edictTypeToWasm(type) {
14
+ if (type.kind === "basic") {
15
+ switch (type.name) {
16
+ case "Int":
17
+ return binaryen.i32;
18
+ case "Float":
19
+ return binaryen.f64;
20
+ case "Bool":
21
+ return binaryen.i32;
22
+ case "String":
23
+ // Strings are (ptr, len) → we use i32 for the pointer.
24
+ // The full string is represented as two i32s, but at the ABI
25
+ // level we pass two separate i32 params. For return values
26
+ // of builtin print, we return just the ptr (i32).
27
+ return binaryen.i32;
28
+ }
29
+ }
30
+ if (type.kind === "unit_type") {
31
+ return binaryen.none;
32
+ }
33
+ // Fallback for anything else
34
+ return binaryen.i32;
35
+ }
36
+ // =============================================================================
37
+ // Compile-time WASM type inference for expressions
38
+ // =============================================================================
39
+ /**
40
+ * Infer the WASM type an expression will produce at runtime.
41
+ * Used to dispatch i32 vs f64 instructions in binops, unops, and block types.
42
+ */
43
+ function inferExprWasmType(expr, ctx, fnSigs) {
44
+ switch (expr.kind) {
45
+ case "literal": {
46
+ // If the literal has an explicit type annotation, use it
47
+ if (expr.type)
48
+ return edictTypeToWasm(expr.type);
49
+ const val = expr.value;
50
+ if (typeof val === "number" && !Number.isInteger(val))
51
+ return binaryen.f64;
52
+ return binaryen.i32; // int, bool, string → i32
53
+ }
54
+ case "ident": {
55
+ const local = ctx.getLocal(expr.name);
56
+ if (local)
57
+ return local.type;
58
+ const globalType = ctx.constGlobals.get(expr.name);
59
+ if (globalType)
60
+ return globalType;
61
+ return binaryen.i32;
62
+ }
63
+ case "binop": {
64
+ // Comparison/logical ops always return i32 (boolean)
65
+ const cmpOps = ["==", "!=", "<", ">", "<=", ">=", "and", "or", "implies"];
66
+ if (cmpOps.includes(expr.op))
67
+ return binaryen.i32;
68
+ // Arithmetic: infer from left operand
69
+ return inferExprWasmType(expr.left, ctx, fnSigs);
70
+ }
71
+ case "unop":
72
+ if (expr.op === "not")
73
+ return binaryen.i32;
74
+ return inferExprWasmType(expr.operand, ctx, fnSigs);
75
+ case "call": {
76
+ if (expr.fn.kind === "ident") {
77
+ const sig = fnSigs.get(expr.fn.name);
78
+ if (sig)
79
+ return sig.returnType;
80
+ }
81
+ return binaryen.i32;
82
+ }
83
+ case "if":
84
+ // Type of if is the type of the then branch's last expression
85
+ if (expr.then.length > 0) {
86
+ return inferExprWasmType(expr.then[expr.then.length - 1], ctx, fnSigs);
87
+ }
88
+ return binaryen.i32;
89
+ case "let":
90
+ return binaryen.none; // let is a statement (local.set), returns void
91
+ case "block":
92
+ if (expr.body.length > 0) {
93
+ return inferExprWasmType(expr.body[expr.body.length - 1], ctx, fnSigs);
94
+ }
95
+ return binaryen.none;
96
+ case "match":
97
+ // Type of match is the type of the first arm's body
98
+ if (expr.arms.length > 0 && expr.arms[0].body.length > 0) {
99
+ const firstBody = expr.arms[0].body;
100
+ return inferExprWasmType(firstBody[firstBody.length - 1], ctx, fnSigs);
101
+ }
102
+ return binaryen.i32;
103
+ case "array":
104
+ case "tuple_expr":
105
+ case "enum_constructor":
106
+ case "record_expr":
107
+ return binaryen.i32; // heap pointer
108
+ case "access": {
109
+ let recordTypeName;
110
+ if (expr.target.kind === "ident") {
111
+ const local = ctx.getLocal(expr.target.name);
112
+ if (local && local.edictTypeName) {
113
+ recordTypeName = local.edictTypeName;
114
+ }
115
+ }
116
+ else if (expr.target.kind === "record_expr") {
117
+ recordTypeName = expr.target.name;
118
+ }
119
+ if (recordTypeName) {
120
+ const layout = ctx.recordLayouts.get(recordTypeName);
121
+ if (layout) {
122
+ const fieldLayout = layout.fields.find((f) => f.name === expr.field);
123
+ if (fieldLayout)
124
+ return fieldLayout.wasmType;
125
+ }
126
+ }
127
+ return binaryen.i32; // fallback
128
+ }
129
+ default:
130
+ return binaryen.i32;
131
+ }
132
+ }
133
+ class FunctionContext {
134
+ nextIndex;
135
+ locals = new Map();
136
+ varTypes = [];
137
+ constGlobals;
138
+ recordLayouts;
139
+ enumLayouts;
140
+ constructor(params, constGlobals = new Map(), recordLayouts = new Map(), enumLayouts = new Map()) {
141
+ this.nextIndex = 0;
142
+ this.constGlobals = constGlobals;
143
+ this.recordLayouts = recordLayouts;
144
+ this.enumLayouts = enumLayouts;
145
+ for (const p of params) {
146
+ this.locals.set(p.name, { index: this.nextIndex, type: p.wasmType, edictTypeName: p.edictTypeName });
147
+ this.nextIndex++;
148
+ }
149
+ }
150
+ getLocal(name) {
151
+ return this.locals.get(name);
152
+ }
153
+ addLocal(name, type, edictTypeName) {
154
+ const index = this.nextIndex++;
155
+ this.locals.set(name, { index, type, edictTypeName });
156
+ this.varTypes.push(type);
157
+ return index;
158
+ }
159
+ }
160
+ // =============================================================================
161
+ // Compiler
162
+ // =============================================================================
163
+ export function compile(module) {
164
+ const mod = new binaryen.Module();
165
+ const strings = new StringTable();
166
+ const errors = [];
167
+ try {
168
+ // Pre-scan: intern all string literals
169
+ for (const def of module.definitions) {
170
+ if (def.kind === "fn") {
171
+ collectStrings(def.body, strings);
172
+ }
173
+ if (def.kind === "const") {
174
+ collectStringExpr(def.value, strings);
175
+ }
176
+ }
177
+ // Setup memory with string data segments
178
+ const segments = strings.toMemorySegments(mod);
179
+ const pages = Math.max(1, Math.ceil(strings.totalBytes / 65536));
180
+ mod.setMemory(pages, 16, "memory", segments);
181
+ // Build RecordLayout registry
182
+ const recordLayouts = new Map();
183
+ const enumLayouts = new Map();
184
+ for (const def of module.definitions) {
185
+ if (def.kind === "record") {
186
+ const fields = def.fields.map((f, i) => ({
187
+ name: f.name,
188
+ offset: i * 8, // 8-byte slots for all fields
189
+ wasmType: edictTypeToWasm(f.type),
190
+ }));
191
+ recordLayouts.set(def.name, { fields, totalSize: def.fields.length * 8 });
192
+ }
193
+ else if (def.kind === "enum") {
194
+ const variants = def.variants.map((v, tag) => {
195
+ const fields = v.fields.map((f, i) => ({
196
+ name: f.name,
197
+ offset: 8 + i * 8, // tag is at offset 0, fields start at 8
198
+ wasmType: edictTypeToWasm(f.type),
199
+ }));
200
+ return {
201
+ name: v.name,
202
+ tag,
203
+ fields,
204
+ totalSize: 8 + v.fields.length * 8 // tag + fields
205
+ };
206
+ });
207
+ enumLayouts.set(def.name, { variants });
208
+ }
209
+ }
210
+ // Initialize bump allocator heap pointer
211
+ // Ensure heap starts at an 8-byte aligned offset after the string table, min 8
212
+ const heapStart = Math.max(8, Math.ceil(strings.totalBytes / 8) * 8);
213
+ mod.addGlobal("__heap_ptr", binaryen.i32, true, mod.i32.const(heapStart));
214
+ // Global for passing dynamic string result lengths from host builtins
215
+ mod.addGlobal("__str_ret_len", binaryen.i32, true, mod.i32.const(0));
216
+ // Pre-scan: build function signature registry
217
+ const fnSigs = new Map();
218
+ for (const def of module.definitions) {
219
+ if (def.kind === "fn") {
220
+ fnSigs.set(def.name, {
221
+ returnType: edictTypeToWasm(def.returnType),
222
+ paramTypes: def.params.map((p) => edictTypeToWasm(p.type)),
223
+ });
224
+ }
225
+ }
226
+ // Import builtins — compute WASM-level params from Edict signatures
227
+ // Each String param becomes two i32 values (ptr, len) at the WASM level
228
+ for (const [name, builtin] of BUILTIN_FUNCTIONS) {
229
+ const [importModule, importBase] = builtin.wasmImport;
230
+ const wasmParams = [];
231
+ for (const param of builtin.type.params) {
232
+ if (param.kind === "basic" && param.name === "String") {
233
+ wasmParams.push(binaryen.i32, binaryen.i32); // ptr, len
234
+ }
235
+ else {
236
+ wasmParams.push(edictTypeToWasm(param));
237
+ }
238
+ }
239
+ mod.addFunctionImport(name, importModule, importBase, wasmParams.length > 0
240
+ ? binaryen.createType(wasmParams)
241
+ : binaryen.none, binaryen.i32);
242
+ }
243
+ // Import module-level imports as WASM host imports
244
+ // Infer param/return types from call sites in the module's functions
245
+ const importedNames = new Set();
246
+ for (const imp of module.imports) {
247
+ for (const name of imp.names) {
248
+ if (!BUILTIN_FUNCTIONS.has(name)) {
249
+ importedNames.add(name);
250
+ }
251
+ }
252
+ }
253
+ if (importedNames.size > 0) {
254
+ // Scan function bodies for calls to imported names to infer types
255
+ const importSigs = inferImportSignatures(module, importedNames);
256
+ for (const [name, sig] of importSigs) {
257
+ const imp = module.imports.find(i => i.names.includes(name));
258
+ const importModule = imp ? imp.module : "host";
259
+ mod.addFunctionImport(name, importModule, name, sig.paramTypes.length > 0
260
+ ? binaryen.createType(sig.paramTypes)
261
+ : binaryen.none, sig.returnType);
262
+ fnSigs.set(name, { returnType: sig.returnType, paramTypes: sig.paramTypes });
263
+ }
264
+ }
265
+ // Compile const definitions as WASM globals
266
+ const constGlobals = new Map();
267
+ for (const def of module.definitions) {
268
+ if (def.kind === "const") {
269
+ const wasmType = edictTypeToWasm(def.type);
270
+ // Create a temporary context for compiling the const init expression
271
+ const tmpCtx = new FunctionContext([]);
272
+ const initExpr = compileExpr(def.value, mod, tmpCtx, strings, fnSigs, errors);
273
+ mod.addGlobal(def.name, wasmType, false, initExpr);
274
+ constGlobals.set(def.name, wasmType);
275
+ }
276
+ }
277
+ // Compile each function
278
+ for (const def of module.definitions) {
279
+ if (def.kind === "fn") {
280
+ compileFunction(def, mod, strings, fnSigs, constGlobals, recordLayouts, enumLayouts, errors);
281
+ }
282
+ }
283
+ // Export the "main" function if it exists
284
+ const mainDef = module.definitions.find((d) => d.kind === "fn" && d.name === "main");
285
+ if (mainDef) {
286
+ mod.addFunctionExport("main", "main");
287
+ }
288
+ // Export getter/setter functions for globals needed by host builtins.
289
+ // Mutable globals can't be directly exported in baseline WASM,
290
+ // so we use function wrappers instead.
291
+ mod.addFunction("__get_heap_ptr", binaryen.none, binaryen.i32, [], mod.global.get("__heap_ptr", binaryen.i32));
292
+ mod.addFunctionExport("__get_heap_ptr", "__get_heap_ptr");
293
+ mod.addFunction("__set_heap_ptr", binaryen.createType([binaryen.i32]), binaryen.none, [], mod.global.set("__heap_ptr", mod.local.get(0, binaryen.i32)));
294
+ mod.addFunctionExport("__set_heap_ptr", "__set_heap_ptr");
295
+ mod.addFunction("__get_str_ret_len", binaryen.none, binaryen.i32, [], mod.global.get("__str_ret_len", binaryen.i32));
296
+ mod.addFunctionExport("__get_str_ret_len", "__get_str_ret_len");
297
+ mod.addFunction("__set_str_ret_len", binaryen.createType([binaryen.i32]), binaryen.none, [], mod.global.set("__str_ret_len", mod.local.get(0, binaryen.i32)));
298
+ mod.addFunctionExport("__set_str_ret_len", "__set_str_ret_len");
299
+ // Memory is already exported via setMemory's exportName parameter
300
+ // Validate
301
+ if (!mod.validate()) {
302
+ errors.push("binaryen validation failed");
303
+ return { ok: false, errors };
304
+ }
305
+ // Optimize
306
+ mod.optimize();
307
+ const wat = mod.emitText();
308
+ const wasm = mod.emitBinary();
309
+ return { ok: true, wasm, wat };
310
+ }
311
+ catch (e) {
312
+ errors.push(e instanceof Error ? e.message : String(e));
313
+ return { ok: false, errors };
314
+ }
315
+ finally {
316
+ mod.dispose();
317
+ }
318
+ }
319
+ // =============================================================================
320
+ // Function compilation
321
+ // =============================================================================
322
+ function compileFunction(fn, mod, strings, fnSigs, constGlobals, recordLayouts, enumLayouts, errors) {
323
+ const params = fn.params.map((p) => ({
324
+ name: p.name,
325
+ edictType: p.type,
326
+ wasmType: edictTypeToWasm(p.type),
327
+ edictTypeName: p.type.kind === "named" ? p.type.name : undefined,
328
+ }));
329
+ const ctx = new FunctionContext(params.map((p) => ({ name: p.name, wasmType: p.wasmType, edictTypeName: p.edictTypeName })), constGlobals, recordLayouts, enumLayouts);
330
+ const returnType = edictTypeToWasm(fn.returnType);
331
+ const paramTypes = params.map((p) => p.wasmType);
332
+ const paramType = paramTypes.length > 0
333
+ ? binaryen.createType(paramTypes)
334
+ : binaryen.none;
335
+ // Compile body — wrap non-final expressions in drop() per WASM semantics
336
+ const bodyExprs = fn.body.map((expr, i) => {
337
+ const compiled = compileExpr(expr, mod, ctx, strings, fnSigs, errors);
338
+ // Non-final expressions that produce values must be dropped
339
+ if (i < fn.body.length - 1 && expr.kind !== "let") {
340
+ return mod.drop(compiled);
341
+ }
342
+ return compiled;
343
+ });
344
+ let body;
345
+ if (bodyExprs.length === 0) {
346
+ body = mod.nop();
347
+ }
348
+ else if (bodyExprs.length === 1) {
349
+ body = bodyExprs[0];
350
+ }
351
+ else {
352
+ body = mod.block(null, bodyExprs, returnType);
353
+ }
354
+ mod.addFunction(fn.name, paramType, returnType, ctx.varTypes, body);
355
+ }
356
+ // =============================================================================
357
+ // Expression compilation
358
+ // =============================================================================
359
+ function compileExpr(expr, mod, ctx, strings, fnSigs, errors) {
360
+ switch (expr.kind) {
361
+ case "literal":
362
+ return compileLiteral(expr, mod, strings);
363
+ case "ident":
364
+ return compileIdent(expr, mod, ctx);
365
+ case "binop":
366
+ return compileBinop(expr, mod, ctx, strings, fnSigs, errors);
367
+ case "unop":
368
+ return compileUnop(expr, mod, ctx, strings, fnSigs, errors);
369
+ case "call":
370
+ return compileCall(expr, mod, ctx, strings, fnSigs, errors);
371
+ case "if":
372
+ return compileIf(expr, mod, ctx, strings, fnSigs, errors);
373
+ case "let":
374
+ return compileLet(expr, mod, ctx, strings, fnSigs, errors);
375
+ case "block":
376
+ return compileBlock(expr, mod, ctx, strings, fnSigs, errors);
377
+ case "match":
378
+ return compileMatch(expr, mod, ctx, strings, fnSigs, errors);
379
+ case "record_expr":
380
+ return compileRecordExpr(expr, mod, ctx, strings, fnSigs, errors);
381
+ case "tuple_expr":
382
+ return compileTupleExpr(expr, mod, ctx, strings, fnSigs, errors);
383
+ case "enum_constructor":
384
+ return compileEnumConstructor(expr, mod, ctx, strings, fnSigs, errors);
385
+ case "access":
386
+ return compileAccess(expr, mod, ctx, strings, fnSigs, errors);
387
+ case "array":
388
+ return compileArrayExpr(expr, mod, ctx, strings, fnSigs, errors);
389
+ case "lambda":
390
+ return compileLambdaExpr(expr, mod, ctx, strings, fnSigs, errors);
391
+ default:
392
+ errors.push(`unsupported expression kind: ${expr.kind}`);
393
+ return mod.unreachable();
394
+ }
395
+ }
396
+ function compileLiteral(expr, mod, strings) {
397
+ const val = expr.value;
398
+ if (typeof val === "boolean") {
399
+ return mod.i32.const(val ? 1 : 0);
400
+ }
401
+ if (typeof val === "number") {
402
+ // Check type annotation first — 0.0 is integer in JS but Float in Edict
403
+ if (expr.type && expr.type.kind === "basic" && expr.type.name === "Float") {
404
+ return mod.f64.const(val);
405
+ }
406
+ if (Number.isInteger(val)) {
407
+ return mod.i32.const(val);
408
+ }
409
+ return mod.f64.const(val);
410
+ }
411
+ if (typeof val === "string") {
412
+ const interned = strings.intern(val);
413
+ // Return the pointer (offset). The caller/callee will also need
414
+ // the length — for builtin calls we handle this specially in compileCall.
415
+ return mod.i32.const(interned.offset);
416
+ }
417
+ return mod.unreachable();
418
+ }
419
+ function compileIdent(expr, mod, ctx) {
420
+ const local = ctx.getLocal(expr.name);
421
+ if (local) {
422
+ return mod.local.get(local.index, local.type);
423
+ }
424
+ // Check module-level const globals
425
+ const globalType = ctx.constGlobals.get(expr.name);
426
+ if (globalType !== undefined) {
427
+ return mod.global.get(expr.name, globalType);
428
+ }
429
+ // Could be a function reference — return unreachable for now
430
+ return mod.unreachable();
431
+ }
432
+ function compileBinop(expr, mod, ctx, strings, fnSigs, errors) {
433
+ const left = compileExpr(expr.left, mod, ctx, strings, fnSigs, errors);
434
+ const right = compileExpr(expr.right, mod, ctx, strings, fnSigs, errors);
435
+ // Determine the WASM type from the left operand.
436
+ // Type checker guarantees matching types for both operands.
437
+ const opType = inferExprWasmType(expr.left, ctx, fnSigs);
438
+ const isFloat = opType === binaryen.f64;
439
+ switch (expr.op) {
440
+ case "+":
441
+ return isFloat ? mod.f64.add(left, right) : mod.i32.add(left, right);
442
+ case "-":
443
+ return isFloat ? mod.f64.sub(left, right) : mod.i32.sub(left, right);
444
+ case "*":
445
+ return isFloat ? mod.f64.mul(left, right) : mod.i32.mul(left, right);
446
+ case "/":
447
+ return isFloat ? mod.f64.div(left, right) : mod.i32.div_s(left, right);
448
+ case "%":
449
+ if (isFloat) {
450
+ errors.push(`modulo (%) not supported for Float`);
451
+ return mod.unreachable();
452
+ }
453
+ return mod.i32.rem_s(left, right);
454
+ case "==":
455
+ return isFloat ? mod.f64.eq(left, right) : mod.i32.eq(left, right);
456
+ case "!=":
457
+ return isFloat ? mod.f64.ne(left, right) : mod.i32.ne(left, right);
458
+ case "<":
459
+ return isFloat ? mod.f64.lt(left, right) : mod.i32.lt_s(left, right);
460
+ case ">":
461
+ return isFloat ? mod.f64.gt(left, right) : mod.i32.gt_s(left, right);
462
+ case "<=":
463
+ return isFloat ? mod.f64.le(left, right) : mod.i32.le_s(left, right);
464
+ case ">=":
465
+ return isFloat ? mod.f64.ge(left, right) : mod.i32.ge_s(left, right);
466
+ case "and":
467
+ return mod.i32.and(left, right);
468
+ case "or":
469
+ return mod.i32.or(left, right);
470
+ case "implies":
471
+ // A implies B ≡ (not A) or B
472
+ return mod.i32.or(mod.i32.eqz(left), right);
473
+ default:
474
+ errors.push(`unsupported binop: ${expr.op}`);
475
+ return mod.unreachable();
476
+ }
477
+ }
478
+ function compileUnop(expr, mod, ctx, strings, fnSigs, errors) {
479
+ const operand = compileExpr(expr.operand, mod, ctx, strings, fnSigs, errors);
480
+ const opType = inferExprWasmType(expr.operand, ctx, fnSigs);
481
+ const isFloat = opType === binaryen.f64;
482
+ switch (expr.op) {
483
+ case "-":
484
+ return isFloat
485
+ ? mod.f64.neg(operand)
486
+ : mod.i32.sub(mod.i32.const(0), operand);
487
+ case "not":
488
+ return mod.i32.eqz(operand);
489
+ default:
490
+ errors.push(`unsupported unop: ${expr.op}`);
491
+ return mod.unreachable();
492
+ }
493
+ }
494
+ function compileCall(expr, mod, ctx, strings, fnSigs, errors) {
495
+ // The fn expression should be an ident for direct calls
496
+ if (expr.fn.kind !== "ident") {
497
+ errors.push("indirect calls not yet supported");
498
+ return mod.unreachable();
499
+ }
500
+ const fnName = expr.fn.name;
501
+ const builtin = BUILTIN_FUNCTIONS.get(fnName);
502
+ // Special handling for builtins that take/return strings:
503
+ // Strings are (ptr, len) pairs at the WASM level.
504
+ if (builtin && (fnName === "print" || fnName === "string_replace")) {
505
+ const wasmArgs = [];
506
+ for (const arg of expr.args) {
507
+ if (arg.kind === "literal" && typeof arg.value === "string") {
508
+ // String literal — ptr and len known at compile time
509
+ const interned = strings.intern(arg.value);
510
+ wasmArgs.push(mod.i32.const(interned.offset));
511
+ wasmArgs.push(mod.i32.const(interned.length));
512
+ }
513
+ else {
514
+ // Non-literal string arg — compile to get ptr,
515
+ // read __str_ret_len for the length
516
+ const ptrExpr = compileExpr(arg, mod, ctx, strings, fnSigs, errors);
517
+ wasmArgs.push(ptrExpr);
518
+ wasmArgs.push(mod.global.get("__str_ret_len", binaryen.i32));
519
+ }
520
+ }
521
+ return mod.call(fnName, wasmArgs, binaryen.i32);
522
+ }
523
+ // Generic function call
524
+ const args = expr.args.map((a, i) => {
525
+ const compiled = compileExpr(a, mod, ctx, strings, fnSigs, errors);
526
+ // Coerce i32→f64 if function expects f64 but arg infers to i32
527
+ const sig = fnSigs.get(fnName);
528
+ if (sig?.paramTypes && sig.paramTypes[i] === binaryen.f64) {
529
+ const argType = inferExprWasmType(a, ctx, fnSigs);
530
+ if (argType === binaryen.i32) {
531
+ return mod.f64.convert_s.i32(compiled);
532
+ }
533
+ }
534
+ return compiled;
535
+ });
536
+ // Look up signature for correct return type
537
+ const sig = fnSigs.get(fnName);
538
+ const returnType = sig ? sig.returnType : binaryen.i32;
539
+ return mod.call(fnName, args, returnType);
540
+ }
541
+ function compileIf(expr, mod, ctx, strings, fnSigs, errors) {
542
+ const cond = compileExpr(expr.condition, mod, ctx, strings, fnSigs, errors);
543
+ // Infer the result type from the then-branch's last expression
544
+ const resultType = expr.then.length > 0
545
+ ? inferExprWasmType(expr.then[expr.then.length - 1], ctx, fnSigs)
546
+ : binaryen.i32;
547
+ const thenExprs = expr.then.map((e) => compileExpr(e, mod, ctx, strings, fnSigs, errors));
548
+ const thenBody = thenExprs.length === 1
549
+ ? thenExprs[0]
550
+ : mod.block(null, thenExprs, resultType);
551
+ if (expr.else) {
552
+ const elseExprs = expr.else.map((e) => compileExpr(e, mod, ctx, strings, fnSigs, errors));
553
+ const elseBody = elseExprs.length === 1
554
+ ? elseExprs[0]
555
+ : mod.block(null, elseExprs, resultType);
556
+ return mod.if(cond, thenBody, elseBody);
557
+ }
558
+ return mod.if(cond, thenBody);
559
+ }
560
+ function compileLet(expr, mod, ctx, strings, fnSigs, errors) {
561
+ const wasmType = expr.type
562
+ ? edictTypeToWasm(expr.type)
563
+ : inferExprWasmType(expr.value, ctx, fnSigs);
564
+ let edictTypeName;
565
+ if (expr.type && expr.type.kind === "named") {
566
+ edictTypeName = expr.type.name;
567
+ }
568
+ else if (expr.value.kind === "record_expr") {
569
+ edictTypeName = expr.value.name;
570
+ }
571
+ else if (expr.value.kind === "enum_constructor") {
572
+ edictTypeName = expr.value.enumName;
573
+ }
574
+ const index = ctx.addLocal(expr.name, wasmType, edictTypeName);
575
+ const value = compileExpr(expr.value, mod, ctx, strings, fnSigs, errors);
576
+ const localSet = mod.local.set(index, value);
577
+ // For String-type let bindings from literals, also set __str_ret_len
578
+ // so downstream string builtins can read the correct length.
579
+ // For calls to string-returning builtins, __str_ret_len is already set by the host.
580
+ const isStringType = expr.type?.kind === "basic" && expr.type.name === "String";
581
+ if (isStringType && expr.value.kind === "literal" && typeof expr.value.value === "string") {
582
+ const interned = strings.intern(expr.value.value);
583
+ return mod.block(null, [
584
+ localSet,
585
+ mod.global.set("__str_ret_len", mod.i32.const(interned.length)),
586
+ ], binaryen.none);
587
+ }
588
+ return localSet;
589
+ }
590
+ function compileBlock(expr, mod, ctx, strings, fnSigs, errors) {
591
+ const bodyExprs = expr.body.map((e) => compileExpr(e, mod, ctx, strings, fnSigs, errors));
592
+ if (bodyExprs.length === 0)
593
+ return mod.nop();
594
+ if (bodyExprs.length === 1)
595
+ return bodyExprs[0];
596
+ const blockType = inferExprWasmType(expr.body[expr.body.length - 1], ctx, fnSigs);
597
+ return mod.block(null, bodyExprs, blockType);
598
+ }
599
+ function compileMatch(expr, mod, ctx, strings, fnSigs, errors) {
600
+ // Attempt to determine the Edict type name of the target for enum matching
601
+ let targetEdictTypeName;
602
+ if (expr.target.kind === "ident") {
603
+ const local = ctx.getLocal(expr.target.name);
604
+ targetEdictTypeName = local?.edictTypeName;
605
+ }
606
+ else if (expr.target.kind === "call") {
607
+ // Can't easily infer return named type yet without a type env here,
608
+ // but let's be pragmatic if it's annotated
609
+ }
610
+ else if ("type" in expr.target && expr.target.type && expr.target.type.kind === "named") {
611
+ targetEdictTypeName = expr.target.type.name;
612
+ }
613
+ // Infer the target and result types
614
+ const targetType = inferExprWasmType(expr.target, ctx, fnSigs);
615
+ const matchResultType = inferExprWasmType(expr, ctx, fnSigs);
616
+ // Evaluate target once and store in a temporary local
617
+ const targetExpr = compileExpr(expr.target, mod, ctx, strings, fnSigs, errors);
618
+ const tmpIndex = ctx.addLocal(`__match_${expr.id}`, targetType);
619
+ const setTarget = mod.local.set(tmpIndex, targetExpr);
620
+ const getTarget = () => mod.local.get(tmpIndex, targetType);
621
+ // Compile body of a match arm (list of expressions → single expression)
622
+ function compileArmBody(body) {
623
+ const compiled = body.map((e) => compileExpr(e, mod, ctx, strings, fnSigs, errors));
624
+ if (compiled.length === 0)
625
+ return mod.nop();
626
+ if (compiled.length === 1)
627
+ return compiled[0];
628
+ const bodyType = body.length > 0
629
+ ? inferExprWasmType(body[body.length - 1], ctx, fnSigs)
630
+ : binaryen.i32;
631
+ return mod.block(null, compiled, bodyType);
632
+ }
633
+ // Build condition for a pattern match against the target
634
+ function compilePatternCondition(pattern) {
635
+ switch (pattern.kind) {
636
+ case "literal_pattern": {
637
+ const val = pattern.value;
638
+ if (typeof val === "number" && Number.isInteger(val)) {
639
+ return mod.i32.eq(getTarget(), mod.i32.const(val));
640
+ }
641
+ if (typeof val === "boolean") {
642
+ return mod.i32.eq(getTarget(), mod.i32.const(val ? 1 : 0));
643
+ }
644
+ // String/float literal patterns — compare i32 representation
645
+ if (typeof val === "number") {
646
+ // Float literal pattern — not yet supported in i32 mode
647
+ errors.push(`float literal patterns not yet supported in match`);
648
+ return null;
649
+ }
650
+ if (typeof val === "string") {
651
+ const interned = strings.intern(val);
652
+ return mod.i32.eq(getTarget(), mod.i32.const(interned.offset));
653
+ }
654
+ return null;
655
+ }
656
+ case "wildcard":
657
+ return null; // always matches
658
+ case "binding":
659
+ return null; // always matches (binding is set up in compileArmWithBinding)
660
+ case "constructor": {
661
+ // Determine the tag value from the enum layout
662
+ let tagValue = -1;
663
+ if (targetEdictTypeName) {
664
+ const enumLayout = ctx.enumLayouts.get(targetEdictTypeName);
665
+ if (enumLayout) {
666
+ const variantLayout = enumLayout.variants.find(v => v.name === pattern.name);
667
+ if (variantLayout) {
668
+ tagValue = variantLayout.tag;
669
+ }
670
+ else {
671
+ errors.push(`unknown variant ${pattern.name} for enum ${targetEdictTypeName}`);
672
+ return null;
673
+ }
674
+ }
675
+ else {
676
+ errors.push(`unknown enum ${targetEdictTypeName}`);
677
+ return null;
678
+ }
679
+ }
680
+ else {
681
+ errors.push(`cannot infer enum type for match target ${expr.id}`);
682
+ return null;
683
+ }
684
+ if (tagValue === -1)
685
+ return null;
686
+ // Load tag at offset 0 from the heap pointer (target)
687
+ const loadTag = mod.i32.load(0, 0, getTarget());
688
+ return mod.i32.eq(loadTag, mod.i32.const(tagValue));
689
+ }
690
+ }
691
+ }
692
+ // Pre-register binding locals so they're available during body compilation.
693
+ // We must do this before compiling arm bodies, otherwise ident lookups
694
+ // for bound names will fail.
695
+ const bindingLocals = new Map(); // arm index → local index
696
+ const constructorFieldBindings = new Map();
697
+ for (let i = 0; i < expr.arms.length; i++) {
698
+ const pattern = expr.arms[i].pattern;
699
+ if (pattern.kind === "binding") {
700
+ const bindIndex = ctx.addLocal(pattern.name, targetType);
701
+ bindingLocals.set(i, bindIndex);
702
+ }
703
+ else if (pattern.kind === "constructor") {
704
+ if (targetEdictTypeName) {
705
+ const enumLayout = ctx.enumLayouts.get(targetEdictTypeName);
706
+ if (enumLayout) {
707
+ const variantLayout = enumLayout.variants.find(v => v.name === pattern.name);
708
+ if (variantLayout) {
709
+ const fieldBindings = [];
710
+ for (let j = 0; j < pattern.fields.length; j++) {
711
+ const subPattern = pattern.fields[j];
712
+ if (subPattern.kind === "binding") {
713
+ const fieldLayout = variantLayout.fields[j];
714
+ if (fieldLayout) {
715
+ const bindIndex = ctx.addLocal(subPattern.name, fieldLayout.wasmType);
716
+ fieldBindings.push({
717
+ localIndex: bindIndex,
718
+ offset: fieldLayout.offset,
719
+ wasmType: fieldLayout.wasmType
720
+ });
721
+ }
722
+ }
723
+ else if (subPattern.kind !== "wildcard") {
724
+ errors.push(`nested patterns inside constructor patterns not yet supported`);
725
+ }
726
+ }
727
+ constructorFieldBindings.set(i, fieldBindings);
728
+ }
729
+ }
730
+ }
731
+ }
732
+ }
733
+ // Build nested if/else chain from arms (right to left)
734
+ // Start from the last arm and work backwards
735
+ let result = mod.unreachable();
736
+ for (let i = expr.arms.length - 1; i >= 0; i--) {
737
+ const arm = expr.arms[i];
738
+ const bodyExpr = compileArmBody(arm.body);
739
+ // Wrap with binding set if this is a binding pattern
740
+ let armExpr = bodyExpr;
741
+ const bindIndex = bindingLocals.get(i);
742
+ if (bindIndex !== undefined) {
743
+ const setBinding = mod.local.set(bindIndex, getTarget());
744
+ armExpr = mod.block(null, [setBinding, bodyExpr], matchResultType);
745
+ }
746
+ else if (arm.pattern.kind === "constructor") {
747
+ const fieldBindings = constructorFieldBindings.get(i);
748
+ if (fieldBindings && fieldBindings.length > 0) {
749
+ const sets = [];
750
+ for (const binding of fieldBindings) {
751
+ const loadField = binding.wasmType === binaryen.f64
752
+ ? mod.f64.load(binding.offset, 0, getTarget())
753
+ : mod.i32.load(binding.offset, 0, getTarget());
754
+ sets.push(mod.local.set(binding.localIndex, loadField));
755
+ }
756
+ armExpr = mod.block(null, [...sets, bodyExpr], matchResultType);
757
+ }
758
+ }
759
+ const condition = compilePatternCondition(arm.pattern);
760
+ if (condition === null) {
761
+ // Wildcard or binding — this arm always matches
762
+ // It becomes the else (or the whole result if it's the only/last arm)
763
+ result = armExpr;
764
+ }
765
+ else {
766
+ // Conditional arm — if condition then this arm else previous result
767
+ result = mod.if(condition, armExpr, result);
768
+ }
769
+ }
770
+ // Wrap: set target, then evaluate the if/else chain
771
+ return mod.block(null, [setTarget, result], matchResultType);
772
+ }
773
+ function compileRecordExpr(expr, mod, ctx, strings, fnSigs, errors) {
774
+ const layout = ctx.recordLayouts.get(expr.name);
775
+ if (!layout) {
776
+ errors.push(`unknown record type: ${expr.name}`);
777
+ return mod.unreachable();
778
+ }
779
+ // Allocate heap space
780
+ // ptr = __heap_ptr
781
+ // __heap_ptr = ptr + layout.totalSize
782
+ const ptrIndex = ctx.addLocal(`__record_ptr_${expr.id}`, binaryen.i32);
783
+ const setPtr = mod.local.set(ptrIndex, mod.global.get("__heap_ptr", binaryen.i32));
784
+ const incrementHeap = mod.global.set("__heap_ptr", mod.i32.add(mod.local.get(ptrIndex, binaryen.i32), mod.i32.const(layout.totalSize)));
785
+ // Evaluate and store each field
786
+ const stores = [];
787
+ for (const fieldInit of expr.fields) {
788
+ const fieldLayout = layout.fields.find((f) => f.name === fieldInit.name);
789
+ if (!fieldLayout) {
790
+ errors.push(`unknown field '${fieldInit.name}' on record '${expr.name}'`);
791
+ continue;
792
+ }
793
+ const valueExpr = compileExpr(fieldInit.value, mod, ctx, strings, fnSigs, errors);
794
+ if (fieldLayout.wasmType === binaryen.f64) {
795
+ stores.push(mod.f64.store(fieldLayout.offset, 0, // align
796
+ mod.local.get(ptrIndex, binaryen.i32), valueExpr));
797
+ }
798
+ else {
799
+ stores.push(mod.i32.store(fieldLayout.offset, 0, // align
800
+ mod.local.get(ptrIndex, binaryen.i32), valueExpr));
801
+ }
802
+ }
803
+ // Return the pointer
804
+ const returnPtr = mod.local.get(ptrIndex, binaryen.i32);
805
+ return mod.block(null, [setPtr, incrementHeap, ...stores, returnPtr], binaryen.i32);
806
+ }
807
+ function compileTupleExpr(expr, mod, ctx, strings, fnSigs, errors) {
808
+ const totalSize = expr.elements.length * 8;
809
+ const ptrIndex = ctx.addLocal(`__tuple_ptr_${expr.id}`, binaryen.i32);
810
+ const setPtr = mod.local.set(ptrIndex, mod.global.get("__heap_ptr", binaryen.i32));
811
+ const incrementHeap = mod.global.set("__heap_ptr", mod.i32.add(mod.local.get(ptrIndex, binaryen.i32), mod.i32.const(totalSize)));
812
+ const stores = [];
813
+ for (let i = 0; i < expr.elements.length; i++) {
814
+ const elExpr = expr.elements[i];
815
+ const valWasm = compileExpr(elExpr, mod, ctx, strings, fnSigs, errors);
816
+ const valType = inferExprWasmType(elExpr, ctx, fnSigs);
817
+ const offset = i * 8;
818
+ const ptrExpr = mod.local.get(ptrIndex, binaryen.i32);
819
+ if (valType === binaryen.f64) {
820
+ stores.push(mod.f64.store(offset, 0, ptrExpr, valWasm));
821
+ }
822
+ else {
823
+ stores.push(mod.i32.store(offset, 0, ptrExpr, valWasm));
824
+ }
825
+ }
826
+ const returnPtr = mod.local.get(ptrIndex, binaryen.i32);
827
+ return mod.block(null, [setPtr, incrementHeap, ...stores, returnPtr], binaryen.i32);
828
+ }
829
+ function compileEnumConstructor(expr, mod, ctx, strings, fnSigs, errors) {
830
+ const enumLayout = ctx.enumLayouts.get(expr.enumName);
831
+ if (!enumLayout) {
832
+ errors.push(`Enum layout not found for ${expr.enumName}`);
833
+ return mod.unreachable();
834
+ }
835
+ const variantLayout = enumLayout.variants.find(v => v.name === expr.variant);
836
+ if (!variantLayout) {
837
+ errors.push(`Variant layout not found for ${expr.enumName}.${expr.variant}`);
838
+ return mod.unreachable();
839
+ }
840
+ const ptrIndex = ctx.addLocal(`__enum_ptr_${expr.id}`, binaryen.i32);
841
+ const setPtr = mod.local.set(ptrIndex, mod.global.get("__heap_ptr", binaryen.i32));
842
+ const incrementHeap = mod.global.set("__heap_ptr", mod.i32.add(mod.local.get(ptrIndex, binaryen.i32), mod.i32.const(variantLayout.totalSize)));
843
+ const stores = [];
844
+ // Store tag
845
+ const ptrExpr = mod.local.get(ptrIndex, binaryen.i32);
846
+ stores.push(mod.i32.store(0, 0, ptrExpr, mod.i32.const(variantLayout.tag)));
847
+ // Store fields
848
+ for (const fieldInit of expr.fields) {
849
+ const valWasm = compileExpr(fieldInit.value, mod, ctx, strings, fnSigs, errors);
850
+ const fieldLayout = variantLayout.fields.find(f => f.name === fieldInit.name);
851
+ if (!fieldLayout)
852
+ continue; // Should be caught by type checker
853
+ const ptrExprForField = mod.local.get(ptrIndex, binaryen.i32);
854
+ if (fieldLayout.wasmType === binaryen.f64) {
855
+ stores.push(mod.f64.store(fieldLayout.offset, 0, ptrExprForField, valWasm));
856
+ }
857
+ else {
858
+ stores.push(mod.i32.store(fieldLayout.offset, 0, ptrExprForField, valWasm));
859
+ }
860
+ }
861
+ const returnPtr = mod.local.get(ptrIndex, binaryen.i32);
862
+ return mod.block(null, [setPtr, incrementHeap, ...stores, returnPtr], binaryen.i32);
863
+ }
864
+ function compileAccess(expr, mod, ctx, strings, fnSigs, errors) {
865
+ let recordTypeName;
866
+ // Try to infer record type from target
867
+ if (expr.target.kind === "ident") {
868
+ const local = ctx.getLocal(expr.target.name);
869
+ if (local && local.edictTypeName) {
870
+ recordTypeName = local.edictTypeName;
871
+ }
872
+ }
873
+ else if (expr.target.kind === "record_expr") {
874
+ recordTypeName = expr.target.name;
875
+ }
876
+ if (!recordTypeName) {
877
+ errors.push(`cannot resolve record type for field access '${expr.field}'`);
878
+ return mod.unreachable();
879
+ }
880
+ const layout = ctx.recordLayouts.get(recordTypeName);
881
+ if (!layout) {
882
+ errors.push(`unknown record type: ${recordTypeName}`);
883
+ return mod.unreachable();
884
+ }
885
+ const fieldLayout = layout.fields.find((f) => f.name === expr.field);
886
+ if (!fieldLayout) {
887
+ errors.push(`unknown field '${expr.field}' on record '${recordTypeName}'`);
888
+ return mod.unreachable();
889
+ }
890
+ const ptrExpr = compileExpr(expr.target, mod, ctx, strings, fnSigs, errors);
891
+ if (fieldLayout.wasmType === binaryen.f64) {
892
+ return mod.f64.load(fieldLayout.offset, 0, ptrExpr);
893
+ }
894
+ else {
895
+ return mod.i32.load(fieldLayout.offset, 0, ptrExpr);
896
+ }
897
+ }
898
+ /**
899
+ * Scan function bodies for calls to imported names and infer WASM types
900
+ * from the function's declared param/return types at call sites.
901
+ */
902
+ function inferImportSignatures(module, importedNames) {
903
+ const sigs = new Map();
904
+ // Initialize with defaults
905
+ for (const name of importedNames) {
906
+ sigs.set(name, { paramTypes: [], returnType: binaryen.i32 });
907
+ }
908
+ // Multi-pass: run inference until stable (handles ordering deps like pow→sqrt)
909
+ for (let pass = 0; pass < 3; pass++) {
910
+ for (const def of module.definitions) {
911
+ if (def.kind !== "fn")
912
+ continue;
913
+ inferFromExprs(def.body, def, sigs, importedNames);
914
+ }
915
+ }
916
+ return sigs;
917
+ }
918
+ function inferFromExprs(exprs, enclosingFn, sigs, importedNames) {
919
+ for (const expr of exprs) {
920
+ inferFromExpr(expr, enclosingFn, sigs, importedNames);
921
+ }
922
+ }
923
+ function inferFromExpr(expr, enclosingFn, sigs, importedNames) {
924
+ if (expr.kind === "call" && expr.fn.kind === "ident" && importedNames.has(expr.fn.name)) {
925
+ const name = expr.fn.name;
926
+ // Infer param types from arguments
927
+ const paramTypes = expr.args.map(arg => inferTypeFromExpr(arg, enclosingFn, sigs));
928
+ // If any param is f64, promote all i32 numeric params to f64
929
+ // (JSON can't distinguish 2.0 from 2; Edict doesn't mix int/float in one function)
930
+ const hasFloat = paramTypes.some(t => t === binaryen.f64);
931
+ if (hasFloat) {
932
+ for (let j = 0; j < paramTypes.length; j++) {
933
+ if (paramTypes[j] === binaryen.i32 && expr.args[j]?.kind === "literal" &&
934
+ typeof expr.args[j].value === "number") {
935
+ paramTypes[j] = binaryen.f64;
936
+ }
937
+ }
938
+ }
939
+ // Infer return type from the enclosing function's return type
940
+ // (if this call is the last expression in the function body, it determines the return type)
941
+ const lastExprInBody = enclosingFn.body.length > 0
942
+ ? enclosingFn.body[enclosingFn.body.length - 1]
943
+ : null;
944
+ const returnType = isExprOrContains(lastExprInBody, expr)
945
+ ? edictTypeToWasm(enclosingFn.returnType)
946
+ : binaryen.i32;
947
+ sigs.set(name, { paramTypes, returnType });
948
+ }
949
+ // Recurse into sub-expressions
950
+ switch (expr.kind) {
951
+ case "binop":
952
+ inferFromExpr(expr.left, enclosingFn, sigs, importedNames);
953
+ inferFromExpr(expr.right, enclosingFn, sigs, importedNames);
954
+ break;
955
+ case "unop":
956
+ inferFromExpr(expr.operand, enclosingFn, sigs, importedNames);
957
+ break;
958
+ case "call":
959
+ inferFromExpr(expr.fn, enclosingFn, sigs, importedNames);
960
+ for (const a of expr.args)
961
+ inferFromExpr(a, enclosingFn, sigs, importedNames);
962
+ break;
963
+ case "if":
964
+ inferFromExpr(expr.condition, enclosingFn, sigs, importedNames);
965
+ inferFromExprs(expr.then, enclosingFn, sigs, importedNames);
966
+ if (expr.else)
967
+ inferFromExprs(expr.else, enclosingFn, sigs, importedNames);
968
+ break;
969
+ case "let":
970
+ inferFromExpr(expr.value, enclosingFn, sigs, importedNames);
971
+ break;
972
+ case "block":
973
+ inferFromExprs(expr.body, enclosingFn, sigs, importedNames);
974
+ break;
975
+ case "match":
976
+ inferFromExpr(expr.target, enclosingFn, sigs, importedNames);
977
+ for (const arm of expr.arms)
978
+ inferFromExprs(arm.body, enclosingFn, sigs, importedNames);
979
+ break;
980
+ case "lambda":
981
+ inferFromExprs(expr.body, enclosingFn, sigs, importedNames);
982
+ break;
983
+ case "array":
984
+ for (const el of expr.elements)
985
+ inferFromExpr(el, enclosingFn, sigs, importedNames);
986
+ break;
987
+ case "record_expr":
988
+ for (const f of expr.fields)
989
+ inferFromExpr(f.value, enclosingFn, sigs, importedNames);
990
+ break;
991
+ case "access":
992
+ inferFromExpr(expr.target, enclosingFn, sigs, importedNames);
993
+ break;
994
+ default: break;
995
+ }
996
+ }
997
+ /**
998
+ * Infer the WASM type of an expression from its AST structure.
999
+ * Used during import signature inference (before we have a FunctionContext).
1000
+ */
1001
+ function inferTypeFromExpr(expr, enclosingFn, sigs) {
1002
+ if (expr.kind === "literal") {
1003
+ if (expr.type)
1004
+ return edictTypeToWasm(expr.type);
1005
+ if (typeof expr.value === "number" && !Number.isInteger(expr.value))
1006
+ return binaryen.f64;
1007
+ return binaryen.i32;
1008
+ }
1009
+ if (expr.kind === "ident") {
1010
+ const param = enclosingFn.params.find(p => p.name === expr.name);
1011
+ if (param)
1012
+ return edictTypeToWasm(param.type);
1013
+ return binaryen.i32;
1014
+ }
1015
+ if (expr.kind === "binop") {
1016
+ // Arithmetic result type follows left operand
1017
+ const cmpOps = ["==", "!=", "<", ">", "<=", ">=", "and", "or", "implies"];
1018
+ if (cmpOps.includes(expr.op))
1019
+ return binaryen.i32;
1020
+ return inferTypeFromExpr(expr.left, enclosingFn, sigs);
1021
+ }
1022
+ if (expr.kind === "call" && expr.fn.kind === "ident") {
1023
+ // Check inferred import sigs first, then fn defs
1024
+ if (sigs?.has(expr.fn.name)) {
1025
+ return sigs.get(expr.fn.name).returnType;
1026
+ }
1027
+ // Check enclosing module's function definitions
1028
+ return binaryen.i32;
1029
+ }
1030
+ return binaryen.i32;
1031
+ }
1032
+ /**
1033
+ * Check if target expression is or contains the needle (by reference).
1034
+ */
1035
+ function isExprOrContains(target, needle) {
1036
+ if (!target)
1037
+ return false;
1038
+ if (target === needle)
1039
+ return true;
1040
+ switch (target.kind) {
1041
+ case "call": return target.args.some(a => isExprOrContains(a, needle)) || isExprOrContains(target.fn, needle);
1042
+ case "binop": return isExprOrContains(target.left, needle) || isExprOrContains(target.right, needle);
1043
+ case "unop": return isExprOrContains(target.operand, needle);
1044
+ default: return false;
1045
+ }
1046
+ }
1047
+ // =============================================================================
1048
+ // Array expression compilation
1049
+ // =============================================================================
1050
+ function compileArrayExpr(expr, mod, ctx, strings, fnSigs, errors) {
1051
+ const elements = expr.elements;
1052
+ // Layout: [length: i32] [elem0: i32] [elem1: i32] ...
1053
+ const headerSize = 4; // i32 for length
1054
+ const elemSize = 4; // i32 per element
1055
+ const totalSize = headerSize + elements.length * elemSize;
1056
+ const ptrIndex = ctx.addLocal(`__array_ptr_${expr.id}`, binaryen.i32);
1057
+ const setPtr = mod.local.set(ptrIndex, mod.global.get("__heap_ptr", binaryen.i32));
1058
+ const incrementHeap = mod.global.set("__heap_ptr", mod.i32.add(mod.local.get(ptrIndex, binaryen.i32), mod.i32.const(totalSize)));
1059
+ // Store length at offset 0
1060
+ const storeLength = mod.i32.store(0, 0, mod.local.get(ptrIndex, binaryen.i32), mod.i32.const(elements.length));
1061
+ // Store each element
1062
+ const stores = [];
1063
+ for (let i = 0; i < elements.length; i++) {
1064
+ const valueExpr = compileExpr(elements[i], mod, ctx, strings, fnSigs, errors);
1065
+ stores.push(mod.i32.store(headerSize + i * elemSize, 0, mod.local.get(ptrIndex, binaryen.i32), valueExpr));
1066
+ }
1067
+ return mod.block(null, [
1068
+ setPtr,
1069
+ incrementHeap,
1070
+ storeLength,
1071
+ ...stores,
1072
+ mod.local.get(ptrIndex, binaryen.i32), // return pointer
1073
+ ], binaryen.i32);
1074
+ }
1075
+ // =============================================================================
1076
+ // Lambda expression compilation
1077
+ // =============================================================================
1078
+ // Counter for generating unique lambda function names
1079
+ let lambdaCounter = 0;
1080
+ function compileLambdaExpr(expr, mod, ctx, strings, fnSigs, errors) {
1081
+ // Compile as a module-level helper function with a generated name
1082
+ const lambdaName = `__lambda_${lambdaCounter++}`;
1083
+ const params = expr.params.map((p) => ({
1084
+ name: p.name,
1085
+ wasmType: edictTypeToWasm(p.type),
1086
+ }));
1087
+ const lambdaCtx = new FunctionContext(params.map(p => ({ name: p.name, wasmType: p.wasmType })), ctx.constGlobals, ctx.recordLayouts, ctx.enumLayouts);
1088
+ const paramTypes = params.map(p => p.wasmType);
1089
+ const paramType = paramTypes.length > 0
1090
+ ? binaryen.createType(paramTypes)
1091
+ : binaryen.none;
1092
+ // Infer return type from last body expression
1093
+ let returnType = binaryen.i32;
1094
+ if (expr.body.length > 0) {
1095
+ returnType = inferExprWasmType(expr.body[expr.body.length - 1], lambdaCtx, fnSigs);
1096
+ }
1097
+ // Compile body
1098
+ const bodyExprs = expr.body.map((e, i) => {
1099
+ const compiled = compileExpr(e, mod, lambdaCtx, strings, fnSigs, errors);
1100
+ if (i < expr.body.length - 1 && e.kind !== "let") {
1101
+ return mod.drop(compiled);
1102
+ }
1103
+ return compiled;
1104
+ });
1105
+ let body;
1106
+ if (bodyExprs.length === 0) {
1107
+ body = mod.nop();
1108
+ }
1109
+ else if (bodyExprs.length === 1) {
1110
+ body = bodyExprs[0];
1111
+ }
1112
+ else {
1113
+ body = mod.block(null, bodyExprs, returnType);
1114
+ }
1115
+ mod.addFunction(lambdaName, paramType, returnType, lambdaCtx.varTypes, body);
1116
+ fnSigs.set(lambdaName, { returnType, paramTypes: paramTypes });
1117
+ // Return the function index as an i32 (for indirect calls / function references)
1118
+ // For now, we add it to a table so it can be called indirectly
1119
+ // Use a simple approach: return an i32 identifier that the caller can use
1120
+ // The function is registered; callers that use it via direct name will resolve it
1121
+ return mod.i32.const(lambdaCounter - 1);
1122
+ }
1123
+ // =============================================================================
1124
+ // String literal collector (pre-scan)
1125
+ // =============================================================================
1126
+ function collectStrings(exprs, strings) {
1127
+ for (const expr of exprs) {
1128
+ collectStringExpr(expr, strings);
1129
+ }
1130
+ }
1131
+ function collectStringExpr(expr, strings) {
1132
+ switch (expr.kind) {
1133
+ case "literal":
1134
+ if (typeof expr.value === "string") {
1135
+ strings.intern(expr.value);
1136
+ }
1137
+ break;
1138
+ case "binop":
1139
+ collectStringExpr(expr.left, strings);
1140
+ collectStringExpr(expr.right, strings);
1141
+ break;
1142
+ case "unop":
1143
+ collectStringExpr(expr.operand, strings);
1144
+ break;
1145
+ case "call":
1146
+ collectStringExpr(expr.fn, strings);
1147
+ for (const arg of expr.args)
1148
+ collectStringExpr(arg, strings);
1149
+ break;
1150
+ case "if":
1151
+ collectStringExpr(expr.condition, strings);
1152
+ collectStrings(expr.then, strings);
1153
+ if (expr.else)
1154
+ collectStrings(expr.else, strings);
1155
+ break;
1156
+ case "let":
1157
+ collectStringExpr(expr.value, strings);
1158
+ break;
1159
+ case "block":
1160
+ collectStrings(expr.body, strings);
1161
+ break;
1162
+ case "match":
1163
+ collectStringExpr(expr.target, strings);
1164
+ for (const arm of expr.arms)
1165
+ collectStrings(arm.body, strings);
1166
+ break;
1167
+ case "lambda":
1168
+ collectStrings(expr.body, strings);
1169
+ break;
1170
+ case "record_expr":
1171
+ for (const field of expr.fields) {
1172
+ collectStringExpr(field.value, strings);
1173
+ }
1174
+ break;
1175
+ case "tuple_expr":
1176
+ for (const el of expr.elements) {
1177
+ collectStringExpr(el, strings);
1178
+ }
1179
+ break;
1180
+ case "enum_constructor":
1181
+ for (const field of expr.fields) {
1182
+ collectStringExpr(field.value, strings);
1183
+ }
1184
+ break;
1185
+ case "access":
1186
+ collectStringExpr(expr.target, strings);
1187
+ break;
1188
+ // ident, array, tuple_expr, enum_constructor
1189
+ // — no string literals directly
1190
+ }
1191
+ }
1192
+ //# sourceMappingURL=codegen.js.map