gofront 1.5.1 → 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 -466
- package/package.json +16 -20
- package/src/backend/js/expressions.js +50 -19
- package/src/backend/js/index.js +100 -55
- package/src/backend/js/runtime.js +102 -0
- package/src/backend/js/source-map.js +32 -0
- package/src/backend/js/statements.js +96 -16
- package/src/backend/js/stdlib/errors.js +2 -2
- package/src/backend/js/stdlib/fmt.js +26 -8
- package/src/backend/js/stdlib/index.js +3 -0
- package/src/backend/js/stdlib/maps.js +1 -1
- package/src/backend/js/stdlib/path.js +2 -2
- package/src/backend/js/stdlib/shared.js +17 -0
- package/src/backend/js/stdlib/slices.js +1 -1
- package/src/backend/js/stdlib/sort.js +1 -1
- package/src/backend/js/stdlib/strconv.js +34 -9
- package/src/backend/js/stdlib/strings.js +4 -1
- package/src/backend/js/stdlib/testing.js +2 -2
- package/src/backend/js/stdlib/time.js +3 -3
- package/src/backend/js/templ.js +1 -1
- package/src/backend/wasm/boundary.js +486 -33
- 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 +606 -5009
- package/src/backend/wasm/encode.js +212 -64
- package/src/backend/wasm/glue.js +19 -5
- package/src/backend/wasm/index.js +1426 -49
- package/src/backend/wasm/monomorph.js +711 -0
- package/src/backend/wasm/optimize.js +227 -0
- package/src/backend/wasm/types.js +159 -35
- package/src/backend/wasm/wat.js +49 -2
- package/src/cli-core.js +275 -72
- package/src/compiler.js +144 -21
- package/src/index.js +86 -115
- package/src/lower/boxing.js +3 -4
- package/src/lower/captures.js +10 -17
- package/src/lower/escape.js +4 -9
- package/src/lower/functions.js +277 -5
- package/src/lower/globals.js +7 -15
- package/src/lower/index.js +5 -2
- package/src/lower/ownership.js +5 -10
- package/src/lower/range.js +2 -5
- package/src/lower/walk.js +25 -0
- package/src/resolver.js +2 -1
- package/src/test-runner.js +20 -0
- package/src/typechecker/assignability.js +42 -1
- package/src/typechecker/expressions.js +142 -2
- package/src/typechecker/index.js +27 -1
- package/src/typechecker/resolve.js +35 -6
- package/src/typechecker/statements.js +0 -7
- package/src/typechecker/stdlib/core.js +17 -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 +206 -7
- package/src/vendor.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"author": "Luuk van Venrooij",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
"url": "https://github.com/seriva/gofront/issues"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/seriva/gofront#readme",
|
|
14
|
-
"description": "A Go-inspired language that
|
|
14
|
+
"description": "A Go-inspired language that compiles to JavaScript and WebAssembly",
|
|
15
15
|
"keywords": [
|
|
16
16
|
"go",
|
|
17
17
|
"compiler",
|
|
18
18
|
"transpiler",
|
|
19
19
|
"javascript",
|
|
20
|
+
"webassembly",
|
|
21
|
+
"wasm",
|
|
22
|
+
"wasmgc",
|
|
20
23
|
"frontend",
|
|
21
24
|
"type-safe"
|
|
22
25
|
],
|
|
@@ -31,31 +34,24 @@
|
|
|
31
34
|
"gofront": "./src/index.js"
|
|
32
35
|
},
|
|
33
36
|
"scripts": {
|
|
34
|
-
"build
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"build:templ": "node src/index.js example/templ/src -o example/templ/app.js",
|
|
38
|
-
"build:webgl": "node src/index.js example/webgl/src -o example/webgl/app.js",
|
|
39
|
-
"build:all": "npm run build:simple && npm run build:reactive && npm run build:gom && npm run build:templ && npm run build:webgl",
|
|
40
|
-
"test": "npm run test:unit",
|
|
41
|
-
"test:all": "npm run test:unit && npm run test:perf && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
|
|
42
|
-
"test:unit": "node test/unit/run.js",
|
|
43
|
-
"test:perf": "node test/e2e/perf/zero-alloc.js",
|
|
44
|
-
"bench:raycast": "node --expose-gc test/e2e/perf/raycast-bench.js",
|
|
45
|
-
"test:examples": "node src/index.js test example/simple/src && node src/index.js test example/simple/src/utils && node src/index.js test example/reactive/src && node src/index.js test example/reactive/src/utils && node src/index.js test example/gom/src && node src/index.js test example/gom/src/utils && node src/index.js test example/templ/src && node src/index.js test example/templ/src/utils",
|
|
46
|
-
"test:examples:dom": "node src/index.js test --dom example/simple/src && node src/index.js test --dom example/gom/src && node src/index.js test --dom example/templ/src",
|
|
37
|
+
"build": "node src/index.js example/simple/src -o example/simple/app.js && node src/index.js example/reactive/src -o example/reactive/app.js && node src/index.js example/gom/src -o example/gom/app.js && node src/index.js example/templ/src -o example/templ/app.js && node src/index.js example/webgl/src -o example/webgl/app.js && node src/index.js example/hybrid/src -o example/hybrid/app.js",
|
|
38
|
+
"test": "node test/unit/run.js && node test/e2e/perf/zero-alloc.js && node src/index.js test --dom example/...",
|
|
39
|
+
"test:e2e": "playwright test",
|
|
47
40
|
"test:coverage": "c8 --include='src/**' --reporter=text --reporter=lcov node test/unit/run.js",
|
|
48
|
-
"
|
|
41
|
+
"bench": "node --expose-gc test/e2e/perf/raycast-bench.js",
|
|
49
42
|
"format": "biome format --write .",
|
|
50
|
-
"check": "biome check . && sentrux check && sentrux gate && node src/index.js
|
|
43
|
+
"check": "biome check . && sentrux check && sentrux gate && node src/index.js check example/...",
|
|
51
44
|
"prepare": "lefthook install"
|
|
52
45
|
},
|
|
53
46
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^2.5.
|
|
47
|
+
"@biomejs/biome": "^2.5.15",
|
|
55
48
|
"@playwright/test": "^1.63.0",
|
|
56
49
|
"c8": "^12.0.0",
|
|
57
|
-
"jsdom": "^30.1.
|
|
58
|
-
"lefthook": "^2.1.
|
|
50
|
+
"jsdom": "^30.1.2",
|
|
51
|
+
"lefthook": "^2.1.17",
|
|
59
52
|
"serve": "^14.2.6"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"binaryen": "^132.0.0"
|
|
60
56
|
}
|
|
61
57
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
/** @typedef {import('./index.js').CodeGen} CodeGen */
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const STRICT_NUMERIC_TYPES = new Set([
|
|
18
18
|
"float32",
|
|
19
19
|
"int8",
|
|
20
20
|
"int16",
|
|
@@ -26,7 +26,7 @@ export const STRICT_NUMERIC_TYPES = new Set([
|
|
|
26
26
|
"uint32",
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const STRICT_OPS = new Set([
|
|
30
30
|
"+",
|
|
31
31
|
"-",
|
|
32
32
|
"*",
|
|
@@ -233,14 +233,19 @@ const BUILTIN_GEN = {
|
|
|
233
233
|
}
|
|
234
234
|
return `{ value: ${s.zeroValueForExpr(arg)} }`;
|
|
235
235
|
},
|
|
236
|
-
print
|
|
236
|
+
// `print` writes without a trailing newline (Go semantics); the wasm glue
|
|
237
|
+
// buffers the same way, so output parity holds across backends.
|
|
238
|
+
print: (s, e) =>
|
|
239
|
+
`((__s) => (typeof process !== "undefined" && process?.stdout?.write ? process.stdout.write(__s) : console.log(__s)))([${e.args.map((a) => s.genExpr(a)).join(", ")}].map(String).join(" "))`,
|
|
237
240
|
println: (s, e) =>
|
|
238
241
|
`console.log(${e.args.map((a) => s.genExpr(a)).join(", ")})`,
|
|
239
242
|
panic: (s, e) => `(() => { throw new Error(${s.genExpr(e.args[0])}); })()`,
|
|
240
|
-
recover: () =>
|
|
241
|
-
|
|
243
|
+
recover: (s) => {
|
|
244
|
+
s.useHelper("panicRt");
|
|
245
|
+
return `__recover(${s._recoverOkInScope ? "__recoverOk" : "false"})`;
|
|
246
|
+
},
|
|
242
247
|
error: (s, e) => {
|
|
243
|
-
s.
|
|
248
|
+
s.useHelper("error");
|
|
244
249
|
return `__error(${s.genExpr(e.args[0])})`;
|
|
245
250
|
},
|
|
246
251
|
min: (s, e) => `Math.min(${e.args.map((a) => s.genExpr(a)).join(", ")})`,
|
|
@@ -272,7 +277,7 @@ export const expressionGenMethods = {
|
|
|
272
277
|
if (expr._ifaceBox && !expr._ifaceBoxing) {
|
|
273
278
|
expr._ifaceBoxing = true;
|
|
274
279
|
try {
|
|
275
|
-
this.
|
|
280
|
+
this.useHelper("ifaceBox");
|
|
276
281
|
const fn = expr._ifaceBox === "ptr" ? "__ifp" : "__ifv";
|
|
277
282
|
return `${fn}(${this.genExpr(expr)})`;
|
|
278
283
|
} finally {
|
|
@@ -365,7 +370,7 @@ export const expressionGenMethods = {
|
|
|
365
370
|
if (base.kind === "struct") {
|
|
366
371
|
if (this._structClassName(t) || this._structClassName(base))
|
|
367
372
|
return `${js}.__clone()`;
|
|
368
|
-
this.
|
|
373
|
+
this.useHelper("sclone");
|
|
369
374
|
return `__sclone(${js})`;
|
|
370
375
|
}
|
|
371
376
|
const elemClone = this._cloneJs(base.elem, "__e");
|
|
@@ -613,7 +618,7 @@ export const expressionGenMethods = {
|
|
|
613
618
|
this._withFnCtx(expr.body, () =>
|
|
614
619
|
this.indented(() => {
|
|
615
620
|
this._emitParamCopies(expr.params);
|
|
616
|
-
this._genBody(expr.body);
|
|
621
|
+
this._genBody(expr.body, expr);
|
|
617
622
|
}),
|
|
618
623
|
);
|
|
619
624
|
const body = this.out.join("\n");
|
|
@@ -702,12 +707,20 @@ export const expressionGenMethods = {
|
|
|
702
707
|
return this._genTimeMethodCall(expr.func.field, expr);
|
|
703
708
|
if (recvName === "testing.T")
|
|
704
709
|
return this._genTestingMethodCall(expr.func.field, expr);
|
|
710
|
+
if (recvName?.startsWith("shared.") && expr.func.field === "Subarray") {
|
|
711
|
+
const args = expr.args.map((a) => this.genExpr(a)).join(", ");
|
|
712
|
+
return `${this.genExpr(expr.func.expr)}.subarray(${args})`;
|
|
713
|
+
}
|
|
705
714
|
return undefined;
|
|
706
715
|
},
|
|
707
716
|
|
|
708
717
|
_genSelectorCall(expr) {
|
|
709
|
-
if (this._isErrorMethodCall(expr))
|
|
710
|
-
|
|
718
|
+
if (this._isErrorMethodCall(expr)) {
|
|
719
|
+
const recv = this.genExpr(expr.func.expr);
|
|
720
|
+
if (!this.strict) return `${recv}.Error()`;
|
|
721
|
+
this.useHelper("nilCall");
|
|
722
|
+
return `__nilcall(${recv}).Error()`;
|
|
723
|
+
}
|
|
711
724
|
if (expr.func.expr.kind === "Ident") {
|
|
712
725
|
const ns = expr.func.expr.name;
|
|
713
726
|
const fn = expr.func.field;
|
|
@@ -716,7 +729,25 @@ export const expressionGenMethods = {
|
|
|
716
729
|
}
|
|
717
730
|
const recvType = expr.func.expr._type;
|
|
718
731
|
const recvName = this._resolveRecvName(recvType);
|
|
719
|
-
|
|
732
|
+
const typed = this._genReceiverTypeCall(recvName, expr);
|
|
733
|
+
if (typed !== undefined) return typed;
|
|
734
|
+
if (this.strict && this._isInterfaceType(recvType)) {
|
|
735
|
+
// Go panics with a nil-dereference runtime error; JS would throw a
|
|
736
|
+
// TypeError with a different message.
|
|
737
|
+
this.useHelper("nilCall");
|
|
738
|
+
const recv = this.genExpr(expr.func.expr);
|
|
739
|
+
const args = expr.args
|
|
740
|
+
.map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)))
|
|
741
|
+
.join(", ");
|
|
742
|
+
return `__nilcall(${recv}).${expr.func.field}(${args})`;
|
|
743
|
+
}
|
|
744
|
+
return undefined;
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
_isInterfaceType(t) {
|
|
748
|
+
if (!t) return false;
|
|
749
|
+
if (t.kind === "interface") return true;
|
|
750
|
+
return t.kind === "named" && t.underlying?.kind === "interface";
|
|
720
751
|
},
|
|
721
752
|
|
|
722
753
|
_genBuiltinLen(expr) {
|
|
@@ -727,7 +758,7 @@ export const expressionGenMethods = {
|
|
|
727
758
|
if (wrapField) return `${this.genExpr(arg)}.${wrapField}.length`;
|
|
728
759
|
const js = this.genExpr(arg);
|
|
729
760
|
if (t?.kind === "map") return `Object.keys(${js}).length`;
|
|
730
|
-
this.
|
|
761
|
+
this.useHelper("len");
|
|
731
762
|
return `__len(${js})`;
|
|
732
763
|
},
|
|
733
764
|
|
|
@@ -760,7 +791,7 @@ export const expressionGenMethods = {
|
|
|
760
791
|
a._spread ? `...${this.genExpr(a)}` : this.genValueExpr(a),
|
|
761
792
|
);
|
|
762
793
|
if (elems.length === 0) return this.genExpr(expr.args[0]);
|
|
763
|
-
this.
|
|
794
|
+
this.useHelper("append");
|
|
764
795
|
return `new ${retType.name}(__append(${sliceJS}, ${elems.join(", ")}))`;
|
|
765
796
|
}
|
|
766
797
|
const slice = this.genExpr(expr.args[0]);
|
|
@@ -768,7 +799,7 @@ export const expressionGenMethods = {
|
|
|
768
799
|
.slice(1)
|
|
769
800
|
.map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genValueExpr(a)));
|
|
770
801
|
if (elems.length === 0) return slice;
|
|
771
|
-
this.
|
|
802
|
+
this.useHelper("append");
|
|
772
803
|
const sliceType = expr._type ?? expr.args[0]._type;
|
|
773
804
|
const typedCtor = isTypedArraySlice(sliceType)
|
|
774
805
|
? typedArrayConstructorForElem(
|
|
@@ -1240,10 +1271,10 @@ export const expressionGenMethods = {
|
|
|
1240
1271
|
case "-":
|
|
1241
1272
|
return `{ re: ${l}.re - ${r}.re, im: ${l}.im - ${r}.im }`;
|
|
1242
1273
|
case "*":
|
|
1243
|
-
this.
|
|
1274
|
+
this.useHelper("cmul");
|
|
1244
1275
|
return `__cmul(${l}, ${r})`;
|
|
1245
1276
|
case "/":
|
|
1246
|
-
this.
|
|
1277
|
+
this.useHelper("cdiv");
|
|
1247
1278
|
return `__cdiv(${l}, ${r})`;
|
|
1248
1279
|
case "==":
|
|
1249
1280
|
return `(${l}.re === ${r}.re && ${l}.im === ${r}.im)`;
|
|
@@ -1261,7 +1292,7 @@ export const expressionGenMethods = {
|
|
|
1261
1292
|
return this._genComplexBinary(expr);
|
|
1262
1293
|
|
|
1263
1294
|
if (this._isStructEquality(expr)) {
|
|
1264
|
-
this.
|
|
1295
|
+
this.useHelper("equal");
|
|
1265
1296
|
const cmp = `__equal(${this.genExpr(expr.left)}, ${this.genExpr(expr.right)})`;
|
|
1266
1297
|
return expr.op === "==" ? cmp : `!${cmp}`;
|
|
1267
1298
|
}
|
|
@@ -1346,7 +1377,7 @@ export const expressionGenMethods = {
|
|
|
1346
1377
|
return this._genSliceTypeConversion(expr, inner, t);
|
|
1347
1378
|
const target = t?.name;
|
|
1348
1379
|
if (target === "error") {
|
|
1349
|
-
this.
|
|
1380
|
+
this.useHelper("error");
|
|
1350
1381
|
return `__error(${inner})`;
|
|
1351
1382
|
}
|
|
1352
1383
|
if (target && this.namedWrapperNames.has(target))
|
package/src/backend/js/index.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
computeEmbeddedStubs,
|
|
22
|
+
hasDirectRecover,
|
|
22
23
|
isReferenceType,
|
|
24
|
+
normalizeDefers,
|
|
23
25
|
scanAddressTaken,
|
|
24
26
|
} from "../../lower/index.js";
|
|
25
27
|
import { isComplex, isNumeric } from "../../typechecker/types.js";
|
|
@@ -34,21 +36,24 @@ import {
|
|
|
34
36
|
HELPER_IFACE_BOX,
|
|
35
37
|
HELPER_INJECT_STYLES,
|
|
36
38
|
HELPER_LEN,
|
|
39
|
+
HELPER_NIL_CALL,
|
|
40
|
+
HELPER_PANIC,
|
|
37
41
|
HELPER_PATH_CLEAN,
|
|
38
42
|
HELPER_S,
|
|
39
43
|
HELPER_SCLONE,
|
|
40
44
|
HELPER_SORT_SLICE,
|
|
41
45
|
HELPER_SPRINTF,
|
|
46
|
+
HELPER_STRCONV,
|
|
42
47
|
HELPER_TESTING,
|
|
43
48
|
HELPER_TIME_FMT,
|
|
44
49
|
HELPER_TIME_PARSE,
|
|
45
50
|
} from "./runtime.js";
|
|
46
|
-
import { buildSourceMap } from "./source-map.js";
|
|
51
|
+
import { buildSourceMap, buildWasmSourceMap } from "./source-map.js";
|
|
47
52
|
import { statementGenMethods } from "./statements.js";
|
|
48
53
|
import { stdlibGenMethods } from "./stdlib/index.js";
|
|
49
54
|
import { templGenMethods } from "./templ.js";
|
|
50
55
|
|
|
51
|
-
export { buildSourceMap };
|
|
56
|
+
export { buildSourceMap, buildWasmSourceMap };
|
|
52
57
|
|
|
53
58
|
// Valid Go identifiers that cannot be used as JS bindings; emitted with a `$` suffix.
|
|
54
59
|
const JS_RESERVED = new Set([
|
|
@@ -90,6 +95,30 @@ const JS_RESERVED = new Set([
|
|
|
90
95
|
const jsSafeName = (n) =>
|
|
91
96
|
typeof n === "string" && JS_RESERVED.has(n) ? `${n}$` : n;
|
|
92
97
|
|
|
98
|
+
// Runtime helpers by registry name, in output order. Mark one as needed with
|
|
99
|
+
// `this.useHelper(name)`; `_prependHelpers` emits every marked entry.
|
|
100
|
+
const HELPER_MAP = [
|
|
101
|
+
["len", HELPER_LEN],
|
|
102
|
+
["append", HELPER_APPEND],
|
|
103
|
+
["sliceGuard", HELPER_S],
|
|
104
|
+
["sclone", HELPER_SCLONE],
|
|
105
|
+
["ifaceBox", HELPER_IFACE_BOX],
|
|
106
|
+
["equal", HELPER_EQUAL],
|
|
107
|
+
["cmul", HELPER_CMUL],
|
|
108
|
+
["cdiv", HELPER_CDIV],
|
|
109
|
+
["panicRt", HELPER_PANIC],
|
|
110
|
+
["nilCall", HELPER_NIL_CALL],
|
|
111
|
+
["sprintf", HELPER_SPRINTF],
|
|
112
|
+
["error", HELPER_ERROR],
|
|
113
|
+
["errorIs", HELPER_ERROR_IS],
|
|
114
|
+
["strconv", HELPER_STRCONV],
|
|
115
|
+
["pathClean", HELPER_PATH_CLEAN],
|
|
116
|
+
["sortSlice", HELPER_SORT_SLICE],
|
|
117
|
+
["timeFmt", HELPER_TIME_FMT],
|
|
118
|
+
["timeParse", HELPER_TIME_PARSE],
|
|
119
|
+
["testing", HELPER_TESTING],
|
|
120
|
+
];
|
|
121
|
+
|
|
93
122
|
export class CodeGen {
|
|
94
123
|
// jsImports: Map<importPath, string[]> — npm package imports to emit at top of file
|
|
95
124
|
// bundledPackages: Set<string> — GoFront package names bundled inline;
|
|
@@ -116,22 +145,7 @@ export class CodeGen {
|
|
|
116
145
|
this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
|
|
117
146
|
this._boxedVars = new Set(); // address-taken scalar variables that need boxing
|
|
118
147
|
// Runtime helper usage tracking — only emit helpers that are actually used
|
|
119
|
-
this.
|
|
120
|
-
this._usesAppend = false;
|
|
121
|
-
this._usesSliceGuard = false;
|
|
122
|
-
this._usesSprintf = false;
|
|
123
|
-
this._usesEqual = false;
|
|
124
|
-
this._usesCmul = false;
|
|
125
|
-
this._usesCdiv = false;
|
|
126
|
-
this._usesError = false;
|
|
127
|
-
this._usesErrorIs = false;
|
|
128
|
-
this._usesPathClean = false;
|
|
129
|
-
this._usesSortSlice = false;
|
|
130
|
-
this._usesTimeFmt = false;
|
|
131
|
-
this._usesTimeParse = false;
|
|
132
|
-
this._usesTesting = false;
|
|
133
|
-
this._usesSClone = false;
|
|
134
|
-
this._usesIfaceBox = false;
|
|
148
|
+
this._helpers = new Set();
|
|
135
149
|
// Per-function context for Go value semantics (see _withFnCtx)
|
|
136
150
|
this._fnCtx = null;
|
|
137
151
|
// Iterator (range-over-func) context
|
|
@@ -217,6 +231,10 @@ export class CodeGen {
|
|
|
217
231
|
|
|
218
232
|
generate(program, options = {}) {
|
|
219
233
|
const isTest = options.isTest ?? false;
|
|
234
|
+
// Defer lowering (argument/receiver hoisting, recover-target marking) is
|
|
235
|
+
// idempotent and owned by the backends: the wasm path runs it via
|
|
236
|
+
// `lower()`, the JS path here.
|
|
237
|
+
normalizeDefers(program);
|
|
220
238
|
// Ensure _pkgName is set on all decls (generateAll sets it for
|
|
221
239
|
// multi-file; for single-file compiles we derive it from the AST).
|
|
222
240
|
const pkgName = program.pkg?.name;
|
|
@@ -407,28 +425,21 @@ export class CodeGen {
|
|
|
407
425
|
return methods;
|
|
408
426
|
}
|
|
409
427
|
|
|
428
|
+
/** Marks a runtime helper (key of HELPER_MAP) as needed by the output. */
|
|
429
|
+
useHelper(name) {
|
|
430
|
+
this._helpers.add(name);
|
|
431
|
+
}
|
|
432
|
+
|
|
410
433
|
_prependHelpers(isTest = false) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
[this._usesCmul, HELPER_CMUL],
|
|
421
|
-
[this._usesCdiv, HELPER_CDIV],
|
|
422
|
-
[this._usesSprintf || needsTesting, HELPER_SPRINTF],
|
|
423
|
-
[this._usesError, HELPER_ERROR],
|
|
424
|
-
[this._usesErrorIs, HELPER_ERROR_IS],
|
|
425
|
-
[this._usesPathClean, HELPER_PATH_CLEAN],
|
|
426
|
-
[this._usesSortSlice, HELPER_SORT_SLICE],
|
|
427
|
-
[this._usesTimeFmt, HELPER_TIME_FMT],
|
|
428
|
-
[this._usesTimeParse, HELPER_TIME_PARSE],
|
|
429
|
-
[needsTesting, HELPER_TESTING],
|
|
430
|
-
];
|
|
431
|
-
const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
|
|
434
|
+
if (isTest || this._helpers.has("testing")) {
|
|
435
|
+
this.useHelper("sprintf");
|
|
436
|
+
this.useHelper("testing");
|
|
437
|
+
}
|
|
438
|
+
const helpers = [];
|
|
439
|
+
if (this.collectedCss.length > 0) helpers.push(HELPER_INJECT_STYLES);
|
|
440
|
+
for (const [name, src] of HELPER_MAP) {
|
|
441
|
+
if (this._helpers.has(name)) helpers.push(src);
|
|
442
|
+
}
|
|
432
443
|
if (helpers.length > 0) {
|
|
433
444
|
const helperLines = helpers.flatMap((h) => h.split("\n"));
|
|
434
445
|
helperLines.push("");
|
|
@@ -661,7 +672,7 @@ export class CodeGen {
|
|
|
661
672
|
}
|
|
662
673
|
}
|
|
663
674
|
this._emitParamCopies(decl.params);
|
|
664
|
-
this._withNamedReturns(decl, () => this._genBody(decl.body));
|
|
675
|
+
this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
|
|
665
676
|
}),
|
|
666
677
|
);
|
|
667
678
|
this._unwrappedRecv = prevUnwrapped;
|
|
@@ -718,7 +729,7 @@ export class CodeGen {
|
|
|
718
729
|
this._withFnCtx(decl.body, () =>
|
|
719
730
|
this.indented(() => {
|
|
720
731
|
this._emitParamCopies(decl.params);
|
|
721
|
-
this._withNamedReturns(decl, () => this._genBody(decl.body));
|
|
732
|
+
this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
|
|
722
733
|
}),
|
|
723
734
|
);
|
|
724
735
|
this._boxedVars = prevBoxed;
|
|
@@ -726,45 +737,74 @@ export class CodeGen {
|
|
|
726
737
|
}
|
|
727
738
|
|
|
728
739
|
_withNamedReturns(decl, fn) {
|
|
740
|
+
const prevDecl = this._currentDecl;
|
|
741
|
+
this._currentDecl = decl;
|
|
729
742
|
const named = decl.returnType?._namedReturns;
|
|
730
743
|
const prev = this.namedReturnVars;
|
|
731
744
|
if (named) {
|
|
732
745
|
// Emit zero-value declarations for named return vars
|
|
733
|
-
for (
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
746
|
+
for (let i = 0; i < named.length; i++) {
|
|
747
|
+
const { name, type } = named[i];
|
|
748
|
+
const varName = !name || name === "_" ? `__ret_blank$${i}` : name;
|
|
749
|
+
this._markOwnership(varName, true);
|
|
750
|
+
this.line(`let ${varName} = ${this.zeroValueForTypeNode(type)};`);
|
|
738
751
|
}
|
|
739
|
-
this.namedReturnVars = named.map((r) =>
|
|
752
|
+
this.namedReturnVars = named.map((r, i) =>
|
|
753
|
+
!r.name || r.name === "_" ? `__ret_blank$${i}` : r.name,
|
|
754
|
+
);
|
|
740
755
|
} else {
|
|
741
756
|
this.namedReturnVars = null;
|
|
742
757
|
}
|
|
743
758
|
fn();
|
|
744
759
|
this.namedReturnVars = prev;
|
|
760
|
+
this._currentDecl = prevDecl;
|
|
745
761
|
}
|
|
746
762
|
|
|
747
763
|
// Emit a function body, wrapping in try/catch/finally for defer if needed.
|
|
748
|
-
|
|
764
|
+
// `fnNode` is the enclosing FuncDecl/MethodDecl/FuncLit; it decides whether
|
|
765
|
+
// the recover-arming prologue is needed (see HELPER_PANIC).
|
|
766
|
+
_genBody(body, fnNode = null) {
|
|
767
|
+
const prevRecoverOk = this._recoverOkInScope;
|
|
768
|
+
this._recoverOkInScope = false;
|
|
769
|
+
const isTarget = Boolean(fnNode?._isDeferTarget);
|
|
770
|
+
const forwards = isTarget && Boolean(fnNode._deferForward);
|
|
771
|
+
if (!forwards && (isTarget || hasDirectRecover(body))) {
|
|
772
|
+
this.useHelper("panicRt");
|
|
773
|
+
this.line(
|
|
774
|
+
"const __recoverOk = __gopanic.armed; __gopanic.armed = false;",
|
|
775
|
+
);
|
|
776
|
+
this._recoverOkInScope = true;
|
|
777
|
+
}
|
|
749
778
|
if (!body._hasDefer) {
|
|
750
779
|
this.genBlock(body);
|
|
780
|
+
this._recoverOkInScope = prevRecoverOk;
|
|
751
781
|
return;
|
|
752
782
|
}
|
|
783
|
+
this.useHelper("panicRt");
|
|
753
784
|
this.line("const __defers = [];");
|
|
754
|
-
this.line("
|
|
785
|
+
this.line("const __frame = { pn: null };");
|
|
755
786
|
this.line("try {");
|
|
756
787
|
this.indented(() => this.genBlock(body));
|
|
757
788
|
this.line("} catch (__err) {");
|
|
758
|
-
this.indented(() =>
|
|
789
|
+
this.indented(() => {
|
|
790
|
+
this.line("__frame.pn = { err: __err, recovered: false };");
|
|
791
|
+
this.line("__gopanic.stack.push(__frame.pn);");
|
|
792
|
+
});
|
|
759
793
|
this.line("} finally {");
|
|
760
794
|
this.indented(() => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
)
|
|
764
|
-
|
|
795
|
+
// Runs defers LIFO and rethrows an unrecovered panic.
|
|
796
|
+
this.line("__runDefers(__defers, __frame);");
|
|
797
|
+
if (this.namedReturnVars?.length > 0) {
|
|
798
|
+
const vars = this.namedReturnVars;
|
|
799
|
+
this.line(
|
|
800
|
+
vars.length === 1
|
|
801
|
+
? `return ${vars[0]};`
|
|
802
|
+
: `return [${vars.join(", ")}];`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
765
805
|
});
|
|
766
806
|
this.line("}");
|
|
767
|
-
// If a recover() cleared
|
|
807
|
+
// If a recover() cleared the panic, execution reaches here.
|
|
768
808
|
// Return named return vars so deferred mutations are visible to the caller.
|
|
769
809
|
if (this.namedReturnVars?.length > 0) {
|
|
770
810
|
const vars = this.namedReturnVars;
|
|
@@ -773,7 +813,12 @@ export class CodeGen {
|
|
|
773
813
|
? `return ${vars[0]};`
|
|
774
814
|
: `return [${vars.join(", ")}];`,
|
|
775
815
|
);
|
|
816
|
+
} else if (this._currentDecl?.returnType) {
|
|
817
|
+
this.line(
|
|
818
|
+
`return ${this.zeroValueForTypeNode(this._currentDecl.returnType)};`,
|
|
819
|
+
);
|
|
776
820
|
}
|
|
821
|
+
this._recoverOkInScope = prevRecoverOk;
|
|
777
822
|
}
|
|
778
823
|
|
|
779
824
|
// Scan AST node for _addressTaken idents on scalars and populate _boxedVars.
|
|
@@ -55,6 +55,13 @@ var __ifp = __ifp || function(x) {
|
|
|
55
55
|
return x;
|
|
56
56
|
};`;
|
|
57
57
|
|
|
58
|
+
// Nil-interface method call guard (strict mode only): matches Go's panic
|
|
59
|
+
// message instead of a JS TypeError.
|
|
60
|
+
export const HELPER_NIL_CALL = `var __nilcall = __nilcall || function(x) {
|
|
61
|
+
if (x == null) throw new Error("runtime error: invalid memory address or nil pointer dereference");
|
|
62
|
+
return x;
|
|
63
|
+
};`;
|
|
64
|
+
|
|
58
65
|
export const HELPER_EQUAL = `var __equal = __equal || function __equal(a, b) {
|
|
59
66
|
if (a === b) return true;
|
|
60
67
|
if (a === null || b === null) return false;
|
|
@@ -86,6 +93,35 @@ export const HELPER_CDIV = `var __cdiv = __cdiv || function(a, b) {
|
|
|
86
93
|
return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d };
|
|
87
94
|
};`;
|
|
88
95
|
|
|
96
|
+
// Panic/defer runtime. \`stack\` holds the in-flight panic of every frame
|
|
97
|
+
// currently running its defers; \`armed\` is set right before a deferred
|
|
98
|
+
// function is invoked and consumed by that function's prologue, so recover()
|
|
99
|
+
// only takes effect when called directly by the deferred function (Go spec).
|
|
100
|
+
export const HELPER_PANIC = `var __gopanic = __gopanic || { stack: [], armed: false };
|
|
101
|
+
var __runDefers = __runDefers || function(defers, frame) {
|
|
102
|
+
for (let i = defers.length - 1; i >= 0; i--) {
|
|
103
|
+
__gopanic.armed = true;
|
|
104
|
+
try { defers[i](); }
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (frame.pn !== null) __gopanic.stack.pop();
|
|
107
|
+
frame.pn = { err: e, recovered: false };
|
|
108
|
+
__gopanic.stack.push(frame.pn);
|
|
109
|
+
} finally { __gopanic.armed = false; }
|
|
110
|
+
}
|
|
111
|
+
if (frame.pn !== null) {
|
|
112
|
+
__gopanic.stack.pop();
|
|
113
|
+
if (!frame.pn.recovered) throw frame.pn.err;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var __recover = __recover || function(ok) {
|
|
117
|
+
if (!ok) return null;
|
|
118
|
+
const s = __gopanic.stack;
|
|
119
|
+
const top = s.length > 0 ? s[s.length - 1] : null;
|
|
120
|
+
if (top === null || top.recovered) return null;
|
|
121
|
+
top.recovered = true;
|
|
122
|
+
return top.err?.message ?? String(top.err);
|
|
123
|
+
};`;
|
|
124
|
+
|
|
89
125
|
export const HELPER_SPRINTF = `var __sprintf = __sprintf || function(f, ...a) {
|
|
90
126
|
let i = 0;
|
|
91
127
|
return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g, (m) => {
|
|
@@ -150,6 +186,72 @@ export const HELPER_ERROR_IS = `var __errorIs = __errorIs || function(err, targe
|
|
|
150
186
|
return false;
|
|
151
187
|
};`;
|
|
152
188
|
|
|
189
|
+
// Go-semantics strconv parsers shared by the JS backend and the wasm runtime
|
|
190
|
+
// imports (boundary.js), so both targets agree. Each parser returns
|
|
191
|
+
// [value, errorMessage | null]; integer values are BigInt (the wasm side
|
|
192
|
+
// needs i64, the JS side converts with Number()).
|
|
193
|
+
export const HELPER_STRCONV = `var __strconv_atoi = __strconv_atoi || function(s) {
|
|
194
|
+
if (!/^[+-]?[0-9]+$/.test(s)) return [0n, 'strconv.Atoi: parsing "' + s + '": invalid syntax'];
|
|
195
|
+
const v = BigInt(s);
|
|
196
|
+
if (v > 9223372036854775807n || v < -9223372036854775808n) {
|
|
197
|
+
return [v > 0n ? 9223372036854775807n : -9223372036854775808n, 'strconv.Atoi: parsing "' + s + '": value out of range'];
|
|
198
|
+
}
|
|
199
|
+
return [v, null];
|
|
200
|
+
};
|
|
201
|
+
var __strconv_parse_int = __strconv_parse_int || function(s, base, bits) {
|
|
202
|
+
let str = s, neg = false;
|
|
203
|
+
if (str[0] === "-" || str[0] === "+") { neg = str[0] === "-"; str = str.slice(1); }
|
|
204
|
+
if (base === 0) {
|
|
205
|
+
if (/^0[xX]/.test(str)) { base = 16; str = str.slice(2); }
|
|
206
|
+
else if (/^0[bB]/.test(str)) { base = 2; str = str.slice(2); }
|
|
207
|
+
else if (/^0[oO]/.test(str)) { base = 8; str = str.slice(2); }
|
|
208
|
+
else if (/^0[0-9]/.test(str)) { base = 8; str = str.slice(1); }
|
|
209
|
+
else base = 10;
|
|
210
|
+
str = str.replaceAll("_", "");
|
|
211
|
+
}
|
|
212
|
+
if (base < 2 || base > 36) return [0n, 'strconv.ParseInt: parsing "' + s + '": invalid base ' + base];
|
|
213
|
+
const digits = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, base);
|
|
214
|
+
if (str === "" || [...str.toLowerCase()].some((c) => !digits.includes(c))) {
|
|
215
|
+
return [0n, 'strconv.ParseInt: parsing "' + s + '": invalid syntax'];
|
|
216
|
+
}
|
|
217
|
+
let v = 0n; const B = BigInt(base);
|
|
218
|
+
for (const c of str.toLowerCase()) v = v * B + BigInt(digits.indexOf(c));
|
|
219
|
+
if (neg) v = -v;
|
|
220
|
+
const w = bits === 0 ? 64 : bits;
|
|
221
|
+
const max = (1n << BigInt(w - 1)) - 1n, min = -(1n << BigInt(w - 1));
|
|
222
|
+
if (v > max || v < min) return [v > max ? max : min, 'strconv.ParseInt: parsing "' + s + '": value out of range'];
|
|
223
|
+
return [v, null];
|
|
224
|
+
};
|
|
225
|
+
var __strconv_parse_float = __strconv_parse_float || function(s) {
|
|
226
|
+
const str = s.replaceAll("_", "");
|
|
227
|
+
if (/^[+-]?(inf|infinity)$/i.test(str)) return [str[0] === "-" ? -Infinity : Infinity, null];
|
|
228
|
+
if (/^[+-]?nan$/i.test(str)) return [NaN, null];
|
|
229
|
+
if (str === "" || !/^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$|^[+-]?0[xX][0-9a-fA-F]+$/.test(str)) {
|
|
230
|
+
return [0, 'strconv.ParseFloat: parsing "' + s + '": invalid syntax'];
|
|
231
|
+
}
|
|
232
|
+
return [Number(str), null];
|
|
233
|
+
};
|
|
234
|
+
var __strconv_parse_bool = __strconv_parse_bool || function(s) {
|
|
235
|
+
if (["1", "t", "T", "TRUE", "true", "True"].includes(s)) return [true, null];
|
|
236
|
+
if (["0", "f", "F", "FALSE", "false", "False"].includes(s)) return [false, null];
|
|
237
|
+
return [false, 'strconv.ParseBool: parsing "' + s + '": invalid syntax'];
|
|
238
|
+
};
|
|
239
|
+
var __strconv_format_float = __strconv_format_float || function(f, fmtc, prec) {
|
|
240
|
+
const c = String.fromCharCode(fmtc);
|
|
241
|
+
if (!Number.isFinite(f)) return f !== f ? "NaN" : (f > 0 ? "+Inf" : "-Inf");
|
|
242
|
+
if (c === "f") return prec < 0 ? String(f) : f.toFixed(prec);
|
|
243
|
+
if (c === "e" || c === "E") {
|
|
244
|
+
const r = prec < 0 ? f.toExponential() : f.toExponential(prec);
|
|
245
|
+
const o = r.replace(/e([+-])(\\d)$/, "e$10$2");
|
|
246
|
+
return c === "E" ? o.toUpperCase() : o;
|
|
247
|
+
}
|
|
248
|
+
if (c === "g" || c === "G") {
|
|
249
|
+
const r = prec < 0 ? String(f) : f.toPrecision(prec);
|
|
250
|
+
return c === "G" ? r.toUpperCase() : r;
|
|
251
|
+
}
|
|
252
|
+
return String(f);
|
|
253
|
+
};`;
|
|
254
|
+
|
|
153
255
|
export const HELPER_PATH_CLEAN = `var __pathClean = __pathClean || function(p) {
|
|
154
256
|
if (!p) return ".";
|
|
155
257
|
const abs = p.startsWith("/");
|
|
@@ -57,3 +57,35 @@ export function buildSourceMap(sources, mappings, sourcesContent) {
|
|
|
57
57
|
if (sourcesContent) map.sourcesContent = sourcesContent;
|
|
58
58
|
return JSON.stringify(map);
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
// WASM source maps use the convention "generated line 0, generated column =
|
|
62
|
+
// byte offset into the module". `mappings` is Array<{ offset, srcFileIdx,
|
|
63
|
+
// srcLine }> (0-based line), one segment per function body (function-level
|
|
64
|
+
// granularity); DevTools shows the Go file/line for each wasm function.
|
|
65
|
+
export function buildWasmSourceMap(sources, mappings, sourcesContent) {
|
|
66
|
+
const sorted = [...mappings].sort((a, b) => a.offset - b.offset);
|
|
67
|
+
const segments = [];
|
|
68
|
+
let prevOffset = 0;
|
|
69
|
+
let prevSrcFile = 0;
|
|
70
|
+
let prevSrcLine = 0;
|
|
71
|
+
for (const m of sorted) {
|
|
72
|
+
const fileIdx = m.srcFileIdx ?? 0;
|
|
73
|
+
segments.push(
|
|
74
|
+
vlqEncode(m.offset - prevOffset) +
|
|
75
|
+
vlqEncode(fileIdx - prevSrcFile) +
|
|
76
|
+
vlqEncode(m.srcLine - prevSrcLine) +
|
|
77
|
+
vlqEncode(0),
|
|
78
|
+
);
|
|
79
|
+
prevOffset = m.offset;
|
|
80
|
+
prevSrcFile = fileIdx;
|
|
81
|
+
prevSrcLine = m.srcLine;
|
|
82
|
+
}
|
|
83
|
+
const map = {
|
|
84
|
+
version: 3,
|
|
85
|
+
sources: sources.map((s) => s.replace(/\\/g, "/")),
|
|
86
|
+
names: [],
|
|
87
|
+
mappings: segments.join(","),
|
|
88
|
+
};
|
|
89
|
+
if (sourcesContent) map.sourcesContent = sourcesContent;
|
|
90
|
+
return JSON.stringify(map);
|
|
91
|
+
}
|