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
package/src/resolver.js CHANGED
@@ -33,6 +33,7 @@ const BUILTIN_PACKAGES = new Set([
33
33
  "strconv",
34
34
  "sort",
35
35
  "math",
36
+ "math/bits",
36
37
  "math/rand",
37
38
  "errors",
38
39
  "time",
@@ -48,6 +49,7 @@ const BUILTIN_PACKAGES = new Set([
48
49
  "path",
49
50
  "path/filepath",
50
51
  "testing",
52
+ "gofront/shared",
51
53
  ]);
52
54
 
53
55
  // Returns true for local relative paths (start with ./ or ../)
@@ -71,7 +73,7 @@ export function resolveGwDir(importPath, fromFile) {
71
73
  return gwFiles.length > 0 ? dir : null;
72
74
  }
73
75
 
74
- export function resolveImport(importPath, fromFile) {
76
+ function resolveImport(importPath, fromFile) {
75
77
  const fromDir = dirname(resolve(fromFile));
76
78
  const nodeModules = findNodeModules(fromDir);
77
79
 
@@ -6,8 +6,26 @@ import { statSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join, resolve } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { HELPER_SPRINTF, HELPER_TESTING } from "./backend/js/runtime.js";
9
10
  import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
10
- import { compilePackageTests, gwFilesIn } from "./compiler.js";
11
+ import {
12
+ compilePackageTests,
13
+ compilePackageTestsWasm,
14
+ gwFilesIn,
15
+ } from "./compiler.js";
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
+ }
11
29
 
12
30
  export function isTestFunc(decl) {
13
31
  if (decl.kind !== "FuncDecl") return false;
@@ -78,7 +96,16 @@ globalThis.document = globalThis.document ?? null;
78
96
  `
79
97
  }
80
98
  globalThis.__gofront_verbose = ${verbose};
81
-
99
+ ${
100
+ options.wasmBytes
101
+ ? `
102
+ // ── WASM module (tests run inside app.wasm; *testing.T stays in JS) ──
103
+ ${HELPER_SPRINTF}
104
+ ${HELPER_TESTING}
105
+ globalThis.__GOFRONT_WASM_BYTES = Uint8Array.from(atob(${JSON.stringify(Buffer.from(options.wasmBytes).toString("base64"))}), (c) => c.charCodeAt(0));
106
+ `
107
+ : ""
108
+ }
82
109
  // ── Compiled Package Bundle ──
83
110
  ${bundleJs}
84
111
  // ─────────────────────────────
@@ -281,6 +308,44 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
281
308
  });
282
309
  }
283
310
 
311
+ function reportBuildFailure(err, resolvedDir, options) {
312
+ const msg = err?.message ?? String(err);
313
+ if (options.captureOutput) {
314
+ const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
315
+ return { exitCode: 1, stdout: "", stderr: out };
316
+ }
317
+ const dest = options.stderr ?? process.stderr;
318
+ const c = colorsFor(options, dest);
319
+ dest.write(
320
+ `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
321
+ );
322
+ return { exitCode: 1 };
323
+ }
324
+
325
+ // `both` packages run twice (JS and wasm) so the two backends are kept in
326
+ // agreement; output of both runs is concatenated.
327
+ async function runDualTarget(compiled, resolvedDir, runHarness, options) {
328
+ let wasmCompiled;
329
+ try {
330
+ wasmCompiled = compilePackageTestsWasm(resolvedDir, options);
331
+ } catch (err) {
332
+ return reportBuildFailure(err, resolvedDir, options);
333
+ }
334
+ const label = compiled.pkgName;
335
+ const jsRun = await runHarness(compiled.js, `${label} [js]`, null);
336
+ const wasmRun = await runHarness(
337
+ wasmCompiled.js,
338
+ `${label} [wasm]`,
339
+ wasmCompiled.wasm,
340
+ );
341
+ return {
342
+ exitCode: jsRun.exitCode !== 0 ? jsRun.exitCode : wasmRun.exitCode,
343
+ stdout: jsRun.stdout + wasmRun.stdout,
344
+ stderr: jsRun.stderr + wasmRun.stderr,
345
+ testCount: jsRun.testCount + wasmRun.testCount,
346
+ };
347
+ }
348
+
284
349
  export async function runTests(targetDir, options = {}) {
285
350
  let resolvedDir = resolve(targetDir);
286
351
  try {
@@ -304,17 +369,7 @@ export async function runTests(targetDir, options = {}) {
304
369
  try {
305
370
  compiled = compilePackageTests(resolvedDir, options);
306
371
  } catch (err) {
307
- const msg = err?.message ?? String(err);
308
- if (options.captureOutput) {
309
- const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
310
- return { exitCode: 1, stdout: "", stderr: out };
311
- }
312
- const dest = options.stderr ?? process.stderr;
313
- const c = colorsFor(options, dest);
314
- dest.write(
315
- `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
316
- );
317
- return { exitCode: 1 };
372
+ return reportBuildFailure(err, resolvedDir, options);
318
373
  }
319
374
 
320
375
  const { pkgName, js, programs } = compiled;
@@ -324,12 +379,31 @@ export async function runTests(targetDir, options = {}) {
324
379
  return reportEmptyTests(pkgName, "no tests to run", options);
325
380
  }
326
381
 
327
- const harnessJs = generateTestHarness(js, testNames, {
328
- ...options,
329
- pkgName,
330
- jsdomPath,
331
- color: colorsFor(options, options.stdout ?? process.stdout).enabled,
332
- });
382
+ const runHarness = (bundleJs, label, wasmBytes) =>
383
+ spawnTestRunner(
384
+ resolvedDir,
385
+ generateTestHarness(bundleJs, testNames, {
386
+ ...options,
387
+ pkgName: label,
388
+ jsdomPath,
389
+ wasmBytes,
390
+ color: colorsFor(options, options.stdout ?? process.stdout).enabled,
391
+ }),
392
+ testNames.length,
393
+ options,
394
+ );
333
395
 
334
- return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
396
+ // `wasm` packages run inside the linked app.wasm; `js` packages that import
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
+ }
405
+ if (compiled.target === "both") {
406
+ return runDualTarget(compiled, resolvedDir, runHarness, options);
407
+ }
408
+ return runHarness(js, pkgName, compiled.wasm ?? null);
335
409
  }
@@ -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
  },
@@ -1,8 +1,10 @@
1
1
  // TypeChecker expression-checking methods — installed as a mixin on TypeChecker.prototype.
2
2
 
3
+ import { sharedInfo } from "./stdlib/shared.js";
3
4
  import {
4
5
  ANY,
5
6
  BOOL,
7
+ BROWSER_GLOBALS,
6
8
  COMPLEX128,
7
9
  ERROR,
8
10
  FLOAT64,
@@ -41,6 +43,13 @@ const BUILTIN_CHECK = {
41
43
  s.err(`cannot append to array (type ${typeStr(st)})`, e);
42
44
  return st;
43
45
  }
46
+ if (sharedInfo(st)) {
47
+ s.err(
48
+ `cannot append to ${typeStr(st)}: shared buffers have a fixed size`,
49
+ e,
50
+ );
51
+ return st;
52
+ }
44
53
  const sb = s.resolveType(st?.kind === "named" ? st.underlying : st);
45
54
  if (sb?.kind === "slice") {
46
55
  for (let i = 1; i < e.args.length; i++) {
@@ -54,7 +63,7 @@ const BUILTIN_CHECK = {
54
63
  }
55
64
  return st;
56
65
  },
57
- copy: () => INT,
66
+ copy: (s, _n, e, at) => s._checkBuiltinCopy(e, at),
58
67
  delete: () => VOID,
59
68
  make: (s, _n, e, _at, sc) => {
60
69
  if (e.args.length < 1) return ANY;
@@ -126,6 +135,26 @@ export const expressionCheckMethods = {
126
135
  const t = scope.lookup(expr.name);
127
136
  if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
128
137
  if (this.types.has(expr.name)) expr._isTypeRef = true;
138
+ if (
139
+ (this.target === "wasm" || this.target === "both") &&
140
+ BROWSER_GLOBALS.has(expr.name) &&
141
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
142
+ ) {
143
+ this.recordBlocker("browser global", expr.name);
144
+ this.err(
145
+ `'${expr.name}' is not available in wasm packages`,
146
+ expr,
147
+ `(package '${this.pkgName}' is //gofront:target ${this.target} — browser globals are only available in js packages)`,
148
+ );
149
+ }
150
+ if (
151
+ (this.target === "wasm" || this.target === "both") &&
152
+ expr.name === "gom" &&
153
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
154
+ ) {
155
+ this.recordBlocker("gom usage", "gom");
156
+ this.err("package 'gom' is not available in wasm packages", expr);
157
+ }
129
158
  return t;
130
159
  }
131
160
  case "BinaryExpr": {
@@ -136,8 +165,16 @@ export const expressionCheckMethods = {
136
165
  return this.binaryResultType(expr.op, ANY, ANY, expr);
137
166
  return this.binaryResultType(expr.op, lt, rt, expr);
138
167
  }
139
- case "AwaitExpr":
168
+ case "AwaitExpr": {
169
+ if (this.target === "wasm" || this.target === "both") {
170
+ this.recordBlocker("async function");
171
+ this.err(
172
+ "async functions are not supported in wasm packages; keep async code in a js package",
173
+ expr,
174
+ );
175
+ }
140
176
  return this.checkExpr(expr.expr, scope);
177
+ }
141
178
  case "InstantiationExpr":
142
179
  return this._checkInstantiationExpr(expr, scope);
143
180
  default: {
@@ -262,10 +299,42 @@ export const expressionCheckMethods = {
262
299
  expr._mapValueType = btu.value;
263
300
  return btu.value;
264
301
  }
302
+ if (btu.kind === "shared") {
303
+ expr._sharedElem = btu.elem;
304
+ const idx = this._constIntValue(expr.index);
305
+ if (idx !== null && idx < 0)
306
+ this.err(`invalid index ${idx} (index must not be negative)`, expr);
307
+ return btu.elem;
308
+ }
265
309
  if (isString(bt)) return INT;
266
310
  return this.err(`Cannot index type ${typeStr(bt)}`, expr);
267
311
  },
268
312
 
313
+ // copy(dst, src): element types must be identical when either side is a
314
+ // shared buffer (the wasm emitter uses one element type for both sides).
315
+ _checkBuiltinCopy(e, at) {
316
+ const elemOf = (t) => {
317
+ const u = this.resolveType(t?.kind === "named" ? t.underlying : t);
318
+ return u?.kind === "slice" || u?.kind === "shared" ? u.elem : null;
319
+ };
320
+ const [dt, st] = at;
321
+ if (!sharedInfo(dt) && !sharedInfo(st)) return INT;
322
+ const de = elemOf(dt);
323
+ const se = elemOf(st);
324
+ if (!de || !se) {
325
+ this.err(
326
+ `copy: cannot copy between ${typeStr(dt)} and ${typeStr(st)}`,
327
+ e,
328
+ );
329
+ } else if (typeStr(de) !== typeStr(se)) {
330
+ this.err(
331
+ `copy: element types differ (${typeStr(dt)} vs ${typeStr(st)})`,
332
+ e,
333
+ );
334
+ }
335
+ return INT;
336
+ },
337
+
269
338
  _checkSliceExpr(expr, scope) {
270
339
  const bt = this.checkExpr(expr.expr, scope);
271
340
  if (expr.low) this.checkExpr(expr.low, scope);
@@ -276,10 +345,35 @@ export const expressionCheckMethods = {
276
345
  if (bt.kind === "slice" || bt.kind === "array")
277
346
  return { kind: "slice", elem: bt.elem };
278
347
  if (isString(bt)) return STRING;
348
+ if (sharedInfo(bt))
349
+ return this.err(
350
+ `Cannot slice type ${typeStr(bt)}: use .Subarray(lo, hi) to create a view of a shared buffer`,
351
+ expr,
352
+ );
279
353
  return this.err(`Cannot slice type ${typeStr(bt)}`, expr);
280
354
  },
281
355
 
282
356
  _checkFuncLit(expr, scope) {
357
+ const savedSharedAlloc = this._sharedAllocOk;
358
+ this._sharedAllocOk = false;
359
+ try {
360
+ return this._checkFuncLitBody(expr, scope);
361
+ } finally {
362
+ this._sharedAllocOk = savedSharedAlloc;
363
+ }
364
+ },
365
+
366
+ _checkFuncLitBody(expr, scope) {
367
+ if (
368
+ (this.target === "wasm" || this.target === "both") &&
369
+ (expr.async || expr.isAsync)
370
+ ) {
371
+ this.recordBlocker("async function");
372
+ this.err(
373
+ "async functions are not supported in wasm packages; keep async code in a js package",
374
+ expr,
375
+ );
376
+ }
283
377
  const inner = new Scope(scope);
284
378
  for (const p of expr.params)
285
379
  inner.define(p.name, p.type ? this.resolveTypeNode(p.type, scope) : ANY);
@@ -392,7 +486,11 @@ export const expressionCheckMethods = {
392
486
 
393
487
  _rangeCollTypeTuple(resolved, collType) {
394
488
  if (isString(resolved)) return { kind: "tuple", types: [INT, RUNE] };
395
- if (resolved?.kind === "slice" || resolved?.kind === "array")
489
+ if (
490
+ resolved?.kind === "slice" ||
491
+ resolved?.kind === "array" ||
492
+ resolved?.kind === "shared"
493
+ )
396
494
  return { kind: "tuple", types: [INT, resolved.elem ?? ANY] };
397
495
  if (resolved?.kind === "map")
398
496
  return {
@@ -428,6 +526,9 @@ export const expressionCheckMethods = {
428
526
  : null;
429
527
  if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
430
528
  }
529
+ expr._typeArgs = typeArgs;
530
+ if (expr.expr) expr.expr._typeArgs = typeArgs;
531
+ expr._genericFnType = baseType;
431
532
  return this.instantiateGenericFunc(baseType, typeArgs);
432
533
  }
433
534
  // The parser's type-arg heuristic mistakes `xs[d.Field]` (uppercase field
@@ -507,6 +608,8 @@ export const expressionCheckMethods = {
507
608
 
508
609
  checkCall(expr, scope) {
509
610
  let fnType = this.checkExpr(expr.func, scope);
611
+ this._checkSharedAllocSite(expr, scope);
612
+ this._checkBoundaryCallInLoop(expr, scope);
510
613
  if (fnType.kind === "builtin") {
511
614
  const argTypes = expr.args.map((a, i) => {
512
615
  // new(T) — first arg is a type name, not a value expression
@@ -537,12 +640,79 @@ export const expressionCheckMethods = {
537
640
 
538
641
  this._checkCallArgs(fnType, argTypes, expr);
539
642
 
643
+ // Pseudo-generic stdlib helpers (maps.Keys, slices.Clone, ...) declare
644
+ // how their result derives from the first argument.
645
+ if (fnType._derivedReturn) {
646
+ const derived = this._derivedReturnType(fnType._derivedReturn, argTypes);
647
+ if (derived) return derived;
648
+ }
649
+
540
650
  const ret = fnType.returns;
541
651
  if (!ret || ret.length === 0) return VOID;
542
652
  if (ret.length === 1) return ret[0];
543
653
  return { kind: "tuple", types: ret };
544
654
  },
545
655
 
656
+ // `shared.NewXxx(n)` allocates from a linear memory that is sized once at
657
+ // startup (no memory.grow → no detached TypedArray views on the JS side).
658
+ // Allocation is therefore only legal in package-level var initializers and
659
+ // init(), which both run inside the module start function.
660
+ // JS→WASM calls copy non-shared arguments across the boundary on every
661
+ // call. Inside a loop that dominates the per-iteration cost, so hint that
662
+ // the loop belongs on the WASM side (or the data in gofront/shared).
663
+ _checkBoundaryCallInLoop(expr, scope) {
664
+ if (this._loopDepth === 0 || this.target !== "js") return;
665
+ const fn = expr.func;
666
+ if (fn.kind !== "SelectorExpr" || fn.expr.kind !== "Ident") return;
667
+ const ns = scope.lookup(fn.expr.name);
668
+ if (ns?.kind !== "namespace" || ns._target !== "wasm") return;
669
+ const member = ns.members[fn.field];
670
+ if (member?.kind !== "func") return;
671
+ this.warn(
672
+ `'${fn.expr.name}.${fn.field}' crosses the JS→WASM boundary inside a loop`,
673
+ fn.expr, // CallExpr nodes carry no position; the callee Ident does
674
+ "hint: each call copies its arguments; move the loop into the wasm package or pass gofront/shared buffers",
675
+ );
676
+ },
677
+
678
+ _checkSharedAllocSite(expr, scope) {
679
+ const f = expr.func;
680
+ if (
681
+ f.kind !== "SelectorExpr" ||
682
+ f.expr.kind !== "Ident" ||
683
+ f.expr.name !== "shared" ||
684
+ !f.field.startsWith("New") ||
685
+ scope.lookup("shared") !== this.globals.lookup("shared")
686
+ )
687
+ return;
688
+ if (this._sharedAllocOk) return;
689
+ const why =
690
+ this.target === "wasm"
691
+ ? "shared buffers are allocated once at startup"
692
+ : "shared buffers are allocated once at startup so the code stays portable to wasm packages";
693
+ this.err(
694
+ `shared.${f.field} must be called from a package-level var initializer or init(); ${why}`,
695
+ expr,
696
+ );
697
+ },
698
+
699
+ _derivedReturnType(mode, argTypes) {
700
+ const arg = argTypes[0];
701
+ const under = arg?.kind === "named" ? arg.underlying : arg;
702
+ switch (mode) {
703
+ case "arg0":
704
+ return under ? arg : null;
705
+ case "keysOfArg0":
706
+ return under?.key ? { kind: "slice", elem: under.key } : null;
707
+ case "valuesOfArg0": {
708
+ const v = under?.value ?? under?.elem;
709
+ return v ? { kind: "slice", elem: v } : null;
710
+ }
711
+ default:
712
+ return null;
713
+ }
714
+ },
715
+
546
716
  _resolveGenericFnType(fnType, argTypes, expr, scope) {
547
717
  const typeArgs = this.inferTypeArgs(fnType, argTypes);
548
718
  if (!typeArgs) {
@@ -560,6 +730,10 @@ export const expressionCheckMethods = {
560
730
  : null;
561
731
  if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
562
732
  }
733
+ expr._typeArgs = typeArgs;
734
+ if (expr.func) expr.func._typeArgs = typeArgs;
735
+ expr._genericFnType = fnType;
736
+ if (expr.func) expr.func._genericFnType = fnType;
563
737
  return this.instantiateGenericFunc(fnType, typeArgs);
564
738
  },
565
739
 
@@ -611,6 +785,11 @@ export const expressionCheckMethods = {
611
785
  if (name === "len") {
612
786
  const size = this._constLenFromType(argTypes[0]);
613
787
  if (size != null) expr._constLen = size;
788
+ } else if (sharedInfo(argTypes[0])) {
789
+ this.err(
790
+ `cannot use cap() on ${typeStr(argTypes[0])}: shared buffers have a fixed size; use len()`,
791
+ expr,
792
+ );
614
793
  }
615
794
  return INT;
616
795
  },