gofront 1.3.12 → 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 (63) hide show
  1. package/README.md +90 -20
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +47 -36
  5. package/src/{codegen → backend/js}/runtime.js +11 -0
  6. package/src/{codegen → backend/js}/statements.js +43 -48
  7. package/src/backend/js/stdlib/bits.js +32 -0
  8. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  9. package/src/{codegen → backend/js}/templ.js +31 -2
  10. package/src/backend/wasm/boundary.js +1345 -0
  11. package/src/backend/wasm/emit.js +5779 -0
  12. package/src/backend/wasm/encode.js +789 -0
  13. package/src/backend/wasm/glue.js +130 -0
  14. package/src/backend/wasm/index.js +1413 -0
  15. package/src/backend/wasm/types.js +420 -0
  16. package/src/backend/wasm/wat.js +255 -0
  17. package/src/cli-core.js +52 -7
  18. package/src/compiler.js +328 -12
  19. package/src/dev-server.js +56 -15
  20. package/src/index.js +17 -9
  21. package/src/lexer.js +18 -0
  22. package/src/lower/boxing.js +36 -0
  23. package/src/lower/captures.js +179 -0
  24. package/src/lower/embedding.js +30 -0
  25. package/src/lower/escape.js +169 -0
  26. package/src/lower/functions.js +30 -0
  27. package/src/lower/globals.js +324 -0
  28. package/src/lower/index.js +115 -0
  29. package/src/lower/ownership.js +142 -0
  30. package/src/lower/range.js +70 -0
  31. package/src/minifier.js +2 -0
  32. package/src/parser/index.js +8 -3
  33. package/src/resolver.js +4 -1
  34. package/src/templ-lexer.js +107 -3
  35. package/src/templ-parser.js +29 -0
  36. package/src/test-runner.js +74 -20
  37. package/src/typechecker/expressions.js +40 -1
  38. package/src/typechecker/index.js +301 -6
  39. package/src/typechecker/statements.js +7 -0
  40. package/src/typechecker/stdlib/core.js +17 -0
  41. package/src/typechecker/types.js +56 -2
  42. /package/src/{codegen → backend/js}/source-map.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  62. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  63. /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
@@ -283,6 +344,12 @@ example/templ/
283
344
  `.templ` syntax at a glance:
284
345
 
285
346
  ```go
347
+ css cardStyle() {
348
+ background: #ffffff;
349
+ border-radius: 8px;
350
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
351
+ }
352
+
286
353
  templ TodoItem(t Todo) {
287
354
  <li class={ todoItemClass(t) } draggable="true" data-id={ t.id }>
288
355
  <input type="checkbox" checked?={ t.done }/>
@@ -294,7 +361,7 @@ templ TodoItem(t Todo) {
294
361
  }
295
362
 
296
363
  templ AppView() {
297
- <div class="card">
364
+ <div class={ cardStyle() }>
298
365
  @Header(highCount(), syncMsg, syncCls)
299
366
  @TodoList(visibleTodos())
300
367
  </div>
@@ -302,6 +369,7 @@ templ AppView() {
302
369
  ```
303
370
 
304
371
  Key syntax features:
372
+ - `css Name() { ... }` — scoped CSS declarations (templ.guide spec); compiles to a deterministic scoped class name (`gfc_<name>_<hash>`) and injects styles automatically into `<head>`
305
373
  - `{ expr }` — interpolate any Go expression (strings auto-cast, others use `String()`)
306
374
  - `attr={ expr }` — dynamic attribute value (expression)
307
375
  - `attr?={ expr }` — boolean attribute (present/absent based on truthiness)
@@ -369,8 +437,8 @@ parity with the other examples (priority mode, validation, localStorage persiste
369
437
  sync status, drag-and-drop reordering).
370
438
 
371
439
  The templ example additionally demonstrates: **`.templ` file compilation**, template
