gofront 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +288 -128
  4. package/src/backend/js/index.js +138 -56
  5. package/src/backend/js/runtime.js +1055 -53
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +151 -83
  8. package/src/backend/js/stdlib/base64.js +12 -0
  9. package/src/backend/js/stdlib/binary.js +102 -0
  10. package/src/backend/js/stdlib/bits.js +15 -1
  11. package/src/backend/js/stdlib/builder.js +28 -9
  12. package/src/backend/js/stdlib/bytes.js +75 -18
  13. package/src/backend/js/stdlib/errors.js +22 -3
  14. package/src/backend/js/stdlib/fmt.js +95 -20
  15. package/src/backend/js/stdlib/hex.js +17 -0
  16. package/src/backend/js/stdlib/html.js +1 -1
  17. package/src/backend/js/stdlib/index.js +19 -0
  18. package/src/backend/js/stdlib/io.js +105 -3
  19. package/src/backend/js/stdlib/maps.js +1 -1
  20. package/src/backend/js/stdlib/math.js +13 -1
  21. package/src/backend/js/stdlib/path.js +20 -6
  22. package/src/backend/js/stdlib/rand.js +10 -5
  23. package/src/backend/js/stdlib/sha256.js +12 -0
  24. package/src/backend/js/stdlib/shared.js +17 -0
  25. package/src/backend/js/stdlib/slices.js +53 -27
  26. package/src/backend/js/stdlib/sort.js +1 -1
  27. package/src/backend/js/stdlib/strconv.js +52 -11
  28. package/src/backend/js/stdlib/strings.js +152 -16
  29. package/src/backend/js/stdlib/sync.js +13 -0
  30. package/src/backend/js/stdlib/testing.js +2 -2
  31. package/src/backend/js/stdlib/time.js +93 -22
  32. package/src/backend/js/stdlib/unicode.js +2 -2
  33. package/src/backend/js/templ.js +1 -1
  34. package/src/backend/wasm/boundary.js +1677 -58
  35. package/src/backend/wasm/const-fold.js +205 -0
  36. package/src/backend/wasm/emit-builtins.js +1230 -0
  37. package/src/backend/wasm/emit-exprs.js +2096 -0
  38. package/src/backend/wasm/emit-maps.js +1999 -0
  39. package/src/backend/wasm/emit-shared.js +519 -0
  40. package/src/backend/wasm/emit-stdlib.js +13 -0
  41. package/src/backend/wasm/emit-stmts.js +2466 -0
  42. package/src/backend/wasm/emit.js +656 -5031
  43. package/src/backend/wasm/encode.js +259 -85
  44. package/src/backend/wasm/glue.js +19 -5
  45. package/src/backend/wasm/index.js +2432 -86
  46. package/src/backend/wasm/monomorph.js +711 -0
  47. package/src/backend/wasm/optimize.js +236 -0
  48. package/src/backend/wasm/stdlib/bytes.js +5039 -0
  49. package/src/backend/wasm/stdlib/core.js +8903 -0
  50. package/src/backend/wasm/stdlib/index.js +40 -0
  51. package/src/backend/wasm/stdlib/io.js +2161 -0
  52. package/src/backend/wasm/stdlib/packages.js +2400 -0
  53. package/src/backend/wasm/stdlib/pure.js +720 -0
  54. package/src/backend/wasm/stdlib/time.js +1200 -0
  55. package/src/backend/wasm/types.js +312 -36
  56. package/src/backend/wasm/wat.js +49 -2
  57. package/src/cli-core.js +276 -73
  58. package/src/compiler.js +162 -25
  59. package/src/dts-parser.js +79 -126
  60. package/src/index.js +88 -121
  61. package/src/lexer.js +80 -29
  62. package/src/lower/boxing.js +4 -6
  63. package/src/lower/captures.js +12 -23
  64. package/src/lower/escape.js +4 -9
  65. package/src/lower/functions.js +277 -5
  66. package/src/lower/globals.js +7 -15
  67. package/src/lower/index.js +5 -2
  68. package/src/lower/ownership.js +35 -19
  69. package/src/lower/range.js +2 -5
  70. package/src/lower/walk.js +25 -0
  71. package/src/minifier.js +20 -68
  72. package/src/parser/declarations.js +3 -6
  73. package/src/parser/expressions.js +3 -8
  74. package/src/parser/index.js +18 -3
  75. package/src/parser/statements.js +25 -38
  76. package/src/parser/types.js +1 -2
  77. package/src/resolver.js +3 -27
  78. package/src/templ-lexer.js +29 -63
  79. package/src/templ-parser.js +22 -45
  80. package/src/test-runner.js +20 -0
  81. package/src/typechecker/assignability.js +123 -14
  82. package/src/typechecker/expressions.js +244 -4
  83. package/src/typechecker/index.js +27 -1
  84. package/src/typechecker/resolve.js +71 -12
  85. package/src/typechecker/statements.js +0 -7
  86. package/src/typechecker/stdlib/core.js +292 -103
  87. package/src/typechecker/stdlib/extended.js +471 -4
  88. package/src/typechecker/stdlib/shared.js +53 -0
  89. package/src/typechecker/stdlib.js +2 -0
  90. package/src/typechecker/types.js +545 -8
  91. package/src/vendor.js +2 -2
