gofront 1.3.7 → 1.3.8
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 +2 -0
- package/package.json +1 -1
- package/src/cli-core.js +86 -0
- package/src/codegen/expressions.js +27 -7
- package/src/codegen/index.js +3 -0
- package/src/codegen/runtime.js +15 -0
- package/src/codegen/statements.js +6 -4
- package/src/codegen/stdlib/math.js +2 -1
- package/src/codegen/stdlib/sort.js +4 -5
- package/src/compiler.js +9 -2
- package/src/index.js +16 -5
- package/src/parser/expressions.js +1 -0
- package/src/typechecker/assignability.js +2 -0
- package/src/typechecker/expressions.js +30 -0
- package/src/typechecker/resolve.js +18 -1
- package/src/typechecker/stdlib/extended.js +2 -1
package/README.md
CHANGED
|
@@ -387,6 +387,8 @@ gofront <file.go> compile single file → stdout
|
|
|
387
387
|
gofront <dir> compile all *.go in directory → stdout
|
|
388
388
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
389
389
|
gofront <input> --check type-check only
|
|
390
|
+
gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
|
|
391
|
+
gofront test <dir>/... [--dom] run tests for every package under <dir>
|
|
390
392
|
gofront <input> --watch watch for changes and recompile
|
|
391
393
|
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
392
394
|
gofront <input> -o out.js --serve --port 8080 use a custom port
|
package/package.json
CHANGED
package/src/cli-core.js
CHANGED
|
@@ -229,6 +229,9 @@ export function parseTestArgs(argv) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
export function handleTest(targetDir = ".", options = {}) {
|
|
232
|
+
const pattern = parsePackagePattern(targetDir);
|
|
233
|
+
if (pattern) return runTestsRecursive(pattern, options);
|
|
234
|
+
|
|
232
235
|
let testTarget = targetDir;
|
|
233
236
|
try {
|
|
234
237
|
const project = detectProject(targetDir);
|
|
@@ -246,6 +249,69 @@ export function handleTest(targetDir = ".", options = {}) {
|
|
|
246
249
|
return runTests(testTarget, options);
|
|
247
250
|
}
|
|
248
251
|
|
|
252
|
+
async function runTestsRecursive(rootDir, options) {
|
|
253
|
+
const dirs = findPackageDirs(rootDir);
|
|
254
|
+
if (dirs.length === 0) {
|
|
255
|
+
throw new Error(`No .go files found under ${rootDir}`);
|
|
256
|
+
}
|
|
257
|
+
let exitCode = 0;
|
|
258
|
+
let stdout = "";
|
|
259
|
+
let stderr = "";
|
|
260
|
+
for (const dir of dirs) {
|
|
261
|
+
const result = await runTests(dir, options);
|
|
262
|
+
if (result.exitCode !== 0) exitCode = result.exitCode;
|
|
263
|
+
if (options.captureOutput) {
|
|
264
|
+
stdout += result.stdout ?? "";
|
|
265
|
+
stderr += result.stderr ?? "";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return options.captureOutput
|
|
269
|
+
? { exitCode, stdout, stderr, packages: dirs }
|
|
270
|
+
: { exitCode, packages: dirs };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Package patterns (`dir/...`) ──────────────────────────────
|
|
274
|
+
|
|
275
|
+
// Returns the root directory of a Go-style recursive pattern (`./...`,
|
|
276
|
+
// `app/src/...`), or null when the argument is a plain path.
|
|
277
|
+
export function parsePackagePattern(arg) {
|
|
278
|
+
if (arg === "...") return resolve(".");
|
|
279
|
+
if (!arg.endsWith("/...")) return null;
|
|
280
|
+
return resolve(arg.slice(0, -4) || ".");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const SKIPPED_DIRS = new Set(["node_modules", "dist", "public"]);
|
|
284
|
+
|
|
285
|
+
// Lists every directory under root (inclusive) that contains .go or .templ
|
|
286
|
+
// files, in sorted order, skipping hidden directories and build output.
|
|
287
|
+
export function findPackageDirs(root) {
|
|
288
|
+
const out = [];
|
|
289
|
+
const walk = (dir) => {
|
|
290
|
+
let entries;
|
|
291
|
+
try {
|
|
292
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
293
|
+
} catch {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
297
|
+
if (
|
|
298
|
+
entries.some(
|
|
299
|
+
(e) =>
|
|
300
|
+
e.isFile() && (e.name.endsWith(".go") || e.name.endsWith(".templ")),
|
|
301
|
+
)
|
|
302
|
+
) {
|
|
303
|
+
out.push(dir);
|
|
304
|
+
}
|
|
305
|
+
for (const e of entries) {
|
|
306
|
+
if (!e.isDirectory()) continue;
|
|
307
|
+
if (e.name.startsWith(".") || SKIPPED_DIRS.has(e.name)) continue;
|
|
308
|
+
walk(join(dir, e.name));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
walk(resolve(root));
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
249
315
|
// ── Project detection ─────────────────────────────────────────
|
|
250
316
|
|
|
251
317
|
function loadProjectConfig(projectRoot) {
|
|
@@ -357,6 +423,26 @@ export function parseCheckArgs(argv) {
|
|
|
357
423
|
}
|
|
358
424
|
|
|
359
425
|
export function handleCheck(targetDir = ".", options = {}) {
|
|
426
|
+
const pattern = parsePackagePattern(targetDir);
|
|
427
|
+
if (pattern) {
|
|
428
|
+
const dirs = findPackageDirs(pattern);
|
|
429
|
+
if (dirs.length === 0) {
|
|
430
|
+
throw new Error(`No .go files found under ${targetDir}`);
|
|
431
|
+
}
|
|
432
|
+
const startMs = performance.now();
|
|
433
|
+
const packages = [];
|
|
434
|
+
for (const dir of dirs) {
|
|
435
|
+
const pkgStart = performance.now();
|
|
436
|
+
runCompile(dir, true, { ...options });
|
|
437
|
+
packages.push({
|
|
438
|
+
dir,
|
|
439
|
+
elapsedMs: (performance.now() - pkgStart).toFixed(0),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
443
|
+
return { target: targetDir, compileTarget: pattern, elapsedMs, packages };
|
|
444
|
+
}
|
|
445
|
+
|
|
360
446
|
const project = detectProject(targetDir);
|
|
361
447
|
const targetPath = resolve(targetDir);
|
|
362
448
|
let isDir = false;
|
|
@@ -610,16 +610,23 @@ export const expressionGenMethods = {
|
|
|
610
610
|
},
|
|
611
611
|
|
|
612
612
|
_genTypeAssertExpr(expr) {
|
|
613
|
-
const
|
|
613
|
+
const src = this.genExpr(expr.expr);
|
|
614
|
+
// The operand appears in both the check and the result; hoist calls so
|
|
615
|
+
// they run once.
|
|
616
|
+
const hoist = this._hasCallExpr(expr.expr);
|
|
617
|
+
const val = hoist ? "__v" : src;
|
|
614
618
|
const check = this._typeCheckExpr(expr.type, val);
|
|
619
|
+
const wrap = (body) => (hoist ? `((__v) => ${body})(${src})` : body);
|
|
615
620
|
if (!expr._commaOk) {
|
|
616
621
|
// plain assertion: panic if check fails (matches Go behavior)
|
|
617
|
-
if (check === "true") return
|
|
618
|
-
return
|
|
622
|
+
if (check === "true") return src; // can't check at runtime — pass through
|
|
623
|
+
return wrap(
|
|
624
|
+
`(${check} ? ${val} : (() => { throw new Error("interface conversion: type assertion failed"); })())`,
|
|
625
|
+
);
|
|
619
626
|
}
|
|
620
627
|
// comma-ok: emit [value-or-zero, runtimeTypeCheck]
|
|
621
628
|
const zero = this.zeroValueForTypeNode(expr.type);
|
|
622
|
-
return `(${check} ? [${val}, true] : [${zero}, false])
|
|
629
|
+
return wrap(`(${check} ? [${val}, true] : [${zero}, false])`);
|
|
623
630
|
},
|
|
624
631
|
|
|
625
632
|
_genTypeConversionCall(expr) {
|
|
@@ -1028,16 +1035,28 @@ export const expressionGenMethods = {
|
|
|
1028
1035
|
|
|
1029
1036
|
getTypeName(typeNode) {
|
|
1030
1037
|
if (!typeNode) return null;
|
|
1031
|
-
if (typeNode.kind === "TypeName") return typeNode.name;
|
|
1038
|
+
if (typeNode.kind === "TypeName") return this._dequalify(typeNode.name);
|
|
1032
1039
|
if (typeNode.kind === "GenericTypeName") return typeNode.name;
|
|
1033
1040
|
if (typeNode.kind === "Ident") return typeNode.name;
|
|
1034
1041
|
if (typeNode.kind === "InstantiationExpr")
|
|
1035
1042
|
return this.getTypeName(typeNode.expr);
|
|
1036
|
-
if (typeNode.kind === "SelectorExpr")
|
|
1037
|
-
|
|
1043
|
+
if (typeNode.kind === "SelectorExpr") {
|
|
1044
|
+
const base = this.getTypeName(typeNode.expr);
|
|
1045
|
+
if (this.bundledPackages.has(base)) return typeNode.field;
|
|
1046
|
+
return `${base}.${typeNode.field}`;
|
|
1047
|
+
}
|
|
1038
1048
|
return null;
|
|
1039
1049
|
},
|
|
1040
1050
|
|
|
1051
|
+
// Bundled GoFront packages are inlined, so `pkg.T` names the class `T`.
|
|
1052
|
+
_dequalify(name) {
|
|
1053
|
+
const dot = name.indexOf(".");
|
|
1054
|
+
if (dot < 0) return name;
|
|
1055
|
+
return this.bundledPackages.has(name.slice(0, dot))
|
|
1056
|
+
? name.slice(dot + 1)
|
|
1057
|
+
: name;
|
|
1058
|
+
},
|
|
1059
|
+
|
|
1041
1060
|
isIntType(t) {
|
|
1042
1061
|
if (!t) return false;
|
|
1043
1062
|
if (t.kind === "untyped") return t.base === "int";
|
|
@@ -1063,6 +1082,7 @@ export const expressionGenMethods = {
|
|
|
1063
1082
|
_zeroForNamedType(name) {
|
|
1064
1083
|
if (name === "strings.Builder") return '{ _buf: "" }';
|
|
1065
1084
|
if (name === "bytes.Buffer") return "{ _buf: [] }";
|
|
1085
|
+
name = this._dequalify(name);
|
|
1066
1086
|
if (this.structNames.has(name)) return `new ${name}()`;
|
|
1067
1087
|
return "null";
|
|
1068
1088
|
},
|
package/src/codegen/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
HELPER_PATH_CLEAN,
|
|
32
32
|
HELPER_S,
|
|
33
33
|
HELPER_SCLONE,
|
|
34
|
+
HELPER_SORT_SLICE,
|
|
34
35
|
HELPER_SPRINTF,
|
|
35
36
|
HELPER_TESTING,
|
|
36
37
|
HELPER_TIME_FMT,
|
|
@@ -113,6 +114,7 @@ export class CodeGen {
|
|
|
113
114
|
this._usesError = false;
|
|
114
115
|
this._usesErrorIs = false;
|
|
115
116
|
this._usesPathClean = false;
|
|
117
|
+
this._usesSortSlice = false;
|
|
116
118
|
this._usesTimeFmt = false;
|
|
117
119
|
this._usesTimeParse = false;
|
|
118
120
|
this._usesTesting = false;
|
|
@@ -377,6 +379,7 @@ export class CodeGen {
|
|
|
377
379
|
[this._usesError, HELPER_ERROR],
|
|
378
380
|
[this._usesErrorIs, HELPER_ERROR_IS],
|
|
379
381
|
[this._usesPathClean, HELPER_PATH_CLEAN],
|
|
382
|
+
[this._usesSortSlice, HELPER_SORT_SLICE],
|
|
380
383
|
[this._usesTimeFmt, HELPER_TIME_FMT],
|
|
381
384
|
[this._usesTimeParse, HELPER_TIME_PARSE],
|
|
382
385
|
[needsTesting, HELPER_TESTING],
|
package/src/codegen/runtime.js
CHANGED
|
@@ -19,6 +19,21 @@ export const HELPER_APPEND = `var __append = __append || function(a, ...b) {
|
|
|
19
19
|
|
|
20
20
|
export const HELPER_S = "var __s = __s || function(a) { return a || []; };";
|
|
21
21
|
|
|
22
|
+
// In-place stable sort driven by Go's less(i, j) index comparator. The
|
|
23
|
+
// comparator reads the live slice, so elements are moved as it runs
|
|
24
|
+
// (binary insertion: O(n log n) compares, O(n^2) moves, fine for typed arrays).
|
|
25
|
+
export const HELPER_SORT_SLICE = `var __sortSlice = __sortSlice || function(s, less) {
|
|
26
|
+
if (!s) return;
|
|
27
|
+
for (let i = 1; i < s.length; i++) {
|
|
28
|
+
let lo = 0, hi = i;
|
|
29
|
+
while (lo < hi) { const mid = (lo + hi) >>> 1; if (less(i, mid)) hi = mid; else lo = mid + 1; }
|
|
30
|
+
if (lo === i) continue;
|
|
31
|
+
const v = s[i];
|
|
32
|
+
for (let k = i; k > lo; k--) s[k] = s[k - 1];
|
|
33
|
+
s[lo] = v;
|
|
34
|
+
}
|
|
35
|
+
};`;
|
|
36
|
+
|
|
22
37
|
// Copy of a struct value whose class is not statically known.
|
|
23
38
|
export const HELPER_SCLONE = `var __sclone = __sclone || function(x) {
|
|
24
39
|
if (x === null || typeof x !== "object") return x;
|
|
@@ -253,8 +253,9 @@ export const statementGenMethods = {
|
|
|
253
253
|
_genCommaOkTypeAssert(stmt) {
|
|
254
254
|
const val = this.genExpr(stmt.rhs[0]);
|
|
255
255
|
const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
this._tmpCounter = (this._tmpCounter ?? 0) + 1;
|
|
257
|
+
const tmp = `__ta${this._tmpCounter}`;
|
|
258
|
+
this.line(`const ${tmp} = ${val};`);
|
|
258
259
|
if (vName !== "_") this.line(`${vName} = ${tmp}[0];`);
|
|
259
260
|
if (okName !== "_") this.line(`${okName} = ${tmp}[1];`);
|
|
260
261
|
},
|
|
@@ -273,8 +274,9 @@ export const statementGenMethods = {
|
|
|
273
274
|
},
|
|
274
275
|
|
|
275
276
|
_genMultiAssignWithBlanks(stmt, lhs, rhs, rhsStr) {
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
this._tmpCounter = (this._tmpCounter ?? 0) + 1;
|
|
278
|
+
const tmp = `__t${this._tmpCounter}`;
|
|
279
|
+
this.line(`const ${tmp} = ${rhsStr};`);
|
|
278
280
|
for (let i = 0; i < lhs.length; i++) {
|
|
279
281
|
if (lhs[i] !== "_") {
|
|
280
282
|
const src = rhs.length === 1 ? `${tmp}[${i}]` : rhs[i];
|
|
@@ -6,7 +6,6 @@ const MATH1 = {
|
|
|
6
6
|
Abs: "abs",
|
|
7
7
|
Floor: "floor",
|
|
8
8
|
Ceil: "ceil",
|
|
9
|
-
Round: "round",
|
|
10
9
|
Sqrt: "sqrt",
|
|
11
10
|
Cbrt: "cbrt",
|
|
12
11
|
Log: "log",
|
|
@@ -29,6 +28,8 @@ const MATH2 = {
|
|
|
29
28
|
Hypot: "hypot",
|
|
30
29
|
};
|
|
31
30
|
const MATH_EXTRA = {
|
|
31
|
+
// Go rounds half away from zero; Math.round rounds half toward +Infinity.
|
|
32
|
+
Round: ([x]) => `(Math.sign(${x}) * Math.round(Math.abs(${x})))`,
|
|
32
33
|
Mod: ([x, y]) => `${x} % ${y}`,
|
|
33
34
|
Inf: ([x]) => `(${x} >= 0 ? Infinity : -Infinity)`,
|
|
34
35
|
IsNaN: ([x]) => `Number.isNaN(${x})`,
|
|
@@ -6,12 +6,10 @@ const SORT_DISPATCH = {
|
|
|
6
6
|
Ints: (a) => `${a[0]}.sort((a, b) => a - b)`,
|
|
7
7
|
Float64s: (a) => `${a[0]}.sort((a, b) => a - b)`,
|
|
8
8
|
Strings: (a) => `${a[0]}.sort()`,
|
|
9
|
-
Slice: (a) =>
|
|
10
|
-
|
|
11
|
-
SliceStable: (a) =>
|
|
12
|
-
`${a[0]}.sort((a, b) => ${a[1]}(a, b) ? -1 : ${a[1]}(b, a) ? 1 : 0)`,
|
|
9
|
+
Slice: (a) => `__sortSlice(${a[0]}, ${a[1]})`,
|
|
10
|
+
SliceStable: (a) => `__sortSlice(${a[0]}, ${a[1]})`,
|
|
13
11
|
SliceIsSorted: (a) =>
|
|
14
|
-
|
|
12
|
+
`((s, less) => { for (let i = (s?.length ?? 0) - 1; i > 0; i--) if (less(i, i - 1)) return false; return true; })(${a[0]}, ${a[1]})`,
|
|
15
13
|
Search: (a) =>
|
|
16
14
|
`((n, f) => { let lo = 0, hi = n; while (lo < hi) { const mid = (lo + hi) >>> 1; if (f(mid)) hi = mid; else lo = mid + 1; } return lo; })(${a[0]}, ${a[1]})`,
|
|
17
15
|
IntsAreSorted: (a) =>
|
|
@@ -25,6 +23,7 @@ const SORT_DISPATCH = {
|
|
|
25
23
|
/** @type {ThisType<CodeGen>} */
|
|
26
24
|
export const sortMethods = {
|
|
27
25
|
_genSort(fn, a) {
|
|
26
|
+
if (fn === "Slice" || fn === "SliceStable") this._usesSortSlice = true;
|
|
28
27
|
const gen = SORT_DISPATCH[fn];
|
|
29
28
|
return gen ? gen(a()) : undefined;
|
|
30
29
|
},
|
package/src/compiler.js
CHANGED
|
@@ -85,6 +85,7 @@ export function resolveImports(
|
|
|
85
85
|
jsImports,
|
|
86
86
|
bundledPackages,
|
|
87
87
|
preambles,
|
|
88
|
+
bundledDirs = new Set(),
|
|
88
89
|
) {
|
|
89
90
|
const fromDir = dirname(resolve(fromFile));
|
|
90
91
|
const allImports = programs.flatMap((p) => p.imports);
|
|
@@ -130,8 +131,13 @@ export function resolveImports(
|
|
|
130
131
|
);
|
|
131
132
|
continue;
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Diamond imports: a package reached through several paths is
|
|
135
|
+
// compiled for its symbols each time but emitted only once.
|
|
136
|
+
const depKey = resolve(depDir);
|
|
137
|
+
const alreadyBundled = bundledDirs.has(depKey);
|
|
138
|
+
bundledDirs.add(depKey);
|
|
139
|
+
const dep = compileDir(depDir, { bundledDirs });
|
|
140
|
+
if (!alreadyBundled) preambles.push(dep.js);
|
|
135
141
|
if (alias === "_") continue;
|
|
136
142
|
if (alias === ".") {
|
|
137
143
|
checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
|
|
@@ -260,6 +266,7 @@ export function compileFiles(files, options = {}) {
|
|
|
260
266
|
jsImports,
|
|
261
267
|
bundledPackages,
|
|
262
268
|
preambles,
|
|
269
|
+
options.bundledDirs ?? new Set(),
|
|
263
270
|
);
|
|
264
271
|
|
|
265
272
|
// ── 3. Type-check ─────────────────────────────────────────────
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const _require = createRequire(import.meta.url);
|
|
|
19
19
|
const { version } = _require("../package.json");
|
|
20
20
|
|
|
21
21
|
import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
22
|
-
import { dirname, resolve } from "node:path";
|
|
22
|
+
import { dirname, relative, resolve } from "node:path";
|
|
23
23
|
import { copyAssets } from "./asset-manager.js";
|
|
24
24
|
import {
|
|
25
25
|
formatBuildSummary,
|
|
@@ -56,8 +56,8 @@ GoFront — a Go-inspired language that compiles to JavaScript
|
|
|
56
56
|
Usage:
|
|
57
57
|
gofront dev [dir] [options] Start dev server with live reload (default port 3000)
|
|
58
58
|
gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify)
|
|
59
|
-
gofront check [dir]
|
|
60
|
-
gofront test [dir] [--dom]
|
|
59
|
+
gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
|
|
60
|
+
gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
61
61
|
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
62
62
|
gofront init [dir] Scaffold a new GoFront project
|
|
63
63
|
gofront <file.go> Compile single file and print to stdout
|
|
@@ -131,8 +131,19 @@ if (args[0] === "test") {
|
|
|
131
131
|
if (args[0] === "check") {
|
|
132
132
|
const { targetDir } = parseCheckArgs(args.slice(1));
|
|
133
133
|
try {
|
|
134
|
-
const { elapsedMs } = handleCheck(targetDir);
|
|
135
|
-
|
|
134
|
+
const { elapsedMs, packages } = handleCheck(targetDir);
|
|
135
|
+
if (packages) {
|
|
136
|
+
for (const pkg of packages) {
|
|
137
|
+
console.error(
|
|
138
|
+
`gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
console.error(
|
|
142
|
+
`gofront: ${packages.length} packages — OK (${elapsedMs}ms)`,
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
|
|
146
|
+
}
|
|
136
147
|
process.exit(0);
|
|
137
148
|
} catch (e) {
|
|
138
149
|
console.error(`gofront: ${e.message}`);
|
|
@@ -343,6 +343,8 @@ export const assignabilityMethods = {
|
|
|
343
343
|
},
|
|
344
344
|
|
|
345
345
|
implements(srcType, iface, _node) {
|
|
346
|
+
// *T has the full method set of T (value + pointer receivers).
|
|
347
|
+
if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
|
|
346
348
|
let base = srcType.kind === "named" ? srcType.underlying : srcType;
|
|
347
349
|
base = this.resolveType(base);
|
|
348
350
|
const methodMap =
|
|
@@ -430,9 +430,39 @@ export const expressionCheckMethods = {
|
|
|
430
430
|
}
|
|
431
431
|
return this.instantiateGenericFunc(baseType, typeArgs);
|
|
432
432
|
}
|
|
433
|
+
// The parser's type-arg heuristic mistakes `xs[d.Field]` (uppercase field
|
|
434
|
+
// selector as index) for a generic instantiation. When the base is an
|
|
435
|
+
// indexable value and the sole "type arg" is a plain/qualified name,
|
|
436
|
+
// rewrite the node in place into an IndexExpr and check that instead.
|
|
437
|
+
const indexExpr = this._instantiationAsIndex(expr, baseType);
|
|
438
|
+
if (indexExpr) return this._checkIndexExpr(indexExpr, scope);
|
|
433
439
|
return baseType;
|
|
434
440
|
},
|
|
435
441
|
|
|
442
|
+
_instantiationAsIndex(expr, baseType) {
|
|
443
|
+
if (!baseType || expr.typeArgs.length !== 1) return null;
|
|
444
|
+
const ta = expr.typeArgs[0];
|
|
445
|
+
if (ta.kind !== "TypeName") return null;
|
|
446
|
+
const btu =
|
|
447
|
+
baseType.kind === "named"
|
|
448
|
+
? (this.resolveType(baseType.underlying) ?? baseType.underlying)
|
|
449
|
+
: baseType;
|
|
450
|
+
const indexable =
|
|
451
|
+
btu?.kind === "slice" ||
|
|
452
|
+
btu?.kind === "array" ||
|
|
453
|
+
btu?.kind === "map" ||
|
|
454
|
+
isString(baseType);
|
|
455
|
+
if (!indexable) return null;
|
|
456
|
+
const parts = ta.name.split(".");
|
|
457
|
+
let index = { kind: "Ident", name: parts[0], _line: expr._line };
|
|
458
|
+
for (let i = 1; i < parts.length; i++)
|
|
459
|
+
index = { kind: "SelectorExpr", expr: index, field: parts[i] };
|
|
460
|
+
expr.kind = "IndexExpr";
|
|
461
|
+
expr.index = index;
|
|
462
|
+
delete expr.typeArgs;
|
|
463
|
+
return expr;
|
|
464
|
+
},
|
|
465
|
+
|
|
436
466
|
_constIntValue(expr) {
|
|
437
467
|
if (expr.kind === "BasicLit" && expr.litKind === "INT")
|
|
438
468
|
return Number(expr.value);
|
|
@@ -88,7 +88,12 @@ export const resolveMethods = {
|
|
|
88
88
|
if (fromScope?.kind === "typeParam") return fromScope;
|
|
89
89
|
}
|
|
90
90
|
const named = this.types.get(node.name);
|
|
91
|
-
if (named)
|
|
91
|
+
if (named) {
|
|
92
|
+
// `pkg.T` in a type position counts as using the import.
|
|
93
|
+
const dot = node.name.indexOf(".");
|
|
94
|
+
if (dot > 0) this.globals.lookup(node.name.slice(0, dot));
|
|
95
|
+
return named;
|
|
96
|
+
}
|
|
92
97
|
return this.err(`Unknown type '${node.name}'`, node);
|
|
93
98
|
}
|
|
94
99
|
case "GenericTypeName": {
|
|
@@ -132,6 +137,18 @@ export const resolveMethods = {
|
|
|
132
137
|
{ kind: "TypeName", name: node.name },
|
|
133
138
|
scope,
|
|
134
139
|
);
|
|
140
|
+
case "SelectorExpr":
|
|
141
|
+
// `pkg.T{}` composite literal — the parser keeps the type as a
|
|
142
|
+
// SelectorExpr; resolve it like a qualified TypeName.
|
|
143
|
+
if (node.expr?.kind === "Ident" && typeof node.field === "string") {
|
|
144
|
+
const qualified = `${node.expr.name}.${node.field}`;
|
|
145
|
+
if (this.types.has(qualified))
|
|
146
|
+
return this.resolveTypeNode(
|
|
147
|
+
{ kind: "TypeName", name: qualified },
|
|
148
|
+
scope,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return ANY;
|
|
135
152
|
case "ArrayType":
|
|
136
153
|
return this._resolveArrayTypeNode(node, scope);
|
|
137
154
|
case "FuncType":
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ANY,
|
|
6
6
|
BOOL,
|
|
7
7
|
ERROR,
|
|
8
|
+
FLOAT32,
|
|
8
9
|
FLOAT64,
|
|
9
10
|
INT,
|
|
10
11
|
STRING,
|
|
@@ -256,7 +257,7 @@ export function setupExtendedGlobals(globals, types) {
|
|
|
256
257
|
members: {
|
|
257
258
|
Intn: { kind: "func", params: [INT], returns: [INT] },
|
|
258
259
|
Float64: { kind: "func", params: [], returns: [FLOAT64] },
|
|
259
|
-
Float32: { kind: "func", params: [], returns: [
|
|
260
|
+
Float32: { kind: "func", params: [], returns: [FLOAT32] },
|
|
260
261
|
Int: { kind: "func", params: [], returns: [INT] },
|
|
261
262
|
Int63: { kind: "func", params: [], returns: [INT] },
|
|
262
263
|
Int63n: { kind: "func", params: [INT], returns: [INT] },
|