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.
Files changed (77) hide show
  1. package/README.md +174 -403
  2. package/package.json +16 -19
  3. package/src/{codegen → backend/js}/expressions.js +265 -77
  4. package/src/{codegen → backend/js}/index.js +120 -89
  5. package/src/{codegen → backend/js}/runtime.js +102 -0
  6. package/src/{codegen → backend/js}/source-map.js +32 -0
  7. package/src/{codegen → backend/js}/statements.js +139 -64
  8. package/src/backend/js/stdlib/bits.js +32 -0
  9. package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
  10. package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
  11. package/src/{codegen → backend/js}/stdlib/index.js +6 -0
  12. package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
  13. package/src/{codegen → backend/js}/stdlib/path.js +2 -2
  14. package/src/backend/js/stdlib/shared.js +17 -0
  15. package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
  16. package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
  17. package/src/backend/js/stdlib/strconv.js +58 -0
  18. package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
  19. package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
  20. package/src/{codegen → backend/js}/stdlib/time.js +3 -3
  21. package/src/{codegen → backend/js}/templ.js +3 -3
  22. package/src/backend/wasm/boundary.js +1798 -0
  23. package/src/backend/wasm/emit-builtins.js +1103 -0
  24. package/src/backend/wasm/emit-exprs.js +1908 -0
  25. package/src/backend/wasm/emit-maps.js +1863 -0
  26. package/src/backend/wasm/emit-shared.js +519 -0
  27. package/src/backend/wasm/emit-stdlib.js +3006 -0
  28. package/src/backend/wasm/emit-stmts.js +2472 -0
  29. package/src/backend/wasm/emit.js +1376 -0
  30. package/src/backend/wasm/encode.js +937 -0
  31. package/src/backend/wasm/glue.js +144 -0
  32. package/src/backend/wasm/index.js +2790 -0
  33. package/src/backend/wasm/monomorph.js +711 -0
  34. package/src/backend/wasm/optimize.js +227 -0
  35. package/src/backend/wasm/types.js +544 -0
  36. package/src/backend/wasm/wat.js +302 -0
  37. package/src/cli-core.js +323 -75
  38. package/src/compiler.js +447 -14
  39. package/src/dev-server.js +56 -15
  40. package/src/index.js +99 -120
  41. package/src/lexer.js +18 -0
  42. package/src/lower/boxing.js +35 -0
  43. package/src/lower/captures.js +172 -0
  44. package/src/lower/embedding.js +30 -0
  45. package/src/lower/escape.js +164 -0
  46. package/src/lower/functions.js +302 -0
  47. package/src/lower/globals.js +316 -0
  48. package/src/lower/index.js +118 -0
  49. package/src/lower/ownership.js +137 -0
  50. package/src/lower/range.js +67 -0
  51. package/src/lower/walk.js +25 -0
  52. package/src/minifier.js +2 -0
  53. package/src/parser/index.js +8 -3
  54. package/src/resolver.js +3 -1
  55. package/src/test-runner.js +94 -20
  56. package/src/typechecker/assignability.js +42 -1
  57. package/src/typechecker/expressions.js +182 -3
  58. package/src/typechecker/index.js +261 -5
  59. package/src/typechecker/resolve.js +35 -6
  60. package/src/typechecker/stdlib/core.js +34 -19
  61. package/src/typechecker/stdlib/extended.js +18 -3
  62. package/src/typechecker/stdlib/shared.js +53 -0
  63. package/src/typechecker/stdlib.js +2 -0
  64. package/src/typechecker/types.js +259 -6
  65. package/src/vendor.js +2 -2
  66. package/src/codegen/stdlib/strconv.js +0 -33
  67. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  68. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  69. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  70. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  71. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  72. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  73. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  74. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  75. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  76. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  77. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