@@ -1,6 +1,8 @@
1
1
  // src/lower/functions.js
2
2
  // Analyzes function signatures, named returns and defer structures.
3
3
 
4
+ import { forEachChild, someChild } from "./walk.js";
5
+
4
6
  export function extractNamedReturns(decl) {
5
7
  const named = decl?.returnType?._namedReturns;
6
8
  if (!named || named.length === 0) return null;
@@ -19,12 +21,282 @@ export function hasDefer(body) {
19
21
  if (Array.isArray(node)) return node.some(walk);
20
22
  if (node.kind === "DeferStmt") return true;
21
23
  if (node.kind === "FuncLit") return false; // separate function boundary
22
- for (const key of Object.keys(node)) {
23
- if (key.startsWith("_")) continue;
24
- if (walk(node[key])) return true;
25
- }
26
- return false;
24
+ return someChild(node, walk);
27
25
  };
28
26
 
29
27
  return walk(body);
30
28
  }
29
+
30
+ let defargCounter = 0;
31
+
32
+ /**
33
+ * True when `body` contains a direct `recover()` call (not nested inside a
34
+ * FuncLit). Used to decide which functions may legitimately observe an
35
+ * in-flight panic: Go only honours recover() when it is called directly by
36
+ * a deferred function.
37
+ */
38
+ export function hasDirectRecover(body) {
39
+ if (!body) return false;
40
+ if (body._hasDirectRecover != null) return Boolean(body._hasDirectRecover);
41
+ const walk = (node) => {
42
+ if (!node || typeof node !== "object") return false;
43
+ if (Array.isArray(node)) return node.some(walk);
44
+ if (node.kind === "FuncLit") return false;
45
+ if (
46
+ node.kind === "CallExpr" &&
47
+ node.func?.kind === "Ident" &&
48
+ node.func.name === "recover"
49
+ )
50
+ return true;
51
+ return someChild(node, walk);
52
+ };
53
+ const result = walk(body);
54
+ body._hasDirectRecover = result;
55
+ return result;
56
+ }
57
+
58
+ const BUILTIN_FUNC_NAMES = new Set([
59
+ "println",
60
+ "print",
61
+ "panic",
62
+ "recover",
63
+ "make",
64
+ "new",
65
+ "len",
66
+ "cap",
67
+ "append",
68
+ "copy",
69
+ "delete",
70
+ "close",
71
+ "clear",
72
+ "min",
73
+ "max",
74
+ ]);
75
+
76
+ export function normalizeDefers(programOrPrograms) {
77
+ defargCounter = 0;
78
+ const programs = Array.isArray(programOrPrograms)
79
+ ? programOrPrograms
80
+ : [programOrPrograms];
81
+
82
+ const pkgFuncNames = new Set(BUILTIN_FUNC_NAMES);
83
+ // Package-level funcs/methods whose body calls recover() directly. A
84
+ // `defer f()` whose callee is one of these must keep recover armed when the
85
+ // synthesized wrapper forwards the call.
86
+ const recoverFuncs = new Set();
87
+ for (const program of programs) {
88
+ for (const d of program?.decls ?? []) {
89
+ if (d.kind === "FuncDecl") pkgFuncNames.add(d.name);
90
+ if (
91
+ (d.kind === "FuncDecl" || d.kind === "MethodDecl") &&
92
+ hasDirectRecover(d.body)
93
+ )
94
+ recoverFuncs.add(d.name);
95
+ }
96
+ }
97
+ const ctx = { pkgFuncNames, recoverFuncs };
98
+
99
+ for (const program of programs) {
100
+ if (!program?.decls) continue;
101
+ for (const d of program.decls) {
102
+ if (d.body) normalizeBlockDefers(d.body, ctx);
103
+ }
104
+ }
105
+ }
106
+
107
+ function normalizeBlockDefers(node, ctx) {
108
+ if (!node || typeof node !== "object") return;
109
+ if (Array.isArray(node)) {
110
+ for (let i = 0; i < node.length; i++) {
111
+ const item = node[i];
112
+ if (item && item.kind === "DeferStmt") {
113
+ const inserted = normalizeDeferStmt(item, node, i, ctx);
114
+ i += inserted;
115
+ } else {
116
+ normalizeBlockDefers(item, ctx);
117
+ }
118
+ }
119
+ return;
120
+ }
121
+
122
+ if (node.kind === "FuncLit") {
123
+ normalizeBlockDefers(node.body, ctx);
124
+ return;
125
+ }
126
+
127
+ forEachChild(node, (child) => normalizeBlockDefers(child, ctx));
128
+ }
129
+
130
+ // Does the deferred callee possibly call recover() directly? Known package
131
+ // funcs/methods are looked up; unknown callees (closure variables, func
132
+ // values) are assumed to, so recover stays armed through the wrapper.
133
+ function deferCalleeMayRecover(funcExpr, ctx) {
134
+ if (!ctx) return false;
135
+ if (funcExpr.kind === "Ident") {
136
+ if (BUILTIN_FUNC_NAMES.has(funcExpr.name)) return false;
137
+ if (ctx.pkgFuncNames.has(funcExpr.name))
138
+ return ctx.recoverFuncs.has(funcExpr.name);
139
+ return true;
140
+ }
141
+ if (funcExpr.kind === "SelectorExpr") {
142
+ if (funcExpr.expr?._type?.kind === "namespace") return false;
143
+ return ctx.recoverFuncs.has(funcExpr.field);
144
+ }
145
+ if (funcExpr.kind === "FuncLit") return hasDirectRecover(funcExpr.body);
146
+ return true;
147
+ }
148
+
149
+ function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
150
+ if (stmt.call?.kind !== "CallExpr") return 0;
151
+ const call = stmt.call;
152
+ const isZeroArgFuncLit =
153
+ call.func?.kind === "FuncLit" && (!call.args || call.args.length === 0);
154
+ if (isZeroArgFuncLit) {
155
+ // The FuncLit itself is the deferred function: recover() inside it is
156
+ // honoured.
157
+ call.func._isDeferTarget = true;
158
+ normalizeBlockDefers(call.func.body, ctx);
159
+ return 0;
160
+ }
161
+ const pkgFuncNames = ctx?.pkgFuncNames ?? null;
162
+
163
+ const tempStmts = [];
164
+ let funcExpr = call.func;
165
+
166
+ // Hoist method receiver if call.func is a method call (e.g. defer r.Close()).
167
+ // Skipped for pointer-receiver methods on an addressable (non-pointer)
168
+ // operand: Go evaluates `&recv` at defer time, so the deferred call must
169
+ // observe later mutations of the original variable — copying the struct
170
+ // into a temp would break that. The receiver expression itself is then
171
+ // re-evaluated at run time, which matches Go for the common `defer x.M()`
172
+ // / `defer s.f.M()` cases.
173
+ const recvIsAddressableValue =
174
+ call.func?._type?._ptrRecv === true &&
175
+ call.func.expr?._type?.kind !== "pointer";
176
+ if (
177
+ parentList &&
178
+ index >= 0 &&
179
+ call.func?.kind === "SelectorExpr" &&
180
+ call.func.expr?._type?.kind !== "namespace" &&
181
+ !recvIsAddressableValue
182
+ ) {
183
+ const recv = call.func.expr;
184
+ defargCounter++;
185
+ const recvName = `__defrecv$${defargCounter}`;
186
+ const recvIdent = {
187
+ kind: "Ident",
188
+ name: recvName,
189
+ _type: recv._type,
190
+ };
191
+ tempStmts.push({
192
+ kind: "DefineStmt",
193
+ lhs: [{ kind: "Ident", name: recvName, _type: recv._type }],
194
+ rhs: [recv],
195
+ });
196
+ funcExpr = {
197
+ ...call.func,
198
+ expr: recvIdent,
199
+ };
200
+ } else if (
201
+ parentList &&
202
+ index >= 0 &&
203
+ call.func?.kind === "Ident" &&
204
+ pkgFuncNames &&
205
+ !pkgFuncNames.has(call.func.name)
206
+ ) {
207
+ // Hoist local function variable (e.g. fn := f1; defer fn(); fn = f2)
208
+ defargCounter++;
209
+ const fnName = `__deffn$${defargCounter}`;
210
+ const fnIdent = {
211
+ kind: "Ident",
212
+ name: fnName,
213
+ _type: call.func._type,
214
+ };
215
+ tempStmts.push({
216
+ kind: "DefineStmt",
217
+ lhs: [{ kind: "Ident", name: fnName, _type: call.func._type }],
218
+ rhs: [call.func],
219
+ });
220
+ funcExpr = fnIdent;
221
+ }
222
+
223
+ const args = call.args ?? [];
224
+ const tempIdents = [];
225
+
226
+ if (parentList && index >= 0 && args.length > 0) {
227
+ for (let a = 0; a < args.length; a++) {
228
+ const arg = args[a];
229
+ if (arg._type?.kind === "tuple" && Array.isArray(arg._type.types)) {
230
+ const tupleTypes = arg._type.types;
231
+ const tupleLhs = [];
232
+ for (let t = 0; t < tupleTypes.length; t++) {
233
+ defargCounter++;
234
+ const tempName = `__defarg$${defargCounter}`;
235
+ const ident = {
236
+ kind: "Ident",
237
+ name: tempName,
238
+ _type: tupleTypes[t],
239
+ };
240
+ tempIdents.push(ident);
241
+ tupleLhs.push(ident);
242
+ }
243
+ tempStmts.push({
244
+ kind: "DefineStmt",
245
+ lhs: tupleLhs,
246
+ rhs: [arg],
247
+ });
248
+ } else {
249
+ defargCounter++;
250
+ const tempName = `__defarg$${defargCounter}`;
251
+ tempIdents.push({
252
+ kind: "Ident",
253
+ name: tempName,
254
+ _type: arg._type,
255
+ });
256
+ tempStmts.push({
257
+ kind: "DefineStmt",
258
+ lhs: [{ kind: "Ident", name: tempName, _type: arg._type }],
259
+ rhs: [arg],
260
+ });
261
+ }
262
+ }
263
+ }
264
+
265
+ if (parentList && index >= 0 && tempStmts.length > 0) {
266
+ parentList.splice(index, 0, ...tempStmts);
267
+ }
268
+
269
+ const callArgs = tempIdents.length > 0 ? tempIdents : args;
270
+ const innerCall = {
271
+ ...call,
272
+ func: funcExpr,
273
+ args: callArgs,
274
+ _multiForward: tempIdents.length > 0 ? false : call._multiForward,
275
+ };
276
+ const synthFuncLit = {
277
+ kind: "FuncLit",
278
+ params: [],
279
+ returnType: null,
280
+ body: {
281
+ kind: "Block",
282
+ stmts: [{ kind: "ExprStmt", expr: innerCall }],
283
+ list: [{ kind: "ExprStmt", expr: innerCall }],
284
+ _hasDefer: false,
285
+ },
286
+ _type: { kind: "FuncType", params: [], returnType: null },
287
+ _isDeferTarget: true,
288
+ // The wrapper only forwards to the real deferred callee; when that
289
+ // callee may call recover() directly, recover must stay armed across
290
+ // the wrapper frame.
291
+ _deferForward: deferCalleeMayRecover(call.func, ctx),
292
+ };
293
+ if (call.func?.kind === "FuncLit") normalizeBlockDefers(call.func.body, ctx);
294
+ stmt.call = {
295
+ kind: "CallExpr",
296
+ func: synthFuncLit,
297
+ args: [],
298
+ _type: { kind: "basic", name: "void" },
299
+ };
300
+
301
+ return tempStmts.length;
302
+ }
@@ -3,6 +3,7 @@
3
3
  // and computes functions that participate in the package initialization call path.
