gofront 1.5.1 → 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 (60) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +50 -19
  4. package/src/backend/js/index.js +100 -55
  5. package/src/backend/js/runtime.js +102 -0
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +96 -16
  8. package/src/backend/js/stdlib/errors.js +2 -2
  9. package/src/backend/js/stdlib/fmt.js +26 -8
  10. package/src/backend/js/stdlib/index.js +3 -0
  11. package/src/backend/js/stdlib/maps.js +1 -1
  12. package/src/backend/js/stdlib/path.js +2 -2
  13. package/src/backend/js/stdlib/shared.js +17 -0
  14. package/src/backend/js/stdlib/slices.js +1 -1
  15. package/src/backend/js/stdlib/sort.js +1 -1
  16. package/src/backend/js/stdlib/strconv.js +34 -9
  17. package/src/backend/js/stdlib/strings.js +4 -1
  18. package/src/backend/js/stdlib/testing.js +2 -2
  19. package/src/backend/js/stdlib/time.js +3 -3
  20. package/src/backend/js/templ.js +1 -1
  21. package/src/backend/wasm/boundary.js +486 -33
  22. package/src/backend/wasm/emit-builtins.js +1103 -0
  23. package/src/backend/wasm/emit-exprs.js +1908 -0
  24. package/src/backend/wasm/emit-maps.js +1863 -0
  25. package/src/backend/wasm/emit-shared.js +519 -0
  26. package/src/backend/wasm/emit-stdlib.js +3006 -0
  27. package/src/backend/wasm/emit-stmts.js +2472 -0
  28. package/src/backend/wasm/emit.js +606 -5009
  29. package/src/backend/wasm/encode.js +212 -64
  30. package/src/backend/wasm/glue.js +19 -5
  31. package/src/backend/wasm/index.js +1426 -49
  32. package/src/backend/wasm/monomorph.js +711 -0
  33. package/src/backend/wasm/optimize.js +227 -0
  34. package/src/backend/wasm/types.js +159 -35
  35. package/src/backend/wasm/wat.js +49 -2
  36. package/src/cli-core.js +275 -72
  37. package/src/compiler.js +144 -21
  38. package/src/index.js +86 -115
  39. package/src/lower/boxing.js +3 -4
  40. package/src/lower/captures.js +10 -17
  41. package/src/lower/escape.js +4 -9
  42. package/src/lower/functions.js +277 -5
  43. package/src/lower/globals.js +7 -15
  44. package/src/lower/index.js +5 -2
  45. package/src/lower/ownership.js +5 -10
  46. package/src/lower/range.js +2 -5
  47. package/src/lower/walk.js +25 -0
  48. package/src/resolver.js +2 -1
  49. package/src/test-runner.js +20 -0
  50. package/src/typechecker/assignability.js +42 -1
  51. package/src/typechecker/expressions.js +142 -2
  52. package/src/typechecker/index.js +27 -1
  53. package/src/typechecker/resolve.js +35 -6
  54. package/src/typechecker/statements.js +0 -7
  55. package/src/typechecker/stdlib/core.js +17 -19
  56. package/src/typechecker/stdlib/extended.js +18 -3
  57. package/src/typechecker/stdlib/shared.js +53 -0
  58. package/src/typechecker/stdlib.js +2 -0
  59. package/src/typechecker/types.js +206 -7
  60. package/src/vendor.js +2 -2
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { collectDeclaredNames } from "./captures.js";
8
8
  import { rootIdentName } from "./ownership.js";
9
+ import { forEachChild } from "./walk.js";
9
10
 
10
11
  /**
11
12
  * Checks whether an expression evaluates to one of the target pointer variables
@@ -15,7 +16,7 @@ import { rootIdentName } from "./ownership.js";
15
16
  * @param {Set<string>} targetNames Set of identifier names being tracked
16
17
  * @returns {boolean}
17
18
  */
