gofront 1.3.12 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +90 -20
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +47 -36
  5. package/src/{codegen → backend/js}/runtime.js +11 -0
  6. package/src/{codegen → backend/js}/statements.js +43 -48
  7. package/src/backend/js/stdlib/bits.js +32 -0
  8. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  9. package/src/{codegen → backend/js}/templ.js +31 -2
  10. package/src/backend/wasm/boundary.js +1345 -0
  11. package/src/backend/wasm/emit.js +5779 -0
  12. package/src/backend/wasm/encode.js +789 -0
  13. package/src/backend/wasm/glue.js +130 -0
  14. package/src/backend/wasm/index.js +1413 -0
  15. package/src/backend/wasm/types.js +420 -0
  16. package/src/backend/wasm/wat.js +255 -0
  17. package/src/cli-core.js +52 -7
  18. package/src/compiler.js +328 -12
  19. package/src/dev-server.js +56 -15
  20. package/src/index.js +17 -9
  21. package/src/lexer.js +18 -0
  22. package/src/lower/boxing.js +36 -0
  23. package/src/lower/captures.js +179 -0
  24. package/src/lower/embedding.js +30 -0
  25. package/src/lower/escape.js +169 -0
  26. package/src/lower/functions.js +30 -0
  27. package/src/lower/globals.js +324 -0
  28. package/src/lower/index.js +115 -0
  29. package/src/lower/ownership.js +142 -0
  30. package/src/lower/range.js +70 -0
  31. package/src/minifier.js +2 -0
  32. package/src/parser/index.js +8 -3
  33. package/src/resolver.js +4 -1
  34. package/src/templ-lexer.js +107 -3
  35. package/src/templ-parser.js +29 -0
  36. package/src/test-runner.js +74 -20
  37. package/src/typechecker/expressions.js +40 -1
  38. package/src/typechecker/index.js +301 -6
  39. package/src/typechecker/statements.js +7 -0
  40. package/src/typechecker/stdlib/core.js +17 -0
  41. package/src/typechecker/types.js +56 -2
  42. /package/src/{codegen → backend/js}/source-map.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  62. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  63. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/src/dev-server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Minimal dev server for gofront --serve watch mode.
2
2
  // Serves static files and pushes reload / error / css-update events via SSE.
3
3
 