@@ -0,0 +1,1376 @@
1
+ // src/backend/wasm/emit.js
2
+ // FunctionEmitter core: function prologue/epilogue, defer/recover frames,
3
+ // locals, control stack and shared helpers. Statement, expression, builtin,
4
+ // map and stdlib emission live in the emit-*.js mixins composed at the bottom.
5
+
6
+ import { hasDefer, hasDirectRecover } from "../../lower/functions.js";
7
+ import { BuiltinsEmitter } from "./emit-builtins.js";
8
+ import { ExprsEmitter } from "./emit-exprs.js";
9
+ import { MapsEmitter } from "./emit-maps.js";
10
+ import { SharedEmitter } from "./emit-shared.js";
11
+ import { StdlibEmitter } from "./emit-stdlib.js";
12
+ import { StmtsEmitter } from "./emit-stmts.js";
13
+ import {
14
+ isArrayType,
15
+ isPointerToStruct,
16
+ isSliceType,
17
+ isStringType,
18
+ isStructType,
19
+ toWasmType,
20
+ } from "./types.js";
21
+
22
+ // The emitter is one class split over several files by concern. Methods from
23
+ // the emit-*.js modules are assigned to FunctionEmitter.prototype below; `this`
24
+ // is shared, so cross-file calls are plain method calls.
25
+ export class FunctionEmitter {
26
+ constructor(moduleEmitter, funcDecl, funcIndex) {
27
+ this.mod = moduleEmitter;
28
+ this.funcDecl = funcDecl;
29
+ this.funcIndex = funcIndex;
30
+
31
+ this.rootFuncDecl = this.funcDecl._rootFuncDecl ?? this.funcDecl;
32
+ // Methods are normalized into synthetic FuncDecls; lower() keyed on the original MethodDecl.
33
+ this.captureAnalysis =
34
+ this.mod.lowerResult?.captures?.get(
35
+ this.rootFuncDecl._sourceDecl ?? this.rootFuncDecl,
36
+ ) ?? null;
37
+ this.mutatedCaptures = this.captureAnalysis?.mutatedCaptures ?? new Set();
38
+
39
+ this.capturedNames = this.funcDecl._funcLit
40
+ ? Array.from(
41
+ this.captureAnalysis?.capturesByClosure?.get(
42
+ this.funcDecl._funcLit,
43
+ ) ?? [],
44
+ ).sort()
45
+ : [];
46
+
47
+ this.locals = new Map(); // name -> { index, type, goType, isParam, isBoxed, boxInfo }
48
+ this.cachedGlobals = new Map(); // globalName -> { index, globalInfo }
49
+ this.localTypes = []; // list of additional local types (beyond params)
50
+ this.controlStack = []; // stack of { type: 'block'|'loop', label: string }
51
+ this.body = []; // emitted instructions
52
+
53
+ this.tempCounter = 0;
54
+
55
+ this.returnTypes = [];
56
+ if (this.funcDecl.returnType) {
57
+ if (
58
+ this.funcDecl.returnType.kind === "TupleType" ||
59
+ this.funcDecl.returnType.kind === "tuple"
60
+ ) {
61
+ for (const t of this.funcDecl.returnType.types) {
62
+ const wt = this.toWasmType(t);
63
+ if (wt) this.returnTypes.push(wt);
64
+ }
65
+ } else {
66
+ const wt = this.toWasmType(this.funcDecl.returnType);
67
+ if (wt) this.returnTypes.push(wt);
68
+ }
69
+ }
70
+
71
+ this._initParams();
72
+
73
+ this.hasDefer = false;
74
+ this.namedReturnVars = null;
75
+ this.returnTempLocals = null;
76
+ this.defersLocalIndex = null;
77
+ this.panicExnLocalIndex = null;
78
+ this.hasPanicLocalIndex = null;
79
+ this.curClosureLocalIndex = null;
80
+ // Local holding the `__deferArmed` snapshot taken in the prologue; null
81
+ // when this frame can never legitimately recover().
82
+ this.recoverOkLocalIndex = null;
83
+ }
84
+
85
+ // Go only honours recover() when it is called directly by a deferred
86
+ // function. The defer loop sets `__deferArmed` right before invoking a
87
+ // deferred closure; the closure's prologue copies the flag into a local and
88
+ // clears the global, so any further callee sees it unset. Synthesized
89
+ // wrappers (`defer f(x)` lowered to `func(){ f(__defarg$1) }`) that forward
90
+ // to a callee which may itself call recover() leave the flag armed.
91
+ _emitRecoverArmPrologue(body) {
92
+ const lit = this.funcDecl._funcLit;
93
+ const isTarget = Boolean(lit?._isDeferTarget);
94
+ const forwards = isTarget && Boolean(lit._deferForward);
95
+ if (forwards) return;
96
+ if (!isTarget && !hasDirectRecover(body)) return;
97
+ const armed = this.mod.getDeferArmedGlobalIndex();
98
+ const local = this.allocLocal("__recoverOk", "i32", null, true);
99
+ this.pushInstruction({ op: "global.get", index: armed });
100
+ this.pushInstruction({ op: "local.set", index: local });
101
+ this.pushInstruction({ op: "i32.const", value: 0 });
102
+ this.pushInstruction({ op: "global.set", index: armed });
103
+ this.recoverOkLocalIndex = local;
104
+ }
105
+
106
+ emitFunctionBody(body) {
107
+ if (
108
+ this.funcDecl._isClosure &&
109
+ !this.funcDecl._isTrampoline &&
110
+ this.capturedNames.length > 0
111
+ ) {
112
+ this._unpackClosureEnv();
113
+ }
114
+
115
+ this._boxMutatedParams();
116
+
117
+ this._cacheScratchGlobals();
118
+
119
+ this._emitRecoverArmPrologue(body);
120
+
121
+ const hasDef = hasDefer(body);
122
+ this.hasDefer = hasDef;
123
+
124
+ if (hasDef) {
125
+ this._emitFunctionBodyWithDefer(body);
126
+ return;
127
+ }
128
+
129
+ const namedReturns = this.funcDecl.returnType?._namedReturns;
130
+ if (namedReturns && namedReturns.length > 0) {
131
+ this.namedReturnVars = [];
132
+ for (let i = 0; i < namedReturns.length; i++) {
133
+ const r = namedReturns[i];
134
+ const isBlank = !r.name || r.name === "_";
135
+ const varName = isBlank ? `__ret_blank$${i}` : r.name;
136
+ const wType = this.toWasmType(r.type);
137
+ const localIdx = this.allocLocal(varName, wType, r.type, isBlank);
138
+ this.namedReturnVars.push({
139
+ name: varName,
140
+ localIdx,
141
+ type: wType,
142
+ goType: r.type,
143
+ isBlank,
144
+ });
145
+ this.emitZeroValue(r.type, wType);
146
+ const localInfo = this.locals.get(varName);
147
+ if (localInfo?.isBoxed) {
148
+ this.pushInstruction({
149
+ op: "struct.new",
150
+ typeIndex: localInfo.boxInfo.typeIndex,
151
+ });
152
+ }
153
+ this.pushInstruction({ op: "local.set", index: localIdx });
154
+ }
155
+ }
156
+
157
+ const needsOobBlock = this._needsOobBlock(body);
158
+ if (needsOobBlock) {
159
+ this.pushInstruction({ op: "block", blockType: "void" });
160
+ this.pushControl("oob");
161
+ }
162
+
163
+ this.emitBlock(body);
164
+
165
+ if (needsOobBlock) {
166
+ const lastOp = this.body[this.body.length - 1]?.op;
167
+ if (lastOp !== "return" && lastOp !== "unreachable") {
168
+ if (this.returnTypes.length === 0) {
169
+ this.pushInstruction({ op: "return" });
170
+ } else {
171
+ this.pushInstruction({ op: "unreachable" });
172
+ }
173
+ }
174
+ this.pushInstruction("end");
175
+
176
+ this.pushInstruction({
177
+ op: "call",
178
+ funcIndex: this.mod.getBoundsPanicFuncIndex(),
179
+ });
180
+ this.pushInstruction("unreachable");
181
+ }
182
+ }
183
+
184
+ _emitFunctionBodyWithDefer(body) {
185
+ const namedReturns = this.funcDecl.returnType?._namedReturns;
186
+ if (namedReturns && namedReturns.length > 0) {
187
+ this.namedReturnVars = [];
188
+ for (let i = 0; i < namedReturns.length; i++) {
189
+ const r = namedReturns[i];
190
+ const isBlank = !r.name || r.name === "_";
191
+ const varName = isBlank ? `__ret_blank$${i}` : r.name;
192
+ const wType = this.toWasmType(r.type);
193
+ const localIdx = this.allocLocal(varName, wType, r.type, isBlank);
194
+ this.namedReturnVars.push({
195
+ name: varName,
196
+ localIdx,
197
+ type: wType,
198
+ goType: r.type,
199
+ isBlank,
200
+ });
201
+ this.emitZeroValue(r.type, wType);
202
+ const localInfo = this.locals.get(varName);
203
+ if (localInfo?.isBoxed) {
204
+ this.pushInstruction({
205
+ op: "struct.new",
206
+ typeIndex: localInfo.boxInfo.typeIndex,
207
+ });
208
+ }
209
+ this.pushInstruction({ op: "local.set", index: localIdx });
210
+ }
211
+ } else if (this.returnTypes.length > 0) {
212
+ this.returnTempLocals = [];
213
+ for (let i = 0; i < this.returnTypes.length; i++) {
214
+ const wType = this.returnTypes[i];
215
+ const localIdx = this.allocLocal(`__ret$${i}`, wType, null, true);
216
+ this.returnTempLocals.push(localIdx);
217
+ this.emitZeroValue(null, wType);
218
+ this.pushInstruction({ op: "local.set", index: localIdx });
219
+ }
220
+ }
221
+
222
+ const deferNodeTypeIndex = this.mod.getDeferNodeTypeIndex();
223
+ const deferClosureSig = { kind: "Signature", params: [], results: [] };
224
+ const deferClosureInfo = this.mod.getClosureType(deferClosureSig);
225
+
226
+ this.defersLocalIndex = this.allocLocal(
227
+ "__defers",
228
+ { kind: "ref", nullable: true, typeIndex: deferNodeTypeIndex },
229
+ null,
230
+ true,
231
+ );
232
+ this.pushInstruction({ op: "ref.null", typeIndex: deferNodeTypeIndex });
233
+ this.pushInstruction({ op: "local.set", index: this.defersLocalIndex });
234
+
235
+ this.panicExnLocalIndex = this.allocLocal(
236
+ "__panicExn",
237
+ "exnref",
238
+ null,
239
+ true,
240
+ );
241
+
242
+ this.hasPanicLocalIndex = this.allocLocal("__hasPanic", "i32", null, true);
243
+ this.pushInstruction({ op: "i32.const", value: 0 });
244
+ this.pushInstruction({ op: "local.set", index: this.hasPanicLocalIndex });
245
+
246
+ this.curClosureLocalIndex = this.allocLocal(
247
+ "__curClosure",
248
+ { kind: "ref", nullable: true, typeIndex: deferClosureInfo.typeIndex },
249
+ null,
250
+ true,
251
+ );
252
+
253
+ this.pushInstruction({ op: "block", blockType: "void" });
254
+ this.pushControl("runDefers");
255
+
256
+ this.pushInstruction({ op: "block", blockType: "exnref" });
257
+ this.pushControl("catchHandler");
258
+
259
+ this.pushInstruction({
260
+ op: "try_table",
261
+ blockType: "void",
262
+ catches: [{ kind: "catch_all_ref", label: 0 }],
263
+ });
264
+ this.pushControl("try_table");
265
+
266
+ const needsOobBlock = this._needsOobBlock(body);
267
+ if (needsOobBlock) {
268
+ this.pushInstruction({ op: "block", blockType: "void" });
269
+ this.pushControl("oob");
270
+ }
271
+
272
+ this.emitBlock(body);
273
+
274
+ if (needsOobBlock) {
275
+ const lastOp = this.body[this.body.length - 1]?.op;
276
+ if (lastOp !== "return" && lastOp !== "unreachable") {
277
+ this.pushInstruction({
278
+ op: "br",
279
+ depth: this.resolveBranchDepthToRole("runDefers"),
280
+ });
281
+ }
282
+ this.pushInstruction("end");
283
+
284
+ this.pushInstruction({
285
+ op: "call",
286
+ funcIndex: this.mod.getBoundsPanicFuncIndex(),
287
+ });
288
+ this.pushInstruction("unreachable");
289
+ }
290
+
291
+ // Close try_table
292
+ this.pushInstruction("end");
293
+
294
+ // If execution reached end of try_table without return, jump to runDefers
295
+ this.pushInstruction({
296
+ op: "br",
297
+ depth: this.resolveBranchDepthToRole("runDefers"),
298
+ });
299
+
300
+ // Close catchHandler
301
+ this.pushInstruction("end"); // stack has exnref!
302
+
303
+ // Catch handler body:
304
+ this.pushInstruction({ op: "local.set", index: this.panicExnLocalIndex });
305
+ this.pushInstruction({ op: "i32.const", value: 1 });
306
+ this.pushInstruction({ op: "local.set", index: this.hasPanicLocalIndex });
307
+
308
+ // Close runDefers block
309
+ this.pushInstruction("end");
310
+
311
+ // Run defers loop
312
+ this.emitRunDefers();
313
+
314
+ // Check if unrecovered panic
315
+ this.emitCheckUnrecoveredPanic();
316
+
317
+ // Return values
318
+ if (this.namedReturnVars?.length > 0) {
319
+ for (const nr of this.namedReturnVars) {
320
+ const loc = this.locals.get(nr.name);
321
+ if (loc.isBoxed) {
322
+ this.pushInstruction({ op: "local.get", index: loc.index });
323
+ this.pushInstruction({
324
+ op: "struct.get",
325
+ typeIndex: loc.boxInfo.typeIndex,
326
+ fieldIndex: 0,
327
+ });
328
+ } else {
329
+ this.pushInstruction({ op: "local.get", index: loc.index });
330
+ }
331
+ }
332
+ this.pushInstruction("return");
333
+ } else if (this.returnTempLocals?.length > 0) {
334
+ for (const retLoc of this.returnTempLocals) {
335
+ this.pushInstruction({ op: "local.get", index: retLoc });
336
+ }
337
+ this.pushInstruction("return");
338
+ } else {
339
+ this.pushInstruction("return");
340
+ }
341
+ }
342
+
343
+ emitRunDefers() {
344
+ const deferNodeTypeIndex = this.mod.getDeferNodeTypeIndex();
345
+ const deferClosureSig = { kind: "Signature", params: [], results: [] };
346
+ const deferClosureInfo = this.mod.getClosureType(deferClosureSig);
347
+
348
+ this.pushInstruction({ op: "loop", blockType: "void" });
349
+ this.pushControl("deferLoop");
350
+ this.pushInstruction({ op: "block", blockType: "void" });
351
+ this.pushControl("exitDefers");
352
+
353
+ // If defers is null -> break out of loop
354
+ this.pushInstruction({ op: "local.get", index: this.defersLocalIndex });
355
+ this.pushInstruction({ op: "ref.is_null" });
356
+ this.pushInstruction({ op: "br_if", depth: 0 }); // break to exitDefers
357
+
358
+ // curClosure = defers.fn (field 0)
359
+ this.pushInstruction({ op: "local.get", index: this.defersLocalIndex });
360
+ this.pushInstruction({
361
+ op: "struct.get",
362
+ typeIndex: deferNodeTypeIndex,
363
+ fieldIndex: 0,
364
+ });
365
+ this.pushInstruction({ op: "local.set", index: this.curClosureLocalIndex });
366
+
367
+ // defers = defers.next (field 1)
368
+ this.pushInstruction({ op: "local.get", index: this.defersLocalIndex });
369
+ this.pushInstruction({
370
+ op: "struct.get",
371
+ typeIndex: deferNodeTypeIndex,
372
+ fieldIndex: 1,
373
+ });
374
+ this.pushInstruction({ op: "local.set", index: this.defersLocalIndex });
375
+
376
+ // Call curClosure: curClosure.fn(curClosure.env) with recover armed.
377
+ // The call is wrapped in its own try_table: a panic raised by a deferred
378
+ // function replaces the in-flight panic (Go spec) and the remaining
379
+ // defers still run.
380
+ const armedGlobal = this.mod.getDeferArmedGlobalIndex();
381
+ const panicNodeTypeIndex = this.mod.getPanicNodeTypeIndex();
382
+ const panicGlobal = this.mod.getPanicGlobalIndex();
383
+
384
+ this.pushInstruction({ op: "block", blockType: "exnref" });
385
+ this.pushControl("deferCatch");
386
+ this.pushInstruction({
387
+ op: "try_table",
388
+ blockType: "void",
389
+ catches: [{ kind: "catch_all_ref", label: 0 }],
390
+ });
391
+ this.pushControl("deferTry");
392
+
393
+ this.pushInstruction({ op: "i32.const", value: 1 });
394
+ this.pushInstruction({ op: "global.set", index: armedGlobal });
395
+ this.pushInstruction({ op: "local.get", index: this.curClosureLocalIndex });
396
+ this.pushInstruction({
397
+ op: "struct.get",
398
+ typeIndex: deferClosureInfo.typeIndex,
399
+ fieldIndex: 1,
400
+ });
401
+ this.pushInstruction({ op: "local.get", index: this.curClosureLocalIndex });
402
+ this.pushInstruction({
403
+ op: "struct.get",
404
+ typeIndex: deferClosureInfo.typeIndex,
405
+ fieldIndex: 0,
406
+ });
407
+ this.pushInstruction({
408
+ op: "call_ref",
409
+ typeIndex: deferClosureInfo.funcTypeIndex,
410
+ });
411
+ // Clear in case the deferred function had no prologue (e.g. a
412
+ // trampoline) and never consumed the flag.
413
+ this.pushInstruction({ op: "i32.const", value: 0 });
414
+ this.pushInstruction({ op: "global.set", index: armedGlobal });
415
+ this.pushInstruction("end"); // deferTry
416
+ // Normal completion: next defer.
417
+ this.pushInstruction({
418
+ op: "br",
419
+ depth: this.resolveBranchDepthToRole("deferLoop"),
420
+ });
421
+ this.pushInstruction("end"); // deferCatch — stack: exnref
422
+
423
+ // Panic inside the deferred function.
424
+ this.pushInstruction({ op: "local.set", index: this.panicExnLocalIndex });
425
+ this.pushInstruction({ op: "i32.const", value: 0 });
426
+ this.pushInstruction({ op: "global.set", index: armedGlobal });
427
+ // If this frame was already panicking, the older panic is aborted:
428
+ // unlink it from under the new top node (__panic.prev = __panic.prev.prev).
429
+ this.pushInstruction({ op: "local.get", index: this.hasPanicLocalIndex });
430
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
431
+ this.pushInstruction({ op: "ref.is_null" });
432
+ this.pushInstruction({ op: "i32.eqz" });
433
+ this.pushInstruction({ op: "i32.and" });
434
+ this.pushInstruction({ op: "if", blockType: "void" });
435
+ this.pushControl("ifNestedPanic");
436
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
437
+ this.pushInstruction({
438
+ op: "struct.get",
439
+ typeIndex: panicNodeTypeIndex,
440
+ fieldIndex: 2, // prev
441
+ });
442
+ this.pushInstruction({ op: "ref.is_null" });
443
+ this.pushInstruction({ op: "i32.eqz" });
444
+ this.pushInstruction({ op: "if", blockType: "void" });
445
+ this.pushControl("ifHasPrev");
446
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
447
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
448
+ this.pushInstruction({
449
+ op: "struct.get",
450
+ typeIndex: panicNodeTypeIndex,
451
+ fieldIndex: 2,
452
+ });
453
+ this.pushInstruction({
454
+ op: "struct.get",
455
+ typeIndex: panicNodeTypeIndex,
456
+ fieldIndex: 2,
457
+ });
458
+ this.pushInstruction({
459
+ op: "struct.set",
460
+ typeIndex: panicNodeTypeIndex,
461
+ fieldIndex: 2,
462
+ });
463
+ this.pushInstruction("end"); // ifHasPrev
464
+ this.pushInstruction("end"); // ifNestedPanic
465
+ this.pushInstruction({ op: "i32.const", value: 1 });
466
+ this.pushInstruction({ op: "local.set", index: this.hasPanicLocalIndex });
467
+
468
+ // Continue loop (br 1 = deferLoop)
469
+ this.pushInstruction({ op: "br", depth: 1 });
470
+
471
+ this.pushInstruction("end");
472
+ this.pushInstruction("end");
473
+ }
474
+
475
+ emitCheckUnrecoveredPanic() {
476
+ const panicNodeTypeIndex = this.mod.getPanicNodeTypeIndex();
477
+ const panicGlobal = this.mod.getPanicGlobalIndex();
478
+
479
+ // if (hasPanic)
480
+ this.pushInstruction({ op: "local.get", index: this.hasPanicLocalIndex });
481
+ this.pushInstruction({ op: "if", blockType: "void" });
482
+ this.pushControl("ifHasPanic");
483
+
484
+ // if (__panic != null)
485
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
486
+ this.pushInstruction({ op: "ref.is_null" });
487
+ this.pushInstruction({ op: "i32.eqz" });
488
+ this.pushInstruction({ op: "if", blockType: "void" });
489
+ this.pushControl("ifHasNode");
490
+
491
+ // if (__panic.recovered == 1)
492
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
493
+ this.pushInstruction({
494
+ op: "struct.get",
495
+ typeIndex: panicNodeTypeIndex,
496
+ fieldIndex: 1, // recovered
497
+ });
498
+ this.pushInstruction({ op: "i32.const", value: 1 });
499
+ this.pushInstruction({ op: "i32.eq" });
500
+ this.pushInstruction({ op: "if", blockType: "void" });
501
+ this.pushControl("ifRecovered");
502
+
503
+ // Pop recovered node: __panic = __panic.prev
504
+ this.pushInstruction({ op: "global.get", index: panicGlobal });
505
+ this.pushInstruction({
506
+ op: "struct.get",
507
+ typeIndex: panicNodeTypeIndex,
508
+ fieldIndex: 2, // prev
509
+ });
510
+ this.pushInstruction({ op: "global.set", index: panicGlobal });
511
+
512
+ this.pushInstruction("else");
513
+
514
+ // Still unrecovered! Rethrow!
515
+ this.pushInstruction({ op: "local.get", index: this.panicExnLocalIndex });
516
+ this.pushInstruction({ op: "throw_ref" });
517
+
518
+ this.pushInstruction("end"); // ifRecovered
519
+
520
+ this.pushInstruction("else");
521
+
522
+ // __panic is null but hasPanic was 1 (e.g. unhandled host exception)
523
+ this.pushInstruction({ op: "local.get", index: this.panicExnLocalIndex });
524
+ this.pushInstruction({ op: "throw_ref" });
525
+
526
+ this.pushInstruction("end"); // ifHasNode
527
+
528
+ this.pushInstruction("end"); // ifHasPanic
529
+ }
530
+
531
+ emitDeferStmt(stmt) {
532
+ const funcLit = stmt.call.func;
533
+ this.emitFuncLit(funcLit);
534
+ // Stack has closure struct: (ref $deferClosure)
535
+ this.pushInstruction({ op: "local.get", index: this.defersLocalIndex });
536
+ this.pushInstruction({
537
+ op: "struct.new",
538
+ typeIndex: this.mod.getDeferNodeTypeIndex(),
539
+ });
540
+ this.pushInstruction({ op: "local.set", index: this.defersLocalIndex });
541
+ }
542
+
543
+ _cacheScratchGlobals() {
544
+ if (this.funcDecl._isTrampoline) return;
545
+ const byFunc = this.mod.lowerResult?.cachedGlobalsByFunc;
546
+ const names =
547
+ byFunc?.get(this.rootFuncDecl) ??
548
+ byFunc?.get(this.rootFuncDecl._sourceDecl) ??
549
+ [];
550
+ // The list is per root function and covers nested closures; each body
551
+ // only pays for the globals it reads itself.
552
+ for (const name of names) {
553
+ if (!this._bodyReadsIdent(this.funcDecl.body, name)) continue;
554
+ const globalInfo = this.mod.resolveGlobal(name);
555
+ if (!globalInfo) continue;
556
+
557
+ const localIdx = this.allocLocal(
558
+ `__cache$${name}`,
559
+ globalInfo.type,
560
+ globalInfo.goType,
561
+ true,
562
+ );
563
+ this.cachedGlobals.set(name, { index: localIdx, globalInfo });
564
+ this.pushInstruction({ op: "global.get", index: globalInfo.index });
565
+ this.pushInstruction({ op: "local.set", index: localIdx });
566
+ }
567
+ }
568
+
569
+ // Like _findIdentInAST but stops at FuncLit boundaries (closures are emitted
570
+ // as separate functions with their own entry).
571
+ _bodyReadsIdent(node, name) {
572
+ if (!node || typeof node !== "object") return false;
573
+ if (Array.isArray(node)) {
574
+ return node.some((item) => this._bodyReadsIdent(item, name));
575
+ }
576
+ if (node.kind === "FuncLit") return false;
577
+ if (node.kind === "Ident") return node.name === name;
578
+ for (const key of Object.keys(node)) {
579
+ if (key.startsWith("_")) continue;
580
+ if (this._bodyReadsIdent(node[key], name)) return true;
581
+ }
582
+ return false;
583
+ }
584
+
585
+ _needsOobBlock(node) {
586
+ if (!node || typeof node !== "object") return false;
587
+ if (Array.isArray(node)) {
588
+ return node.some((item) => this._needsOobBlock(item));
589
+ }
590
+ if (node.kind === "FuncLit") return false;
591
+ if (node.kind === "IndexExpr") {
592
+ const baseGoType = node.expr?._type ?? this._resolveExprGoType(node.expr);
593
+ if (
594
+ baseGoType &&
595
+ (baseGoType.kind === "map" || baseGoType.name === "map")
596
+ ) {
597
+ return false;
598
+ }
599
+ return true;
600
+ }
601
+ for (const key of Object.keys(node)) {
602
+ if (key.startsWith("_")) continue;
603
+ if (this._needsOobBlock(node[key])) return true;
604
+ }
605
+ return false;
606
+ }
607
+
608
+ _findCapturedVarGoType(name) {
609
+ for (const p of this.rootFuncDecl.params ?? []) {
610
+ if (p.name === name) return p.type;
611
+ }
612
+ if (this.rootFuncDecl.recvName === name) {
613
+ return this.rootFuncDecl.recvType;
614
+ }
615
+ for (const r of this.rootFuncDecl.returnType?._namedReturns ?? []) {
616
+ if (r.name === name) return r.type;
617
+ }
618
+ const found = this._findGoTypeInAST(this.rootFuncDecl.body, name);
619
+ if (found) return found;
620
+ const global = this.mod.resolveGlobal(name);
621
+ if (global?.goType) return global.goType;
622
+ return { kind: "basic", name: "int" };
623
+ }
624
+
625
+ _findGoTypeInAST(node, varName) {
626
+ if (!node || typeof node !== "object") return null;
627
+ if (Array.isArray(node)) {
628
+ for (const item of node) {
629
+ const t = this._findGoTypeInAST(item, varName);
630
+ if (t) return t;
631
+ }
632
+ return null;
633
+ }
634
+ if (node.kind === "VarDecl" || node.kind === "ConstDecl") {
635
+ for (const spec of node.decls ?? node.specs ?? [node]) {
636
+ const names = spec.names ?? (spec.name ? [spec.name] : []);
637
+ const idx = names.indexOf(varName);
638
+ if (idx !== -1) {
639
+ if (spec.type) return spec.type;
640
+ if (spec.value?.[idx]?._type) return spec.value[idx]._type;
641
+ if (spec.init?.[idx]?._type) return spec.init[idx]._type;
642
+ }
643
+ }
644
+ }
645
+ if (node.kind === "DefineStmt") {
646
+ const lhs = node.lhs ?? [];
647
+ for (let i = 0; i < lhs.length; i++) {
648
+ if (lhs[i].name === varName) {
649
+ if (lhs[i]._type) return lhs[i]._type;
650
+ if (node.rhs?.[i]?._type) return node.rhs[i]._type;
651
+ }
652
+ }
653
+ }
654
+ if (node.kind === "Ident" && node.name === varName && node._type) {
655
+ return node._type;
656
+ }
657
+ for (const key of Object.keys(node)) {
658
+ if (key.startsWith("_")) continue;
659
+ const t = this._findGoTypeInAST(node[key], varName);
660
+ if (t) return t;
661
+ }
662
+ return null;
663
+ }
664
+
665
+ _unpackClosureEnv() {
666
+ const envFields = [];
667
+ for (const name of this.capturedNames) {
668
+ const isBoxed = this.mutatedCaptures.has(name);
669
+ const goType = this._findCapturedVarGoType(name);
670
+ if (isBoxed) {
671
+ const boxInfo = this.mod.getBoxType(goType);
672
+ envFields.push({
673
+ name,
674
+ goType,
675
+ isBoxed: true,
676
+ boxInfo,
677
+ wType: {
678
+ kind: "ref",
679
+ nullable: true,
680
+ typeIndex: boxInfo.typeIndex,
681
+ },
682
+ });
683
+ } else {
684
+ const wType = this.toWasmType(goType);
685
+ envFields.push({
686
+ name,
687
+ goType,
688
+ isBoxed: false,
689
+ boxInfo: null,
690
+ wType,
691
+ });
692
+ }
693
+ }
694
+
695
+ const envInfo = this.mod.getEnvType(envFields);
696
+ this.envInfo = envInfo;
697
+ this.envFields = envFields;
698
+
699
+ const envCastTmp = this.allocLocal(
700
+ null,
701
+ {
702
+ kind: "ref",
703
+ nullable: true,
704
+ typeIndex: envInfo.typeIndex,
705
+ },
706
+ null,
707
+ true,
708
+ );
709
+
710
+ this.pushInstruction({ op: "local.get", index: 0 }); // __env
711
+ this.pushInstruction({
712
+ op: "ref.cast_null",
713
+ typeIndex: envInfo.typeIndex,
714
+ });
715
+ this.pushInstruction({ op: "local.set", index: envCastTmp });
716
+
717
+ for (let i = 0; i < envFields.length; i++) {
718
+ const f = envFields[i];
719
+ const locIdx = this.allocLocal(f.name, f.wType, f.goType, true);
720
+ this.pushInstruction({ op: "local.get", index: envCastTmp });
721
+ this.pushInstruction({
722
+ op: "struct.get",
723
+ typeIndex: envInfo.typeIndex,
724
+ fieldIndex: i,
725
+ });
726
+ this.pushInstruction({ op: "local.set", index: locIdx });
727
+
728
+ this.locals.set(f.name, {
729
+ index: locIdx,
730
+ type: f.wType,
731
+ goType: f.goType,
732
+ isParam: false,
733
+ isBoxed: f.isBoxed,
734
+ boxInfo: f.boxInfo,
735
+ });
736
+ }
737
+ }
738
+
739
+ _boxMutatedParams() {
740
+ for (const param of this.funcDecl.params ?? []) {
741
+ if (
742
+ param.name &&
743
+ param.name !== "_" &&
744
+ param.name !== "__env" &&
745
+ this.mutatedCaptures.has(param.name)
746
+ ) {
747
+ const pInfo = this.locals.get(param.name);
748
+ if (!pInfo || pInfo.isBoxed) continue;
749
+ const boxInfo = this.mod.getBoxType(pInfo.goType);
750
+ const boxWType = {
751
+ kind: "ref",
752
+ nullable: true,
753
+ typeIndex: boxInfo.typeIndex,
754
+ };
755
+ const boxLocalIdx = this.allocLocal(null, boxWType, pInfo.goType, true);
756
+
757
+ this.pushInstruction({ op: "local.get", index: pInfo.index });
758
+ this.pushInstruction({
759
+ op: "struct.new",
760
+ typeIndex: boxInfo.typeIndex,
761
+ });
762
+ this.pushInstruction({ op: "local.set", index: boxLocalIdx });
763
+
764
+ this.locals.set(param.name, {
765
+ index: boxLocalIdx,
766
+ type: boxWType,
767
+ goType: pInfo.goType,
768
+ isParam: false,
769
+ isBoxed: true,
770
+ boxInfo,
771
+ });
772
+ }
773
+ }
774
+ }
775
+
776
+ toWasmType(t) {
777
+ return toWasmType(t, this.mod.checker, this.mod);
778
+ }
779
+
780
+ _initParams() {
781
+ for (const param of this.funcDecl.params ?? []) {
782
+ const wType = this.toWasmType(param.type);
783
+ const idx = this.locals.size;
784
+ this.locals.set(param.name, {
785
+ index: idx,
786
+ type: wType,
787
+ goType: param.type,
788
+ isParam: true,
789
+ isBoxed: false,
790
+ });
791
+ }
792
+ }
793
+
794
+ allocLocal(name, wasmType, goType = null, forceRaw = false) {
795
+ const isMutatedCapture =
796
+ !forceRaw &&
797
+ Boolean(name && name !== "_" && this.mutatedCaptures.has(name));
798
+ let finalWasmType = wasmType;
799
+ let boxInfo = null;
800
+
801
+ if (isMutatedCapture) {
802
+ boxInfo = this.mod.getBoxType(goType ?? { kind: "basic", name: "int" });
803
+ finalWasmType = {
804
+ kind: "ref",
805
+ nullable: true,
806
+ typeIndex: boxInfo.typeIndex,
807
+ };
808
+ }
809
+
810
+ const idx = (this.funcDecl.params?.length ?? 0) + this.localTypes.length;
811
+ this.localTypes.push(finalWasmType);
812
+ if (name && name !== "_") {
813
+ this.locals.set(name, {
814
+ index: idx,
815
+ type: finalWasmType,
816
+ goType,
817
+ isParam: false,
818
+ isBoxed: isMutatedCapture,
819
+ boxInfo,
820
+ });
821
+ }
822
+ return idx;
823
+ }
824
+
825
+ // `pkg.Name` where `pkg` is a linked local package -> `Name` (the module is
826
+ // flat, like the JS bundle). Returns a replacement Ident or null.
827
+ _dequalify(expr) {
828
+ if (
829
+ expr?.kind === "SelectorExpr" &&
830
+ expr.expr?.kind === "Ident" &&
831
+ this.mod.bundledPackages?.has(expr.expr.name) &&
832
+ !this.resolveLocal(expr.expr.name) &&
833
+ !this.mod.resolveGlobal(expr.expr.name)
834
+ ) {
835
+ return {
836
+ kind: "Ident",
837
+ name: expr.field,
838
+ _type: expr._type,
839
+ _line: expr._line,
840
+ _col: expr._col,
841
+ };
842
+ }
843
+ return null;
844
+ }
845
+
846
+ _resolveStructInfo(expr) {
847
+ if (!expr) return null;
848
+ let t = expr._type;
849
+ if (!t && expr.kind === "Ident") {
850
+ const local = this.resolveLocal(expr.name);
851
+ if (local) t = local.goType;
852
+ if (!t && this.cachedGlobals.has(expr.name)) {
853
+ t = this.cachedGlobals.get(expr.name).globalInfo.goType;
854
+ }
855
+ if (!t) {
856
+ const global = this.mod.resolveGlobal(expr.name);
857
+ if (global) t = global.goType;
858
+ }
859
+ }
860
+ if (!t) return null;
861
+ while (
862
+ t.kind === "pointer" ||
863
+ t.kind === "PointerType" ||
864
+ t.kind === "StarExpr"
865
+ ) {
866
+ t = t.base ?? t.expr ?? t.operand;
867
+ }
868
+ const name = t.name ?? (t.kind === "named" ? t.name : null);
869
+ if (name && this.mod.structTypes.has(name)) {
870
+ return this.mod.structTypes.get(name);
871
+ }
872
+ if (t.kind === "TypeName" || t.kind === "Ident") {
873
+ if (this.mod.structTypes.has(t.name))
874
+ return this.mod.structTypes.get(t.name);
875
+ }
876
+ return null;
877
+ }
878
+
879
+ _resolveFieldPath(structInfo, fieldName) {
880
+ if (!structInfo) return null;
881
+ if (structInfo.fieldIndexMap.has(fieldName)) {
882
+ const idx = structInfo.fieldIndexMap.get(fieldName);
883
+ return [{ structInfo, fieldIndex: idx, field: structInfo.fields[idx] }];
884
+ }
885
+ for (const embed of structInfo.embeds) {
886
+ const embedInfo = this.mod.structTypes.get(embed.name);
887
+ if (embedInfo) {
888
+ const sub = this._resolveFieldPath(embedInfo, fieldName);
889
+ if (sub) {
890
+ return [
891
+ {
892
+ structInfo,
893
+ fieldIndex: embed.fieldIndex,
894
+ field: structInfo.fields[embed.fieldIndex],
895
+ },
896
+ ...sub,
897
+ ];
898
+ }
899
+ }
900
+ }
901
+ return null;
902
+ }
903
+
904
+ emitCloneStruct(structInfo, baseWType) {
905
+ const tmp = this.acquireTemp(baseWType);
906
+ this.pushInstruction({ op: "local.set", index: tmp });
907
+ for (let i = 0; i < structInfo.fields.length; i++) {
908
+ this.pushInstruction({ op: "local.get", index: tmp });
909
+ this.pushInstruction({
910
+ op: "struct.get",
911
+ typeIndex: structInfo.typeIndex,
912
+ fieldIndex: i,
913
+ });
914
+ }
915
+ this.pushInstruction({
916
+ op: "struct.new",
917
+ typeIndex: structInfo.typeIndex,
918
+ });
919
+ this.releaseTemp(tmp, baseWType);
920
+ }
921
+
922
+ // Go copies structs on assignment. After `node` has been emitted (value on
923
+ // the stack), clone it unless it is already a fresh temporary
924
+ // (composite literal or `*p` deref) or not a value struct at all.
925
+ _emitCopyIfValueStruct(node, goType, wType) {
926
+ if (
927
+ !isStructType(goType, this.mod.checker, this.mod) ||
928
+ isPointerToStruct(goType, this.mod.checker, this.mod)
929
+ )
930
+ return;
931
+ if (
932
+ node.kind === "CompositeLit" ||
933
+ (node.kind === "UnaryExpr" && node.op === "*")
934
+ )
935
+ return;
936
+ const sInfo =
937
+ this._resolveStructInfo(node) ?? this.mod.getStructType(goType?.name);
938
+ if (sInfo) this.emitCloneStruct(sInfo, wType);
939
+ }
940
+
941
+ // Unpacks slice header fields (0 arr, 1 off, 2 len, 3 cap) from the slice
942
+ // held in `sliceTmp` into the given temps; fields without a temp are skipped.
943
+ _emitSliceUnpack(sliceTmp, sliceInfo, { arr, off, len, cap }) {
944
+ const parts = [arr, off, len, cap];
945
+ for (let fieldIndex = 0; fieldIndex < parts.length; fieldIndex++) {
946
+ const tmp = parts[fieldIndex];
947
+ if (tmp === undefined || tmp === null) continue;
948
+ this.pushInstruction({ op: "local.get", index: sliceTmp });
949
+ this.pushInstruction({
950
+ op: "struct.get",
951
+ typeIndex: sliceInfo.typeIndex,
952
+ fieldIndex,
953
+ });
954
+ this.pushInstruction({ op: "local.set", index: tmp });
955
+ }
956
+ }
957
+
958
+ _getReceiverTypeName(recvType, node = null) {
959
+ if (recvType) {
960
+ let t = recvType;
961
+ while (t.kind === "pointer") t = t.base;
962
+ if (t.name) return t.name;
963
+ if (t.kind === "named") return t.name;
964
+ if (t.kind === "TypeName" || t.kind === "Ident") return t.name;
965
+ }
966
+ if (node && node.kind === "Ident") {
967
+ const loc = this.resolveLocal(node.name);
968
+ if (loc?.goType) {
969
+ return this._getReceiverTypeName(loc.goType);
970
+ }
971
+ }
972
+ return null;
973
+ }
974
+
975
+ emitInterfaceDispatcher(fn) {
976
+ let candidates = this.mod.findInterfaceCandidates(
977
+ fn._ifaceType,
978
+ fn._methodName,
979
+ );
980
+ const recvLocal = 0; // __recv is local 0
981
+ // Soft dispatchers (errors.Unwrap) return nil instead of panicking when
982
+ // the receiver is nil or has no such method.
983
+ const soft = Boolean(fn._softDispatch);
984
+ if (soft) {
985
+ // Only methods with the dispatcher's own result shape are callable
986
+ // (e.g. `Unwrap() []error` is not an `Unwrap() error`).
987
+ const want = this.mod.funcReturnTypesByIdx.get(fn._globalFuncIndex);
988
+ const key = (ts) =>
989
+ ts
990
+ .map((t) => (typeof t === "object" ? JSON.stringify(t) : t))
991
+ .join(",");
992
+ candidates = candidates.filter((c) => {
993
+ const got = this.mod.funcReturnTypesByIdx.get(c.funcIndex);
994
+ return (
995
+ got !== undefined && want !== undefined && key(got) === key(want)
996
+ );
997
+ });
998
+ }
999
+ const emitMiss = (msg) => {
1000
+ if (soft) {
1001
+ this.pushInstruction({ op: "ref.null", heapType: "any" });
1002
+ this.pushInstruction("return");
1003
+ } else {
1004
+ this.emitPanic(msg);
1005
+ }
1006
+ };
1007
+
1008
+ // 1. Nil check
1009
+ this.pushInstruction({ op: "local.get", index: recvLocal });
1010
+ this.pushInstruction("ref.is_null");
1011
+ this.pushInstruction({ op: "if", blockType: "void" });
1012
+ emitMiss(
1013
+ "runtime error: invalid memory address or nil pointer dereference",
1014
+ );
1015
+ this.pushInstruction("end");
1016
+
1017
+ // 2. Iterate candidates
1018
+ for (const cand of candidates) {
1019
+ this.pushInstruction({ op: "block", blockType: "void" });
1020
+ this.pushInstruction({ op: "local.get", index: recvLocal });
1021
+ this.pushInstruction({ op: "ref.test", typeIndex: cand.testTypeIndex });
1022
+ this.pushInstruction("i32.eqz");
1023
+ this.pushInstruction({ op: "br_if", depth: 0 });
1024
+
1025
+ // Matched! Cast receiver
1026
+ this.pushInstruction({ op: "local.get", index: recvLocal });
1027
+ this.pushInstruction({
1028
+ op: "ref.cast_null",
1029
+ typeIndex: cand.testTypeIndex,
1030
+ });
1031
+
1032
+ if (cand.isBoxedValue) {
1033
+ this.pushInstruction({
1034
+ op: "struct.get",
1035
+ typeIndex: cand.testTypeIndex,
1036
+ fieldIndex: 0,
1037
+ });
1038
+ if (cand.embedPath) {
1039
+ for (const step of cand.embedPath) {
1040
+ this.pushInstruction({
1041
+ op: "struct.get",
1042
+ typeIndex: step.parentTypeIndex,
1043
+ fieldIndex: step.fieldIndex,
1044
+ });
1045
+ }
1046
+ }
1047
+ } else {
1048
+ if (cand.embedPath) {
1049
+ for (const step of cand.embedPath) {
1050
+ this.pushInstruction({
1051
+ op: "struct.get",
1052
+ typeIndex: step.parentTypeIndex,
1053
+ fieldIndex: step.fieldIndex,
1054
+ });
1055
+ }
1056
+ }
1057
+ if (cand.needsValueDeref && cand.structInfo) {
1058
+ this.emitCloneStruct(cand.structInfo, cand.targetRecvWType);
1059
+ }
1060
+ }
1061
+
1062
+ // Push method arguments: __arg0 (local 1), __arg1 (local 2), ...
1063
+ for (let p = 1; p < fn.params.length; p++) {
1064
+ this.pushInstruction({ op: "local.get", index: p });
1065
+ }
1066
+
1067
+ this.pushInstruction({ op: "call", funcIndex: cand.funcIndex });
1068
+ this.pushInstruction("return");
1069
+ this.pushInstruction("end");
1070
+ }
1071
+
1072
+ emitMiss("interface conversion: nil or unmatched type for method call");
1073
+ }
1074
+
1075
+ _getArraySize(t, defaultSize = 0) {
1076
+ if (!t) return defaultSize;
1077
+ if (typeof t.size === "number") return t.size;
1078
+ if (t.size?.value !== undefined) return Number(t.size.value);
1079
+ if (typeof t.len === "number") return t.len;
1080
+ if (t.len?.value !== undefined) return Number(t.len.value);
1081
+ if (t.underlying) return this._getArraySize(t.underlying, defaultSize);
1082
+ return defaultSize;
1083
+ }
1084
+
1085
+ emitZeroValue(goType, wType) {
1086
+ if (wType === "i32") {
1087
+ this.pushInstruction({ op: "i32.const", value: 0 });
1088
+ } else if (wType === "i64") {
1089
+ this.pushInstruction({ op: "i64.const", value: 0n });
1090
+ } else if (wType === "f32") {
1091
+ this.pushInstruction({ op: "f32.const", value: 0.0 });
1092
+ } else if (wType === "f64") {
1093
+ this.pushInstruction({ op: "f64.const", value: 0.0 });
1094
+ } else if (
1095
+ typeof wType === "object" &&
1096
+ wType !== null &&
1097
+ wType.kind === "ref"
1098
+ ) {
1099
+ if (
1100
+ goType &&
1101
+ goType.kind !== "pointer" &&
1102
+ isStructType(goType, this.mod.checker)
1103
+ ) {
1104
+ const sName =
1105
+ goType.name ?? (goType.kind === "named" ? goType.name : null);
1106
+ const structInfo = this.mod.getStructType(sName);
1107
+ if (structInfo) {
1108
+ for (const f of structInfo.fields) {
1109
+ this.emitZeroValue(f.goType, f.wType);
1110
+ }
1111
+ this.pushInstruction({
1112
+ op: "struct.new",
1113
+ typeIndex: structInfo.typeIndex,
1114
+ });
1115
+ return;
1116
+ }
1117
+ }
1118
+ if (
1119
+ goType &&
1120
+ goType.kind !== "pointer" &&
1121
+ isArrayType(goType, this.mod.checker)
1122
+ ) {
1123
+ const elemType = goType.elem;
1124
+ const arrInfo = this.mod.getArrayType(elemType);
1125
+ const size = this._getArraySize(goType);
1126
+ if (isStructType(elemType, this.mod.checker, this.mod)) {
1127
+ for (let i = 0; i < size; i++) {
1128
+ this.emitZeroValue(elemType, arrInfo.elemWType);
1129
+ }
1130
+ this.pushInstruction({
1131
+ op: "array.new_fixed",
1132
+ typeIndex: arrInfo.typeIndex,
1133
+ size,
1134
+ });
1135
+ } else {
1136
+ this.pushInstruction({ op: "i32.const", value: size });
1137
+ this.pushInstruction({
1138
+ op: "array.new_default",
1139
+ typeIndex: arrInfo.typeIndex,
1140
+ });
1141
+ }
1142
+ return;
1143
+ }
1144
+ if (
1145
+ (goType &&
1146
+ goType.kind !== "pointer" &&
1147
+ isSliceType(goType, this.mod.checker)) ||
1148
+ (typeof wType.typeIndex === "number" &&
1149
+ this.mod.getSliceTypeByIndex(wType.typeIndex))
1150
+ ) {
1151
+ const elemType = goType ? this._getSliceElemType(goType) : null;
1152
+ const sliceInfo = elemType
1153
+ ? this.mod.getSliceType(elemType)
1154
+ : this.mod.getSliceTypeByIndex(wType.typeIndex);
1155
+ if (sliceInfo) {
1156
+ this.pushInstruction({
1157
+ op: "global.get",
1158
+ index: sliceInfo.emptyGlobalIndex,
1159
+ });
1160
+ return;
1161
+ }
1162
+ }
1163
+ this.pushInstruction({
1164
+ op: "ref.null",
1165
+ heapType: wType.typeIndex ?? "any",
1166
+ });
1167
+ } else if (
1168
+ wType === "externref" ||
1169
+ isStringType(goType, this.mod?.checker)
1170
+ ) {
1171
+ const strIdx = this.mod.internString("");
1172
+ const funcIdx = this.mod.getStringImportIndex();
1173
+ this.pushInstruction({ op: "i32.const", value: strIdx });
1174
+ this.pushInstruction({ op: "call", funcIndex: funcIdx });
1175
+ } else if (wType === "anyref") {
1176
+ this.pushInstruction({ op: "ref.null", heapType: "any" });
1177
+ } else {
1178
+ this.pushInstruction({ op: "ref.null", heapType: "any" });
1179
+ }
1180
+ }
1181
+
1182
+ allocTemp(wasmType) {
1183
+ return this.acquireTemp(wasmType);
1184
+ }
1185
+
1186
+ // Temps are keyed by a canonical string so ref-typed temps (fresh object
1187
+ // literals at every call site) are actually recycled across sites.
1188
+ _tempKey(wasmType) {
1189
+ if (typeof wasmType === "string") return wasmType;
1190
+ const nn = wasmType.nullable === false ? "!" : "";
1191
+ if (typeof wasmType.typeIndex === "number")
1192
+ return `r${wasmType.typeIndex}${nn}`;
1193
+ return `h${wasmType.heapType ?? "any"}${nn}`;
1194
+ }
1195
+
1196
+ acquireTemp(wasmType) {
1197
+ if (!this.freeTemps) this.freeTemps = new Map();
1198
+ const list = this.freeTemps.get(this._tempKey(wasmType));
1199
+ if (list && list.length > 0) {
1200
+ return list.pop();
1201
+ }
1202
+ return this.allocLocal(null, wasmType);
1203
+ }
1204
+
1205
+ releaseTemp(idx, wasmType) {
1206
+ if (!this.freeTemps) this.freeTemps = new Map();
1207
+ const key = this._tempKey(wasmType);
1208
+ let list = this.freeTemps.get(key);
1209
+ if (!list) {
1210
+ list = [];
1211
+ this.freeTemps.set(key, list);
1212
+ }
1213
+ list.push(idx);
1214
+ }
1215
+
1216
+ resolveLocal(name) {
1217
+ return this.locals.get(name) ?? null;
1218
+ }
1219
+
1220
+ // The control stack mirrors every block/loop/if so `br_if` depths to the
1221
+ // function-level out-of-bounds block stay correct inside nested structures
1222
+ // that never call pushControl (struct equality, string slicing, ...).
1223
+ pushInstruction(inst) {
1224
+ const op = typeof inst === "string" ? inst : inst.op;
1225
+ if (op === "block" || op === "loop" || op === "if" || op === "try_table") {
1226
+ this.controlStack.push({ op, role: null, label: null });
1227
+ } else if (op === "end") {
1228
+ this.controlStack.pop();
1229
+ }
1230
+ this.body.push(typeof inst === "string" ? { op: inst } : inst);
1231
+ }
1232
+
1233
+ // ── Control Stack ──────────────────────────────────────────
1234
+
1235
+ // Tags the innermost open block as a break/continue/oob target.
1236
+ pushControl(role, label = null) {
1237
+ const top = this.controlStack[this.controlStack.length - 1];
1238
+ if (top) {
1239
+ top.role = role;
1240
+ top.label = label;
1241
+ }
1242
+ }
1243
+
1244
+ resolveBranchDepth(targetLabel, isContinue = false) {
1245
+ const targetRole = isContinue ? "continue" : "break";
1246
+ if (!targetLabel) {
1247
+ // Innermost break or continue
1248
+ for (let i = this.controlStack.length - 1; i >= 0; i--) {
1249
+ const ctrl = this.controlStack[i];
1250
+ if (ctrl.role === targetRole) {
1251
+ return this.controlStack.length - 1 - i;
1252
+ }
1253
+ }
1254
+ return 0;
1255
+ }
1256
+
1257
+ for (let i = this.controlStack.length - 1; i >= 0; i--) {
1258
+ const ctrl = this.controlStack[i];
1259
+ if (ctrl.label === targetLabel && ctrl.role === targetRole) {
1260
+ return this.controlStack.length - 1 - i;
1261
+ }
1262
+ }
1263
+ return 0;
1264
+ }
1265
+
1266
+ resolveBranchDepthToOob() {
1267
+ for (let i = this.controlStack.length - 1; i >= 0; i--) {
1268
+ const ctrl = this.controlStack[i];
1269
+ if (ctrl.role === "oob") {
1270
+ return this.controlStack.length - 1 - i;
1271
+ }
1272
+ }
1273
+ return null;
1274
+ }
1275
+
1276
+ resolveBranchDepthToRole(targetRole) {
1277
+ for (let i = this.controlStack.length - 1; i >= 0; i--) {
1278
+ const ctrl = this.controlStack[i];
1279
+ if (ctrl.role === targetRole) {
1280
+ return this.controlStack.length - 1 - i;
1281
+ }
1282
+ }
1283
+ return null;
1284
+ }
1285
+
1286
+ // ── Statements ─────────────────────────────────────────────
1287
+
1288
+ emitBlock(block) {
1289
+ for (const stmt of block.stmts ?? []) {
1290
+ this.emitStmt(stmt);
1291
+ }
1292
+ }
1293
+
1294
+ emitStmt(stmt) {
1295
+ if (!stmt) return;
1296
+ switch (stmt.kind) {
1297
+ case "Block":
1298
+ this.emitBlock(stmt);
1299
+ break;
1300
+
1301
+ case "VarDecl":
1302
+ this.emitVarDecl(stmt);
1303
+ break;
1304
+
1305
+ case "DefineStmt":
1306
+ this.emitDefineStmt(stmt);
1307
+ break;
1308
+
1309
+ case "AssignStmt":
1310
+ this.emitAssignStmt(stmt);
1311
+ break;
1312
+
1313
+ case "IncDecStmt":
1314
+ this.emitIncDecStmt(stmt);
1315
+ break;
1316
+
1317
+ case "IfStmt":
1318
+ this.emitIfStmt(stmt);
1319
+ break;
1320
+
1321
+ case "ForStmt":
1322
+ this.emitForStmt(stmt);
1323
+ break;
1324
+
1325
+ case "ReturnStmt":
1326
+ this.emitReturnStmt(stmt);
1327
+ break;
1328
+
1329
+ case "BranchStmt":
1330
+ this.emitBranchStmt(stmt);
1331
+ break;
1332
+
1333
+ case "DeferStmt":
1334
+ this.emitDeferStmt(stmt);
1335
+ break;
1336
+
1337
+ case "ExprStmt":
1338
+ this.emitExprStmt(stmt);
1339
+ break;
1340
+
1341
+ case "LabeledStmt":
1342
+ if (stmt.body) {
1343
+ if (stmt.body.kind === "ForStmt") {
1344
+ stmt.body.label = stmt.label;
1345
+ }
1346
+ this.emitStmt(stmt.body);
1347
+ }
1348
+ break;
1349
+
1350
+ case "SwitchStmt":
1351
+ this.emitSwitchStmt(stmt);
1352
+ break;
1353
+
1354
+ case "TypeSwitchStmt":
1355
+ this.emitTypeSwitchStmt(stmt);
1356
+ break;
1357
+
1358
+ default:
1359
+ // ignore comments or unsupported stmts
1360
+ break;
1361
+ }
1362
+ }
1363
+ }
1364
+
1365
+ function copyMethods(target, source) {
1366
+ const descriptors = Object.getOwnPropertyDescriptors(source.prototype);
1367
+ delete descriptors.constructor;
1368
+ Object.defineProperties(target.prototype, descriptors);
1369
+ }
1370
+
1371
+ copyMethods(FunctionEmitter, MapsEmitter);
1372
+ copyMethods(FunctionEmitter, StmtsEmitter);
1373
+ copyMethods(FunctionEmitter, ExprsEmitter);
1374
+ copyMethods(FunctionEmitter, BuiltinsEmitter);
1375
+ copyMethods(FunctionEmitter, StdlibEmitter);
1376
+ copyMethods(FunctionEmitter, SharedEmitter);