gofront 1.4.0 → 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 +72 -9
- package/package.json +2 -1
- package/src/{codegen → backend/js}/expressions.js +217 -60
- package/src/{codegen → backend/js}/index.js +21 -35
- 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 +2 -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 +322 -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 +1 -0
- package/src/test-runner.js +74 -20
- package/src/typechecker/expressions.js +40 -1
- package/src/typechecker/index.js +234 -4
- 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}/runtime.js +0 -0
- /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/parser/index.js
CHANGED
|
@@ -114,6 +114,8 @@ export class Parser {
|
|
|
114
114
|
pkg,
|
|
115
115
|
imports,
|
|
116
116
|
decls,
|
|
117
|
+
target: this.tokens?.target ?? null,
|
|
118
|
+
_targetDirective: this.tokens?.targetDirective ?? null,
|
|
117
119
|
_filename: this.filename,
|
|
118
120
|
_source: this.source,
|
|
119
121
|
};
|
|
@@ -130,7 +132,9 @@ export class Parser {
|
|
|
130
132
|
this.expect(T.IMPORT);
|
|
131
133
|
const imports = [];
|
|
132
134
|
const parseOne = () => {
|
|
133
|
-
const
|
|
135
|
+
const tok = this.peek();
|
|
136
|
+
const _line = tok.line;
|
|
137
|
+
const _col = tok.col;
|
|
134
138
|
let alias = null;
|
|
135
139
|
if (this.check(T.IDENT)) {
|
|
136
140
|
alias = this.advance().value;
|
|
@@ -138,8 +142,9 @@ export class Parser {
|
|
|
138
142
|
alias = ".";
|
|
139
143
|
this.advance();
|
|
140
144
|
}
|
|
141
|
-
const
|
|
142
|
-
|
|
145
|
+
const pathTok = this.expect(T.STRING);
|
|
146
|
+
const path = pathTok.value;
|
|
147
|
+
return { path, alias, _line, _col, line: pathTok.line, col: pathTok.col };
|
|
143
148
|
};
|
|
144
149
|
if (this.match(T.LPAREN)) {
|
|
145
150
|
while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
|
package/src/resolver.js
CHANGED
package/src/test-runner.js
CHANGED
|
@@ -6,8 +6,13 @@ import { statSync } from "node:fs";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { HELPER_SPRINTF, HELPER_TESTING } from "./backend/js/runtime.js";
|
|
9
10
|
import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
compilePackageTests,
|
|
13
|
+
compilePackageTestsWasm,
|
|
14
|
+
gwFilesIn,
|
|
15
|
+
} from "./compiler.js";
|
|
11
16
|
|
|
12
17
|
export function isTestFunc(decl) {
|
|
13
18
|
if (decl.kind !== "FuncDecl") return false;
|
|
@@ -78,7 +83,16 @@ globalThis.document = globalThis.document ?? null;
|
|
|
78
83
|
`
|
|
79
84
|
}
|
|
80
85
|
globalThis.__gofront_verbose = ${verbose};
|
|
81
|
-
|
|
86
|
+
${
|
|
87
|
+
options.wasmBytes
|
|
88
|
+
? `
|
|
89
|
+
// ── WASM module (tests run inside app.wasm; *testing.T stays in JS) ──
|
|
90
|
+
${HELPER_SPRINTF}
|
|
91
|
+
${HELPER_TESTING}
|
|
92
|
+
globalThis.__GOFRONT_WASM_BYTES = Uint8Array.from(atob(${JSON.stringify(Buffer.from(options.wasmBytes).toString("base64"))}), (c) => c.charCodeAt(0));
|
|
93
|
+
`
|
|
94
|
+
: ""
|
|
95
|
+
}
|
|
82
96
|
// ── Compiled Package Bundle ──
|
|
83
97
|
${bundleJs}
|
|
84
98
|
// ─────────────────────────────
|
|
@@ -281,6 +295,44 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
|
281
295
|
});
|
|
282
296
|
}
|
|
283
297
|
|
|
298
|
+
function reportBuildFailure(err, resolvedDir, options) {
|
|
299
|
+
const msg = err?.message ?? String(err);
|
|
300
|
+
if (options.captureOutput) {
|
|
301
|
+
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
|
|
302
|
+
return { exitCode: 1, stdout: "", stderr: out };
|
|
303
|
+
}
|
|
304
|
+
const dest = options.stderr ?? process.stderr;
|
|
305
|
+
const c = colorsFor(options, dest);
|
|
306
|
+
dest.write(
|
|
307
|
+
`${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
|
|
308
|
+
);
|
|
309
|
+
return { exitCode: 1 };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// `both` packages run twice (JS and wasm) so the two backends are kept in
|
|
313
|
+
// agreement; output of both runs is concatenated.
|
|
314
|
+
async function runDualTarget(compiled, resolvedDir, runHarness, options) {
|
|
315
|
+
let wasmCompiled;
|
|
316
|
+
try {
|
|
317
|
+
wasmCompiled = compilePackageTestsWasm(resolvedDir, options);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return reportBuildFailure(err, resolvedDir, options);
|
|
320
|
+
}
|
|
321
|
+
const label = compiled.pkgName;
|
|
322
|
+
const jsRun = await runHarness(compiled.js, `${label} [js]`, null);
|
|
323
|
+
const wasmRun = await runHarness(
|
|
324
|
+
wasmCompiled.js,
|
|
325
|
+
`${label} [wasm]`,
|
|
326
|
+
wasmCompiled.wasm,
|
|
327
|
+
);
|
|
328
|
+
return {
|
|
329
|
+
exitCode: jsRun.exitCode !== 0 ? jsRun.exitCode : wasmRun.exitCode,
|
|
330
|
+
stdout: jsRun.stdout + wasmRun.stdout,
|
|
331
|
+
stderr: jsRun.stderr + wasmRun.stderr,
|
|
332
|
+
testCount: jsRun.testCount + wasmRun.testCount,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
284
336
|
export async function runTests(targetDir, options = {}) {
|
|
285
337
|
let resolvedDir = resolve(targetDir);
|
|
286
338
|
try {
|
|
@@ -304,17 +356,7 @@ export async function runTests(targetDir, options = {}) {
|
|
|
304
356
|
try {
|
|
305
357
|
compiled = compilePackageTests(resolvedDir, options);
|
|
306
358
|
} catch (err) {
|
|
307
|
-
|
|
308
|
-
if (options.captureOutput) {
|
|
309
|
-
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
|
|
310
|
-
return { exitCode: 1, stdout: "", stderr: out };
|
|
311
|
-
}
|
|
312
|
-
const dest = options.stderr ?? process.stderr;
|
|
313
|
-
const c = colorsFor(options, dest);
|
|
314
|
-
dest.write(
|
|
315
|
-
`${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
|
|
316
|
-
);
|
|
317
|
-
return { exitCode: 1 };
|
|
359
|
+
return reportBuildFailure(err, resolvedDir, options);
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
const { pkgName, js, programs } = compiled;
|
|
@@ -324,12 +366,24 @@ export async function runTests(targetDir, options = {}) {
|
|
|
324
366
|
return reportEmptyTests(pkgName, "no tests to run", options);
|
|
325
367
|
}
|
|
326
368
|
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
369
|
+
const runHarness = (bundleJs, label, wasmBytes) =>
|
|
370
|
+
spawnTestRunner(
|
|
371
|
+
resolvedDir,
|
|
372
|
+
generateTestHarness(bundleJs, testNames, {
|
|
373
|
+
...options,
|
|
374
|
+
pkgName: label,
|
|
375
|
+
jsdomPath,
|
|
376
|
+
wasmBytes,
|
|
377
|
+
color: colorsFor(options, options.stdout ?? process.stdout).enabled,
|
|
378
|
+
}),
|
|
379
|
+
testNames.length,
|
|
380
|
+
options,
|
|
381
|
+
);
|
|
333
382
|
|
|
334
|
-
|
|
383
|
+
// `wasm` packages run inside the linked app.wasm; `js` packages that import
|
|
384
|
+
// wasm packages need the linked module too (hybrid packages).
|
|
385
|
+
if (compiled.target === "both") {
|
|
386
|
+
return runDualTarget(compiled, resolvedDir, runHarness, options);
|
|
387
|
+
}
|
|
388
|
+
return runHarness(js, pkgName, compiled.wasm ?? null);
|
|
335
389
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
ANY,
|
|
5
5
|
BOOL,
|
|
6
|
+
BROWSER_GLOBALS,
|
|
6
7
|
COMPLEX128,
|
|
7
8
|
ERROR,
|
|
8
9
|
FLOAT64,
|
|
@@ -126,6 +127,26 @@ export const expressionCheckMethods = {
|
|
|
126
127
|
const t = scope.lookup(expr.name);
|
|
127
128
|
if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
|
|
128
129
|
if (this.types.has(expr.name)) expr._isTypeRef = true;
|
|
130
|
+
if (
|
|
131
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
132
|
+
BROWSER_GLOBALS.has(expr.name) &&
|
|
133
|
+
scope.lookup(expr.name) === this.globals.lookup(expr.name)
|
|
134
|
+
) {
|
|
135
|
+
this.recordBlocker("browser global", expr.name);
|
|
136
|
+
this.err(
|
|
137
|
+
`'${expr.name}' is not available in wasm packages`,
|
|
138
|
+
expr,
|
|
139
|
+
`(package '${this.pkgName}' is //gofront:target ${this.target} — browser globals are only available in js packages)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
144
|
+
expr.name === "gom" &&
|
|
145
|
+
scope.lookup(expr.name) === this.globals.lookup(expr.name)
|
|
146
|
+
) {
|
|
147
|
+
this.recordBlocker("gom usage", "gom");
|
|
148
|
+
this.err("package 'gom' is not available in wasm packages", expr);
|
|
149
|
+
}
|
|
129
150
|
return t;
|
|
130
151
|
}
|
|
131
152
|
case "BinaryExpr": {
|
|
@@ -136,8 +157,16 @@ export const expressionCheckMethods = {
|
|
|
136
157
|
return this.binaryResultType(expr.op, ANY, ANY, expr);
|
|
137
158
|
return this.binaryResultType(expr.op, lt, rt, expr);
|
|
138
159
|
}
|
|
139
|
-
case "AwaitExpr":
|
|
160
|
+
case "AwaitExpr": {
|
|
161
|
+
if (this.target === "wasm" || this.target === "both") {
|
|
162
|
+
this.recordBlocker("async function");
|
|
163
|
+
this.err(
|
|
164
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
165
|
+
expr,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
140
168
|
return this.checkExpr(expr.expr, scope);
|
|
169
|
+
}
|
|
141
170
|
case "InstantiationExpr":
|
|
142
171
|
return this._checkInstantiationExpr(expr, scope);
|
|
143
172
|
default: {
|
|
@@ -280,6 +309,16 @@ export const expressionCheckMethods = {
|
|
|
280
309
|
},
|
|
281
310
|
|
|
282
311
|
_checkFuncLit(expr, scope) {
|
|
312
|
+
if (
|
|
313
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
314
|
+
(expr.async || expr.isAsync)
|
|
315
|
+
) {
|
|
316
|
+
this.recordBlocker("async function");
|
|
317
|
+
this.err(
|
|
318
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
319
|
+
expr,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
283
322
|
const inner = new Scope(scope);
|
|
284
323
|
for (const p of expr.params)
|
|
285
324
|
inner.define(p.name, p.type ? this.resolveTypeNode(p.type, scope) : ANY);
|
package/src/typechecker/index.js
CHANGED
|
@@ -25,9 +25,18 @@ import {
|
|
|
25
25
|
TAINTED_ANY,
|
|
26
26
|
TypeCheckError,
|
|
27
27
|
typeStr,
|
|
28
|
+
VALID_TARGETS,
|
|
28
29
|
VOID,
|
|
29
30
|
} from "./types.js";
|
|
30
31
|
|
|
32
|
+
function rootIdentName(e) {
|
|
33
|
+
if (!e) return null;
|
|
34
|
+
if (e.kind === "Ident") return e.name;
|
|
35
|
+
if (e.kind === "SelectorExpr" || e.kind === "IndexExpr")
|
|
36
|
+
return rootIdentName(e.expr);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
// Re-export for consumers that import from typechecker.js
|
|
32
41
|
export { TypeCheckError, typeStr };
|
|
33
42
|
|
|
@@ -36,6 +45,10 @@ export class TypeChecker {
|
|
|
36
45
|
this.types = new Map(); // named types
|
|
37
46
|
this.globals = new Scope();
|
|
38
47
|
this.errors = [];
|
|
48
|
+
this.target = "js";
|
|
49
|
+
this.pkgName = null;
|
|
50
|
+
this.blockers = new Map();
|
|
51
|
+
this.summary = null;
|
|
39
52
|
this._currentFile = null; // filename of file currently being checked
|
|
40
53
|
this._currentSource = null;
|
|
41
54
|
this._loopDepth = 0; // for break/continue validation
|
|
@@ -117,12 +130,42 @@ export class TypeChecker {
|
|
|
117
130
|
return new Map(this.types);
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
|
|
133
|
+
recordBlocker(category, item = null) {
|
|
134
|
+
if (!this.blockers.has(category)) {
|
|
135
|
+
this.blockers.set(category, []);
|
|
136
|
+
}
|
|
137
|
+
this.blockers.get(category).push(item);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getSummary() {
|
|
141
|
+
if (this.blockers.size === 0) return null;
|
|
142
|
+
let total = 0;
|
|
143
|
+
const parts = [];
|
|
144
|
+
for (const [category, items] of this.blockers) {
|
|
145
|
+
const uniqueItems = [
|
|
146
|
+
...new Set(items.filter((x) => x != null && x !== "")),
|
|
147
|
+
];
|
|
148
|
+
const count = uniqueItems.length > 0 ? uniqueItems.length : items.length;
|
|
149
|
+
total += count;
|
|
150
|
+
const itemsStr =
|
|
151
|
+
uniqueItems.length > 0 ? ` (${uniqueItems.join(", ")})` : "";
|
|
152
|
+
// "gom usage" is a mass noun; everything else pluralises on the last word.
|
|
153
|
+
const label =
|
|
154
|
+
count === 1 || category === "gom usage" ? category : `${category}s`;
|
|
155
|
+
parts.push(`${count} ${label}${itemsStr}`);
|
|
156
|
+
}
|
|
157
|
+
const noun = total === 1 ? "blocker" : "blockers";
|
|
158
|
+
const what = this.target === "both" ? "both" : "wasm";
|
|
159
|
+
return `package '${this.pkgName}' cannot be ${what}: ${total} ${noun} — ${parts.join(", ")}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
err(msg, node, hint = null) {
|
|
121
163
|
const e = new TypeCheckError(
|
|
122
164
|
msg,
|
|
123
165
|
node,
|
|
124
166
|
this._currentFile,
|
|
125
167
|
this._currentSource,
|
|
168
|
+
hint,
|
|
126
169
|
);
|
|
127
170
|
this.errors.push(e);
|
|
128
171
|
return TAINTED_ANY; // tainted recovery type — suppresses downstream cascade errors
|
|
@@ -163,18 +206,181 @@ export class TypeChecker {
|
|
|
163
206
|
}
|
|
164
207
|
}
|
|
165
208
|
|
|
166
|
-
// Like check() but operates over multiple programs (same-package multi-file).
|
|
167
|
-
// All passes run across all files before moving to the next pass,
|
|
168
|
-
// so every file sees every other file's declarations.
|
|
169
209
|
checkAll(programs) {
|
|
210
|
+
for (const p of programs) {
|
|
211
|
+
const d = p._targetDirective;
|
|
212
|
+
if (!d) continue;
|
|
213
|
+
this._setCurrentFile(p);
|
|
214
|
+
if (!VALID_TARGETS.has(d.value)) {
|
|
215
|
+
this.err(
|
|
216
|
+
`unknown //gofront:target '${d.value}' (expected js, wasm or both)`,
|
|
217
|
+
d,
|
|
218
|
+
);
|
|
219
|
+
p.target = null;
|
|
220
|
+
if (this.target === d.value) this.target = "js";
|
|
221
|
+
} else if (!d.beforePackage) {
|
|
222
|
+
this.err("//gofront:target must appear before the package clause", d);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (programs.length > 0) {
|
|
226
|
+
if (!this.target || this.target === "js") {
|
|
227
|
+
const pTarget = programs.find((p) => p.target)?.target;
|
|
228
|
+
if (pTarget) this.target = pTarget;
|
|
229
|
+
}
|
|
230
|
+
if (!this.pkgName) {
|
|
231
|
+
this.pkgName = programs[0].pkg?.name ?? null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (this.target === "wasm" || this.target === "both") {
|
|
236
|
+
for (const p of programs) {
|
|
237
|
+
if (p._filename?.endsWith(".templ")) {
|
|
238
|
+
this._setCurrentFile(p);
|
|
239
|
+
const fileName = p._filename.split("/").pop();
|
|
240
|
+
this.recordBlocker(".templ file", fileName);
|
|
241
|
+
this.err(
|
|
242
|
+
`.templ files are not allowed in wasm packages; move '${fileName}' to a js package`,
|
|
243
|
+
p.pkg,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
170
249
|
this._collectTypesPass(programs);
|
|
171
250
|
this._collectFuncsPass(programs);
|
|
172
251
|
this._promoteEmbeddedMethods();
|
|
173
252
|
this._collectVarsConstsPass(programs);
|
|
174
253
|
this._checkTopDeclsPass(programs);
|
|
254
|
+
|
|
255
|
+
if (this.target === "both") {
|
|
256
|
+
this._checkMutablePackageVars(programs);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (this.blockers.size > 0) {
|
|
260
|
+
const summaryLine = this.getSummary();
|
|
261
|
+
this.summary = summaryLine;
|
|
262
|
+
const summaryErr = new Error(summaryLine);
|
|
263
|
+
summaryErr.isSummary = true;
|
|
264
|
+
this.errors.push(summaryErr);
|
|
265
|
+
}
|
|
266
|
+
|
|
175
267
|
return this.errors;
|
|
176
268
|
}
|
|
177
269
|
|
|
270
|
+
_checkMutablePackageVars(programs) {
|
|
271
|
+
const pkgVars = new Set();
|
|
272
|
+
for (const p of programs) {
|
|
273
|
+
for (const d of p.decls) {
|
|
274
|
+
if (d.kind === "VarDecl") {
|
|
275
|
+
for (const spec of d.decls) {
|
|
276
|
+
for (const name of spec.names) {
|
|
277
|
+
if (name !== "_") pkgVars.add(name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (pkgVars.size === 0) return;
|
|
284
|
+
|
|
285
|
+
const SCOPE_KINDS = new Set([
|
|
286
|
+
"Block",
|
|
287
|
+
"FuncLit",
|
|
288
|
+
"ForStmt",
|
|
289
|
+
"IfStmt",
|
|
290
|
+
"SwitchStmt",
|
|
291
|
+
"TypeSwitchStmt",
|
|
292
|
+
"CaseClause",
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const report = (root, node) => {
|
|
296
|
+
this.recordBlocker("mutable package variable", root);
|
|
297
|
+
this.err(
|
|
298
|
+
`package-level variable '${root}' is mutated; not allowed in 'both' packages (each target gets its own copy)`,
|
|
299
|
+
node,
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const checkBody = (body, initialLocals) => {
|
|
304
|
+
const scopes = [new Set(initialLocals)];
|
|
305
|
+
const isLocal = (name) => scopes.some((s) => s.has(name));
|
|
306
|
+
const declare = (name) => {
|
|
307
|
+
if (name && name !== "_") scopes[scopes.length - 1].add(name);
|
|
308
|
+
};
|
|
309
|
+
const flag = (e, node) => {
|
|
310
|
+
const root = rootIdentName(e);
|
|
311
|
+
if (root && pkgVars.has(root) && !isLocal(root)) report(root, node);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const walk = (node) => {
|
|
315
|
+
if (!node || typeof node !== "object") return;
|
|
316
|
+
if (Array.isArray(node)) {
|
|
317
|
+
for (const item of node) walk(item);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const opensScope = SCOPE_KINDS.has(node.kind);
|
|
321
|
+
if (opensScope) scopes.push(new Set());
|
|
322
|
+
switch (node.kind) {
|
|
323
|
+
case "FuncLit":
|
|
324
|
+
for (const p of node.params ?? []) declare(p.name);
|
|
325
|
+
for (const r of node.returnType?._namedReturns ?? [])
|
|
326
|
+
declare(r.name);
|
|
327
|
+
break;
|
|
328
|
+
case "VarDecl":
|
|
329
|
+
for (const spec of node.decls ?? [])
|
|
330
|
+
for (const name of spec.names ?? []) declare(name);
|
|
331
|
+
break;
|
|
332
|
+
case "DefineStmt":
|
|
333
|
+
for (const e of node.lhs ?? [])
|
|
334
|
+
if (e.kind === "Ident") declare(e.name);
|
|
335
|
+
break;
|
|
336
|
+
case "TypeSwitchStmt":
|
|
337
|
+
declare(node.assign);
|
|
338
|
+
break;
|
|
339
|
+
case "AssignStmt":
|
|
340
|
+
for (const e of node.lhs) flag(e, node);
|
|
341
|
+
break;
|
|
342
|
+
case "IncDecStmt":
|
|
343
|
+
flag(node.expr, node);
|
|
344
|
+
break;
|
|
345
|
+
case "UnaryExpr":
|
|
346
|
+
if (node.op === "&") flag(node.operand, node);
|
|
347
|
+
break;
|
|
348
|
+
case "SelectorExpr":
|
|
349
|
+
// Pointer-receiver method call or value: `G.Bump()` mutates G.
|
|
350
|
+
if (node._isMethodValue && node._type?._ptrRecv)
|
|
351
|
+
flag(node.expr, node);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
for (const key of Object.keys(node)) {
|
|
355
|
+
if (key.startsWith("_")) continue;
|
|
356
|
+
walk(node[key]);
|
|
357
|
+
}
|
|
358
|
+
if (opensScope) scopes.pop();
|
|
359
|
+
};
|
|
360
|
+
walk(body);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
for (const p of programs) {
|
|
364
|
+
if (p._filename?.endsWith("_test.go")) continue;
|
|
365
|
+
this._currentFile = p._filename;
|
|
366
|
+
this._currentSource = p._source;
|
|
367
|
+
for (const d of p.decls) {
|
|
368
|
+
if ((d.kind === "FuncDecl" || d.kind === "MethodDecl") && d.body) {
|
|
369
|
+
if (d.name === "init" && d.kind === "FuncDecl") continue;
|
|
370
|
+
const locals = [];
|
|
371
|
+
if (d.recvName) locals.push(d.recvName);
|
|
372
|
+
for (const prm of d.params ?? []) if (prm.name) locals.push(prm.name);
|
|
373
|
+
for (const r of d.returnType?._namedReturns ?? [])
|
|
374
|
+
if (r.name) locals.push(r.name);
|
|
375
|
+
checkBody(d.body, locals);
|
|
376
|
+
} else if (d.kind === "VarDecl") {
|
|
377
|
+
// Closures in package-level initializers run after init.
|
|
378
|
+
for (const spec of d.decls ?? []) checkBody(spec.value, []);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
178
384
|
_collectTypesPass(programs) {
|
|
179
385
|
for (const p of programs) {
|
|
180
386
|
for (const d of p.decls) {
|
|
@@ -507,6 +713,10 @@ export class TypeChecker {
|
|
|
507
713
|
}
|
|
508
714
|
|
|
509
715
|
_collectCssDecl(decl) {
|
|
716
|
+
if (this.target === "wasm" || this.target === "both") {
|
|
717
|
+
this.recordBlocker("css declaration");
|
|
718
|
+
this.err("css declarations are not allowed in wasm packages", decl);
|
|
719
|
+
}
|
|
510
720
|
const paramTypes = (decl.params || []).map((p) =>
|
|
511
721
|
this.resolveTypeNode(p.type, this.globals),
|
|
512
722
|
);
|
|
@@ -551,6 +761,16 @@ export class TypeChecker {
|
|
|
551
761
|
}
|
|
552
762
|
|
|
553
763
|
checkFuncDecl(decl, outer) {
|
|
764
|
+
if (
|
|
765
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
766
|
+
(decl.async || decl.isAsync)
|
|
767
|
+
) {
|
|
768
|
+
this.recordBlocker("async function", decl.name);
|
|
769
|
+
this.err(
|
|
770
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
771
|
+
decl,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
554
774
|
const inner = new Scope(outer);
|
|
555
775
|
this._injectTypeParams(decl, inner, outer);
|
|
556
776
|
for (const p of decl.params) {
|
|
@@ -562,6 +782,16 @@ export class TypeChecker {
|
|
|
562
782
|
}
|
|
563
783
|
|
|
564
784
|
checkMethodDecl(decl, outer) {
|
|
785
|
+
if (
|
|
786
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
787
|
+
(decl.async || decl.isAsync)
|
|
788
|
+
) {
|
|
789
|
+
this.recordBlocker("async function", decl.name);
|
|
790
|
+
this.err(
|
|
791
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
792
|
+
decl,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
565
795
|
const inner = new Scope(outer);
|
|
566
796
|
const recvTypeName =
|
|
567
797
|
decl.recvType.kind === "GenericTypeName"
|
|
@@ -43,6 +43,13 @@ export const statementCheckMethods = {
|
|
|
43
43
|
this.checkExpr(stmt.expr, scope);
|
|
44
44
|
break;
|
|
45
45
|
case "DeferStmt":
|
|
46
|
+
if (this.target === "wasm" || this.target === "both") {
|
|
47
|
+
this.recordBlocker("defer statement");
|
|
48
|
+
this.err(
|
|
49
|
+
"'defer' is not yet supported in wasm packages (planned)",
|
|
50
|
+
stmt,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
46
53
|
if (stmt.call.kind !== "CallExpr")
|
|
47
54
|
this.err("defer requires a function call", stmt.call);
|
|
48
55
|
this.checkExpr(stmt.call, scope);
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
INT,
|
|
10
10
|
STRING,
|
|
11
11
|
UINT8,
|
|
12
|
+
UINT32,
|
|
13
|
+
UINT64,
|
|
12
14
|
VOID,
|
|
13
15
|
} from "../types.js";
|
|
14
16
|
|
|
@@ -428,6 +430,21 @@ export function setupCoreGlobals(globals, types) {
|
|
|
428
430
|
},
|
|
429
431
|
});
|
|
430
432
|
|
|
433
|
+
globals.define("bits", {
|
|
434
|
+
kind: "namespace",
|
|
435
|
+
name: "bits",
|
|
436
|
+
members: {
|
|
437
|
+
LeadingZeros32: { kind: "func", params: [UINT32], returns: [INT] },
|
|
438
|
+
TrailingZeros32: { kind: "func", params: [UINT32], returns: [INT] },
|
|
439
|
+
OnesCount32: { kind: "func", params: [UINT32], returns: [INT] },
|
|
440
|
+
RotateLeft32: { kind: "func", params: [UINT32, INT], returns: [UINT32] },
|
|
441
|
+
LeadingZeros64: { kind: "func", params: [UINT64], returns: [INT] },
|
|
442
|
+
TrailingZeros64: { kind: "func", params: [UINT64], returns: [INT] },
|
|
443
|
+
OnesCount64: { kind: "func", params: [UINT64], returns: [INT] },
|
|
444
|
+
RotateLeft64: { kind: "func", params: [UINT64, INT], returns: [UINT64] },
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
431
448
|
globals.define("errors", {
|
|
432
449
|
kind: "namespace",
|
|
433
450
|
name: "errors",
|
package/src/typechecker/types.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// { kind: 'named', name: string, underlying: Type }
|
|
14
14
|
|
|
15
15
|
export class TypeCheckError extends Error {
|
|
16
|
-
constructor(msg, node, filename, sourceCode) {
|
|
16
|
+
constructor(msg, node, filename, sourceCode, hint = null) {
|
|
17
17
|
const lineNum = node?.line || node?._line;
|
|
18
18
|
const colNum = node?.col || node?._col;
|
|
19
19
|
const loc = filename
|
|
@@ -23,6 +23,7 @@ export class TypeCheckError extends Error {
|
|
|
23
23
|
: lineNum
|
|
24
24
|
? ` at line ${lineNum}:${colNum ?? 1}`
|
|
25
25
|
: "";
|
|
26
|
+
const hintText = hint ? `\n ${hint}` : "";
|
|
26
27
|
let lineContext = "";
|
|
27
28
|
if (lineNum && sourceCode) {
|
|
28
29
|
const lines = sourceCode.split("\n");
|
|
@@ -33,10 +34,63 @@ export class TypeCheckError extends Error {
|
|
|
33
34
|
lineContext = `\n${prefix}${lineStr}\n${" ".repeat(prefix.length)}${caretPad}^`;
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
super(`Type error${loc}: ${msg}${lineContext}`);
|
|
37
|
+
super(`Type error${loc}: ${msg}${hintText}${lineContext}`);
|
|
38
|
+
this.line = lineNum;
|
|
39
|
+
this.col = colNum;
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
export const BROWSER_GLOBALS = new Set([
|
|
44
|
+
"console",
|
|
45
|
+
"document",
|
|
46
|
+
"window",
|
|
47
|
+
"navigator",
|
|
48
|
+
"location",
|
|
49
|
+
"history",
|
|
50
|
+
"screen",
|
|
51
|
+
"performance",
|
|
52
|
+
"crypto",
|
|
53
|
+
"indexedDB",
|
|
54
|
+
"fetch",
|
|
55
|
+
"setTimeout",
|
|
56
|
+
"setInterval",
|
|
57
|
+
"clearTimeout",
|
|
58
|
+
"clearInterval",
|
|
59
|
+
"requestAnimationFrame",
|
|
60
|
+
"cancelAnimationFrame",
|
|
61
|
+
"Math",
|
|
62
|
+
"JSON",
|
|
63
|
+
"Date",
|
|
64
|
+
"RegExp",
|
|
65
|
+
"Promise",
|
|
66
|
+
"Error",
|
|
67
|
+
"Symbol",
|
|
68
|
+
"String",
|
|
69
|
+
"Number",
|
|
70
|
+
"Boolean",
|
|
71
|
+
"Array",
|
|
72
|
+
"Object",
|
|
73
|
+
"parseInt",
|
|
74
|
+
"parseFloat",
|
|
75
|
+
"isNaN",
|
|
76
|
+
"isFinite",
|
|
77
|
+
"encodeURIComponent",
|
|
78
|
+
"decodeURIComponent",
|
|
79
|
+
"atob",
|
|
80
|
+
"btoa",
|
|
81
|
+
"alert",
|
|
82
|
+
"confirm",
|
|
83
|
+
"prompt",
|
|
84
|
+
"localStorage",
|
|
85
|
+
"sessionStorage",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Stdlib packages the WASM backend can emit. Extend as src/backend/wasm/emit.js gains support;
|
|
89
|
+
// anything not listed is rejected at import with "not yet available in wasm packages".
|
|
90
|
+
export const WASM_SUPPORTED_STDLIB = new Set(["math", "math/bits", "testing"]);
|
|
91
|
+
|
|
92
|
+
export const VALID_TARGETS = new Set(["js", "wasm", "both"]);
|
|
93
|
+
|
|
40
94
|
// ── Static operator sets (module-level for reuse) ────────────
|
|
41
95
|
export const CMP_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
|
|
42
96
|
export const LOG_OPS = new Set(["&&", "||"]);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|