gofront 0.0.1 → 0.0.3

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.
@@ -1,148 +1,40 @@
1
1
  // Type checker: walks the AST, resolves types for all expressions,
2
2
  // and reports type errors at compile time.
3
3
  //
4
- // Type representation:
5
- // { kind: 'basic', name: 'int'|'float64'|'string'|'bool'|'any'|'void' }
6
- // { kind: 'slice', elem: Type }
7
- // { kind: 'array', size: n, elem: Type }
8
- // { kind: 'map', key: Type, value: Type }
9
- // { kind: 'struct', name: string, fields: Map<string, Type>, methods: Map<string, FuncType> }
10
- // { kind: 'interface', name: string, methods: Map<string, FuncType> }
11
- // { kind: 'func', params: Type[], returns: Type[] }
12
- // { kind: 'tuple', types: Type[] } ← multiple return values
13
- // { kind: 'named', name: string, underlying: Type }
14
-
15
- export class TypeCheckError extends Error {
16
- constructor(msg, node, filename, sourceCode) {
17
- const lineNum = node?.line || node?._line;
18
- const loc = filename
19
- ? lineNum
20
- ? ` in ${filename} at line ${lineNum}`
21
- : ` in ${filename}`
22
- : lineNum
23
- ? ` at line ${lineNum}`
24
- : "";
25
- let lineContext = "";
26
- if (lineNum && sourceCode) {
27
- const lines = sourceCode.split("\n");
28
- const lineStr = lines[lineNum - 1];
29
- if (lineStr !== undefined) {
30
- lineContext = `\n ${lineNum} | ${lineStr}`;
31
- }
32
- }
33
- super(`Type error${loc}: ${msg}${lineContext}`);
34
- }
35
- }
36
-
37
- // ── Built-in types ───────────────────────────────────────────
38
-
39
- const INT = { kind: "basic", name: "int" };
40
- const FLOAT64 = { kind: "basic", name: "float64" };
41
- const STRING = { kind: "basic", name: "string" };
42
- const BOOL = { kind: "basic", name: "bool" };
43
- const ANY = { kind: "basic", name: "any" };
44
- const VOID = { kind: "basic", name: "void" };
45
- const NIL = { kind: "basic", name: "nil" };
46
- const ERROR = { kind: "basic", name: "error" };
47
-
48
- const BASIC_TYPES = {
49
- int: INT,
50
- float64: FLOAT64,
51
- string: STRING,
52
- bool: BOOL,
53
- any: ANY,
54
- byte: INT,
55
- rune: INT,
56
- error: ERROR,
57
- };
58
-
59
- function isNumeric(t) {
60
- return t.kind === "basic" && (t.name === "int" || t.name === "float64");
61
- }
62
- function isString(t) {
63
- return t.kind === "basic" && t.name === "string";
64
- }
65
- function isBool(t) {
66
- return t.kind === "basic" && t.name === "bool";
67
- }
68
- function isAny(t) {
69
- return t.kind === "basic" && t.name === "any";
70
- }
71
- function isNil(t) {
72
- return t.kind === "basic" && t.name === "nil";
73
- }
74
- function isVoid(t) {
75
- return t.kind === "basic" && t.name === "void";
76
- }
77
- function isError(t) {
78
- return t.kind === "basic" && t.name === "error";
79
- }
80
-
81
- export function typeStr(t) {
82
- if (!t) return "void";
83
- if (t.kind === "basic" && t.alias) return t.alias;
84
- switch (t.kind) {
85
- case "basic":
86
- return t.name;
87
- case "slice":
88
- return `[]${typeStr(t.elem)}`;
89
- case "array":
90
- return `[${t.size}]${typeStr(t.elem)}`;
91
- case "map":
92
- return `map[${typeStr(t.key)}]${typeStr(t.value)}`;
93
- case "struct":
94
- return t.name || "struct{...}";
95
- case "interface":
96
- return t.name || "interface{...}";
97
- case "namespace":
98
- return t.name || "namespace{...}";
99
- case "func":
100
- return `func(${t.params.map(typeStr).join(", ")}) ${typeStr(t.returns[0] ?? VOID)}`;
101
- case "tuple":
102
- return `(${t.types.map(typeStr).join(", ")})`;
103
- case "named":
104
- return t.name;
105
- case "pointer":
106
- return `*${typeStr(t.base)}`;
107
- default:
108
- return "?";
109
- }
110
- }
111
-
112
- // ── Scope / environment ──────────────────────────────────────
113
-
114
- class Scope {
115
- constructor(parent = null) {
116
- this.parent = parent;
117
- this.symbols = new Map();
118
- this._consts = new Set(); // names declared as const in this scope
119
- }
120
- define(name, type) {
121
- this.symbols.set(name, type);
122
- }
123
- defineConst(name, type) {
124
- this.symbols.set(name, type);
125
- this._consts.add(name);
126
- }
127
- isConst(name) {
128
- if (this._consts.has(name)) return true;
129
- // Don't walk parent — shadowing a const with a var in a child scope is valid
130
- return false;
131
- }
132
- lookup(name) {
133
- if (this.symbols.has(name)) return this.symbols.get(name);
134
- if (this.parent) return this.parent.lookup(name);
135
- return null;
136
- }
137
- // Lookup which scope owns the name (to check const flag)
138
- lookupScope(name) {
139
- if (this.symbols.has(name)) return this;
140
- if (this.parent) return this.parent.lookupScope(name);
141
- return null;
142
- }
143
- }
144
-
145
- // ── Type checker ─────────────────────────────────────────────
4
+ // Split into sub-modules under typechecker/:
5
+ // types.js — shared type constants, predicates, Scope
6
+ // statements.js — checkBlock, checkStmt
7
+ // expressions.js — checkExpr, checkCall, checkBuiltin, checkCompositeLit
8
+
9
+ import { expressionCheckMethods } from "./typechecker/expressions.js";
10
+ import { statementCheckMethods } from "./typechecker/statements.js";
11
+ import {
12
+ ANY,
13
+ BASIC_TYPES,
14
+ BOOL,
15
+ CMP_OPS,
16
+ defaultType,
17
+ ERROR,
18
+ FLOAT64,
19
+ INT,
20
+ isAny,
21
+ isError,
22
+ isNil,
23
+ isNumeric,
24
+ isString,
25
+ isUntyped,
26
+ LOG_OPS,
27
+ Scope,
28
+ STRING,
29
+ TypeCheckError,
30
+ typeStr,
31
+ UNTYPED_FLOAT,
32
+ UNTYPED_INT,
33
+ VOID,
34
+ } from "./typechecker/types.js";
35
+
36
+ // Re-export for consumers that import from typechecker.js
37
+ export { TypeCheckError, typeStr };
146
38
 
