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 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.js`)
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.js`)
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.js`)
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` | Single destructured-object constructor: `new Point({ X: 1, Y: 2 })` |
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` → spread, `len` → `.length` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
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 val = this.genExpr(expr.expr);
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 val; // can't check at runtime — pass through
618
- return `(${check} ? ${val} : (() => { throw new Error("interface conversion: type assertion failed"); })())`;
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
- return `${this.getTypeName(typeNode.expr)}.${typeNode.field}`;
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
  },
@@ -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],
@@ -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
- const tmp = "__ta";
257
- this.line(`let ${tmp} = ${val};`);
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
- const tmp = "__t";
277
- this.line(`let ${tmp} = ${rhsStr};`);
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
- `${a[0]}.sort((a, b) => ${a[1]}(a, b) ? -1 : ${a[1]}(b, a) ? 1 : 0)`,
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
- `${a[0]}.every((v, i, a) => i === 0 || ${a[1]}(a[i - 1], v))`,
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
- const dep = compileDir(depDir);
134
- preambles.push(dep.js);
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] Type-check only
60
- gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
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
- console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
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}`);
@@ -352,6 +352,7 @@ export const expressionParserMethods = {
352
352
  if (
353
353
  tt === T.MAP ||
354
354
  tt === T.IDENT ||
355
+ tt === T.DOT || // qualified types: []pkg.Type{...}
355
356
  tt === T.STAR ||
356
357
  tt === T.INT ||
357
358
  tt === T.FLOAT ||
@@ -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);
@@ -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(decl.recvName, recvType);
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) return 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: [FLOAT64] },
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] },