gofront 1.4.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 +174 -403
- package/package.json +16 -19
- package/src/{codegen → backend/js}/expressions.js +265 -77
- package/src/{codegen → backend/js}/index.js +120 -89
- package/src/{codegen → backend/js}/runtime.js +102 -0
- package/src/{codegen → backend/js}/source-map.js +32 -0
- package/src/{codegen → backend/js}/statements.js +139 -64
- package/src/backend/js/stdlib/bits.js +32 -0
- package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
- package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
- package/src/{codegen → backend/js}/stdlib/index.js +6 -0
- package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
- package/src/{codegen → backend/js}/stdlib/path.js +2 -2
- package/src/backend/js/stdlib/shared.js +17 -0
- package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
- package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
- package/src/backend/js/stdlib/strconv.js +58 -0
- package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
- package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
- package/src/{codegen → backend/js}/stdlib/time.js +3 -3
- package/src/{codegen → backend/js}/templ.js +3 -3
- package/src/backend/wasm/boundary.js +1798 -0
- package/src/backend/wasm/emit-builtins.js +1103 -0
- package/src/backend/wasm/emit-exprs.js +1908 -0
- package/src/backend/wasm/emit-maps.js +1863 -0
- package/src/backend/wasm/emit-shared.js +519 -0
- package/src/backend/wasm/emit-stdlib.js +3006 -0
- package/src/backend/wasm/emit-stmts.js +2472 -0
- package/src/backend/wasm/emit.js +1376 -0
- package/src/backend/wasm/encode.js +937 -0
- package/src/backend/wasm/glue.js +144 -0
- package/src/backend/wasm/index.js +2790 -0
- package/src/backend/wasm/monomorph.js +711 -0
- package/src/backend/wasm/optimize.js +227 -0
- package/src/backend/wasm/types.js +544 -0
- package/src/backend/wasm/wat.js +302 -0
- package/src/cli-core.js +323 -75
- package/src/compiler.js +447 -14
- package/src/dev-server.js +56 -15
- package/src/index.js +99 -120
- package/src/lexer.js +18 -0
- package/src/lower/boxing.js +35 -0
- package/src/lower/captures.js +172 -0
- package/src/lower/embedding.js +30 -0
- package/src/lower/escape.js +164 -0
- package/src/lower/functions.js +302 -0
- package/src/lower/globals.js +316 -0
- package/src/lower/index.js +118 -0
- package/src/lower/ownership.js +137 -0
- package/src/lower/range.js +67 -0
- package/src/lower/walk.js +25 -0
- package/src/minifier.js +2 -0
- package/src/parser/index.js +8 -3
- package/src/resolver.js +3 -1
- package/src/test-runner.js +94 -20
- package/src/typechecker/assignability.js +42 -1
- package/src/typechecker/expressions.js +182 -3
- package/src/typechecker/index.js +261 -5
- package/src/typechecker/resolve.js +35 -6
- package/src/typechecker/stdlib/core.js +34 -19
- package/src/typechecker/stdlib/extended.js +18 -3
- package/src/typechecker/stdlib/shared.js +53 -0
- package/src/typechecker/stdlib.js +2 -0
- package/src/typechecker/types.js +259 -6
- package/src/vendor.js +2 -2
- package/src/codegen/stdlib/strconv.js +0 -33
- /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/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/math.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/os.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/unicode.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
|
@@ -0,0 +1,1798 @@
|
|
|
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
|
+
// - named non-empty interfaces (Task H5.2): wasm values reach JS as their
|
|
24
|
+
// handle class / live view when the dynamic type is an exported `*T`,
|
|
25
|
+
// otherwise as a facade whose methods call the wasm dispatchers. Only
|
|
26
|
+
// wasm-owned values (`__ref`) go back in; JS-implemented values are
|
|
27
|
+
// rejected (no cross-boundary itab proxies).
|
|
28
|
+
// - maps, anonymous interfaces, error, pointers to non-structs: rejected
|
|
29
|
+
// with a compile error (planned for a later boundary version).
|
|
30
|
+
// - `gofront/shared` buffers (Task H6.1): `(base, len)` views over the
|
|
31
|
+
// module's exported linear memory; JS sees a zero-copy TypedArray over
|
|
32
|
+
// `memory.buffer`. Only TypedArrays that already alias that memory (ones
|
|
33
|
+
// handed out by this facade) may be passed back in.
|
|
34
|
+
// - `*testing.T`: the harness's JS test object, passed as an opaque
|
|
35
|
+
// externref (test methods are routed back to JS via `env.testing_*`).
|
|
36
|
+
|
|
37
|
+
import { HELPER_SPRINTF, HELPER_STRCONV } from "../js/runtime.js";
|
|
38
|
+
import { getSharedInfo, isTestingT } from "./types.js";
|
|
39
|
+
|
|
40
|
+
// ── Type classification ──────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const I32_INTS = new Set([
|
|
43
|
+
"int8",
|
|
44
|
+
"int16",
|
|
45
|
+
"int32",
|
|
46
|
+
"uint8",
|
|
47
|
+
"uint16",
|
|
48
|
+
"uint32",
|
|
49
|
+
"byte",
|
|
50
|
+
"rune",
|
|
51
|
+
]);
|
|
52
|
+
const I64_INTS = new Set(["int", "uint", "int64", "uint64", "uintptr"]);
|
|
53
|
+
const UNSIGNED = new Set([
|
|
54
|
+
"uint",
|
|
55
|
+
"uint8",
|
|
56
|
+
"uint16",
|
|
57
|
+
"uint32",
|
|
58
|
+
"uint64",
|
|
59
|
+
"uintptr",
|
|
60
|
+
"byte",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export function wTypeKey(wType) {
|
|
64
|
+
if (typeof wType === "string") return wType;
|
|
65
|
+
if (wType && typeof wType.typeIndex === "number")
|
|
66
|
+
return `r${wType.typeIndex}`;
|
|
67
|
+
return `h${wType?.heapType ?? "any"}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toRef(info) {
|
|
71
|
+
return { kind: "ref", nullable: true, typeIndex: info.typeIndex };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function structDesc(info, ptr) {
|
|
75
|
+
return { k: "struct", name: info.name, ptr, info, wType: toRef(info) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function funcSigFromAst(t) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "func",
|
|
81
|
+
params: (t.params ?? []).map((p) => p.type ?? p),
|
|
82
|
+
returns: t.returnType
|
|
83
|
+
? t.returnType.kind === "TupleType" || t.returnType.kind === "tuple"
|
|
84
|
+
? t.returnType.types
|
|
85
|
+
: [t.returnType]
|
|
86
|
+
: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Classifies a Go type (AST node or checker type) for boundary marshalling.
|
|
91
|
+
// Returns a descriptor `{ k, ... }`; `k === "unsupported"` carries `what`.
|
|
92
|
+
function classifyType(goType, mod) {
|
|
93
|
+
if (!goType) return { k: "void" };
|
|
94
|
+
if (isTestingT(goType)) return { k: "extern", wType: "externref" };
|
|
95
|
+
|
|
96
|
+
if (goType.kind === "TypeName" || goType.kind === "Ident") {
|
|
97
|
+
const info = mod.getStructType(goType.name);
|
|
98
|
+
if (info) return structDesc(info, false);
|
|
99
|
+
const resolved = mod.checker?.types?.get(goType.name);
|
|
100
|
+
if (resolved) return classifyType(resolved, mod);
|
|
101
|
+
return classifyBasic(goType.name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const t = normalizeAstType(goType);
|
|
105
|
+
switch (t.kind) {
|
|
106
|
+
case "named":
|
|
107
|
+
return classifyNamed(t, mod);
|
|
108
|
+
case "shared":
|
|
109
|
+
return classifyShared(t, mod);
|
|
110
|
+
case "untyped":
|
|
111
|
+
return classifyBasic(t.base);
|
|
112
|
+
case "basic":
|
|
113
|
+
return classifyBasic(t.name);
|
|
114
|
+
case "pointer":
|
|
115
|
+
return classifyPointer(t, mod);
|
|
116
|
+
case "slice":
|
|
117
|
+
return classifySlice(t, mod);
|
|
118
|
+
case "array":
|
|
119
|
+
return classifyArray(t, mod);
|
|
120
|
+
case "func":
|
|
121
|
+
return classifyFunc(t, mod);
|
|
122
|
+
case "interface":
|
|
123
|
+
return classifyInterface(t, mod);
|
|
124
|
+
default:
|
|
125
|
+
return {
|
|
126
|
+
k: "unsupported",
|
|
127
|
+
what: UNSUPPORTED_KINDS[t.kind] ?? `type kind '${t.kind}'`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const UNSUPPORTED_KINDS = {
|
|
133
|
+
map: "map",
|
|
134
|
+
MapType: "map",
|
|
135
|
+
struct: "anonymous struct",
|
|
136
|
+
StructType: "anonymous struct",
|
|
137
|
+
InterfaceType: "interface",
|
|
138
|
+
ChanType: "chan",
|
|
139
|
+
chan: "chan",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Maps parser AST type nodes onto the checker's type shapes.
|
|
143
|
+
function normalizeAstType(t) {
|
|
144
|
+
switch (t.kind) {
|
|
145
|
+
case "PointerType":
|
|
146
|
+
case "StarExpr":
|
|
147
|
+
return { kind: "pointer", base: t.base ?? t.expr ?? t.operand };
|
|
148
|
+
case "SliceType":
|
|
149
|
+
return { kind: "slice", elem: t.elem };
|
|
150
|
+
case "ArrayType":
|
|
151
|
+
return { kind: "array", elem: t.elem, size: t.size };
|
|
152
|
+
case "FuncType":
|
|
153
|
+
return funcSigFromAst(t);
|
|
154
|
+
case "InterfaceType":
|
|
155
|
+
return {
|
|
156
|
+
kind: "interface",
|
|
157
|
+
name: t.name,
|
|
158
|
+
methods: new Set((t.methods ?? []).map((m) => m.name ?? m)),
|
|
159
|
+
};
|
|
160
|
+
default:
|
|
161
|
+
return t;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const BASIC_DESCS = {
|
|
166
|
+
void: { k: "void" },
|
|
167
|
+
bool: { k: "bool", wType: "i32" },
|
|
168
|
+
float32: { k: "f32", wType: "f32" },
|
|
169
|
+
float64: { k: "f64", wType: "f64" },
|
|
170
|
+
string: { k: "string", wType: "externref" },
|
|
171
|
+
any: { k: "any", wType: "anyref" },
|
|
172
|
+
error: { k: "unsupported", what: "error" },
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function classifyBasic(n) {
|
|
176
|
+
if (I32_INTS.has(n))
|
|
177
|
+
return { k: "i32", unsigned: UNSIGNED.has(n), wType: "i32" };
|
|
178
|
+
if (I64_INTS.has(n))
|
|
179
|
+
return { k: "i64", unsigned: UNSIGNED.has(n), wType: "i64" };
|
|
180
|
+
const d = BASIC_DESCS[n];
|
|
181
|
+
return d ? { ...d } : { k: "unsupported", what: `type '${n}'` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function classifyNamed(t, mod) {
|
|
185
|
+
const info = mod.getStructType(t.name);
|
|
186
|
+
if (info) return structDesc(info, false);
|
|
187
|
+
if (t.underlying) return classifyType(t.underlying, mod);
|
|
188
|
+
return { k: "unsupported", what: `named type '${t.name}'` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function classifyPointer(t, mod) {
|
|
192
|
+
const base = classifyType(t.base, mod);
|
|
193
|
+
if (base.k === "struct" && !base.ptr) return { ...base, ptr: true };
|
|
194
|
+
const what = base.k === "unsupported" ? base.what : (t.base?.name ?? base.k);
|
|
195
|
+
return { k: "unsupported", what: `pointer to ${what}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function classifyShared(t, mod) {
|
|
199
|
+
const info = getSharedInfo(t, mod.checker);
|
|
200
|
+
return {
|
|
201
|
+
k: "shared",
|
|
202
|
+
key: info.key,
|
|
203
|
+
ctor: info.ctor,
|
|
204
|
+
bytes: info.bytes,
|
|
205
|
+
wType: mod.getSharedType().wType,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function classifySlice(t, mod) {
|
|
210
|
+
const elem = classifyType(t.elem, mod);
|
|
211
|
+
if (elem.k === "unsupported") return elem;
|
|
212
|
+
const sliceInfo = mod.getSliceType(t.elem);
|
|
213
|
+
return {
|
|
214
|
+
k: "slice",
|
|
215
|
+
elem,
|
|
216
|
+
sliceInfo,
|
|
217
|
+
key: wTypeKey(sliceInfo.elemWType),
|
|
218
|
+
wType: { kind: "ref", nullable: true, typeIndex: sliceInfo.typeIndex },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function arraySize(t) {
|
|
223
|
+
if (typeof t.size === "number") return t.size;
|
|
224
|
+
if (t.size?.value !== undefined) return Number(t.size.value);
|
|
225
|
+
return t.len ?? 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function classifyArray(t, mod) {
|
|
229
|
+
const elem = classifyType(t.elem, mod);
|
|
230
|
+
if (elem.k === "unsupported") return elem;
|
|
231
|
+
const arrInfo = mod.getArrayType(t.elem);
|
|
232
|
+
return {
|
|
233
|
+
k: "array",
|
|
234
|
+
elem,
|
|
235
|
+
size: arraySize(t),
|
|
236
|
+
arrInfo,
|
|
237
|
+
key: wTypeKey(arrInfo.elemWType),
|
|
238
|
+
wType: { kind: "ref", nullable: true, typeIndex: arrInfo.typeIndex },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function classifySig(t, mod) {
|
|
243
|
+
const params = (t.params ?? []).map((p) => classifyType(p, mod));
|
|
244
|
+
const returns = (t.returns ?? [])
|
|
245
|
+
.map((r) => classifyType(r, mod))
|
|
246
|
+
.filter((d) => d.k !== "void");
|
|
247
|
+
const bad = [...params, ...returns].find((d) => d.k === "unsupported");
|
|
248
|
+
return bad ?? { params, returns };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function classifyFunc(t, mod) {
|
|
252
|
+
const sig = classifySig(t, mod);
|
|
253
|
+
if (sig.k === "unsupported") return sig;
|
|
254
|
+
const closureInfo = mod.getClosureType(t);
|
|
255
|
+
return {
|
|
256
|
+
k: "func",
|
|
257
|
+
params: sig.params,
|
|
258
|
+
returns: sig.returns,
|
|
259
|
+
closureInfo,
|
|
260
|
+
key: `c${closureInfo.typeIndex}`,
|
|
261
|
+
wType: { kind: "ref", nullable: true, typeIndex: closureInfo.typeIndex },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function classifyInterface(t, mod) {
|
|
266
|
+
if (t.name === "error") return { k: "unsupported", what: "error" };
|
|
267
|
+
if (!t.methods || t.methods.size === 0) return { k: "any", wType: "anyref" };
|
|
268
|
+
if (!t.name || !(t.methods instanceof Map))
|
|
269
|
+
return { k: "unsupported", what: "anonymous interface" };
|
|
270
|
+
mod._boundaryIfaces ??= new Map();
|
|
271
|
+
return mod._boundaryIfaces.get(t.name) ?? classifyNamedInterface(t, mod);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function classifyNamedInterface(t, mod) {
|
|
275
|
+
const desc = {
|
|
276
|
+
k: "iface",
|
|
277
|
+
name: t.name,
|
|
278
|
+
key: t.name,
|
|
279
|
+
wType: "anyref",
|
|
280
|
+
methods: [],
|
|
281
|
+
concrete: [],
|
|
282
|
+
};
|
|
283
|
+
// Cached before the walk so self-referencing method signatures terminate.
|
|
284
|
+
mod._boundaryIfaces.set(t.name, desc);
|
|
285
|
+
for (const [mName, sigType] of t.methods) {
|
|
286
|
+
const sig = classifySig(sigType, mod);
|
|
287
|
+
if (sig.k === "unsupported") {
|
|
288
|
+
const err = {
|
|
289
|
+
k: "unsupported",
|
|
290
|
+
what: `${sig.what} (in method ${t.name}.${mName})`,
|
|
291
|
+
};
|
|
292
|
+
mod._boundaryIfaces.set(t.name, err);
|
|
293
|
+
return err;
|
|
294
|
+
}
|
|
295
|
+
desc.methods.push({
|
|
296
|
+
name: mName,
|
|
297
|
+
dispatch: `__dispatch_${t.name}_${mName}`,
|
|
298
|
+
...sig,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return desc;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Export metadata ──────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function isExported(name) {
|
|
307
|
+
return name[0] >= "A" && name[0] <= "Z";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function returnDescs(returnType, mod) {
|
|
311
|
+
if (!returnType) return [];
|
|
312
|
+
const list =
|
|
313
|
+
returnType.kind === "TupleType" || returnType.kind === "tuple"
|
|
314
|
+
? returnType.types
|
|
315
|
+
: [returnType];
|
|
316
|
+
return list.map((t) => classifyType(t, mod)).filter((d) => d.k !== "void");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function literalConst(spec, i) {
|
|
320
|
+
let node = spec.value?.[i];
|
|
321
|
+
let sign = "";
|
|
322
|
+
if (node?.kind === "UnaryExpr" && node.op === "-" && node.operand) {
|
|
323
|
+
node = node.operand;
|
|
324
|
+
sign = "-";
|
|
325
|
+
}
|
|
326
|
+
if (node?.kind !== "BasicLit") return null;
|
|
327
|
+
switch (node.litKind) {
|
|
328
|
+
case "INT":
|
|
329
|
+
case "FLOAT":
|
|
330
|
+
return `${sign}${node.value}`;
|
|
331
|
+
case "STRING":
|
|
332
|
+
return JSON.stringify(String(node.value));
|
|
333
|
+
case "BOOL":
|
|
334
|
+
return node.value === "true" ? "true" : "false";
|
|
335
|
+
default:
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function unsupportedMsg(where, what) {
|
|
341
|
+
return `${where}: ${what} is not yet supported across the wasm boundary (planned)`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Builds the boundary metadata for every exported symbol of `wasm`-target
|
|
345
|
+
// packages (plus struct descriptors of `both` packages they expose).
|
|
346
|
+
// Throws a compile error for unsupported boundary types.
|
|
347
|
+
export function collectBoundaryMeta(progs, funcDecls, mod) {
|
|
348
|
+
const meta = {
|
|
349
|
+
structs: [],
|
|
350
|
+
funcs: [],
|
|
351
|
+
consts: [],
|
|
352
|
+
vars: [], // exported package-level shared buffers of wasm packages
|
|
353
|
+
structByName: new Map(),
|
|
354
|
+
_mod: mod,
|
|
355
|
+
};
|
|
356
|
+
const errors = [];
|
|
357
|
+
|
|
358
|
+
for (const p of progs) {
|
|
359
|
+
const pkgName = p.pkg?.name ?? "main";
|
|
360
|
+
const pkgTarget = p.target ?? "wasm";
|
|
361
|
+
for (const d of p.decls ?? []) {
|
|
362
|
+
if (d.kind === "TypeDecl" && d.type?.kind === "StructType") {
|
|
363
|
+
collectStructDecl(d, pkgName, pkgTarget, meta, mod);
|
|
364
|
+
} else if (d.kind === "ConstDecl" && pkgTarget === "wasm") {
|
|
365
|
+
collectConstDecl(d, pkgName, meta, errors);
|
|
366
|
+
} else if (d.kind === "VarDecl" && pkgTarget === "wasm") {
|
|
367
|
+
collectSharedVarDecl(d, meta, mod);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const fn of funcDecls) {
|
|
373
|
+
if (fn._isClosure || fn._pkgTarget !== "wasm") continue;
|
|
374
|
+
if (fn._isMethod) collectMethodMeta(fn, meta, errors, mod);
|
|
375
|
+
else collectFuncMeta(fn, meta, errors, mod);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
|
|
379
|
+
return meta;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function collectStructDecl(d, pkgName, pkgTarget, meta, mod) {
|
|
383
|
+
if (!isExported(d.name) || meta.structByName.has(d.name)) return;
|
|
384
|
+
const info = mod.getStructType(d.name);
|
|
385
|
+
if (!info) return;
|
|
386
|
+
const s = { name: d.name, pkgName, pkgTarget, info, methods: [] };
|
|
387
|
+
meta.structs.push(s);
|
|
388
|
+
meta.structByName.set(d.name, s);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function collectConstDecl(d, pkgName, meta, errors) {
|
|
392
|
+
for (const spec of d.decls ?? []) {
|
|
393
|
+
for (let i = 0; i < spec.names.length; i++) {
|
|
394
|
+
const name = spec.names[i];
|
|
395
|
+
if (!isExported(name)) continue;
|
|
396
|
+
const value = literalConst(spec, i);
|
|
397
|
+
if (value !== null) meta.consts.push({ name, value });
|
|
398
|
+
else
|
|
399
|
+
errors.push(
|
|
400
|
+
unsupportedMsg(
|
|
401
|
+
`${pkgName}.${name}`,
|
|
402
|
+
"a constant with a non-literal value",
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function checkDesc(desc, where, errors) {
|
|
410
|
+
if (desc.k === "unsupported") errors.push(unsupportedMsg(where, desc.what));
|
|
411
|
+
return desc;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Exported `var Buf = shared.NewFloat32(n)` globals are exposed to JS as
|
|
415
|
+
// TypedArray views (read once after instantiation; sizing is fixed).
|
|
416
|
+
function collectSharedVarDecl(d, meta, mod) {
|
|
417
|
+
for (const spec of d.decls ?? d.specs ?? [d]) {
|
|
418
|
+
const names = spec.names ?? (spec.name ? [spec.name] : []);
|
|
419
|
+
const values = spec.value ?? (spec.init ? [spec.init] : []);
|
|
420
|
+
for (let i = 0; i < names.length; i++) {
|
|
421
|
+
const name = names[i];
|
|
422
|
+
if (!isExported(name)) continue;
|
|
423
|
+
const goType = spec.type ?? values[i]?._type;
|
|
424
|
+
if (!getSharedInfo(goType, mod.checker)) continue;
|
|
425
|
+
meta.vars.push({ name, desc: classifyType(goType, mod) });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function collectMethodMeta(fn, meta, errors, mod) {
|
|
431
|
+
if (!isExported(fn._methodName)) return;
|
|
432
|
+
const s = meta.structByName.get(fn._recvTypeName);
|
|
433
|
+
if (!s) return;
|
|
434
|
+
const where = `${fn._recvTypeName}.${fn._methodName}`;
|
|
435
|
+
s.methods.push({
|
|
436
|
+
name: fn._methodName,
|
|
437
|
+
exportName: fn._exportName,
|
|
438
|
+
fnName: fn.name,
|
|
439
|
+
ptrRecv: fn.params[0].type?.kind === "pointer",
|
|
440
|
+
params: fn.params
|
|
441
|
+
.slice(1)
|
|
442
|
+
.map((p) => checkDesc(classifyType(p.type, mod), where, errors)),
|
|
443
|
+
returns: returnDescs(fn.returnType, mod).map((d) =>
|
|
444
|
+
checkDesc(d, where, errors),
|
|
445
|
+
),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function collectFuncMeta(fn, meta, errors, mod) {
|
|
450
|
+
if (!isExported(fn.name)) return;
|
|
451
|
+
const where = `${fn._pkgName}.${fn.name}`;
|
|
452
|
+
meta.funcs.push({
|
|
453
|
+
name: fn.name,
|
|
454
|
+
exportName: fn.name,
|
|
455
|
+
pkgName: fn._pkgName,
|
|
456
|
+
params: (fn.params ?? []).map((p) =>
|
|
457
|
+
checkDesc(classifyType(p.type, mod), where, errors),
|
|
458
|
+
),
|
|
459
|
+
returns: returnDescs(fn.returnType, mod).map((d) =>
|
|
460
|
+
checkDesc(d, where, errors),
|
|
461
|
+
),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Descriptor walk ──────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
function visitStructNeed(desc, needs, errors, mod) {
|
|
468
|
+
if (needs.structs.has(desc.name)) return;
|
|
469
|
+
const entry = { info: desc.info, fields: [] };
|
|
470
|
+
needs.structs.set(desc.name, entry);
|
|
471
|
+
for (const f of desc.info.fields) {
|
|
472
|
+
const fd = classifyType(f.goType, mod);
|
|
473
|
+
if (fd.k === "unsupported")
|
|
474
|
+
errors.push(unsupportedMsg(`${desc.name}.${f.name}`, fd.what));
|
|
475
|
+
entry.fields.push({ name: f.name, desc: fd });
|
|
476
|
+
visitNeed(fd, needs, errors, mod);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function visitNeed(desc, needs, errors, mod) {
|
|
481
|
+
if (!desc || desc.k === "void") return;
|
|
482
|
+
switch (desc.k) {
|
|
483
|
+
case "struct":
|
|
484
|
+
visitStructNeed(desc, needs, errors, mod);
|
|
485
|
+
return;
|
|
486
|
+
case "slice":
|
|
487
|
+
case "array": {
|
|
488
|
+
const map = desc.k === "slice" ? needs.slices : needs.arrays;
|
|
489
|
+
if (map.has(desc.key)) return;
|
|
490
|
+
map.set(desc.key, desc);
|
|
491
|
+
visitNeed(desc.elem, needs, errors, mod);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
case "func":
|
|
495
|
+
if (needs.funcs.has(desc.key)) return;
|
|
496
|
+
needs.funcs.set(desc.key, desc);
|
|
497
|
+
visitSignatureNeeds(desc, needs, errors, mod);
|
|
498
|
+
return;
|
|
499
|
+
case "iface":
|
|
500
|
+
if (needs.ifaces.has(desc.key)) return;
|
|
501
|
+
needs.ifaces.set(desc.key, desc);
|
|
502
|
+
for (const m of desc.methods) visitSignatureNeeds(m, needs, errors, mod);
|
|
503
|
+
return;
|
|
504
|
+
case "shared":
|
|
505
|
+
needs.shared = true;
|
|
506
|
+
return;
|
|
507
|
+
default:
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Exported `*T` types (already on the boundary surface) whose JS class exposes
|
|
513
|
+
// every interface method: such values reach JS as the handle / live view
|
|
514
|
+
// itself, so type switches and identity still work. `__itag` matches by
|
|
515
|
+
// `ref.test`, so a struct is only chosen for values that really hold it.
|
|
516
|
+
function concreteCandidates(desc, needs, meta, mod) {
|
|
517
|
+
const out = [];
|
|
518
|
+
for (const [name, { info }] of needs.structs) {
|
|
519
|
+
const both = info.pkgTarget === "both";
|
|
520
|
+
const names = both
|
|
521
|
+
? [...(mod.checker?.types?.get(name)?.underlying?.methods?.keys() ?? [])]
|
|
522
|
+
: (meta.structByName.get(name)?.methods ?? []).map((m) => m.name);
|
|
523
|
+
const have = new Set(names);
|
|
524
|
+
if (!desc.methods.every((m) => have.has(m.name))) continue;
|
|
525
|
+
out.push({ name, typeIndex: info.typeIndex, both });
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function visitSignatureNeeds(sig, needs, errors, mod) {
|
|
531
|
+
for (const p of sig.params) visitNeed(p, needs, errors, mod);
|
|
532
|
+
for (const r of sig.returns) visitNeed(r, needs, errors, mod);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Collects every descriptor reachable from the exported surface so helpers are
|
|
536
|
+
// emitted once per struct / slice key / array key / closure key.
|
|
537
|
+
function collectNeeds(meta) {
|
|
538
|
+
const mod = meta._mod;
|
|
539
|
+
const needs = {
|
|
540
|
+
structs: new Map(), // name -> { info, fields: [{ name, desc }] }
|
|
541
|
+
slices: new Map(), // key -> desc
|
|
542
|
+
arrays: new Map(), // key -> desc
|
|
543
|
+
funcs: new Map(), // key -> desc
|
|
544
|
+
ifaces: new Map(), // interface name -> desc
|
|
545
|
+
shared: false, // any shared buffer on the surface
|
|
546
|
+
};
|
|
547
|
+
const errors = [];
|
|
548
|
+
for (const s of meta.structs) {
|
|
549
|
+
if (s.pkgTarget === "wasm")
|
|
550
|
+
visitNeed(structDesc(s.info, false), needs, errors, mod);
|
|
551
|
+
for (const m of s.methods) visitSignatureNeeds(m, needs, errors, mod);
|
|
552
|
+
}
|
|
553
|
+
for (const f of meta.funcs) visitSignatureNeeds(f, needs, errors, mod);
|
|
554
|
+
for (const v of meta.vars ?? []) visitNeed(v.desc, needs, errors, mod);
|
|
555
|
+
if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
|
|
556
|
+
for (const desc of needs.ifaces.values())
|
|
557
|
+
desc.concrete = concreteCandidates(desc, needs, meta, mod);
|
|
558
|
+
return needs;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── WASM-side helpers ────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
// Pre-lock: registers env imports for JS callbacks (`__invoke$<key>`).
|
|
564
|
+
export function scanBoundaryImports(mod, meta) {
|
|
565
|
+
const needs = collectNeeds(meta);
|
|
566
|
+
meta._needs = needs;
|
|
567
|
+
for (const [key, desc] of needs.funcs) {
|
|
568
|
+
mod.getOrAddFuncImport(
|
|
569
|
+
"env",
|
|
570
|
+
`__invoke$${key}`,
|
|
571
|
+
["externref", ...desc.closureInfo.paramWTypes.map(extType)],
|
|
572
|
+
desc.closureInfo.returnWTypes.map(extType),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Boundary representation of a wasm value type. i64 crosses as f64 so JS
|
|
578
|
+
// never sees (or allocates) a BigInt for Go `int`; every GC reference (struct,
|
|
579
|
+
// array, closure, anyref) crosses as externref. V8 only inlines the
|
|
580
|
+
// JS→wasm wrapper (passing f32/f64 unboxed) when the signature holds nothing
|
|
581
|
+
// but i32/i64/f32/f64/externref: a single anyref or `(ref $T)` in the
|
|
582
|
+
// signature costs a 16-byte HeapNumber per float argument and result.
|
|
583
|
+
function isGcRef(wType) {
|
|
584
|
+
return wType === "anyref" || (typeof wType === "object" && wType !== null);
|
|
585
|
+
}
|
|
586
|
+
function extType(wType) {
|
|
587
|
+
if (wType === "i64") return "f64";
|
|
588
|
+
if (isGcRef(wType)) return "externref";
|
|
589
|
+
return wType;
|
|
590
|
+
}
|
|
591
|
+
function needsConv(wTypes) {
|
|
592
|
+
return wTypes.some((w) => w === "i64" || isGcRef(w));
|
|
593
|
+
}
|
|
594
|
+
// Instructions converting the value on the stack from its boundary form.
|
|
595
|
+
function fromExt(wType) {
|
|
596
|
+
if (wType === "i64") return [{ op: "i64.trunc_sat_f64_s" }];
|
|
597
|
+
if (wType === "anyref") return [{ op: "any.convert_extern" }];
|
|
598
|
+
if (isGcRef(wType)) {
|
|
599
|
+
const out = [{ op: "any.convert_extern" }];
|
|
600
|
+
if (wType.typeIndex !== undefined)
|
|
601
|
+
out.push({ op: "ref.cast_null", typeIndex: wType.typeIndex });
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
// Instructions converting the value on the stack to its boundary form.
|
|
607
|
+
function toExt(wType) {
|
|
608
|
+
if (wType === "i64") return [{ op: "f64.convert_i64_s" }];
|
|
609
|
+
if (isGcRef(wType)) return [{ op: "extern.convert_any" }];
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
// Rewrites the values on top of the stack (types `wTypes`) with `conv`,
|
|
613
|
+
// spilling through locals starting at `localBase` when more than one.
|
|
614
|
+
function convertResults(wTypes, conv, localBase) {
|
|
615
|
+
if (wTypes.length === 0 || !needsConv(wTypes)) return [];
|
|
616
|
+
if (wTypes.length === 1) return conv(wTypes[0]);
|
|
617
|
+
const out = [];
|
|
618
|
+
for (let i = wTypes.length - 1; i >= 0; i--)
|
|
619
|
+
out.push({ op: "local.set", index: localBase + i });
|
|
620
|
+
for (let i = 0; i < wTypes.length; i++)
|
|
621
|
+
out.push({ op: "local.get", index: localBase + i }, ...conv(wTypes[i]));
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function zeroInstr(wType) {
|
|
626
|
+
if (wType === "i64") return { op: "i64.const", value: 0n };
|
|
627
|
+
if (wType === "f32" || wType === "f64")
|
|
628
|
+
return { op: `${wType}.const`, value: 0 };
|
|
629
|
+
if (typeof wType === "object" && wType !== null) {
|
|
630
|
+
return {
|
|
631
|
+
op: "ref.null",
|
|
632
|
+
heapType: wType.typeIndex ?? wType.heapType ?? "any",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (wType === "externref") return { op: "ref.null", heapType: "extern" };
|
|
636
|
+
if (wType === "anyref") return { op: "ref.null", heapType: "any" };
|
|
637
|
+
return { op: "i32.const", value: 0 };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Type entry at a flat type index (rec groups hold several entries each).
|
|
641
|
+
function typeAt(mod, index) {
|
|
642
|
+
if (index === undefined) return null;
|
|
643
|
+
let i = 0;
|
|
644
|
+
for (const entry of mod.types) {
|
|
645
|
+
const group = entry.form === "rec" ? entry.types : [entry];
|
|
646
|
+
if (index < i + group.length) return group[index - i];
|
|
647
|
+
i += group.length;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Post user functions: appends marshalling helper functions and exports them.
|
|
653
|
+
export function addBoundaryHelpers(mod, meta) {
|
|
654
|
+
const needs = meta._needs ?? collectNeeds(meta);
|
|
655
|
+
const importFuncCount = mod.imports.filter((i) => i.kind === "func").length;
|
|
656
|
+
|
|
657
|
+
const addFunc = (
|
|
658
|
+
name,
|
|
659
|
+
params,
|
|
660
|
+
results,
|
|
661
|
+
body,
|
|
662
|
+
exported = true,
|
|
663
|
+
locals = [],
|
|
664
|
+
) => {
|
|
665
|
+
const idx = importFuncCount + mod.funcs.length;
|
|
666
|
+
const typeIndex = mod.getTypeIndex(params, results);
|
|
667
|
+
mod.funcs.push({
|
|
668
|
+
typeIndex,
|
|
669
|
+
locals,
|
|
670
|
+
body: [...body, { op: "return" }],
|
|
671
|
+
});
|
|
672
|
+
mod.funcMap.set(name, idx);
|
|
673
|
+
if (exported) mod.exports.push({ name, kind: "func", index: idx });
|
|
674
|
+
return idx;
|
|
675
|
+
};
|
|
676
|
+
const get = (i) => ({ op: "local.get", index: i });
|
|
677
|
+
// Parameter `i` converted from its boundary form.
|
|
678
|
+
const getIn = (i, wType) => [get(i), ...fromExt(wType)];
|
|
679
|
+
const EXT = "externref";
|
|
680
|
+
|
|
681
|
+
for (const [name, { info }] of needs.structs) {
|
|
682
|
+
const ref = toRef(info);
|
|
683
|
+
const ti = info.typeIndex;
|
|
684
|
+
const fieldWTypes = info.fields.map((f) => f.wType);
|
|
685
|
+
addFunc(
|
|
686
|
+
`__new_${name}`,
|
|
687
|
+
fieldWTypes.map(extType),
|
|
688
|
+
[EXT],
|
|
689
|
+
[
|
|
690
|
+
...fieldWTypes.flatMap((w, i) => getIn(i, w)),
|
|
691
|
+
{ op: "struct.new", typeIndex: ti },
|
|
692
|
+
...toExt(ref),
|
|
693
|
+
],
|
|
694
|
+
);
|
|
695
|
+
addFunc(
|
|
696
|
+
`__zero_${name}`,
|
|
697
|
+
[],
|
|
698
|
+
[EXT],
|
|
699
|
+
[
|
|
700
|
+
...fieldWTypes.map((w) => zeroInstr(w)),
|
|
701
|
+
{ op: "struct.new", typeIndex: ti },
|
|
702
|
+
...toExt(ref),
|
|
703
|
+
],
|
|
704
|
+
);
|
|
705
|
+
addFunc(
|
|
706
|
+
`__clone_${name}`,
|
|
707
|
+
[EXT],
|
|
708
|
+
[EXT],
|
|
709
|
+
[
|
|
710
|
+
...fieldWTypes.flatMap((_, i) => [
|
|
711
|
+
...getIn(0, ref),
|
|
712
|
+
{ op: "struct.get", typeIndex: ti, fieldIndex: i },
|
|
713
|
+
]),
|
|
714
|
+
{ op: "struct.new", typeIndex: ti },
|
|
715
|
+
...toExt(ref),
|
|
716
|
+
],
|
|
717
|
+
);
|
|
718
|
+
for (let i = 0; i < info.fields.length; i++) {
|
|
719
|
+
const f = info.fields[i];
|
|
720
|
+
addFunc(
|
|
721
|
+
`__get_${name}_${f.name}`,
|
|
722
|
+
[EXT],
|
|
723
|
+
[extType(f.wType)],
|
|
724
|
+
[
|
|
725
|
+
...getIn(0, ref),
|
|
726
|
+
{ op: "struct.get", typeIndex: ti, fieldIndex: i },
|
|
727
|
+
...toExt(f.wType),
|
|
728
|
+
],
|
|
729
|
+
);
|
|
730
|
+
addFunc(
|
|
731
|
+
`__set_${name}_${f.name}`,
|
|
732
|
+
[EXT, extType(f.wType)],
|
|
733
|
+
[],
|
|
734
|
+
[
|
|
735
|
+
...getIn(0, ref),
|
|
736
|
+
...getIn(1, f.wType),
|
|
737
|
+
{ op: "struct.set", typeIndex: ti, fieldIndex: i },
|
|
738
|
+
],
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
for (const [key, desc] of needs.arrays) {
|
|
744
|
+
const { arrInfo } = desc;
|
|
745
|
+
const ref = desc.wType;
|
|
746
|
+
addFunc(
|
|
747
|
+
`__array_new_${key}`,
|
|
748
|
+
["i32"],
|
|
749
|
+
[EXT],
|
|
750
|
+
[
|
|
751
|
+
get(0),
|
|
752
|
+
{ op: "array.new_default", typeIndex: arrInfo.typeIndex },
|
|
753
|
+
...toExt(ref),
|
|
754
|
+
],
|
|
755
|
+
);
|
|
756
|
+
addFunc(
|
|
757
|
+
`__array_len_${key}`,
|
|
758
|
+
[EXT],
|
|
759
|
+
["i32"],
|
|
760
|
+
[...getIn(0, ref), { op: "array.len" }],
|
|
761
|
+
);
|
|
762
|
+
addFunc(
|
|
763
|
+
`__array_get_${key}`,
|
|
764
|
+
[EXT, "i32"],
|
|
765
|
+
[extType(arrInfo.elemWType)],
|
|
766
|
+
[
|
|
767
|
+
...getIn(0, ref),
|
|
768
|
+
get(1),
|
|
769
|
+
{ op: "array.get", typeIndex: arrInfo.typeIndex },
|
|
770
|
+
...toExt(arrInfo.elemWType),
|
|
771
|
+
],
|
|
772
|
+
);
|
|
773
|
+
addFunc(
|
|
774
|
+
`__array_set_${key}`,
|
|
775
|
+
[EXT, "i32", extType(arrInfo.elemWType)],
|
|
776
|
+
[],
|
|
777
|
+
[
|
|
778
|
+
...getIn(0, ref),
|
|
779
|
+
get(1),
|
|
780
|
+
...getIn(2, arrInfo.elemWType),
|
|
781
|
+
{ op: "array.set", typeIndex: arrInfo.typeIndex },
|
|
782
|
+
],
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
for (const [key, desc] of needs.slices) {
|
|
787
|
+
const { sliceInfo } = desc;
|
|
788
|
+
const ref = desc.wType;
|
|
789
|
+
const arrTI = sliceInfo.arrInfo.typeIndex;
|
|
790
|
+
const sTI = sliceInfo.typeIndex;
|
|
791
|
+
const elemAddr = [
|
|
792
|
+
...getIn(0, ref),
|
|
793
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 0 },
|
|
794
|
+
...getIn(0, ref),
|
|
795
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 1 },
|
|
796
|
+
get(1),
|
|
797
|
+
{ op: "i32.add" },
|
|
798
|
+
];
|
|
799
|
+
addFunc(
|
|
800
|
+
`__slice_new_${key}`,
|
|
801
|
+
["i32"],
|
|
802
|
+
[EXT],
|
|
803
|
+
[
|
|
804
|
+
get(0),
|
|
805
|
+
{ op: "array.new_default", typeIndex: arrTI },
|
|
806
|
+
{ op: "i32.const", value: 0 },
|
|
807
|
+
get(0),
|
|
808
|
+
get(0),
|
|
809
|
+
{ op: "struct.new", typeIndex: sTI },
|
|
810
|
+
...toExt(ref),
|
|
811
|
+
],
|
|
812
|
+
);
|
|
813
|
+
addFunc(
|
|
814
|
+
`__slice_len_${key}`,
|
|
815
|
+
[EXT],
|
|
816
|
+
["i32"],
|
|
817
|
+
[...getIn(0, ref), { op: "struct.get", typeIndex: sTI, fieldIndex: 2 }],
|
|
818
|
+
);
|
|
819
|
+
addFunc(
|
|
820
|
+
`__slice_get_${key}`,
|
|
821
|
+
[EXT, "i32"],
|
|
822
|
+
[extType(sliceInfo.elemWType)],
|
|
823
|
+
[
|
|
824
|
+
...elemAddr,
|
|
825
|
+
{ op: "array.get", typeIndex: arrTI },
|
|
826
|
+
...toExt(sliceInfo.elemWType),
|
|
827
|
+
],
|
|
828
|
+
);
|
|
829
|
+
addFunc(
|
|
830
|
+
`__slice_nil_${key}`,
|
|
831
|
+
[],
|
|
832
|
+
[EXT],
|
|
833
|
+
[{ op: "global.get", index: sliceInfo.emptyGlobalIndex }, ...toExt(ref)],
|
|
834
|
+
);
|
|
835
|
+
addFunc(
|
|
836
|
+
`__slice_is_nil_${key}`,
|
|
837
|
+
[EXT],
|
|
838
|
+
["i32"],
|
|
839
|
+
[
|
|
840
|
+
...getIn(0, ref),
|
|
841
|
+
{ op: "struct.get", typeIndex: sTI, fieldIndex: 0 },
|
|
842
|
+
{ op: "ref.is_null" },
|
|
843
|
+
],
|
|
844
|
+
);
|
|
845
|
+
addFunc(
|
|
846
|
+
`__slice_set_${key}`,
|
|
847
|
+
[EXT, "i32", extType(sliceInfo.elemWType)],
|
|
848
|
+
[],
|
|
849
|
+
[
|
|
850
|
+
...elemAddr,
|
|
851
|
+
...getIn(2, sliceInfo.elemWType),
|
|
852
|
+
{ op: "array.set", typeIndex: arrTI },
|
|
853
|
+
],
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const [key, desc] of needs.funcs) {
|
|
858
|
+
const ci = desc.closureInfo;
|
|
859
|
+
const invokeIdx = mod.importCache.get(`env.__invoke$${key}`);
|
|
860
|
+
const nParams = ci.paramWTypes.length;
|
|
861
|
+
// Thunk: wasm closure body calling the JS callback (boundary forms out, back in).
|
|
862
|
+
const thunkIdx = addFunc(
|
|
863
|
+
`__jsthunk$${key}`,
|
|
864
|
+
["anyref", ...ci.paramWTypes],
|
|
865
|
+
ci.returnWTypes,
|
|
866
|
+
[
|
|
867
|
+
get(0),
|
|
868
|
+
{ op: "extern.convert_any" },
|
|
869
|
+
...ci.paramWTypes.flatMap((w, i) => [get(i + 1), ...toExt(w)]),
|
|
870
|
+
{ op: "call", funcIndex: invokeIdx },
|
|
871
|
+
...convertResults(ci.returnWTypes, fromExt, nParams + 1),
|
|
872
|
+
],
|
|
873
|
+
false,
|
|
874
|
+
ci.returnWTypes.length > 1 ? ci.returnWTypes.map(extType) : [],
|
|
875
|
+
);
|
|
876
|
+
mod.addRefFuncElement(thunkIdx);
|
|
877
|
+
addFunc(
|
|
878
|
+
`__wrap_fn$${key}`,
|
|
879
|
+
[EXT],
|
|
880
|
+
[EXT],
|
|
881
|
+
[
|
|
882
|
+
{ op: "ref.func", funcIndex: thunkIdx },
|
|
883
|
+
get(0),
|
|
884
|
+
{ op: "any.convert_extern" },
|
|
885
|
+
{ op: "struct.new", typeIndex: ci.typeIndex },
|
|
886
|
+
...toExt(desc.wType),
|
|
887
|
+
],
|
|
888
|
+
);
|
|
889
|
+
// JS calling a wasm closure (boundary forms in, results out).
|
|
890
|
+
addFunc(
|
|
891
|
+
`__call_fn$${key}`,
|
|
892
|
+
[EXT, ...ci.paramWTypes.map(extType)],
|
|
893
|
+
ci.returnWTypes.map(extType),
|
|
894
|
+
[
|
|
895
|
+
...getIn(0, desc.wType),
|
|
896
|
+
{ op: "struct.get", typeIndex: ci.typeIndex, fieldIndex: 1 },
|
|
897
|
+
...ci.paramWTypes.flatMap((w, i) => getIn(i + 1, w)),
|
|
898
|
+
...getIn(0, desc.wType),
|
|
899
|
+
{ op: "struct.get", typeIndex: ci.typeIndex, fieldIndex: 0 },
|
|
900
|
+
{ op: "call_ref", typeIndex: ci.funcTypeIndex },
|
|
901
|
+
...convertResults(ci.returnWTypes, toExt, nParams + 1),
|
|
902
|
+
],
|
|
903
|
+
true,
|
|
904
|
+
ci.returnWTypes.length > 1 ? ci.returnWTypes : [],
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Exported user functions / methods whose signature carries i64 or GC refs
|
|
909
|
+
// get a trampoline (`__x_<export>`) with the boundary signature; the facade
|
|
910
|
+
// calls that instead of the raw export.
|
|
911
|
+
const addTrampoline = (entry) => {
|
|
912
|
+
// Methods are keyed `T.M` in funcMap but exported as `T_M`.
|
|
913
|
+
const fnIdx = mod.funcMap.get(entry.fnName ?? entry.exportName);
|
|
914
|
+
if (fnIdx === undefined) return;
|
|
915
|
+
const sig = typeAt(mod, mod.funcs[fnIdx - importFuncCount]?.typeIndex);
|
|
916
|
+
if (sig?.form !== "func") return;
|
|
917
|
+
const paramW = sig.params;
|
|
918
|
+
const resultW = sig.results;
|
|
919
|
+
if (!needsConv(paramW) && !needsConv(resultW)) return;
|
|
920
|
+
entry.callName = `__x_${entry.exportName}`;
|
|
921
|
+
addFunc(
|
|
922
|
+
entry.callName,
|
|
923
|
+
paramW.map(extType),
|
|
924
|
+
resultW.map(extType),
|
|
925
|
+
[
|
|
926
|
+
...paramW.flatMap((w, i) => getIn(i, w)),
|
|
927
|
+
{ op: "call", funcIndex: fnIdx },
|
|
928
|
+
...convertResults(resultW, toExt, paramW.length),
|
|
929
|
+
],
|
|
930
|
+
true,
|
|
931
|
+
resultW.length > 1 ? resultW : [],
|
|
932
|
+
);
|
|
933
|
+
};
|
|
934
|
+
for (const s of meta.structs) for (const m of s.methods) addTrampoline(m);
|
|
935
|
+
for (const f of meta.funcs) addTrampoline(f);
|
|
936
|
+
for (const desc of needs.ifaces.values())
|
|
937
|
+
addIfaceHelpers(desc, mod, addFunc, addTrampoline);
|
|
938
|
+
|
|
939
|
+
if (needs.shared) {
|
|
940
|
+
const { wType, typeIndex } = mod.getSharedType();
|
|
941
|
+
addFunc(
|
|
942
|
+
"__shared_base",
|
|
943
|
+
[EXT],
|
|
944
|
+
["i32"],
|
|
945
|
+
[...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 0 }],
|
|
946
|
+
);
|
|
947
|
+
addFunc(
|
|
948
|
+
"__shared_len",
|
|
949
|
+
[EXT],
|
|
950
|
+
["i32"],
|
|
951
|
+
[...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 1 }],
|
|
952
|
+
);
|
|
953
|
+
// JS-side views (e.g. a `subarray`) go back in as a fresh (base, len).
|
|
954
|
+
addFunc(
|
|
955
|
+
"__shared_make",
|
|
956
|
+
["i32", "i32"],
|
|
957
|
+
[EXT],
|
|
958
|
+
[get(0), get(1), { op: "struct.new", typeIndex }, ...toExt(wType)],
|
|
959
|
+
);
|
|
960
|
+
for (const v of meta.vars ?? []) {
|
|
961
|
+
const g = mod.resolveGlobal(v.name);
|
|
962
|
+
if (!g) continue;
|
|
963
|
+
addFunc(
|
|
964
|
+
`__var_${v.name}`,
|
|
965
|
+
[],
|
|
966
|
+
[EXT],
|
|
967
|
+
[{ op: "global.get", index: g.index }, ...toExt(wType)],
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// `(externref) -> i32`: `ref.test`s param 0 against each type index and
|
|
974
|
+
// returns `resultFor(i)` for the first match, 0 otherwise.
|
|
975
|
+
function refTestChain(typeIndices, resultFor) {
|
|
976
|
+
const body = typeIndices.flatMap((ti, i) => [
|
|
977
|
+
{ op: "local.get", index: 0 },
|
|
978
|
+
{ op: "any.convert_extern" },
|
|
979
|
+
{ op: "ref.test", typeIndex: ti },
|
|
980
|
+
{ op: "if", blockType: "void" },
|
|
981
|
+
{ op: "i32.const", value: resultFor(i) },
|
|
982
|
+
{ op: "return" },
|
|
983
|
+
{ op: "end" },
|
|
984
|
+
]);
|
|
985
|
+
body.push({ op: "i32.const", value: 0 });
|
|
986
|
+
return body;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Type indices (incl. unexported and boxed-value types) whose method set
|
|
990
|
+
// covers the interface, from the same candidate search the dispatchers use.
|
|
991
|
+
function implementingTypeIndices(desc, mod) {
|
|
992
|
+
let acc = null;
|
|
993
|
+
for (const m of desc.methods) {
|
|
994
|
+
const have = new Set(
|
|
995
|
+
mod.findInterfaceCandidates(null, m.name).map((c) => c.testTypeIndex),
|
|
996
|
+
);
|
|
997
|
+
acc = acc === null ? have : new Set([...acc].filter((t) => have.has(t)));
|
|
998
|
+
}
|
|
999
|
+
return [...(acc ?? [])];
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Interfaces: method calls go through the existing `ref.test` dispatchers;
|
|
1003
|
+
// `__impl$<I>` tells whether a wasm value implements I (guards JS inputs);
|
|
1004
|
+
// `__itag$<I>` reports which exported `*T` (1-based) a value holds, 0 if none.
|
|
1005
|
+
function addIfaceHelpers(desc, mod, addFunc, addTrampoline) {
|
|
1006
|
+
for (const m of desc.methods) {
|
|
1007
|
+
const entry = { exportName: m.dispatch };
|
|
1008
|
+
addTrampoline(entry);
|
|
1009
|
+
if (!entry.callName)
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`internal: no wasm dispatcher for interface method ${desc.name}.${m.name}`,
|
|
1012
|
+
);
|
|
1013
|
+
m.callName = entry.callName;
|
|
1014
|
+
}
|
|
1015
|
+
addFunc(
|
|
1016
|
+
`__impl$${desc.key}`,
|
|
1017
|
+
["externref"],
|
|
1018
|
+
["i32"],
|
|
1019
|
+
refTestChain(implementingTypeIndices(desc, mod), () => 1),
|
|
1020
|
+
);
|
|
1021
|
+
if (desc.concrete.length === 0) return;
|
|
1022
|
+
addFunc(
|
|
1023
|
+
`__itag$${desc.key}`,
|
|
1024
|
+
["externref"],
|
|
1025
|
+
["i32"],
|
|
1026
|
+
refTestChain(
|
|
1027
|
+
desc.concrete.map((c) => c.typeIndex),
|
|
1028
|
+
(i) => i + 1,
|
|
1029
|
+
),
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ── JS facade generation ─────────────────────────────────────
|
|
1034
|
+
|
|
1035
|
+
const P = "__gfw_"; // facade helper prefix
|
|
1036
|
+
|
|
1037
|
+
// JS expression converting JS value `v` into the wasm representation.
|
|
1038
|
+
function inExpr(desc, v) {
|
|
1039
|
+
switch (desc.k) {
|
|
1040
|
+
case "bool":
|
|
1041
|
+
return `(${v} ? 1 : 0)`;
|
|
1042
|
+
case "i32":
|
|
1043
|
+
return `(${v} | 0)`;
|
|
1044
|
+
case "i64":
|
|
1045
|
+
return `${P}i64in(${v})`;
|
|
1046
|
+
case "f32":
|
|
1047
|
+
case "f64":
|
|
1048
|
+
return `(+${v})`;
|
|
1049
|
+
case "string":
|
|
1050
|
+
return `${P}strin(${v})`;
|
|
1051
|
+
case "any":
|
|
1052
|
+
return `(${v} === undefined ? null : ${v})`;
|
|
1053
|
+
case "struct":
|
|
1054
|
+
if (desc.info.pkgTarget === "both")
|
|
1055
|
+
return `${P}to_${desc.name}(${v}, ${desc.ptr ? "false" : "true"})`;
|
|
1056
|
+
return desc.ptr
|
|
1057
|
+
? `${P}href_${desc.name}(${v})`
|
|
1058
|
+
: `${P}hval_${desc.name}(${v})`;
|
|
1059
|
+
case "slice":
|
|
1060
|
+
return `${P}slin_${desc.key}(${v})`;
|
|
1061
|
+
case "array":
|
|
1062
|
+
return `${P}arrin_${desc.key}(${v})`;
|
|
1063
|
+
case "func":
|
|
1064
|
+
return `${P}fnin_${desc.key}(${v})`;
|
|
1065
|
+
case "iface":
|
|
1066
|
+
return `${P}ifin_${desc.key}(${v})`;
|
|
1067
|
+
case "shared":
|
|
1068
|
+
return `${P}shin(${v}, ${desc.ctor})`;
|
|
1069
|
+
default:
|
|
1070
|
+
return v;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// JS expression converting wasm value `v` into the JS representation.
|
|
1075
|
+
function outExpr(desc, v) {
|
|
1076
|
+
switch (desc.k) {
|
|
1077
|
+
case "bool":
|
|
1078
|
+
return `(${v} !== 0)`;
|
|
1079
|
+
case "i32":
|
|
1080
|
+
return desc.unsigned ? `(${v} >>> 0)` : v;
|
|
1081
|
+
case "i64":
|
|
1082
|
+
return desc.unsigned ? `${P}u64out(${v})` : `${P}i64out(${v})`;
|
|
1083
|
+
case "string":
|
|
1084
|
+
return `(${v} ?? "")`;
|
|
1085
|
+
case "struct":
|
|
1086
|
+
return desc.info.pkgTarget === "both"
|
|
1087
|
+
? `${P}from_${desc.name}(${v})`
|
|
1088
|
+
: `${desc.name}.__wrap(${v})`;
|
|
1089
|
+
case "slice":
|
|
1090
|
+
return `${P}slout_${desc.key}(${v})`;
|
|
1091
|
+
case "array":
|
|
1092
|
+
return `${P}arrout_${desc.key}(${v})`;
|
|
1093
|
+
case "func":
|
|
1094
|
+
return `${P}fnout_${desc.key}(${v})`;
|
|
1095
|
+
case "iface":
|
|
1096
|
+
return `${P}ifout_${desc.key}(${v})`;
|
|
1097
|
+
case "shared":
|
|
1098
|
+
return `${P}shout(${v}, ${desc.ctor})`;
|
|
1099
|
+
default:
|
|
1100
|
+
return v;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function zeroExpr(desc) {
|
|
1105
|
+
switch (desc.k) {
|
|
1106
|
+
case "bool":
|
|
1107
|
+
return "false";
|
|
1108
|
+
case "i32":
|
|
1109
|
+
case "i64":
|
|
1110
|
+
case "f32":
|
|
1111
|
+
case "f64":
|
|
1112
|
+
return "0";
|
|
1113
|
+
case "string":
|
|
1114
|
+
return '""';
|
|
1115
|
+
case "struct":
|
|
1116
|
+
return desc.ptr ? "null" : `new ${desc.name}()`;
|
|
1117
|
+
case "array":
|
|
1118
|
+
return `${P}arrzero_${desc.key}(${desc.size})`;
|
|
1119
|
+
default:
|
|
1120
|
+
return "null";
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// JS expression exposing wasm storage `v` reached through a handle field as a
|
|
1125
|
+
// *live* JS value: aggregate fields alias the wasm object instead of copying
|
|
1126
|
+
// it, so `h.Pos.X = 1` and `h.Items[0] = 2` reach wasm like they do in Go.
|
|
1127
|
+
function viewExpr(desc, v) {
|
|
1128
|
+
switch (desc.k) {
|
|
1129
|
+
case "struct":
|
|
1130
|
+
return desc.info.pkgTarget === "both"
|
|
1131
|
+
? `${P}view_${desc.name}(${v})`
|
|
1132
|
+
: `${desc.name}.__wrap(${v})`;
|
|
1133
|
+
case "slice":
|
|
1134
|
+
return `${P}slview_${desc.key}(${v})`;
|
|
1135
|
+
case "array":
|
|
1136
|
+
return `${P}arrview_${desc.key}(${v})`;
|
|
1137
|
+
default:
|
|
1138
|
+
return outExpr(desc, v);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function isPtrBoth(d) {
|
|
1143
|
+
return d.k === "struct" && d.ptr && d.info.pkgTarget === "both";
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function isBothStruct(d) {
|
|
1147
|
+
return d.k === "struct" && d.info.pkgTarget === "both";
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function returnLines(returns, resultExpr) {
|
|
1151
|
+
if (returns.length === 0) return [];
|
|
1152
|
+
if (returns.length === 1)
|
|
1153
|
+
return [`\treturn ${outExpr(returns[0], resultExpr)};`];
|
|
1154
|
+
return [
|
|
1155
|
+
`\treturn [${returns.map((d, i) => outExpr(d, `${resultExpr}[${i}]`)).join(", ")}];`,
|
|
1156
|
+
];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Generates the body of a JS wrapper calling `__w.<exportName>(leadArgs..., params...)`.
|
|
1160
|
+
function genFuncBody(exportName, params, returns, leadArgs = []) {
|
|
1161
|
+
const names = params.map((_, i) => `a${i}`);
|
|
1162
|
+
const lines = [];
|
|
1163
|
+
const argExprs = [...leadArgs];
|
|
1164
|
+
const writeBack = params.some(isPtrBoth);
|
|
1165
|
+
if (writeBack) lines.push("\tconst __m = new Map();");
|
|
1166
|
+
for (let i = 0; i < params.length; i++) {
|
|
1167
|
+
const d = params[i];
|
|
1168
|
+
if (isPtrBoth(d)) {
|
|
1169
|
+
lines.push(`\tconst __p${i} = ${P}to_${d.name}(a${i}, false, __m);`);
|
|
1170
|
+
argExprs.push(`__p${i}`);
|
|
1171
|
+
} else {
|
|
1172
|
+
argExprs.push(inExpr(d, names[i]));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const call = `__w.${exportName}(${argExprs.join(", ")})`;
|
|
1176
|
+
lines.push("\ttry {");
|
|
1177
|
+
if (!writeBack) {
|
|
1178
|
+
if (returns.length === 0) lines.push(`\t\t${call};`);
|
|
1179
|
+
else if (returns.length === 1)
|
|
1180
|
+
lines.push(...returnLines(returns, call).map((l) => `\t${l}`));
|
|
1181
|
+
else {
|
|
1182
|
+
lines.push(`\t\tconst __r = ${call};`);
|
|
1183
|
+
lines.push(...returnLines(returns, "__r").map((l) => `\t${l}`));
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
lines.push(`\t\tconst __r = ${call};`);
|
|
1187
|
+
for (let i = 0; i < params.length; i++) {
|
|
1188
|
+
if (isPtrBoth(params[i]))
|
|
1189
|
+
lines.push(`\t\t${P}back_${params[i].name}(__p${i}, a${i});`);
|
|
1190
|
+
}
|
|
1191
|
+
lines.push(...returnLines(returns, "__r").map((l) => `\t${l}`));
|
|
1192
|
+
}
|
|
1193
|
+
lines.push(`\t} catch (e) { throw ${P}mapTrap(e); }`);
|
|
1194
|
+
return { names, body: lines.join("\n") };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function indent(text, prefix) {
|
|
1198
|
+
return text.replace(/^/gm, prefix);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Runtime import table shared by the facade and glue.js (single source so
|
|
1202
|
+
// the tests exercise the same env the shipped bundle uses). Written to
|
|
1203
|
+
// survive the built-in mangler: no shorthand destructuring, import keys
|
|
1204
|
+
// quoted, helpers prefixed with `__`.
|
|
1205
|
+
export const WASM_IMPORTS_JS = `const ${P}MAX = 9007199254740991n;
|
|
1206
|
+
function ${P}imports(stringTable, extraEnv, tag, write) {
|
|
1207
|
+
let lineBuf = "";
|
|
1208
|
+
let targs = [];
|
|
1209
|
+
const late = { exports: null };
|
|
1210
|
+
const flush = () => { write(lineBuf); lineBuf = ""; };
|
|
1211
|
+
const print = (v) => {
|
|
1212
|
+
const s = String(v);
|
|
1213
|
+
if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
|
|
1214
|
+
lineBuf += " ";
|
|
1215
|
+
}
|
|
1216
|
+
lineBuf += s;
|
|
1217
|
+
};
|
|
1218
|
+
const println = (v) => {
|
|
1219
|
+
const s = String(v);
|
|
1220
|
+
if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
|
|
1221
|
+
lineBuf += " ";
|
|
1222
|
+
}
|
|
1223
|
+
lineBuf += s;
|
|
1224
|
+
flush();
|
|
1225
|
+
};
|
|
1226
|
+
const targ = (v) => { targs.push(v); };
|
|
1227
|
+
const env = {
|
|
1228
|
+
"panicTag": tag,
|
|
1229
|
+
"panic": (msg) => {
|
|
1230
|
+
let str;
|
|
1231
|
+
try {
|
|
1232
|
+
str = String(msg);
|
|
1233
|
+
} catch {
|
|
1234
|
+
str = "[panic object]";
|
|
1235
|
+
}
|
|
1236
|
+
throw new Error(str);
|
|
1237
|
+
},
|
|
1238
|
+
"str": (i) => stringTable[i] ?? "",
|
|
1239
|
+
"str_len": (s) => (s ? s.length : 0),
|
|
1240
|
+
"str_concat": (a, b) => (a ?? "") + (b ?? ""),
|
|
1241
|
+
"str_eq": (a, b) => (a === b ? 1 : 0),
|
|
1242
|
+
"str_ne": (a, b) => (a !== b ? 1 : 0),
|
|
1243
|
+
"str_lt": (a, b) => ((a ?? "") < (b ?? "") ? 1 : 0),
|
|
1244
|
+
"str_le": (a, b) => ((a ?? "") <= (b ?? "") ? 1 : 0),
|
|
1245
|
+
"str_gt": (a, b) => ((a ?? "") > (b ?? "") ? 1 : 0),
|
|
1246
|
+
"str_ge": (a, b) => ((a ?? "") >= (b ?? "") ? 1 : 0),
|
|
1247
|
+
"str_get": (s, i) => (s ? s.charCodeAt(i) : 0),
|
|
1248
|
+
"str_slice": (s, a, b) => (s ? s.slice(a, b) : ""),
|
|
1249
|
+
"str_from_code_point": (c) => String.fromCodePoint(c),
|
|
1250
|
+
"str_code_point_at": (s, i) => (s ? s.codePointAt(i) : 0),
|
|
1251
|
+
"str_hash": (s) => {
|
|
1252
|
+
if (!s) return 0;
|
|
1253
|
+
let h = 0;
|
|
1254
|
+
for (let i = 0; i < s.length; i++) {
|
|
1255
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
1256
|
+
}
|
|
1257
|
+
return h;
|
|
1258
|
+
},
|
|
1259
|
+
"str_to_upper": (s) => (s ? s.toUpperCase() : ""),
|
|
1260
|
+
"str_to_lower": (s) => (s ? s.toLowerCase() : ""),
|
|
1261
|
+
"str_trim_space": (s) => (s ? s.trim() : ""),
|
|
1262
|
+
"str_contains": (s, sub) => (s && sub !== undefined ? (s.includes(sub) ? 1 : 0) : 0),
|
|
1263
|
+
"str_has_prefix": (s, pre) => (s && pre !== undefined ? (s.startsWith(pre) ? 1 : 0) : 0),
|
|
1264
|
+
"str_has_suffix": (s, suf) => (s && suf !== undefined ? (s.endsWith(suf) ? 1 : 0) : 0),
|
|
1265
|
+
"str_index": (s, sub) => (s ? s.indexOf(sub) : -1),
|
|
1266
|
+
"str_last_index": (s, sub) => (s ? s.lastIndexOf(sub) : -1),
|
|
1267
|
+
"str_repeat": (s, n) => (s && n > 0 ? s.repeat(Number(n)) : ""),
|
|
1268
|
+
"str_replace_all": (s, o, n) => (s ? s.replaceAll(o, n) : ""),
|
|
1269
|
+
"str_equal_fold": (a, b) => ((a ?? "").toLowerCase() === (b ?? "").toLowerCase() ? 1 : 0),
|
|
1270
|
+
"str_count": (s, sep) => {
|
|
1271
|
+
if (!s) return sep === "" ? 1 : 0;
|
|
1272
|
+
if (sep === "") return s.length + 1;
|
|
1273
|
+
return s.split(sep).length - 1;
|
|
1274
|
+
},
|
|
1275
|
+
"str_from_i64": (n) => String(n),
|
|
1276
|
+
"str_from_i32": (n) => String(n),
|
|
1277
|
+
"str_from_f64": (f) => String(f),
|
|
1278
|
+
"is_string": (v) => (typeof v === "string" ? 1 : 0),
|
|
1279
|
+
"print_i32": print, "print_i64": print, "print_f32": print, "print_f64": print,
|
|
1280
|
+
"print_str": print, "print_any": print,
|
|
1281
|
+
"print_bool": (b) => print(b !== 0 ? "true" : "false"),
|
|
1282
|
+
"println_i32": println, "println_i64": println, "println_f32": println, "println_f64": println,
|
|
1283
|
+
"println_str": println, "println_any": println,
|
|
1284
|
+
"println_bool": (b) => println(b !== 0 ? "true" : "false"),
|
|
1285
|
+
"println_empty": flush,
|
|
1286
|
+
"testing_arg_i32": targ, "testing_arg_f32": targ, "testing_arg_f64": targ,
|
|
1287
|
+
"testing_arg_str": targ, "testing_arg_any": targ,
|
|
1288
|
+
"testing_arg_i64": (v) => targ(v > ${P}MAX || v < -${P}MAX ? v : Number(v)),
|
|
1289
|
+
"testing_arg_bool": (v) => targ(v !== 0),
|
|
1290
|
+
"testing_call": (t, i) => { const a = targs; targs = []; t[stringTable[i]](...a); },
|
|
1291
|
+
"testing_name": (t) => t.Name(),
|
|
1292
|
+
"testing_flag": (t, i) => (t[stringTable[i]]() ? 1 : 0),
|
|
1293
|
+
};
|
|
1294
|
+
// Stdlib imports (strings/strconv/utf8/fmt/t.Run) are a separate block so
|
|
1295
|
+
// modules that never call them do not ship it (see WASM_STDLIB_JS).
|
|
1296
|
+
if (typeof ${P}stdlib === "function") {
|
|
1297
|
+
Object.assign(env, ${P}stdlib(stringTable, {
|
|
1298
|
+
"take": () => { const a = targs; targs = []; return a; },
|
|
1299
|
+
"printf": (s) => {
|
|
1300
|
+
lineBuf += s;
|
|
1301
|
+
const nl = lineBuf.lastIndexOf("\\n");
|
|
1302
|
+
if (nl >= 0) { write(lineBuf.slice(0, nl)); lineBuf = lineBuf.slice(nl + 1); }
|
|
1303
|
+
},
|
|
1304
|
+
"late": late,
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
Object.assign(env, extraEnv);
|
|
1308
|
+
const m = {};
|
|
1309
|
+
for (const k of ["sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "exp", "log", "log2", "log10", "cbrt", "hypot"]) m[k] = Math[k];
|
|
1310
|
+
m.mod = (x, y) => x % y;
|
|
1311
|
+
// Go rounds half away from zero; Math.round rounds half toward +Infinity.
|
|
1312
|
+
m.round = (x) => Math.sign(x) * Math.round(Math.abs(x));
|
|
1313
|
+
// Exports are needed by imports that call back into the module (t.Run).
|
|
1314
|
+
return { "env": env, "Math": m, "__bind": (ex) => { late.exports = ex; } };
|
|
1315
|
+
}`;
|
|
1316
|
+
|
|
1317
|
+
// JS side of the STDLIB_IMPORTS table (index.js); emitted only when the
|
|
1318
|
+
// module imports one of the packages that use it.
|
|
1319
|
+
export const WASM_STDLIB_JS = `${HELPER_SPRINTF}
|
|
1320
|
+
${HELPER_STRCONV}
|
|
1321
|
+
function ${P}stdlib(stringTable, io) {
|
|
1322
|
+
let parts = [];
|
|
1323
|
+
let strconvErr = null;
|
|
1324
|
+
let decodeSize = 0;
|
|
1325
|
+
return {
|
|
1326
|
+
"testing_run": (t, name, fn) => (t.Run(name, (sub) => { io.late.exports.__testing_run_cb(fn, sub); }) ? 1 : 0),
|
|
1327
|
+
"fmt_sprintf": (f) => __sprintf(f ?? "", ...io.take()),
|
|
1328
|
+
"fmt_printf": (f) => { io.printf(__sprintf(f ?? "", ...io.take())); },
|
|
1329
|
+
"str_trim": (s, c) => ${P}trim(s ?? "", c ?? "", true, true),
|
|
1330
|
+
"str_trim_left": (s, c) => ${P}trim(s ?? "", c ?? "", true, false),
|
|
1331
|
+
"str_trim_right": (s, c) => ${P}trim(s ?? "", c ?? "", false, true),
|
|
1332
|
+
"str_trim_prefix": (s, p) => (s && p && s.startsWith(p) ? s.slice(p.length) : (s ?? "")),
|
|
1333
|
+
"str_trim_suffix": (s, p) => (s && p && s.endsWith(p) ? s.slice(0, s.length - p.length) : (s ?? "")),
|
|
1334
|
+
"str_replace": (s, o, n, cnt) => {
|
|
1335
|
+
s = s ?? ""; o = o ?? ""; n = n ?? "";
|
|
1336
|
+
if (cnt < 0) return s.replaceAll(o, n);
|
|
1337
|
+
let out = "", from = 0;
|
|
1338
|
+
for (let k = 0; k < cnt; k++) {
|
|
1339
|
+
const at = s.indexOf(o, from);
|
|
1340
|
+
if (at < 0) break;
|
|
1341
|
+
out += s.slice(from, at) + n;
|
|
1342
|
+
from = at + o.length;
|
|
1343
|
+
if (o === "") { if (from >= s.length) break; out += s[from]; from++; }
|
|
1344
|
+
}
|
|
1345
|
+
return out + s.slice(from);
|
|
1346
|
+
},
|
|
1347
|
+
"str_contains_rune": (s, r) => ((s ?? "").includes(String.fromCodePoint(r)) ? 1 : 0),
|
|
1348
|
+
"str_contains_any": (s, cs) => { for (const c of (cs ?? "")) if ((s ?? "").includes(c)) return 1; return 0; },
|
|
1349
|
+
"str_index_byte": (s, b) => (s ?? "").indexOf(String.fromCharCode(b)),
|
|
1350
|
+
"str_index_rune": (s, r) => (s ?? "").indexOf(String.fromCodePoint(r)),
|
|
1351
|
+
"str_index_any": (s, cs) => { s = s ?? ""; let best = -1; for (const c of (cs ?? "")) { const i = s.indexOf(c); if (i >= 0 && (best < 0 || i < best)) best = i; } return best; },
|
|
1352
|
+
"str_last_index_byte": (s, b) => (s ?? "").lastIndexOf(String.fromCharCode(b)),
|
|
1353
|
+
"str_title": (s) => (s ?? "").replace(/(^|[^\\p{L}\\p{N}_'])(\\p{Ll})/gu, (m, a, b) => a + b.toUpperCase()),
|
|
1354
|
+
"str_split": (s, sep) => { parts = (s ?? "").split(sep ?? ""); return parts.length; },
|
|
1355
|
+
"str_fields": (s) => { parts = (s ?? "").split(/\\s+/).filter((p) => p !== ""); return parts.length; },
|
|
1356
|
+
"str_part": (i) => parts[i] ?? "",
|
|
1357
|
+
"strconv_atoi": (s) => { const r = __strconv_atoi(s ?? ""); strconvErr = r[1]; return r[0]; },
|
|
1358
|
+
"strconv_parse_int": (s, base, bits) => { const r = __strconv_parse_int(s ?? "", base, bits); strconvErr = r[1]; return r[0]; },
|
|
1359
|
+
"strconv_parse_float": (s) => { const r = __strconv_parse_float(s ?? ""); strconvErr = r[1]; return r[0]; },
|
|
1360
|
+
"strconv_parse_bool": (s) => { const r = __strconv_parse_bool(s ?? ""); strconvErr = r[1]; return r[0] ? 1 : 0; },
|
|
1361
|
+
"strconv_err": () => strconvErr,
|
|
1362
|
+
"strconv_format_float": (f, fmtc, prec, _bits) => __strconv_format_float(f, fmtc, prec),
|
|
1363
|
+
"strconv_format_int": (n, base) => n.toString(base),
|
|
1364
|
+
"strconv_quote": (s) => JSON.stringify(s ?? ""),
|
|
1365
|
+
"utf8_rune_count": (s) => { let n = 0; for (const _ of (s ?? "")) n++; return n; },
|
|
1366
|
+
"utf8_rune_len": (r) => (r < 0 || r > 0x10ffff || (r >= 0xd800 && r <= 0xdfff) ? -1 : r < 0x80 ? 1 : r < 0x800 ? 2 : r < 0x10000 ? 3 : 4),
|
|
1367
|
+
"utf8_valid_string": (s) => (s ?? "").isWellFormed ? ((s ?? "").isWellFormed() ? 1 : 0) : (/[\\uD800-\\uDFFF]/.test((s ?? "").replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g, "")) ? 0 : 1),
|
|
1368
|
+
"utf8_decode_rune": (s, last) => {
|
|
1369
|
+
s = s ?? "";
|
|
1370
|
+
if (s.length === 0) { decodeSize = 0; return 0xfffd; }
|
|
1371
|
+
let cp;
|
|
1372
|
+
if (last) { const tail = s.codePointAt(s.length - 1); cp = (tail >= 0xdc00 && tail <= 0xdfff && s.length >= 2) ? s.codePointAt(s.length - 2) : tail; }
|
|
1373
|
+
else cp = s.codePointAt(0);
|
|
1374
|
+
if (cp >= 0xd800 && cp <= 0xdfff) { decodeSize = 1; return 0xfffd; }
|
|
1375
|
+
// Size is in UTF-16 code units, matching the JS backend (strings are JS strings).
|
|
1376
|
+
decodeSize = cp > 0xffff ? 2 : 1;
|
|
1377
|
+
return cp;
|
|
1378
|
+
},
|
|
1379
|
+
"utf8_decode_size": () => decodeSize,
|
|
1380
|
+
"utf8_full_rune": (s) => { s = s ?? ""; if (s.length === 0) return 0; const c = s.charCodeAt(0); return (c >= 0xd800 && c <= 0xdbff && s.length < 2) ? 0 : 1; },
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function ${P}trim(s, cutset, left, right) {
|
|
1384
|
+
const cs = Array.from(cutset);
|
|
1385
|
+
const chars = Array.from(s);
|
|
1386
|
+
let a = 0, b = chars.length;
|
|
1387
|
+
if (left) while (a < b && cs.includes(chars[a])) a++;
|
|
1388
|
+
if (right) while (b > a && cs.includes(chars[b - 1])) b--;
|
|
1389
|
+
return chars.slice(a, b).join("");
|
|
1390
|
+
}`;
|
|
1391
|
+
|
|
1392
|
+
// Runtime loader emitted at the top of the facade.
|
|
1393
|
+
const wasmLoaderJs = (stdlibEnv) => `${WASM_IMPORTS_JS}
|
|
1394
|
+
${stdlibEnv ? WASM_STDLIB_JS : ""}
|
|
1395
|
+
async function ${P}fetch(url) {
|
|
1396
|
+
const res = await fetch(url);
|
|
1397
|
+
if (!res.ok) throw new Error("GoFront: failed to fetch " + url + " (" + res.status + ")");
|
|
1398
|
+
return res.arrayBuffer();
|
|
1399
|
+
}
|
|
1400
|
+
async function ${P}instantiate(imports) {
|
|
1401
|
+
const bytes = globalThis.__GOFRONT_WASM_BYTES;
|
|
1402
|
+
if (bytes) return WebAssembly.instantiate(bytes, imports);
|
|
1403
|
+
// Resolve relative to the bundle so the app works from any page path.
|
|
1404
|
+
const url = globalThis.__GOFRONT_WASM_URL ?? new URL("app.wasm", import.meta.url).href;
|
|
1405
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
1406
|
+
try { return await WebAssembly.instantiateStreaming(fetch(url), imports); }
|
|
1407
|
+
catch { /* fall through: wrong MIME type or no streaming support */ }
|
|
1408
|
+
}
|
|
1409
|
+
return WebAssembly.instantiate(await ${P}fetch(url), imports);
|
|
1410
|
+
}
|
|
1411
|
+
async function ${P}load(stringTable, extraEnv) {
|
|
1412
|
+
if (typeof WebAssembly === "undefined" || typeof WebAssembly.Tag !== "function") {
|
|
1413
|
+
throw new Error("GoFront: this runtime lacks the WebAssembly GC / exception support required by app.wasm");
|
|
1414
|
+
}
|
|
1415
|
+
const tag = new WebAssembly.Tag({ "parameters": ["externref"] });
|
|
1416
|
+
const imports = ${P}imports(stringTable, extraEnv, tag, (s) => console.log(s));
|
|
1417
|
+
// Panics arrive as plain JS Errors thrown by env.panic (also from the
|
|
1418
|
+
// start function while package-level initializers run), so exports are
|
|
1419
|
+
// returned raw: no try/catch wrapper, which keeps JS→wasm calls inlinable.
|
|
1420
|
+
const result = await ${P}instantiate(imports);
|
|
1421
|
+
imports.__bind(result.instance.exports);
|
|
1422
|
+
return result.instance.exports;
|
|
1423
|
+
}
|
|
1424
|
+
// Go int/int64 cross the boundary as f64 (exact within the safe-integer range).
|
|
1425
|
+
const ${P}i64in = (v) => (typeof v === "bigint" ? Number(v) : +v);
|
|
1426
|
+
const ${P}i64out = (v) => {
|
|
1427
|
+
if (v > ${P}MAX || v < -${P}MAX) throw new RangeError("GoFront: int64 value " + v + " exceeds the safe JS integer range");
|
|
1428
|
+
return v;
|
|
1429
|
+
};
|
|
1430
|
+
const ${P}u64out = (v) => {
|
|
1431
|
+
if (v < 0 || v > ${P}MAX) throw new RangeError("GoFront: uint64 value exceeds the safe JS integer range");
|
|
1432
|
+
return v;
|
|
1433
|
+
};
|
|
1434
|
+
const ${P}strin = (v) => (v == null ? "" : String(v));
|
|
1435
|
+
const ${P}href = (h) => (h == null ? null : h.__ref);
|
|
1436
|
+
// Tags a struct pointer held in an interface (same marker as the JS backend's __ifp).
|
|
1437
|
+
const ${P}ptag = (h) => {
|
|
1438
|
+
if (h !== null && h.__p !== true && Object.isExtensible(h)) Object.defineProperty(h, "__p", { "value": true, "configurable": true });
|
|
1439
|
+
return h;
|
|
1440
|
+
};
|
|
1441
|
+
const ${P}NIL_DEREF_PATTERNS = [
|
|
1442
|
+
"dereferencing a null pointer", // V8
|
|
1443
|
+
"dereferencing null pointer", // SpiderMonkey
|
|
1444
|
+
"null pointer dereference", // SpiderMonkey
|
|
1445
|
+
"null dereference", // JavaScriptCore
|
|
1446
|
+
];
|
|
1447
|
+
function ${P}mapTrap(e) {
|
|
1448
|
+
if (typeof WebAssembly !== "undefined" && e instanceof WebAssembly.RuntimeError) {
|
|
1449
|
+
const msg = e.message || "";
|
|
1450
|
+
for (const p of ${P}NIL_DEREF_PATTERNS) {
|
|
1451
|
+
if (msg.includes(p)) {
|
|
1452
|
+
return new Error("runtime error: invalid memory address or nil pointer dereference");
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return e;
|
|
1457
|
+
}
|
|
1458
|
+
// Live index view over a wasm array/slice (reads and writes go through to wasm).
|
|
1459
|
+
function ${P}idxview(n, getAt, setAt) {
|
|
1460
|
+
const inRange = (k) => { if (typeof k !== "string") return -1; const i = +k; return i === (i | 0) && i >= 0 && i < n ? i : -1; };
|
|
1461
|
+
return new Proxy(new Array(n), {
|
|
1462
|
+
"get": (t, k, r) => { const i = inRange(k); return i < 0 ? Reflect.get(t, k, r) : getAt(i); },
|
|
1463
|
+
"set": (t, k, v, r) => { const i = inRange(k); if (i < 0) return Reflect.set(t, k, v, r); setAt(i, v); return true; },
|
|
1464
|
+
"has": (t, k) => inRange(k) >= 0 || Reflect.has(t, k),
|
|
1465
|
+
"getOwnPropertyDescriptor": (t, k) => {
|
|
1466
|
+
const i = inRange(k);
|
|
1467
|
+
if (i < 0) return Reflect.getOwnPropertyDescriptor(t, k);
|
|
1468
|
+
return { "value": getAt(i), "writable": true, "enumerable": true, "configurable": true };
|
|
1469
|
+
},
|
|
1470
|
+
"ownKeys": (t) => { const keys = []; for (let i = 0; i < n; i++) keys.push(String(i)); keys.push("length"); return keys; },
|
|
1471
|
+
});
|
|
1472
|
+
}`;
|
|
1473
|
+
|
|
1474
|
+
// Generates the JS facade for the linked wasm module.
|
|
1475
|
+
export function generateFacade(
|
|
1476
|
+
meta,
|
|
1477
|
+
{ stringTable = [], callMain = false, stdlibEnv = true } = {},
|
|
1478
|
+
) {
|
|
1479
|
+
const needs = meta._needs ?? collectNeeds(meta);
|
|
1480
|
+
const out = [wasmLoaderJs(stdlibEnv)];
|
|
1481
|
+
|
|
1482
|
+
// JS callback imports (registered before instantiation; bodies run after).
|
|
1483
|
+
out.push(`const ${P}env = {};`);
|
|
1484
|
+
for (const [key, desc] of needs.funcs) {
|
|
1485
|
+
const names = desc.params.map((_, i) => `a${i}`);
|
|
1486
|
+
const args = desc.params.map((d, i) =>
|
|
1487
|
+
isPtrBoth(d) ? `${P}view_${d.name}(${names[i]})` : outExpr(d, names[i]),
|
|
1488
|
+
);
|
|
1489
|
+
const call = `fn(${args.join(", ")})`;
|
|
1490
|
+
let body;
|
|
1491
|
+
if (desc.returns.length === 0) body = `${call};`;
|
|
1492
|
+
else if (desc.returns.length === 1)
|
|
1493
|
+
body = `return ${inExpr(desc.returns[0], call)};`;
|
|
1494
|
+
else
|
|
1495
|
+
body = `const __r = ${call}; return [${desc.returns
|
|
1496
|
+
.map((d, i) => inExpr(d, `__r[${i}]`))
|
|
1497
|
+
.join(", ")}];`;
|
|
1498
|
+
const sig = ["fn", ...names].join(", ");
|
|
1499
|
+
out.push(
|
|
1500
|
+
`${P}env["__invoke$${key}"] = (${sig}) => { try { ${body} } catch (__e) { if (__w && __w.__push_panic) __w.__push_panic(__e.message ?? String(__e)); throw __e; } };`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
out.push(
|
|
1504
|
+
`const __w = await ${P}load(${JSON.stringify(stringTable)}, ${P}env);`,
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
for (const [name, { info, fields }] of needs.structs) {
|
|
1508
|
+
if (info.pkgTarget === "both") genBothStruct(out, name, fields);
|
|
1509
|
+
else {
|
|
1510
|
+
const methods = meta.structByName.get(name)?.methods ?? [];
|
|
1511
|
+
genHandleStruct(out, name, fields, methods);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
for (const [key, desc] of needs.slices) {
|
|
1516
|
+
out.push(`function ${P}slin_${key}(arr) {
|
|
1517
|
+
if (arr == null) return __w.__slice_nil_${key}();
|
|
1518
|
+
const n = arr.length;
|
|
1519
|
+
const s = __w.__slice_new_${key}(n);
|
|
1520
|
+
for (let i = 0; i < n; i++) __w.__slice_set_${key}(s, i, ${inExpr(desc.elem, "arr[i]")});
|
|
1521
|
+
return s;
|
|
1522
|
+
}
|
|
1523
|
+
function ${P}slout_${key}(s) {
|
|
1524
|
+
if (s == null || __w.__slice_is_nil_${key}(s)) return null;
|
|
1525
|
+
const n = __w.__slice_len_${key}(s);
|
|
1526
|
+
const out = new Array(n);
|
|
1527
|
+
for (let i = 0; i < n; i++) out[i] = ${outExpr(desc.elem, `__w.__slice_get_${key}(s, i)`)};
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
function ${P}slview_${key}(s) {
|
|
1531
|
+
if (s == null || __w.__slice_is_nil_${key}(s)) return null;
|
|
1532
|
+
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")}));
|
|
1533
|
+
}`);
|
|
1534
|
+
}
|
|
1535
|
+
for (const [key, desc] of needs.arrays) {
|
|
1536
|
+
out.push(`function ${P}arrzero_${key}(n) {
|
|
1537
|
+
const out = new Array(n);
|
|
1538
|
+
for (let i = 0; i < n; i++) out[i] = ${zeroExpr(desc.elem)};
|
|
1539
|
+
return out;
|
|
1540
|
+
}
|
|
1541
|
+
function ${P}arrin_${key}(arr) {
|
|
1542
|
+
if (arr == null) return null;
|
|
1543
|
+
const n = arr.length;
|
|
1544
|
+
const a = __w.__array_new_${key}(n);
|
|
1545
|
+
for (let i = 0; i < n; i++) __w.__array_set_${key}(a, i, ${inExpr(desc.elem, "arr[i]")});
|
|
1546
|
+
return a;
|
|
1547
|
+
}
|
|
1548
|
+
function ${P}arrout_${key}(a) {
|
|
1549
|
+
if (a == null) return null;
|
|
1550
|
+
const n = __w.__array_len_${key}(a);
|
|
1551
|
+
const out = new Array(n);
|
|
1552
|
+
for (let i = 0; i < n; i++) out[i] = ${outExpr(desc.elem, `__w.__array_get_${key}(a, i)`)};
|
|
1553
|
+
return out;
|
|
1554
|
+
}
|
|
1555
|
+
function ${P}arrview_${key}(a) {
|
|
1556
|
+
if (a == null) return null;
|
|
1557
|
+
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")}));
|
|
1558
|
+
}`);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
for (const [key, desc] of needs.funcs) {
|
|
1562
|
+
const { names, body } = genFuncBody(
|
|
1563
|
+
`__call_fn$${key}`,
|
|
1564
|
+
desc.params,
|
|
1565
|
+
desc.returns,
|
|
1566
|
+
["c"],
|
|
1567
|
+
);
|
|
1568
|
+
out.push(`const ${P}fn_j2w_${key} = new WeakMap();
|
|
1569
|
+
const ${P}fn_w2j_${key} = new WeakMap();
|
|
1570
|
+
function ${P}fnin_${key}(fn) {
|
|
1571
|
+
if (fn == null) return null;
|
|
1572
|
+
let c = ${P}fn_j2w_${key}.get(fn);
|
|
1573
|
+
if (!c) {
|
|
1574
|
+
c = __w.__wrap_fn$${key}(fn);
|
|
1575
|
+
${P}fn_j2w_${key}.set(fn, c);
|
|
1576
|
+
${P}fn_w2j_${key}.set(c, fn);
|
|
1577
|
+
}
|
|
1578
|
+
return c;
|
|
1579
|
+
}
|
|
1580
|
+
function ${P}fnout_${key}(c) {
|
|
1581
|
+
if (c == null) return null;
|
|
1582
|
+
let fn = ${P}fn_w2j_${key}.get(c);
|
|
1583
|
+
if (!fn) {
|
|
1584
|
+
fn = (${names.join(", ")}) => {
|
|
1585
|
+
${indent(body, "\t")}
|
|
1586
|
+
};
|
|
1587
|
+
fn.__ref = c;
|
|
1588
|
+
${P}fn_w2j_${key}.set(c, fn);
|
|
1589
|
+
${P}fn_j2w_${key}.set(fn, c);
|
|
1590
|
+
}
|
|
1591
|
+
return fn;
|
|
1592
|
+
}`);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
for (const [key, desc] of needs.ifaces) genIfaceFacade(out, key, desc);
|
|
1596
|
+
|
|
1597
|
+
if (needs.shared) {
|
|
1598
|
+
// Zero-copy TypedArray views over the exported memory. Memory never
|
|
1599
|
+
// grows after the start function (allocation is startup-only), but the
|
|
1600
|
+
// cached view is still re-validated against the live buffer.
|
|
1601
|
+
out.push(`const ${P}shc = new WeakMap();
|
|
1602
|
+
function ${P}shout(r, Ctor) {
|
|
1603
|
+
if (r == null) return null;
|
|
1604
|
+
let v = ${P}shc.get(r);
|
|
1605
|
+
if (!v || v.buffer !== __w.memory.buffer) {
|
|
1606
|
+
v = new Ctor(__w.memory.buffer, __w.__shared_base(r), __w.__shared_len(r));
|
|
1607
|
+
${P}shc.set(r, v);
|
|
1608
|
+
}
|
|
1609
|
+
return v;
|
|
1610
|
+
}
|
|
1611
|
+
function ${P}shin(v, Ctor) {
|
|
1612
|
+
if (v == null) return null;
|
|
1613
|
+
if (!(v instanceof Ctor) || v.buffer !== __w.memory.buffer) throw new TypeError("GoFront: expected a " + Ctor.name + " view over the wasm shared memory");
|
|
1614
|
+
return __w.__shared_make(v.byteOffset, v.length);
|
|
1615
|
+
}`);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
for (const c of meta.consts) out.push(`const ${c.name} = ${c.value};`);
|
|
1619
|
+
for (const v of meta.vars ?? [])
|
|
1620
|
+
out.push(
|
|
1621
|
+
`const ${v.name} = ${P}shout(__w.__var_${v.name}(), ${v.desc.ctor});`,
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1624
|
+
for (const f of meta.funcs) {
|
|
1625
|
+
const { names, body } = genFuncBody(
|
|
1626
|
+
f.callName ?? f.exportName,
|
|
1627
|
+
f.params,
|
|
1628
|
+
f.returns,
|
|
1629
|
+
);
|
|
1630
|
+
out.push(`function ${f.name}(${names.join(", ")}) {\n${body}\n}`);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (callMain) out.push("__w.main();");
|
|
1634
|
+
return bindExports(out.join("\n"));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Rewrites `__w.name(...)` into calls through module-level `const` bindings.
|
|
1638
|
+
// V8 only inlines a JS→wasm call (and so passes f32/f64 arguments unboxed)
|
|
1639
|
+
// when the callee is a compile-time constant; a property load off the exports
|
|
1640
|
+
// object is not, and every float argument then costs a 16-byte HeapNumber.
|
|
1641
|
+
function bindExports(src) {
|
|
1642
|
+
const loadLine = "const __w = await ";
|
|
1643
|
+
const at = src.indexOf(loadLine);
|
|
1644
|
+
if (at < 0) return src;
|
|
1645
|
+
const eol = src.indexOf("\n", at);
|
|
1646
|
+
const head = src.slice(0, eol + 1);
|
|
1647
|
+
let tail = src.slice(eol + 1);
|
|
1648
|
+
const names = new Set();
|
|
1649
|
+
tail = tail.replace(/__w\.([A-Za-z_$][\w$]*)/g, (_, n) => {
|
|
1650
|
+
names.add(n);
|
|
1651
|
+
return `__w$${n}`;
|
|
1652
|
+
});
|
|
1653
|
+
const binds = [...names].map((n) => `const __w$${n} = __w.${n};`).join("\n");
|
|
1654
|
+
return `${head}${binds}\n${tail}`;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function genBothStruct(out, name, fields) {
|
|
1658
|
+
const toArgs = fields.map((f) => {
|
|
1659
|
+
const v = `o.${f.name}`;
|
|
1660
|
+
if (isBothStruct(f.desc))
|
|
1661
|
+
return `${P}to_${f.desc.name}(${v}, ${f.desc.ptr ? "false" : "true"}, m)`;
|
|
1662
|
+
return inExpr(f.desc, v);
|
|
1663
|
+
});
|
|
1664
|
+
const backLines = fields.map((f) => {
|
|
1665
|
+
const got = `__w.__get_${name}_${f.name}(r)`;
|
|
1666
|
+
if (isBothStruct(f.desc))
|
|
1667
|
+
return `\t${P}back_${f.desc.name}(${got}, o.${f.name});`;
|
|
1668
|
+
return `\to.${f.name} = ${outExpr(f.desc, got)};`;
|
|
1669
|
+
});
|
|
1670
|
+
const fromArgs = fields.map((f) =>
|
|
1671
|
+
outExpr(f.desc, `__w.__get_${name}_${f.name}(r)`),
|
|
1672
|
+
);
|
|
1673
|
+
const viewProps = fields.map((f) => {
|
|
1674
|
+
const got = `__w.__get_${name}_${f.name}(this.__ref)`;
|
|
1675
|
+
return `"${f.name}": { "enumerable": true, "get"() { return ${viewExpr(f.desc, got)}; }, "set"(v) { __w.__set_${name}_${f.name}(this.__ref, ${inExpr(f.desc, "v")}); } }`;
|
|
1676
|
+
});
|
|
1677
|
+
out.push(`function ${P}to_${name}(o, isValue, m) {
|
|
1678
|
+
if (o == null) return isValue ? __w.__zero_${name}() : null;
|
|
1679
|
+
if (m) { const hit = m.get(o); if (hit) return hit; }
|
|
1680
|
+
const r = __w.__new_${name}(${toArgs.join(", ")});
|
|
1681
|
+
if (m) m.set(o, r);
|
|
1682
|
+
return r;
|
|
1683
|
+
}
|
|
1684
|
+
function ${P}from_${name}(r) {
|
|
1685
|
+
if (r == null) return null;
|
|
1686
|
+
return new ${name}(${fromArgs.join(", ")});
|
|
1687
|
+
}
|
|
1688
|
+
// Live views share one prototype and are cached per wasm object so hot paths
|
|
1689
|
+
// (\`h.Pos.X = 1\` in a frame loop) do not allocate a wrapper per access. The
|
|
1690
|
+
// prototype is built on first use: the facade may precede the \`both\`
|
|
1691
|
+
// package's own JS (and so the \`${name}\` class) in the bundle.
|
|
1692
|
+
let ${P}vp_${name} = null;
|
|
1693
|
+
const ${P}vc_${name} = new WeakMap();
|
|
1694
|
+
function ${P}view_${name}(r) {
|
|
1695
|
+
if (r == null) return null;
|
|
1696
|
+
let o = ${P}vc_${name}.get(r);
|
|
1697
|
+
if (!o) {
|
|
1698
|
+
if (${P}vp_${name} === null) ${P}vp_${name} = Object.create(${name}.prototype, { ${viewProps.join(", ")} });
|
|
1699
|
+
o = Object.create(${P}vp_${name}); o.__ref = r; ${P}vc_${name}.set(r, o);
|
|
1700
|
+
}
|
|
1701
|
+
return o;
|
|
1702
|
+
}
|
|
1703
|
+
function ${P}back_${name}(r, o) {
|
|
1704
|
+
if (r == null || o == null) return;
|
|
1705
|
+
${backLines.join("\n")}
|
|
1706
|
+
}`);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function genIfaceFacade(out, key, desc) {
|
|
1710
|
+
const methods = desc.methods.map((m) => {
|
|
1711
|
+
const { names, body } = genFuncBody(m.callName, m.params, m.returns, [
|
|
1712
|
+
"this.__ref",
|
|
1713
|
+
]);
|
|
1714
|
+
return `\t${m.name}(${names.join(", ")}) {\n${indent(body, "\t")}\n\t}`;
|
|
1715
|
+
});
|
|
1716
|
+
const cases = desc.concrete.map((c, i) => {
|
|
1717
|
+
const wrap = c.both ? `${P}view_${c.name}(r)` : `${c.name}.__wrap(r)`;
|
|
1718
|
+
return `\t\tcase ${i + 1}: return ${P}ptag(${wrap});`;
|
|
1719
|
+
});
|
|
1720
|
+
const tag =
|
|
1721
|
+
cases.length > 0
|
|
1722
|
+
? `\tswitch (__w.__itag$${key}(r)) {\n${cases.join("\n")}\n\t}\n`
|
|
1723
|
+
: "";
|
|
1724
|
+
const msg = `GoFront: a JS-implemented value cannot cross into wasm as interface '${desc.name}'; implement it in a wasm package or pass a func callback`;
|
|
1725
|
+
const implMsg = `GoFront: wasm value does not implement interface '${desc.name}'`;
|
|
1726
|
+
out.push(`class ${P}I_${key} {
|
|
1727
|
+
constructor(r) { this.__ref = r; }
|
|
1728
|
+
${methods.join("\n")}
|
|
1729
|
+
}
|
|
1730
|
+
const ${P}ic_${key} = new WeakMap();
|
|
1731
|
+
function ${P}ifout_${key}(r) {
|
|
1732
|
+
if (r == null) return null;
|
|
1733
|
+
${tag} let o = ${P}ic_${key}.get(r);
|
|
1734
|
+
if (!o) { o = new ${P}I_${key}(r); ${P}ic_${key}.set(r, o); }
|
|
1735
|
+
return o;
|
|
1736
|
+
}
|
|
1737
|
+
function ${P}ifin_${key}(v) {
|
|
1738
|
+
if (v == null) return null;
|
|
1739
|
+
if (v.__ref === undefined) throw new TypeError(${JSON.stringify(msg)});
|
|
1740
|
+
if (!__w.__impl$${key}(v.__ref)) throw new TypeError(${JSON.stringify(implMsg)});
|
|
1741
|
+
return v.__ref;
|
|
1742
|
+
}`);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function genHandleStruct(out, name, fields, methods) {
|
|
1746
|
+
// `$`-suffixed params keep the mangler from renaming the matching getters.
|
|
1747
|
+
const ctorParams = fields.map((f) => `${f.name}$ = ${zeroExpr(f.desc)}`);
|
|
1748
|
+
const ctorArgs = fields.map((f) => inExpr(f.desc, `${f.name}$`));
|
|
1749
|
+
const fromArgs = fields.map((f) => `o.${f.name}`);
|
|
1750
|
+
const ptrMsg = `GoFront: expected a ${name} handle for a *${name} parameter (use ${name}.from(obj) to create one)`;
|
|
1751
|
+
const lines = [
|
|
1752
|
+
`const ${P}h_${name} = new WeakMap();
|
|
1753
|
+
function ${P}hval_${name}(h) {
|
|
1754
|
+
if (h == null) return __w.__zero_${name}();
|
|
1755
|
+
if (h.__ref !== undefined) return __w.__clone_${name}(h.__ref);
|
|
1756
|
+
return ${name}.from(h).__ref;
|
|
1757
|
+
}
|
|
1758
|
+
function ${P}href_${name}(h) {
|
|
1759
|
+
if (h == null) return null;
|
|
1760
|
+
if (h.__ref !== undefined) return h.__ref;
|
|
1761
|
+
throw new TypeError(${JSON.stringify(ptrMsg)});
|
|
1762
|
+
}
|
|
1763
|
+
class ${name} {
|
|
1764
|
+
constructor(${ctorParams.join(", ")}) {
|
|
1765
|
+
this.__ref = __w.__new_${name}(${ctorArgs.join(", ")});
|
|
1766
|
+
${P}h_${name}.set(this.__ref, this);
|
|
1767
|
+
}
|
|
1768
|
+
static from(o) { return new ${name}(${fromArgs.join(", ")}); }
|
|
1769
|
+
static __wrap(ref) {
|
|
1770
|
+
if (ref == null) return null;
|
|
1771
|
+
let h = ${P}h_${name}.get(ref);
|
|
1772
|
+
if (!h) { h = Object.create(${name}.prototype); h.__ref = ref; ${P}h_${name}.set(ref, h); }
|
|
1773
|
+
return h;
|
|
1774
|
+
}
|
|
1775
|
+
__clone() { return ${name}.__wrap(__w.__clone_${name}(this.__ref)); }`,
|
|
1776
|
+
];
|
|
1777
|
+
for (const f of fields) {
|
|
1778
|
+
const got = `__w.__get_${name}_${f.name}(this.__ref)`;
|
|
1779
|
+
lines.push(`\tget ${f.name}() { return ${viewExpr(f.desc, got)}; }`);
|
|
1780
|
+
lines.push(
|
|
1781
|
+
`\tset ${f.name}(v) { __w.__set_${name}_${f.name}(this.__ref, ${inExpr(f.desc, "v")}); }`,
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
for (const m of methods) {
|
|
1785
|
+
const recv = m.ptrRecv ? "this.__ref" : `__w.__clone_${name}(this.__ref)`;
|
|
1786
|
+
const { names, body } = genFuncBody(
|
|
1787
|
+
m.callName ?? m.exportName,
|
|
1788
|
+
m.params,
|
|
1789
|
+
m.returns,
|
|
1790
|
+
[recv],
|
|
1791
|
+
);
|
|
1792
|
+
lines.push(
|
|
1793
|
+
`\t${m.name}(${names.join(", ")}) {\n${indent(body, "\t")}\n\t}`,
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
lines.push("}");
|
|
1797
|
+
out.push(lines.join("\n"));
|
|
1798
|
+
}
|