18
- export function evaluatesToPointer(expr, targetNames) {
19
+ function evaluatesToPointer(expr, targetNames) {
19
20
  if (!expr || typeof expr !== "object") return false;
20
21
  if (expr.kind === "Ident") {
21
22
  return targetNames.has(expr.name);
@@ -144,10 +145,7 @@ export function paramEscapes(body, paramName) {
144
145
  captured = true;
145
146
  return;
146
147
  }
147
- for (const k of Object.keys(cNode)) {
148
- if (k.startsWith("_")) continue;
149
- checkClosure(cNode[k]);
150
- }
148
+ forEachChild(cNode, checkClosure);
151
149
  };
152
150
  checkClosure(node.body);
153
151
  if (captured) {
@@ -158,10 +156,7 @@ export function paramEscapes(body, paramName) {
158
156
  return;
159
157
  }
160
158
 
161
- for (const key of Object.keys(node)) {
162
- if (key.startsWith("_")) continue;
163
- walk(node[key]);
164
- }
159
+ forEachChild(node, walk);
165
160
  };
166
161
 
167
162
  walk(body);
@@ -1,6 +1,8 @@
1
1
  // src/lower/functions.js
2
2
  // Analyzes function signatures, named returns and defer structures.
3
3
 
4
+ import { forEachChild, someChild } from "./walk.js";
5
+
4
6
  export function extractNamedReturns(decl) {
5
7
  const named = decl?.returnType?._namedReturns;
6
8
  if (!named || named.length === 0) return null;
@@ -19,12 +21,282 @@ export function hasDefer(body) {
19
21
  if (Array.isArray(node)) return node.some(walk);
20
22
  if (node.kind === "DeferStmt") return true;
21
23
  if (node.kind === "FuncLit") return false; // separate function boundary
22
- for (const key of Object.keys(node)) {
23
- if (key.startsWith("_")) continue;
24
- if (walk(node[key])) return true;
25
- }
26
- return false;
24
+ return someChild(node, walk);
27
25
  };
28
26
 
29
27
  return walk(body);
30
28
  }
29
+
30
+ let defargCounter = 0;
31
+
32
+ /**
33
+ * True when `body` contains a direct `recover()` call (not nested inside a
34
+ * FuncLit). Used to decide which functions may legitimately observe an
35
+ * in-flight panic: Go only honours recover() when it is called directly by
36
+ * a deferred function.
37
+ */
38
+ export function hasDirectRecover(body) {
39
+ if (!body) return false;
40
+ if (body._hasDirectRecover != null) return Boolean(body._hasDirectRecover);
41
+ const walk = (node) => {
42
+ if (!node || typeof node !== "object") return false;
43
+ if (Array.isArray(node)) return node.some(walk);
44
+ if (node.kind === "FuncLit") return false;
45
+ if (
46
+ node.kind === "CallExpr" &&
47
+ node.func?.kind === "Ident" &&
48
+ node.func.name === "recover"
49
+ )
50
+ return true;
51
+ return someChild(node, walk);
52
+ };
53
+ const result = walk(body);
54
+ body._hasDirectRecover = result;
55
+ return result;
56
+ }
57
+
58
+ const BUILTIN_FUNC_NAMES = new Set([
59
+ "println",
60
+ "print",
61
+ "panic",
62
+ "recover",
63
+ "make",
64
+ "new",
65
+ "len",
66
+ "cap",
67
+ "append",
68
+ "copy",
69
+ "delete",
70
+ "close",
71
+ "clear",
72
+ "min",
73
+ "max",
74
+ ]);
75
+
76
+ export function normalizeDefers(programOrPrograms) {
77
+ defargCounter = 0;
78
+ const programs = Array.isArray(programOrPrograms)
79
+ ? programOrPrograms
80
+ : [programOrPrograms];
81
+
82
+ const pkgFuncNames = new Set(BUILTIN_FUNC_NAMES);
83
+ // Package-level funcs/methods whose body calls recover() directly. A
84
+ // `defer f()` whose callee is one of these must keep recover armed when the
85
+ // synthesized wrapper forwards the call.
86
+ const recoverFuncs = new Set();
87
+ for (const program of programs) {
88
+ for (const d of program?.decls ?? []) {
89
+ if (d.kind === "FuncDecl") pkgFuncNames.add(d.name);
90
+ if (
91
+ (d.kind === "FuncDecl" || d.kind === "MethodDecl") &&
92
+ hasDirectRecover(d.body)
93
+ )
94
+ recoverFuncs.add(d.name);
95
+ }
96
+ }
97
+ const ctx = { pkgFuncNames, recoverFuncs };
98
+
99
+ for (const program of programs) {
100
+ if (!program?.decls) continue;
101
+ for (const d of program.decls) {
102
+ if (d.body) normalizeBlockDefers(d.body, ctx);
103
+ }
104
+ }
105
+ }
106
+
107
+ function normalizeBlockDefers(node, ctx) {
108
+ if (!node || typeof node !== "object") return;
109
+ if (Array.isArray(node)) {
110
+ for (let i = 0; i < node.length; i++) {
111
+ const item = node[i];
112
+ if (item && item.kind === "DeferStmt") {
113
+ const inserted = normalizeDeferStmt(item, node, i, ctx);
114
+ i += inserted;
115
+ } else {
116
+ normalizeBlockDefers(item, ctx);
117
+ }
118
+ }
119
+ return;
120
+ }
121
+
122
+ if (node.kind === "FuncLit") {
123
+ normalizeBlockDefers(node.body, ctx);
124
+ return;
125
+ }
126
+
127
+ forEachChild(node, (child) => normalizeBlockDefers(child, ctx));
128
+ }
129
+
130
+ // Does the deferred callee possibly call recover() directly? Known package
131
+ // funcs/methods are looked up; unknown callees (closure variables, func
132
+ // values) are assumed to, so recover stays armed through the wrapper.
133
+ function deferCalleeMayRecover(funcExpr, ctx) {
134
+ if (!ctx) return false;
135
+ if (funcExpr.kind === "Ident") {
136
+ if (BUILTIN_FUNC_NAMES.has(funcExpr.name)) return false;
137
+ if (ctx.pkgFuncNames.has(funcExpr.name))
138
+ return ctx.recoverFuncs.has(funcExpr.name);
139
+ return true;
140
+ }
141
+ if (funcExpr.kind === "SelectorExpr") {
142
+ if (funcExpr.expr?._type?.kind === "namespace") return false;
143
+ return ctx.recoverFuncs.has(funcExpr.field);
144
+ }
145
+ if (funcExpr.kind === "FuncLit") return hasDirectRecover(funcExpr.body);
146
+ return true;
147
+ }
148
+
149
+ function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
150
+ if (stmt.call?.kind !== "CallExpr") return 0;
151
+ const call = stmt.call;
152
+ const isZeroArgFuncLit =
153
+ call.func?.kind === "FuncLit" && (!call.args || call.args.length === 0);
154
+ if (isZeroArgFuncLit) {
155
+ // The FuncLit itself is the deferred function: recover() inside it is
156
+ // honoured.
157
+ call.func._isDeferTarget = true;
158
+ normalizeBlockDefers(call.func.body, ctx);
159
+ return 0;
160
+ }
161
+ const pkgFuncNames = ctx?.pkgFuncNames ?? null;
162
+
163
+ const tempStmts = [];
164
+ let funcExpr = call.func;
165
+
166
+ // Hoist method receiver if call.func is a method call (e.g. defer r.Close()).
167
+ // Skipped for pointer-receiver methods on an addressable (non-pointer)
168
+ // operand: Go evaluates `&recv` at defer time, so the deferred call must
169
+ // observe later mutations of the original variable — copying the struct
170
+ // into a temp would break that. The receiver expression itself is then
171
+ // re-evaluated at run time, which matches Go for the common `defer x.M()`
172
+ // / `defer s.f.M()` cases.
173
+ const recvIsAddressableValue =
174
+ call.func?._type?._ptrRecv === true &&
175
+ call.func.expr?._type?.kind !== "pointer";
176
+ if (
177
+ parentList &&
178
+ index >= 0 &&
179
+ call.func?.kind === "SelectorExpr" &&
180
+ call.func.expr?._type?.kind !== "namespace" &&
181
+ !recvIsAddressableValue
182
+ ) {
183
+ const recv = call.func.expr;
184
+ defargCounter++;
185
+ const recvName = `__defrecv$${defargCounter}`;
186
+ const recvIdent = {
187
+ kind: "Ident",
188
+ name: recvName,
189
+ _type: recv._type,
190
+ };
191
+ tempStmts.push({
192
+ kind: "DefineStmt",
193
+ lhs: [{ kind: "Ident", name: recvName, _type: recv._type }],
194
+ rhs: [recv],
195
+ });
196
+ funcExpr = {
197
+ ...call.func,
198
+ expr: recvIdent,
199
+ };
200
+ } else if (
201
+ parentList &&
202
+ index >= 0 &&
203
+ call.func?.kind === "Ident" &&
204
+ pkgFuncNames &&
205
+ !pkgFuncNames.has(call.func.name)
206
+ ) {
207
+ // Hoist local function variable (e.g. fn := f1; defer fn(); fn = f2)
208
+ defargCounter++;
209
+ const fnName = `__deffn$${defargCounter}`;
210
+ const fnIdent = {
211
+ kind: "Ident",
212
+ name: fnName,
213
+ _type: call.func._type,
214
+ };
215
+ tempStmts.push({
216
+ kind: "DefineStmt",
217
+ lhs: [{ kind: "Ident", name: fnName, _type: call.func._type }],
218
+ rhs: [call.func],
219
+ });
220
+ funcExpr = fnIdent;
221
+ }
222
+
223
+ const args = call.args ?? [];
224
+ const tempIdents = [];
225
+
226
+ if (parentList && index >= 0 && args.length > 0) {
227
+ for (let a = 0; a < args.length; a++) {
228
+ const arg = args[a];
229
+ if (arg._type?.kind === "tuple" && Array.isArray(arg._type.types)) {
230
+ const tupleTypes = arg._type.types;
231
+ const tupleLhs = [];
232
+ for (let t = 0; t < tupleTypes.length; t++) {
233
+ defargCounter++;
234
+ const tempName = `__defarg$${defargCounter}`;
235
+ const ident = {
236
+ kind: "Ident",
237
+ name: tempName,
238
+ _type: tupleTypes[t],
239
+ };
240
+ tempIdents.push(ident);
241
+ tupleLhs.push(ident);
242
+ }
243
+ tempStmts.push({
244
+ kind: "DefineStmt",
245
+ lhs: tupleLhs,
246
+ rhs: [arg],
247
+ });
248
+ } else {
249
+ defargCounter++;
250
+ const tempName = `__defarg$${defargCounter}`;
251
+ tempIdents.push({
252
+ kind: "Ident",
253
+ name: tempName,
254
+ _type: arg._type,
255
+ });
256
+ tempStmts.push({
257
+ kind: "DefineStmt",
258
+ lhs: [{ kind: "Ident", name: tempName, _type: arg._type }],
259
+ rhs: [arg],
260
+ });
261
+ }
262
+ }
263
+ }
264
+
265
+ if (parentList && index >= 0 && tempStmts.length > 0) {
266
+ parentList.splice(index, 0, ...tempStmts);
267
+ }
268
+
269
+ const callArgs = tempIdents.length > 0 ? tempIdents : args;
270
+ const innerCall = {
271
+ ...call,
272
+ func: funcExpr,
273
+ args: callArgs,
274
+ _multiForward: tempIdents.length > 0 ? false : call._multiForward,
275
+ };
276
+ const synthFuncLit = {
277
+ kind: "FuncLit",
278
+ params: [],
279
+ returnType: null,
280
+ body: {
281
+ kind: "Block",
282
+ stmts: [{ kind: "ExprStmt", expr: innerCall }],
283
+ list: [{ kind: "ExprStmt", expr: innerCall }],
284
+ _hasDefer: false,
285
+ },
286
+ _type: { kind: "FuncType", params: [], returnType: null },
287
+ _isDeferTarget: true,
288
+ // The wrapper only forwards to the real deferred callee; when that
289
+ // callee may call recover() directly, recover must stay armed across
290
+ // the wrapper frame.
291
+ _deferForward: deferCalleeMayRecover(call.func, ctx),
292
+ };
293
+ if (call.func?.kind === "FuncLit") normalizeBlockDefers(call.func.body, ctx);
294
+ stmt.call = {
295
+ kind: "CallExpr",
296
+ func: synthFuncLit,
297
+ args: [],
298
+ _type: { kind: "basic", name: "void" },
299
+ };
300
+
301
+ return tempStmts.length;
302
+ }
@@ -3,6 +3,7 @@
3
3
  // and computes functions that participate in the package initialization call path.
4
4
 
5
5
  import { isReferenceType } from "./boxing.js";
6
+ import { forEachChild, someChild } from "./walk.js";
6
7
 
7
8
  /**
8
9
  * Computes:
@@ -117,11 +118,9 @@ function scanReassignments(node, packageGlobals, reassigned, insideInit) {
117
118
  }
118
119
  }
119
120
  checkAddressTaken(node, packageGlobals, reassigned);
120
- for (const k of Object.keys(node)) {
121
- if (!k.startsWith("_")) {
122
- scanReassignments(node[k], packageGlobals, reassigned, insideInit);
123
- }
124
- }
121
+ forEachChild(node, (child) =>
122
+ scanReassignments(child, packageGlobals, reassigned, insideInit),
123
+ );
125
124
  }
126
125
 
127
126
  function addCallTarget(call, called) {
@@ -145,9 +144,7 @@ function findCalledFunctions(node, called = new Set()) {
145
144
  return called;
146
145
  }
147
146
  if (node.kind === "CallExpr") addCallTarget(node, called);
148
- for (const k of Object.keys(node)) {
149
- if (!k.startsWith("_")) findCalledFunctions(node[k], called);
150
- }
147
+ forEachChild(node, (child) => findCalledFunctions(child, called));
151
148
  return called;
152
149
  }
153
150
 
@@ -244,9 +241,7 @@ function scanReadIdents(node, readSet) {
244
241
  readSet.add(node.name);
245
242
  return;
246
243
  }
247
- for (const k of Object.keys(node)) {
248
- if (!k.startsWith("_")) scanReadIdents(node[k], readSet);
249
- }
244
+ forEachChild(node, (child) => scanReadIdents(child, readSet));
250
245
  }
251
246
 
252
247
  function isLocallyDeclared(node, name) {
@@ -265,10 +260,7 @@ function isLocallyDeclared(node, name) {
265
260
  return true;
266
261
  }
267
262
  }
268
- for (const k of Object.keys(node)) {
269
- if (!k.startsWith("_") && isLocallyDeclared(node[k], name)) return true;
270
- }
271
- return false;
263
+ return someChild(node, (child) => isLocallyDeclared(child, name));
272
264
  }
273
265
 
274
266
  function getGlobalsToCache(fn, neverAssignedGlobals, initPathFunctions) {
@@ -9,16 +9,17 @@ export * from "./functions.js";
9
9
  export * from "./globals.js";
10
10
  export * from "./ownership.js";
11
11
  export * from "./range.js";
12
+ export * from "./walk.js";
12
13
 
13
14
  import { scanAddressTaken } from "./boxing.js";
14
15
  import { analyzeCaptures } from "./captures.js";
15
16
  import { computeEmbeddedStubs } from "./embedding.js";
16
17
  import { paramEscapes } from "./escape.js";
17
- import { extractNamedReturns, hasDefer } from "./functions.js";
18
+ import { extractNamedReturns, hasDefer, normalizeDefers } from "./functions.js";
18
19
  import { computeGlobalAnalysis } from "./globals.js";
19
20
  import { OwnershipContext } from "./ownership.js";
20
21
 
21
- export class LowerResult {
22
+ class LowerResult {
22
23
  constructor() {
23
24
  // Map<ASTNode (FuncDecl, MethodDecl, FuncLit), Set<string>>
24
25
  this.boxedVars = new Map();
@@ -53,6 +54,8 @@ export function lower(programOrPrograms, checker = null) {
53
54
  ? programOrPrograms
54
55
  : [programOrPrograms];
55
56
 
57
+ normalizeDefers(programs);
58
+
56
59
  // 1. Aggregate methods across all files in the package
57
60
  const methodMap = new Map();
58
61
  for (const program of programs) {
@@ -1,6 +1,8 @@
1
1
  // src/lower/ownership.js
2
2
  // Tracks variable ownership, mutation and clone elision for Go value semantics.
3
3
 
4
+ import { forEachChild, someChild } from "./walk.js";
5
+
4
6
  export function rootIdentName(e) {
5
7
  while (e) {
6
8
  if (e.kind === "Ident") return e.name;
@@ -42,11 +44,7 @@ export function nodeMutatesVar(node, name, addrOnly = false) {
42
44
  )
43
45
  return true;
44
46
  if (!addrOnly && nodeWritesVar(node, name)) return true;
45
- for (const key of Object.keys(node)) {
46
- if (key.startsWith("_")) continue;
47
- if (nodeMutatesVar(node[key], name, addrOnly)) return true;
48
- }
49
- return false;
47
+ return someChild(node, (child) => nodeMutatesVar(child, name, addrOnly));
50
48
  }
51
49
 
52
50
  // Single-pass collector of all mutated or address-taken variables in an AST node.
@@ -85,10 +83,7 @@ export function collectMutatedVars(
85
83
  }
86
84
  }
87
85
 
88
- for (const key of Object.keys(node)) {
89
- if (key.startsWith("_")) continue;
90
- collectMutatedVars(node[key], mutated, addrOnly);
91
- }
86
+ forEachChild(node, (child) => collectMutatedVars(child, mutated, addrOnly));
92
87
  return mutated;
93
88
  }
94
89
 
@@ -112,7 +107,7 @@ export function closureMutates(body, name, cache = null) {
112
107
  if (!node || typeof node !== "object") return false;
113
108
  if (Array.isArray(node)) return node.some(walk);
114
109
  if (node.kind === "FuncLit") return nodeMutatesVar(node.body, name);
115
- return Object.keys(node).some((k) => !k.startsWith("_") && walk(node[k]));
110
+ return someChild(node, walk);
116
111
  };
117
112
  const res = walk(body);
118
113
  if (cache) cache.set(key, res);
@@ -2,6 +2,7 @@
2
2
  // Classifies for-range loop shapes and variable scoping/redelcarations.
3
3
 
4
4
  import { isNumeric } from "../typechecker/types.js";
5
+ import { someChild } from "./walk.js";
5
6
 
6
7
  export function isRangeFor(stmt) {
7
8
  if (!stmt?.init) return false;
@@ -40,11 +41,7 @@ export function nodeAssigns(node, name) {
40
41
  node.expr.name === name)
41
42
  )
42
43
  return true;
43
- for (const key of Object.keys(node)) {
44
- if (key.startsWith("_")) continue;
45
- if (nodeAssigns(node[key], name)) return true;
46
- }
47
- return false;
44
+ return someChild(node, (child) => nodeAssigns(child, name));
48
45
  }
49
46
 
50
47
  // True when the body re-declares one of `names` at its top level (legal Go shadowing).
@@ -0,0 +1,25 @@
1
+ // src/lower/walk.js
2
+ // Shared AST child traversal for the lowering passes. Side-table fields
3
+ // (`_type`, `_boxed`, …) start with "_" and are never AST children.
4
+
5
+ /** True for a side-table key that traversal must skip. */
6
+ function isSideTableKey(key) {
7
+ return key.charCodeAt(0) === 95; // "_"
8
+ }
9
+
10
+ /** Calls `fn(child)` for every non-side-table property of `node`. */
11
+ export function forEachChild(node, fn) {
12
+ for (const key of Object.keys(node)) {
13
+ if (isSideTableKey(key)) continue;
14
+ fn(node[key]);
15
+ }
16
+ }
17
+
18
+ /** True when `pred(child)` holds for some non-side-table property of `node`. */
19
+ export function someChild(node, pred) {
20
+ for (const key of Object.keys(node)) {
21
+ if (isSideTableKey(key)) continue;
22
+ if (pred(node[key])) return true;
23
+ }
24
+ return false;
25
+ }
package/src/resolver.js CHANGED
@@ -49,6 +49,7 @@ const BUILTIN_PACKAGES = new Set([
49
49
  "path",
50
50
  "path/filepath",
51
51
  "testing",
52
+ "gofront/shared",
52
53
  ]);
53
54
 
54
55
  // Returns true for local relative paths (start with ./ or ../)
@@ -72,7 +73,7 @@ export function resolveGwDir(importPath, fromFile) {
72
73
  return gwFiles.length > 0 ? dir : null;
73
74
  }
74
75
 
75
- export function resolveImport(importPath, fromFile) {
76
+ function resolveImport(importPath, fromFile) {
76
77
  const fromDir = dirname(resolve(fromFile));
77
78
  const nodeModules = findNodeModules(fromDir);
78
79
 
@@ -14,6 +14,19 @@ import {
14
14
  gwFilesIn,
15
15
  } from "./compiler.js";
16
16
 
17
+ // WasmGC + exception handling (what the wasm backend emits) ship enabled by
18
+ // default from Node 22 (V8 11.9+). Older runtimes fail at instantiate time
19
+ // with an opaque CompileError, so check up front and explain.
20
+ export const MIN_WASM_NODE_MAJOR = 22;
21
+
22
+ export function checkWasmRuntime(version = process.version) {
23
+ const major = Number.parseInt(version.replace(/^v/, ""), 10);
24
+ if (Number.isNaN(major) || major >= MIN_WASM_NODE_MAJOR) return;
25
+ throw new Error(
26
+ `running wasm packages requires Node ${MIN_WASM_NODE_MAJOR}+ (WasmGC); found ${version}\n hint: upgrade Node, or compile with --js-only to run the JS backend instead`,
27
+ );
28
+ }
29
+
17
30
  export function isTestFunc(decl) {
18
31
  if (decl.kind !== "FuncDecl") return false;
19
32
  if (!decl.name.startsWith("Test")) return false;
@@ -382,6 +395,13 @@ export async function runTests(targetDir, options = {}) {
382
395
 
383
396
  // `wasm` packages run inside the linked app.wasm; `js` packages that import
384
397
  // wasm packages need the linked module too (hybrid packages).
398
+ if (compiled.target === "both" || compiled.wasm) {
399
+ try {
400
+ checkWasmRuntime();
401
+ } catch (err) {
402
+ return reportBuildFailure(err, resolvedDir, options);
403
+ }
404
+ }
385
405
  if (compiled.target === "both") {
386
406
  return runDualTarget(compiled, resolvedDir, runHarness, options);
387
407
  }
@@ -1,6 +1,7 @@
1
1
  // Assignability, binary result types, and interface satisfaction.
2
2
  // Installed as a mixin on TypeChecker.prototype.
3
3
 
4
+ import { sharedInfo } from "./stdlib/shared.js";
4
5
  import {
5
6
  ANY,
6
7
  BOOL,
@@ -159,6 +160,7 @@ export const assignabilityMethods = {
159
160
  target = this.resolveType(target);
160
161
  source = this.resolveType(source);
161
162
  this._markIfaceBox(target, source, node);
163
+ this._checkWasmIfaceProvider(target, source, node);
162
164
  if (this._assertAssignableEarlyReturn(target, source)) return;
163
165
  if (isPointer(target) && isPointer(source)) {
164
166
  this.assertAssignable(
@@ -200,6 +202,24 @@ export const assignabilityMethods = {
200
202
  node._ifaceBox = "ptr";
201
203
  },
202
204
 
205
+ // A js package cannot provide an implementation of a wasm interface: the
206
+ // value would cross into wasm, and JS-implemented proxies are not supported.
207
+ _checkWasmIfaceProvider(target, source, node) {
208
+ if (this.target !== "js" || target?.kind !== "named") return;
209
+ if (target._target !== "wasm" || !source) return;
210
+ const iface = this.resolveType(target.underlying);
211
+ if (iface?.kind !== "interface" || !(iface.methods?.size > 0)) return;
212
+ const ptr = source.kind === "pointer";
213
+ const src = ptr ? this.resolveType(source.base) : source;
214
+ if (src?.kind !== "named" || src._target === "wasm") return;
215
+ if (this.resolveType(src.underlying)?.kind === "interface") return;
216
+ if (!this.implements(source, iface, node)) return;
217
+ this.err(
218
+ `type '${ptr ? "*" : ""}${src.name}' (${src._target ?? "js"}) cannot implement wasm interface '${target.name}' across the boundary; implement it in a wasm package or pass a func callback`,
219
+ node,
220
+ );
221
+ },
222
+
203
223
  _assertAssignableEarlyReturn(target, source) {
204
224
  return (
205
225
  target?.kind === "typeParam" ||
@@ -222,6 +242,23 @@ export const assignabilityMethods = {
222
242
  );
223
243
  return;
224
244
  }
245
+ const sBase = this.resolveType(
246
+ source.kind === "named" ? source.underlying : source,
247
+ );
248
+ if (sharedInfo(source) && tBase?.kind === "slice") {
249
+ this.err(
250
+ `cannot use ${typeStr(source)} as ${typeStr(target)}: a linear-memory buffer cannot be passed as a GC slice without an explicit copy`,
251
+ node,
252
+ );
253
+ return;
254
+ }
255
+ if (tBase?.kind === "shared" && sBase?.kind === "slice") {
256
+ this.err(
257
+ `cannot use ${typeStr(source)} as ${typeStr(target)}: a GC slice cannot be used as a shared buffer; copy its elements into one allocated with shared.New${tBase.shared}`,
258
+ node,
259
+ );
260
+ return;
261
+ }
225
262
  this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
226
263
  },
227
264
 
@@ -346,6 +383,7 @@ export const assignabilityMethods = {
346
383
  },
347
384
 
348
385
  implements(srcType, iface, _node) {
386
+ const isPtr = srcType?.kind === "pointer";
349
387
  // *T has the full method set of T (value + pointer receivers).
350
388
  if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
351
389
  let base = srcType.kind === "named" ? srcType.underlying : srcType;
@@ -366,7 +404,10 @@ export const assignabilityMethods = {
366
404
  : null;
367
405
  if (!methodMap) return false;
368
406
  for (const [name, required] of iface.methods) {
369
- if (!this._implementsMethod(required, methodMap.get(name))) return false;
407
+ const actual = methodMap.get(name);
408
+ if (!actual) return false;
409
+ if (!isPtr && actual._ptrRecv) return false;
410
+ if (!this._implementsMethod(required, actual)) return false;
370
411
  }
371
412
  return true;
372
413
  },