gofront 1.4.0 → 1.5.1
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 +72 -9
- package/package.json +2 -1
- package/src/{codegen → backend/js}/expressions.js +217 -60
- package/src/{codegen → backend/js}/index.js +21 -35
- package/src/{codegen → backend/js}/statements.js +43 -48
- package/src/backend/js/stdlib/bits.js +32 -0
- package/src/{codegen → backend/js}/stdlib/index.js +3 -0
- package/src/{codegen → backend/js}/templ.js +2 -2
- package/src/backend/wasm/boundary.js +1345 -0
- package/src/backend/wasm/emit.js +5779 -0
- package/src/backend/wasm/encode.js +789 -0
- package/src/backend/wasm/glue.js +130 -0
- package/src/backend/wasm/index.js +1413 -0
- package/src/backend/wasm/types.js +420 -0
- package/src/backend/wasm/wat.js +255 -0
- package/src/cli-core.js +52 -7
- package/src/compiler.js +322 -12
- package/src/dev-server.js +56 -15
- package/src/index.js +17 -9
- package/src/lexer.js +18 -0
- package/src/lower/boxing.js +36 -0
- package/src/lower/captures.js +179 -0
- package/src/lower/embedding.js +30 -0
- package/src/lower/escape.js +169 -0
- package/src/lower/functions.js +30 -0
- package/src/lower/globals.js +324 -0
- package/src/lower/index.js +115 -0
- package/src/lower/ownership.js +142 -0
- package/src/lower/range.js +70 -0
- package/src/minifier.js +2 -0
- package/src/parser/index.js +8 -3
- package/src/resolver.js +1 -0
- package/src/test-runner.js +74 -20
- package/src/typechecker/expressions.js +40 -1
- package/src/typechecker/index.js +234 -4
- package/src/typechecker/statements.js +7 -0
- package/src/typechecker/stdlib/core.js +17 -0
- package/src/typechecker/types.js +56 -2
- /package/src/{codegen → backend/js}/runtime.js +0 -0
- /package/src/{codegen → backend/js}/source-map.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
// src/backend/wasm/boundary.js
|
|
2
|
+
// JS <-> WASM boundary: type classification, marshalling helpers (WASM IR)
|
|
3
|
+
// and the generated JS facade that exposes a wasm package to JS callers.
|
|
4
|
+
//
|
|
5
|
+
// Boundary v1 rules (docs/v1.5.0/wasm-mvp-plan.md, Phase 5):
|
|
6
|
+
// - primitives: bool <-> i32, narrow ints <-> i32, int/int64 <-> f64 at the
|
|
7
|
+
// boundary (i64 inside; exact for |v| <= 2^53, which the facade already
|
|
8
|
+
// enforces, and keeps BigInt boxing off hot paths), floats direct,
|
|
9
|
+
// string externref, any anyref (crossing as externref).
|
|
10
|
+
// - every GC reference (struct/array/closure/anyref) crosses as externref
|
|
11
|
+
// and exports are bound to module-level consts: V8 only inlines JS->wasm
|
|
12
|
+
// calls (floats unboxed) for const callees with i32/i64/f32/f64/externref
|
|
13
|
+
// signatures outside try/catch. Panics are raised through `env.panic`
|
|
14
|
+
// (a JS Error) rather than a wasm `throw`, so no guard wrapper is needed.
|
|
15
|
+
// - struct values from `both` packages: copied field-by-field into the JS
|
|
16
|
+
// copy class (`new T(...)`); `*T` params are copied in and written back.
|
|
17
|
+
// - struct values from `wasm` packages: opaque handles with stable identity
|
|
18
|
+
// (class `T` wrapping the WasmGC ref, WeakMap keyed registry).
|
|
19
|
+
// - slices / arrays: element-wise copy (plain JS arrays on the JS side,
|
|
20
|
+
// TypedArray inputs accepted). No copy-back for slices.
|
|
21
|
+
// - func values: JS callbacks wrapped into wasm closures via thunks; wasm
|
|
22
|
+
// closures exposed to JS as callable functions.
|
|
23
|
+
// - maps, non-empty interfaces, error, pointers to non-structs: rejected
|
|
24
|
+
// with a compile error (planned for a later boundary version).
|
|
25
|
+
// - `*testing.T`: the harness's JS test object, passed as an opaque
|
|
26
|
+
// externref (test methods are routed back to JS via `env.testing_*`).
|
|
27
|
+
|
|
28
|
+
import { isTestingT } from "./types.js";
|
|
29
|
+
|
|
30
|
+
// ── Type classification ──────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const I32_INTS = new Set([
|
|
33
|
+
"int8",
|
|
34
|
+
"int16",
|
|
35
|
+
"int32",
|
|
36
|
+
"uint8",
|
|
37
|
+
"uint16",
|
|
38
|
+
"uint32",
|
|
39
|
+
"byte",
|
|
40
|
+
"rune",
|
|
41
|
+
]);
|
|
42
|
+
const I64_INTS = new Set(["int", "uint", "int64", "uint64", "uintptr"]);
|
|
43
|
+
const UNSIGNED = new Set([
|
|
44
|
+
"uint",
|
|
45
|
+
"uint8",
|
|
46
|
+
"uint16",
|
|
47
|
+
"uint32",
|
|
48
|
+
"uint64",
|
|
49
|
+
"uintptr",
|
|
50
|
+
"byte",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export function wTypeKey(wType) {
|
|
54
|
+
if (typeof wType === "string") return wType;
|
|
55
|
+
if (wType && typeof wType.typeIndex === "number")
|
|
56
|
+
return `r${wType.typeIndex}`;
|
|
57
|
+
return `h${wType?.heapType ?? "any"}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toRef(info) {
|
|
61
|
+
return { kind: "ref", nullable: true, typeIndex: info.typeIndex };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function structDesc(info, ptr) {
|
|
65
|
+
return { k: "struct", name: info.name, ptr, info, wType: toRef(info) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function funcSigFromAst(t) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "func",
|
|
71
|
+
params: (t.params ?? []).map((p) => p.type ?? p),
|
|
72
|
+
returns: t.returnType
|
|
73
|
+
? t.returnType.kind === "TupleType" || t.returnType.kind === "tuple"
|
|
74
|
+
? t.returnType.types
|
|
75
|
+
: [t.returnType]
|
|
76
|
+
: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Classifies a Go type (AST node or checker type) for boundary marshalling.
|
|
81
|
+
// Returns a descriptor `{ k, ... }`; `k === "unsupported"` carries `what`.
|
|
82
|
+
export function classifyType(goType, mod) {
|
|
83
|
+
if (!goType) return { k: "void" };
|
|
84
|
+
if (isTestingT(goType)) return { k: "extern", wType: "externref" };
|
|
85
|
+
|
|
86
|
+
if (goType.kind === "TypeName" || goType.kind === "Ident") {
|
|
87
|
+
const info = mod.getStructType(goType.name);
|
|
88
|
+
if (info) return structDesc(info, false);
|
|
89
|
+
const resolved = mod.checker?.types?.get(goType.name);
|
|
90
|
+
if (resolved) return classifyType(resolved, mod);
|
|
91
|
+
return classifyBasic(goType.name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const t = normalizeAstType(goType);
|
|
95
|
+
switch (t.kind) {
|
|
96
|
+
case "named":
|
|
97
|
+
return classifyNamed(t, mod);
|
|
98
|
+
case "untyped":
|
|
99
|
+
return classifyBasic(t.base);
|
|
100
|
+
case "basic":
|
|
101
|
+
return classifyBasic(t.name);
|
|
102
|
+
case "pointer":
|
|
103
|
+
return classifyPointer(t, mod);
|
|
104
|
+
case "slice":
|
|
105
|
+
return classifySlice(t, mod);
|
|
106
|
+
case "array":
|
|
107
|
+
return classifyArray(t, mod);
|
|
108
|
+
case "func":
|
|
109
|
+
return classifyFunc(t, mod);
|
|
110
|
+
case "interface":
|
|
111
|
+
return classifyInterface(t);
|
|
112
|
+
default:
|
|
113
|
+
return {
|
|
114
|
+
k: "unsupported",
|
|
115
|
+
what: UNSUPPORTED_KINDS[t.kind] ?? `type kind '${t.kind}'`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const UNSUPPORTED_KINDS = {
|
|
121
|
+
map: "map",
|
|
122
|
+
MapType: "map",
|
|
123
|
+
struct: "anonymous struct",
|
|
124
|
+
StructType: "anonymous struct",
|
|
125
|
+
InterfaceType: "interface",
|
|
126
|
+
ChanType: "chan",
|
|
127
|
+
chan: "chan",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Maps parser AST type nodes onto the checker's type shapes.
|
|
131
|
+
function normalizeAstType(t) {
|
|
132
|
+
switch (t.kind) {
|
|
133
|
+
case "PointerType":
|
|
134
|
+
case "StarExpr":
|
|
135
|
+
return { kind: "pointer", base: t.base ?? t.expr ?? t.operand };
|
|
136
|
+
case "SliceType":
|
|
137
|
+
return { kind: "slice", elem: t.elem };
|
|
138
|
+
case "ArrayType":
|
|
139
|
+
return { kind: "array", elem: t.elem, size: t.size };
|
|
140
|
+
case "FuncType":
|
|
141
|
+
return funcSigFromAst(t);
|
|
142
|
+
case "InterfaceType":
|
|
143
|
+
return {
|
|
144
|
+
kind: "interface",
|
|
145
|
+
name: t.name,
|
|
146
|
+
methods: new Set((t.methods ?? []).map((m) => m.name ?? m)),
|
|
147
|
+
};
|
|
148
|
+
default:
|
|
149
|
+
return t;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const BASIC_DESCS = {
|
|
154
|
+
void: { k: "void" },
|
|
155
|
+
bool: { k: "bool", wType: "i32" },
|
|
156
|
+
float32: { k: "f32", wType: "f32" },
|
|
157
|
+
float64: { k: "f64", wType: "f64" },
|
|
158
|
+
string: { k: "string", wType: "externref" },
|
|
159
|
+
any: { k: "any", wType: "anyref" },
|
|
160
|
+
error: { k: "unsupported", what: "error" },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function classifyBasic(n) {
|
|
164
|
+
if (I32_INTS.has(n))
|
|
165
|
+
return { k: "i32", unsigned: UNSIGNED.has(n), wType: "i32" };
|
|
166
|
+
if (I64_INTS.has(n))
|
|
167
|
+
return { k: "i64", unsigned: UNSIGNED.has(n), wType: "i64" };
|
|
168
|
+
const d = BASIC_DESCS[n];
|
|
169
|
+
return d ? { ...d } : { k: "unsupported", what: `type '${n}'` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function classifyNamed(t, mod) {
|
|
173
|
+
const info = mod.getStructType(t.name);
|
|
174
|
+
if (info) return structDesc(info, false);
|
|
175
|
+
if (t.underlying) return classifyType(t.underlying, mod);
|
|
176
|
+
return { k: "unsupported", what: `named type '${t.name}'` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function classifyPointer(t, mod) {
|
|
180
|
+
const base = classifyType(t.base, mod);
|
|
181
|
+
if (base.k === "struct" && !base.ptr) return { ...base, ptr: true };
|
|
182
|
+
const what = base.k === "unsupported" ? base.what : (t.base?.name ?? base.k);
|
|
183
|
+
return { k: "unsupported", what: `pointer to ${what}` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function classifySlice(t, mod) {
|
|
187
|
+
const elem = classifyType(t.elem, mod);
|
|
188
|
+
if (elem.k === "unsupported") return elem;
|
|
189
|
+
const sliceInfo = mod.getSliceType(t.elem);
|
|
190
|
+
return {
|
|
191
|
+
k: "slice",
|
|
192
|
+
elem,
|
|
193
|
+
sliceInfo,
|
|
194
|
+
key: wTypeKey(sliceInfo.elemWType),
|
|
195
|
+
wType: { kind: "ref", nullable: true, typeIndex: sliceInfo.typeIndex },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function arraySize(t) {
|
|
200
|
+
if (typeof t.size === "number") return t.size;
|
|
201
|
+
if (t.size?.value !== undefined) return Number(t.size.value);
|
|
202
|
+
return t.len ?? 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function classifyArray(t, mod) {
|
|
206
|
+
const elem = classifyType(t.elem, mod);
|
|
207
|
+
if (elem.k === "unsupported") return elem;
|
|
208
|
+
const arrInfo = mod.getArrayType(t.elem);
|
|
209
|
+
return {
|
|
210
|
+
k: "array",
|
|
211
|
+
elem,
|
|
212
|
+
size: arraySize(t),
|
|
213
|
+
arrInfo,
|
|
214
|
+
key: wTypeKey(arrInfo.elemWType),
|
|
215
|
+
wType: { kind: "ref", nullable: true, typeIndex: arrInfo.typeIndex },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function classifyFunc(t, mod) {
|
|
220
|
+
const params = (t.params ?? []).map((p) => classifyType(p, mod));
|
|
221
|
+
const returns = (t.returns ?? [])
|
|
222
|
+
.map((r) => classifyType(r, mod))
|
|
223
|
+
.filter((d) => d.k !== "void");
|
|
224
|
+
const bad = [...params, ...returns].find((d) => d.k === "unsupported");
|
|
225
|
+
if (bad) return bad;
|
|
226
|
+
const closureInfo = mod.getClosureType(t);
|
|
227
|
+
return {
|
|
228
|
+
k: "func",
|
|
229
|
+
params,
|
|
230
|
+
returns,
|
|
231
|
+
closureInfo,
|
|
232
|
+
key: `c${closureInfo.typeIndex}`,
|
|
233
|
+
wType: { kind: "ref", nullable: true, typeIndex: closureInfo.typeIndex },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function classifyInterface(t) {
|
|
238
|
+
if (t.name === "error") return { k: "unsupported", what: "error" };
|
|
239
|
+
if (!t.methods || t.methods.size === 0) return { k: "any", wType: "anyref" };
|
|
240
|
+
return {
|
|
241
|
+
k: "unsupported",
|
|
242
|
+
what: `interface${t.name ? ` '${t.name}'` : ""}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Export metadata ──────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function isExported(name) {
|
|
249
|
+
return name[0] >= "A" && name[0] <= "Z";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function returnDescs(returnType, mod) {
|
|
253
|
+
if (!returnType) return [];
|
|
254
|
+
const list =
|
|
255
|
+
returnType.kind === "TupleType" || returnType.kind === "tuple"
|
|
256
|
+
? returnType.types
|
|
257
|
+
: [returnType];
|
|
258
|
+
return list.map((t) => classifyType(t, mod)).filter((d) => d.k !== "void");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function literalConst(spec, i) {
|
|
262
|
+
let node = spec.value?.[i];
|
|
263
|
+
let sign = "";
|
|
264
|
+
if (node?.kind === "UnaryExpr" && node.op === "-" && node.operand) {
|
|
265
|
+
node = node.operand;
|
|
266
|
+
sign = "-";
|
|
267
|
+
}
|
|
268
|
+
if (node?.kind !== "BasicLit") return null;
|
|
269
|
+
switch (node.litKind) {
|
|
270
|
+
case "INT":
|
|
271
|
+
case "FLOAT":
|
|
272
|
+
return `${sign}${node.value}`;
|
|
273
|
+
case "STRING":
|
|
274
|
+
return JSON.stringify(String(node.value));
|
|
275
|
+
case "BOOL":
|
|
276
|
+
return node.value === "true" ? "true" : "false";
|
|
277
|
+
default:
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function unsupportedMsg(where, what) {
|
|
283
|
+
return `${where}: ${what} is not yet supported across the wasm boundary (planned)`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Builds the boundary metadata for every exported symbol of `wasm`-target
|
|
287
|
+
// packages (plus struct descriptors of `both` packages they expose).
|
|
288
|
+
// Throws a compile error for unsupported boundary types.
|
|
289
|
+
export function collectBoundaryMeta(progs, funcDecls, mod) {
|
|
290
|
+
const meta = {
|
|
291
|
+
structs: [],
|
|
292
|
+
funcs: [],
|
|
293
|
+
consts: [],
|
|
294
|
+
structByName: new Map(),
|
|
295
|
+
_mod: mod,
|
|
296
|
+
};
|
|
297
|
+
const errors = [];
|
|
298
|
+
|
|
299
|
+
for (const p of progs) {
|
|
300
|
+
const pkgName = p.pkg?.name ?? "main";
|
|
301
|
+
const pkgTarget = p.target ?? "wasm";
|
|
302
|
+
for (const d of p.decls ?? []) {
|
|
303
|
+
if (d.kind === "TypeDecl" && d.type?.kind === "StructType") {
|
|
304
|
+
collectStructDecl(d, pkgName, pkgTarget, meta, mod);
|
|
305
|
+
} else if (d.kind === "ConstDecl" && pkgTarget === "wasm") {
|
|
306
|
+
collectConstDecl(d, pkgName, meta, errors);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const fn of funcDecls) {
|
|
312
|
+
if (fn._isClosure || fn._pkgTarget !== "wasm") continue;
|
|
313
|
+
if (fn._isMethod) collectMethodMeta(fn, meta, errors, mod);
|
|
314
|
+
else collectFuncMeta(fn, meta, errors, mod);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
|
|
318
|
+
return meta;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function collectStructDecl(d, pkgName, pkgTarget, meta, mod) {
|
|
322
|
+
if (!isExported(d.name) || meta.structByName.has(d.name)) return;
|
|
323
|
+
const info = mod.getStructType(d.name);
|
|
324
|
+
if (!info) return;
|
|
325
|
+
const s = { name: d.name, pkgName, pkgTarget, info, methods: [] };
|
|
326
|
+
meta.structs.push(s);
|
|
327
|
+
meta.structByName.set(d.name, s);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function collectConstDecl(d, pkgName, meta, errors) {
|
|
331
|
+
for (const spec of d.decls ?? []) {
|
|
332
|
+
for (let i = 0; i < spec.names.length; i++) {
|
|
333
|
+
const name = spec.names[i];
|
|
334
|
+
if (!isExported(name)) continue;
|
|
335
|
+
const value = literalConst(spec, i);
|
|
336
|
+
if (value !== null) meta.consts.push({ name, value });
|
|
337
|
+
else
|
|
338
|
+
errors.push(
|
|
339
|
+
unsupportedMsg(
|
|
340
|
+
`${pkgName}.${name}`,
|
|
341
|
+
"a constant with a non-literal value",
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function checkDesc(desc, where, errors) {
|
|
349
|
+
if (desc.k === "unsupported") errors.push(unsupportedMsg(where, desc.what));
|
|
350
|
+
return desc;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function collectMethodMeta(fn, meta, errors, mod) {
|
|
354
|
+
if (!isExported(fn._methodName)) return;
|
|
355
|
+
const s = meta.structByName.get(fn._recvTypeName);
|
|
356
|
+
if (!s) return;
|
|
357
|
+
const where = `${fn._recvTypeName}.${fn._methodName}`;
|
|
358
|
+
s.methods.push({
|
|
359
|
+
name: fn._methodName,
|
|
360
|
+
exportName: fn._exportName,
|
|
361
|
+
ptrRecv: fn.params[0].type?.kind === "pointer",
|
|
362
|
+
params: fn.params
|
|
363
|
+
.slice(1)
|
|
364
|
+
.map((p) => checkDesc(classifyType(p.type, mod), where, errors)),
|
|
365
|
+
returns: returnDescs(fn.returnType, mod).map((d) =>
|
|
366
|
+
checkDesc(d, where, errors),
|
|
367
|
+
),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function collectFuncMeta(fn, meta, errors, mod) {
|
|
372
|
+
if (!isExported(fn.name)) return;
|
|
373
|
+
const where = `${fn._pkgName}.${fn.name}`;
|
|
374
|
+
meta.funcs.push({
|
|
375
|
+
name: fn.name,
|
|
376
|
+
exportName: fn.name,
|
|
377
|
+
pkgName: fn._pkgName,
|
|
378
|
+
params: (fn.params ?? []).map((p) =>
|
|
379
|
+
checkDesc(classifyType(p.type, mod), where, errors),
|
|
380
|
+
),
|
|
381
|
+
returns: returnDescs(fn.returnType, mod).map((d) =>
|
|
382
|
+
checkDesc(d, where, errors),
|
|
383
|
+
),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Descriptor walk ──────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
function visitStructNeed(desc, needs, errors, mod) {
|
|
390
|
+
if (needs.structs.has(desc.name)) return;
|
|
391
|
+
const entry = { info: desc.info, fields: [] };
|
|
392
|
+
needs.structs.set(desc.name, entry);
|
|
393
|
+
for (const f of desc.info.fields) {
|
|
394
|
+
const fd = classifyType(f.goType, mod);
|
|
395
|
+
if (fd.k === "unsupported")
|
|
396
|
+
errors.push(unsupportedMsg(`${desc.name}.${f.name}`, fd.what));
|
|
397
|
+
entry.fields.push({ name: f.name, desc: fd });
|
|
398
|
+
visitNeed(fd, needs, errors, mod);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function visitNeed(desc, needs, errors, mod) {
|
|
403
|
+
if (!desc || desc.k === "void") return;
|
|
404
|
+
switch (desc.k) {
|
|
405
|
+
case "struct":
|
|
406
|
+
visitStructNeed(desc, needs, errors, mod);
|
|
407
|
+
return;
|
|
408
|
+
case "slice":
|
|
409
|
+
case "array": {
|
|
410
|
+
const map = desc.k === "slice" ? needs.slices : needs.arrays;
|
|
411
|
+
if (map.has(desc.key)) return;
|
|
412
|
+
map.set(desc.key, desc);
|
|
413
|
+
visitNeed(desc.elem, needs, errors, mod);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
case "func":
|
|
417
|
+
if (needs.funcs.has(desc.key)) return;
|
|
418
|
+
needs.funcs.set(desc.key, desc);
|
|
419
|
+
visitSignatureNeeds(desc, needs, errors, mod);
|
|
420
|
+
return;
|
|
421
|
+
default:
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function visitSignatureNeeds(sig, needs, errors, mod) {
|
|
427
|
+
for (const p of sig.params) visitNeed(p, needs, errors, mod);
|
|
428
|
+
for (const r of sig.returns) visitNeed(r, needs, errors, mod);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Collects every descriptor reachable from the exported surface so helpers are
|
|
432
|
+
// emitted once per struct / slice key / array key / closure key.
|
|
433
|
+
function collectNeeds(meta) {
|
|
434
|
+
const mod = meta._mod;
|
|
435
|
+
const needs = {
|
|
436
|
+
structs: new Map(), // name -> { info, fields: [{ name, desc }] }
|
|
437
|
+
slices: new Map(), // key -> desc
|
|
438
|
+
arrays: new Map(), // key -> desc
|
|
439
|
+
funcs: new Map(), // key -> desc
|
|
440
|
+
};
|
|
441
|
+
const errors = [];
|
|
442
|
+
for (const s of meta.structs) {
|
|
443
|
+
if (s.pkgTarget === "wasm")
|
|
444
|
+
visitNeed(structDesc(s.info, false), needs, errors, mod);
|
|
445
|
+
for (const m of s.methods) visitSignatureNeeds(m, needs, errors, mod);
|
|
446
|
+
}
|
|
447
|
+
for (const f of meta.funcs) visitSignatureNeeds(f, needs, errors, mod);
|
|
448
|
+
if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
|
|
449
|
+
return needs;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── WASM-side helpers ────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
// Pre-lock: registers env imports for JS callbacks (`__invoke$<key>`).
|
|
455
|
+
export function scanBoundaryImports(mod, meta) {
|
|
456
|
+
const needs = collectNeeds(meta);
|
|
457
|
+
meta._needs = needs;
|
|
458
|
+
for (const [key, desc] of needs.funcs) {
|
|
459
|
+
mod.getOrAddFuncImport(
|
|
460
|
+
"env",
|
|
461
|
+
`__invoke$${key}`,
|
|
462
|
+
["externref", ...desc.closureInfo.paramWTypes.map(extType)],
|
|
463
|
+
desc.closureInfo.returnWTypes.map(extType),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Boundary representation of a wasm value type. i64 crosses as f64 so JS
|
|
469
|
+
// never sees (or allocates) a BigInt for Go `int`; every GC reference (struct,
|
|
470
|
+
// array, closure, anyref) crosses as externref. V8 only inlines the
|
|
471
|
+
// JS→wasm wrapper (passing f32/f64 unboxed) when the signature holds nothing
|
|
472
|
+
// but i32/i64/f32/f64/externref: a single anyref or `(ref $T)` in the
|
|
473
|
+
// signature costs a 16-byte HeapNumber per float argument and result.
|
|
474
|
+
function isGcRef(wType) {
|
|
475
|
+
return wType === "anyref" || (typeof wType === "object" && wType !== null);
|
|
476
|
+
}
|
|
477
|
+
function extType(wType) {
|
|
478
|
+
if (wType === "i64") return "f64";
|
|
479
|
+
if (isGcRef(wType)) return "externref";
|
|
480
|
+
return wType;
|
|
481
|
+
}
|
|
482
|
+
function needsConv(wTypes) {
|
|
483
|
+
return wTypes.some((w) => w === "i64" || isGcRef(w));
|
|
484
|
+
}
|
|
485
|
+
// Instructions converting the value on the stack from its boundary form.
|
|
486
|
+
function fromExt(wType) {
|
|
487
|
+
if (wType === "i64") return [{ op: "i64.trunc_sat_f64_s" }];
|
|
488
|
+
if (wType === "anyref") return [{ op: "any.convert_extern" }];
|
|
489
|
+
if (isGcRef(wType)) {
|
|
490
|
+
const out = [{ op: "any.convert_extern" }];
|
|
491
|
+
if (wType.typeIndex !== undefined)
|
|
492
|
+
out.push({ op: "ref.cast_null", typeIndex: wType.typeIndex });
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
// Instructions converting the value on the stack to its boundary form.
|
|
498
|
+
function toExt(wType) {
|
|
499
|
+
if (wType === "i64") return [{ op: "f64.convert_i64_s" }];
|
|
500
|
+
if (isGcRef(wType)) return [{ op: "extern.convert_any" }];
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
// Rewrites the values on top of the stack (types `wTypes`) with `conv`,
|
|
504
|
+
// spilling through locals starting at `localBase` when more than one.
|
|
505
|
+
function convertResults(wTypes, conv, localBase) {
|
|
506
|
+
if (wTypes.length === 0 || !needsConv(wTypes)) return [];
|
|
507
|
+
if (wTypes.length === 1) return conv(wTypes[0]);
|
|
508
|
+
const out = [];
|
|
509
|
+
for (let i = wTypes.length - 1; i >= 0; i--)
|
|
510
|
+
out.push({ op: "local.set", index: localBase + i });
|
|
511
|
+
for (let i = 0; i < wTypes.length; i++)
|
|
512
|
+
out.push({ op: "local.get", index: localBase + i }, ...conv(wTypes[i]));
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function zeroInstr(wType) {
|
|
517
|
+
if (wType === "i64") return { op: "i64.const", value: 0n };
|
|
518
|
+
if (wType === "f32" || wType === "f64")
|
|
519
|
+
return { op: `${wType}.const`, value: 0 };
|
|
520
|
+
if (typeof wType === "object" && wType !== null) {
|
|
521
|
+
return {
|
|
522
|
+
op: "ref.null",
|
|
523
|
+
heapType: wType.typeIndex ?? wType.heapType ?? "any",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (wType === "externref") return { op: "ref.null", heapType: "extern" };
|
|
527
|
+
if (wType === "anyref") return { op: "ref.null", heapType: "any" };
|
|
528
|
+
return { op: "i32.const", value: 0 };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Type entry at a flat type index (rec groups hold several entries each).
|
|
532
|
+
function typeAt(mod, index) {
|
|
533
|
+
if (index === undefined) return null;
|
|
534
|
+
let i = 0;
|
|
535
|
+
for (const entry of mod.types) {
|
|
536
|
+
const group = entry.form === "rec" ? entry.types : [entry];
|
|
537
|
+
if (index < i + group.length) return group[index - i];
|
|
538
|
+
i += group.length;
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Post user functions: appends marshalling helper functions and exports them.
|
|
544
|
+
export function addBoundaryHelpers(mod, meta) {
|
|
545
|
+
const needs = meta._needs ?? collectNeeds(meta);
|
|
546
|
+
const importFuncCount = mod.imports.filter((i) => i.kind === "func").length;
|
|
547
|
+
|
|
548
|
+
const addFunc = (
|
|
549
|
+
name,
|
|
550
|
+
params,
|
|
551
|
+
results,
|
|
552
|
+
body,
|
|
553
|
+
exported = true,
|
|
554
|
+
locals = [],
|
|
555
|
+
) => {
|
|
556
|
+
const idx = importFuncCount + mod.funcs.length;
|
|
557
|
+
const typeIndex = mod.getTypeIndex(params, results);
|
|
558
|
+
mod.funcs.push({
|
|
559
|
+
typeIndex,
|
|
560
|
+
locals,
|
|
561
|
+
body: [...body, { op: "return" }],
|
|
562
|
+
});
|
|
563
|
+
mod.funcMap.set(name, idx);
|
|
564
|
+
if (exported) mod.exports.push({ name, kind: "func", index: idx });
|
|
565
|
+
return idx;
|
|
566
|
+
};
|
|
567
|
+
const get = (i) => ({ op: "local.get", index: i });
|
|
568
|
+
// Parameter `i` converted from its boundary form.
|
|
569
|
+
const getIn = (i, wType) => [get(i), ...fromExt(wType)];
|
|
570
|
+
const EXT = "externref";
|
|
571
|
+
|
|
572
|
+
for (const [name, { info }] of needs.structs) {
|
|
573
|
+
const ref = toRef(info);
|
|
574
|
+
const ti = info.typeIndex;
|
|
575
|
+
const fieldWTypes = info.fields.map((f) => f.wType);
|
|
576
|
+
addFunc(
|
|
577
|
+
`__new_${name}`,
|
|
578
|
+
fieldWTypes.map(extType),
|
|
579
|
+
[EXT],
|
|
580
|
+
[
|
|
581
|
+
...fieldWTypes.flatMap((w, i) => getIn(i, w)),
|
|
582
|
+
{ op: "struct.new", typeIndex: ti },
|
|
583
|
+
...toExt(ref),
|
|
584
|
+
],
|
|
585
|
+
);
|
|
586
|
+
addFunc(
|
|
587
|
+
`__zero_${name}`,
|
|
588
|
+
[],
|
|
589
|
+
[EXT],
|
|
590
|
+
[
|
|
591
|
+
...fieldWTypes.map((w) => zeroInstr(w)),
|
|
592
|
+
{ op: "struct.new", typeIndex: ti },
|
|
593
|
+
...toExt(ref),
|
|
594
|
+
],
|
|
595
|
+
);
|
|
596
|
+
addFunc(
|
|
597
|
+
`__clone_${name}`,
|
|
598
|
+
[EXT],
|
|
599
|
+
[EXT],
|
|
600
|
+
[
|
|
601
|
+
...fieldWTypes.flatMap((_, i) => [
|
|
602
|
+
...getIn(0, ref),
|
|
603
|
+
{ op: "struct.get", typeIndex: ti, fieldIndex: i },
|
|
604
|
+
]),
|
|
605
|
+
{ op: "struct.new", typeIndex: ti },
|
|
606
|
+
...toExt(ref),
|
|
607
|
+
],
|
|
608
|
+
);
|
|
609
|
+
for (let i = 0; i < info.fields.length; i++) {
|
|
610
|
+
const f = info.fields[i];
|
|
611
|
+
addFunc(
|
|
612
|
+
`__get_${name}_${f.name}`,
|
|
613
|
+
[EXT],
|
|
614
|
+
[extType(f.wType)],
|
|
615
|
+
[
|
|
616
|
+
...getIn(0, ref),
|
|
617
|
+
{ op: "struct.get", typeIndex: ti, fieldIndex: i },
|
|
618
|
+
...toExt(f.wType),
|
|
619
|
+
],
|
|
620
|
+
);
|
|
621
|
+
addFunc(
|
|
622
|
+
`__set_${name}_${f.name}`,
|
|
623
|
+
[EXT, extType(f.wType)],
|
|
624
|
+
[],
|
|
625
|
+
[
|
|
626
|
+
...getIn(0, ref),
|
|
627
|
+
...getIn(1, f.wType),
|
|
628
|
+
{ op: "struct.set", typeIndex: ti, fieldIndex: i },
|
|
629
|
+
],
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const [key, desc] of needs.arrays) {
|
|
635
|
+
const { arrInfo } = desc;
|
|
636
|
+
const ref = desc.wType;
|
|
637
|
+
addFunc(
|
|
638
|
+
`__array_new_${key}`,
|
|
639
|
+
["i32"],
|
|
640
|
+
[EXT],
|
|
641
|
+
[
|
|
642
|
+
get(0),
|
|
643
|
+
{ op: "array.new_default", typeIndex: arrInfo.typeIndex },
|
|
644
|
+
...toExt(ref),
|
|
645
|
+
],
|
|
646
|
+
);
|
|
647
|
+
addFunc(
|
|
648
|
+
`__array_len_${key}`,
|
|
649
|
+
[EXT],
|
|
650
|
+
["i32"],
|
|
651
|
+
[...getIn(0, ref), { op: "array.len" }],
|
|
652
|
+
);
|
|
653
|
+
addFunc(
|
|
654
|
+
`__array_get_${key}`,
|
|
655
|
+
[EXT, "i32"],
|
|
656
|
+
[extType(arrInfo.elemWType)],
|
|
657
|
+
[
|
|
658
|
+
...getIn(0, ref),
|
|
659
|
+
get(1),
|
|
660
|
+
{ op: "array.get", typeIndex: arrInfo.typeIndex },
|
|
661
|
+
...toExt(arrInfo.elemWType),
|
|
662
|
+
],
|
|
663
|
+
);
|
|
664
|
+
addFunc(
|
|
665
|
+
`__array_set_${key}`,
|
|
666
|
+
[EXT, "i32", extType(arrInfo.elemWType)],
|
|
667
|
+
[],
|
|
668
|
+
[
|
|
669
|
+
...getIn(0, ref),
|
|
670
|
+
get(1),
|
|
671
|
+
...getIn(2, arrInfo.elemWType),
|
|
672
|
+
{ op: "array.set", typeIndex: arrInfo.typeIndex },
|
|
673
|
+
],
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (const [key, desc] of needs.slices) {
|
|
678
|
+
const { sliceInfo } = desc;
|
|
679
|
+
const ref = desc.wType;
|
|
680
|
+
const arrTI = sliceInfo.arrInfo.typeIndex;
|
|
681
|
+
const sTI = sliceInfo.typeIndex;
|
|
682
|
+
const elemAddr = [
|
|
683
|
+
...getIn(0, ref),
|
|
684
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 0 },
|
|
685
|
+
...getIn(0, ref),
|
|
686
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 1 },
|
|
687
|
+
get(1),
|
|
688
|
+
{ op: "i32.add" },
|
|
689
|
+
];
|
|
690
|
+
addFunc(
|
|
691
|
+
`__slice_new_${key}`,
|
|
692
|
+
["i32"],
|
|
693
|
+
[EXT],
|
|
694
|
+
[
|
|
695
|
+
get(0),
|
|
696
|
+
{ op: "array.new_default", typeIndex: arrTI },
|
|
697
|
+
{ op: "i32.const", value: 0 },
|
|
698
|
+
get(0),
|
|
699
|
+
get(0),
|
|
700
|
+
{ op: "struct.new", typeIndex: sTI },
|
|
701
|
+
...toExt(ref),
|
|
702
|
+
],
|
|
703
|
+
);
|
|
704
|
+
addFunc(
|
|
705
|
+
`__slice_len_${key}`,
|
|
706
|
+
[EXT],
|
|
707
|
+
["i32"],
|
|
708
|
+
[...getIn(0, ref), { op: "struct.get", typeIndex: sTI, fieldIndex: 2 }],
|
|
709
|
+
);
|
|
710
|
+
addFunc(
|
|
711
|
+
`__slice_get_${key}`,
|
|
712
|
+
[EXT, "i32"],
|
|
713
|
+
[extType(sliceInfo.elemWType)],
|
|
714
|
+
[
|
|
715
|
+
...elemAddr,
|
|
716
|
+
{ op: "array.get", typeIndex: arrTI },
|
|
717
|
+
...toExt(sliceInfo.elemWType),
|
|
718
|
+
],
|
|
719
|
+
);
|
|
720
|
+
addFunc(
|
|
721
|
+
`__slice_nil_${key}`,
|
|
722
|
+
[],
|
|
723
|
+
[EXT],
|
|
724
|
+
[{ op: "global.get", index: sliceInfo.emptyGlobalIndex }, ...toExt(ref)],
|
|
725
|
+
);
|
|
726
|
+
addFunc(
|
|
727
|
+
`__slice_is_nil_${key}`,
|
|
728
|
+
[EXT],
|
|
729
|
+
["i32"],
|
|
730
|
+
[
|
|
731
|
+
...getIn(0, ref),
|
|
732
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 0 },
|
|
733
|
+
{ op: "ref.is_null" },
|
|
734
|
+
],
|
|
735
|
+
);
|
|
736
|
+
addFunc(
|
|
737
|
+
`__slice_set_${key}`,
|
|
738
|
+
[EXT, "i32", extType(sliceInfo.elemWType)],
|
|
739
|
+
[],
|
|
740
|
+
[
|
|
741
|
+
...elemAddr,
|
|
742
|
+
...getIn(2, sliceInfo.elemWType),
|
|
743
|
+
{ op: "array.set", typeIndex: arrTI },
|
|
744
|
+
],
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const [key, desc] of needs.funcs) {
|
|
749
|
+
const ci = desc.closureInfo;
|
|
750
|
+
const invokeIdx = mod.importCache.get(`env.__invoke$${key}`);
|
|
751
|
+
const nParams = ci.paramWTypes.length;
|
|
752
|
+
// Thunk: wasm closure body calling the JS callback (boundary forms out, back in).
|
|
753
|
+
const thunkIdx = addFunc(
|
|
754
|
+
`__jsthunk$${key}`,
|
|
755
|
+
["anyref", ...ci.paramWTypes],
|
|
756
|
+
ci.returnWTypes,
|
|
757
|
+
[
|
|
758
|
+
get(0),
|
|
759
|
+
{ op: "extern.convert_any" },
|
|
760
|
+
...ci.paramWTypes.flatMap((w, i) => [get(i + 1), ...toExt(w)]),
|
|
761
|
+
{ op: "call", funcIndex: invokeIdx },
|
|
762
|
+
...convertResults(ci.returnWTypes, fromExt, nParams + 1),
|
|
763
|
+
],
|
|
764
|
+
false,
|
|
765
|
+
ci.returnWTypes.length > 1 ? ci.returnWTypes.map(extType) : [],
|
|
766
|
+
);
|
|
767
|
+
mod.addRefFuncElement(thunkIdx);
|
|
768
|
+
addFunc(
|
|
769
|
+
`__wrap_fn$${key}`,
|
|
770
|
+
[EXT],
|
|
771
|
+
[EXT],
|
|
772
|
+
[
|
|
773
|
+
{ op: "ref.func", funcIndex: thunkIdx },
|
|
774
|
+
get(0),
|
|
775
|
+
{ op: "any.convert_extern" },
|
|
776
|
+
{ op: "struct.new", typeIndex: ci.typeIndex },
|
|
777
|
+
...toExt(desc.wType),
|
|
778
|
+
],
|
|
779
|
+
);
|
|
780
|
+
// JS calling a wasm closure (boundary forms in, results out).
|
|
781
|
+
addFunc(
|
|
782
|
+
`__call_fn$${key}`,
|
|
783
|
+
[EXT, ...ci.paramWTypes.map(extType)],
|
|
784
|
+
ci.returnWTypes.map(extType),
|
|
785
|
+
[
|
|
786
|
+
...getIn(0, desc.wType),
|
|
787
|
+
{ op: "struct.get", typeIndex: ci.typeIndex, fieldIndex: 1 },
|
|
788
|
+
...ci.paramWTypes.flatMap((w, i) => getIn(i + 1, w)),
|
|
789
|
+
...getIn(0, desc.wType),
|
|
790
|
+
{ op: "struct.get", typeIndex: ci.typeIndex, fieldIndex: 0 },
|
|
791
|
+
{ op: "call_ref", typeIndex: ci.funcTypeIndex },
|
|
792
|
+
...convertResults(ci.returnWTypes, toExt, nParams + 1),
|
|
793
|
+
],
|
|
794
|
+
true,
|
|
795
|
+
ci.returnWTypes.length > 1 ? ci.returnWTypes : [],
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Exported user functions / methods whose signature carries i64 or GC refs
|
|
800
|
+
// get a trampoline (`__x_<export>`) with the boundary signature; the facade
|
|
801
|
+
// calls that instead of the raw export.
|
|
802
|
+
const addTrampoline = (entry) => {
|
|
803
|
+
const fnIdx = mod.funcMap.get(entry.exportName);
|
|
804
|
+
if (fnIdx === undefined) return;
|
|
805
|
+
const sig = typeAt(mod, mod.funcs[fnIdx - importFuncCount]?.typeIndex);
|
|
806
|
+
if (sig?.form !== "func") return;
|
|
807
|
+
const paramW = sig.params;
|
|
808
|
+
const resultW = sig.results;
|
|
809
|
+
if (!needsConv(paramW) && !needsConv(resultW)) return;
|
|
810
|
+
entry.callName = `__x_${entry.exportName}`;
|
|
811
|
+
addFunc(
|
|
812
|
+
entry.callName,
|
|
813
|
+
paramW.map(extType),
|
|
814
|
+
resultW.map(extType),
|
|
815
|
+
[
|
|
816
|
+
...paramW.flatMap((w, i) => getIn(i, w)),
|
|
817
|
+
{ op: "call", funcIndex: fnIdx },
|
|
818
|
+
...convertResults(resultW, toExt, paramW.length),
|
|
819
|
+
],
|
|
820
|
+
true,
|
|
821
|
+
resultW.length > 1 ? resultW : [],
|
|
822
|
+
);
|
|
823
|
+
};
|
|
824
|
+
for (const s of meta.structs) for (const m of s.methods) addTrampoline(m);
|
|
825
|
+
for (const f of meta.funcs) addTrampoline(f);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ── JS facade generation ─────────────────────────────────────
|
|
829
|
+
|
|
830
|
+
const P = "__gfw_"; // facade helper prefix
|
|
831
|
+
|
|
832
|
+
// JS expression converting JS value `v` into the wasm representation.
|
|
833
|
+
function inExpr(desc, v) {
|
|
834
|
+
switch (desc.k) {
|
|
835
|
+
case "bool":
|
|
836
|
+
return `(${v} ? 1 : 0)`;
|
|
837
|
+
case "i32":
|
|
838
|
+
return `(${v} | 0)`;
|
|
839
|
+
case "i64":
|
|
840
|
+
return `${P}i64in(${v})`;
|
|
841
|
+
case "f32":
|
|
842
|
+
case "f64":
|
|
843
|
+
return `(+${v})`;
|
|
844
|
+
case "string":
|
|
845
|
+
return `${P}strin(${v})`;
|
|
846
|
+
case "any":
|
|
847
|
+
return `(${v} === undefined ? null : ${v})`;
|
|
848
|
+
case "struct":
|
|
849
|
+
if (desc.info.pkgTarget === "both")
|
|
850
|
+
return `${P}to_${desc.name}(${v}, ${desc.ptr ? "false" : "true"})`;
|
|
851
|
+
return desc.ptr ? `${P}href(${v})` : `${P}hval_${desc.name}(${v})`;
|
|
852
|
+
case "slice":
|
|
853
|
+
return `${P}slin_${desc.key}(${v})`;
|
|
854
|
+
case "array":
|
|
855
|
+
return `${P}arrin_${desc.key}(${v})`;
|
|
856
|
+
case "func":
|
|
857
|
+
return `${P}fnin_${desc.key}(${v})`;
|
|
858
|
+
default:
|
|
859
|
+
return v;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// JS expression converting wasm value `v` into the JS representation.
|
|
864
|
+
function outExpr(desc, v) {
|
|
865
|
+
switch (desc.k) {
|
|
866
|
+
case "bool":
|
|
867
|
+
return `(${v} !== 0)`;
|
|
868
|
+
case "i32":
|
|
869
|
+
return desc.unsigned ? `(${v} >>> 0)` : v;
|
|
870
|
+
case "i64":
|
|
871
|
+
return desc.unsigned ? `${P}u64out(${v})` : `${P}i64out(${v})`;
|
|
872
|
+
case "string":
|
|
873
|
+
return `(${v} ?? "")`;
|
|
874
|
+
case "struct":
|
|
875
|
+
return desc.info.pkgTarget === "both"
|
|
876
|
+
? `${P}from_${desc.name}(${v})`
|
|
877
|
+
: `${desc.name}.__wrap(${v})`;
|
|
878
|
+
case "slice":
|
|
879
|
+
return `${P}slout_${desc.key}(${v})`;
|
|
880
|
+
case "array":
|
|
881
|
+
return `${P}arrout_${desc.key}(${v})`;
|
|
882
|
+
case "func":
|
|
883
|
+
return `${P}fnout_${desc.key}(${v})`;
|
|
884
|
+
default:
|
|
885
|
+
return v;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function zeroExpr(desc) {
|
|
890
|
+
switch (desc.k) {
|
|
891
|
+
case "bool":
|
|
892
|
+
return "false";
|
|
893
|
+
case "i32":
|
|
894
|
+
case "i64":
|
|
895
|
+
case "f32":
|
|
896
|
+
case "f64":
|
|
897
|
+
return "0";
|
|
898
|
+
case "string":
|
|
899
|
+
return '""';
|
|
900
|
+
case "struct":
|
|
901
|
+
return desc.ptr ? "null" : `new ${desc.name}()`;
|
|
902
|
+
case "array":
|
|
903
|
+
return `${P}arrzero_${desc.key}(${desc.size})`;
|
|
904
|
+
default:
|
|
905
|
+
return "null";
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// JS expression exposing wasm storage `v` reached through a handle field as a
|
|
910
|
+
// *live* JS value: aggregate fields alias the wasm object instead of copying
|
|
911
|
+
// it, so `h.Pos.X = 1` and `h.Items[0] = 2` reach wasm like they do in Go.
|
|
912
|
+
function viewExpr(desc, v) {
|
|
913
|
+
switch (desc.k) {
|
|
914
|
+
case "struct":
|
|
915
|
+
return desc.info.pkgTarget === "both"
|
|
916
|
+
? `${P}view_${desc.name}(${v})`
|
|
917
|
+
: `${desc.name}.__wrap(${v})`;
|
|
918
|
+
case "slice":
|
|
919
|
+
return `${P}slview_${desc.key}(${v})`;
|
|
920
|
+
case "array":
|
|
921
|
+
return `${P}arrview_${desc.key}(${v})`;
|
|
922
|
+
default:
|
|
923
|
+
return outExpr(desc, v);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function isPtrBoth(d) {
|
|
928
|
+
return d.k === "struct" && d.ptr && d.info.pkgTarget === "both";
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function isBothStruct(d) {
|
|
932
|
+
return d.k === "struct" && d.info.pkgTarget === "both";
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function returnLines(returns, resultExpr) {
|
|
936
|
+
if (returns.length === 0) return [];
|
|
937
|
+
if (returns.length === 1)
|
|
938
|
+
return [`\treturn ${outExpr(returns[0], resultExpr)};`];
|
|
939
|
+
return [
|
|
940
|
+
`\treturn [${returns.map((d, i) => outExpr(d, `${resultExpr}[${i}]`)).join(", ")}];`,
|
|
941
|
+
];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Generates the body of a JS wrapper calling `__w.<exportName>(leadArgs..., params...)`.
|
|
945
|
+
function genFuncBody(exportName, params, returns, leadArgs = []) {
|
|
946
|
+
const names = params.map((_, i) => `a${i}`);
|
|
947
|
+
const lines = [];
|
|
948
|
+
const argExprs = [...leadArgs];
|
|
949
|
+
const writeBack = params.some(isPtrBoth);
|
|
950
|
+
if (writeBack) lines.push("\tconst __m = new Map();");
|
|
951
|
+
for (let i = 0; i < params.length; i++) {
|
|
952
|
+
const d = params[i];
|
|
953
|
+
if (isPtrBoth(d)) {
|
|
954
|
+
lines.push(`\tconst __p${i} = ${P}to_${d.name}(a${i}, false, __m);`);
|
|
955
|
+
argExprs.push(`__p${i}`);
|
|
956
|
+
} else {
|
|
957
|
+
argExprs.push(inExpr(d, names[i]));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const call = `__w.${exportName}(${argExprs.join(", ")})`;
|
|
961
|
+
lines.push("\ttry {");
|
|
962
|
+
if (!writeBack) {
|
|
963
|
+
if (returns.length === 0) lines.push(`\t\t${call};`);
|
|
964
|
+
else if (returns.length === 1)
|
|
965
|
+
lines.push(...returnLines(returns, call).map((l) => `\t${l}`));
|
|
966
|
+
else {
|
|
967
|
+
lines.push(`\t\tconst __r = ${call};`);
|
|
968
|
+
lines.push(...returnLines(returns, "__r").map((l) => `\t${l}`));
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
lines.push(`\t\tconst __r = ${call};`);
|
|
972
|
+
for (let i = 0; i < params.length; i++) {
|
|
973
|
+
if (isPtrBoth(params[i]))
|
|
974
|
+
lines.push(`\t\t${P}back_${params[i].name}(__p${i}, a${i});`);
|
|
975
|
+
}
|
|
976
|
+
lines.push(...returnLines(returns, "__r").map((l) => `\t${l}`));
|
|
977
|
+
}
|
|
978
|
+
lines.push(`\t} catch (e) { throw ${P}mapTrap(e); }`);
|
|
979
|
+
return { names, body: lines.join("\n") };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function indent(text, prefix) {
|
|
983
|
+
return text.replace(/^/gm, prefix);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Runtime import table shared by the facade and glue.js (single source so
|
|
987
|
+
// the tests exercise the same env the shipped bundle uses). Written to
|
|
988
|
+
// survive the built-in mangler: no shorthand destructuring, import keys
|
|
989
|
+
// quoted, helpers prefixed with `__`.
|
|
990
|
+
export const WASM_IMPORTS_JS = `const ${P}MAX = 9007199254740991n;
|
|
991
|
+
function ${P}imports(stringTable, extraEnv, tag, write) {
|
|
992
|
+
let buf = [];
|
|
993
|
+
let targs = [];
|
|
994
|
+
const flush = () => { const s = buf.join(" "); buf = []; write(s); };
|
|
995
|
+
const print = (v) => { buf.push(String(v)); };
|
|
996
|
+
const println = (v) => { buf.push(String(v)); flush(); };
|
|
997
|
+
const targ = (v) => { targs.push(v); };
|
|
998
|
+
const env = {
|
|
999
|
+
"panicTag": tag,
|
|
1000
|
+
"panic": (msg) => { throw new Error(String(msg)); },
|
|
1001
|
+
"str": (i) => stringTable[i] ?? "",
|
|
1002
|
+
"str_len": (s) => (s ? s.length : 0),
|
|
1003
|
+
"str_concat": (a, b) => (a ?? "") + (b ?? ""),
|
|
1004
|
+
"str_eq": (a, b) => (a === b ? 1 : 0),
|
|
1005
|
+
"str_ne": (a, b) => (a !== b ? 1 : 0),
|
|
1006
|
+
"str_lt": (a, b) => ((a ?? "") < (b ?? "") ? 1 : 0),
|
|
1007
|
+
"str_le": (a, b) => ((a ?? "") <= (b ?? "") ? 1 : 0),
|
|
1008
|
+
"str_gt": (a, b) => ((a ?? "") > (b ?? "") ? 1 : 0),
|
|
1009
|
+
"str_ge": (a, b) => ((a ?? "") >= (b ?? "") ? 1 : 0),
|
|
1010
|
+
"str_get": (s, i) => (s ? s.charCodeAt(i) : 0),
|
|
1011
|
+
"str_slice": (s, a, b) => (s ? s.slice(a, b) : ""),
|
|
1012
|
+
"str_from_code_point": (c) => String.fromCodePoint(c),
|
|
1013
|
+
"str_code_point_at": (s, i) => (s ? s.codePointAt(i) : 0),
|
|
1014
|
+
"is_string": (v) => (typeof v === "string" ? 1 : 0),
|
|
1015
|
+
"print_i32": print, "print_i64": print, "print_f32": print, "print_f64": print,
|
|
1016
|
+
"print_str": print, "print_any": print,
|
|
1017
|
+
"print_bool": (b) => print(b !== 0 ? "true" : "false"),
|
|
1018
|
+
"println_i32": println, "println_i64": println, "println_f32": println, "println_f64": println,
|
|
1019
|
+
"println_str": println, "println_any": println,
|
|
1020
|
+
"println_bool": (b) => println(b !== 0 ? "true" : "false"),
|
|
1021
|
+
"println_empty": flush,
|
|
1022
|
+
"testing_arg_i32": targ, "testing_arg_f32": targ, "testing_arg_f64": targ,
|
|
1023
|
+
"testing_arg_str": targ, "testing_arg_any": targ,
|
|
1024
|
+
"testing_arg_i64": (v) => targ(v > ${P}MAX || v < -${P}MAX ? v : Number(v)),
|
|
1025
|
+
"testing_arg_bool": (v) => targ(v !== 0),
|
|
1026
|
+
"testing_call": (t, i) => { const a = targs; targs = []; t[stringTable[i]](...a); },
|
|
1027
|
+
"testing_name": (t) => t.Name(),
|
|
1028
|
+
"testing_flag": (t, i) => (t[stringTable[i]]() ? 1 : 0),
|
|
1029
|
+
};
|
|
1030
|
+
Object.assign(env, extraEnv);
|
|
1031
|
+
const m = {};
|
|
1032
|
+
for (const k of ["sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "exp", "log", "log2", "log10", "round"]) m[k] = Math[k];
|
|
1033
|
+
return { "env": env, "Math": m };
|
|
1034
|
+
}`;
|
|
1035
|
+
|
|
1036
|
+
// Runtime loader emitted at the top of the facade.
|
|
1037
|
+
export const WASM_LOADER_JS = `${WASM_IMPORTS_JS}
|
|
1038
|
+
async function ${P}fetch(url) {
|
|
1039
|
+
const res = await fetch(url);
|
|
1040
|
+
if (!res.ok) throw new Error("GoFront: failed to fetch " + url + " (" + res.status + ")");
|
|
1041
|
+
return res.arrayBuffer();
|
|
1042
|
+
}
|
|
1043
|
+
async function ${P}instantiate(imports) {
|
|
1044
|
+
const bytes = globalThis.__GOFRONT_WASM_BYTES;
|
|
1045
|
+
if (bytes) return WebAssembly.instantiate(bytes, imports);
|
|
1046
|
+
const url = globalThis.__GOFRONT_WASM_URL ?? "app.wasm";
|
|
1047
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
1048
|
+
try { return await WebAssembly.instantiateStreaming(fetch(url), imports); }
|
|
1049
|
+
catch { /* fall through: wrong MIME type or no streaming support */ }
|
|
1050
|
+
}
|
|
1051
|
+
return WebAssembly.instantiate(await ${P}fetch(url), imports);
|
|
1052
|
+
}
|
|
1053
|
+
async function ${P}load(stringTable, extraEnv) {
|
|
1054
|
+
if (typeof WebAssembly === "undefined" || typeof WebAssembly.Tag !== "function") {
|
|
1055
|
+
throw new Error("GoFront: this runtime lacks the WebAssembly GC / exception support required by app.wasm");
|
|
1056
|
+
}
|
|
1057
|
+
const tag = new WebAssembly.Tag({ "parameters": ["externref"] });
|
|
1058
|
+
const imports = ${P}imports(stringTable, extraEnv, tag, (s) => console.log(s));
|
|
1059
|
+
// Panics arrive as plain JS Errors thrown by env.panic (also from the
|
|
1060
|
+
// start function while package-level initializers run), so exports are
|
|
1061
|
+
// returned raw: no try/catch wrapper, which keeps JS→wasm calls inlinable.
|
|
1062
|
+
const result = await ${P}instantiate(imports);
|
|
1063
|
+
return result.instance.exports;
|
|
1064
|
+
}
|
|
1065
|
+
// Go int/int64 cross the boundary as f64 (exact within the safe-integer range).
|
|
1066
|
+
const ${P}i64in = (v) => (typeof v === "bigint" ? Number(v) : +v);
|
|
1067
|
+
const ${P}i64out = (v) => {
|
|
1068
|
+
if (v > ${P}MAX || v < -${P}MAX) throw new RangeError("GoFront: int64 value " + v + " exceeds the safe JS integer range");
|
|
1069
|
+
return v;
|
|
1070
|
+
};
|
|
1071
|
+
const ${P}u64out = (v) => {
|
|
1072
|
+
if (v < 0 || v > ${P}MAX) throw new RangeError("GoFront: uint64 value exceeds the safe JS integer range");
|
|
1073
|
+
return v;
|
|
1074
|
+
};
|
|
1075
|
+
const ${P}strin = (v) => (v == null ? "" : String(v));
|
|
1076
|
+
const ${P}href = (h) => (h == null ? null : h.__ref);
|
|
1077
|
+
const ${P}NIL_DEREF_PATTERNS = [
|
|
1078
|
+
"dereferencing a null pointer", // V8
|
|
1079
|
+
"dereferencing null pointer", // SpiderMonkey
|
|
1080
|
+
"null pointer dereference", // SpiderMonkey
|
|
1081
|
+
"null dereference", // JavaScriptCore
|
|
1082
|
+
];
|
|
1083
|
+
function ${P}mapTrap(e) {
|
|
1084
|
+
if (typeof WebAssembly !== "undefined" && e instanceof WebAssembly.RuntimeError) {
|
|
1085
|
+
const msg = e.message || "";
|
|
1086
|
+
for (const p of ${P}NIL_DEREF_PATTERNS) {
|
|
1087
|
+
if (msg.includes(p)) {
|
|
1088
|
+
return new Error("runtime error: invalid memory address or nil pointer dereference");
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return e;
|
|
1093
|
+
}
|
|
1094
|
+
// Live index view over a wasm array/slice (reads and writes go through to wasm).
|
|
1095
|
+
function ${P}idxview(n, getAt, setAt) {
|
|
1096
|
+
const inRange = (k) => { if (typeof k !== "string") return -1; const i = +k; return i === (i | 0) && i >= 0 && i < n ? i : -1; };
|
|
1097
|
+
return new Proxy(new Array(n), {
|
|
1098
|
+
"get": (t, k, r) => { const i = inRange(k); return i < 0 ? Reflect.get(t, k, r) : getAt(i); },
|
|
1099
|
+
"set": (t, k, v, r) => { const i = inRange(k); if (i < 0) return Reflect.set(t, k, v, r); setAt(i, v); return true; },
|
|
1100
|
+
"has": (t, k) => inRange(k) >= 0 || Reflect.has(t, k),
|
|
1101
|
+
"getOwnPropertyDescriptor": (t, k) => {
|
|
1102
|
+
const i = inRange(k);
|
|
1103
|
+
if (i < 0) return Reflect.getOwnPropertyDescriptor(t, k);
|
|
1104
|
+
return { "value": getAt(i), "writable": true, "enumerable": true, "configurable": true };
|
|
1105
|
+
},
|
|
1106
|
+
"ownKeys": (t) => { const keys = []; for (let i = 0; i < n; i++) keys.push(String(i)); keys.push("length"); return keys; },
|
|
1107
|
+
});
|
|
1108
|
+
}`;
|
|
1109
|
+
|
|
1110
|
+
// Generates the JS facade for the linked wasm module.
|
|
1111
|
+
export function generateFacade(
|
|
1112
|
+
meta,
|
|
1113
|
+
{ stringTable = [], callMain = false } = {},
|
|
1114
|
+
) {
|
|
1115
|
+
const needs = meta._needs ?? collectNeeds(meta);
|
|
1116
|
+
const out = [WASM_LOADER_JS];
|
|
1117
|
+
|
|
1118
|
+
// JS callback imports (registered before instantiation; bodies run after).
|
|
1119
|
+
out.push(`const ${P}env = {};`);
|
|
1120
|
+
for (const [key, desc] of needs.funcs) {
|
|
1121
|
+
const names = desc.params.map((_, i) => `a${i}`);
|
|
1122
|
+
const call = `fn(${desc.params.map((d, i) => outExpr(d, names[i])).join(", ")})`;
|
|
1123
|
+
let body;
|
|
1124
|
+
if (desc.returns.length === 0) body = `${call};`;
|
|
1125
|
+
else if (desc.returns.length === 1)
|
|
1126
|
+
body = `return ${inExpr(desc.returns[0], call)};`;
|
|
1127
|
+
else
|
|
1128
|
+
body = `const __r = ${call}; return [${desc.returns
|
|
1129
|
+
.map((d, i) => inExpr(d, `__r[${i}]`))
|
|
1130
|
+
.join(", ")}];`;
|
|
1131
|
+
const sig = ["fn", ...names].join(", ");
|
|
1132
|
+
out.push(`${P}env["__invoke$${key}"] = (${sig}) => { ${body} };`);
|
|
1133
|
+
}
|
|
1134
|
+
out.push(
|
|
1135
|
+
`const __w = await ${P}load(${JSON.stringify(stringTable)}, ${P}env);`,
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
for (const [name, { info, fields }] of needs.structs) {
|
|
1139
|
+
if (info.pkgTarget === "both") genBothStruct(out, name, fields);
|
|
1140
|
+
else {
|
|
1141
|
+
const methods = meta.structByName.get(name)?.methods ?? [];
|
|
1142
|
+
genHandleStruct(out, name, fields, methods);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
for (const [key, desc] of needs.slices) {
|
|
1147
|
+
out.push(`function ${P}slin_${key}(arr) {
|
|
1148
|
+
if (arr == null) return __w.__slice_nil_${key}();
|
|
1149
|
+
const n = arr.length;
|
|
1150
|
+
const s = __w.__slice_new_${key}(n);
|
|
1151
|
+
for (let i = 0; i < n; i++) __w.__slice_set_${key}(s, i, ${inExpr(desc.elem, "arr[i]")});
|
|
1152
|
+
return s;
|
|
1153
|
+
}
|
|
1154
|
+
function ${P}slout_${key}(s) {
|
|
1155
|
+
if (s == null || __w.__slice_is_nil_${key}(s)) return null;
|
|
1156
|
+
const n = __w.__slice_len_${key}(s);
|
|
1157
|
+
const out = new Array(n);
|
|
1158
|
+
for (let i = 0; i < n; i++) out[i] = ${outExpr(desc.elem, `__w.__slice_get_${key}(s, i)`)};
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
function ${P}slview_${key}(s) {
|
|
1162
|
+
if (s == null || __w.__slice_is_nil_${key}(s)) return null;
|
|
1163
|
+
return ${P}idxview(__w.__slice_len_${key}(s), (i) => ${viewExpr(desc.elem, `__w.__slice_get_${key}(s, i)`)}, (i, v) => __w.__slice_set_${key}(s, i, ${inExpr(desc.elem, "v")}));
|
|
1164
|
+
}`);
|
|
1165
|
+
}
|
|
1166
|
+
for (const [key, desc] of needs.arrays) {
|
|
1167
|
+
out.push(`function ${P}arrzero_${key}(n) {
|
|
1168
|
+
const out = new Array(n);
|
|
1169
|
+
for (let i = 0; i < n; i++) out[i] = ${zeroExpr(desc.elem)};
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1172
|
+
function ${P}arrin_${key}(arr) {
|
|
1173
|
+
if (arr == null) return null;
|
|
1174
|
+
const n = arr.length;
|
|
1175
|
+
const a = __w.__array_new_${key}(n);
|
|
1176
|
+
for (let i = 0; i < n; i++) __w.__array_set_${key}(a, i, ${inExpr(desc.elem, "arr[i]")});
|
|
1177
|
+
return a;
|
|
1178
|
+
}
|
|
1179
|
+
function ${P}arrout_${key}(a) {
|
|
1180
|
+
if (a == null) return null;
|
|
1181
|
+
const n = __w.__array_len_${key}(a);
|
|
1182
|
+
const out = new Array(n);
|
|
1183
|
+
for (let i = 0; i < n; i++) out[i] = ${outExpr(desc.elem, `__w.__array_get_${key}(a, i)`)};
|
|
1184
|
+
return out;
|
|
1185
|
+
}
|
|
1186
|
+
function ${P}arrview_${key}(a) {
|
|
1187
|
+
if (a == null) return null;
|
|
1188
|
+
return ${P}idxview(__w.__array_len_${key}(a), (i) => ${viewExpr(desc.elem, `__w.__array_get_${key}(a, i)`)}, (i, v) => __w.__array_set_${key}(a, i, ${inExpr(desc.elem, "v")}));
|
|
1189
|
+
}`);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
for (const [key, desc] of needs.funcs) {
|
|
1193
|
+
const { names, body } = genFuncBody(
|
|
1194
|
+
`__call_fn$${key}`,
|
|
1195
|
+
desc.params,
|
|
1196
|
+
desc.returns,
|
|
1197
|
+
["c"],
|
|
1198
|
+
);
|
|
1199
|
+
out.push(`const ${P}fnmap_${key} = new WeakMap();
|
|
1200
|
+
function ${P}fnin_${key}(fn) {
|
|
1201
|
+
if (fn == null) return null;
|
|
1202
|
+
let c = ${P}fnmap_${key}.get(fn);
|
|
1203
|
+
if (!c) { c = __w.__wrap_fn$${key}(fn); ${P}fnmap_${key}.set(fn, c); }
|
|
1204
|
+
return c;
|
|
1205
|
+
}
|
|
1206
|
+
function ${P}fnout_${key}(c) {
|
|
1207
|
+
if (c == null) return null;
|
|
1208
|
+
return (${names.join(", ")}) => {
|
|
1209
|
+
${indent(body, "\t")}
|
|
1210
|
+
};
|
|
1211
|
+
}`);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
for (const c of meta.consts) out.push(`const ${c.name} = ${c.value};`);
|
|
1215
|
+
|
|
1216
|
+
for (const f of meta.funcs) {
|
|
1217
|
+
const { names, body } = genFuncBody(
|
|
1218
|
+
f.callName ?? f.exportName,
|
|
1219
|
+
f.params,
|
|
1220
|
+
f.returns,
|
|
1221
|
+
);
|
|
1222
|
+
out.push(`function ${f.name}(${names.join(", ")}) {\n${body}\n}`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (callMain) out.push("__w.main();");
|
|
1226
|
+
return bindExports(out.join("\n"));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Rewrites `__w.name(...)` into calls through module-level `const` bindings.
|
|
1230
|
+
// V8 only inlines a JS→wasm call (and so passes f32/f64 arguments unboxed)
|
|
1231
|
+
// when the callee is a compile-time constant; a property load off the exports
|
|
1232
|
+
// object is not, and every float argument then costs a 16-byte HeapNumber.
|
|
1233
|
+
function bindExports(src) {
|
|
1234
|
+
const loadLine = "const __w = await ";
|
|
1235
|
+
const at = src.indexOf(loadLine);
|
|
1236
|
+
if (at < 0) return src;
|
|
1237
|
+
const eol = src.indexOf("\n", at);
|
|
1238
|
+
const head = src.slice(0, eol + 1);
|
|
1239
|
+
let tail = src.slice(eol + 1);
|
|
1240
|
+
const names = new Set();
|
|
1241
|
+
tail = tail.replace(/__w\.([A-Za-z_$][\w$]*)/g, (_, n) => {
|
|
1242
|
+
names.add(n);
|
|
1243
|
+
return `__w$${n}`;
|
|
1244
|
+
});
|
|
1245
|
+
const binds = [...names].map((n) => `const __w$${n} = __w.${n};`).join("\n");
|
|
1246
|
+
return `${head}${binds}\n${tail}`;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function genBothStruct(out, name, fields) {
|
|
1250
|
+
const toArgs = fields.map((f) => {
|
|
1251
|
+
const v = `o.${f.name}`;
|
|
1252
|
+
if (isBothStruct(f.desc))
|
|
1253
|
+
return `${P}to_${f.desc.name}(${v}, ${f.desc.ptr ? "false" : "true"}, m)`;
|
|
1254
|
+
return inExpr(f.desc, v);
|
|
1255
|
+
});
|
|
1256
|
+
const backLines = fields.map((f) => {
|
|
1257
|
+
const got = `__w.__get_${name}_${f.name}(r)`;
|
|
1258
|
+
if (isBothStruct(f.desc))
|
|
1259
|
+
return `\t${P}back_${f.desc.name}(${got}, o.${f.name});`;
|
|
1260
|
+
return `\to.${f.name} = ${outExpr(f.desc, got)};`;
|
|
1261
|
+
});
|
|
1262
|
+
const fromArgs = fields.map((f) =>
|
|
1263
|
+
outExpr(f.desc, `__w.__get_${name}_${f.name}(r)`),
|
|
1264
|
+
);
|
|
1265
|
+
const viewProps = fields.map((f) => {
|
|
1266
|
+
const got = `__w.__get_${name}_${f.name}(this.__ref)`;
|
|
1267
|
+
return `"${f.name}": { "enumerable": true, "get"() { return ${viewExpr(f.desc, got)}; }, "set"(v) { __w.__set_${name}_${f.name}(this.__ref, ${inExpr(f.desc, "v")}); } }`;
|
|
1268
|
+
});
|
|
1269
|
+
out.push(`function ${P}to_${name}(o, isValue, m) {
|
|
1270
|
+
if (o == null) return isValue ? __w.__zero_${name}() : null;
|
|
1271
|
+
if (m) { const hit = m.get(o); if (hit) return hit; }
|
|
1272
|
+
const r = __w.__new_${name}(${toArgs.join(", ")});
|
|
1273
|
+
if (m) m.set(o, r);
|
|
1274
|
+
return r;
|
|
1275
|
+
}
|
|
1276
|
+
function ${P}from_${name}(r) {
|
|
1277
|
+
if (r == null) return null;
|
|
1278
|
+
return new ${name}(${fromArgs.join(", ")});
|
|
1279
|
+
}
|
|
1280
|
+
// Live views share one prototype and are cached per wasm object so hot paths
|
|
1281
|
+
// (\`h.Pos.X = 1\` in a frame loop) do not allocate a wrapper per access. The
|
|
1282
|
+
// prototype is built on first use: the facade may precede the \`both\`
|
|
1283
|
+
// package's own JS (and so the \`${name}\` class) in the bundle.
|
|
1284
|
+
let ${P}vp_${name} = null;
|
|
1285
|
+
const ${P}vc_${name} = new WeakMap();
|
|
1286
|
+
function ${P}view_${name}(r) {
|
|
1287
|
+
if (r == null) return null;
|
|
1288
|
+
let o = ${P}vc_${name}.get(r);
|
|
1289
|
+
if (!o) {
|
|
1290
|
+
if (${P}vp_${name} === null) ${P}vp_${name} = Object.create(${name}.prototype, { ${viewProps.join(", ")} });
|
|
1291
|
+
o = Object.create(${P}vp_${name}); o.__ref = r; ${P}vc_${name}.set(r, o);
|
|
1292
|
+
}
|
|
1293
|
+
return o;
|
|
1294
|
+
}
|
|
1295
|
+
function ${P}back_${name}(r, o) {
|
|
1296
|
+
if (r == null || o == null) return;
|
|
1297
|
+
${backLines.join("\n")}
|
|
1298
|
+
}`);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function genHandleStruct(out, name, fields, methods) {
|
|
1302
|
+
// `$`-suffixed params keep the mangler from renaming the matching getters.
|
|
1303
|
+
const ctorParams = fields.map((f) => `${f.name}$ = ${zeroExpr(f.desc)}`);
|
|
1304
|
+
const ctorArgs = fields.map((f) => inExpr(f.desc, `${f.name}$`));
|
|
1305
|
+
const lines = [
|
|
1306
|
+
`const ${P}h_${name} = new WeakMap();
|
|
1307
|
+
function ${P}hval_${name}(h) {
|
|
1308
|
+
if (h == null) return __w.__zero_${name}();
|
|
1309
|
+
return __w.__clone_${name}(h.__ref);
|
|
1310
|
+
}
|
|
1311
|
+
class ${name} {
|
|
1312
|
+
constructor(${ctorParams.join(", ")}) {
|
|
1313
|
+
this.__ref = __w.__new_${name}(${ctorArgs.join(", ")});
|
|
1314
|
+
${P}h_${name}.set(this.__ref, this);
|
|
1315
|
+
}
|
|
1316
|
+
static __wrap(ref) {
|
|
1317
|
+
if (ref == null) return null;
|
|
1318
|
+
let h = ${P}h_${name}.get(ref);
|
|
1319
|
+
if (!h) { h = Object.create(${name}.prototype); h.__ref = ref; ${P}h_${name}.set(ref, h); }
|
|
1320
|
+
return h;
|
|
1321
|
+
}
|
|
1322
|
+
__clone() { return ${name}.__wrap(__w.__clone_${name}(this.__ref)); }`,
|
|
1323
|
+
];
|
|
1324
|
+
for (const f of fields) {
|
|
1325
|
+
const got = `__w.__get_${name}_${f.name}(this.__ref)`;
|
|
1326
|
+
lines.push(`\tget ${f.name}() { return ${viewExpr(f.desc, got)}; }`);
|
|
1327
|
+
lines.push(
|
|
1328
|
+
`\tset ${f.name}(v) { __w.__set_${name}_${f.name}(this.__ref, ${inExpr(f.desc, "v")}); }`,
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
for (const m of methods) {
|
|
1332
|
+
const recv = m.ptrRecv ? "this.__ref" : `__w.__clone_${name}(this.__ref)`;
|
|
1333
|
+
const { names, body } = genFuncBody(
|
|
1334
|
+
m.callName ?? m.exportName,
|
|
1335
|
+
m.params,
|
|
1336
|
+
m.returns,
|
|
1337
|
+
[recv],
|
|
1338
|
+
);
|
|
1339
|
+
lines.push(
|
|
1340
|
+
`\t${m.name}(${names.join(", ")}) {\n${indent(body, "\t")}\n\t}`,
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
lines.push("}");
|
|
1344
|
+
out.push(lines.join("\n"));
|
|
1345
|
+
}
|