gofront 1.4.0 → 1.5.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 (61) hide show
  1. package/README.md +72 -9
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +21 -35
  5. package/src/{codegen → backend/js}/statements.js +43 -48
  6. package/src/backend/js/stdlib/bits.js +32 -0
  7. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  8. package/src/{codegen → backend/js}/templ.js +2 -2
  9. package/src/backend/wasm/boundary.js +1345 -0
  10. package/src/backend/wasm/emit.js +5779 -0
  11. package/src/backend/wasm/encode.js +789 -0
  12. package/src/backend/wasm/glue.js +130 -0
  13. package/src/backend/wasm/index.js +1413 -0
  14. package/src/backend/wasm/types.js +420 -0
  15. package/src/backend/wasm/wat.js +255 -0
  16. package/src/cli-core.js +52 -7
  17. package/src/compiler.js +322 -12
  18. package/src/dev-server.js +56 -15
  19. package/src/index.js +17 -9
  20. package/src/lexer.js +18 -0
  21. package/src/lower/boxing.js +36 -0
  22. package/src/lower/captures.js +179 -0
  23. package/src/lower/embedding.js +30 -0
  24. package/src/lower/escape.js +169 -0
  25. package/src/lower/functions.js +30 -0
  26. package/src/lower/globals.js +324 -0
  27. package/src/lower/index.js +115 -0
  28. package/src/lower/ownership.js +142 -0
  29. package/src/lower/range.js +70 -0
  30. package/src/minifier.js +2 -0
  31. package/src/parser/index.js +8 -3
  32. package/src/resolver.js +1 -0
  33. package/src/test-runner.js +74 -20
  34. package/src/typechecker/expressions.js +40 -1
  35. package/src/typechecker/index.js +234 -4
  36. package/src/typechecker/statements.js +7 -0
  37. package/src/typechecker/stdlib/core.js +17 -0
  38. package/src/typechecker/types.js +56 -2
  39. /package/src/{codegen → backend/js}/runtime.js +0 -0
  40. /package/src/{codegen → backend/js}/source-map.js +0 -0
  41. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  42. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/README.md CHANGED
@@ -4,8 +4,8 @@ Go for the backend: simple, type-safe, no nonsense. JavaScript for the frontend:
4
4
  everywhere, no setup. The problem is JavaScript's loose typing — and TypeScript never
5
5
  quite felt like home either.
6
6
 
7
- So I built GoFront. Go syntax and type safety, compiling to plain ES modules. One language
8
- front and back, no runtime, no framework, no tsconfig.json.
7
+ So I built GoFront. Go syntax and type safety, compiling to plain ES modules and
8
+ WebAssembly (WasmGC). One language front and back, no runtime, no framework, no tsconfig.json.
9
9
 
10
10
  With built-in support for declarative DOM rendering via the [`gom`](https://www.gomponents.com) standard library, JSX-like [`.templ`](https://templ.guide/) files, and seamless integration with external JavaScript libraries through TypeScript definition files (`.d.ts`), GoFront is designed specifically as a frontend development target. Build complex, reactive user interfaces entirely in Go.
11
11
 
@@ -54,15 +54,16 @@ Requires Node.js 20+.
54
54
 
55
55
  ## How it works
56
56
 
57
- GoFront is a four-stage compiler written in pure Node.js (no dependencies). Every stage
58
- operates on the same AST (abstract syntax tree), running in a single pass per stage:
57
+ GoFront is a five-stage compiler written in pure Node.js (no dependencies). Every stage
58
+ operates on the AST (abstract syntax tree):
59
59
 
60
60
  ```
61
61
  source text (.go files)
62
62
  → Lexer tokenize + Go-style semicolon insertion
63
63
  → Parser recursive-descent → AST
64
64
  → Type Checker annotate AST with types + collect errors
65
- → Code Gen AST → JavaScript string
65
+ → Lowering ownership/clone elision, escape analysis, capture analysis
66
+ → Code Gen AST → JavaScript ES module or WebAssembly GC module (.wasm)
66
67
 
67
68
  source text (.templ files)
68
69
  → TemplLexer dual-mode: Go mode for declarations, HTML mode inside templ bodies
@@ -103,7 +104,7 @@ Types are plain JS objects (`{ kind: "basic", name: "int" }`, `{ kind: "slice",
103
104
  elem: ... }`, etc.). The special `any` type acts as a recovery/escape hatch — any
104
105
  operation on it is silently permitted, preventing cascading errors.
105
106
 
106
- ### 4. Code Generator (`src/codegen/`)
107
+ ### 4. Code Generator (`src/backend/js/`)
107
108
 
108
109
  Walks the typed AST and emits clean, readable JavaScript. No intermediate representation
109
110
  — the codegen writes directly to an output buffer with indentation tracking.
@@ -113,6 +114,66 @@ Runtime helpers (`__len`, `__append`, `__s`, `__sprintf`, `__equal`, `__cmul`, `
113
114
  `__sclone`, `__ifv`, `__ifp`) are tree-shaken: only emitted when actually used. Optional