147
39
  export class TypeChecker {
148
40
  constructor() {
@@ -153,6 +45,9 @@ export class TypeChecker {
153
45
  this._currentSource = null;
154
46
  this._loopDepth = 0; // for break/continue validation
155
47
  this._switchDepth = 0; // for break/fallthrough validation
48
+ this._typeSwitchDepth = 0; // for rejecting fallthrough in type switch
49
+ this._deferCount = 0; // tracks defer usage in current function body
50
+ this._imports = []; // tracked imports for unused-import detection
156
51
  this._setupGlobals();
157
52
  }
158
53
 
@@ -188,7 +83,6 @@ export class TypeChecker {
188
83
  "Boolean",
189
84
  "Array",
190
85
  "Object",
191
- "JSON",
192
86
  "parseInt",
193
87
  "parseFloat",
194
88
  "isNaN",
@@ -219,6 +113,8 @@ export class TypeChecker {
219
113
  "FileReader",
220
114
  "ArrayBuffer",
221
115
  "Uint8Array",
116
+ "TextEncoder",
117
+ "TextDecoder",
222
118
  "WebGLRenderingContext",
223
119
  "WebGL2RenderingContext",
224
120
  "GPUDevice",
@@ -246,6 +142,210 @@ export class TypeChecker {
246
142
  },
247
143
  });
248
144
 
145
+ // strings package
146
+ const strFn1 = (ret) => ({
147
+ kind: "func",
148
+ params: [STRING],
149
+ returns: [ret],
150
+ });
151
+ const strFn2 = (p2, ret) => ({
152
+ kind: "func",
153
+ params: [STRING, p2],
154
+ returns: [ret],
155
+ });
156
+ const strFn3 = (p2, p3, ret) => ({
157
+ kind: "func",
158
+ params: [STRING, p2, p3],
159
+ returns: [ret],
160
+ });
161
+ this.globals.define("strings", {
162
+ kind: "namespace",
163
+ name: "strings",
164
+ members: {
165
+ Contains: strFn2(STRING, BOOL),
166
+ HasPrefix: strFn2(STRING, BOOL),
167
+ HasSuffix: strFn2(STRING, BOOL),
168
+ Index: strFn2(STRING, INT),
169
+ LastIndex: strFn2(STRING, INT),
170
+ Count: strFn2(STRING, INT),
171
+ Repeat: strFn2(INT, STRING),
172
+ Replace: {
173
+ kind: "func",
174
+ params: [STRING, STRING, STRING, INT],
175
+ returns: [STRING],
176
+ },
177
+ ReplaceAll: strFn3(STRING, STRING, STRING),
178
+ ToUpper: strFn1(STRING),
179
+ ToLower: strFn1(STRING),
180
+ TrimSpace: strFn1(STRING),
181
+ Trim: strFn2(STRING, STRING),
182
+ TrimPrefix: strFn2(STRING, STRING),
183
+ TrimSuffix: strFn2(STRING, STRING),
184
+ TrimLeft: strFn2(STRING, STRING),
185
+ TrimRight: strFn2(STRING, STRING),
186
+ Split: strFn2(STRING, { kind: "slice", elem: STRING }),
187
+ Join: {
188
+ kind: "func",
189
+ params: [{ kind: "slice", elem: STRING }, STRING],
190
+ returns: [STRING],
191
+ },
192
+ EqualFold: strFn2(STRING, BOOL),
193
+ },
194
+ });
195
+
196
+ // strconv package
197
+ this.globals.define("strconv", {
198
+ kind: "namespace",
199
+ name: "strconv",
200
+ members: {
201
+ Itoa: { kind: "func", params: [INT], returns: [STRING] },
202
+ Atoi: {
203
+ kind: "func",
204
+ params: [STRING],
205
+ returns: [INT, ERROR],
206
+ },
207
+ FormatFloat: {
208
+ kind: "func",
209
+ params: [FLOAT64, INT, INT, INT],
210
+ returns: [STRING],
211
+ },
212
+ FormatBool: { kind: "func", params: [BOOL], returns: [STRING] },
213
+ FormatInt: { kind: "func", params: [INT, INT], returns: [STRING] },
214
+ ParseFloat: {
215
+ kind: "func",
216
+ params: [STRING, INT],
217
+ returns: [FLOAT64, ERROR],
218
+ },
219
+ ParseInt: {
220
+ kind: "func",
221
+ params: [STRING, INT, INT],
222
+ returns: [INT, ERROR],
223
+ },
224
+ ParseBool: {
225
+ kind: "func",
226
+ params: [STRING],
227
+ returns: [BOOL, ERROR],
228
+ },
229
+ },
230
+ });
231
+
232
+ // sort package
233
+ this.globals.define("sort", {
234
+ kind: "namespace",
235
+ name: "sort",
236
+ members: {
237
+ Ints: {
238
+ kind: "func",
239
+ params: [{ kind: "slice", elem: INT }],
240
+ returns: [VOID],
241
+ },
242
+ Float64s: {
243
+ kind: "func",
244
+ params: [{ kind: "slice", elem: FLOAT64 }],
245
+ returns: [VOID],
246
+ },
247
+ Strings: {
248
+ kind: "func",
249
+ params: [{ kind: "slice", elem: STRING }],
250
+ returns: [VOID],
251
+ },
252
+ Slice: {
253
+ kind: "func",
254
+ params: [ANY, ANY],
255
+ returns: [VOID],
256
+ },
257
+ SliceStable: {
258
+ kind: "func",
259
+ params: [ANY, ANY],
260
+ returns: [VOID],
261
+ },
262
+ SliceIsSorted: {
263
+ kind: "func",
264
+ params: [ANY, ANY],
265
+ returns: [BOOL],
266
+ },
267
+ },
268
+ });
269
+
270
+ // math package
271
+ this.globals.define("math", {
272
+ kind: "namespace",
273
+ name: "math",
274
+ members: {
275
+ Abs: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
276
+ Floor: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
277
+ Ceil: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
278
+ Round: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
279
+ Sqrt: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
280
+ Cbrt: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
281
+ Pow: {
282
+ kind: "func",
283
+ params: [FLOAT64, FLOAT64],
284
+ returns: [FLOAT64],
285
+ },
286
+ Log: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
287
+ Log2: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
288
+ Log10: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
289
+ Sin: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
290
+ Cos: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
291
+ Tan: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
292
+ Min: {
293
+ kind: "func",
294
+ params: [FLOAT64, FLOAT64],
295
+ returns: [FLOAT64],
296
+ },
297
+ Max: {
298
+ kind: "func",
299
+ params: [FLOAT64, FLOAT64],
300
+ returns: [FLOAT64],
301
+ },
302
+ Mod: {
303
+ kind: "func",
304
+ params: [FLOAT64, FLOAT64],
305
+ returns: [FLOAT64],
306
+ },
307
+ Inf: { kind: "func", params: [INT], returns: [FLOAT64] },
308
+ IsNaN: { kind: "func", params: [FLOAT64], returns: [BOOL] },
309
+ IsInf: {
310
+ kind: "func",
311
+ params: [FLOAT64, INT],
312
+ returns: [BOOL],
313
+ },
314
+ NaN: { kind: "func", params: [], returns: [FLOAT64] },
315
+ // Constants (typed as func with no params for namespace member access)
316
+ Pi: FLOAT64,
317
+ E: FLOAT64,
318
+ MaxFloat64: FLOAT64,
319
+ SmallestNonzeroFloat64: FLOAT64,
320
+ MaxInt: INT,
321
+ MinInt: INT,
322
+ },
323
+ });
324
+
325
+ // errors package
326
+ this.globals.define("errors", {
327
+ kind: "namespace",
328
+ name: "errors",
329
+ members: {
330
+ New: { kind: "func", params: [STRING], returns: [ERROR] },
331
+ },
332
+ });
333
+
334
+ // time package (partial — JS-friendly subset)
335
+ this.globals.define("time", {
336
+ kind: "namespace",
337
+ name: "time",
338
+ members: {
339
+ Now: { kind: "func", params: [], returns: [ANY] },
340
+ Since: { kind: "func", params: [ANY], returns: [ANY] },
341
+ Sleep: { kind: "func", params: [ANY], returns: [VOID], async: true },
342
+ Millisecond: INT,
343
+ Second: INT,
344
+ Minute: INT,
345
+ Hour: INT,
346
+ },
347
+ });
348
+
249
349
  this.globals.define("append", { kind: "builtin", name: "append" });
250
350
  this.globals.define("len", { kind: "builtin", name: "len" });
251
351
  this.globals.define("cap", { kind: "builtin", name: "cap" });
@@ -254,9 +354,13 @@ export class TypeChecker {
254
354
  this.globals.define("print", { kind: "builtin", name: "print" });
255
355
  this.globals.define("println", { kind: "builtin", name: "println" });
256
356
  this.globals.define("panic", { kind: "builtin", name: "panic" });
357
+ this.globals.define("recover", { kind: "builtin", name: "recover" });
257
358
  this.globals.define("new", { kind: "builtin", name: "new" });
258
359
  this.globals.define("copy", { kind: "builtin", name: "copy" });
259
360
  this.globals.define("error", { kind: "builtin", name: "error" });
361
+ this.globals.define("min", { kind: "builtin", name: "min" });
362
+ this.globals.define("max", { kind: "builtin", name: "max" });
363
+ this.globals.define("clear", { kind: "builtin", name: "clear" });
260
364
  }
261
365
 
262
366
  addDefinitions(types, values) {
@@ -278,6 +382,27 @@ export class TypeChecker {
278
382
  for (const [name, type] of types) this.types.set(name, type);
279
383
  }
280
384
 
385
+ // Register an import for unused-import detection.
386
+ // `name` is the package name or alias used in code.
387
+ // `node` carries _line for error reporting.
388
+ // `filename` / `source` identify the file containing the import.
389
+ trackImport(name, node, filename, source) {
390
+ this._imports.push({ name, node, filename, source });
391
+ }
392
+
393
+ // Report any tracked imports whose package name was never referenced.
394
+ reportUnusedImports() {
395
+ for (const { name, node, filename, source } of this._imports) {
396
+ if (!this.globals._used.has(name)) {
397
+ const saved = [this._currentFile, this._currentSource];
398
+ this._currentFile = filename;
399
+ this._currentSource = source;
400
+ this.err(`'${name}' imported and not used`, node);
401
+ [this._currentFile, this._currentSource] = saved;
402
+ }
403
+ }
404
+ }
405
+
281
406
  // Return a snapshot of all user-defined globals (excludes built-ins).
282
407
  // Used by the compiler to expose a package's exports to importers.
283
408
  getExportedSymbols() {
@@ -305,6 +430,12 @@ export class TypeChecker {
305
430
  return ANY; // recovery type
306
431
  }
307
432
 
433
+ _reportUnused(scope, node) {
434
+ for (const name of scope.unusedLocals()) {
435
+ this.err(`'${name}' declared and not used`, node);
436
+ }
437
+ }
438
+
308
439
  check(program) {
309
440
  this._currentFile = program._filename ?? null;
310
441
  this._currentSource = program._source ?? null;
@@ -412,6 +543,12 @@ export class TypeChecker {
412
543
 
413
544
  collectType(decl) {
414
545
  const underlying = this.resolveTypeNode(decl.type, this.globals);
546
+ if (decl.isAlias) {
547
+ // type A = B — transparent alias, A and B are identical types
548
+ this.types.set(decl.name, underlying);
549
+ this.globals.define(decl.name, underlying);
550
+ return;
551
+ }
415
552
  const named = { kind: "named", name: decl.name, underlying };
416
553
  if (underlying.kind === "struct") {
417
554
  underlying.name = decl.name;
@@ -490,7 +627,12 @@ export class TypeChecker {
490
627
  outer,
491
628
  );
492
629
  if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
630
+ const savedDefer = this._deferCount;
631
+ this._deferCount = 0;
493
632
  this.checkBlock(decl.body, inner, returnType);
633
+ this._reportUnused(inner, decl);
634
+ if (this._deferCount > 0) decl.body._hasDefer = true;
635
+ this._deferCount = savedDefer;
494
636
  decl._returnType = returnType;
495
637
  }
496
638
 
@@ -510,7 +652,12 @@ export class TypeChecker {
510
652
  outer,
511
653
  );
512
654
  if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
655
+ const savedDefer = this._deferCount;
656
+ this._deferCount = 0;
513
657
  this.checkBlock(decl.body, inner, returnType);
658
+ this._reportUnused(inner, decl);
659
+ if (this._deferCount > 0) decl.body._hasDefer = true;
660
+ this._deferCount = savedDefer;
514
661
  decl._returnType = returnType;
515
662
  }
516
663
 
@@ -527,20 +674,21 @@ export class TypeChecker {
527
674
  let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
528
675
  if (spec.value) {
529
676
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
530
- if (!type) type = valTypes[0] ?? ANY;
677
+ if (!type) type = defaultType(valTypes[0]) ?? ANY;
531
678
  for (let i = 0; i < spec.names.length; i++) {
532
679
  const vt = valTypes[i] ?? ANY;
533
680
  this.assertAssignable(type, vt, spec.value[i]);
534
681
  }
535
682
  }
536
683
  if (!type) type = ANY;
537
- for (const name of spec.names) scope.define(name, type);
684
+ for (const name of spec.names) scope.defineLocal(name, type);
538
685
  }
539
686
  }
540
687
 
541
688
  checkConstDecl(decl, scope) {
542
689
  for (const spec of decl.decls) {
543
690
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
691
+ // Explicit type annotation → typed constant; otherwise preserve untyped
544
692
  const type = spec.type
545
693
  ? this.resolveTypeNode(spec.type, scope)
546
694
  : (valTypes[0] ?? ANY);
@@ -550,495 +698,6 @@ export class TypeChecker {
550
698
 
551
699
  // ── Statement checking ───────────────────────────────────────
552
700
 
553
- checkBlock(block, scope, returnType) {
554
- for (const stmt of block.stmts) {
555
- this.checkStmt(stmt, scope, returnType);
556
- }
557
- }
558
-
559
- checkStmt(stmt, scope, returnType) {
560
- switch (stmt.kind) {
561
- case "VarDecl":
562
- this.checkVarDecl(stmt, scope);
563
- break;
564
- case "ConstDecl":
565
- this.checkConstDecl(stmt, scope);
566
- break;
567
- case "TypeDecl":
568
- this.collectType(stmt);
569
- break;
570
-
571
- case "DefineStmt": {
572
- // comma-ok type assertion: n, ok := x.(T)
573
- if (
574
- stmt.lhs.length === 2 &&
575
- stmt.rhs.length === 1 &&
576
- stmt.rhs[0].kind === "TypeAssertExpr"
577
- ) {
578
- stmt.rhs[0]._commaOk = true;
579
- }
580
- const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
581
- // Flatten tuple returns
582
- const rhsFlat =
583
- rhs.length === 1 && rhs[0].kind === "tuple" ? rhs[0].types : rhs;
584
- // comma-ok type assertion returns [assertedType, bool]
585
- if (stmt.rhs[0]?._commaOk) {
586
- const lhsNames = stmt.lhs.map((e) => e.name ?? e);
587
- if (lhsNames[0] !== "_") scope.define(lhsNames[0], rhs[0]);
588
- if (lhsNames[1] !== "_") scope.define(lhsNames[1], BOOL);
589
- stmt._rhsTypes = [rhs[0], BOOL];
590
- break;
591
- }
592
- for (let i = 0; i < stmt.lhs.length; i++) {
593
- const name = stmt.lhs[i].name ?? stmt.lhs[i];
594
- if (name === "_") continue;
595
- // Short variable re-declaration: mark vars that already exist
596
- // in the current (not parent) scope as redeclared.
597
- if (scope.symbols.has(name)) {
598
- stmt.lhs[i]._redecl = true;
599
- }
600
- scope.define(name, rhsFlat[i] ?? ANY);
601
- }
602
- // Go requires at least one new variable in :=
603
- const allRedecl = stmt.lhs.every(
604
- (e) => e._redecl || (e.name ?? e) === "_",
605
- );
606
- if (allRedecl && stmt.lhs.length > 0) {
607
- this.err("no new variables on left side of :=", stmt.lhs[0]);
608
- }
609
- stmt._rhsTypes = rhsFlat;
610
- break;
611
- }
612
-
613
- case "AssignStmt": {
614
- // Check for const reassignment before evaluating RHS
615
- for (const lhs of stmt.lhs) {
616
- if (lhs.kind === "Ident") {
617
- const ownerScope = scope.lookupScope(lhs.name);
618
- if (ownerScope?.isConst(lhs.name))
619
- this.err(`cannot assign to const '${lhs.name}'`, lhs);
620
- }
621
- }
622
- const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
623
- const rhsFlat =
624
- rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
625
- const lhsTypes = stmt.lhs.map((e) => this.checkExpr(e, scope));
626
- for (let i = 0; i < lhsTypes.length; i++) {
627
- const r = rhsFlat[i] ?? ANY;
628
- if (!isAny(lhsTypes[i]) && !isAny(r)) {
629
- this.assertAssignable(lhsTypes[i], r, stmt.lhs[i]);
630
- }
631
- }
632
- break;
633
- }
634
-
635
- case "IncDecStmt":
636
- this.checkExpr(stmt.expr, scope);
637
- break;
638
-
639
- case "ExprStmt":
640
- this.checkExpr(stmt.expr, scope);
641
- break;
642
-
643
- case "ReturnStmt": {
644
- const types = stmt.values.map((v) => this.checkExpr(v, scope));
645
- stmt._types = types;
646
- if (
647
- !isVoid(returnType) &&
648
- types.length === 0 &&
649
- !returnType?._hasNamedReturns
650
- ) {
651
- this.err("Missing return value", stmt);
652
- }
653
- // Check each returned value is assignable to its declared type
654
- if (!isVoid(returnType) && types.length > 0) {
655
- const expected =
656
- returnType.kind === "tuple" ? returnType.types : [returnType];
657
- for (let i = 0; i < expected.length; i++) {
658
- if (types[i])
659
- this.assertAssignable(
660
- expected[i],
661
- types[i],
662
- stmt.values[i] ?? stmt,
663
- );
664
- }
665
- }
666
- break;
667
- }
668
-
669
- case "IfStmt": {
670
- const inner = new Scope(scope);
671
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
672
- const ct = this.checkExpr(stmt.cond, inner);
673
- if (!isBool(ct) && !isAny(ct))
674
- this.err("If condition must be bool", stmt);
675
- this.checkBlock(stmt.body, new Scope(inner), returnType);
676
- if (stmt.elseBody) {
677
- if (stmt.elseBody.kind === "Block")
678
- this.checkBlock(stmt.elseBody, new Scope(inner), returnType);
679
- else this.checkStmt(stmt.elseBody, inner, returnType);
680
- }
681
- break;
682
- }
683
-
684
- case "ForStmt": {
685
- const inner = new Scope(scope);
686
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
687
- if (stmt.cond) {
688
- const ct = this.checkExpr(stmt.cond, inner);
689
- if (!isBool(ct) && !isAny(ct))
690
- this.err("For condition must be bool", stmt);
691
- }
692
- if (stmt.post) this.checkStmt(stmt.post, inner, returnType);
693
- this._loopDepth++;
694
- this.checkBlock(stmt.body, new Scope(inner), returnType);
695
- this._loopDepth--;
696
- break;
697
- }
698
-
699
- case "SwitchStmt": {
700
- const inner = new Scope(scope);
701
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
702
- const _tagType = stmt.tag ? this.checkExpr(stmt.tag, inner) : BOOL;
703
- this._switchDepth++;
704
- for (const c of stmt.cases) {
705
- const caseScope = new Scope(inner);
706
- if (c.list) for (const e of c.list) this.checkExpr(e, caseScope);
707
- for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
708
- }
709
- this._switchDepth--;
710
- break;
711
- }
712
-
713
- case "DeferStmt": {
714
- if (stmt.call.kind !== "CallExpr")
715
- this.err("defer requires a function call", stmt.call);
716
- this.checkExpr(stmt.call, scope);
717
- break;
718
- }
719
-
720
- case "BranchStmt": {
721
- const kw = stmt.keyword;
722
- if (kw === "continue" && this._loopDepth === 0)
723
- this.err("continue statement outside for loop", stmt);
724
- else if (
725
- kw === "break" &&
726
- this._loopDepth === 0 &&
727
- this._switchDepth === 0
728
- )
729
- this.err("break statement outside for loop or switch", stmt);
730
- else if (kw === "fallthrough" && this._switchDepth === 0)
731
- this.err("fallthrough statement outside switch", stmt);
732
- break;
733
- }
734
- case "Block":
735
- this.checkBlock(stmt, new Scope(scope), returnType);
736
- break;
737
- default:
738
- break;
739
- }
740
- }
741
-
742
- // ── Expression checking (returns type) ───────────────────────
743
-
744
- checkExpr(expr, scope) {
745
- const t = this._checkExpr(expr, scope);
746
- expr._type = t;
747
- return t;
748
- }
749
-
750
- _checkExpr(expr, scope) {
751
- switch (expr.kind) {
752
- case "BasicLit": {
753
- switch (expr.litKind) {
754
- case "INT":
755
- return INT;
756
- case "FLOAT":
757
- return FLOAT64;
758
- case "STRING":
759
- return STRING;
760
- case "BOOL":
761
- return BOOL;
762
- case "NIL":
763
- return NIL;
764
- }
765
- break;
766
- }
767
-
768
- case "Ident": {
769
- const t = scope.lookup(expr.name);
770
- if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
771
- return t;
772
- }
773
-
774
- case "UnaryExpr": {
775
- const ot = this.checkExpr(expr.operand, scope);
776
- if (expr.op === "!") {
777
- if (!isBool(ot) && !isAny(ot)) this.err("! requires bool", expr);
778
- return BOOL;
779
- }
780
- if (expr.op === "-" || expr.op === "+") {
781
- if (!isNumeric(ot) && !isAny(ot))
782
- this.err(`${expr.op} requires numeric`, expr);
783
- return ot;
784
- }
785
- return ot;
786
- }
787
-
788
- case "BinaryExpr": {
789
- const lt = this.checkExpr(expr.left, scope);
790
- const rt = this.checkExpr(expr.right, scope);
791
- if (isAny(lt) || isAny(rt)) return this.binaryResultType(expr.op, ANY);
792
- return this.binaryResultType(expr.op, lt, rt, expr);
793
- }
794
-
795
- case "CallExpr": {
796
- return this.checkCall(expr, scope);
797
- }
798
-
799
- case "SelectorExpr": {
800
- const baseType = this.checkExpr(expr.expr, scope);
801
- return this.fieldType(baseType, expr.field, expr);
802
- }
803
-
804
- case "IndexExpr": {
805
- const bt = this.checkExpr(expr.expr, scope);
806
- this.checkExpr(expr.index, scope);
807
- if (isAny(bt)) return ANY;
808
- if (bt.kind === "slice" || bt.kind === "array") return bt.elem;
809
- if (bt.kind === "map") {
810
- expr._mapValueType = bt.value; // for codegen zero-value fallback
811
- return bt.value;
812
- }
813
- if (isString(bt)) return INT; // byte
814
- return this.err(`Cannot index type ${typeStr(bt)}`, expr);
815
- }
816
-
817
- case "SliceExpr": {
818
- const bt = this.checkExpr(expr.expr, scope);
819
- if (expr.low) this.checkExpr(expr.low, scope);
820
- if (expr.high) this.checkExpr(expr.high, scope);
821
- if (isAny(bt)) return ANY;
822
- if (bt.kind === "slice" || bt.kind === "array")
823
- return { kind: "slice", elem: bt.elem };
824
- if (isString(bt)) return STRING;
825
- return this.err(`Cannot slice type ${typeStr(bt)}`, expr);
826
- }
827
-
828
- case "CompositeLit": {
829
- return this.checkCompositeLit(expr, scope);
830
- }
831
-
832
- case "FuncLit": {
833
- const inner = new Scope(scope);
834
- for (const p of expr.params)
835
- inner.define(
836
- p.name,
837
- p.type ? this.resolveTypeNode(p.type, scope) : ANY,
838
- );
839
- const ret = expr.returnType
840
- ? this.resolveTypeNode(expr.returnType, scope)
841
- : VOID;
842
- this.checkBlock(expr.body, inner, ret);
843
- const paramTypes = expr.params.map((p) =>
844
- p.type ? this.resolveTypeNode(p.type, scope) : ANY,
845
- );
846
- return {
847
- kind: "func",
848
- params: paramTypes,
849
- returns: [ret],
850
- async: expr.async,
851
- };
852
- }
853
-
854
- case "AwaitExpr": {
855
- // await unwraps whatever the expression produces — no Promise type modelling
856
- return this.checkExpr(expr.expr, scope);
857
- }
858
-
859
- case "TypeConversion": {
860
- this.checkExpr(expr.expr, scope);
861
- return this.resolveTypeNode(expr.targetType, scope);
862
- }
863
-
864
- case "TypeAssertExpr": {
865
- this.checkExpr(expr.expr, scope);
866
- return this.resolveTypeNode(expr.type, scope);
867
- }
868
-
869
- case "RangeExpr": {
870
- // Type-check the iterated expression so its _type is annotated
871
- return this.checkExpr(expr.expr, scope);
872
- }
873
-
874
- default:
875
- return ANY;
876
- }
877
- return ANY;
878
- }
879
-
880
- checkCall(expr, scope) {
881
- const fnType = this.checkExpr(expr.func, scope);
882
- const argTypes = expr.args.map((a) => this.checkExpr(a, scope));
883
-
884
- // Built-in handling
885
- if (fnType.kind === "builtin") {
886
- return this.checkBuiltin(fnType.name, expr, argTypes, scope);
887
- }
888
-
889
- if (isAny(fnType)) return ANY;
890
-
891
- if (fnType.kind !== "func") {
892
- return this.err(`Cannot call non-function type ${typeStr(fnType)}`, expr);
893
- }
894
-
895
- // Check arg count (allow variadic slack)
896
- const minArgs = fnType.params.filter(
897
- (_, i) => !fnType.variadic || i < fnType.params.length - 1,
898
- ).length;
899
- if (argTypes.length < minArgs) {
900
- this.err(
901
- `Too few arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
902
- expr,
903
- );
904
- }
905
- if (!fnType.variadic && argTypes.length > fnType.params.length) {
906
- this.err(
907
- `Too many arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
908
- expr,
909
- );
910
- }
911
-
912
- // Check each argument is assignable to its parameter type
913
- for (let i = 0; i < fnType.params.length && i < argTypes.length; i++) {
914
- const paramIdx =
915
- fnType.variadic && i >= fnType.params.length - 1
916
- ? fnType.params.length - 1
917
- : i;
918
- this.assertAssignable(fnType.params[paramIdx], argTypes[i], expr.args[i]);
919
- }
920
-
921
- const ret = fnType.returns;
922
- if (!ret || ret.length === 0) return VOID;
923
- if (ret.length === 1) return ret[0];
924
- return { kind: "tuple", types: ret };
925
- }
926
-
927
- checkBuiltin(name, expr, argTypes, scope) {
928
- switch (name) {
929
- case "len":
930
- case "cap":
931
- return INT;
932
- case "append":
933
- return argTypes[0] ?? ANY;
934
- case "copy":
935
- return INT;
936
- case "delete":
937
- return VOID;
938
- case "make": {
939
- if (expr.args.length < 1) return ANY;
940
- const typeArg = expr.args[0];
941
- const typeNode = typeArg.kind === "TypeExpr" ? typeArg.type : typeArg;
942
- return this.resolveTypeNode(typeNode, scope) ?? ANY;
943
- }
944
- case "new": {
945
- if (expr.args.length < 1) return ANY;
946
- const inner = this.resolveTypeNode(expr.args[0], scope);
947
- return { kind: "pointer", base: inner };
948
- }
949
- case "print":
950
- case "println":
951
- case "panic":
952
- return VOID;
953
- case "error":
954
- return ERROR;
955
- default:
956
- return ANY;
957
- }
958
- }
959
-
960
- checkCompositeLit(expr, scope, hintType = null) {
961
- // null typeExpr = implicit lit inside a slice/map: {X:1} in []Point{{X:1}}
962
- const t =
963
- expr.typeExpr === null
964
- ? (hintType ?? ANY)
965
- : this.resolveTypeNode(expr.typeExpr, scope);
966
- const base = t.kind === "named" ? t.underlying : t;
967
-
968
- if (base?.kind === "struct") {
969
- for (const elem of expr.elems) {
970
- if (elem.kind === "KeyValueExpr") {
971
- const keyName = elem.key.name;
972
- const fieldType = base.fields?.get(keyName);
973
- if (fieldType) {
974
- const vt = this.checkExpr(elem.value, scope);
975
- this.assertAssignable(fieldType, vt, elem.value);
976
- } else {
977
- // Check if it's an embedded type name (e.g. Dog{Animal: Animal{...}})
978
- const embed = base._embeds?.find(
979
- (e) => (e.kind === "named" ? e.name : null) === keyName,
980
- );
981
- if (embed) {
982
- const vt = this.checkExpr(elem.value, scope);
983
- this.assertAssignable(embed, vt, elem.value);
984
- elem._isEmbedInit = true;
985
- } else {
986
- this.err(`Unknown field '${keyName}'`, elem.key);
987
- }
988
- }
989
- } else {
990
- this.checkExpr(elem, scope);
991
- }
992
- }
993
- return t;
994
- }
995
-
996
- if (base?.kind === "slice") {
997
- for (const elem of expr.elems) {
998
- if (elem.kind === "CompositeLit" && elem.typeExpr === null) {
999
- const et = this.checkCompositeLit(elem, scope, base.elem);
1000
- elem._type = et;
1001
- } else if (elem.kind !== "KeyValueExpr") {
1002
- const et = this.checkExpr(elem, scope);
1003
- this.assertAssignable(base.elem, et, elem);
1004
- }
1005
- }
1006
- return t;
1007
- }
1008
-
1009
- if (base?.kind === "map") {
1010
- for (const elem of expr.elems) {
1011
- if (elem.kind === "KeyValueExpr") {
1012
- const kt = this.checkExpr(elem.key, scope);
1013
- // value may be an implicit composite lit
1014
- let vt;
1015
- if (
1016
- elem.value.kind === "CompositeLit" &&
1017
- elem.value.typeExpr === null
1018
- ) {
1019
- vt = this.checkCompositeLit(elem.value, scope, base.value);
1020
- elem.value._type = vt;
1021
- } else {
1022
- vt = this.checkExpr(elem.value, scope);
1023
- }
1024
- this.assertAssignable(base.key, kt, elem.key);
1025
- this.assertAssignable(base.value, vt, elem.value);
1026
- }
1027
- }
1028
- return t;
1029
- }
1030
-
1031
- // Unknown type context — still check elements
1032
- for (const elem of expr.elems) {
1033
- if (elem.kind === "KeyValueExpr") {
1034
- this.checkExpr(elem.value, scope);
1035
- } else {
1036
- this.checkExpr(elem, scope);
1037
- }
1038
- }
1039
- return t ?? ANY;
1040
- }
1041
-
1042
701
  // ── Type resolution ──────────────────────────────────────────
1043
702
 
1044
703
  resolveTypeNode(node, scope) {
@@ -1058,7 +717,12 @@ export class TypeChecker {
1058
717
  case "ArrayType":
1059
718
  return {
1060
719
  kind: "array",
1061
- size: node.size,
720
+ // inferLen: true means [...]T — size is determined by composite literal
721
+ size: node.inferLen
722
+ ? null
723
+ : node.size?.value !== undefined
724
+ ? Number(node.size.value)
725
+ : node.size,
1062
726
  elem: this.resolveTypeNode(node.elem, scope),
1063
727
  };
1064
728
  case "MapType":
@@ -1114,7 +778,29 @@ export class TypeChecker {
1114
778
  const returns = m.returnType
1115
779
  ? [this.resolveTypeNode(m.returnType, scope)]
1116
780
  : [VOID];
1117
- methods.set(m.name, { kind: "func", params, returns });
781
+ const isVariadic =
782
+ m.params.length > 0 && m.params[m.params.length - 1].variadic;
783
+ const mType = { kind: "func", params, returns };
784
+ if (isVariadic) mType.variadic = true;
785
+ methods.set(m.name, mType);
786
+ }
787
+ // Flatten embedded interface methods
788
+ if (node.embeds) {
789
+ for (const embed of node.embeds) {
790
+ const resolved = this.resolveTypeNode(embed, scope);
791
+ const base =
792
+ resolved?.kind === "named" ? resolved.underlying : resolved;
793
+ if (base?.kind === "interface") {
794
+ for (const [mName, mType] of base.methods) {
795
+ if (!methods.has(mName)) methods.set(mName, mType);
796
+ }
797
+ } else if (!isAny(resolved)) {
798
+ this.err(
799
+ `cannot embed non-interface type ${typeStr(resolved)}`,
800
+ embed,
801
+ );
802
+ }
803
+ }
1118
804
  }
1119
805
  return { kind: "interface", methods };
1120
806
  }
@@ -1189,16 +875,30 @@ export class TypeChecker {
1189
875
  }
1190
876
 
1191
877
  binaryResultType(op, lt, rt, node) {
1192
- const cmp = ["==", "!=", "<", ">", "<=", ">="];
1193
- const log = ["&&", "||"];
1194
- if (cmp.includes(op)) return BOOL;
1195
- if (log.includes(op)) return BOOL;
878
+ if (CMP_OPS.has(op)) return BOOL;
879
+ if (LOG_OPS.has(op)) return BOOL;
1196
880
  if (isAny(lt) || isAny(rt)) return ANY;
1197
881
  if (isNumeric(lt) && isNumeric(rt)) {
1198
- // float64 is contagious
1199
- return lt.name === "float64" || rt.name === "float64" ? FLOAT64 : INT;
882
+ const lFloat = (lt.kind === "untyped" ? lt.base : lt.name) === "float64";
883
+ const rFloat = (rt.kind === "untyped" ? rt.base : rt.name) === "float64";
884
+ const isFloat = lFloat || rFloat;
885
+ // Both untyped → result is untyped
886
+ if (lt.kind === "untyped" && rt.kind === "untyped")
887
+ return isFloat ? UNTYPED_FLOAT : UNTYPED_INT;
888
+ // One typed, one untyped → typed wins
889
+ if (lt.kind === "untyped")
890
+ return isFloat && rt.name !== "float64" ? FLOAT64 : rt;
891
+ if (rt.kind === "untyped")
892
+ return isFloat && lt.name !== "float64" ? FLOAT64 : lt;
893
+ // Both typed
894
+ return isFloat ? FLOAT64 : INT;
895
+ }
896
+ if (isString(lt) && isString(rt) && op === "+") {
897
+ if (lt.kind === "untyped" && rt.kind === "untyped") return lt;
898
+ if (lt.kind === "untyped") return rt;
899
+ if (rt.kind === "untyped") return lt;
900
+ return STRING;
1200
901
  }
1201
- if (isString(lt) && isString(rt) && op === "+") return STRING;
1202
902
  if (node)
1203
903
  this.err(`Invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`, node);
1204
904
  return ANY;
@@ -1211,6 +911,29 @@ export class TypeChecker {
1211
911
  if (isAny(target) || isAny(source)) return;
1212
912
  if (isNil(source)) return; // nil is assignable to anything nullable
1213
913
 
914
+ // Untyped constants coerce to any compatible typed target
915
+ if (isUntyped(source)) {
916
+ if (isUntyped(target)) return; // untyped → untyped always OK
917
+ if (target.kind === "basic") {
918
+ // untyped int → int or float64
919
+ if (
920
+ source.base === "int" &&
921
+ (target.name === "int" || target.name === "float64")
922
+ )
923
+ return;
924
+ // untyped float → float64 (and int for compatibility)
925
+ if (
926
+ source.base === "float64" &&
927
+ (target.name === "float64" || target.name === "int")
928
+ )
929
+ return;
930
+ // untyped string → string
931
+ if (source.base === "string" && target.name === "string") return;
932
+ // untyped bool → bool
933
+ if (source.base === "bool" && target.name === "bool") return;
934
+ }
935
+ }
936
+
1214
937
  // Go untyped constant promotion: int literals are assignable to float64
1215
938
  if (target.kind === "basic" && source.kind === "basic") {
1216
939
  if (target.name === "float64" && source.name === "int") return;
@@ -1242,11 +965,31 @@ export class TypeChecker {
1242
965
  for (const [name, required] of iface.methods) {
1243
966
  const actual = base.methods?.get(name);
1244
967
  if (!actual) return false;
1245
- // Check return type matches (params are not strictly checked — Go allows covariance)
1246
- const reqRet = required.returns?.[0];
1247
- const actRet = actual.returns?.[0];
1248
- if (reqRet && actRet && typeStr(reqRet) !== typeStr(actRet)) return false;
968
+
969
+ // Check parameter count matches
970
+ const reqParams = required.params ?? [];
971
+ const actParams = actual.params ?? [];
972
+ if (reqParams.length !== actParams.length) return false;
973
+
974
+ // Check each parameter type matches
975
+ for (let i = 0; i < reqParams.length; i++) {
976
+ if (typeStr(reqParams[i]) !== typeStr(actParams[i])) return false;
977
+ }
978
+
979
+ // Check variadic flag matches
980
+ if (!!required.variadic !== !!actual.variadic) return false;
981
+
982
+ // Check return types match (count and each type)
983
+ const reqRets = required.returns ?? [];
984
+ const actRets = actual.returns ?? [];
985
+ if (reqRets.length !== actRets.length) return false;
986
+ for (let i = 0; i < reqRets.length; i++) {
987
+ if (typeStr(reqRets[i]) !== typeStr(actRets[i])) return false;
988
+ }
1249
989
  }
1250
990
  return true;
1251
991
  }
1252
992
  }
993
+
994
+ Object.assign(TypeChecker.prototype, statementCheckMethods);
995
+ Object.assign(TypeChecker.prototype, expressionCheckMethods);