372
- components with parameters, `{ expr }` interpolation, `attr?={}` conditional boolean
373
- attributes, `@Component()` calls inside templates, `if / else if / else` chains,
440
+ components with parameters, scoped `css` declarations (`css Name() { ... }`), `{ expr }` interpolation,
441
+ `attr?={}` conditional boolean attributes, `@Component()` calls inside templates, `if / else if / else` chains,
374
442
  `switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
375
443
  all inside template bodies.
376
444
 
@@ -405,13 +473,14 @@ npm run build:webgl # → example/webgl/app.js
405
473
  ## CLI
406
474
 
407
475
  ```
408
- 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)
409
477
  gofront dev [dir] --port 8080 use a custom port
410
- gofront build [dir] clean + compile + minify + vendor → production output
478
+ gofront build [dir] clean + compile + minify + vendor → production output (hybrid projects emit app.wasm)
411
479
  gofront build [dir] --pwa also generate offline service worker (sw.js) + precache manifest
412
480
  gofront build [dir] --source-map include inline source maps in the release bundle
413
481
  gofront build [dir] --no-minify skip minification
414
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)
415
484
  gofront prep [dir] [--minify] run asset copying + vendor bundling only (alias: gofront vendor)
416
485
  gofront check <dir> type-check a single package
417
486
  gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
@@ -419,7 +488,8 @@ gofront test <dir> [--dom] run tests for a single package
419
488
  gofront test <dir>/... [--dom] [-v] [-run <regex>] run tests recursively
420
489
  gofront <file.go> compile single file → stdout
421
490
  gofront <dir> compile all *.go in directory → stdout
422
- 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
423
493
  gofront <input> -o out.js --copy-assets compile + copy static assets
424
494
  gofront <input> --check type-check only (single file / directory)
425
495
  gofront <input> --watch watch for changes and recompile
@@ -620,7 +690,7 @@ signatures into GoFront's internal type representation.
620
690
  | Unused import detection | ✓ |
621
691
  | External `.d.ts` types | ✓ |
622
692
  | npm package type resolution | ✓ |
623
- | `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components visible across the whole package |
693
+ | `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components and `css` declarations visible across the whole package |
624
694
 
625
695
  ---
626
696
 
@@ -694,18 +764,18 @@ npm run test:e2e # E2E tests (Playwright, headless Chromium)
694
764
  npm run test:all # all of the above
695
765
  ```
696
766
 
697
- **Unit tests** (~1,396) cover language features, type errors, edge cases, DOM (jsdom),
767
+ **Unit tests** (~1,400+) cover language features, type errors, edge cases, DOM (jsdom),
698
768
  external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
699
769
  formatting, map iteration order, integer overflow semantics, unused variable detection,
700
770
  unused import detection, semantic difference verification, stdlib shim packages, generics,
701
771
  the `testing` framework itself, and `.templ` file compilation (element rendering,
702
- interpolation, boolean attrs, component calls, `if/else/else-if` chains, `for range`,
703
- `switch/case/default`, `@templ.Raw()` raw HTML injection, SVG namespace handling, mixed
704
- `.go`+`.templ` packages).
772
+ interpolation, boolean attrs, component calls, scoped `css` declarations with class hashing,
773
+ `if/else/else-if` chains, `for range`, `switch/case/default`, `@templ.Raw()` raw HTML injection,
774
+ SVG namespace handling, mixed `.go`+`.templ` packages).
705
775
 
706
- **E2E tests** (~104, Playwright) run all five example apps in a real browser and verify
776
+ **E2E tests** (~105, Playwright) run all five example apps in a real browser and verify
707
777
  CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
708
778
  status. Per-app suites check app-specific behaviour: scoped styles, stats bar, loading
709
- placeholder, `gom.If` conditional rendering, templ-specific features (`if/else`
710
- priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
779
+ placeholder, `gom.If` conditional rendering, templ-specific features (scoped `css` injection,
780
+ `if/else` priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
711
781
  rendering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.12",
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
  };