114
115
  inline source maps are supported via VLQ-encoded mappings.
115
116
 
117
+ ### 5. Lowering & WASM backend (`src/lower/`, `src/backend/wasm/`) — experimental
118
+
119
+ `src/lower/` runs analyses shared by both backends (ownership/clone elision, address-taken
120
+ boxing, range shape, named returns/`defer`, embedded-method stubs, closure captures, pointer
121
+ escape) and stores the results in side tables keyed by AST node.
122
+
123
+ A package that starts with `//gofront:target wasm` (before the `package` clause) is compiled
124
+ by `src/backend/wasm/` to a WebAssembly GC module instead of JavaScript.
125
+ `//gofront:target both` marks a package that must compile under either backend: it may only
126
+ import other `both` packages, may not use `gom`, may not mutate package-level variables, and
127
+ its JS output uses strict Go numeric semantics (sized-integer wrapping, `float32` rounding,
128
+ integer divide-by-zero panics). Running WASM output requires a WasmGC-capable runtime
129
+ (Node ≥ 22 or a current Chrome/Firefox/Safari).
130
+
131
+ **Hybrid builds.** Every `wasm`/`both` package a build reaches is linked into a single
132
+ `app.wasm`, written next to `app.js` by `gofront build`, `gofront dev` and `-o`. The JS
133
+ bundle contains a generated facade in place of the wasm packages: exported functions,
134
+ methods, literal constants and structs are callable from JS code as if they were compiled
135
+ to JS. Primitives convert directly, strings and `any` pass through, `both` struct values
136
+ are copied into their JS class, `wasm` structs become opaque handle classes with stable
137
+ identity, slices/arrays are copied element-wise (TypedArrays accepted) and func values are
138
+ wrapped in both directions. Maps, `error`, non-empty interfaces and pointers to non-structs
139
+ are not yet supported at the boundary and are rejected at compile time. The loader fetches
140
+ `app.wasm` relative to the page (override with `globalThis.__GOFRONT_WASM_URL`, or
141
+ pre-supply bytes via `globalThis.__GOFRONT_WASM_BYTES`). `--emit-wat` additionally writes
142
+ a textual `app.wat`.
143
+
144
+ ```
145
+ GoFront Source Code
146
+ ┌───────────────────────┬───────────────────────┐
147
+ │ //gofront:target js │ //gofront:target wasm │
148
+ │ UI, DOM, WebGL, Game │ Collision, Raycasting │
149
+ └───────────┬───────────┴───────────┬───────────┘
150
+ │ │
151
+ │ GoFront Compiler │
152
+ ▼ ▼
153
+ ┌───────────┐ ┌───────────┐
154
+ │ app.js │ ◄───────► │ app.wasm │
155
+ └─────┬─────┘ Boundary └─────┬─────┘
156
+ │ Facade │
157
+ ▼ ▼
158
+ DOM & Browser Tight Loops & Math
159
+ Dynamic & Ergonomic Predictable & Fast
160
+ ```
161
+
162
+ `gofront test` runs the tests of a `wasm` package inside the linked module (`*testing.T`
163
+ stays a JS object) and runs the tests of a `both` package twice — once per backend,
164
+ reported as `pkg [js]` and `pkg [wasm]` — so both must agree.
165
+
166
+ **Performance.** Splitting an app into high-level JavaScript orchestration and low-level WebAssembly compute delivers the best of both worlds. On a real-world Möller–Trumbore raycast benchmark (131,072 triangles, 100,000 rays; `npm run bench:raycast`):
167
+
168
+ | Target | Throughput | Allocation | Engine Stability |
169
+ | :--- | :--- | :--- | :--- |
170
+ | **Pure JS** | `26,720 rays/s` ▰▰▰▰▰▰▰▰▱▱ | `6.20 B / ray` | Subject to periodic V8 young-gen GC pauses |
171
+ | **Hybrid WASM** | `31,293 rays/s` ▰▰▰▰▰▰▰▰▰▰ | `0.86 B / ray` | **Near-zero alloc (7.2× less)**, smooth 60 FPS |
172
+
173
+ - **+17.1% higher throughput:** Direct WasmGC typed arrays, local-cached scratch globals, and hardware-trapped nil dereferences outperform JIT compiled JS.
174
+ - **86% memory churn reduction:** Dropping allocations from 6.2 B/ray to 0.86 B/ray prevents garbage collection pauses from causing micro-stutter in 60 FPS loops.
175
+ - **Minimal boundary overhead:** The boundary trampoline consumes only ~0.5% of total runtime, ensuring batch computations cross between JS and WASM with virtually zero penalty.
176
+
116
177
  ---
