gofront 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +45 -49
  2. package/package.json +12 -8
  3. package/src/backend/js/expressions.js +1627 -148
  4. package/src/backend/js/fmt-runtime.js +573 -0
  5. package/src/backend/js/index.js +508 -102
  6. package/src/backend/js/runtime.js +1997 -106
  7. package/src/backend/js/statements.js +931 -87
  8. package/src/backend/js/stdlib/bits.js +87 -14
  9. package/src/backend/js/stdlib/builder.js +41 -6
  10. package/src/backend/js/stdlib/bytes.js +253 -7
  11. package/src/backend/js/stdlib/errors.js +7 -1
  12. package/src/backend/js/stdlib/fmt.js +349 -10
  13. package/src/backend/js/stdlib/index.js +19 -2
  14. package/src/backend/js/stdlib/json.js +1167 -0
  15. package/src/backend/js/stdlib/maps.js +0 -8
  16. package/src/backend/js/stdlib/math.js +17 -1
  17. package/src/backend/js/stdlib/simd.js +34 -0
  18. package/src/backend/js/stdlib/slices.js +36 -6
  19. package/src/backend/js/stdlib/strconv.js +10 -5
  20. package/src/backend/js/stdlib/strings.js +83 -4
  21. package/src/backend/js/stdlib/testing.js +7 -0
  22. package/src/backend/js/stdlib/time.js +79 -55
  23. package/src/backend/js/stdlib/unicode.js +1 -1
  24. package/src/backend/js/stdlib/utf8.js +1 -1
  25. package/src/backend/js/templ.js +38 -76
  26. package/src/backend/js/type-info.js +272 -0
  27. package/src/backend/shared/fmt-types.js +188 -0
  28. package/src/backend/wasm/boundary.js +446 -72
  29. package/src/backend/wasm/emit-builtins.js +320 -13
  30. package/src/backend/wasm/emit-exprs.js +1404 -199
  31. package/src/backend/wasm/emit-maps.js +61 -13
  32. package/src/backend/wasm/emit-stmts.js +1359 -164
  33. package/src/backend/wasm/emit.js +518 -74
  34. package/src/backend/wasm/encode.js +128 -13
  35. package/src/backend/wasm/globals.js +452 -0
  36. package/src/backend/wasm/index.js +194 -2489
  37. package/src/backend/wasm/method-expr.js +381 -0
  38. package/src/backend/wasm/module-emitter.js +2240 -0
  39. package/src/backend/wasm/monomorph.js +62 -4
  40. package/src/backend/wasm/optimize.js +4 -2
  41. package/src/backend/wasm/prescan.js +397 -0
  42. package/src/backend/wasm/stdlib/bytes.js +1804 -127
  43. package/src/backend/wasm/stdlib/core.js +1574 -199
  44. package/src/backend/wasm/stdlib/index.js +3 -5
  45. package/src/backend/wasm/stdlib/io.js +1567 -150
  46. package/src/backend/wasm/stdlib/packages.js +2 -0
  47. package/src/backend/wasm/stdlib/simd.js +499 -0
  48. package/src/backend/wasm/stdlib/time.js +1041 -12
  49. package/src/backend/wasm/types.js +221 -15
  50. package/src/backend/wasm/variadic.js +93 -0
  51. package/src/backend/wasm/wat.js +84 -20
  52. package/src/{cli-core.js → cli/cli-core.js} +236 -10
  53. package/src/{test-runner.js → cli/test-runner.js} +332 -39
  54. package/src/{vendor.js → cli/vendor.js} +89 -15
  55. package/src/compiler.js +107 -269
  56. package/src/dts-parser.js +71 -14
  57. package/src/index.js +15 -169
  58. package/src/{lexer.js → lexer/lexer.js} +102 -55
  59. package/src/{tokens.js → lexer/tokens.js} +1 -0
  60. package/src/link/sourcemap.js +97 -0
  61. package/src/link/stdlib.js +515 -0
  62. package/src/link/wasm.js +294 -0
  63. package/src/lower/bounds.js +394 -0
  64. package/src/lower/boxing.js +1 -6
  65. package/src/lower/captures.js +176 -32
  66. package/src/lower/embedding.js +2 -1
  67. package/src/lower/index.js +26 -18
  68. package/src/lower/json-codecs.js +1398 -0
  69. package/src/lower/ownership.js +22 -1
  70. package/src/lower/range.js +86 -1
  71. package/src/lower/reachability.js +132 -0
  72. package/src/parser/declarations.js +58 -13
  73. package/src/parser/expressions.js +29 -7
  74. package/src/parser/index.js +2 -1
  75. package/src/parser/statements.js +37 -22
  76. package/src/parser/struct-tag.js +144 -0
  77. package/src/{templ-parser.js → parser/templ.js} +138 -15
  78. package/src/parser/types.js +130 -13
  79. package/src/resolver.js +22 -1
  80. package/src/typechecker/assignability.js +215 -11
  81. package/src/typechecker/const-fold.js +356 -0
  82. package/src/typechecker/constants.js +256 -0
  83. package/src/typechecker/expressions.js +960 -99
  84. package/src/typechecker/index.js +491 -72
  85. package/src/typechecker/init-order.js +330 -0
  86. package/src/typechecker/resolve.js +596 -52
  87. package/src/typechecker/statements.js +157 -17
  88. package/src/typechecker/stdlib/core.js +333 -55
  89. package/src/typechecker/stdlib/excluded.js +898 -0
  90. package/src/typechecker/stdlib/extended.js +359 -26
  91. package/src/typechecker/{stdlib.js → stdlib/index.js} +6 -4
  92. package/src/typechecker/stdlib/simd.js +123 -0
  93. package/src/typechecker/termination.js +1 -1
  94. package/src/typechecker/types.js +165 -26
  95. package/stdlib/bytes/iter.go +192 -0
  96. package/stdlib/bytes/search.go +240 -0
  97. package/stdlib/bytes/split.go +218 -0
  98. package/stdlib/cmp/cmp.go +65 -0
  99. package/stdlib/encoding/json/errors.go +31 -0
  100. package/stdlib/encoding/json/indent.go +71 -0
  101. package/stdlib/encoding/json/number.go +12 -0
  102. package/stdlib/encoding/json/stream.go +173 -0
  103. package/stdlib/errors/overlay.go +12 -0
  104. package/stdlib/hash/hash.go +53 -0
  105. package/stdlib/internal/jsonrt/dec.go +738 -0
  106. package/stdlib/internal/jsonrt/enc.go +366 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +649 -0
  111. package/stdlib/io/io.go +346 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/iter.go +65 -0
  114. package/stdlib/math/extra.go +270 -0
  115. package/stdlib/slices/extra.go +73 -0
  116. package/stdlib/slices/iter.go +113 -0
  117. package/stdlib/sort/sort.go +561 -0
  118. package/stdlib/strconv/quote.go +341 -0
  119. package/stdlib/strings/extra.go +35 -0
  120. package/stdlib/strings/iter.go +122 -0
  121. package/stdlib/sync/extra.go +105 -0
  122. package/stdlib/unicode/unicode.go +112 -0
  123. package/stdlib/unicode/utf8/bytes.go +239 -0
  124. package/src/backend/wasm/const-fold.js +0 -205
  125. package/src/backend/wasm/emit-stdlib.js +0 -13
  126. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  127. /package/src/{colors.js → cli/colors.js} +0 -0
  128. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  129. /package/src/{minifier.js → cli/minifier.js} +0 -0
  130. /package/src/{project-config.js → cli/project-config.js} +0 -0
  131. /package/src/{pwa.js → cli/pwa.js} +0 -0
  132. /package/src/{templ-lexer.js → lexer/templ-lexer.js} +0 -0
