gofront 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +288 -128
  4. package/src/backend/js/index.js +138 -56
  5. package/src/backend/js/runtime.js +1055 -53
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +151 -83
  8. package/src/backend/js/stdlib/base64.js +12 -0
  9. package/src/backend/js/stdlib/binary.js +102 -0
  10. package/src/backend/js/stdlib/bits.js +15 -1
  11. package/src/backend/js/stdlib/builder.js +28 -9
  12. package/src/backend/js/stdlib/bytes.js +75 -18
  13. package/src/backend/js/stdlib/errors.js +22 -3
  14. package/src/backend/js/stdlib/fmt.js +95 -20
  15. package/src/backend/js/stdlib/hex.js +17 -0
  16. package/src/backend/js/stdlib/html.js +1 -1
  17. package/src/backend/js/stdlib/index.js +19 -0
  18. package/src/backend/js/stdlib/io.js +105 -3
  19. package/src/backend/js/stdlib/maps.js +1 -1
  20. package/src/backend/js/stdlib/math.js +13 -1
  21. package/src/backend/js/stdlib/path.js +20 -6
  22. package/src/backend/js/stdlib/rand.js +10 -5
  23. package/src/backend/js/stdlib/sha256.js +12 -0
  24. package/src/backend/js/stdlib/shared.js +17 -0
  25. package/src/backend/js/stdlib/slices.js +53 -27
  26. package/src/backend/js/stdlib/sort.js +1 -1
  27. package/src/backend/js/stdlib/strconv.js +52 -11
  28. package/src/backend/js/stdlib/strings.js +152 -16
  29. package/src/backend/js/stdlib/sync.js +13 -0
  30. package/src/backend/js/stdlib/testing.js +2 -2
  31. package/src/backend/js/stdlib/time.js +93 -22
  32. package/src/backend/js/stdlib/unicode.js +2 -2
  33. package/src/backend/js/templ.js +1 -1
  34. package/src/backend/wasm/boundary.js +1677 -58
  35. package/src/backend/wasm/const-fold.js +205 -0
  36. package/src/backend/wasm/emit-builtins.js +1230 -0
  37. package/src/backend/wasm/emit-exprs.js +2096 -0
  38. package/src/backend/wasm/emit-maps.js +1999 -0
  39. package/src/backend/wasm/emit-shared.js +519 -0
  40. package/src/backend/wasm/emit-stdlib.js +13 -0
  41. package/src/backend/wasm/emit-stmts.js +2466 -0
  42. package/src/backend/wasm/emit.js +656 -5031
  43. package/src/backend/wasm/encode.js +259 -85
  44. package/src/backend/wasm/glue.js +19 -5
  45. package/src/backend/wasm/index.js +2432 -86
  46. package/src/backend/wasm/monomorph.js +711 -0
  47. package/src/backend/wasm/optimize.js +236 -0
  48. package/src/backend/wasm/stdlib/bytes.js +5039 -0
  49. package/src/backend/wasm/stdlib/core.js +8903 -0
  50. package/src/backend/wasm/stdlib/index.js +40 -0
  51. package/src/backend/wasm/stdlib/io.js +2161 -0
  52. package/src/backend/wasm/stdlib/packages.js +2400 -0
  53. package/src/backend/wasm/stdlib/pure.js +720 -0
  54. package/src/backend/wasm/stdlib/time.js +1200 -0
  55. package/src/backend/wasm/types.js +312 -36
  56. package/src/backend/wasm/wat.js +49 -2
  57. package/src/cli-core.js +276 -73
  58. package/src/compiler.js +162 -25
  59. package/src/dts-parser.js +79 -126
  60. package/src/index.js +88 -121
  61. package/src/lexer.js +80 -29
  62. package/src/lower/boxing.js +4 -6
  63. package/src/lower/captures.js +12 -23
  64. package/src/lower/escape.js +4 -9
  65. package/src/lower/functions.js +277 -5
  66. package/src/lower/globals.js +7 -15
  67. package/src/lower/index.js +5 -2
  68. package/src/lower/ownership.js +35 -19
  69. package/src/lower/range.js +2 -5
  70. package/src/lower/walk.js +25 -0
  71. package/src/minifier.js +20 -68
  72. package/src/parser/declarations.js +3 -6
  73. package/src/parser/expressions.js +3 -8
  74. package/src/parser/index.js +18 -3
  75. package/src/parser/statements.js +25 -38
  76. package/src/parser/types.js +1 -2
  77. package/src/resolver.js +3 -27
  78. package/src/templ-lexer.js +29 -63
  79. package/src/templ-parser.js +22 -45
  80. package/src/test-runner.js +20 -0
  81. package/src/typechecker/assignability.js +123 -14
  82. package/src/typechecker/expressions.js +244 -4
  83. package/src/typechecker/index.js +27 -1
  84. package/src/typechecker/resolve.js +71 -12
  85. package/src/typechecker/statements.js +0 -7
  86. package/src/typechecker/stdlib/core.js +292 -103
  87. package/src/typechecker/stdlib/extended.js +471 -4
  88. package/src/typechecker/stdlib/shared.js +53 -0
  89. package/src/typechecker/stdlib.js +2 -0
  90. package/src/typechecker/types.js +545 -8
  91. package/src/vendor.js +2 -2
