edict-lang 1.2.0 → 1.6.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.
- package/README.md +39 -13
- package/dist/ast/nodes.d.ts +4 -3
- package/dist/ast/nodes.d.ts.map +1 -1
- package/dist/ast/nodes.js +1 -0
- package/dist/ast/nodes.js.map +1 -1
- package/dist/ast/type-constants.d.ts +12 -0
- package/dist/ast/type-constants.d.ts.map +1 -0
- package/dist/ast/type-constants.js +16 -0
- package/dist/ast/type-constants.js.map +1 -0
- package/dist/ast/types.d.ts +1 -1
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/builtins/builtin-enums.d.ts +12 -0
- package/dist/builtins/builtin-enums.d.ts.map +1 -0
- package/dist/builtins/builtin-enums.js +45 -0
- package/dist/builtins/builtin-enums.js.map +1 -0
- package/dist/builtins/builtin-types.d.ts +20 -0
- package/dist/builtins/builtin-types.d.ts.map +1 -0
- package/dist/builtins/builtin-types.js +6 -0
- package/dist/builtins/builtin-types.js.map +1 -0
- package/dist/builtins/builtins.d.ts +3 -0
- package/dist/builtins/builtins.d.ts.map +1 -0
- package/dist/builtins/builtins.js +3 -0
- package/dist/builtins/builtins.js.map +1 -0
- package/dist/builtins/domains/array.d.ts +3 -0
- package/dist/builtins/domains/array.d.ts.map +1 -0
- package/dist/builtins/domains/array.js +236 -0
- package/dist/builtins/domains/array.js.map +1 -0
- package/dist/builtins/domains/core.d.ts +3 -0
- package/dist/builtins/domains/core.d.ts.map +1 -0
- package/dist/builtins/domains/core.js +45 -0
- package/dist/builtins/domains/core.js.map +1 -0
- package/dist/builtins/domains/crypto.d.ts +3 -0
- package/dist/builtins/domains/crypto.d.ts.map +1 -0
- package/dist/builtins/domains/crypto.js +49 -0
- package/dist/builtins/domains/crypto.js.map +1 -0
- package/dist/builtins/domains/datetime.d.ts +3 -0
- package/dist/builtins/domains/datetime.d.ts.map +1 -0
- package/dist/builtins/domains/datetime.js +45 -0
- package/dist/builtins/domains/datetime.js.map +1 -0
- package/dist/builtins/domains/http.d.ts +3 -0
- package/dist/builtins/domains/http.d.ts.map +1 -0
- package/dist/builtins/domains/http.js +55 -0
- package/dist/builtins/domains/http.js.map +1 -0
- package/dist/builtins/domains/int64.d.ts +3 -0
- package/dist/builtins/domains/int64.d.ts.map +1 -0
- package/dist/builtins/domains/int64.js +31 -0
- package/dist/builtins/domains/int64.js.map +1 -0
- package/dist/builtins/domains/io.d.ts +3 -0
- package/dist/builtins/domains/io.d.ts.map +1 -0
- package/dist/builtins/domains/io.js +79 -0
- package/dist/builtins/domains/io.js.map +1 -0
- package/dist/builtins/domains/json.d.ts +3 -0
- package/dist/builtins/domains/json.d.ts.map +1 -0
- package/dist/builtins/domains/json.js +47 -0
- package/dist/builtins/domains/json.js.map +1 -0
- package/dist/builtins/domains/math.d.ts +3 -0
- package/dist/builtins/domains/math.d.ts.map +1 -0
- package/dist/builtins/domains/math.js +47 -0
- package/dist/builtins/domains/math.js.map +1 -0
- package/dist/builtins/domains/option.d.ts +3 -0
- package/dist/builtins/domains/option.d.ts.map +1 -0
- package/dist/builtins/domains/option.js +56 -0
- package/dist/builtins/domains/option.js.map +1 -0
- package/dist/builtins/domains/random.d.ts +3 -0
- package/dist/builtins/domains/random.d.ts.map +1 -0
- package/dist/builtins/domains/random.js +50 -0
- package/dist/builtins/domains/random.js.map +1 -0
- package/dist/builtins/domains/regex.d.ts +3 -0
- package/dist/builtins/domains/regex.d.ts.map +1 -0
- package/dist/builtins/domains/regex.js +69 -0
- package/dist/builtins/domains/regex.js.map +1 -0
- package/dist/builtins/domains/result.d.ts +3 -0
- package/dist/builtins/domains/result.d.ts.map +1 -0
- package/dist/builtins/domains/result.js +84 -0
- package/dist/builtins/domains/result.js.map +1 -0
- package/dist/builtins/domains/string.d.ts +3 -0
- package/dist/builtins/domains/string.d.ts.map +1 -0
- package/dist/builtins/domains/string.js +139 -0
- package/dist/builtins/domains/string.js.map +1 -0
- package/dist/builtins/domains/type-conversion.d.ts +3 -0
- package/dist/builtins/domains/type-conversion.d.ts.map +1 -0
- package/dist/builtins/domains/type-conversion.js +42 -0
- package/dist/builtins/domains/type-conversion.js.map +1 -0
- package/dist/builtins/host-helpers.d.ts +66 -0
- package/dist/builtins/host-helpers.d.ts.map +1 -0
- package/dist/builtins/host-helpers.js +127 -0
- package/dist/builtins/host-helpers.js.map +1 -0
- package/dist/builtins/registry.d.ts +48 -0
- package/dist/builtins/registry.d.ts.map +1 -0
- package/dist/builtins/registry.js +118 -0
- package/dist/builtins/registry.js.map +1 -0
- package/dist/check.d.ts +9 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +30 -9
- package/dist/check.js.map +1 -1
- package/dist/checker/check.d.ts +18 -1
- package/dist/checker/check.d.ts.map +1 -1
- package/dist/checker/check.js +157 -108
- package/dist/checker/check.js.map +1 -1
- package/dist/checker/type-env.d.ts +2 -0
- package/dist/checker/type-env.d.ts.map +1 -1
- package/dist/checker/type-env.js +9 -0
- package/dist/checker/type-env.js.map +1 -1
- package/dist/codegen/browser-host-adapter.d.ts +29 -0
- package/dist/codegen/browser-host-adapter.d.ts.map +1 -0
- package/dist/codegen/browser-host-adapter.js +51 -0
- package/dist/codegen/browser-host-adapter.js.map +1 -0
- package/dist/codegen/builtins.d.ts +2 -26
- package/dist/codegen/builtins.d.ts.map +1 -1
- package/dist/codegen/builtins.js +2 -341
- package/dist/codegen/builtins.js.map +1 -1
- package/dist/codegen/closures.d.ts +17 -0
- package/dist/codegen/closures.d.ts.map +1 -0
- package/dist/codegen/closures.js +140 -0
- package/dist/codegen/closures.js.map +1 -0
- package/dist/codegen/codegen.d.ts +6 -30
- package/dist/codegen/codegen.d.ts.map +1 -1
- package/dist/codegen/codegen.js +154 -1103
- package/dist/codegen/codegen.js.map +1 -1
- package/dist/codegen/collect-strings.d.ts +4 -0
- package/dist/codegen/collect-strings.d.ts.map +1 -0
- package/dist/codegen/collect-strings.js +76 -0
- package/dist/codegen/collect-strings.js.map +1 -0
- package/dist/codegen/compile-calls.d.ts +10 -0
- package/dist/codegen/compile-calls.d.ts.map +1 -0
- package/dist/codegen/compile-calls.js +374 -0
- package/dist/codegen/compile-calls.js.map +1 -0
- package/dist/codegen/compile-data.d.ts +22 -0
- package/dist/codegen/compile-data.d.ts.map +1 -0
- package/dist/codegen/compile-data.js +243 -0
- package/dist/codegen/compile-data.js.map +1 -0
- package/dist/codegen/compile-expr.d.ts +10 -0
- package/dist/codegen/compile-expr.d.ts.map +1 -0
- package/dist/codegen/compile-expr.js +156 -0
- package/dist/codegen/compile-expr.js.map +1 -0
- package/dist/codegen/compile-match.d.ts +7 -0
- package/dist/codegen/compile-match.d.ts.map +1 -0
- package/dist/codegen/compile-match.js +195 -0
- package/dist/codegen/compile-match.js.map +1 -0
- package/dist/codegen/compile-scalars.d.ts +25 -0
- package/dist/codegen/compile-scalars.d.ts.map +1 -0
- package/dist/codegen/compile-scalars.js +211 -0
- package/dist/codegen/compile-scalars.js.map +1 -0
- package/dist/codegen/hof-generators.d.ts +39 -0
- package/dist/codegen/hof-generators.d.ts.map +1 -0
- package/dist/codegen/hof-generators.js +336 -0
- package/dist/codegen/hof-generators.js.map +1 -0
- package/dist/codegen/host-adapter.d.ts +44 -0
- package/dist/codegen/host-adapter.d.ts.map +1 -0
- package/dist/codegen/host-adapter.js +9 -0
- package/dist/codegen/host-adapter.js.map +1 -0
- package/dist/codegen/imports.d.ts +15 -0
- package/dist/codegen/imports.d.ts.map +1 -0
- package/dist/codegen/imports.js +165 -0
- package/dist/codegen/imports.js.map +1 -0
- package/dist/codegen/node-host-adapter.d.ts +35 -0
- package/dist/codegen/node-host-adapter.d.ts.map +1 -0
- package/dist/codegen/node-host-adapter.js +155 -0
- package/dist/codegen/node-host-adapter.js.map +1 -0
- package/dist/codegen/runner.d.ts +36 -2
- package/dist/codegen/runner.d.ts.map +1 -1
- package/dist/codegen/runner.js +147 -271
- package/dist/codegen/runner.js.map +1 -1
- package/dist/codegen/types.d.ts +96 -0
- package/dist/codegen/types.d.ts.map +1 -0
- package/dist/codegen/types.js +63 -0
- package/dist/codegen/types.js.map +1 -0
- package/dist/compact/expand.d.ts +25 -0
- package/dist/compact/expand.d.ts.map +1 -0
- package/dist/compact/expand.js +199 -0
- package/dist/compact/expand.js.map +1 -0
- package/dist/compile.d.ts +2 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +2 -2
- package/dist/compile.js.map +1 -1
- package/dist/contracts/translate.js.map +1 -1
- package/dist/contracts/verify.d.ts +10 -2
- package/dist/contracts/verify.d.ts.map +1 -1
- package/dist/contracts/verify.js +26 -21
- package/dist/contracts/verify.js.map +1 -1
- package/dist/effects/call-graph.d.ts.map +1 -1
- package/dist/effects/call-graph.js +27 -4
- package/dist/effects/call-graph.js.map +1 -1
- package/dist/effects/effect-check.d.ts +10 -2
- package/dist/effects/effect-check.d.ts.map +1 -1
- package/dist/effects/effect-check.js +12 -7
- package/dist/effects/effect-check.js.map +1 -1
- package/dist/errors/error-catalog.d.ts +1 -1
- package/dist/errors/error-catalog.d.ts.map +1 -1
- package/dist/errors/error-catalog.js +119 -0
- package/dist/errors/error-catalog.js.map +1 -1
- package/dist/errors/structured-errors.d.ts +28 -1
- package/dist/errors/structured-errors.d.ts.map +1 -1
- package/dist/errors/structured-errors.js +9 -0
- package/dist/errors/structured-errors.js.map +1 -1
- package/dist/index.d.ts +20 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -10
- package/dist/index.js.map +1 -1
- package/dist/lint/lint.d.ts +9 -0
- package/dist/lint/lint.d.ts.map +1 -0
- package/dist/lint/lint.js +354 -0
- package/dist/lint/lint.js.map +1 -0
- package/dist/lint/warnings.d.ts +54 -0
- package/dist/lint/warnings.d.ts.map +1 -0
- package/dist/lint/warnings.js +39 -0
- package/dist/lint/warnings.js.map +1 -0
- package/dist/mcp/create-server.d.ts.map +1 -1
- package/dist/mcp/create-server.js +66 -5
- package/dist/mcp/create-server.js.map +1 -1
- package/dist/mcp/handlers.d.ts +21 -5
- package/dist/mcp/handlers.d.ts.map +1 -1
- package/dist/mcp/handlers.js +65 -15
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/prompts.d.ts +17 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +181 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/server.js +1 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/resolver/resolve.d.ts.map +1 -1
- package/dist/resolver/resolve.js +62 -16
- package/dist/resolver/resolve.js.map +1 -1
- package/dist/validator/node-validators.d.ts.map +1 -1
- package/dist/validator/node-validators.js +60 -5
- package/dist/validator/node-validators.js.map +1 -1
- package/package.json +4 -2
package/dist/codegen/codegen.js
CHANGED
|
@@ -6,166 +6,23 @@
|
|
|
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 "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 "string_interp":
|
|
109
|
-
return binaryen.i32; // string pointer
|
|
110
|
-
case "access": {
|
|
111
|
-
let recordTypeName;
|
|
112
|
-
if (expr.target.kind === "ident") {
|
|
113
|
-
const local = ctx.getLocal(expr.target.name);
|
|
114
|
-
if (local && local.edictTypeName) {
|
|
115
|
-
recordTypeName = local.edictTypeName;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else if (expr.target.kind === "record_expr") {
|
|
119
|
-
recordTypeName = expr.target.name;
|
|
120
|
-
}
|
|
121
|
-
if (recordTypeName) {
|
|
122
|
-
const layout = ctx.recordLayouts.get(recordTypeName);
|
|
123
|
-
if (layout) {
|
|
124
|
-
const fieldLayout = layout.fields.find((f) => f.name === expr.field);
|
|
125
|
-
if (fieldLayout)
|
|
126
|
-
return fieldLayout.wasmType;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return binaryen.i32; // fallback
|
|
130
|
-
}
|
|
131
|
-
default:
|
|
132
|
-
return binaryen.i32;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
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
|
-
}
|
|
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 { generateWasmBuiltins } from "../builtins/registry.js";
|
|
13
|
+
import { FunctionContext, edictTypeToWasm, } from "./types.js";
|
|
14
|
+
import { inferImportSignatures } from "./imports.js";
|
|
15
|
+
import { compileExpr, inferExprWasmType } from "./compile-expr.js";
|
|
16
|
+
// Re-export expression compilation functions (moved to compile-expr.ts)
|
|
17
|
+
export { compileExpr, inferExprWasmType };
|
|
162
18
|
// =============================================================================
|
|
163
19
|
// Compiler
|
|
164
20
|
// =============================================================================
|
|
165
|
-
export function compile(module) {
|
|
21
|
+
export function compile(module, options) {
|
|
166
22
|
const mod = new binaryen.Module();
|
|
167
23
|
const strings = new StringTable();
|
|
168
24
|
const errors = [];
|
|
25
|
+
const maxPages = options?.maxMemoryPages ?? 16;
|
|
169
26
|
try {
|
|
170
27
|
// Pre-scan: intern all string literals
|
|
171
28
|
for (const def of module.definitions) {
|
|
@@ -173,13 +30,13 @@ export function compile(module) {
|
|
|
173
30
|
collectStrings(def.body, strings);
|
|
174
31
|
}
|
|
175
32
|
if (def.kind === "const") {
|
|
176
|
-
|
|
33
|
+
collectStrings([def.value], strings);
|
|
177
34
|
}
|
|
178
35
|
}
|
|
179
36
|
// Setup memory with string data segments
|
|
180
37
|
const segments = strings.toMemorySegments(mod);
|
|
181
38
|
const pages = Math.max(1, Math.ceil(strings.totalBytes / 65536));
|
|
182
|
-
mod.setMemory(pages,
|
|
39
|
+
mod.setMemory(pages, maxPages, "memory", segments);
|
|
183
40
|
// Build RecordLayout registry
|
|
184
41
|
const recordLayouts = new Map();
|
|
185
42
|
const enumLayouts = new Map();
|
|
@@ -209,6 +66,26 @@ export function compile(module) {
|
|
|
209
66
|
enumLayouts.set(def.name, { variants });
|
|
210
67
|
}
|
|
211
68
|
}
|
|
69
|
+
// Register built-in Option enum layout: None (tag 0), Some(value) (tag 1)
|
|
70
|
+
// Guard lets user-defined Option enums override the built-in.
|
|
71
|
+
if (!enumLayouts.has("Option")) {
|
|
72
|
+
enumLayouts.set("Option", {
|
|
73
|
+
variants: [
|
|
74
|
+
{ name: "None", tag: 0, fields: [], totalSize: 8 },
|
|
75
|
+
{ name: "Some", tag: 1, fields: [{ name: "value", offset: 8, wasmType: binaryen.i32 }], totalSize: 16 },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Register built-in Result enum layout: Ok (tag 0), Err (tag 1)
|
|
80
|
+
// Guard lets user-defined Result enums override the built-in.
|
|
81
|
+
if (!enumLayouts.has("Result")) {
|
|
82
|
+
enumLayouts.set("Result", {
|
|
83
|
+
variants: [
|
|
84
|
+
{ name: "Ok", tag: 0, fields: [{ name: "value", offset: 8, wasmType: binaryen.i32 }], totalSize: 16 },
|
|
85
|
+
{ name: "Err", tag: 1, fields: [{ name: "error", offset: 8, wasmType: binaryen.i32 }], totalSize: 16 },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
212
89
|
// Initialize bump allocator heap pointer
|
|
213
90
|
// Ensure heap starts at an 8-byte aligned offset after the string table, min 8
|
|
214
91
|
const heapStart = Math.max(8, Math.ceil(strings.totalBytes / 8) * 8);
|
|
@@ -227,16 +104,45 @@ export function compile(module) {
|
|
|
227
104
|
}
|
|
228
105
|
for (const def of module.definitions) {
|
|
229
106
|
if (def.kind === "fn") {
|
|
107
|
+
// Closure convention: all user functions have __env:i32 as first WASM param
|
|
108
|
+
// String params are expanded to (ptr: i32, len: i32) pairs at the WASM level
|
|
109
|
+
const edictParamTypes = ["other"]; // __env
|
|
110
|
+
const wasmParamTypes = [binaryen.i32]; // __env
|
|
111
|
+
for (const p of def.params) {
|
|
112
|
+
if (p.type.kind === "basic" && p.type.name === "String") {
|
|
113
|
+
wasmParamTypes.push(binaryen.i32, binaryen.i32); // ptr, len
|
|
114
|
+
edictParamTypes.push("String");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
wasmParamTypes.push(edictTypeToWasm(p.type));
|
|
118
|
+
edictParamTypes.push("other");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
230
121
|
fnSigs.set(def.name, {
|
|
231
|
-
returnType: edictTypeToWasm(def.returnType),
|
|
232
|
-
paramTypes:
|
|
122
|
+
returnType: def.returnType ? edictTypeToWasm(def.returnType) : (options?.typeInfo?.inferredReturnTypes.get(def.id) ? edictTypeToWasm(options.typeInfo.inferredReturnTypes.get(def.id)) : binaryen.i32),
|
|
123
|
+
paramTypes: wasmParamTypes,
|
|
124
|
+
edictParamTypes,
|
|
233
125
|
});
|
|
234
126
|
}
|
|
235
127
|
}
|
|
128
|
+
// HOF support: function table for indirect calls (call_indirect)
|
|
129
|
+
// Pre-assign table indices to user-defined functions.
|
|
130
|
+
// Lambdas will be appended during compilation.
|
|
131
|
+
const tableFunctions = [];
|
|
132
|
+
const fnTableIndices = new Map();
|
|
133
|
+
for (const def of module.definitions) {
|
|
134
|
+
if (def.kind === "fn") {
|
|
135
|
+
fnTableIndices.set(def.name, tableFunctions.length);
|
|
136
|
+
tableFunctions.push(def.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
236
139
|
// Import builtins — compute WASM-level params from Edict signatures
|
|
237
140
|
// Each String param becomes two i32 values (ptr, len) at the WASM level
|
|
238
141
|
for (const [name, builtin] of BUILTIN_FUNCTIONS) {
|
|
239
142
|
const [importModule, importBase] = builtin.wasmImport;
|
|
143
|
+
// WASM-native builtins (HOFs) are generated as internal functions, not imported
|
|
144
|
+
if (importModule === "__wasm")
|
|
145
|
+
continue;
|
|
240
146
|
const wasmParams = [];
|
|
241
147
|
for (const param of builtin.type.params) {
|
|
242
148
|
if (param.kind === "basic" && param.name === "String") {
|
|
@@ -251,17 +157,42 @@ export function compile(module) {
|
|
|
251
157
|
: binaryen.none, edictTypeToWasm(builtin.type.returnType));
|
|
252
158
|
}
|
|
253
159
|
// Import module-level imports as WASM host imports
|
|
254
|
-
//
|
|
160
|
+
// Use declared types when available, fall back to inference for untyped imports
|
|
255
161
|
const importedNames = new Set();
|
|
162
|
+
const typedImportNames = new Set();
|
|
256
163
|
for (const imp of module.imports) {
|
|
257
164
|
for (const name of imp.names) {
|
|
258
165
|
if (!BUILTIN_FUNCTIONS.has(name)) {
|
|
259
|
-
|
|
166
|
+
const declaredType = imp.types?.[name];
|
|
167
|
+
if (declaredType && declaredType.kind === "fn_type") {
|
|
168
|
+
// Typed import — derive WASM signature from declared type
|
|
169
|
+
const wasmParams = [];
|
|
170
|
+
const edictParamTypes = [];
|
|
171
|
+
for (const param of declaredType.params) {
|
|
172
|
+
if (param.kind === "basic" && param.name === "String") {
|
|
173
|
+
wasmParams.push(binaryen.i32, binaryen.i32); // ptr, len
|
|
174
|
+
edictParamTypes.push("String");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
wasmParams.push(edictTypeToWasm(param));
|
|
178
|
+
edictParamTypes.push("other");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const wasmReturnType = edictTypeToWasm(declaredType.returnType);
|
|
182
|
+
mod.addFunctionImport(name, imp.module, name, wasmParams.length > 0
|
|
183
|
+
? binaryen.createType(wasmParams)
|
|
184
|
+
: binaryen.none, wasmReturnType);
|
|
185
|
+
fnSigs.set(name, { returnType: wasmReturnType, paramTypes: wasmParams, edictParamTypes });
|
|
186
|
+
typedImportNames.add(name);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
importedNames.add(name);
|
|
190
|
+
}
|
|
260
191
|
}
|
|
261
192
|
}
|
|
262
193
|
}
|
|
263
194
|
if (importedNames.size > 0) {
|
|
264
|
-
//
|
|
195
|
+
// Fallback: infer WASM types from call sites for untyped imports
|
|
265
196
|
const importSigs = inferImportSignatures(module, importedNames);
|
|
266
197
|
for (const [name, sig] of importSigs) {
|
|
267
198
|
const imp = module.imports.find(i => i.names.includes(name));
|
|
@@ -274,12 +205,19 @@ export function compile(module) {
|
|
|
274
205
|
}
|
|
275
206
|
// Compile const definitions as WASM globals
|
|
276
207
|
const constGlobals = new Map();
|
|
208
|
+
// Create the compilation context — bundles compile-wide state
|
|
209
|
+
const cc = {
|
|
210
|
+
mod, strings, fnSigs, errors,
|
|
211
|
+
constGlobals, recordLayouts, enumLayouts, fnTableIndices, tableFunctions,
|
|
212
|
+
lambdaCounter: 0,
|
|
213
|
+
typeInfo: options?.typeInfo,
|
|
214
|
+
};
|
|
277
215
|
for (const def of module.definitions) {
|
|
278
216
|
if (def.kind === "const") {
|
|
279
217
|
const wasmType = edictTypeToWasm(def.type);
|
|
280
218
|
// Create a temporary context for compiling the const init expression
|
|
281
219
|
const tmpCtx = new FunctionContext([]);
|
|
282
|
-
const initExpr = compileExpr(def.value,
|
|
220
|
+
const initExpr = compileExpr(def.value, cc, tmpCtx);
|
|
283
221
|
mod.addGlobal(def.name, wasmType, false, initExpr);
|
|
284
222
|
constGlobals.set(def.name, wasmType);
|
|
285
223
|
}
|
|
@@ -287,9 +225,17 @@ export function compile(module) {
|
|
|
287
225
|
// Compile each function
|
|
288
226
|
for (const def of module.definitions) {
|
|
289
227
|
if (def.kind === "fn") {
|
|
290
|
-
compileFunction(def,
|
|
228
|
+
compileFunction(def, cc);
|
|
291
229
|
}
|
|
292
230
|
}
|
|
231
|
+
// Generate WASM-native HOF builtins from the unified registry
|
|
232
|
+
generateWasmBuiltins(mod);
|
|
233
|
+
// Build function table for indirect calls (call_indirect)
|
|
234
|
+
// This must happen after all functions (including lambdas) are compiled
|
|
235
|
+
if (tableFunctions.length > 0) {
|
|
236
|
+
mod.addTable("__fn_table", tableFunctions.length, tableFunctions.length);
|
|
237
|
+
mod.addActiveElementSegment("__fn_table", "__fn_elems", tableFunctions, mod.i32.const(0));
|
|
238
|
+
}
|
|
293
239
|
// Export the "main" function if it exists
|
|
294
240
|
const mainDef = module.definitions.find((d) => d.kind === "fn" && d.name === "main");
|
|
295
241
|
if (mainDef) {
|
|
@@ -308,18 +254,21 @@ export function compile(module) {
|
|
|
308
254
|
mod.addFunctionExport("__set_str_ret_len", "__set_str_ret_len");
|
|
309
255
|
// Memory is already exported via setMemory's exportName parameter
|
|
310
256
|
// Validate
|
|
257
|
+
if (errors.length > 0) {
|
|
258
|
+
return { ok: false, errors };
|
|
259
|
+
}
|
|
311
260
|
if (!mod.validate()) {
|
|
312
|
-
errors.push("binaryen validation failed");
|
|
261
|
+
errors.push(wasmValidationError("binaryen validation failed"));
|
|
313
262
|
return { ok: false, errors };
|
|
314
263
|
}
|
|
315
264
|
// Optimize
|
|
316
265
|
mod.optimize();
|
|
317
|
-
const wat = mod.emitText();
|
|
266
|
+
const wat = options?.emitWat ? mod.emitText() : undefined;
|
|
318
267
|
const wasm = mod.emitBinary();
|
|
319
|
-
return { ok: true, wasm, wat };
|
|
268
|
+
return { ok: true, wasm, ...(wat ? { wat } : {}) };
|
|
320
269
|
}
|
|
321
270
|
catch (e) {
|
|
322
|
-
errors.push(e instanceof Error ? e.message : String(e));
|
|
271
|
+
errors.push(wasmValidationError(e instanceof Error ? e.message : String(e)));
|
|
323
272
|
return { ok: false, errors };
|
|
324
273
|
}
|
|
325
274
|
finally {
|
|
@@ -329,22 +278,49 @@ export function compile(module) {
|
|
|
329
278
|
// =============================================================================
|
|
330
279
|
// Function compilation
|
|
331
280
|
// =============================================================================
|
|
332
|
-
function compileFunction(fn,
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
281
|
+
function compileFunction(fn, cc) {
|
|
282
|
+
const { mod } = cc;
|
|
283
|
+
const params = fn.params.map((p) => {
|
|
284
|
+
const resolvedType = cc.typeInfo?.inferredLambdaParamTypes.get(p.id) ?? p.type;
|
|
285
|
+
return {
|
|
286
|
+
name: p.name,
|
|
287
|
+
edictType: resolvedType,
|
|
288
|
+
wasmType: edictTypeToWasm(resolvedType),
|
|
289
|
+
edictTypeName: resolvedType.kind === "named" ? resolvedType.name : resolvedType.kind === "option" ? "Option" : resolvedType.kind === "result" ? "Result" : undefined,
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
// Closure convention: all user functions have __env:i32 as first WASM param.
|
|
293
|
+
// The __env param is ignored for non-lambda functions but ensures uniform
|
|
294
|
+
// call_indirect signatures when functions are used as values.
|
|
295
|
+
// String params are widened to (ptr: i32, len: i32) pairs — the companion
|
|
296
|
+
// __str_len_{name} becomes a real WASM param so existing lookups find it.
|
|
297
|
+
const allParams = [
|
|
298
|
+
{ name: "__env", wasmType: binaryen.i32, edictTypeName: undefined },
|
|
299
|
+
];
|
|
300
|
+
for (const p of params) {
|
|
301
|
+
if (p.edictType.kind === "basic" && p.edictType.name === "String") {
|
|
302
|
+
allParams.push({ name: p.name, wasmType: binaryen.i32, edictTypeName: undefined });
|
|
303
|
+
allParams.push({ name: `__str_len_${p.name}`, wasmType: binaryen.i32, edictTypeName: undefined });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
allParams.push({ name: p.name, wasmType: p.wasmType, edictTypeName: p.edictTypeName });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const ctx = new FunctionContext(allParams);
|
|
310
|
+
const returnType = fn.returnType
|
|
311
|
+
? edictTypeToWasm(fn.returnType)
|
|
312
|
+
: (cc.typeInfo?.inferredReturnTypes.get(fn.id)
|
|
313
|
+
? edictTypeToWasm(cc.typeInfo.inferredReturnTypes.get(fn.id))
|
|
314
|
+
: (fn.body.length > 0
|
|
315
|
+
? inferExprWasmType(fn.body[fn.body.length - 1], cc, ctx)
|
|
316
|
+
: binaryen.i32));
|
|
317
|
+
const paramTypes = allParams.map((p) => p.wasmType);
|
|
342
318
|
const paramType = paramTypes.length > 0
|
|
343
319
|
? binaryen.createType(paramTypes)
|
|
344
320
|
: binaryen.none;
|
|
345
321
|
// Compile body — wrap non-final expressions in drop() per WASM semantics
|
|
346
322
|
const bodyExprs = fn.body.map((expr, i) => {
|
|
347
|
-
const compiled = compileExpr(expr,
|
|
323
|
+
const compiled = compileExpr(expr, cc, ctx);
|
|
348
324
|
// Non-final expressions that produce values must be dropped
|
|
349
325
|
if (i < fn.body.length - 1 && expr.kind !== "let") {
|
|
350
326
|
return mod.drop(compiled);
|
|
@@ -363,929 +339,4 @@ function compileFunction(fn, mod, strings, fnSigs, constGlobals, recordLayouts,
|
|
|
363
339
|
}
|
|
364
340
|
mod.addFunction(fn.name, paramType, returnType, ctx.varTypes, body);
|
|
365
341
|
}
|
|
366
|
-
// =============================================================================
|
|
367
|
-
// Expression compilation
|
|
368
|
-
// =============================================================================
|
|
369
|
-
function compileExpr(expr, mod, ctx, strings, fnSigs, errors) {
|
|
370
|
-
switch (expr.kind) {
|
|
371
|
-
case "literal":
|
|
372
|
-
return compileLiteral(expr, mod, strings);
|
|
373
|
-
case "ident":
|
|
374
|
-
return compileIdent(expr, mod, ctx);
|
|
375
|
-
case "binop":
|
|
376
|
-
return compileBinop(expr, mod, ctx, strings, fnSigs, errors);
|
|
377
|
-
case "unop":
|
|
378
|
-
return compileUnop(expr, mod, ctx, strings, fnSigs, errors);
|
|
379
|
-
case "call":
|
|
380
|
-
return compileCall(expr, mod, ctx, strings, fnSigs, errors);
|
|
381
|
-
case "if":
|
|
382
|
-
return compileIf(expr, mod, ctx, strings, fnSigs, errors);
|
|
383
|
-
case "let":
|
|
384
|
-
return compileLet(expr, mod, ctx, strings, fnSigs, errors);
|
|
385
|
-
case "block":
|
|
386
|
-
return compileBlock(expr, mod, ctx, strings, fnSigs, errors);
|
|
387
|
-
case "match":
|
|
388
|
-
return compileMatch(expr, mod, ctx, strings, fnSigs, errors);
|
|
389
|
-
case "record_expr":
|
|
390
|
-
return compileRecordExpr(expr, mod, ctx, strings, fnSigs, errors);
|
|
391
|
-
case "tuple_expr":
|
|
392
|
-
return compileTupleExpr(expr, mod, ctx, strings, fnSigs, errors);
|
|
393
|
-
case "enum_constructor":
|
|
394
|
-
return compileEnumConstructor(expr, mod, ctx, strings, fnSigs, errors);
|
|
395
|
-
case "access":
|
|
396
|
-
return compileAccess(expr, mod, ctx, strings, fnSigs, errors);
|
|
397
|
-
case "array":
|
|
398
|
-
return compileArrayExpr(expr, mod, ctx, strings, fnSigs, errors);
|
|
399
|
-
case "lambda":
|
|
400
|
-
return compileLambdaExpr(expr, mod, ctx, strings, fnSigs, errors);
|
|
401
|
-
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);
|
|
501
|
-
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
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
342
|
//# sourceMappingURL=codegen.js.map
|