gofront 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -403
- package/package.json +16 -19
- package/src/{codegen → backend/js}/expressions.js +265 -77
- package/src/{codegen → backend/js}/index.js +120 -89
- package/src/{codegen → backend/js}/runtime.js +102 -0
- package/src/{codegen → backend/js}/source-map.js +32 -0
- package/src/{codegen → backend/js}/statements.js +139 -64
- package/src/backend/js/stdlib/bits.js +32 -0
- package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
- package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
- package/src/{codegen → backend/js}/stdlib/index.js +6 -0
- package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
- package/src/{codegen → backend/js}/stdlib/path.js +2 -2
- package/src/backend/js/stdlib/shared.js +17 -0
- package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
- package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
- package/src/backend/js/stdlib/strconv.js +58 -0
- package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
- package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
- package/src/{codegen → backend/js}/stdlib/time.js +3 -3
- package/src/{codegen → backend/js}/templ.js +3 -3
- package/src/backend/wasm/boundary.js +1798 -0
- package/src/backend/wasm/emit-builtins.js +1103 -0
- package/src/backend/wasm/emit-exprs.js +1908 -0
- package/src/backend/wasm/emit-maps.js +1863 -0
- package/src/backend/wasm/emit-shared.js +519 -0
- package/src/backend/wasm/emit-stdlib.js +3006 -0
- package/src/backend/wasm/emit-stmts.js +2472 -0
- package/src/backend/wasm/emit.js +1376 -0
- package/src/backend/wasm/encode.js +937 -0
- package/src/backend/wasm/glue.js +144 -0
- package/src/backend/wasm/index.js +2790 -0
- package/src/backend/wasm/monomorph.js +711 -0
- package/src/backend/wasm/optimize.js +227 -0
- package/src/backend/wasm/types.js +544 -0
- package/src/backend/wasm/wat.js +302 -0
- package/src/cli-core.js +323 -75
- package/src/compiler.js +447 -14
- package/src/dev-server.js +56 -15
- package/src/index.js +99 -120
- package/src/lexer.js +18 -0
- package/src/lower/boxing.js +35 -0
- package/src/lower/captures.js +172 -0
- package/src/lower/embedding.js +30 -0
- package/src/lower/escape.js +164 -0
- package/src/lower/functions.js +302 -0
- package/src/lower/globals.js +316 -0
- package/src/lower/index.js +118 -0
- package/src/lower/ownership.js +137 -0
- package/src/lower/range.js +67 -0
- package/src/lower/walk.js +25 -0
- package/src/minifier.js +2 -0
- package/src/parser/index.js +8 -3
- package/src/resolver.js +3 -1
- package/src/test-runner.js +94 -20
- package/src/typechecker/assignability.js +42 -1
- package/src/typechecker/expressions.js +182 -3
- package/src/typechecker/index.js +261 -5
- package/src/typechecker/resolve.js +35 -6
- package/src/typechecker/stdlib/core.js +34 -19
- package/src/typechecker/stdlib/extended.js +18 -3
- package/src/typechecker/stdlib/shared.js +53 -0
- package/src/typechecker/stdlib.js +2 -0
- package/src/typechecker/types.js +259 -6
- package/src/vendor.js +2 -2
- package/src/codegen/stdlib/strconv.js +0 -33
- /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/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/math.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/os.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/unicode.js +0 -0
- /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/src/resolver.js
CHANGED
|
@@ -33,6 +33,7 @@ const BUILTIN_PACKAGES = new Set([
|
|
|
33
33
|
"strconv",
|
|
34
34
|
"sort",
|
|
35
35
|
"math",
|
|
36
|
+
"math/bits",
|
|
36
37
|
"math/rand",
|
|
37
38
|
"errors",
|
|
38
39
|
"time",
|
|
@@ -48,6 +49,7 @@ const BUILTIN_PACKAGES = new Set([
|
|
|
48
49
|
"path",
|
|
49
50
|
"path/filepath",
|
|
50
51
|
"testing",
|
|
52
|
+
"gofront/shared",
|
|
51
53
|
]);
|
|
52
54
|
|
|
53
55
|
// Returns true for local relative paths (start with ./ or ../)
|
|
@@ -71,7 +73,7 @@ export function resolveGwDir(importPath, fromFile) {
|
|
|
71
73
|
return gwFiles.length > 0 ? dir : null;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
function resolveImport(importPath, fromFile) {
|
|
75
77
|
const fromDir = dirname(resolve(fromFile));
|
|
76
78
|
const nodeModules = findNodeModules(fromDir);
|
|
77
79
|
|
package/src/test-runner.js
CHANGED
|
@@ -6,8 +6,26 @@ 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";
|
|
16
|
+
|
|
17
|
+
// WasmGC + exception handling (what the wasm backend emits) ship enabled by
|
|
18
|
+
// default from Node 22 (V8 11.9+). Older runtimes fail at instantiate time
|
|
19
|
+
// with an opaque CompileError, so check up front and explain.
|
|
20
|
+
export const MIN_WASM_NODE_MAJOR = 22;
|
|
21
|
+
|
|
22
|
+
export function checkWasmRuntime(version = process.version) {
|
|
23
|
+
const major = Number.parseInt(version.replace(/^v/, ""), 10);
|
|
24
|
+
if (Number.isNaN(major) || major >= MIN_WASM_NODE_MAJOR) return;
|
|
25
|
+
throw new Error(
|
|
26
|
+
`running wasm packages requires Node ${MIN_WASM_NODE_MAJOR}+ (WasmGC); found ${version}\n hint: upgrade Node, or compile with --js-only to run the JS backend instead`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
11
29
|
|
|
12
30
|
export function isTestFunc(decl) {
|
|
13
31
|
if (decl.kind !== "FuncDecl") return false;
|
|
@@ -78,7 +96,16 @@ globalThis.document = globalThis.document ?? null;
|
|
|
78
96
|
`
|
|
79
97
|
}
|
|
80
98
|
globalThis.__gofront_verbose = ${verbose};
|
|
81
|
-
|
|
99
|
+
${
|
|
100
|
+
options.wasmBytes
|
|
101
|
+
? `
|
|
102
|
+
// ── WASM module (tests run inside app.wasm; *testing.T stays in JS) ──
|
|
103
|
+
${HELPER_SPRINTF}
|
|
104
|
+
${HELPER_TESTING}
|
|
105
|
+
globalThis.__GOFRONT_WASM_BYTES = Uint8Array.from(atob(${JSON.stringify(Buffer.from(options.wasmBytes).toString("base64"))}), (c) => c.charCodeAt(0));
|
|
106
|
+
`
|
|
107
|
+
: ""
|
|
108
|
+
}
|
|
82
109
|
// ── Compiled Package Bundle ──
|
|
83
110
|
${bundleJs}
|
|
84
111
|
// ─────────────────────────────
|
|
@@ -281,6 +308,44 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
|
281
308
|
});
|
|
282
309
|
}
|
|
283
310
|
|
|
311
|
+
function reportBuildFailure(err, resolvedDir, options) {
|
|
312
|
+
const msg = err?.message ?? String(err);
|
|
313
|
+
if (options.captureOutput) {
|
|
314
|
+
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
|
|
315
|
+
return { exitCode: 1, stdout: "", stderr: out };
|
|
316
|
+
}
|
|
317
|
+
const dest = options.stderr ?? process.stderr;
|
|
318
|
+
const c = colorsFor(options, dest);
|
|
319
|
+
dest.write(
|
|
320
|
+
`${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
|
|
321
|
+
);
|
|
322
|
+
return { exitCode: 1 };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// `both` packages run twice (JS and wasm) so the two backends are kept in
|
|
326
|
+
// agreement; output of both runs is concatenated.
|
|
327
|
+
async function runDualTarget(compiled, resolvedDir, runHarness, options) {
|
|
328
|
+
let wasmCompiled;
|
|
329
|
+
try {
|
|
330
|
+
wasmCompiled = compilePackageTestsWasm(resolvedDir, options);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return reportBuildFailure(err, resolvedDir, options);
|
|
333
|
+
}
|
|
334
|
+
const label = compiled.pkgName;
|
|
335
|
+
const jsRun = await runHarness(compiled.js, `${label} [js]`, null);
|
|
336
|
+
const wasmRun = await runHarness(
|
|
337
|
+
wasmCompiled.js,
|
|
338
|
+
`${label} [wasm]`,
|
|
339
|
+
wasmCompiled.wasm,
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
exitCode: jsRun.exitCode !== 0 ? jsRun.exitCode : wasmRun.exitCode,
|
|
343
|
+
stdout: jsRun.stdout + wasmRun.stdout,
|
|
344
|
+
stderr: jsRun.stderr + wasmRun.stderr,
|
|
345
|
+
testCount: jsRun.testCount + wasmRun.testCount,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
284
349
|
export async function runTests(targetDir, options = {}) {
|
|
285
350
|
let resolvedDir = resolve(targetDir);
|
|
286
351
|
try {
|
|
@@ -304,17 +369,7 @@ export async function runTests(targetDir, options = {}) {
|
|
|
304
369
|
try {
|
|
305
370
|
compiled = compilePackageTests(resolvedDir, options);
|
|
306
371
|
} 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 };
|
|
372
|
+
return reportBuildFailure(err, resolvedDir, options);
|
|
318
373
|
}
|
|
319
374
|
|
|
320
375
|
const { pkgName, js, programs } = compiled;
|
|
@@ -324,12 +379,31 @@ export async function runTests(targetDir, options = {}) {
|
|
|
324
379
|
return reportEmptyTests(pkgName, "no tests to run", options);
|
|
325
380
|
}
|
|
326
381
|
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
382
|
+
const runHarness = (bundleJs, label, wasmBytes) =>
|
|
383
|
+
spawnTestRunner(
|
|
384
|
+
resolvedDir,
|
|
385
|
+
generateTestHarness(bundleJs, testNames, {
|
|
386
|
+
...options,
|
|
387
|
+
pkgName: label,
|
|
388
|
+
jsdomPath,
|
|
389
|
+
wasmBytes,
|
|
390
|
+
color: colorsFor(options, options.stdout ?? process.stdout).enabled,
|
|
391
|
+
}),
|
|
392
|
+
testNames.length,
|
|
393
|
+
options,
|
|
394
|
+
);
|
|
333
395
|
|
|
334
|
-
|
|
396
|
+
// `wasm` packages run inside the linked app.wasm; `js` packages that import
|
|
397
|
+
// wasm packages need the linked module too (hybrid packages).
|
|
398
|
+
if (compiled.target === "both" || compiled.wasm) {
|
|
399
|
+
try {
|
|
400
|
+
checkWasmRuntime();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return reportBuildFailure(err, resolvedDir, options);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (compiled.target === "both") {
|
|
406
|
+
return runDualTarget(compiled, resolvedDir, runHarness, options);
|
|
407
|
+
}
|
|
408
|
+
return runHarness(js, pkgName, compiled.wasm ?? null);
|
|
335
409
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Assignability, binary result types, and interface satisfaction.
|
|
2
2
|
// Installed as a mixin on TypeChecker.prototype.
|
|
3
3
|
|
|
4
|
+
import { sharedInfo } from "./stdlib/shared.js";
|
|
4
5
|
import {
|
|
5
6
|
ANY,
|
|
6
7
|
BOOL,
|
|
@@ -159,6 +160,7 @@ export const assignabilityMethods = {
|
|
|
159
160
|
target = this.resolveType(target);
|
|
160
161
|
source = this.resolveType(source);
|
|
161
162
|
this._markIfaceBox(target, source, node);
|
|
163
|
+
this._checkWasmIfaceProvider(target, source, node);
|
|
162
164
|
if (this._assertAssignableEarlyReturn(target, source)) return;
|
|
163
165
|
if (isPointer(target) && isPointer(source)) {
|
|
164
166
|
this.assertAssignable(
|
|
@@ -200,6 +202,24 @@ export const assignabilityMethods = {
|
|
|
200
202
|
node._ifaceBox = "ptr";
|
|
201
203
|
},
|
|
202
204
|
|
|
205
|
+
// A js package cannot provide an implementation of a wasm interface: the
|
|
206
|
+
// value would cross into wasm, and JS-implemented proxies are not supported.
|
|
207
|
+
_checkWasmIfaceProvider(target, source, node) {
|
|
208
|
+
if (this.target !== "js" || target?.kind !== "named") return;
|
|
209
|
+
if (target._target !== "wasm" || !source) return;
|
|
210
|
+
const iface = this.resolveType(target.underlying);
|
|
211
|
+
if (iface?.kind !== "interface" || !(iface.methods?.size > 0)) return;
|
|
212
|
+
const ptr = source.kind === "pointer";
|
|
213
|
+
const src = ptr ? this.resolveType(source.base) : source;
|
|
214
|
+
if (src?.kind !== "named" || src._target === "wasm") return;
|
|
215
|
+
if (this.resolveType(src.underlying)?.kind === "interface") return;
|
|
216
|
+
if (!this.implements(source, iface, node)) return;
|
|
217
|
+
this.err(
|
|
218
|
+
`type '${ptr ? "*" : ""}${src.name}' (${src._target ?? "js"}) cannot implement wasm interface '${target.name}' across the boundary; implement it in a wasm package or pass a func callback`,
|
|
219
|
+
node,
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
|
|
203
223
|
_assertAssignableEarlyReturn(target, source) {
|
|
204
224
|
return (
|
|
205
225
|
target?.kind === "typeParam" ||
|
|
@@ -222,6 +242,23 @@ export const assignabilityMethods = {
|
|
|
222
242
|
);
|
|
223
243
|
return;
|
|
224
244
|
}
|
|
245
|
+
const sBase = this.resolveType(
|
|
246
|
+
source.kind === "named" ? source.underlying : source,
|
|
247
|
+
);
|
|
248
|
+
if (sharedInfo(source) && tBase?.kind === "slice") {
|
|
249
|
+
this.err(
|
|
250
|
+
`cannot use ${typeStr(source)} as ${typeStr(target)}: a linear-memory buffer cannot be passed as a GC slice without an explicit copy`,
|
|
251
|
+
node,
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (tBase?.kind === "shared" && sBase?.kind === "slice") {
|
|
256
|
+
this.err(
|
|
257
|
+
`cannot use ${typeStr(source)} as ${typeStr(target)}: a GC slice cannot be used as a shared buffer; copy its elements into one allocated with shared.New${tBase.shared}`,
|
|
258
|
+
node,
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
225
262
|
this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
|
|
226
263
|
},
|
|
227
264
|
|
|
@@ -346,6 +383,7 @@ export const assignabilityMethods = {
|
|
|
346
383
|
},
|
|
347
384
|
|
|
348
385
|
implements(srcType, iface, _node) {
|
|
386
|
+
const isPtr = srcType?.kind === "pointer";
|
|
349
387
|
// *T has the full method set of T (value + pointer receivers).
|
|
350
388
|
if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
|
|
351
389
|
let base = srcType.kind === "named" ? srcType.underlying : srcType;
|
|
@@ -366,7 +404,10 @@ export const assignabilityMethods = {
|
|
|
366
404
|
: null;
|
|
367
405
|
if (!methodMap) return false;
|
|
368
406
|
for (const [name, required] of iface.methods) {
|
|
369
|
-
|
|
407
|
+
const actual = methodMap.get(name);
|
|
408
|
+
if (!actual) return false;
|
|
409
|
+
if (!isPtr && actual._ptrRecv) return false;
|
|
410
|
+
if (!this._implementsMethod(required, actual)) return false;
|
|
370
411
|
}
|
|
371
412
|
return true;
|
|
372
413
|
},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// TypeChecker expression-checking methods — installed as a mixin on TypeChecker.prototype.
|
|
2
2
|
|
|
3
|
+
import { sharedInfo } from "./stdlib/shared.js";
|
|
3
4
|
import {
|
|
4
5
|
ANY,
|
|
5
6
|
BOOL,
|
|
7
|
+
BROWSER_GLOBALS,
|
|
6
8
|
COMPLEX128,
|
|
7
9
|
ERROR,
|
|
8
10
|
FLOAT64,
|
|
@@ -41,6 +43,13 @@ const BUILTIN_CHECK = {
|
|
|
41
43
|
s.err(`cannot append to array (type ${typeStr(st)})`, e);
|
|
42
44
|
return st;
|
|
43
45
|
}
|
|
46
|
+
if (sharedInfo(st)) {
|
|
47
|
+
s.err(
|
|
48
|
+
`cannot append to ${typeStr(st)}: shared buffers have a fixed size`,
|
|
49
|
+
e,
|
|
50
|
+
);
|
|
51
|
+
return st;
|
|
52
|
+
}
|
|
44
53
|
const sb = s.resolveType(st?.kind === "named" ? st.underlying : st);
|
|
45
54
|
if (sb?.kind === "slice") {
|
|
46
55
|
for (let i = 1; i < e.args.length; i++) {
|
|
@@ -54,7 +63,7 @@ const BUILTIN_CHECK = {
|
|
|
54
63
|
}
|
|
55
64
|
return st;
|
|
56
65
|
},
|
|
57
|
-
copy: () =>
|
|
66
|
+
copy: (s, _n, e, at) => s._checkBuiltinCopy(e, at),
|
|
58
67
|
delete: () => VOID,
|
|
59
68
|
make: (s, _n, e, _at, sc) => {
|
|
60
69
|
if (e.args.length < 1) return ANY;
|
|
@@ -126,6 +135,26 @@ export const expressionCheckMethods = {
|
|
|
126
135
|
const t = scope.lookup(expr.name);
|
|
127
136
|
if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
|
|
128
137
|
if (this.types.has(expr.name)) expr._isTypeRef = true;
|
|
138
|
+
if (
|
|
139
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
140
|
+
BROWSER_GLOBALS.has(expr.name) &&
|
|
141
|
+
scope.lookup(expr.name) === this.globals.lookup(expr.name)
|
|
142
|
+
) {
|
|
143
|
+
this.recordBlocker("browser global", expr.name);
|
|
144
|
+
this.err(
|
|
145
|
+
`'${expr.name}' is not available in wasm packages`,
|
|
146
|
+
expr,
|
|
147
|
+
`(package '${this.pkgName}' is //gofront:target ${this.target} — browser globals are only available in js packages)`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
152
|
+
expr.name === "gom" &&
|
|
153
|
+
scope.lookup(expr.name) === this.globals.lookup(expr.name)
|
|
154
|
+
) {
|
|
155
|
+
this.recordBlocker("gom usage", "gom");
|
|
156
|
+
this.err("package 'gom' is not available in wasm packages", expr);
|
|
157
|
+
}
|
|
129
158
|
return t;
|
|
130
159
|
}
|
|
131
160
|
case "BinaryExpr": {
|
|
@@ -136,8 +165,16 @@ export const expressionCheckMethods = {
|
|
|
136
165
|
return this.binaryResultType(expr.op, ANY, ANY, expr);
|
|
137
166
|
return this.binaryResultType(expr.op, lt, rt, expr);
|
|
138
167
|
}
|
|
139
|
-
case "AwaitExpr":
|
|
168
|
+
case "AwaitExpr": {
|
|
169
|
+
if (this.target === "wasm" || this.target === "both") {
|
|
170
|
+
this.recordBlocker("async function");
|
|
171
|
+
this.err(
|
|
172
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
173
|
+
expr,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
140
176
|
return this.checkExpr(expr.expr, scope);
|
|
177
|
+
}
|
|
141
178
|
case "InstantiationExpr":
|
|
142
179
|
return this._checkInstantiationExpr(expr, scope);
|
|
143
180
|
default: {
|
|
@@ -262,10 +299,42 @@ export const expressionCheckMethods = {
|
|
|
262
299
|
expr._mapValueType = btu.value;
|
|
263
300
|
return btu.value;
|
|
264
301
|
}
|
|
302
|
+
if (btu.kind === "shared") {
|
|
303
|
+
expr._sharedElem = btu.elem;
|
|
304
|
+
const idx = this._constIntValue(expr.index);
|
|
305
|
+
if (idx !== null && idx < 0)
|
|
306
|
+
this.err(`invalid index ${idx} (index must not be negative)`, expr);
|
|
307
|
+
return btu.elem;
|
|
308
|
+
}
|
|
265
309
|
if (isString(bt)) return INT;
|
|
266
310
|
return this.err(`Cannot index type ${typeStr(bt)}`, expr);
|
|
267
311
|
},
|
|
268
312
|
|
|
313
|
+
// copy(dst, src): element types must be identical when either side is a
|
|
314
|
+
// shared buffer (the wasm emitter uses one element type for both sides).
|
|
315
|
+
_checkBuiltinCopy(e, at) {
|
|
316
|
+
const elemOf = (t) => {
|
|
317
|
+
const u = this.resolveType(t?.kind === "named" ? t.underlying : t);
|
|
318
|
+
return u?.kind === "slice" || u?.kind === "shared" ? u.elem : null;
|
|
319
|
+
};
|
|
320
|
+
const [dt, st] = at;
|
|
321
|
+
if (!sharedInfo(dt) && !sharedInfo(st)) return INT;
|
|
322
|
+
const de = elemOf(dt);
|
|
323
|
+
const se = elemOf(st);
|
|
324
|
+
if (!de || !se) {
|
|
325
|
+
this.err(
|
|
326
|
+
`copy: cannot copy between ${typeStr(dt)} and ${typeStr(st)}`,
|
|
327
|
+
e,
|
|
328
|
+
);
|
|
329
|
+
} else if (typeStr(de) !== typeStr(se)) {
|
|
330
|
+
this.err(
|
|
331
|
+
`copy: element types differ (${typeStr(dt)} vs ${typeStr(st)})`,
|
|
332
|
+
e,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return INT;
|
|
336
|
+
},
|
|
337
|
+
|
|
269
338
|
_checkSliceExpr(expr, scope) {
|
|
270
339
|
const bt = this.checkExpr(expr.expr, scope);
|
|
271
340
|
if (expr.low) this.checkExpr(expr.low, scope);
|
|
@@ -276,10 +345,35 @@ export const expressionCheckMethods = {
|
|
|
276
345
|
if (bt.kind === "slice" || bt.kind === "array")
|
|
277
346
|
return { kind: "slice", elem: bt.elem };
|
|
278
347
|
if (isString(bt)) return STRING;
|
|
348
|
+
if (sharedInfo(bt))
|
|
349
|
+
return this.err(
|
|
350
|
+
`Cannot slice type ${typeStr(bt)}: use .Subarray(lo, hi) to create a view of a shared buffer`,
|
|
351
|
+
expr,
|
|
352
|
+
);
|
|
279
353
|
return this.err(`Cannot slice type ${typeStr(bt)}`, expr);
|
|
280
354
|
},
|
|
281
355
|
|
|
282
356
|
_checkFuncLit(expr, scope) {
|
|
357
|
+
const savedSharedAlloc = this._sharedAllocOk;
|
|
358
|
+
this._sharedAllocOk = false;
|
|
359
|
+
try {
|
|
360
|
+
return this._checkFuncLitBody(expr, scope);
|
|
361
|
+
} finally {
|
|
362
|
+
this._sharedAllocOk = savedSharedAlloc;
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
_checkFuncLitBody(expr, scope) {
|
|
367
|
+
if (
|
|
368
|
+
(this.target === "wasm" || this.target === "both") &&
|
|
369
|
+
(expr.async || expr.isAsync)
|
|
370
|
+
) {
|
|
371
|
+
this.recordBlocker("async function");
|
|
372
|
+
this.err(
|
|
373
|
+
"async functions are not supported in wasm packages; keep async code in a js package",
|
|
374
|
+
expr,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
283
377
|
const inner = new Scope(scope);
|
|
284
378
|
for (const p of expr.params)
|
|
285
379
|
inner.define(p.name, p.type ? this.resolveTypeNode(p.type, scope) : ANY);
|
|
@@ -392,7 +486,11 @@ export const expressionCheckMethods = {
|
|
|
392
486
|
|
|
393
487
|
_rangeCollTypeTuple(resolved, collType) {
|
|
394
488
|
if (isString(resolved)) return { kind: "tuple", types: [INT, RUNE] };
|
|
395
|
-
if (
|
|
489
|
+
if (
|
|
490
|
+
resolved?.kind === "slice" ||
|
|
491
|
+
resolved?.kind === "array" ||
|
|
492
|
+
resolved?.kind === "shared"
|
|
493
|
+
)
|
|
396
494
|
return { kind: "tuple", types: [INT, resolved.elem ?? ANY] };
|
|
397
495
|
if (resolved?.kind === "map")
|
|
398
496
|
return {
|
|
@@ -428,6 +526,9 @@ export const expressionCheckMethods = {
|
|
|
428
526
|
: null;
|
|
429
527
|
if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
|
|
430
528
|
}
|
|
529
|
+
expr._typeArgs = typeArgs;
|
|
530
|
+
if (expr.expr) expr.expr._typeArgs = typeArgs;
|
|
531
|
+
expr._genericFnType = baseType;
|
|
431
532
|
return this.instantiateGenericFunc(baseType, typeArgs);
|
|
432
533
|
}
|
|
433
534
|
// The parser's type-arg heuristic mistakes `xs[d.Field]` (uppercase field
|
|
@@ -507,6 +608,8 @@ export const expressionCheckMethods = {
|
|
|
507
608
|
|
|
508
609
|
checkCall(expr, scope) {
|
|
509
610
|
let fnType = this.checkExpr(expr.func, scope);
|
|
611
|
+
this._checkSharedAllocSite(expr, scope);
|
|
612
|
+
this._checkBoundaryCallInLoop(expr, scope);
|
|
510
613
|
if (fnType.kind === "builtin") {
|
|
511
614
|
const argTypes = expr.args.map((a, i) => {
|
|
512
615
|
// new(T) — first arg is a type name, not a value expression
|
|
@@ -537,12 +640,79 @@ export const expressionCheckMethods = {
|
|
|
537
640
|
|
|
538
641
|
this._checkCallArgs(fnType, argTypes, expr);
|
|
539
642
|
|
|
643
|
+
// Pseudo-generic stdlib helpers (maps.Keys, slices.Clone, ...) declare
|
|
644
|
+
// how their result derives from the first argument.
|
|
645
|
+
if (fnType._derivedReturn) {
|
|
646
|
+
const derived = this._derivedReturnType(fnType._derivedReturn, argTypes);
|
|
647
|
+
if (derived) return derived;
|
|
648
|
+
}
|
|
649
|
+
|
|
540
650
|
const ret = fnType.returns;
|
|
541
651
|
if (!ret || ret.length === 0) return VOID;
|
|
542
652
|
if (ret.length === 1) return ret[0];
|
|
543
653
|
return { kind: "tuple", types: ret };
|
|
544
654
|
},
|
|
545
655
|
|
|
656
|
+
// `shared.NewXxx(n)` allocates from a linear memory that is sized once at
|
|
657
|
+
// startup (no memory.grow → no detached TypedArray views on the JS side).
|
|
658
|
+
// Allocation is therefore only legal in package-level var initializers and
|
|
659
|
+
// init(), which both run inside the module start function.
|
|
660
|
+
// JS→WASM calls copy non-shared arguments across the boundary on every
|
|
661
|
+
// call. Inside a loop that dominates the per-iteration cost, so hint that
|
|
662
|
+
// the loop belongs on the WASM side (or the data in gofront/shared).
|
|
663
|
+
_checkBoundaryCallInLoop(expr, scope) {
|
|
664
|
+
if (this._loopDepth === 0 || this.target !== "js") return;
|
|
665
|
+
const fn = expr.func;
|
|
666
|
+
if (fn.kind !== "SelectorExpr" || fn.expr.kind !== "Ident") return;
|
|
667
|
+
const ns = scope.lookup(fn.expr.name);
|
|
668
|
+
if (ns?.kind !== "namespace" || ns._target !== "wasm") return;
|
|
669
|
+
const member = ns.members[fn.field];
|
|
670
|
+
if (member?.kind !== "func") return;
|
|
671
|
+
this.warn(
|
|
672
|
+
`'${fn.expr.name}.${fn.field}' crosses the JS→WASM boundary inside a loop`,
|
|
673
|
+
fn.expr, // CallExpr nodes carry no position; the callee Ident does
|
|
674
|
+
"hint: each call copies its arguments; move the loop into the wasm package or pass gofront/shared buffers",
|
|
675
|
+
);
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
_checkSharedAllocSite(expr, scope) {
|
|
679
|
+
const f = expr.func;
|
|
680
|
+
if (
|
|
681
|
+
f.kind !== "SelectorExpr" ||
|
|
682
|
+
f.expr.kind !== "Ident" ||
|
|
683
|
+
f.expr.name !== "shared" ||
|
|
684
|
+
!f.field.startsWith("New") ||
|
|
685
|
+
scope.lookup("shared") !== this.globals.lookup("shared")
|
|
686
|
+
)
|
|
687
|
+
return;
|
|
688
|
+
if (this._sharedAllocOk) return;
|
|
689
|
+
const why =
|
|
690
|
+
this.target === "wasm"
|
|
691
|
+
? "shared buffers are allocated once at startup"
|
|
692
|
+
: "shared buffers are allocated once at startup so the code stays portable to wasm packages";
|
|
693
|
+
this.err(
|
|
694
|
+
`shared.${f.field} must be called from a package-level var initializer or init(); ${why}`,
|
|
695
|
+
expr,
|
|
696
|
+
);
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
_derivedReturnType(mode, argTypes) {
|
|
700
|
+
const arg = argTypes[0];
|
|
701
|
+
const under = arg?.kind === "named" ? arg.underlying : arg;
|
|
702
|
+
switch (mode) {
|
|
703
|
+
case "arg0":
|
|
704
|
+
return under ? arg : null;
|
|
705
|
+
case "keysOfArg0":
|
|
706
|
+
return under?.key ? { kind: "slice", elem: under.key } : null;
|
|
707
|
+
case "valuesOfArg0": {
|
|
708
|
+
const v = under?.value ?? under?.elem;
|
|
709
|
+
return v ? { kind: "slice", elem: v } : null;
|
|
710
|
+
}
|
|
711
|
+
default:
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
|
|
546
716
|
_resolveGenericFnType(fnType, argTypes, expr, scope) {
|
|
547
717
|
const typeArgs = this.inferTypeArgs(fnType, argTypes);
|
|
548
718
|
if (!typeArgs) {
|
|
@@ -560,6 +730,10 @@ export const expressionCheckMethods = {
|
|
|
560
730
|
: null;
|
|
561
731
|
if (constraint) this.checkConstraint(typeArgs[i], constraint, expr);
|
|
562
732
|
}
|
|
733
|
+
expr._typeArgs = typeArgs;
|
|
734
|
+
if (expr.func) expr.func._typeArgs = typeArgs;
|
|
735
|
+
expr._genericFnType = fnType;
|
|
736
|
+
if (expr.func) expr.func._genericFnType = fnType;
|
|
563
737
|
return this.instantiateGenericFunc(fnType, typeArgs);
|
|
564
738
|
},
|
|
565
739
|
|
|
@@ -611,6 +785,11 @@ export const expressionCheckMethods = {
|
|
|
611
785
|
if (name === "len") {
|
|
612
786
|
const size = this._constLenFromType(argTypes[0]);
|
|
613
787
|
if (size != null) expr._constLen = size;
|
|
788
|
+
} else if (sharedInfo(argTypes[0])) {
|
|
789
|
+
this.err(
|
|
790
|
+
`cannot use cap() on ${typeStr(argTypes[0])}: shared buffers have a fixed size; use len()`,
|
|
791
|
+
expr,
|
|
792
|
+
);
|
|
614
793
|
}
|
|
615
794
|
return INT;
|
|
616
795
|
},
|