gofront 1.3.6 → 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 +9 -6
- package/package.json +1 -1
- package/src/cli-core.js +86 -0
- package/src/codegen/expressions.js +27 -7
- package/src/codegen/index.js +12 -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/index.js +4 -1
- package/src/typechecker/resolve.js +18 -1
- package/src/typechecker/stdlib/extended.js +2 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ identifier, literal, `)`, `]`, `}`, or certain keywords (`return`, `break`, `con
|
|
|
79
79
|
`fallthrough`). This is why Go doesn't need explicit semicolons — and neither does
|
|
80
80
|
GoFront.
|
|
81
81
|
|
|
82
|
-
### 2. Parser (`src/parser
|
|
82
|
+
### 2. Parser (`src/parser/`)
|
|
83
83
|
|
|
84
84
|
A hand-written recursive-descent parser. No parser generators, no grammar files — just
|
|
85
85
|
straightforward top-down parsing. Produces an AST where every node is a plain JS object
|
|
@@ -88,7 +88,7 @@ with a `kind` field (`"FuncDecl"`, `"IfStmt"`, `"BinaryExpr"`, etc.).
|
|
|
88
88
|
Operator precedence is handled via a Pratt-style expression parser with numeric
|
|
89
89
|
precedence levels.
|
|
90
90
|
|
|
91
|
-
### 3. Type Checker (`src/typechecker
|
|
91
|
+
### 3. Type Checker (`src/typechecker/`)
|
|
92
92
|
|
|
93
93
|
Three-pass type checker operating on the AST:
|
|
94
94
|
|
|
@@ -103,7 +103,7 @@ Types are plain JS objects (`{ kind: "basic", name: "int" }`, `{ kind: "slice",
|
|
|
103
103
|
elem: ... }`, etc.). The special `any` type acts as a recovery/escape hatch — any
|
|
104
104
|
operation on it is silently permitted, preventing cascading errors.
|
|
105
105
|
|
|
106
|
-
### 4. Code Generator (`src/codegen
|
|
106
|
+
### 4. Code Generator (`src/codegen/`)
|
|
107
107
|
|
|
108
108
|
Walks the typed AST and emits clean, readable JavaScript. No intermediate representation
|
|
109
109
|
— the codegen writes directly to an output buffer with indentation tracking.
|
|
@@ -123,10 +123,11 @@ readable and debuggable — no name mangling, no opaque wrappers.
|
|
|
123
123
|
|
|
124
124
|
| Go | JavaScript | Notes |
|
|
125
125
|
|---|---|---|
|
|
126
|
-
| `struct` | ES6 `class` |
|
|
127
|
-
| Methods | Class instance methods | Receiver is `this` |
|
|
126
|
+
| `struct` | ES6 `class` | Positional constructor with zero-value defaults: `new Point(1, 2)`; a generated `__clone()` gives struct values Go copy semantics on assignment, call, return, `append`, and `range` |
|
|
127
|
+
| Methods | Class instance methods | Receiver is `this` (typed `*T` for pointer receivers) |
|
|
128
128
|
| Embedded structs | Flattened fields + delegation stubs | `Greet(...a) { return Base.prototype.Greet.call(this, ...a); }` |
|
|
129
|
-
| `[]T` (slice) | `Array` | `append` →
|
|
129
|
+
| `[]T` (slice) | `Array` | `append` → `__append`, `len` → `__len` |
|
|
130
|
+
| `[]float32`, `[]int8`…`[]int32`, `[]uint8`/`[]byte`…`[]uint32`, `[]rune` | TypedArray (`Float32Array`, `Int32Array`, `Uint8Array`, …) | `[]float64` and `[]int` stay plain `Array`; slicing → `.subarray` |
|
|
130
131
|
| `map[K]V` | Plain object `{}` | Key access via `[]`, iteration via `Object.entries()` |
|
|
131
132
|
| `nil` | `null` | |
|
|
132
133
|
| `error` | `__error` object | `error("msg")` → `__error("msg")`, `.Error()` → real method call. `toString()` for string context compat |
|
|
@@ -386,6 +387,8 @@ gofront <file.go> compile single file → stdout
|
|
|
386
387
|
gofront <dir> compile all *.go in directory → stdout
|
|
387
388
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
388
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>
|
|
389
392
|
gofront <input> --watch watch for changes and recompile
|
|
390
393
|
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
391
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;
|
|
@@ -319,6 +321,15 @@ export class CodeGen {
|
|
|
319
321
|
if (d.kind === "TypeDecl" && d.type.kind === "StructType")
|
|
320
322
|
this.structNames.add(d.name);
|
|
321
323
|
}
|
|
324
|
+
// Structs from bundled GoFront packages are inlined under their bare name.
|
|
325
|
+
// Skip aliases (key differs from the type's own name) — no class is emitted for them.
|
|
326
|
+
if (this.checker) {
|
|
327
|
+
for (const [name, t] of this.checker.types) {
|
|
328
|
+
if (name.includes(".") || t?.name !== name) continue;
|
|
329
|
+
if (t.kind === "named" && t.underlying?.kind === "struct")
|
|
330
|
+
this.structNames.add(name);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
_collectMethodMap(program) {
|
|
@@ -368,6 +379,7 @@ export class CodeGen {
|
|
|
368
379
|
[this._usesError, HELPER_ERROR],
|
|
369
380
|
[this._usesErrorIs, HELPER_ERROR_IS],
|
|
370
381
|
[this._usesPathClean, HELPER_PATH_CLEAN],
|
|
382
|
+
[this._usesSortSlice, HELPER_SORT_SLICE],
|
|
371
383
|
[this._usesTimeFmt, HELPER_TIME_FMT],
|
|
372
384
|
[this._usesTimeParse, HELPER_TIME_PARSE],
|
|
373
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);
|
package/src/typechecker/index.js
CHANGED
|
@@ -505,7 +505,10 @@ export class TypeChecker {
|
|
|
505
505
|
const recvNamedType = this.types.get(recvTypeName);
|
|
506
506
|
this._injectGenericReceiverTypeParams(inner, recvNamedType);
|
|
507
507
|
const recvType = this.resolveTypeNodeName(decl.recvType, outer);
|
|
508
|
-
inner.define(
|
|
508
|
+
inner.define(
|
|
509
|
+
decl.recvName,
|
|
510
|
+
decl.recvPointer ? { kind: "pointer", base: recvType } : recvType,
|
|
511
|
+
);
|
|
509
512
|
for (const p of decl.params) {
|
|
510
513
|
inner.define(p.name, this.resolveTypeNode(p.type, inner));
|
|
511
514
|
}
|
|
@@ -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] },
|