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