package/src/index.js CHANGED
@@ -13,18 +13,14 @@
13
13
  // gofront <file.go> --tokens — dump tokens of first file (debug)
14
14
  // gofront init [dir] — scaffold a new GoFront project
15
15
 
16
- import { createRequire } from "node:module";
17
-
18
- const _require = createRequire(import.meta.url);
19
- const { version } = _require("../package.json");
20
-
21
- import { statSync, watch } from "node:fs";
16
+ import { statSync } from "node:fs";
22
17
  import { dirname, relative, resolve } from "node:path";
23
18
  import { copyAssets } from "./asset-manager.js";
24
19
  import {
25
20
  formatBuildSummary,
26
21
  formatPrepSummary,
27
22
  formatWrittenDesc,
23
+ GOFRONT_VERSION,
28
24
  handleBuild,
29
25
  handleCheck,
30
26
  handleDev,
@@ -35,30 +31,30 @@ import {
35
31
  parseBuildArgs,
36
32
  parseCheckArgs,
37
33
  parseDevArgs,
34
+ parseLegacyArgs,
38
35
  parsePrepArgs,
39
36
  parseTestArgs,
40
37
  runCompile,
41
38
  writeCompileOutput,
42
39
  } from "./cli-core.js";
43
40
  import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
44
- import { createDevServer } from "./dev-server.js";
45
41
 
46
42
  // ── Parse CLI args ───────────────────────────────────────────
47
43
 
48
44
  const args = process.argv.slice(2);
49
45
  if (args[0] === "--version" || args[0] === "-v") {
50
- console.log(`gofront ${version}`);
46
+ console.log(`gofront ${GOFRONT_VERSION}`);
51
47
  process.exit(0);
52
48
  }
53
49
 
54
50
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
55
51
  console.log(
56
52
  `
57
- GoFront — a Go-inspired language that compiles to JavaScript
53
+ GoFront — a Go-inspired language that compiles to JavaScript and WebAssembly
58
54
 
59
55
  Usage:
60
56
  gofront dev [dir] [options] Start dev server with live reload (default port 3000)
61
- gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat)
57
+ gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat, --release, --wasm-opt, --js-only)
62
58
  gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
63
59
  gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
64
60
  gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
@@ -66,6 +62,8 @@ Usage:
66
62
  gofront <file.go> Compile single file and print to stdout
67
63
  gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
68
64
  gofront <input> -o out.js Compile and write to file (wasm packages also emit app.wasm)
65
+ gofront <input> --release Production build with minification and Binaryen wasm optimization
66
+ gofront <input> --wasm-opt Optimize wasm output with Binaryen (-O3 + GUFA)
69
67
  gofront <input> --check Type-check only
70
68
  gofront <input> --watch Watch for changes and recompile
71
69
  gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
@@ -131,9 +129,9 @@ if (args[0] === "test") {
131
129
  // ── check subcommand ──────────────────────────────────────────
132
130
 
133
131
  if (args[0] === "check") {
134
- const { targetDir } = parseCheckArgs(args.slice(1));
132
+ const { targetDir, forceTarget } = parseCheckArgs(args.slice(1));
135
133
  try {
136
- const { elapsedMs, packages } = handleCheck(targetDir);
134
+ const { elapsedMs, packages } = handleCheck(targetDir, { forceTarget });
137
135
  const okLine = (label, t) =>
138
136
  log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
139
137
  if (packages) {
@@ -202,20 +200,22 @@ if (args[0] === "dev") {
202
200
  }
203
201
  }
204
202
 
205
- const inputArg = args[0];
206
- const outputFlag = args.indexOf("-o");
207
- const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
208
- const checkOnly = args.includes("--check");
209
- const dumpAst = args.includes("--ast");
210
- const dumpTokens = args.includes("--tokens");
211
- const sourceMap = args.includes("--source-map");
212
- const serveMode = args.includes("--serve");
213
- const watchMode = args.includes("--watch") || serveMode;
214
- const copyAssetsFlag = args.includes("--copy-assets");
215
- const minifyOutput = args.includes("--minify");
216
- const mangleOutput = args.includes("--mangle");
217
- const portFlag = args.indexOf("--port");
218
- const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
203
+ const {
204
+ inputArg,
205
+ outputFile,
206
+ checkOnly,
207
+ dumpAst,
208
+ dumpTokens,
209
+ sourceMap,
210
+ serve: serveMode,
211
+ watch: watchMode,
212
+ copyAssets: copyAssetsFlag,
213
+ wasmOpt,
214
+ minify: minifyOutput,
215
+ mangle: mangleOutput,
216
+ port: servePort,
217
+ forceTarget,
218
+ } = parseLegacyArgs(args);
219
219
 
220
220
  // ── Determine input mode ─────────────────────────────────────
221
221
 
@@ -228,6 +228,17 @@ try {
228
228
  process.exit(1);
229
229
  }
230
230
 
231
+ function copyProjectAssets(verbose) {
232
+ try {
233
+ const { copied, skipped } = copyAssets(resolve("."));
234
+ if (verbose && (copied > 0 || skipped > 0)) {
235
+ log.info(`copied ${copied} assets (${skipped} skipped)`);
236
+ }
237
+ } catch (e) {
238
+ log.warn(`asset copy failed: ${e.message}`);
239
+ }
240
+ }
241
+
231
242
  // ── Single-shot mode ─────────────────────────────────────────
232
243
 
233
244
  if (!watchMode) {
@@ -239,6 +250,8 @@ if (!watchMode) {
239
250
  outputFile,
240
251
  dumpTokens,
241
252
  dumpAst,
253
+ wasmOpt,
254
+ forceTarget,
242
255
  });
243
256
  } catch (e) {
244
257
  log.fail(e.message);
@@ -282,6 +295,13 @@ if (!watchMode) {
282
295
  log.info(
283
296
  `wrote ${formatWrittenDesc(written, outputFile)} ${ms(elapsedMs)}`,
284
297
  );
298
+ if (result.wasmOptInfo && result.wasmOptInfo.originalSize > 0) {
299
+ const origKb = (result.wasmOptInfo.originalSize / 1024).toFixed(1);
300
+ const optKb = (result.wasmOptInfo.optimizedSize / 1024).toFixed(1);
301
+ log.info(
302
+ `wasm-opt: app.wasm ${origKb} kB → ${optKb} kB (-${result.wasmOptInfo.percentSaved}%) [${result.wasmOptInfo.engine}]`,
303
+ );
304
+ }
285
305
  } catch (e) {
286
306
  log.fail(`cannot write '${outputFile}': ${e.message}`);
287
307
  process.exit(1);
@@ -295,110 +315,57 @@ if (!watchMode) {
295
315
  }
296
316
  }
297
317
 
298
- if (copyAssetsFlag) {
299
- try {
300
- const projectDir = resolve(".");
301
- const { copied, skipped } = copyAssets(projectDir);
302
- if (copied > 0 || skipped > 0) {
303
- log.info(`copied ${copied} assets (${skipped} skipped)`);
304
- }
305
- } catch (e) {
306
- log.warn(`asset copy failed: ${e.message}`);
307
- }
308
- }
318
+ if (copyAssetsFlag) copyProjectAssets(true);
309
319
 
310
320
  process.exit(0);
311
321
  }
312
322
 
313
323
  // ── Watch mode ───────────────────────────────────────────────
314
324
 
315
- // Start dev server before first build so the browser can connect immediately
316
- let devServer = null;
317
- if (serveMode) {
318
- if (!outputFile) {
319
- log.fail("--serve requires -o <output file>");
320
- process.exit(1);
321
- }
322
- const serveDir = dirname(resolve(outputFile));
323
- devServer = createDevServer(serveDir, servePort);
325
+ if (serveMode && !outputFile) {
326
+ log.fail("--serve requires -o <output file>");
327
+ process.exit(1);
324
328
  }
325
329
 
326
- function buildOnce(changedFile = null) {
327
- try {
328
- const startMs = performance.now();
329
- const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
330
- const js = maybeMinify(result.js, {
331
- minify: minifyOutput,
332
- mangle: mangleOutput,
333
- sourceMap,
334
- });
335
- const elapsedMs = (performance.now() - startMs).toFixed(0);
336
- const changeNote = changedFile ? ` — ${changedFile} changed` : "";
337
- const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
338
- if (outputFile) {
339
- const written = writeCompileOutput(resolve(outputFile), result, js);
340
- console.error(
341
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
342
- );
343
- } else {
344
- // Clear screen then print
345
- process.stdout.write("\x1Bc");
346
- console.log(js);
347
- console.error(
348
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
349
- );
350
- }
330
+ const prefix = () => `${stamp()} ${colors.bold("gofront:")}`;
351
331
 
352
- if (copyAssetsFlag) {
353
- try {
354
- copyAssets(resolve("."));
355
- } catch (e) {
356
- log.warn(`asset copy failed: ${e.message}`);
332
+ try {
333
+ await handleDev(".", {
334
+ srcDir: inputPath,
335
+ outputFile,
336
+ serve: serveMode,
337
+ serveDir: outputFile ? dirname(resolve(outputFile)) : undefined,
338
+ port: servePort,
339
+ silent: false,
340
+ sourceMap,
341
+ minify: minifyOutput,
342
+ mangle: mangleOutput,
343
+ copyAssets: copyAssetsFlag,
344
+ forceTarget,
345
+ onBuild: ({ js, written, elapsedMs, changedFile }) => {
346
+ const changeNote = changedFile ? ` — ${changedFile} changed` : "";
347
+ const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
348
+ if (outputFile) {
349
+ console.error(
350
+ `${prefix()} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
351
+ );
352
+ } else {
353
+ // Clear screen then print
354
+ process.stdout.write("\x1Bc");
355
+ console.log(js);
356
+ console.error(`${prefix()} ${colors.green("OK")} ${timing}`);
357
357
  }