4
- import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { colors, log } from "./colors.js";
@@ -9,8 +9,10 @@ import { colors, log } from "./colors.js";
9
9
  export const MIME = {
10
10
  ".html": "text/html; charset=utf-8",
11
11
  ".js": "application/javascript; charset=utf-8",
12
+ ".wasm": "application/wasm",
12
13
  ".css": "text/css; charset=utf-8",
13
14
  ".json": "application/json; charset=utf-8",
15
+ ".map": "application/json; charset=utf-8",
14
16
  ".go": "text/plain; charset=utf-8",
15
17
  ".svg": "image/svg+xml",
16
18
  ".png": "image/png",
@@ -151,7 +153,11 @@ function handleSseRequest(req, res, clients, lastError = null) {
151
153
  if (lastError)
152
154
  res.write(`event: build-error\ndata: ${JSON.stringify(lastError)}\n\n`);
153
155
  clients.add(res);
154
- req.on?.("close", () => clients.delete(res));
156
+ const cleanup = () => clients.delete(res);
157
+ req.on?.("close", cleanup);
158
+ res.on?.("close", cleanup);
159
+ res.on?.("finish", cleanup);
160
+ res.on?.("error", cleanup);
155
161
  }
156
162
 
157
163
  function resolveStaticPath(serveDir, urlPath) {
@@ -161,49 +167,73 @@ function resolveStaticPath(serveDir, urlPath) {
161
167
  : urlPath.startsWith("/")
162
168
  ? urlPath
163
169
  : `/${urlPath}`;
164
- const filePath = resolve(serveDir, `.${cleanPath}`);
165
- if (!isInsideDir(resolve(serveDir), filePath)) return { forbidden: true };
170
+ const rootPath = existsSync(serveDir)
171
+ ? realpathSync(resolve(serveDir))
172
+ : resolve(serveDir);
173
+ const filePath = resolve(rootPath, `.${cleanPath}`);
174
+ if (!isInsideDir(rootPath, filePath)) return { forbidden: true };
166
175
 
167
176
  if (existsSync(filePath)) {
168
- const stat = statSync(filePath);
177
+ const realTarget = realpathSync(filePath);
178
+ if (!isInsideDir(rootPath, realTarget)) return { forbidden: true };
179
+ const stat = statSync(realTarget);
169
180
  if (stat.isDirectory()) {
170
- const dirIndex = join(filePath, "index.html");
171
- if (existsSync(dirIndex)) return { filePath: dirIndex };
172
- const rootIndex = join(serveDir, "index.html");
173
- if (existsSync(rootIndex)) return { filePath: rootIndex };
181
+ const dirIndex = join(realTarget, "index.html");
182
+ if (
183
+ existsSync(dirIndex) &&
184
+ isInsideDir(rootPath, realpathSync(dirIndex))
185
+ ) {
186
+ return { filePath: dirIndex };
187
+ }
188
+ const rootIndex = join(rootPath, "index.html");
189
+ if (
190
+ existsSync(rootIndex) &&
191
+ isInsideDir(rootPath, realpathSync(rootIndex))
192
+ ) {
193
+ return { filePath: rootIndex };
194
+ }
174
195
  return { notFound: true };
175
196
  }
176
- return { filePath };
197
+ return { filePath: realTarget };
177
198
  }
178
199
 
179
200
  // SPA fallback: clean paths with no file extension fall back to index.html
180
201
  if (!extname(cleanPath)) {
181
- const indexPath = join(serveDir, "index.html");
202
+ const indexPath = join(rootPath, "index.html");
182
203
  if (existsSync(indexPath)) return { filePath: indexPath };
183
204
  }
184
205
 
185
206
  return { notFound: true };
186
207
  }
187
208
 
188
- function sendStaticFile(res, filePath, shouldInject) {
209
+ function sendStaticFile(res, filePath, shouldInject, req = null) {
189
210
  try {
190
211
  const ext = extname(filePath);
212
+ const isHead = req?.method === "HEAD";
191
213
  if (ext === ".html") {
192
214
  const data = readFileSync(filePath, "utf8");
193
215
  const body = shouldInject ? injectLiveReload(data) : data;
194
216
  res.writeHead(200, {
195
217
  "Content-Type": MIME[".html"],
218
+ "X-Content-Type-Options": "nosniff",
196
219
  "Cache-Control": "no-cache, no-store, must-revalidate",
197
220
  });
198
- res.end(body);
221
+ if (isHead) res.end();
222
+ else res.end(body);
199
223
  return;
200
224
  }
201
225
 
202
226
  const data = readFileSync(filePath);
203
227
  res.writeHead(200, {
204
228
  "Content-Type": MIME[ext] ?? "application/octet-stream",
205
- "Cache-Control": "no-cache, no-store, must-revalidate",
229
+ "X-Content-Type-Options": "nosniff",
230
+ "Cache-Control":
231
+ ext === ".wasm" ? "no-store" : "no-cache, no-store, must-revalidate",
206
232
  });
233
+ if (isHead) {
234
+ res.end();
235
+ return;
236
+ }
207
237
  res.end(data);
208
238
  } catch {
209
239
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -218,7 +248,18 @@ export function handleDevRequest(
218
248
  clients = new Set(),
219
249
  options = {},
220
250
  ) {
251
+ if (req.method && req.method !== "GET" && req.method !== "HEAD") {
252
+ res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET, HEAD" });
253
+ res.end("Method Not Allowed");
254
+ return;
255
+ }
256
+
221
257
  if (req.url === "/_gofront/events") {
258
+ if (req.method && req.method !== "GET") {
259
+ res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET" });
260
+ res.end("Method Not Allowed");
261
+ return;
262
+ }
222
263
  handleSseRequest(req, res, clients, options.getLastError?.() ?? null);
223
264
  return;
224
265
  }
@@ -240,7 +281,7 @@ export function handleDevRequest(
240
281
  return;
241
282
  }
242
283
 
243
- sendStaticFile(res, resolved.filePath, options.injectReload ?? true);
284
+ sendStaticFile(res, resolved.filePath, options.injectReload ?? true, req);
244
285
  }
245
286
 
246
287
  function broadcastToClients(clients, event, payload) {
package/src/index.js CHANGED
@@ -18,12 +18,13 @@ import { createRequire } from "node:module";
18
18
  const _require = createRequire(import.meta.url);
19
19
  const { version } = _require("../package.json");
20
20
 
21
- import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
21
+ import { statSync, watch } from "node:fs";
22
22
  import { dirname, relative, resolve } from "node:path";
23
23
  import { copyAssets } from "./asset-manager.js";
24
24
  import {
25
25
  formatBuildSummary,
26
26
  formatPrepSummary,
27
+ formatWrittenDesc,
27
28
  handleBuild,
28
29
  handleCheck,
29
30
  handleDev,
@@ -37,6 +38,7 @@ import {
37
38
  parsePrepArgs,
38
39
  parseTestArgs,
39
40
  runCompile,
41
+ writeCompileOutput,
40
42
  } from "./cli-core.js";
41
43
  import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
42
44
  import { createDevServer } from "./dev-server.js";
@@ -56,14 +58,14 @@ GoFront — a Go-inspired language that compiles to JavaScript
56
58
 
57
59
  Usage:
58
60
  gofront dev [dir] [options] Start dev server with live reload (default port 3000)
59
- gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify)
61
+ gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat)
60
62
  gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
61
63
  gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
62
64
  gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
63
65
  gofront init [dir] Scaffold a new GoFront project
64
66
  gofront <file.go> Compile single file and print to stdout
65
67
  gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
66
- gofront <input> -o out.js Compile and write to file
68
+ gofront <input> -o out.js Compile and write to file (wasm packages also emit app.wasm)
67
69
  gofront <input> --check Type-check only
68
70
  gofront <input> --watch Watch for changes and recompile
69
71
  gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
@@ -180,7 +182,7 @@ if (args[0] === "dev") {
180
182
  if (dev.initialError) {
181
183
  log.error(dev.initialError.message);
182
184
  } else {
183
- log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
185
+ log.ok(`— wrote ${formatWrittenDesc(dev.written, dev.outputFile)}`);
184
186
  }
185
187
  log.info(
186
188
  `dev server running → ${colors.cyan(`http://localhost:${dev.port}`)}`,
@@ -276,15 +278,21 @@ if (!watchMode) {
276
278
 
277
279
  if (outputFile) {
278
280
  try {
279
- mkdirSync(dirname(resolve(outputFile)), { recursive: true });
280
- writeFileSync(outputFile, `${js}\n`);
281
- log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
281
+ const written = writeCompileOutput(resolve(outputFile), result, js);
282
+ log.info(
283
+ `wrote ${formatWrittenDesc(written, outputFile)} ${ms(elapsedMs)}`,
284
+ );
282
285
  } catch (e) {
283
286
  log.fail(`cannot write '${outputFile}': ${e.message}`);
284
287
  process.exit(1);
285
288
  }
286
289
  } else {
287
290
  console.log(js);
291
+ if (result.wasm) {
292
+ log.warn(
293
+ "wasm packages were linked but app.wasm needs -o <file> to be written",
294
+ );
295
+ }
288
296
  }
289
297
 
290
298
  if (copyAssetsFlag) {
@@ -328,9 +336,9 @@ function buildOnce(changedFile = null) {
328
336
  const changeNote = changedFile ? ` — ${changedFile} changed` : "";
329
337
  const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
330
338
  if (outputFile) {
331
- writeFileSync(outputFile, `${js}\n`);
339
+ const written = writeCompileOutput(resolve(outputFile), result, js);
332
340
  console.error(
333
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${colors.cyan(outputFile)} ${timing}`,
341
+ `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
334
342
  );
335
343
  } else {
336
344
  // Clear screen then print
package/src/lexer.js CHANGED
@@ -111,6 +111,8 @@ export class Lexer {
111
111
  this.col = 1;
112
112
  this.tokens = [];
113
113
  this.filename = filename;
114
+ this.target = null;
115
+ this.targetDirective = null;
114
116
  }
115
117
 
116
118
  // ── Primitives ──────────────────────────────────────────────
@@ -150,8 +152,22 @@ export class Lexer {
150
152
  if (ch === " " || ch === "\t" || ch === "\r") {
151
153
  this.advance();
152
154
  } else if (ch === "/" && this.peek(1) === "/") {
155
+ const start = this.pos;
156
+ const line = this.line,
157
+ col = this.col;
153
158
  while (this.pos < this.src.length && this.peek() !== "\n")
154
159
  this.advance();
160
+ const comment = this.src.slice(start, this.pos);
161
+ const match = comment.match(/^\/\/gofront:target\s+(\S+)/);
162
+ if (match) {
163
+ this.target = match[1];
164
+ this.targetDirective = {
165
+ value: match[1],
166
+ line,
167
+ col,
168
+ beforePackage: this.tokens.length === 0,
169
+ };
170
+ }
155
171
  } else if (ch === "/" && this.peek(1) === "*") {
156
172
  const startLine = this.line,
157
173
  startCol = this.col;
@@ -472,6 +488,8 @@ export class Lexer {
472
488
 
473
489
  if (this.shouldSemi()) this.push(T.SEMICOLON, ";", this.line, this.col);
474
490
  this.push(T.EOF, "", this.line, this.col);
491
+ this.tokens.target = this.target;
492
+ this.tokens.targetDirective = this.targetDirective;
475
493
  return this.tokens;
476
494
  }
477
495
  _readPlusOp(l, c) {
@@ -0,0 +1,36 @@
1
+ // src/lower/boxing.js
2
+ // Identifies address-taken scalar variables that require heap boxing ({ value: x }).
3
+
4
+ export function isReferenceType(t) {
5
+ if (!t) return false;
6
+ const base = t.kind === "named" ? t.underlying : t;
7
+ return (
8
+ base?.kind === "struct" ||
9
+ base?.kind === "slice" ||
10
+ base?.kind === "map" ||
11
+ base?.kind === "func" ||
12
+ base?.kind === "interface"
13
+ );
14
+ }
15
+
16
+ // Scan AST node for _addressTaken idents on scalars and populate a Set of boxed variable names.
17
+ export function scanAddressTaken(node, boxedVars = new Set()) {
18
+ if (!node || typeof node !== "object") return boxedVars;
19
+ if (Array.isArray(node)) {
20
+ for (const child of node) scanAddressTaken(child, boxedVars);
21
+ return boxedVars;
22
+ }
23
+ // &x — the operand ident will have _addressTaken set by typechecker
24
+ if (node.kind === "Ident" && node._addressTaken) {
25
+ const t = node._type;
26
+ if (t && !isReferenceType(t)) {
27
+ boxedVars.add(node.name);
28
+ }
29
+ }
30
+ // Recurse into FuncLit too — closures may take address of outer vars
31
+ for (const key of Object.keys(node)) {
32
+ if (key.startsWith("_")) continue;
33
+ scanAddressTaken(node[key], boxedVars);
34
+ }
35
+ return boxedVars;
36
+ }
@@ -0,0 +1,179 @@
1
+ // src/lower/captures.js
2
+ // Identifies free variables captured by closures and determines which ones
3
+ // are mutated and thus require heap-allocated environment cells for WASM closures.
4
+
5
+ import { collectMutatedVars, nodeMutatesVar } from "./ownership.js";
6
+
7
+ /**
8
+ * Result of capture analysis on a function or closure.
9
+ */
10
+ export class CaptureAnalysis {
11
+ constructor() {
12
+ // Map<FuncLit, Set<string>>: free variables captured by each closure
13
+ this.capturesByClosure = new Map();
14
+ // Set<string>: variables in the enclosing scope that are captured AND mutated
15
+ this.mutatedCaptures = new Set();
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Finds all variable declarations in a block or list of statements.
21
+ */
22
+ export function collectDeclaredNames(node, names = new Set()) {
23
+ if (!node || typeof node !== "object") return names;
24
+ if (Array.isArray(node)) {
25
+ for (const item of node) collectDeclaredNames(item, names);
26
+ return names;
27
+ }
28
+
29
+ if (node.kind === "VarDecl" || node.kind === "ConstDecl") {
30
+ for (const spec of node.decls ?? []) {
31
+ for (const name of spec.names ?? []) names.add(name);
32
+ }
33
+ } else if (node.kind === "DefineStmt") {
34
+ for (const lhs of node.lhs ?? []) {
35
+ if (lhs.kind === "Ident") names.add(lhs.name);
36
+ }
37
+ } else if (node.kind === "TypeSwitchStmt" && node.assign) {
38
+ names.add(node.assign);
39
+ }
40
+
41
+ // Do not descend into nested functions when collecting local declarations
42
+ if (node.kind === "FuncLit") return names;
43
+
44
+ for (const key of Object.keys(node)) {
45
+ if (key.startsWith("_")) continue;
46
+ collectDeclaredNames(node[key], names);
47
+ }
48
+ return names;
49
+ }
50
+
51
+ /**
52
+ * Finds all identifier references in a node that are not shadowed by inner declarations.
53
+ */
54
+ export function collectReferencedNames(node, localDecls, refs = new Set()) {
55
+ if (!node || typeof node !== "object") return refs;
56
+ if (Array.isArray(node)) {
57
+ for (const item of node) collectReferencedNames(item, localDecls, refs);
58
+ return refs;
59
+ }
60
+
61
+ // In struct literals, field names in KeyValueExpr are not variable references
62
+ if (node.kind === "KeyValueExpr") {
63
+ collectReferencedNames(node.value, localDecls, refs);
64
+ return refs;
65
+ }
66
+
67
+ if (node.kind === "FuncLit") {
68
+ const innerLocals = new Set(localDecls);
69
+ for (const p of node.params ?? []) {
70
+ if (p.name && p.name !== "_") innerLocals.add(p.name);
71
+ }
72
+ for (const r of node.returnType?._namedReturns ?? []) {
73
+ if (r.name && r.name !== "_") innerLocals.add(r.name);
74
+ }
75
+ collectDeclaredNames(node.body, innerLocals);
76
+ for (const key of Object.keys(node)) {
77
+ if (key.startsWith("_")) continue;
78
+ collectReferencedNames(node[key], innerLocals, refs);
79
+ }
80
+ return refs;
81
+ }
82
+
83
+ if (
84
+ node.kind === "Ident" &&
85
+ !localDecls.has(node.name) &&
86
+ node.name !== "_"
87
+ ) {
88
+ refs.add(node.name);
89
+ }
90
+
91
+ // For nested closures, free variables are also referenced
92
+ for (const key of Object.keys(node)) {
93
+ if (key.startsWith("_")) continue;
94
+ collectReferencedNames(node[key], localDecls, refs);
95
+ }
96
+ return refs;
97
+ }
98
+
99
+ /**
100
+ * Analyzes a function or method declaration for closures and their captured variables.
101
+ * @param {Object} fnDecl AST FuncDecl or FuncLit node
102
+ * @returns {CaptureAnalysis}
103
+ */
104
+ export function analyzeCaptures(fnDecl) {
105
+ const result = new CaptureAnalysis();
106
+ if (!fnDecl?.body) return result;
107
+
108
+ // Collect parameters, receiver and named returns of the root function
109
+ const rootScope = new Set();
110
+ if (fnDecl.recvName && fnDecl.recvName !== "_") {
111
+ rootScope.add(fnDecl.recvName);
112
+ }
113
+ for (const p of fnDecl.params ?? []) {
114
+ if (p.name && p.name !== "_") rootScope.add(p.name);
115
+ }
116
+ for (const r of fnDecl.returnType?._namedReturns ?? []) {
117
+ if (r.name && r.name !== "_") rootScope.add(r.name);
118
+ }
119
+ collectDeclaredNames(fnDecl.body, rootScope);
120
+
121
+ // Fast pre-collected set of all mutated variables in the entire function
122
+ const mutatedVars = collectMutatedVars(fnDecl.body);
123
+
124
+ // Traverse closures with lexical scope stack to properly handle arbitrary nesting
125
+ const walkClosures = (node, scopeStack) => {
126
+ if (!node || typeof node !== "object") return;
127
+ if (Array.isArray(node)) {
128
+ for (const item of node) walkClosures(item, scopeStack);
129
+ return;
130
+ }
131
+
132
+ if (node.kind === "FuncLit") {
133
+ const closureLocals = new Set();
134
+ for (const p of node.params ?? []) {
135
+ if (p.name && p.name !== "_") closureLocals.add(p.name);
136
+ }
137
+ for (const r of node.returnType?._namedReturns ?? []) {
138
+ if (r.name && r.name !== "_") closureLocals.add(r.name);
139
+ }
140
+ collectDeclaredNames(node.body, closureLocals);
141
+
142
+ // Visible outer variables from all enclosing ancestor scopes
143
+ const allOuterScope = new Set();
144
+ for (const scope of scopeStack) {
145
+ for (const name of scope) allOuterScope.add(name);
146
+ }
147
+
148
+ const allRefs = collectReferencedNames(node.body, closureLocals);
149
+ const captured = new Set();
150
+ for (const ref of allRefs) {
151
+ if (allOuterScope.has(ref)) {
152
+ captured.add(ref);
153
+ }
154
+ }
155
+ result.capturesByClosure.set(node, captured);
156
+
157
+ for (const varName of captured) {
158
+ if (
159
+ mutatedVars.has(varName) ||
160
+ nodeMutatesVar(fnDecl.body, varName) ||
161
+ nodeMutatesVar(node.body, varName)
162
+ ) {
163
+ result.mutatedCaptures.add(varName);
164
+ }
165
+ }
166
+
167
+ walkClosures(node.body, [...scopeStack, closureLocals]);
168
+ return;
169
+ }
170
+
171
+ for (const key of Object.keys(node)) {
172
+ if (key.startsWith("_")) continue;
173
+ walkClosures(node[key], scopeStack);
174
+ }
175
+ };
176
+
177
+ walkClosures(fnDecl.body, [rootScope]);
178
+ return result;
179
+ }
@@ -0,0 +1,30 @@
1
+ // src/lower/embedding.js
2
+ // Computes promoted methods and delegation stubs for embedded structs.
3
+
4
+ export function computeEmbeddedStubs(name, methodDecls, checker) {
5
+ if (!checker) return [];
6
+ const resolvedType = checker.types.get(name)?.underlying;
7
+ if (resolvedType?.kind !== "struct" || !resolvedType._embeds) return [];
8
+
9
+ const declared = new Set(methodDecls.map((m) => m.name));
10
+ const stubs = [];
11
+
12
+ for (const embed of resolvedType._embeds) {
13
+ const embedName = embed.kind === "named" ? embed.name : null;
14
+ if (!embedName) continue;
15
+ const embedBase = embed.kind === "named" ? embed.underlying : embed;
16
+ if (embedBase?.kind !== "struct" || !embedBase.methods) continue;
17
+
18
+ for (const [mName] of embedBase.methods.entries()) {
19
+ if (!declared.has(mName)) {
20
+ stubs.push({
21
+ methodName: mName,
22
+ embedName,
23
+ });
24
+ declared.add(mName); // Prevent duplicate stubs for diamond embeds
25
+ }
26
+ }
27
+ }
28
+
29
+ return stubs;
30
+ }
@@ -0,0 +1,169 @@
1
+ // src/lower/escape.js
2
+ // Analyzes pointer retention / escaping for arguments across the WASM boundary.
3
+ // In GoFront's hybrid model, `*T` where T is a `both` type (e.g. `*mathx.Vec3`) uses
4
+ // copy-in/copy-out per call. If the WASM side retains the pointer beyond the call,
5
+ // it is flagged as an escape error.
6
+
7
+ import { collectDeclaredNames } from "./captures.js";
8
+ import { rootIdentName } from "./ownership.js";
9
+
10
+ /**
11
+ * Checks whether an expression evaluates to one of the target pointer variables
12
+ * (or an interior pointer into them).
13
+ *
14
+ * @param {Object} expr AST expression node
15
+ * @param {Set<string>} targetNames Set of identifier names being tracked
16
+ * @returns {boolean}
17
+ */
18
+ export function evaluatesToPointer(expr, targetNames) {
19
+ if (!expr || typeof expr !== "object") return false;
20
+ if (expr.kind === "Ident") {
21
+ return targetNames.has(expr.name);
22
+ }
23
+ if (expr.kind === "ParenExpr" || expr.kind === "TypeAssertExpr") {
24
+ return evaluatesToPointer(expr.expr, targetNames);
25
+ }
26
+ // Type conversions: (*Vec3)(p)
27
+ if (expr.kind === "CallExpr" && expr.args?.length === 1) {
28
+ return evaluatesToPointer(expr.args[0], targetNames);
29
+ }
30
+ // Interior pointer: &p.Field or &p[i] retains a pointer into p, or &CompositeLit holding p
31
+ if (expr.kind === "UnaryExpr" && expr.op === "&") {
32
+ const root = rootIdentName(expr.operand);
33
+ if (root != null && targetNames.has(root)) return true;
34
+ return evaluatesToPointer(expr.operand, targetNames);
35
+ }
36
+ // Composite literal holding pointer: Holder{ P: p } or []*Vec{ p }
37
+ if (expr.kind === "CompositeLit") {
38
+ for (const elem of expr.elems ?? []) {
39
+ const val = elem.kind === "KeyValueExpr" ? elem.value : elem;
40
+ if (evaluatesToPointer(val, targetNames)) return true;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Checks if a parameter identifier escapes or is retained in `body`.
48
+ * A parameter `name` is considered retained if it:
49
+ * - is returned from the function
50
+ * - is assigned to a package global or external object field
51
+ * - is stored into a struct field or slice/map element (e.g., `obj.field = p`, `arr[i] = p`)
52
+ * - is captured by a closure that outlives or is returned from the function
53
+ *
54
+ * @param {Object} body AST block of the function
55
+ * @param {string} paramName Name of the parameter to check
56
+ * @returns {boolean} True if param escapes/is retained, false if only used locally in-place
57
+ */
58
+ export function paramEscapes(body, paramName) {
59
+ if (!body || !paramName || paramName === "_") return false;
60
+
61
+ const aliases = new Set([paramName]);
62
+ const localNames = new Set([paramName]);
63
+ collectDeclaredNames(body, localNames);
64
+
65
+ let escapes = false;
66
+
67
+ const walk = (node) => {
68
+ if (escapes || !node || typeof node !== "object") return;
69
+ if (Array.isArray(node)) {
70
+ for (const item of node) walk(item);
71
+ return;
72
+ }
73
+
74
+ // 1. Returning the pointer
75
+ if (node.kind === "ReturnStmt") {
76
+ for (const res of node.values ?? node.results ?? []) {
77
+ if (evaluatesToPointer(res, aliases)) {
78
+ escapes = true;
79
+ return;
80
+ }
81
+ }
82
+ }
83
+
84
+ // 2. Assigning the pointer:
85
+ // Storing into a field, index, or package-level global escapes.
86
+ // Storing into a local variable aliases the pointer.
87
+ if (node.kind === "AssignStmt") {
88
+ for (let i = 0; i < node.lhs.length; i++) {
89
+ const lhs = node.lhs[i];
90
+ const rhs = node.rhs?.[i];
91
+ if (!rhs) continue;
92
+ if (evaluatesToPointer(rhs, aliases)) {
93
+ // Storing into a field or index: `target.field = p` or `arr[i] = p`
94
+ if (lhs.kind === "SelectorExpr" || lhs.kind === "IndexExpr") {
95
+ escapes = true;
96
+ return;
97
+ }
98
+ // If LHS is not a local variable declared in body, it's package-level / global
99
+ if (lhs.kind === "Ident") {
100
+ if (!localNames.has(lhs.name)) {
101
+ escapes = true;
102
+ return;
103
+ }
104
+ aliases.add(lhs.name);
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // 3. Short variable definition: local alias `tmp := p`
111
+ if (node.kind === "DefineStmt") {
112
+ for (let i = 0; i < node.lhs.length; i++) {
113
+ const lhs = node.lhs[i];
114
+ const rhs = node.rhs?.[i];
115
+ if (rhs && evaluatesToPointer(rhs, aliases)) {
116
+ if (lhs.kind === "Ident") {
117
+ aliases.add(lhs.name);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // 4. Storing into append call: `slice = append(slice, p)`
124
+ const fnName = node.func?.name ?? node.fun?.name;
125
+ if (node.kind === "CallExpr" && fnName === "append") {
126
+ for (const arg of node.args?.slice(1) ?? []) {
127
+ if (evaluatesToPointer(arg, aliases)) {
128
+ escapes = true;
129
+ return;
130
+ }
131
+ }
132
+ }
133
+
134
+ // 5. Storing into a closure (FuncLit)
135
+ if (node.kind === "FuncLit") {
136
+ let captured = false;
137
+ const checkClosure = (cNode) => {
138
+ if (captured || !cNode || typeof cNode !== "object") return;
139
+ if (Array.isArray(cNode)) {
140
+ for (const item of cNode) checkClosure(item);
141
+ return;
142
+ }
143
+ if (cNode.kind === "Ident" && aliases.has(cNode.name)) {
144
+ captured = true;
145
+ return;
146
+ }
147
+ for (const k of Object.keys(cNode)) {
148
+ if (k.startsWith("_")) continue;
149
+ checkClosure(cNode[k]);
150
+ }
151
+ };
152
+ checkClosure(node.body);
153
+ if (captured) {
154
+ escapes = true;
155
+ return;
156
+ }
157
+ // Do not descend redundantly into closure body
158
+ return;
159
+ }
160
+
161
+ for (const key of Object.keys(node)) {
162
+ if (key.startsWith("_")) continue;
163
+ walk(node[key]);
164
+ }
165
+ };
166
+
167
+ walk(body);
168
+ return escapes;
169
+ }