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.
- package/README.md +90 -20
- package/package.json +2 -1
- package/src/{codegen → backend/js}/expressions.js +217 -60
- package/src/{codegen → backend/js}/index.js +47 -36
- package/src/{codegen → backend/js}/runtime.js +11 -0
- package/src/{codegen → backend/js}/statements.js +43 -48
- package/src/backend/js/stdlib/bits.js +32 -0
- package/src/{codegen → backend/js}/stdlib/index.js +3 -0
- package/src/{codegen → backend/js}/templ.js +31 -2
- package/src/backend/wasm/boundary.js +1345 -0
- package/src/backend/wasm/emit.js +5779 -0
- package/src/backend/wasm/encode.js +789 -0
- package/src/backend/wasm/glue.js +130 -0
- package/src/backend/wasm/index.js +1413 -0
- package/src/backend/wasm/types.js +420 -0
- package/src/backend/wasm/wat.js +255 -0
- package/src/cli-core.js +52 -7
- package/src/compiler.js +328 -12
- package/src/dev-server.js +56 -15
- package/src/index.js +17 -9
- package/src/lexer.js +18 -0
- package/src/lower/boxing.js +36 -0
- package/src/lower/captures.js +179 -0
- package/src/lower/embedding.js +30 -0
- package/src/lower/escape.js +169 -0
- package/src/lower/functions.js +30 -0
- package/src/lower/globals.js +324 -0
- package/src/lower/index.js +115 -0
- package/src/lower/ownership.js +142 -0
- package/src/lower/range.js +70 -0
- package/src/minifier.js +2 -0
- package/src/parser/index.js +8 -3
- package/src/resolver.js +4 -1
- package/src/templ-lexer.js +107 -3
- package/src/templ-parser.js +29 -0
- package/src/test-runner.js +74 -20
- package/src/typechecker/expressions.js +40 -1
- package/src/typechecker/index.js +301 -6
- package/src/typechecker/statements.js +7 -0
- package/src/typechecker/stdlib/core.js +17 -0
- package/src/typechecker/types.js +56 -2
- /package/src/{codegen → backend/js}/source-map.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
- /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
|
|
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
|
|
58
|
-
operates on the
|
|
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
|
-
→
|
|
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/
|
|
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=
|
|
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, `{
|
|
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,
|
|
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, `
|
|
703
|
-
`switch/case/default`, `@templ.Raw()` raw HTML injection,
|
|
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** (~
|
|
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 (`
|
|
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
|
+
"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 "
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|