117
178
 
118
179
  ## Go → JavaScript mapping
@@ -412,13 +473,14 @@ npm run build:webgl # → example/webgl/app.js
412
473
  ## CLI
413
474
 
414
475
  ```
415
- gofront dev [dir] watch + compile + asset sync + live reload (default port 3000)
476
+ gofront dev [dir] watch + compile + asset sync + live reload (hybrid projects emit app.wasm; default port 3000)
416
477
  gofront dev [dir] --port 8080 use a custom port
417
- gofront build [dir] clean + compile + minify + vendor → production output
478
+ gofront build [dir] clean + compile + minify + vendor → production output (hybrid projects emit app.wasm)
418
479
  gofront build [dir] --pwa also generate offline service worker (sw.js) + precache manifest
419
480
  gofront build [dir] --source-map include inline source maps in the release bundle
420
481
  gofront build [dir] --no-minify skip minification
421
482
  gofront build [dir] --no-mangle minify but keep original identifiers
483
+ gofront build [dir] --emit-wat also write app.wat next to app.wasm (hybrid projects)
422
484
  gofront prep [dir] [--minify] run asset copying + vendor bundling only (alias: gofront vendor)
423
485
  gofront check <dir> type-check a single package
424
486
  gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
@@ -426,7 +488,8 @@ gofront test <dir> [--dom] run tests for a single package
426
488
  gofront test <dir>/... [--dom] [-v] [-run <regex>] run tests recursively
427
489
  gofront <file.go> compile single file → stdout
428
490
  gofront <dir> compile all *.go in directory → stdout
429
- gofront <input> -o out.js write output to file (prints elapsed compile time)
491
+ gofront <input> -o out.js write output to file (prints elapsed compile time; hybrid projects also write app.wasm)
492
+ gofront <input> -o out.js --emit-wat also write app.wat
430
493
  gofront <input> -o out.js --copy-assets compile + copy static assets
431
494
  gofront <input> --check type-check only (single file / directory)
432
495
  gofront <input> --watch watch for changes and recompile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  "test:all": "npm run test:unit && npm run test:perf && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
42
42
  "test:unit": "node test/unit/run.js",
43
43
  "test:perf": "node test/e2e/perf/zero-alloc.js",
44
+ "bench:raycast": "node --expose-gc test/e2e/perf/raycast-bench.js",
44
45
  "test:examples": "node src/index.js test example/simple/src && node src/index.js test example/simple/src/utils && node src/index.js test example/reactive/src && node src/index.js test example/reactive/src/utils && node src/index.js test example/gom/src && node src/index.js test example/gom/src/utils && node src/index.js test example/templ/src && node src/index.js test example/templ/src/utils",
45
46
  "test:examples:dom": "node src/index.js test --dom example/simple/src && node src/index.js test --dom example/gom/src && node src/index.js test --dom example/templ/src",
46
47
  "test:coverage": "c8 --include='src/**' --reporter=text --reporter=lcov node test/unit/run.js",
@@ -1,12 +1,78 @@
1
+ import {
2
+ closureMutates,
3
+ fnMutates,
4
+ nodeMutatesVar,
5
+ nodeWritesVar,
6
+ rootIdentName,
7
+ } from "../../lower/index.js";
1
8
  import {
2
9
  ERROR,
3
10
  isComplex,
4
11
  isTypedArraySlice,
5
12
  typedArrayConstructorForElem,
6
- } from "../typechecker/types.js";
13
+ } from "../../typechecker/types.js";
7
14
 
8
15
  /** @typedef {import('./index.js').CodeGen} CodeGen */
9
16
 
17
+ export const STRICT_NUMERIC_TYPES = new Set([
18
+ "float32",
19
+ "int8",
20
+ "int16",
21
+ "int32",
22
+ "rune",
23
+ "uint8",
24
+ "byte",
25
+ "uint16",
26
+ "uint32",
27
+ ]);
28
+
29
+ export const STRICT_OPS = new Set([
30
+ "+",
31
+ "-",
32
+ "*",
33
+ "/",
34
+ "%",
35
+ "&",
36
+ "|",
37
+ "^",
38
+ "&^",
39
+ "<<",
40
+ ">>",
41
+ ]);
42
+
43
+ // Splits a generated JS lvalue at its outermost trailing access:
44
+ // `a.b[i]` → { obj: "a.b", key: "i" }, `a.b.c` → { obj: "a.b", prop: "c" }.
45
+ function splitLastAccess(js) {
46
+ if (js.endsWith("]")) {
47
+ let depth = 0;
48
+ let quote = null;
49
+ for (let i = js.length - 1; i >= 0; i--) {
50
+ const ch = js[i];
51
+ if (quote) {
52
+ if (ch === quote && js[i - 1] !== "\\") quote = null;
53
+ continue;
54
+ }
55
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
56
+ else if (ch === "]" || ch === ")") depth++;
57
+ else if (ch === "[" || ch === "(") {
58
+ depth--;
59
+ if (depth === 0 && ch === "[" && i > 0)
60
+ return { obj: js.slice(0, i), key: js.slice(i + 1, -1) };
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ const m = /^(.*[^.])\.([A-Za-z_$][\w$]*)$/s.exec(js);
66
+ if (!m) return null;
67
+ // Only split when the dot is at nesting depth 0, e.g. not inside `f(a.b)`.
68
+ let depth = 0;
69
+ for (const ch of m[1]) {
70
+ if (ch === "(" || ch === "[") depth++;
71
+ else if (ch === ")" || ch === "]") depth--;
72
+ }
73
+ return depth === 0 ? { obj: m[1], prop: m[2] } : null;
74
+ }
75
+
10
76
  // Namespace constants: pkg.Field → JS literal
11
77
  const NS_CONSTANTS = {
12
78
  math: {
@@ -144,7 +210,7 @@ const TYPEOF_NUMBER_NAMES = new Set([...INT_TYPE_NAMES, "float32", "float64"]);
144
210
  const BUILTIN_GEN = {
145
211
  append: (s, e) => s.genAppend(e),
146
212
  len: (s, e) => s._genBuiltinLen(e),
147
- cap: (s, e) => `${s.genExpr(e.args[0])}.length`,
213
+ cap: (s, e) => `(${s.genExpr(e.args[0])}?.length ?? 0)`,
148
214
  make: (s, e) => s.genMake(e),
149
215
  delete: (s, e) => {
150
216
  const [m, k] = e.args.map((a) => s.genExpr(a));
@@ -335,60 +401,21 @@ export const expressionGenMethods = {
335
401
  },
336
402
 
337
403
  _rootIdentName(e) {
338
- while (e) {
339
- if (e.kind === "Ident") return e.name;
340
- if (e.kind === "SelectorExpr" || e.kind === "IndexExpr") e = e.expr;
341
- else if (e.kind === "UnaryExpr" && e.op === "*") e = e.operand;
342
- else return null;
343
- }
344
- return null;
404
+ return rootIdentName(e);
345
405
  },
346
406
 
347
407
  // True when `node` itself (not its children) writes to variable `name`.
348
408
  _nodeWritesVar(node, name) {
349
- const root = (e) => this._rootIdentName(e) === name;
350
- switch (node.kind) {
351
- case "AssignStmt":
352
- return node.lhs.some(root);
353
- case "DefineStmt":
354
- return node.lhs.some((e) => e._redecl && e.name === name);
355
- case "IncDecStmt":
356
- return root(node.expr);
357
- case "SelectorExpr":
358
- return Boolean(
359
- node._isMethodValue && node._type?._ptrRecv && root(node.expr),
360
- );
361
- default:
362
- return false;
363
- }
409
+ return nodeWritesVar(node, name);
364
410
  },
365
411
 
366
412
  // True when `node` may modify (or take the address of) the value held by variable `name`.
367
413
  _nodeMutatesVar(node, name, addrOnly = false) {
368
- if (!node || typeof node !== "object") return false;
369
- if (Array.isArray(node))
370
- return node.some((n) => this._nodeMutatesVar(n, name, addrOnly));
371
- if (
372
- node.kind === "UnaryExpr" &&
373
- node.op === "&" &&
374
- this._rootIdentName(node.operand) === name
375
- )
376
- return true;
377
- if (!addrOnly && this._nodeWritesVar(node, name)) return true;
378
- for (const key of Object.keys(node)) {
379
- if (key.startsWith("_")) continue;
380
- if (this._nodeMutatesVar(node[key], name, addrOnly)) return true;
381
- }
382
- return false;
414
+ return nodeMutatesVar(node, name, addrOnly);
383
415
  },
384
416
 
385
417
  _fnMutates(name, addrOnly = false) {
386
- const ctx = this._fnCtx;
387
- if (!ctx) return true;
388
- const key = `${addrOnly ? "&" : ""}${name}`;
389
- if (!ctx.mut.has(key))
390
- ctx.mut.set(key, this._nodeMutatesVar(ctx.body, name, addrOnly));
391
- return ctx.mut.get(key);
418
+ return fnMutates(this._fnCtx?.body, name, addrOnly, this._fnCtx?.mut);
392
419
  },
393
420
 
394
421
  _withFnCtx(body, fn) {
@@ -447,21 +474,7 @@ export const expressionGenMethods = {
447
474
  },
448
475
 
449
476
  _closureMutates(name) {
450
- const ctx = this._fnCtx;
451
- const key = `λ${name}`;
452
- if (!ctx.mut.has(key)) {
453
- const walk = (node) => {
454
- if (!node || typeof node !== "object") return false;
455
- if (Array.isArray(node)) return node.some(walk);
456
- if (node.kind === "FuncLit")
457
- return this._nodeMutatesVar(node.body, name);
458
- return Object.keys(node).some(
459
- (k) => !k.startsWith("_") && walk(node[k]),
460
- );
461
- };
462
- ctx.mut.set(key, walk(ctx.body));
463
- }
464
- return ctx.mut.get(key);
477
+ return closureMutates(this._fnCtx?.body, name, this._fnCtx?.mut);
465
478
  },
466
479
 
467
480
  _genUnaryExpr(expr) {
@@ -1253,6 +1266,12 @@ export const expressionGenMethods = {
1253
1266
  return expr.op === "==" ? cmp : `!${cmp}`;
1254
1267
  }
1255
1268
 
1269
+ if (this.strict && this._needsStrictOp(expr.op, expr._type)) {
1270
+ const l = this.genExpr(expr.left);
1271
+ const r = this.genExpr(expr.right);
1272
+ return this._genStrictBinary(expr.op, l, r, expr._type);
1273
+ }
1274
+
1256
1275
  const jsOp = this._emittedBinaryOp(expr) ?? "/";
1257
1276
  const operand = (child, isRight) =>
1258
1277
  wrapForJsOp(
@@ -1422,4 +1441,142 @@ export const expressionGenMethods = {
1422
1441
  return inner;
1423
1442
  }
1424
1443
  },
1444
+
1445
+ _typeName(t) {
1446
+ if (!t) return null;
1447
+ if (t.kind === "untyped") return t.base;
1448
+ if (t.kind === "named") return this._typeName(t.underlying);
1449
+ if (t.kind === "basic") return t.name;
1450
+ return null;
1451
+ },
1452
+
1453
+ _isStrictAtomicOp(_op, typeName) {
1454
+ return STRICT_NUMERIC_TYPES.has(typeName);
1455
+ },
1456
+
1457
+ _needsStrictOp(op, type) {
1458
+ if (!STRICT_OPS.has(op)) return false;
1459
+ if (this._isStrictAtomicOp(op, this._typeName(type))) return true;
1460
+ return this.isIntType(type) && (op === "/" || op === "%");
1461
+ },
1462
+
1463
+ // Emits `lhs = strict(lhs op rhs)` for any lvalue shape; returns false when not applicable.
1464
+ // Indexed / nested targets are split into object + key temps so sub-expressions run once.
1465
+ _genStrictCompound(lhsNode, op, rhsJs, srcLine = null, lhsJs = null) {
1466
+ const type = lhsNode._type;
1467
+ if (!this._needsStrictOp(op, type)) return false;
1468
+ if (lhsNode.kind === "IndexExpr" && lhsNode.expr?._type?.kind === "map")
1469
+ return false;
1470
+ const l = lhsJs ?? this._genAssignLhsExpr(lhsNode);
1471
+ if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(l)) {
1472
+ this.line(
1473
+ `${l} = ${this._genStrictBinary(op, l, rhsJs, type)};`,
1474
+ srcLine,
1475
+ );
1476
+ return true;
1477
+ }
1478
+ const split = splitLastAccess(l);
1479
+ if (!split) return false;
1480
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
1481
+ const o = `__o${this._tmpCounter}`;
1482
+ if (split.key != null) {
1483
+ const k = `__k${this._tmpCounter}`;
1484
+ const target = `${o}[${k}]`;
1485
+ this.line(
1486
+ `{ const ${o} = ${split.obj}, ${k} = ${split.key}; ${target} = ${this._genStrictBinary(op, target, rhsJs, type)}; }`,
1487
+ srcLine,
1488
+ );
1489
+ } else {
1490
+ const target = `${o}.${split.prop}`;
1491
+ this.line(
1492
+ `{ const ${o} = ${split.obj}; ${target} = ${this._genStrictBinary(op, target, rhsJs, type)}; }`,
1493
+ srcLine,
1494
+ );
1495
+ }
1496
+ return true;
1497
+ },
1498
+
1499
+ _genStrictBinary(op, l, r, type) {
1500
+ const tName = this._typeName(type);
1501
+ if (tName === "float32") {
1502
+ return `Math.fround(${l} ${op} ${r})`;
1503
+ }
1504
+ if (tName === "int32" || tName === "rune") {
1505
+ if (op === "*") return `Math.imul(${l}, ${r})`;
1506
+ if (op === "/")
1507
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (Math.trunc((${l}) / (${r})) | 0))`;
1508
+ if (op === "%")
1509
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((${l}) % (${r})) | 0))`;
1510
+ if (op === "<<" || op === ">>") {
1511
+ return `((${r}) >= 32 ? (${op === ">>" ? `((${l}) < 0 ? -1 : 0)` : "0"}) : ((${l}) ${op} (${r}))) | 0`;
1512
+ }
1513
+ if (op === "&^") return `((${l}) & ~(${r})) | 0`;
1514
+ return `((${l} ${op} ${r}) | 0)`;
1515
+ }
1516
+ if (tName === "uint32") {
1517
+ if (op === "/")
1518
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (Math.trunc((${l}) / (${r})) >>> 0))`;
1519
+ if (op === "%")
1520
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((${l}) % (${r})) >>> 0))`;
1521
+ if (op === "<<" || op === ">>") {
1522
+ const jsOp = op === ">>" ? ">>>" : "<<";
1523
+ return `((${r}) >= 32 ? 0 : ((${l}) ${jsOp} (${r}))) >>> 0`;
1524
+ }
1525
+ if (op === "&^") return `((${l}) & ~(${r})) >>> 0`;
1526
+ return `((${l} ${op} ${r}) >>> 0)`;
1527
+ }
1528
+ if (tName === "int8") {
1529
+ if (op === "/")
1530
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : ((Math.trunc((${l}) / (${r})) << 24) >> 24))`;
1531
+ if (op === "%")
1532
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((((${l}) % (${r})) << 24) >> 24)))`;
1533
+ if (op === "<<" || op === ">>") {
1534
+ return `((${r}) >= 8 ? (${op === ">>" ? `((${l}) < 0 ? -1 : 0)` : "0"}) : (((${l}) ${op} (${r})) << 24 >> 24))`;
1535
+ }
1536
+ if (op === "&^") return `((((${l}) & ~(${r})) << 24) >> 24)`;
1537
+ return `((((${l} ${op} ${r})) << 24) >> 24)`;
1538
+ }
1539
+ if (tName === "int16") {
1540
+ if (op === "/")
1541
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : ((Math.trunc((${l}) / (${r})) << 16) >> 16))`;
1542
+ if (op === "%")
1543
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((((${l}) % (${r})) << 16) >> 16)))`;
1544
+ if (op === "<<" || op === ">>") {
1545
+ return `((${r}) >= 16 ? (${op === ">>" ? `((${l}) < 0 ? -1 : 0)` : "0"}) : (((${l}) ${op} (${r})) << 16 >> 16))`;
1546
+ }
1547
+ if (op === "&^") return `((((${l}) & ~(${r})) << 16) >> 16)`;
1548
+ return `((((${l} ${op} ${r})) << 16) >> 16)`;
1549
+ }
1550
+ if (tName === "uint8" || tName === "byte") {
1551
+ if (op === "/")
1552
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (Math.trunc((${l}) / (${r})) & 0xFF))`;
1553
+ if (op === "%")
1554
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((${l}) % (${r})) & 0xFF))`;
1555
+ if (op === "<<" || op === ">>") {
1556
+ const jsOp = op === ">>" ? ">>>" : "<<";
1557
+ return `((${r}) >= 8 ? 0 : (((${l}) ${jsOp} (${r})) & 0xFF))`;
1558
+ }
1559
+ if (op === "&^") return `(((${l}) & ~(${r})) & 0xFF)`;
1560
+ return `((${l} ${op} ${r}) & 0xFF)`;
1561
+ }
1562
+ if (tName === "uint16") {
1563
+ if (op === "/")
1564
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (Math.trunc((${l}) / (${r})) & 0xFFFF))`;
1565
+ if (op === "%")
1566
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : (((${l}) % (${r})) & 0xFFFF))`;
1567
+ if (op === "<<" || op === ">>") {
1568
+ const jsOp = op === ">>" ? ">>>" : "<<";
1569
+ return `((${r}) >= 16 ? 0 : (((${l}) ${jsOp} (${r})) & 0xFFFF))`;
1570
+ }
1571
+ if (op === "&^") return `(((${l}) & ~(${r})) & 0xFFFF)`;
1572
+ return `((${l} ${op} ${r}) & 0xFFFF)`;
1573
+ }
1574
+ if (this.isIntType(type)) {
1575
+ if (op === "/")
1576
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : Math.trunc((${l}) / (${r})))`;
1577
+ if (op === "%")
1578
+ return `((${r}) === 0 ? (() => { throw new Error("runtime error: integer divide by zero"); })() : ((${l}) % (${r})))`;
1579
+ }
1580
+ return `${l} ${op} ${r}`;
1581
+ },
1425
1582
  };