4
4
 
5
5
  import { isReferenceType } from "./boxing.js";
6
+ import { forEachChild, someChild } from "./walk.js";
6
7
 
7
8
  /**
8
9
  * Computes:
@@ -117,11 +118,9 @@ function scanReassignments(node, packageGlobals, reassigned, insideInit) {
117
118
  }
118
119
  }
119
120
  checkAddressTaken(node, packageGlobals, reassigned);
120
- for (const k of Object.keys(node)) {
121
- if (!k.startsWith("_")) {
122
- scanReassignments(node[k], packageGlobals, reassigned, insideInit);
123
- }
124
- }
121
+ forEachChild(node, (child) =>
122
+ scanReassignments(child, packageGlobals, reassigned, insideInit),
123
+ );
125
124
  }
126
125
 
127
126
  function addCallTarget(call, called) {
@@ -145,9 +144,7 @@ function findCalledFunctions(node, called = new Set()) {
145
144
  return called;
146
145
  }
147
146
  if (node.kind === "CallExpr") addCallTarget(node, called);
148
- for (const k of Object.keys(node)) {
149
- if (!k.startsWith("_")) findCalledFunctions(node[k], called);
150
- }
147
+ forEachChild(node, (child) => findCalledFunctions(child, called));
151
148
  return called;
152
149
  }
153
150
 
@@ -244,9 +241,7 @@ function scanReadIdents(node, readSet) {
244
241
  readSet.add(node.name);
245
242
  return;
246
243
  }
247
- for (const k of Object.keys(node)) {
248
- if (!k.startsWith("_")) scanReadIdents(node[k], readSet);
249
- }
244
+ forEachChild(node, (child) => scanReadIdents(child, readSet));
250
245
  }
251
246
 
252
247
  function isLocallyDeclared(node, name) {
@@ -265,10 +260,7 @@ function isLocallyDeclared(node, name) {
265
260
  return true;
266
261
  }
267
262
  }
268
- for (const k of Object.keys(node)) {
269
- if (!k.startsWith("_") && isLocallyDeclared(node[k], name)) return true;
270
- }
271
- return false;
263
+ return someChild(node, (child) => isLocallyDeclared(child, name));
272
264
  }
273
265
 
274
266
  function getGlobalsToCache(fn, neverAssignedGlobals, initPathFunctions) {
@@ -9,16 +9,17 @@ export * from "./functions.js";
9
9
  export * from "./globals.js";
10
10
  export * from "./ownership.js";
11
11
  export * from "./range.js";
12
+ export * from "./walk.js";
12
13
 
13
14
  import { scanAddressTaken } from "./boxing.js";
14
15
  import { analyzeCaptures } from "./captures.js";
15
16
  import { computeEmbeddedStubs } from "./embedding.js";
16
17
  import { paramEscapes } from "./escape.js";
17
- import { extractNamedReturns, hasDefer } from "./functions.js";
18
+ import { extractNamedReturns, hasDefer, normalizeDefers } from "./functions.js";
18
19
  import { computeGlobalAnalysis } from "./globals.js";
19
20
  import { OwnershipContext } from "./ownership.js";
20
21
 
21
- export class LowerResult {
22
+ class LowerResult {
22
23
  constructor() {
23
24
  // Map<ASTNode (FuncDecl, MethodDecl, FuncLit), Set<string>>
24
25
  this.boxedVars = new Map();
@@ -53,6 +54,8 @@ export function lower(programOrPrograms, checker = null) {
53
54
  ? programOrPrograms
54
55
  : [programOrPrograms];
55
56
 
57
+ normalizeDefers(programs);
58
+
56
59
  // 1. Aggregate methods across all files in the package
57
60
  const methodMap = new Map();
58
61
  for (const program of programs) {
@@ -1,6 +1,8 @@
1
1
  // src/lower/ownership.js
2
2
  // Tracks variable ownership, mutation and clone elision for Go value semantics.
3
3
 
4
+ import { forEachChild, someChild } from "./walk.js";
5
+
4
6
  export function rootIdentName(e) {
5
7
  while (e) {
6
8
  if (e.kind === "Ident") return e.name;
@@ -42,11 +44,7 @@ export function nodeMutatesVar(node, name, addrOnly = false) {
42
44
  )
43
45
  return true;
44
46
  if (!addrOnly && nodeWritesVar(node, name)) return true;
45
- for (const key of Object.keys(node)) {
46
- if (key.startsWith("_")) continue;
47
- if (nodeMutatesVar(node[key], name, addrOnly)) return true;
48
- }
49
- return false;
47
+ return someChild(node, (child) => nodeMutatesVar(child, name, addrOnly));
50
48
  }
51
49
 
52
50
  // Single-pass collector of all mutated or address-taken variables in an AST node.
@@ -85,38 +83,56 @@ export function collectMutatedVars(
85
83
  }
86
84
  }
87
85
 
88
- for (const key of Object.keys(node)) {
89
- if (key.startsWith("_")) continue;
90
- collectMutatedVars(node[key], mutated, addrOnly);
91
- }
86
+ forEachChild(node, (child) => collectMutatedVars(child, mutated, addrOnly));
92
87
  return mutated;
93
88
  }
94
89
 
95
90
  export function fnMutates(body, name, addrOnly = false, cache = null) {
96
91
  if (!body) return true;
97
- const key = `${addrOnly ? "&" : ""}${name}`;
98
92
  if (cache) {
99
- if (!cache.has(key)) {
100
- cache.set(key, nodeMutatesVar(body, name, addrOnly));
93
+ const setKey = addrOnly ? "&*" : "*";
94
+ let set = cache.get(setKey);
95
+ if (!set) {
96
+ set = collectMutatedVars(body, new Set(), addrOnly);
97
+ cache.set(setKey, set);
101
98
  }
102
- return cache.get(key);
99
+ return set.has(name);
103
100
  }
104
101
  return nodeMutatesVar(body, name, addrOnly);
105
102
  }
106
103
 
104
+ function collectClosureMutatedVars(node, mutated = new Set()) {
105
+ if (!node || typeof node !== "object") return mutated;
106
+ if (Array.isArray(node)) {
107
+ for (const item of node) collectClosureMutatedVars(item, mutated);
108
+ return mutated;
109
+ }
110
+ if (node.kind === "FuncLit") {
111
+ collectMutatedVars(node.body, mutated, false);
112
+ return mutated;
113
+ }
114
+ forEachChild(node, (child) => collectClosureMutatedVars(child, mutated));
115
+ return mutated;
116
+ }
117
+
107
118
  export function closureMutates(body, name, cache = null) {
108
- const key = `λ${name}`;
109
- if (cache?.has(key)) return cache.get(key);
119
+ if (!body) return false;
120
+ if (cache) {
121
+ let set = cache.get("λ*");
122
+ if (!set) {
123
+ set = collectClosureMutatedVars(body, new Set());
124
+ cache.set("λ*", set);
125
+ }
126
+ return set.has(name);
127
+ }
110
128
 
111
129
  const walk = (node) => {
112
130
  if (!node || typeof node !== "object") return false;
113
131
  if (Array.isArray(node)) return node.some(walk);
114
132
  if (node.kind === "FuncLit") return nodeMutatesVar(node.body, name);
115
- return Object.keys(node).some((k) => !k.startsWith("_") && walk(node[k]));
133
+ return someChild(node, walk);
116
134
  };
117
- const res = walk(body);
118
- if (cache) cache.set(key, res);
119
- return res;
135
+ return walk(body);
120
136
  }
121
137
 
122
138
  export class OwnershipContext {
@@ -2,6 +2,7 @@
2
2
  // Classifies for-range loop shapes and variable scoping/redelcarations.
3
3
 
4
4
  import { isNumeric } from "../typechecker/types.js";
5
+ import { someChild } from "./walk.js";
5
6
 
6
7
  export function isRangeFor(stmt) {
7
8
  if (!stmt?.init) return false;
@@ -40,11 +41,7 @@ export function nodeAssigns(node, name) {
40
41
  node.expr.name === name)
41
42
  )
42
43
  return true;
43
- for (const key of Object.keys(node)) {
44
- if (key.startsWith("_")) continue;
45
- if (nodeAssigns(node[key], name)) return true;
46
- }
47
- return false;
44
+ return someChild(node, (child) => nodeAssigns(child, name));
48
45
  }
49
46
 
50
47
  // True when the body re-declares one of `names` at its top level (legal Go shadowing).
@@ -0,0 +1,25 @@
1
+ // src/lower/walk.js
2
+ // Shared AST child traversal for the lowering passes. Side-table fields
3
+ // (`_type`, `_boxed`, …) start with "_" and are never AST children.
4
+
5
+ /** True for a side-table key that traversal must skip. */
6
+ function isSideTableKey(key) {
7
+ return key.charCodeAt(0) === 95; // "_"
8
+ }
9
+
10
+ /** Calls `fn(child)` for every non-side-table property of `node`. */
11
+ export function forEachChild(node, fn) {
12
+ for (const key of Object.keys(node)) {
13
+ if (isSideTableKey(key)) continue;
14
+ fn(node[key]);
15
+ }
16
+ }
17
+
18
+ /** True when `pred(child)` holds for some non-side-table property of `node`. */
19
+ export function someChild(node, pred) {
20
+ for (const key of Object.keys(node)) {
21
+ if (isSideTableKey(key)) continue;
22
+ if (pred(node[key])) return true;
23
+ }
24
+ return false;
25
+ }
package/src/minifier.js CHANGED
@@ -121,7 +121,7 @@ const REGEX_CONTEXT_VALUES = new Set([
121
121
  const DIVISION_AFTER_OPS = new Set([")", "]", "++", "--"]);
122
122
 
123
123
  function _isRegexContext(tokens) {
124
- const prev = lastNonWhitespaceToken(tokens);
124
+ const prev = findPrevNonWs(tokens);
125
125
  if (!prev) return true;
126
126
  if (prev.type === "op") return !DIVISION_AFTER_OPS.has(prev.value);
127
127
  return REGEX_CONTEXT_VALUES.has(prev.value);
@@ -323,14 +323,6 @@ function tokenize(code) {
323
323
  return tokens;
324
324
  }
325
325
 
326
- function lastNonWhitespaceToken(tokens) {
327
- for (let i = tokens.length - 1; i >= 0; i--) {
328
- if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
329
- return tokens[i];
330
- }
331
- return null;
332
- }
333
-
334
326
  // ── Stage 1 & 2: Compress ────────────────────────────────────
335
327
 
336
328
  function isIdentLike(type) {
@@ -348,15 +340,6 @@ const SPACE_AFTER_KW_RIGHT_TYPES = new Set([
348
340
  ]);
349
341
  // Operator values that do NOT need a space after a keyword
350
342
  const NO_SPACE_AFTER_KW_OPS = new Set(["(", "[", "!", "-", "+"]);
351
- // Adjacent op pairs that would accidentally merge into a different operator
352
- const _INCREMENT_MERGE_PAIRS = new Set([
353
- "+|+",
354
- "-|-",
355
- "+|++",
356
- "-|--",
357
- "++|+",
358
- "--|−-",
359
- ]);
360
343
 
361
344
  function _wouldMergeUnaryOps(left, right) {
362
345
  return (
@@ -436,11 +419,7 @@ function compressTokens(tokens) {
436
419
  return out;
437
420
  }
438
421
 
439
- function compress(code) {
440
- return compressTokens(tokenize(code));
441
- }
442
-
443
- function findPrevNonWs(tokens, index) {
422
+ function findPrevNonWs(tokens, index = tokens.length) {
444
423
  for (let i = index - 1; i >= 0; i--) {
445
424
  if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
446
425
  return tokens[i];
@@ -477,7 +456,7 @@ function _matchingParen(tokens, openIdx) {
477
456
  // A `(` opens a parameter list only when its `)` is followed by a body `{` or `=>`;
478
457
  // otherwise `name(...)` is a call and its arguments may be free globals.
479
458
  function _collectParamDecls(tokens, i, declaredLocals) {
480
- const prev = findPrevNonWsToken(tokens, i);
459
+ const prev = findPrevNonWs(tokens, i);
481
460
  if (
482
461
  !prev ||
483
462
  (prev.value !== "function" && prev.type !== "ident" && prev.value !== ")")
@@ -583,22 +562,7 @@ function _filterRenameCandidates(declaredLocals) {
583
562
  }
584
563
  }
585
564
 
586
- function _applyRenaming(tokens, renameMap) {
587
- let out = "";
588
- for (let i = 0; i < tokens.length; i++) {
589
- const tok = tokens[i];
590
- if (tok.type === "ident" && renameMap.has(tok.value)) {
591
- const prev = findPrevNonWsToken(tokens, i);
592
- out += prev && prev.value === "." ? tok.value : renameMap.get(tok.value);
593
- } else {
594
- out += tok.value;
595
- }
596
- }
597
- return out;
598
- }
599
-
600
- function mangle(code) {
601
- const tokens = tokenize(code);
565
+ function mangleTokens(tokens) {
602
566
  const { declaredLocals } = _collectLocalDecls(tokens);
603
567
  _filterRenameCandidates(declaredLocals);
604
568
 
@@ -612,7 +576,15 @@ function mangle(code) {
612
576
  const nameGen = shortNameGeneratorFn(occupied);
613
577
  const renameMap = new Map();
614
578
  for (const name of declaredLocals) renameMap.set(name, nameGen());
615
- return _applyRenaming(tokens, renameMap);
579
+ return tokens.map((tok, i) => {
580
+ if (tok.type === "ident" && renameMap.has(tok.value)) {
581
+ const prev = findPrevNonWs(tokens, i);
582
+ if (prev?.value !== ".") {
583
+ return { type: "ident", value: renameMap.get(tok.value) };
584
+ }
585
+ }
586
+ return tok;
587
+ });
616
588
  }
617
589
 
618
590
  function nextNonWs(tokens, index) {
@@ -624,14 +596,6 @@ function nextNonWs(tokens, index) {
624
596
  return null;
625
597
  }
626
598
 
627
- function findPrevNonWsToken(tokens, index) {
628
- for (let i = index - 1; i >= 0; i--) {
629
- if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
630
- return tokens[i];
631
- }
632
- return null;
633
- }
634
-
635
599
  function collectDestructured(tokens, startIdx, locals) {
636
600
  const open = tokens[startIdx].value;
637
601
  const close = open === "[" ? "]" : "}";
@@ -647,7 +611,7 @@ function collectDestructured(tokens, startIdx, locals) {
647
611
  }
648
612
 
649
613
  function _isParamStart(tokens, i) {
650
- const prev = findPrevNonWsToken(tokens, i);
614
+ const prev = findPrevNonWs(tokens, i);
651
615
  return (
652
616
  !prev ||
653
617
  prev.value === "(" ||
@@ -839,28 +803,16 @@ function foldTokens(tokens) {
839
803
  return current;
840
804
  }
841
805
 
842
- function foldLiterals(code) {
843
- const tokens = tokenize(code);
844
- return compressTokens(foldTokens(tokens));
845
- }
846
-
847
806
  // ── Public API ───────────────────────────────────────────────
848
807
 
849
808
  export function minify(code, options = {}) {
850
809
  const { mangle: doMangle = false } = options;
851
-
852
- // Stage 1 + 2: compress (strips comments, whitespace, compresses tokens)
853
- let result = compress(code);
854
-
855
- // Stage 4: literal folding
856
- result = foldLiterals(result);
857
-
858
- // Stage 3: mangling (optional)
810
+ let tokens = tokenize(code).filter(
811
+ (t) => t.type !== "ws" && t.type !== "comment",
812
+ );
813
+ tokens = foldTokens(tokens);
859
814
  if (doMangle) {
860
- result = mangle(result);
861
- // Re-compress after mangling to clean up any leftover whitespace
862
- result = compress(result);
815
+ tokens = mangleTokens(tokens);
863
816
  }
864
-
865
- return result;
817
+ return compressTokens(tokens);
866
818
  }