358
- }
359
-
360
- devServer?.notify();
361
- } catch (e) {
362
- console.error(
363
- `${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
364
- );
365
- for (const line of formatDiagnostic(e.message).split("\n"))
366
- console.error(` ${line}`);
367
- devServer?.notifyError?.(e);
368
- }
369
- }
370
-
371
- // Initial build
372
- buildOnce();
373
-
374
- // Determine what to watch
375
- const watchTarget = isDir ? inputPath : dirname(inputPath);
376
-
377
- function handleCssWatch(filename) {
378
- if (copyAssetsFlag) {
379
- try {
380
- copyAssets(resolve("."));
381
- } catch (e) {
382
- log.warn(`asset copy failed: ${e.message}`);
383
- }
384
- }
385
- devServer?.notifyCss?.(filename);
358
+ if (copyAssetsFlag) copyProjectAssets(false);
359
+ },
360
+ onError: (e) => {
361
+ console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
362
+ for (const line of formatDiagnostic(e.message).split("\n"))
363
+ console.error(` ${line}`);
364
+ },
365
+ });
366
+ } catch (e) {
367
+ log.fail(e.message);
368
+ process.exit(1);
386
369
  }
387
370
 
388
- let debounce = null;
389
- let cssDebounce = null;
390
- watch(watchTarget, { recursive: true }, (_event, filename) => {
391
- if (!filename) return;
392
- if (filename.endsWith(".css")) {
393
- clearTimeout(cssDebounce);
394
- cssDebounce = setTimeout(() => handleCssWatch(filename), 50);
395
- return;
396
- }
397
- if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
398
- clearTimeout(debounce);
399
- debounce = setTimeout(() => buildOnce(filename), 80);
400
- });
401
-
402
- console.error(
403
- `${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
404
- );
371
+ console.error(`${prefix()} watching ${colors.cyan(inputArg)} ...`);
package/src/lexer.js CHANGED
@@ -203,11 +203,25 @@ export class Lexer {
203
203
  const l = this.line,
204
204
  c = this.col;
205
205
  this.advance(); // opening "
206
- const parts = [];
207
- while (this.pos < this.src.length && this.peek() !== '"') {
208
- if (this.peek() === "\n") this.err("Unterminated string literal");
209
- if (this.peek() === "\\") {
210
- this.advance();
206
+ let start = this.pos;
207
+ let parts = null;
208
+ while (this.pos < this.src.length) {
209
+ const ch = this.src[this.pos];
210
+ if (ch === '"') {
211
+ if (parts === null) {
212
+ const str = this.src.slice(start, this.pos);
213
+ this.advance(); // closing "
214
+ return str;
215
+ }
216
+ parts.push(this.src.slice(start, this.pos));
217
+ this.advance(); // closing "
218
+ return parts.join("");
219
+ }
220
+ if (ch === "\n") this.err("Unterminated string literal");
221
+ if (ch === "\\") {
222
+ if (parts === null) parts = [];
223
+ if (this.pos > start) parts.push(this.src.slice(start, this.pos));
224
+ this.advance(); // skip \
211
225
  const esc = this.advance();
212
226
  switch (esc) {
213
227
  case "n":
@@ -231,14 +245,12 @@ export class Lexer {
231
245
  default:
232
246
  parts.push(`\\${esc}`);
233
247
  }
248
+ start = this.pos;
234
249
  } else {
235
- parts.push(this.advance());
250
+ this.advance();
236
251
  }
237
252
  }
238
- if (this.pos >= this.src.length)
239
- throw new LexError("Unterminated string", l, c, this.filename, this.src);
240
- this.advance(); // closing "
241
- return parts.join("");
253
+ throw new LexError("Unterminated string", l, c, this.filename, this.src);
242
254
  }
243
255
 
244
256
  readRawString() {
@@ -383,39 +395,78 @@ export class Lexer {
383
395
  _readDecimalExponent(n) {
384
396
  n += this.advance(); // consume e/E
385
397
  if (this.peek() === "+" || this.peek() === "-") n += this.advance();
386
- while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
387
- const ch = this.advance();
388
- if (ch !== "_") n += ch;
398
+ while (this.pos < this.src.length) {
399
+ const c = this.src.charCodeAt(this.pos);
400
+ if (c >= 48 && c <= 57) {
401
+ n += this.advance();
402
+ } else if (c === 95) {
403
+ this.advance();
404
+ } else {
405
+ break;
406
+ }
389
407
  }
390
408
  return n;
391
409
  }
392
410
 
393
411
  _readDecimalNumber(n) {
394
412
  let isFloat = false;
395
- while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
396
- const ch = this.advance();
397
- if (ch !== "_") n += ch;
413
+ while (this.pos < this.src.length) {
414
+ const c = this.src.charCodeAt(this.pos);
415
+ if (c >= 48 && c <= 57) {
416
+ n += this.advance();
417
+ } else if (c === 95) {
418
+ this.advance(); // skip _
419
+ } else {
420
+ break;
421
+ }
398
422
  }
399
- if (this.peek() === "." && /[0-9]/.test(this.peek(1))) {
400
- isFloat = true;
401
- n += this.advance();
402
- while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
403
- const ch = this.advance();
404
- if (ch !== "_") n += ch;
423
+ if (
424
+ this.pos + 1 < this.src.length &&
425
+ this.src.charCodeAt(this.pos) === 46
426
+ ) {
427
+ const next = this.src.charCodeAt(this.pos + 1);
428
+ if (next >= 48 && next <= 57) {
429
+ isFloat = true;
430
+ n += this.advance();
431
+ while (this.pos < this.src.length) {
432
+ const c = this.src.charCodeAt(this.pos);
433
+ if (c >= 48 && c <= 57) {
434
+ n += this.advance();
435
+ } else if (c === 95) {
436
+ this.advance(); // skip _
437
+ } else {
438
+ break;
439
+ }
440
+ }
405
441
  }
406
442
  }
407
- if (this.peek() === "e" || this.peek() === "E") {
408
- isFloat = true;
409
- n = this._readDecimalExponent(n);
443
+ if (this.pos < this.src.length) {
444
+ const c = this.src.charCodeAt(this.pos);
445
+ if (c === 101 || c === 69) {
446
+ isFloat = true;
447
+ n = this._readDecimalExponent(n);
448
+ }
410
449
  }
411
450
  return { n, isFloat };
412
451
  }
413
452
 
414
453
  readIdent() {
415
- let s = "";
416
- while (this.pos < this.src.length && /[a-zA-Z0-9_]/.test(this.peek()))
417
- s += this.advance();
418
- return s;
454
+ const start = this.pos;
455
+ while (this.pos < this.src.length) {
456
+ const code = this.src.charCodeAt(this.pos);
457
+ if (
458
+ (code >= 97 && code <= 122) ||
459
+ (code >= 65 && code <= 90) ||
460
+ (code >= 48 && code <= 57) ||
461
+ code === 95
462
+ ) {
463
+ this.pos++;
464
+ } else {
465
+ break;
466
+ }
467
+ }
468
+ this.col += this.pos - start;
469
+ return this.src.slice(start, this.pos);
419
470
  }
420
471
 
421
472
  // ── Semicolon insertion ──────────────────────────────────────
@@ -1,6 +1,8 @@
1
1
  // src/lower/boxing.js
2
2
  // Identifies address-taken scalar variables that require heap boxing ({ value: x }).
3
3
 
4
+ import { forEachChild } from "./walk.js";
5
+
4
6
  export function isReferenceType(t) {
5
7
  if (!t) return false;
6
8
  const base = t.kind === "named" ? t.underlying : t;
@@ -8,8 +10,7 @@ export function isReferenceType(t) {
8
10
  base?.kind === "struct" ||
9
11
  base?.kind === "slice" ||
10
12
  base?.kind === "map" ||
11
- base?.kind === "func" ||
12
- base?.kind === "interface"
13
+ base?.kind === "func"
13
14
  );
14
15
  }
15
16
 
@@ -28,9 +29,6 @@ export function scanAddressTaken(node, boxedVars = new Set()) {
28
29
  }
29
30
  }
30
31
  // Recurse into FuncLit too — closures may take address of outer vars
31
- for (const key of Object.keys(node)) {
32
- if (key.startsWith("_")) continue;
33
- scanAddressTaken(node[key], boxedVars);
34
- }
32
+ forEachChild(node, (child) => scanAddressTaken(child, boxedVars));
35
33
  return boxedVars;
36
34
  }
@@ -2,7 +2,8 @@
2
2
  // Identifies free variables captured by closures and determines which ones
3
3
  // are mutated and thus require heap-allocated environment cells for WASM closures.
4
4
 
5
- import { collectMutatedVars, nodeMutatesVar } from "./ownership.js";
5
+ import { collectMutatedVars } from "./ownership.js";
6
+ import { forEachChild } from "./walk.js";
6
7
 
7
8
  /**
8
9
  * Result of capture analysis on a function or closure.
@@ -41,17 +42,14 @@ export function collectDeclaredNames(node, names = new Set()) {
41
42
  // Do not descend into nested functions when collecting local declarations
42
43
  if (node.kind === "FuncLit") return names;
43
44
 
44
- for (const key of Object.keys(node)) {
45
- if (key.startsWith("_")) continue;
46
- collectDeclaredNames(node[key], names);
47
- }
45
+ forEachChild(node, (child) => collectDeclaredNames(child, names));
48
46
  return names;
49
47
  }
50
48
 
51
49
  /**
52
50
  * Finds all identifier references in a node that are not shadowed by inner declarations.
53
51
  */
54
- export function collectReferencedNames(node, localDecls, refs = new Set()) {
52
+ function collectReferencedNames(node, localDecls, refs = new Set()) {
55
53
  if (!node || typeof node !== "object") return refs;
56
54
  if (Array.isArray(node)) {
57
55
  for (const item of node) collectReferencedNames(item, localDecls, refs);
@@ -73,10 +71,9 @@ export function collectReferencedNames(node, localDecls, refs = new Set()) {
73
71
  if (r.name && r.name !== "_") innerLocals.add(r.name);
74
72
  }
75
73
  collectDeclaredNames(node.body, innerLocals);
76
- for (const key of Object.keys(node)) {
77
- if (key.startsWith("_")) continue;
78
- collectReferencedNames(node[key], innerLocals, refs);
79
- }
74
+ forEachChild(node, (child) =>
75
+ collectReferencedNames(child, innerLocals, refs),
76
+ );
80
77
  return refs;
81
78
  }
82
79
 
@@ -89,10 +86,9 @@ export function collectReferencedNames(node, localDecls, refs = new Set()) {
89
86
  }
90
87
 
91
88
  // For nested closures, free variables are also referenced
92
- for (const key of Object.keys(node)) {
93
- if (key.startsWith("_")) continue;
94
- collectReferencedNames(node[key], localDecls, refs);
95
- }
89
+ forEachChild(node, (child) =>
90
+ collectReferencedNames(child, localDecls, refs),
91
+ );
96
92
  return refs;
97
93
  }
98
94
 
@@ -155,11 +151,7 @@ export function analyzeCaptures(fnDecl) {
155
151
  result.capturesByClosure.set(node, captured);
156
152
 
157
153
  for (const varName of captured) {
158
- if (
159
- mutatedVars.has(varName) ||
160
- nodeMutatesVar(fnDecl.body, varName) ||
161
- nodeMutatesVar(node.body, varName)
162
- ) {
154
+ if (mutatedVars.has(varName)) {
163
155
  result.mutatedCaptures.add(varName);
164
156
  }
165
157
  }
@@ -168,10 +160,7 @@ export function analyzeCaptures(fnDecl) {
168
160
  return;
169
161
  }
170
162
 
171
- for (const key of Object.keys(node)) {
172
- if (key.startsWith("_")) continue;
173
- walkClosures(node[key], scopeStack);
174
- }
163
+ forEachChild(node, (child) => walkClosures(child, scopeStack));
175
164
  };
176
165
 
177
166
  walkClosures(fnDecl.body, [rootScope]);
@@ -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);