@@ -1,6 +1,6 @@
1
1
  // Code generator: walks the typed AST and emits clean JavaScript.
2
2
  //
3
- // Split into sub-modules under codegen/:
3
+ // Split into sub-modules under backend/js/:
4
4
  // source-map.js — VLQ encoder and source map builder
5
5
  // statements.js — genBlock, genStmt, genFor, genSwitch, etc.
6
6
  // expressions.js — genExpr, genCall, genCompositeLit, helpers
@@ -17,7 +17,12 @@
17
17
  // - make(map[K]V) → {}
18
18
  // - for range → for...of with .entries()
19
19
 
20
- import { isComplex, isNumeric } from "../typechecker/types.js";
20
+ import {
21
+ computeEmbeddedStubs,
22
+ isReferenceType,
23
+ scanAddressTaken,
24
+ } from "../../lower/index.js";
25
+ import { isComplex, isNumeric } from "../../typechecker/types.js";
21
26
  import { expressionGenMethods } from "./expressions.js";
22
27
  import {
23
28
  HELPER_APPEND,
@@ -43,6 +48,8 @@ import { statementGenMethods } from "./statements.js";
43
48
  import { stdlibGenMethods } from "./stdlib/index.js";
44
49
  import { templGenMethods } from "./templ.js";
45
50
 
51
+ export { buildSourceMap };
52
+
46
53
  // Valid Go identifiers that cannot be used as JS bindings; emitted with a `$` suffix.
47
54
  const JS_RESERVED = new Set([
48
55
  "arguments",
@@ -91,8 +98,11 @@ export class CodeGen {
91
98
  checker = null,
92
99
  jsImports = new Map(),
93
100
  bundledPackages = new Set(),
101
+ options = {},
94
102
  ) {
95
103
  this.checker = checker;
104
+ this.target = options.target ?? "js";
105
+ this.strict = Boolean(options.strict || this.target === "both");
96
106
  this.out = [];
97
107
  this.indent = 0;
98
108
  this.structNames = new Set();
@@ -621,12 +631,13 @@ export class CodeGen {
621
631
  }
622
632
 
623
633
  _genEmbeddedMethodStubs(name, methodDecls) {
624
- if (!this.checker) return;
625
- const resolvedType = this.checker.types.get(name)?.underlying;
626
- if (resolvedType?.kind !== "struct" || !resolvedType._embeds) return;
627
- const declared = new Set(methodDecls.map((m) => m.name));
628
- for (const embed of resolvedType._embeds)
629
- this._genSingleEmbedStubs(embed, declared);
634
+ const stubs = computeEmbeddedStubs(name, methodDecls, this.checker);
635
+ for (const stub of stubs) {
636
+ this.blank();
637
+ this.line(
638
+ `${stub.methodName}(...__a) { return ${stub.embedName}.prototype.${stub.methodName}.call(this, ...__a); }`,
639
+ );
640
+ }
630
641
  }
631
642
 
632
643
  genMethod(decl, recvField = null) {
@@ -767,36 +778,11 @@ export class CodeGen {
767
778
 
768
779
  // Scan AST node for _addressTaken idents on scalars and populate _boxedVars.
769
780
  _scanAddressTaken(node) {
770
- if (!node || typeof node !== "object") return;
771
- if (Array.isArray(node)) {
772
- for (const child of node) this._scanAddressTaken(child);
773
- return;
774
- }
775
- // &x — the operand ident will have _addressTaken set by typechecker
776
- if (node.kind === "Ident" && node._addressTaken) {
777
- // Check if the type is a scalar (needs boxing) vs reference type (no boxing)
778
- const t = node._type;
779
- if (t && !this._isReferenceType(t)) {
780
- this._boxedVars.add(node.name);
781
- }
782
- }
783
- // Recurse into FuncLit too — closures may take address of outer vars
784
- for (const key of Object.keys(node)) {
785
- if (key.startsWith("_")) continue;
786
- this._scanAddressTaken(node[key]);
787
- }
781
+ scanAddressTaken(node, this._boxedVars);
788
782
  }
789
783
 
790
784
  _isReferenceType(t) {
791
- if (!t) return false;
792
- const base = t.kind === "named" ? t.underlying : t;
793
- return (
794
- base?.kind === "struct" ||
795
- base?.kind === "slice" ||
796
- base?.kind === "map" ||
797
- base?.kind === "func" ||
798
- base?.kind === "interface"
799
- );
785
+ return isReferenceType(t);
800
786
  }
801
787
 
802
788
  // ── Variable / const declarations ────────────────────────────