@@ -0,0 +1,294 @@
1
+ // Linking of `wasm`/`both` package units into one `app.wasm`: unit
2
+ // registration, collision checks, link-copy AST rewrites and the facade splice.
3
+
4
+ import { compileWasmModule } from "../backend/wasm/index.js";
5
+ import { lower } from "../lower/index.js";
6
+ import { reachableStdlibDecls } from "../lower/reachability.js";
7
+ import { forEachChild } from "../lower/walk.js";
8
+ import { TypeCheckError, TypeChecker } from "../typechecker/index.js";
9
+
10
+ // ── WASM linking ──────────────────────────────────────────────
11
+ //
12
+ // Every `wasm`/`both` package reached during a build registers a unit in the
13
+ // shared `options.wasmUnits` array. The root compile links all units into a
14
+ // single `app.wasm` and splices the generated JS facade into the bundle where
15
+ // the first wasm package's JS would have been.
16
+
17
+ const WASM_MARKER_RE = /^\/\*__GOFRONT_WASM_UNIT:[^*]*\*\/$/m;
18
+
19
+ export function wasmMarker(pkgName) {
20
+ return `/*__GOFRONT_WASM_UNIT:${pkgName}*/`;
21
+ }
22
+
23
+ export function registerWasmUnit(options, unit) {
24
+ const units = options.wasmUnits;
25
+ if (!units) return;
26
+ if (units.some((u) => u.key === unit.key)) return;
27
+ units.push(unit);
28
+ }
29
+
30
+ // Registers a `wasm`/`both` package as a link unit and returns the target the
31
+ // package's own code is emitted for. `options.wasmTests` runs a `both`
32
+ // package's tests through the wasm backend (the package is then treated as
33
+ // `wasm` for linking so its test functions are exposed by the facade).
34
+ export function registerPackageUnit(options, unit) {
35
+ const { pkgTarget, programs } = unit;
36
+ if (pkgTarget !== "wasm" && pkgTarget !== "both") return pkgTarget;
37
+ const emitTarget =
38
+ options.wasmTests && options.isTest && pkgTarget === "both"
39
+ ? "wasm"
40
+ : pkgTarget;
41
+ registerWasmUnit(options, {
42
+ ...unit,
43
+ target: emitTarget,
44
+ programs:
45
+ emitTarget === pkgTarget
46
+ ? programs
47
+ : programs.map((p) => ({ ...p, target: emitTarget })),
48
+ });
49
+ return emitTarget;
50
+ }
51
+
52
+ function linkWasmUnits(
53
+ units,
54
+ { emitWat = false, rootTarget = "js", sourceMappingURL = null } = {},
55
+ ) {
56
+ if (!units.some((u) => u.target === "wasm")) return null;
57
+ checkLinkCollisions(units);
58
+
59
+ const types = new Map();
60
+ const bundledPackages = new Set();
61
+ const programs = [];
62
+ const stdlibPrograms = [];
63
+ for (const u of units) {
64
+ for (const b of u.bundledPackages) bundledPackages.add(b);
65
+ const copies = u.programs.map((p) => cloneAstForWasm(p));
66
+ programs.push(...copies);
67
+ if (u.key.startsWith("stdlib:")) stdlibPrograms.push(...copies);
68
+ }
69
+ lowerStdlibRefs(programs);
70
+ dequalifyBundledRefs(programs, bundledPackages);
71
+ const keepType = pruneStdlibPrograms(programs, stdlibPrograms);
72
+ for (const u of units)
73
+ for (const [k, v] of u.types) if (keepType(v)) types.set(k, v);
74
+ const checker = Object.assign(new TypeChecker(), {
75
+ types,
76
+ errors: [],
77
+ err(msg, node) {
78
+ const e = new TypeCheckError(
79
+ msg,
80
+ node,
81
+ this._currentFile,
82
+ this._currentSource,
83
+ );
84
+ this.errors.push(e);
85
+ return e;
86
+ },
87
+ });
88
+ const lowerRes = lower(programs, checker);
89
+ return compileWasmModule(programs, checker, lowerRes, {
90
+ boundary: true,
91
+ bundledPackages,
92
+ emitWat,
93
+ callMain: rootTarget === "wasm",
94
+ sourceMappingURL,
95
+ });
96
+ }
97
+
98
+ // The WASM pipeline rewrites ASTs in place (package dequalification,
99
+ // monomorphisation, variadic desugaring). Parsed ASTs are cached across
100
+ // builds and shared with JS codegen, so the link works on a structural copy;
101
+ // side tables (`_type`, …) are shared by reference.
102
+ function cloneAstForWasm(node, seen = new Map()) {
103
+ if (!node || typeof node !== "object") return node;
104
+ const prev = seen.get(node);
105
+ if (prev) return prev;
106
+ const copy = Array.isArray(node) ? [] : {};
107
+ seen.set(node, copy);
108
+ if (Array.isArray(node)) {
109
+ for (const n of node) copy.push(cloneAstForWasm(n, seen));
110
+ return copy;
111
+ }
112
+ for (const [k, v] of Object.entries(node)) {
113
+ copy[k] = k.charCodeAt(0) === 95 ? v : cloneAstForWasm(v, seen);
114
+ }
115
+ return copy;
116
+ }
117
+
118
+ // Drops unreachable Go-source stdlib declarations from the link copy and
119
+ // returns a filter that drops their (mangled) named types.
120
+ function pruneStdlibPrograms(programs, stdlibPrograms) {
121
+ if (stdlibPrograms.length === 0) return () => true;
122
+ const std = new Set(stdlibPrograms);
123
+ const reached = reachableStdlibDecls(
124
+ programs.filter((p) => !std.has(p)),
125
+ stdlibPrograms,
126
+ );
127
+ const types = new Set();
128
+ for (const p of stdlibPrograms) {
129
+ p.decls = p.decls.filter((d) => reached.has(d));
130
+ for (const d of p.decls) if (d.kind === "TypeDecl") types.add(d.name);
131
+ }
132
+ return (t) => {
133
+ const name = typeof t?.name === "string" ? t.name : "";
134
+ return !name.includes("$$") || types.has(name.replace(/\[.*$/, ""));
135
+ };
136
+ }
137
+
138
+ // The linked module is flat, so `util.Two` names the top-level `Two`. Done on
139
+ // the link copy before monomorphisation, whose re-check of all programs has no
140
+ // package namespaces. A local variable that shadows the package is typed and
141
+ // is left alone.
142
+ function isBundledSelector(node, bundled) {
143
+ const base = node.expr;
144
+ return (
145
+ base?.kind === "Ident" &&
146
+ bundled.has(base.name) &&
147
+ (base._type == null || base._type.kind === "namespace")
148
+ );
149
+ }
150
+
151
+ function dequalifyBundledRefs(node, bundled) {
152
+ if (!node || typeof node !== "object") return;
153
+ if (Array.isArray(node)) {
154
+ for (const n of node) dequalifyBundledRefs(n, bundled);
155
+ return;
156
+ }
157
+ if (node.kind === "SelectorExpr" && isBundledSelector(node, bundled)) {
158
+ node.kind = "Ident";
159
+ node.name = node.field;
160
+ delete node.expr;
161
+ delete node.field;
162
+ } else if (node.kind === "TypeName" || node.kind === "GenericTypeName") {
163
+ const dot = node.name?.indexOf(".") ?? -1;
164
+ if (dot > 0 && bundled.has(node.name.slice(0, dot)))
165
+ node.name = node.name.slice(dot + 1);
166
+ }
167
+ forEachChild(node, (child) => dequalifyBundledRefs(child, bundled));
168
+ }
169
+
170
+ // The linked module is one flat namespace (like the JS bundle), so a top-level
171
+ // name declared by two different packages would silently resolve to whichever
172
+ // was emitted last. Reject it up front.
173
+ function topLevelDeclNames(decl) {
174
+ switch (decl.kind) {
175
+ case "FuncDecl":
176
+ return [decl.name];
177
+ case "MethodDecl":
178
+ return [`${decl.recvType?.name ?? "?"}.${decl.name}`];
179
+ case "TypeDecl":
180
+ return [decl.name];
181
+ case "VarDecl":
182
+ case "ConstDecl":
183
+ return (decl.decls ?? []).flatMap((spec) => spec.names ?? []);
184
+ default:
185
+ return [];
186
+ }
187
+ }
188
+
189
+ export function checkLinkCollisions(units) {
190
+ const owner = new Map(); // name -> pkgName
191
+ const errors = [];
192
+ for (const u of units) {
193
+ for (const p of u.programs) {
194
+ for (const d of p.decls ?? []) {
195
+ for (const name of topLevelDeclNames(d)) {
196
+ if (name === "_") continue;
197
+ const prev = owner.get(name);
198
+ if (prev === undefined) owner.set(name, u.pkgName);
199
+ else if (prev !== u.pkgName)
200
+ errors.push(
201
+ `cannot link wasm packages: '${name}' is declared in both '${prev}' and '${u.pkgName}' (linked wasm packages share one namespace)`,
202
+ );
203
+ }
204
+ }
205
+ }
206
+ }
207
+ if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
208
+ }
209
+
210
+ // Replaces the first wasm marker with the facade (dropping the others) and
211
+ // shifts source-map lines that follow it.
212
+ function spliceFacade(merged, facade) {
213
+ const lines = merged.js.split("\n");
214
+ const first = lines.findIndex((l) => WASM_MARKER_RE.test(l));
215
+ if (first < 0) {
216
+ merged.js = `${facade}\n${merged.js}`;
217
+ const shift = facade.split("\n").length;
218
+ for (const m of merged.mappings) m.genLine += shift;
219
+ return;
220
+ }
221
+ const facadeLines = facade.split("\n");
222
+ const delta = facadeLines.length - 1;
223
+ for (const m of merged.mappings) {
224
+ if (m.genLine > first) m.genLine += delta;
225
+ }
226
+ const out = [...lines.slice(0, first), ...facadeLines];
227
+ for (let i = first + 1; i < lines.length; i++) {
228
+ if (!WASM_MARKER_RE.test(lines[i])) out.push(lines[i]);
229
+ }
230
+ merged.js = out.join("\n");
231
+ }
232
+
233
+ // Root compiles only: link every registered unit into app.wasm and splice the
234
+ // facade into the merged bundle. Returns `{ wasm, wat, sourceMapInfo }`
235
+ // (nulls when there is nothing to link).
236
+ export function linkIntoBundle(
237
+ options,
238
+ merged,
239
+ pkgTarget,
240
+ { sourceMappingURL } = {},
241
+ ) {
242
+ const none = { wasm: null, wat: null, sourceMapInfo: null };
243
+ if (options.isDependency) return none;
244
+ const linked = linkWasmUnits(options.wasmUnits, {
245
+ emitWat: options.emitWat,
246
+ rootTarget: pkgTarget,
247
+ sourceMappingURL,
248
+ });
249
+ if (!linked) return none;
250
+ spliceFacade(merged, linked.facade);
251
+ return {
252
+ wasm: linked.wasm,
253
+ wat: linked.wat ?? null,
254
+ sourceMapInfo: linked.sourceMapInfo ?? null,
255
+ };
256
+ }
257
+
258
+ // Turns `cmp.Compare` / `cmp.Ordered` references (marked `_stdName` by the
259
+ // checker) into plain identifiers with the mangled name, so WASM
260
+ // monomorphisation sees ordinary top-level references. Only for ASTs that
261
+ // are not cached: a stdlib package's own parse and the WASM link copy (user
262
+ // ASTs are re-checked on the next build; the JS backend reads `_stdName`).
263
+ export function lowerStdlibRefs(node) {
264
+ if (!node || typeof node !== "object") return;
265
+ if (Array.isArray(node)) {
266
+ for (const n of node) lowerStdlibRefs(n);
267
+ return;
268
+ }
269
+ if (node._stdName) {
270
+ if (node.kind === "SelectorExpr") {
271
+ delete node.expr;
272
+ delete node.field;
273
+ node.kind = "Ident";
274
+ }
275
+ node.name = node._stdName;
276
+ delete node._stdName;
277
+ }
278
+ if (node.kind === "CallExpr" && node.func?._jsonStream) {
279
+ const entry = node.func._jsonGen;
280
+ node.args.unshift(node.func.expr);
281
+ delete node.func.expr;
282
+ delete node.func.field;
283
+ delete node.func._jsonStream;
284
+ delete node.func._jsonGen;
285
+ node.func.kind = "Ident";
286
+ node.func.name = entry;
287
+ } else if (node._jsonGen && node.kind === "SelectorExpr") {
288
+ delete node.expr;
289
+ delete node.field;
290
+ node.kind = "Ident";
291
+ node.name = node._jsonGen;
292
+ }
293
+ forEachChild(node, lowerStdlibRefs);
294
+ }
@@ -0,0 +1,394 @@
1
+ // src/lower/bounds.js
2
+ // Index-check elision for strict mode (`both` packages in JS). Marks an
3
+ // IndexExpr `_inBounds` when its index is provably within the operand's
4
+ // length, so the backend emits a plain `xs[i]` instead of `__at`/`__ix`:
5
+ //
6
+ // - `for i := range xs { … xs[i] … }`
7
+ // - `for i := K; i < len(xs); i++ { … xs[i] … }` with a constant K >= 0
8
+ // - a constant index into an array (the checker rejects out-of-range ones)
9
+ // - constant indexes into a slice after a length proof: the hint `_ = xs[K]`,
10
+ // `if len(xs) > K { … }`, or `if len(xs) <= K { return }` (also `>=`, `<`
11
+ // and the mirrored `K < len(xs)` forms); proves `xs[0]` through `xs[K]`
12
+ //
13
+
14
+ // The loop forms need `xs` to be a local slice or array that the body never
15
+ // assigns or shadows, `i` to be written only by the loop, and neither to be
16
+ // address-taken, used as a method receiver, or assigned inside any closure
17
+ // in the function. Indexes inside func literals in the body are not marked:
18
+ // a closure may run after the loop, when `xs` has changed.
19
+
20
+ import { constIntValue } from "../typechecker/const-fold.js";
21
+ import { forEachChild } from "./walk.js";
22
+
23
+ function underlying(t) {
24
+ return t?.kind === "named" ? underlying(t.underlying) : t;
25
+ }
26
+
27
+ function isSliceOrArray(t) {
28
+ const u = underlying(t);
29
+ return u?.kind === "slice" || u?.kind === "array";
30
+ }
31
+
32
+ function constInt(node) {
33
+ const v = node?._const ? constIntValue(node._const) : null;
34
+ return typeof v === "bigint" ? v : null;
35
+ }
36
+
37
+ function isIdent(node, name) {
38
+ return node?.kind === "Ident" && (name === undefined || node.name === name);
39
+ }
40
+
41
+ // Names a node makes unsafe to reason about: taken by address, or used as a
42
+ // selector base (a possible pointer-receiver method call).
43
+ function unsafeNamesOf(node) {
44
+ if (node.kind === "UnaryExpr" && node.op === "&" && isIdent(node.operand))
45
+ return [node.operand.name];
46
+ if (node.kind === "SelectorExpr" && isIdent(node.expr))
47
+ return [node.expr.name];
48
+ return [];
49
+ }
50
+
51
+ // Names that are unsafe anywhere in the function: see unsafeNamesOf, plus
52
+ // every name assigned inside a closure.
53
+ function collectUnsafeNames(fnBody) {
54
+ const unsafe = new Set();
55
+ const visit = (node, inClosure) => {
56
+ if (!node || typeof node !== "object") return;
57
+ if (Array.isArray(node)) {
58
+ for (const n of node) visit(n, inClosure);
59
+ return;
60
+ }
61
+ const names = inClosure
62
+ ? [...unsafeNamesOf(node), ...assignedNames(node)]
63
+ : unsafeNamesOf(node);
64
+ for (const n of names) unsafe.add(n);
65
+ forEachChild(node, (c) => visit(c, inClosure || node.kind === "FuncLit"));
66
+ };
67
+ visit(fnBody, false);
68
+ return unsafe;
69
+ }
70
+
71
+ // Identifiers a single node assigns to (not declarations).
72
+ function assignedNames(node) {
73
+ if (node.kind === "AssignStmt")
74
+ return node.lhs.filter((l) => isIdent(l)).map((l) => l.name);
75
+ if (node.kind === "IncDecStmt" && isIdent(node.expr)) return [node.expr.name];
76
+ return [];
77
+ }
78
+
79
+ // Identifiers a single node declares.
80
+ function declaredNames(node) {
81
+ switch (node.kind) {
82
+ case "DefineStmt":
83
+ return node.lhs.filter((l) => isIdent(l)).map((l) => l.name);
84
+ case "VarDecl":
85
+ return (node.decls ?? []).flatMap((d) =>
86
+ (d.names ?? []).map((n) => (typeof n === "string" ? n : n?.name)),
87
+ );
88
+ case "FuncLit":
89
+ return (node.params ?? []).map((p) => p.name).filter(Boolean);
90
+ case "TypeSwitchStmt":
91
+ return node.assign ? [node.assign] : [];
92
+ default:
93
+ return [];
94
+ }
95
+ }
96
+
97
+ // True when `body` assigns or redeclares any of `names`.
98
+ function touches(body, names) {
99
+ let hit = false;
100
+ const visit = (node) => {
101
+ if (hit || !node || typeof node !== "object") return;
102
+ if (Array.isArray(node)) {
103
+ for (const n of node) visit(n);
104
+ return;
105
+ }
106
+ for (const n of [...assignedNames(node), ...declaredNames(node)])
107
+ if (names.includes(n)) hit = true;
108
+ forEachChild(node, visit);
109
+ };
110
+ visit(body);
111
+ return hit;
112
+ }
113
+
114
+ // Marks `xs[i]` in `body`, outside func literals.
115
+ function markIndexes(body, xs, i) {
116
+ const visit = (node) => {
117
+ if (!node || typeof node !== "object") return;
118
+ if (Array.isArray(node)) {
119
+ for (const n of node) visit(n);
120
+ return;
121
+ }
122
+ if (node.kind === "FuncLit") return;
123
+ if (
124
+ node.kind === "IndexExpr" &&
125
+ isIdent(node.expr, xs) &&
126
+ isIdent(node.index, i)
127
+ )
128
+ node._inBounds = true;
129
+ forEachChild(node, visit);
130
+ };
131
+ visit(body);
132
+ }
133
+
134
+ // `len(xs)` over a slice or array identifier: its name, or null.
135
+ function lenOfName(node) {
136
+ const ok =
137
+ node?.kind === "CallExpr" &&
138
+ isIdent(node.func, "len") &&
139
+ node.args?.length === 1 &&
140
+ isIdent(node.args[0]) &&
141
+ isSliceOrArray(node.args[0]._type);
142
+ return ok ? node.args[0].name : null;
143
+ }
144
+
145
+ // for i := range xs / for i, v := range xs
146
+ function rangeLoop(init, i) {
147
+ const rhs = init.rhs?.[0];
148
+ if (!isIdent(rhs.expr) || !isSliceOrArray(rhs.expr._type)) return null;
149
+ // `for i, xs := range xs`: the body's xs is the element, not the slice.
150
+ if (isIdent(init.lhs[1], rhs.expr.name)) return null;
151
+ return { xs: rhs.expr.name, i };
152
+ }
153
+
154
+ // for i := K; i < len(xs); i++ with a constant K >= 0
155
+ function countingLoop(stmt, i) {
156
+ const { init, cond, post } = stmt;
157
+ const k = constInt(init.rhs?.[0]);
158
+ if (init.lhs.length !== 1 || k === null || k < 0n) return null;
159
+ if (cond?.kind !== "BinaryExpr" || cond.op !== "<" || !isIdent(cond.left, i))
160
+ return null;
161
+ const xs = lenOfName(cond.right);
162
+ const step =
163
+ post?.kind === "IncDecStmt" && post.op === "++" && isIdent(post.expr, i);
164
+ return xs && step ? { xs, i } : null;
165
+ }
166
+
167
+ // The (slice name, index name) a for statement proves in range, or null.
168
+ function inBoundsLoop(stmt) {
169
+ const init = stmt.init;
170
+ if (init?.kind !== "DefineStmt" || !isIdent(init.lhs?.[0])) return null;
171
+ const i = init.lhs[0].name;
172
+ return init.rhs?.[0]?.kind === "RangeExpr"
173
+ ? rangeLoop(init, i)
174
+ : countingLoop(stmt, i);
175
+ }
176
+
177
+ // Marks the loop's `xs[i]` when its slice is a local the loop cannot change.
178
+ function markLoop(stmt, scope, unsafe) {
179
+ const loop = inBoundsLoop(stmt);
180
+ if (!loop || !scope.has(loop.xs) || scope.has("len")) return;
181
+ if (unsafe.has(loop.xs) || unsafe.has(loop.i)) return;
182
+ if (touches(stmt.body, [loop.xs, loop.i])) return;
183
+ markIndexes(stmt.body, loop.xs, loop.i);
184
+ }
185
+
186
+ const SCOPE_KINDS = new Set([
187
+ "Block",
188
+ "ForStmt",
189
+ "IfStmt",
190
+ "FuncLit",
191
+ "CaseClause",
192
+ "TypeSwitchStmt",
193
+ ]);
194
+
195
+ // How often each name is declared in the function (parameters included) and
196
+ // which names are assigned anywhere. A slice declared once and never
197
+ // assigned keeps its length for the whole function.
198
+ function nameStats(params, body) {
199
+ const declared = new Map();
200
+ const assigned = new Set();
201
+ const bump = (n) => n && declared.set(n, (declared.get(n) ?? 0) + 1);
202
+ for (const n of params) bump(n);
203
+ const visit = (node) => {
204
+ if (!node || typeof node !== "object") return;
205
+ if (Array.isArray(node)) {
206
+ for (const n of node) visit(n);
207
+ return;
208
+ }
209
+ for (const n of declaredNames(node)) bump(n);
210
+ for (const n of assignedNames(node)) assigned.add(n);
211
+ forEachChild(node, visit);
212
+ };
213
+ visit(body);
214
+ return { declared, assigned };
215
+ }
216
+
217
+ // A slice whose length no statement in the function can change.
218
+ function isStableSlice(node, stats, unsafe) {
219
+ if (!isIdent(node) || underlying(node._type)?.kind !== "slice") return false;
220
+ const { name } = node;
221
+ return (
222
+ stats.declared.get(name) === 1 &&
223
+ !stats.assigned.has(name) &&
224
+ !unsafe.has(name)
225
+ );
226
+ }
227
+
228
+ // `_ = xs[K]` with a constant K >= 0: the slice and K, or null.
229
+ function hintOf(stmt, stats, unsafe) {
230
+ if (stmt?.kind !== "AssignStmt" || stmt.op !== "=") return null;
231
+ const [lhs] = stmt.lhs;
232
+ const rhs = stmt.rhs?.[0];
233
+ if (stmt.lhs.length !== 1 || !isIdent(lhs, "_")) return null;
234
+ if (rhs?.kind !== "IndexExpr" || !isStableSlice(rhs.expr, stats, unsafe))
235
+ return null;
236
+ const k = constInt(rhs.index);
237
+ return k !== null && k >= 0n ? { xs: rhs.expr.name, k } : null;
238
+ }
239
+
240
+ const FLIPPED = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" };
241
+
242
+ // `len(xs) op K` (or `K op len(xs)`) over a stable slice: [xs, op, K] or null.
243
+ function lenComparison(cond, stats, unsafe) {
244
+ if (cond?.kind !== "BinaryExpr" || !(cond.op in FLIPPED)) return null;
245
+ const direct = cond.left?.kind === "CallExpr";
246
+ const call = direct ? cond.left : cond.right;
247
+ const k = constInt(direct ? cond.right : cond.left);
248
+ const arg = call?.args?.[0];
249
+ const ok =
250
+ k !== null &&
251
+ isIdent(call?.func, "len") &&
252
+ call.args.length === 1 &&
253
+ isStableSlice(arg, stats, unsafe);
254
+ if (!ok || stats.declared.has("len")) return null;
255
+ return [arg.name, direct ? cond.op : FLIPPED[cond.op], k];
256
+ }
257
+
258
+ // Highest index `len(xs) op K` proves when it holds (`holds`) or fails.
259
+ function provenMax(op, k, holds) {
260
+ let max = null;
261
+ if (holds && op === ">") max = k;
262
+ else if (holds && op === ">=") max = k - 1n;
263
+ else if (!holds && op === "<=") max = k;
264
+ else if (!holds && op === "<") max = k - 1n;
265
+ return max !== null && max >= 0n ? max : null;
266
+ }
267
+
268
+ function isTerminating(stmt) {
269
+ if (stmt?.kind === "ReturnStmt") return true;
270
+ const call = stmt?.kind === "ExprStmt" ? stmt.expr : null;
271
+ return call?.kind === "CallExpr" && isIdent(call.func, "panic");
272
+ }
273
+
274
+ // Marks `xs[0]` … `xs[max]` (constant indexes) anywhere under `root`.
275
+ function markProven(root, xs, max) {
276
+ const visit = (node) => {
277
+ if (!node || typeof node !== "object") return;
278
+ if (Array.isArray(node)) {
279
+ for (const n of node) visit(n);
280
+ return;
281
+ }
282
+ if (node.kind === "IndexExpr" && isIdent(node.expr, xs)) {
283
+ const k = constInt(node.index);
284
+ if (k !== null && k >= 0n && k <= max) node._inBounds = true;
285
+ }
286
+ forEachChild(node, visit);
287
+ };
288
+ visit(root);
289
+ }
290
+
291
+ // Length facts a statement establishes for what follows it in its block.
292
+ function factAfter(stmt, stats, unsafe) {
293
+ const hint = hintOf(stmt, stats, unsafe);
294
+ if (hint) return { xs: hint.xs, max: hint.k };
295
+ const exits =
296
+ stmt?.kind === "IfStmt" &&
297
+ !stmt.init &&
298
+ !stmt.elseBody &&
299
+ isTerminating(stmt.body?.stmts?.at(-1));
300
+ const cmp = exits ? lenComparison(stmt.cond, stats, unsafe) : null;
301
+ const max = cmp ? provenMax(cmp[1], cmp[2], false) : null;
302
+ return max === null ? null : { xs: cmp[0], max };
303
+ }
304
+
305
+ // Slice length proofs: hints and guards over every statement list.
306
+ function markSliceFacts(body, stats, unsafe) {
307
+ const visit = (node) => {
308
+ if (!node || typeof node !== "object") return;
309
+ if (Array.isArray(node)) {
310
+ for (const n of node) visit(n);
311
+ return;
312
+ }
313
+ if (node.kind === "IfStmt") {
314
+ const cmp = lenComparison(node.cond, stats, unsafe);
315
+ const max = cmp ? provenMax(cmp[1], cmp[2], true) : null;
316
+ if (max !== null) markProven(node.body, cmp[0], max);
317
+ }
318
+ if (Array.isArray(node.stmts)) {
319
+ node.stmts.forEach((stmt, i) => {
320
+ const fact = factAfter(stmt, stats, unsafe);
321
+ if (fact) markProven(node.stmts.slice(i + 1), fact.xs, fact.max);
322
+ });
323
+ }
324
+ forEachChild(node, visit);
325
+ };
326
+ visit(body);
327
+ }
328
+
329
+ function analyzeFunc(params, body) {
330
+ const unsafe = collectUnsafeNames(body);
331
+ // Walk in source order with a scope chain of local names, so `xs` counts
332
+ // as local only when a declaration in an enclosing scope precedes the loop.
333
+ const visit = (node, scope) => {
334
+ if (!node || typeof node !== "object") return;
335
+ if (Array.isArray(node)) {
336
+ for (const n of node) visit(n, scope);
337
+ return;
338
+ }
339
+ if (node.kind === "IndexExpr") markConstArrayIndex(node);
340
+ if (node.kind === "ForStmt") markLoop(node, scope, unsafe);
341
+ const inner = SCOPE_KINDS.has(node.kind) ? scope.child() : scope;
342
+ const declared = declaredNames(node);
343
+ // Func literal parameters and a type switch binding belong to its
344
+ // body; other declarations follow the node in its enclosing scope.
345
+ const ownsDecls = node.kind === "FuncLit" || node.kind === "TypeSwitchStmt";
346
+ const target = ownsDecls ? inner : null;
347
+ if (target) for (const n of declared) target.add(n);
348
+ forEachChild(node, (c) => visit(c, inner));
349
+ if (!target) for (const n of declared) scope.add(n);
350
+ };
351
+ const root = new LocalScope(null);
352
+ for (const n of params) root.add(n);
353
+ visit(body, root);
354
+ markSliceFacts(body, nameStats(params, body), unsafe);
355
+ }
356
+
357
+ function markConstArrayIndex(node) {
358
+ const u = underlying(node.expr?._type);
359
+ if (u?.kind !== "array" || typeof u.size !== "number") return;
360
+ const k = constInt(node.index);
361
+ if (k !== null && k >= 0n && k < BigInt(u.size)) node._inBounds = true;
362
+ }
363
+
364
+ class LocalScope {
365
+ constructor(parent) {
366
+ this.parent = parent;
367
+ this.names = new Set();
368
+ }
369
+ child() {
370
+ return new LocalScope(this);
371
+ }
372
+ add(name) {
373
+ if (name && name !== "_") this.names.add(name);
374
+ }
375
+ has(name) {
376
+ return this.names.has(name) || (this.parent?.has(name) ?? false);
377
+ }
378
+ }
379
+
380
+ function paramNames(decl) {
381
+ const names = (decl.params ?? []).map((p) => p.name);
382
+ if (decl.recvName) names.push(decl.recvName);
383
+ for (const r of decl.returnType?._namedReturns ?? [])
384
+ names.push(typeof r === "string" ? r : r?.name);
385
+ return names;
386
+ }
387
+
388
+ /** Marks provably in-range IndexExprs `_inBounds` in every function. */
389
+ export function markInBoundsIndexes(program) {
390
+ for (const d of program?.decls ?? []) {
391
+ if ((d.kind === "FuncDecl" || d.kind === "MethodDecl") && d.body)
392
+ analyzeFunc(paramNames(d), d.body);
393
+ }
394
+ }
@@ -6,12 +6,7 @@ import { forEachChild } from "./walk.js";
6
6
  export function isReferenceType(t) {
7
7
  if (!t) return false;
8
8
  const base = t.kind === "named" ? t.underlying : t;
9
- return (
10
- base?.kind === "struct" ||
11
- base?.kind === "slice" ||
12
- base?.kind === "map" ||
13
- base?.kind === "func"
14
- );
9
+ return base?.kind === "struct";
15
10
  }
16
11
 
17
12
  // Scan AST node for _addressTaken idents on scalars and populate a Set of boxed variable names.