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/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
|
-
|
|
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
|
|
165
|
-
|
|
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
|
|
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(
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
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 {
|
|
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 ${
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
339
|
+
const written = writeCompileOutput(resolve(outputFile), result, js);
|
|
332
340
|
console.error(
|
|
333
|
-
`${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${
|
|
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
|
+
}
|