gofront 0.0.2 → 0.0.4

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,164 +1,41 @@
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
- // ── Static operator sets (module-level for reuse) ────────────
38
- const CMP_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
39
- const LOG_OPS = new Set(["&&", "||"]);
40
-
41
- // ── Built-in types ───────────────────────────────────────────
42
-
43
- const INT = { kind: "basic", name: "int" };
44
- const FLOAT64 = { kind: "basic", name: "float64" };
45
- const STRING = { kind: "basic", name: "string" };
46
- const BOOL = { kind: "basic", name: "bool" };
47
- const ANY = { kind: "basic", name: "any" };
48
- const VOID = { kind: "basic", name: "void" };
49
- const NIL = { kind: "basic", name: "nil" };
50
- const ERROR = { kind: "basic", name: "error" };
51
-
52
- const BASIC_TYPES = {
53
- int: INT,
54
- float64: FLOAT64,
55
- string: STRING,
56
- bool: BOOL,
57
- any: ANY,
58
- byte: INT,
59
- rune: INT,
60
- error: ERROR,
61
- // Sized integer / float aliases — all map to int or float64 at runtime
62
- uint: INT,
63
- int8: INT,
64
- int16: INT,
65
- int32: INT,
66
- int64: INT,
67
- uint8: INT,
68
- uint16: INT,
69
- uint32: INT,
70
- uint64: INT,
71
- uintptr: INT,
72
- float32: FLOAT64,
73
- };
74
-
75
- function isNumeric(t) {
76
- return t.kind === "basic" && (t.name === "int" || t.name === "float64");
77
- }
78
- function isString(t) {
79
- return t.kind === "basic" && t.name === "string";
80
- }
81
- function isBool(t) {
82
- return t.kind === "basic" && t.name === "bool";
83
- }
84
- function isAny(t) {
85
- return t.kind === "basic" && t.name === "any";
86
- }
87
- function isNil(t) {
88
- return t.kind === "basic" && t.name === "nil";
89
- }
90
- function isVoid(t) {
91
- return t.kind === "basic" && t.name === "void";
92
- }
93
- function isError(t) {
94
- return t.kind === "basic" && t.name === "error";
95
- }
96
-
97
- export function typeStr(t) {
98
- if (!t) return "void";
99
- if (t.kind === "basic" && t.alias) return t.alias;
100
- switch (t.kind) {
101
- case "basic":
102
- return t.name;
103
- case "slice":
104
- return `[]${typeStr(t.elem)}`;
105
- case "array":
106
- return `[${t.size}]${typeStr(t.elem)}`;
107
- case "map":
108
- return `map[${typeStr(t.key)}]${typeStr(t.value)}`;
109
- case "struct":
110
- return t.name || "struct{...}";
111
- case "interface":
112
- return t.name || "interface{...}";
113
- case "namespace":
114
- return t.name || "namespace{...}";
115
- case "func":
116
- return `func(${t.params.map(typeStr).join(", ")}) ${typeStr(t.returns[0] ?? VOID)}`;
117
- case "tuple":
118
- return `(${t.types.map(typeStr).join(", ")})`;
119
- case "named":
120
- return t.name;
121
- case "pointer":
122
- return `*${typeStr(t.base)}`;
123
- default:
124
- return "?";
125
- }
126
- }
127
-
128
- // ── Scope / environment ──────────────────────────────────────
129
-
130
- class Scope {
131
- constructor(parent = null) {
132
- this.parent = parent;
133
- this.symbols = new Map();
134
- this._consts = new Set(); // names declared as const in this scope
135
- }
136
- define(name, type) {
137
- this.symbols.set(name, type);
138
- }
139
- defineConst(name, type) {
140
- this.symbols.set(name, type);
141
- this._consts.add(name);
142
- }
143
- isConst(name) {
144
- if (this._consts.has(name)) return true;
145
- // Don't walk parent — shadowing a const with a var in a child scope is valid
146
- return false;
147
- }
148
- lookup(name) {
149
- if (this.symbols.has(name)) return this.symbols.get(name);
150
- if (this.parent) return this.parent.lookup(name);
151
- return null;
152
- }
153
- // Lookup which scope owns the name (to check const flag)
154
- lookupScope(name) {
155
- if (this.symbols.has(name)) return this;
156
- if (this.parent) return this.parent.lookupScope(name);
157
- return null;
158
- }
159
- }
160
-
161
- // ── 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
+ isVoid,
27
+ LOG_OPS,
28
+ Scope,
29
+ STRING,
30
+ TypeCheckError,
31
+ typeStr,
32
+ UNTYPED_FLOAT,
33
+ UNTYPED_INT,
34
+ VOID,
35
+ } from "./typechecker/types.js";
36
+
37
+ // Re-export for consumers that import from typechecker.js
38
+ export { TypeCheckError, typeStr };
162
39
 
163
40
  export class TypeChecker {
164
41
  constructor() {
@@ -169,6 +46,9 @@ export class TypeChecker {
169
46
  this._currentSource = null;
170
47
  this._loopDepth = 0; // for break/continue validation
171
48
  this._switchDepth = 0; // for break/fallthrough validation
49
+ this._typeSwitchDepth = 0; // for rejecting fallthrough in type switch
50
+ this._deferCount = 0; // tracks defer usage in current function body
51
+ this._imports = []; // tracked imports for unused-import detection
172
52
  this._setupGlobals();
173
53
  }
174
54
 
@@ -263,6 +143,245 @@ export class TypeChecker {
263
143
  },
264
144
  });
265
145
 
146
+ // strings package
147
+ const strFn1 = (ret) => ({
148
+ kind: "func",
149
+ params: [STRING],
150
+ returns: [ret],
151
+ });
152
+ const strFn2 = (p2, ret) => ({
153
+ kind: "func",
154
+ params: [STRING, p2],
155
+ returns: [ret],
156
+ });
157
+ const strFn3 = (p2, p3, ret) => ({
158
+ kind: "func",
159
+ params: [STRING, p2, p3],
160
+ returns: [ret],
161
+ });
162
+ this.globals.define("strings", {
163
+ kind: "namespace",
164
+ name: "strings",
165
+ members: {
166
+ Contains: strFn2(STRING, BOOL),
167
+ HasPrefix: strFn2(STRING, BOOL),
168
+ HasSuffix: strFn2(STRING, BOOL),
169
+ Index: strFn2(STRING, INT),
170
+ LastIndex: strFn2(STRING, INT),
171
+ Count: strFn2(STRING, INT),
172
+ Repeat: strFn2(INT, STRING),
173
+ Replace: {
174
+ kind: "func",
175
+ params: [STRING, STRING, STRING, INT],
176
+ returns: [STRING],
177
+ },
178
+ ReplaceAll: strFn3(STRING, STRING, STRING),
179
+ ToUpper: strFn1(STRING),
180
+ ToLower: strFn1(STRING),
181
+ TrimSpace: strFn1(STRING),
182
+ Trim: strFn2(STRING, STRING),
183
+ TrimPrefix: strFn2(STRING, STRING),
184
+ TrimSuffix: strFn2(STRING, STRING),
185
+ TrimLeft: strFn2(STRING, STRING),
186
+ TrimRight: strFn2(STRING, STRING),
187
+ Split: strFn2(STRING, { kind: "slice", elem: STRING }),
188
+ Join: {
189
+ kind: "func",
190
+ params: [{ kind: "slice", elem: STRING }, STRING],
191
+ returns: [STRING],
192
+ },
193
+ EqualFold: strFn2(STRING, BOOL),
194
+ },
195
+ });
196
+
197
+ // strconv package
198
+ this.globals.define("strconv", {
199
+ kind: "namespace",
200
+ name: "strconv",
201
+ members: {
202
+ Itoa: { kind: "func", params: [INT], returns: [STRING] },
203
+ Atoi: {
204
+ kind: "func",
205
+ params: [STRING],
206
+ returns: [INT, ERROR],
207
+ },
208
+ FormatFloat: {
209
+ kind: "func",
210
+ params: [FLOAT64, INT, INT, INT],
211
+ returns: [STRING],
212
+ },
213
+ FormatBool: { kind: "func", params: [BOOL], returns: [STRING] },
214
+ FormatInt: { kind: "func", params: [INT, INT], returns: [STRING] },
215
+ ParseFloat: {
216
+ kind: "func",
217
+ params: [STRING, INT],
218
+ returns: [FLOAT64, ERROR],
219
+ },
220
+ ParseInt: {
221
+ kind: "func",
222
+ params: [STRING, INT, INT],
223
+ returns: [INT, ERROR],
224
+ },
225
+ ParseBool: {
226
+ kind: "func",
227
+ params: [STRING],
228
+ returns: [BOOL, ERROR],
229
+ },
230
+ },
231
+ });
232
+
233
+ // sort package
234
+ this.globals.define("sort", {
235
+ kind: "namespace",
236
+ name: "sort",
237
+ members: {
238
+ Ints: {
239
+ kind: "func",
240
+ params: [{ kind: "slice", elem: INT }],
241
+ returns: [VOID],
242
+ },
243
+ Float64s: {
244
+ kind: "func",
245
+ params: [{ kind: "slice", elem: FLOAT64 }],
246
+ returns: [VOID],
247
+ },
248
+ Strings: {
249
+ kind: "func",
250
+ params: [{ kind: "slice", elem: STRING }],
251
+ returns: [VOID],
252
+ },
253
+ Slice: {
254
+ kind: "func",
255
+ params: [ANY, ANY],
256
+ returns: [VOID],
257
+ },
258
+ SliceStable: {
259
+ kind: "func",
260
+ params: [ANY, ANY],
261
+ returns: [VOID],
262
+ },
263
+ SliceIsSorted: {
264
+ kind: "func",
265
+ params: [ANY, ANY],
266
+ returns: [BOOL],
267
+ },
268
+ },
269
+ });
270
+
271
+ // math package
272
+ this.globals.define("math", {
273
+ kind: "namespace",
274
+ name: "math",
275
+ members: {
276
+ Abs: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
277
+ Floor: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
278
+ Ceil: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
279
+ Round: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
280
+ Sqrt: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
281
+ Cbrt: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
282
+ Pow: {
283
+ kind: "func",
284
+ params: [FLOAT64, FLOAT64],
285
+ returns: [FLOAT64],
286
+ },
287
+ Log: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
288
+ Log2: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
289
+ Log10: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
290
+ Sin: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
291
+ Cos: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
292
+ Tan: { kind: "func", params: [FLOAT64], returns: [FLOAT64] },
293
+ Min: {
294
+ kind: "func",
295
+ params: [FLOAT64, FLOAT64],
296
+ returns: [FLOAT64],
297
+ },
298
+ Max: {
299
+ kind: "func",
300
+ params: [FLOAT64, FLOAT64],
301
+ returns: [FLOAT64],
302
+ },
303
+ Mod: {
304
+ kind: "func",
305
+ params: [FLOAT64, FLOAT64],
306
+ returns: [FLOAT64],
307
+ },
308
+ Inf: { kind: "func", params: [INT], returns: [FLOAT64] },
309
+ IsNaN: { kind: "func", params: [FLOAT64], returns: [BOOL] },
310
+ IsInf: {
311
+ kind: "func",
312
+ params: [FLOAT64, INT],
313
+ returns: [BOOL],
314
+ },
315
+ NaN: { kind: "func", params: [], returns: [FLOAT64] },
316
+ // Constants (typed as func with no params for namespace member access)
317
+ Pi: FLOAT64,
318
+ E: FLOAT64,
319
+ MaxFloat64: FLOAT64,
320
+ SmallestNonzeroFloat64: FLOAT64,
321
+ MaxInt: INT,
322
+ MinInt: INT,
323
+ },
324
+ });
325
+
326
+ // errors package
327
+ this.globals.define("errors", {
328
+ kind: "namespace",
329
+ name: "errors",
330
+ members: {
331
+ New: { kind: "func", params: [STRING], returns: [ERROR] },
332
+ },
333
+ });
334
+
335
+ // time package (partial — JS-friendly subset)
336
+ this.globals.define("time", {
337
+ kind: "namespace",
338
+ name: "time",
339
+ members: {
340
+ Now: { kind: "func", params: [], returns: [ANY] },
341
+ Since: { kind: "func", params: [ANY], returns: [ANY] },
342
+ Sleep: { kind: "func", params: [ANY], returns: [VOID], async: true },
343
+ Millisecond: INT,
344
+ Second: INT,
345
+ Minute: INT,
346
+ Hour: INT,
347
+ },
348
+ });
349
+
350
+ // unicode package
351
+ const runeToStr = { kind: "func", params: [INT], returns: [BOOL] };
352
+ const runeTrans = { kind: "func", params: [INT], returns: [INT] };
353
+ this.globals.define("unicode", {
354
+ kind: "namespace",
355
+ name: "unicode",
356
+ members: {
357
+ IsLetter: runeToStr,
358
+ IsDigit: runeToStr,
359
+ IsSpace: runeToStr,
360
+ IsUpper: runeToStr,
361
+ IsLower: runeToStr,
362
+ IsPunct: runeToStr,
363
+ IsControl: runeToStr,
364
+ IsPrint: runeToStr,
365
+ IsGraphic: runeToStr,
366
+ ToUpper: runeTrans,
367
+ ToLower: runeTrans,
368
+ },
369
+ });
370
+
371
+ // os package (JS-friendly subset)
372
+ this.globals.define("os", {
373
+ kind: "namespace",
374
+ name: "os",
375
+ members: {
376
+ Exit: { kind: "func", params: [INT], returns: [VOID] },
377
+ Args: { kind: "slice", elem: STRING },
378
+ Getenv: { kind: "func", params: [STRING], returns: [STRING] },
379
+ Stdout: ANY,
380
+ Stderr: ANY,
381
+ Stdin: ANY,
382
+ },
383
+ });
384
+
266
385
  this.globals.define("append", { kind: "builtin", name: "append" });
267
386
  this.globals.define("len", { kind: "builtin", name: "len" });
268
387
  this.globals.define("cap", { kind: "builtin", name: "cap" });
@@ -295,10 +414,37 @@ export class TypeChecker {
295
414
  addPackageNamespace(pkgName, symbols, types) {
296
415
  const members = {};
297
416
  for (const [name, type] of symbols) members[name] = type;
298
- this.globals.define(pkgName, { kind: "namespace", name: pkgName, members });
417
+ // _gofront: true marks this as a GoFront package — exported identifier rules apply
418
+ this.globals.define(pkgName, {
419
+ kind: "namespace",
420
+ name: pkgName,
421
+ members,
422
+ _gofront: true,
423
+ });
299
424
  for (const [name, type] of types) this.types.set(name, type);
300
425
  }
301
426
 
427
+ // Register an import for unused-import detection.
428
+ // `name` is the package name or alias used in code.
429
+ // `node` carries _line for error reporting.
430
+ // `filename` / `source` identify the file containing the import.
431
+ trackImport(name, node, filename, source) {
432
+ this._imports.push({ name, node, filename, source });
433
+ }
434
+
435
+ // Report any tracked imports whose package name was never referenced.
436
+ reportUnusedImports() {
437
+ for (const { name, node, filename, source } of this._imports) {
438
+ if (!this.globals._used.has(name)) {
439
+ const saved = [this._currentFile, this._currentSource];
440
+ this._currentFile = filename;
441
+ this._currentSource = source;
442
+ this.err(`'${name}' imported and not used`, node);
443
+ [this._currentFile, this._currentSource] = saved;
444
+ }
445
+ }
446
+ }
447
+
302
448
  // Return a snapshot of all user-defined globals (excludes built-ins).
303
449
  // Used by the compiler to expose a package's exports to importers.
304
450
  getExportedSymbols() {
@@ -326,6 +472,12 @@ export class TypeChecker {
326
472
  return ANY; // recovery type
327
473
  }
328
474
 
475
+ _reportUnused(scope, node) {
476
+ for (const name of scope.unusedLocals()) {
477
+ this.err(`'${name}' declared and not used`, node);
478
+ }
479
+ }
480
+
329
481
  check(program) {
330
482
  this._currentFile = program._filename ?? null;
331
483
  this._currentSource = program._source ?? null;
@@ -508,7 +660,7 @@ export class TypeChecker {
508
660
  for (const p of decl.params) {
509
661
  inner.define(p.name, this.resolveTypeNode(p.type, outer));
510
662
  }
511
- const returnType = decl.returnType
663
+ let returnType = decl.returnType
512
664
  ? this.resolveTypeNode(decl.returnType, outer)
513
665
  : VOID;
514
666
  const hasNamedReturns = this._injectNamedReturns(
@@ -516,9 +668,22 @@ export class TypeChecker {
516
668
  inner,
517
669
  outer,
518
670
  );
519
- if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
671
+ // Wrap the return type to avoid mutating the shared singleton
672
+ if (hasNamedReturns && returnType)
673
+ returnType = { ...returnType, _hasNamedReturns: true };
674
+ const savedDefer = this._deferCount;
675
+ this._deferCount = 0;
520
676
  this.checkBlock(decl.body, inner, returnType);
677
+ this._reportUnused(inner, decl);
678
+ if (this._deferCount > 0) decl.body._hasDefer = true;
679
+ this._deferCount = savedDefer;
521
680
  decl._returnType = returnType;
681
+ // Missing return check: non-void functions must terminate on all paths
682
+ if (!isVoid(returnType) && !returnType._hasNamedReturns) {
683
+ if (!this._isTerminating(decl.body)) {
684
+ this.err(`missing return at end of function '${decl.name}'`, decl);
685
+ }
686
+ }
522
687
  }
523
688
 
524
689
  checkMethodDecl(decl, outer) {
@@ -528,7 +693,7 @@ export class TypeChecker {
528
693
  for (const p of decl.params) {
529
694
  inner.define(p.name, this.resolveTypeNode(p.type, outer));
530
695
  }
531
- const returnType = decl.returnType
696
+ let returnType = decl.returnType
532
697
  ? this.resolveTypeNode(decl.returnType, outer)
533
698
  : VOID;
534
699
  const hasNamedReturns = this._injectNamedReturns(
@@ -536,9 +701,101 @@ export class TypeChecker {
536
701
  inner,
537
702
  outer,
538
703
  );
539
- if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
704
+ if (hasNamedReturns && returnType)
705
+ returnType = { ...returnType, _hasNamedReturns: true };
706
+ const savedDefer = this._deferCount;
707
+ this._deferCount = 0;
540
708
  this.checkBlock(decl.body, inner, returnType);
709
+ this._reportUnused(inner, decl);
710
+ if (this._deferCount > 0) decl.body._hasDefer = true;
711
+ this._deferCount = savedDefer;
541
712
  decl._returnType = returnType;
713
+ // Missing return check
714
+ if (!isVoid(returnType) && !returnType._hasNamedReturns) {
715
+ if (!this._isTerminating(decl.body)) {
716
+ this.err(`missing return at end of method '${decl.name}'`, decl);
717
+ }
718
+ }
719
+ }
720
+
721
+ // Go spec §Terminating statements — returns true if the statement always terminates.
722
+ _isTerminating(stmt) {
723
+ if (!stmt) return false;
724
+ switch (stmt.kind) {
725
+ case "ReturnStmt":
726
+ return true;
727
+ case "BranchStmt":
728
+ // panic() is in ExprStmt, not BranchStmt — but "goto" could be here
729
+ return false;
730
+ case "ExprStmt":
731
+ // panic(...) is a terminating call
732
+ if (
733
+ stmt.expr?.kind === "CallExpr" &&
734
+ stmt.expr?.func?.kind === "Ident" &&
735
+ stmt.expr?.func?.name === "panic"
736
+ )
737
+ return true;
738
+ return false;
739
+ case "Block":
740
+ return this._isTerminatingBlock(stmt);
741
+ case "IfStmt":
742
+ // Terminating if: has else, and both branches terminate
743
+ if (!stmt.elseBody) return false;
744
+ return (
745
+ this._isTerminatingBlock(stmt.body) &&
746
+ (stmt.elseBody.kind === "Block"
747
+ ? this._isTerminatingBlock(stmt.elseBody)
748
+ : this._isTerminating(stmt.elseBody))
749
+ );
750
+ case "ForStmt":
751
+ // Infinite loop (no condition) with no break — terminating
752
+ if (!stmt.cond && !this._blockHasBreak(stmt.body)) return true;
753
+ return false;
754
+ case "SwitchStmt":
755
+ // Terminating switch: has default and every case terminates
756
+ return this._isTerminatingSwitch(stmt);
757
+ case "TypeSwitchStmt":
758
+ return this._isTerminatingTypeSwitch(stmt);
759
+ case "LabeledStmt":
760
+ return this._isTerminating(stmt.body);
761
+ default:
762
+ return false;
763
+ }
764
+ }
765
+
766
+ _isTerminatingBlock(block) {
767
+ if (!block?.stmts?.length) return false;
768
+ return this._isTerminating(block.stmts[block.stmts.length - 1]);
769
+ }
770
+
771
+ _blockHasBreak(block) {
772
+ if (!block?.stmts) return false;
773
+ for (const s of block.stmts) {
774
+ if (s.kind === "BranchStmt" && s.keyword === "break") return true;
775
+ }
776
+ return false;
777
+ }
778
+
779
+ _isTerminatingSwitch(stmt) {
780
+ let hasDefault = false;
781
+ for (const c of stmt.cases ?? []) {
782
+ if (!c.list) hasDefault = true;
783
+ if (!c.stmts?.length) return false;
784
+ const last = c.stmts[c.stmts.length - 1];
785
+ if (!this._isTerminating(last)) return false;
786
+ }
787
+ return hasDefault;
788
+ }
789
+
790
+ _isTerminatingTypeSwitch(stmt) {
791
+ let hasDefault = false;
792
+ for (const c of stmt.cases ?? []) {
793
+ if (!c.types) hasDefault = true;
794
+ if (!c.stmts?.length) return false;
795
+ const last = c.stmts[c.stmts.length - 1];
796
+ if (!this._isTerminating(last)) return false;
797
+ }
798
+ return hasDefault;
542
799
  }
543
800
 
544
801
  _injectNamedReturns(returnTypeNode, scope, outer) {
@@ -554,20 +811,21 @@ export class TypeChecker {
554
811
  let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
555
812
  if (spec.value) {
556
813
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
557
- if (!type) type = valTypes[0] ?? ANY;
814
+ if (!type) type = defaultType(valTypes[0]) ?? ANY;
558
815
  for (let i = 0; i < spec.names.length; i++) {
559
816
  const vt = valTypes[i] ?? ANY;
560
817
  this.assertAssignable(type, vt, spec.value[i]);
561
818
  }
562
819
  }
563
820
  if (!type) type = ANY;
564
- for (const name of spec.names) scope.define(name, type);
821
+ for (const name of spec.names) scope.defineLocal(name, type);
565
822
  }
566
823
  }
567
824
 
568
825
  checkConstDecl(decl, scope) {
569
826
  for (const spec of decl.decls) {
570
827
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
828
+ // Explicit type annotation → typed constant; otherwise preserve untyped
571
829
  const type = spec.type
572
830
  ? this.resolveTypeNode(spec.type, scope)
573
831
  : (valTypes[0] ?? ANY);
@@ -577,541 +835,6 @@ export class TypeChecker {
577
835
 
578
836
  // ── Statement checking ───────────────────────────────────────
579
837
 
580
- checkBlock(block, scope, returnType) {
581
- for (const stmt of block.stmts) {
582
- this.checkStmt(stmt, scope, returnType);
583
- }
584
- }
585
-
586
- checkStmt(stmt, scope, returnType) {
587
- switch (stmt.kind) {
588
- case "VarDecl":
589
- this.checkVarDecl(stmt, scope);
590
- break;
591
- case "ConstDecl":
592
- this.checkConstDecl(stmt, scope);
593
- break;
594
- case "TypeDecl":
595
- this.collectType(stmt);
596
- break;
597
-
598
- case "DefineStmt": {
599
- // comma-ok type assertion: n, ok := x.(T)
600
- if (
601
- stmt.lhs.length === 2 &&
602
- stmt.rhs.length === 1 &&
603
- stmt.rhs[0].kind === "TypeAssertExpr"
604
- ) {
605
- stmt.rhs[0]._commaOk = true;
606
- }
607
- const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
608
- // Flatten tuple returns
609
- const rhsFlat =
610
- rhs.length === 1 && rhs[0].kind === "tuple" ? rhs[0].types : rhs;
611
- // comma-ok type assertion returns [assertedType, bool]
612
- if (stmt.rhs[0]?._commaOk) {
613
- const lhsNames = stmt.lhs.map((e) => e.name ?? e);
614
- if (lhsNames[0] !== "_") scope.define(lhsNames[0], rhs[0]);
615
- if (lhsNames[1] !== "_") scope.define(lhsNames[1], BOOL);
616
- stmt._rhsTypes = [rhs[0], BOOL];
617
- break;
618
- }
619
- for (let i = 0; i < stmt.lhs.length; i++) {
620
- const name = stmt.lhs[i].name ?? stmt.lhs[i];
621
- if (name === "_") continue;
622
- // Short variable re-declaration: mark vars that already exist
623
- // in the current (not parent) scope as redeclared.
624
- if (scope.symbols.has(name)) {
625
- stmt.lhs[i]._redecl = true;
626
- }
627
- scope.define(name, rhsFlat[i] ?? ANY);
628
- }
629
- // Go requires at least one new variable in :=
630
- const allRedecl = stmt.lhs.every(
631
- (e) => e._redecl || (e.name ?? e) === "_",
632
- );
633
- if (allRedecl && stmt.lhs.length > 0) {
634
- this.err("no new variables on left side of :=", stmt.lhs[0]);
635
- }
636
- stmt._rhsTypes = rhsFlat;
637
- break;
638
- }
639
-
640
- case "AssignStmt": {
641
- // Check for const reassignment before evaluating RHS
642
- for (const lhs of stmt.lhs) {
643
- if (lhs.kind === "Ident") {
644
- const ownerScope = scope.lookupScope(lhs.name);
645
- if (ownerScope?.isConst(lhs.name))
646
- this.err(`cannot assign to const '${lhs.name}'`, lhs);
647
- }
648
- }
649
- const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
650
- const rhsFlat =
651
- rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
652
- const lhsTypes = stmt.lhs.map((e) => this.checkExpr(e, scope));
653
- for (let i = 0; i < lhsTypes.length; i++) {
654
- const r = rhsFlat[i] ?? ANY;
655
- if (!isAny(lhsTypes[i]) && !isAny(r)) {
656
- this.assertAssignable(lhsTypes[i], r, stmt.lhs[i]);
657
- }
658
- }
659
- break;
660
- }
661
-
662
- case "IncDecStmt":
663
- this.checkExpr(stmt.expr, scope);
664
- break;
665
-
666
- case "ExprStmt":
667
- this.checkExpr(stmt.expr, scope);
668
- break;
669
-
670
- case "ReturnStmt": {
671
- const types = stmt.values.map((v) => this.checkExpr(v, scope));
672
- stmt._types = types;
673
- if (
674
- !isVoid(returnType) &&
675
- types.length === 0 &&
676
- !returnType?._hasNamedReturns
677
- ) {
678
- this.err("Missing return value", stmt);
679
- }
680
- // Check each returned value is assignable to its declared type
681
- if (!isVoid(returnType) && types.length > 0) {
682
- const expected =
683
- returnType.kind === "tuple" ? returnType.types : [returnType];
684
- for (let i = 0; i < expected.length; i++) {
685
- if (types[i])
686
- this.assertAssignable(
687
- expected[i],
688
- types[i],
689
- stmt.values[i] ?? stmt,
690
- );
691
- }
692
- }
693
- break;
694
- }
695
-
696
- case "IfStmt": {
697
- const inner = new Scope(scope);
698
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
699
- const ct = this.checkExpr(stmt.cond, inner);
700
- if (!isBool(ct) && !isAny(ct))
701
- this.err("If condition must be bool", stmt);
702
- this.checkBlock(stmt.body, new Scope(inner), returnType);
703
- if (stmt.elseBody) {
704
- if (stmt.elseBody.kind === "Block")
705
- this.checkBlock(stmt.elseBody, new Scope(inner), returnType);
706
- else this.checkStmt(stmt.elseBody, inner, returnType);
707
- }
708
- break;
709
- }
710
-
711
- case "ForStmt": {
712
- const inner = new Scope(scope);
713
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
714
- if (stmt.cond) {
715
- // for range N { } — RangeExpr as condition is valid (Go 1.22 int range)
716
- if (stmt.cond.kind !== "RangeExpr") {
717
- const ct = this.checkExpr(stmt.cond, inner);
718
- if (!isBool(ct) && !isAny(ct))
719
- this.err("For condition must be bool", stmt);
720
- } else {
721
- this.checkExpr(stmt.cond.expr, inner);
722
- }
723
- }
724
- if (stmt.post) this.checkStmt(stmt.post, inner, returnType);
725
- this._loopDepth++;
726
- this.checkBlock(stmt.body, new Scope(inner), returnType);
727
- this._loopDepth--;
728
- break;
729
- }
730
-
731
- case "SwitchStmt": {
732
- const inner = new Scope(scope);
733
- if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
734
- if (stmt.tag) this.checkExpr(stmt.tag, inner);
735
- this._switchDepth++;
736
- for (const c of stmt.cases) {
737
- const caseScope = new Scope(inner);
738
- if (c.list) for (const e of c.list) this.checkExpr(e, caseScope);
739
- for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
740
- }
741
- this._switchDepth--;
742
- break;
743
- }
744
-
745
- case "TypeSwitchStmt": {
746
- const inner = new Scope(scope);
747
- this.checkExpr(stmt.expr, inner);
748
- this._switchDepth++;
749
- for (const c of stmt.cases) {
750
- const caseScope = new Scope(inner);
751
- if (stmt.assign) {
752
- // Bind the variable to the single case type, or any for multi/default
753
- const bindType =
754
- c.types?.length === 1
755
- ? this.resolveTypeNode(c.types[0], inner)
756
- : ANY;
757
- caseScope.define(stmt.assign, bindType);
758
- }
759
- for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
760
- }
761
- this._switchDepth--;
762
- break;
763
- }
764
-
765
- case "DeferStmt": {
766
- if (stmt.call.kind !== "CallExpr")
767
- this.err("defer requires a function call", stmt.call);
768
- this.checkExpr(stmt.call, scope);
769
- break;
770
- }
771
-
772
- case "LabeledStmt":
773
- this.checkStmt(stmt.body, scope, returnType);
774
- break;
775
-
776
- case "BranchStmt": {
777
- const kw = stmt.keyword;
778
- if (stmt.label) break; // labeled branch — target checked by JS
779
- if (kw === "continue" && this._loopDepth === 0)
780
- this.err("continue statement outside for loop", stmt);
781
- else if (
782
- kw === "break" &&
783
- this._loopDepth === 0 &&
784
- this._switchDepth === 0
785
- )
786
- this.err("break statement outside for loop or switch", stmt);
787
- else if (kw === "fallthrough" && this._switchDepth === 0)
788
- this.err("fallthrough statement outside switch", stmt);
789
- break;
790
- }
791
- case "Block":
792
- this.checkBlock(stmt, new Scope(scope), returnType);
793
- break;
794
- default:
795
- break;
796
- }
797
- }
798
-
799
- // ── Expression checking (returns type) ───────────────────────
800
-
801
- checkExpr(expr, scope) {
802
- const t = this._checkExpr(expr, scope);
803
- expr._type = t;
804
- return t;
805
- }
806
-
807
- _checkExpr(expr, scope) {
808
- switch (expr.kind) {
809
- case "BasicLit": {
810
- switch (expr.litKind) {
811
- case "INT":
812
- return INT;
813
- case "FLOAT":
814
- return FLOAT64;
815
- case "STRING":
816
- return STRING;
817
- case "BOOL":
818
- return BOOL;
819
- case "NIL":
820
- return NIL;
821
- }
822
- break;
823
- }
824
-
825
- case "Ident": {
826
- const t = scope.lookup(expr.name);
827
- if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
828
- return t;
829
- }
830
-
831
- case "UnaryExpr": {
832
- const ot = this.checkExpr(expr.operand, scope);
833
- if (expr.op === "!") {
834
- if (!isBool(ot) && !isAny(ot)) this.err("! requires bool", expr);
835
- return BOOL;
836
- }
837
- if (expr.op === "-" || expr.op === "+") {
838
- if (!isNumeric(ot) && !isAny(ot))
839
- this.err(`${expr.op} requires numeric`, expr);
840
- return ot;
841
- }
842
- return ot;
843
- }
844
-
845
- case "BinaryExpr": {
846
- const lt = this.checkExpr(expr.left, scope);
847
- const rt = this.checkExpr(expr.right, scope);
848
- if (isAny(lt) || isAny(rt)) return this.binaryResultType(expr.op, ANY);
849
- return this.binaryResultType(expr.op, lt, rt, expr);
850
- }
851
-
852
- case "CallExpr": {
853
- return this.checkCall(expr, scope);
854
- }
855
-
856
- case "SelectorExpr": {
857
- const baseType = this.checkExpr(expr.expr, scope);
858
- return this.fieldType(baseType, expr.field, expr);
859
- }
860
-
861
- case "IndexExpr": {
862
- const bt = this.checkExpr(expr.expr, scope);
863
- this.checkExpr(expr.index, scope);
864
- if (isAny(bt)) return ANY;
865
- if (bt.kind === "slice" || bt.kind === "array") return bt.elem;
866
- if (bt.kind === "map") {
867
- expr._mapValueType = bt.value; // for codegen zero-value fallback
868
- return bt.value;
869
- }
870
- if (isString(bt)) return INT; // byte
871
- return this.err(`Cannot index type ${typeStr(bt)}`, expr);
872
- }
873
-
874
- case "SliceExpr": {
875
- const bt = this.checkExpr(expr.expr, scope);
876
- if (expr.low) this.checkExpr(expr.low, scope);
877
- if (expr.high) this.checkExpr(expr.high, scope);
878
- if (isAny(bt)) return ANY;
879
- if (bt.kind === "slice" || bt.kind === "array")
880
- return { kind: "slice", elem: bt.elem };
881
- if (isString(bt)) return STRING;
882
- return this.err(`Cannot slice type ${typeStr(bt)}`, expr);
883
- }
884
-
885
- case "CompositeLit": {
886
- return this.checkCompositeLit(expr, scope);
887
- }
888
-
889
- case "FuncLit": {
890
- const inner = new Scope(scope);
891
- for (const p of expr.params)
892
- inner.define(
893
- p.name,
894
- p.type ? this.resolveTypeNode(p.type, scope) : ANY,
895
- );
896
- const ret = expr.returnType
897
- ? this.resolveTypeNode(expr.returnType, scope)
898
- : VOID;
899
- this.checkBlock(expr.body, inner, ret);
900
- const paramTypes = expr.params.map((p) =>
901
- p.type ? this.resolveTypeNode(p.type, scope) : ANY,
902
- );
903
- return {
904
- kind: "func",
905
- params: paramTypes,
906
- returns: [ret],
907
- async: expr.async,
908
- };
909
- }
910
-
911
- case "AwaitExpr": {
912
- // await unwraps whatever the expression produces — no Promise type modelling
913
- return this.checkExpr(expr.expr, scope);
914
- }
915
-
916
- case "TypeConversion": {
917
- this.checkExpr(expr.expr, scope);
918
- return this.resolveTypeNode(expr.targetType, scope);
919
- }
920
-
921
- case "TypeAssertExpr": {
922
- this.checkExpr(expr.expr, scope);
923
- return this.resolveTypeNode(expr.type, scope);
924
- }
925
-
926
- case "RangeExpr": {
927
- // Type-check the iterated expression so its _type is annotated
928
- return this.checkExpr(expr.expr, scope);
929
- }
930
-
931
- default:
932
- return ANY;
933
- }
934
- return ANY;
935
- }
936
-
937
- checkCall(expr, scope) {
938
- const fnType = this.checkExpr(expr.func, scope);
939
-
940
- // Handle built-ins before evaluating args; some (e.g. new) take type names as
941
- // arguments, which would otherwise produce false "Undefined" errors.
942
- if (fnType.kind === "builtin") {
943
- const argTypes = expr.args.map((a, i) => {
944
- // new(T) — first arg is a type name, not a value expression
945
- if (fnType.name === "new" && i === 0) return ANY;
946
- return this.checkExpr(a, scope);
947
- });
948
- return this.checkBuiltin(fnType.name, expr, argTypes, scope);
949
- }
950
-
951
- const argTypes = expr.args.map((a) => this.checkExpr(a, scope));
952
-
953
- if (isAny(fnType)) return ANY;
954
-
955
- if (fnType.kind !== "func") {
956
- return this.err(`Cannot call non-function type ${typeStr(fnType)}`, expr);
957
- }
958
-
959
- // Check arg count (allow variadic slack)
960
- const minArgs = fnType.params.filter(
961
- (_, i) => !fnType.variadic || i < fnType.params.length - 1,
962
- ).length;
963
- if (argTypes.length < minArgs) {
964
- this.err(
965
- `Too few arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
966
- expr,
967
- );
968
- }
969
- if (!fnType.variadic && argTypes.length > fnType.params.length) {
970
- this.err(
971
- `Too many arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
972
- expr,
973
- );
974
- }
975
-
976
- // Check each argument is assignable to its parameter type
977
- for (let i = 0; i < fnType.params.length && i < argTypes.length; i++) {
978
- const paramIdx =
979
- fnType.variadic && i >= fnType.params.length - 1
980
- ? fnType.params.length - 1
981
- : i;
982
- // Spread arg (f(slice...)) passes a whole slice into a variadic — skip per-element check
983
- if (expr.args[i]?._spread) continue;
984
- this.assertAssignable(fnType.params[paramIdx], argTypes[i], expr.args[i]);
985
- }
986
-
987
- const ret = fnType.returns;
988
- if (!ret || ret.length === 0) return VOID;
989
- if (ret.length === 1) return ret[0];
990
- return { kind: "tuple", types: ret };
991
- }
992
-
993
- checkBuiltin(name, expr, argTypes, scope) {
994
- switch (name) {
995
- case "len":
996
- case "cap":
997
- return INT;
998
- case "append":
999
- return argTypes[0] ?? ANY;
1000
- case "copy":
1001
- return INT;
1002
- case "delete":
1003
- return VOID;
1004
- case "make": {
1005
- if (expr.args.length < 1) return ANY;
1006
- const typeArg = expr.args[0];
1007
- const typeNode = typeArg.kind === "TypeExpr" ? typeArg.type : typeArg;
1008
- return this.resolveTypeNode(typeNode, scope) ?? ANY;
1009
- }
1010
- case "new": {
1011
- if (expr.args.length < 1) return ANY;
1012
- const inner = this.resolveTypeNode(expr.args[0], scope);
1013
- return { kind: "pointer", base: inner };
1014
- }
1015
- case "print":
1016
- case "println":
1017
- case "panic":
1018
- return VOID;
1019
- case "recover":
1020
- return ANY;
1021
- case "error":
1022
- return ERROR;
1023
- case "min":
1024
- case "max":
1025
- return argTypes[0] ?? ANY;
1026
- case "clear":
1027
- return VOID;
1028
- default:
1029
- return ANY;
1030
- }
1031
- }
1032
-
1033
- checkCompositeLit(expr, scope, hintType = null) {
1034
- // null typeExpr = implicit lit inside a slice/map: {X:1} in []Point{{X:1}}
1035
- const t =
1036
- expr.typeExpr === null
1037
- ? (hintType ?? ANY)
1038
- : this.resolveTypeNode(expr.typeExpr, scope);
1039
- const base = t.kind === "named" ? t.underlying : t;
1040
-
1041
- if (base?.kind === "struct") {
1042
- for (const elem of expr.elems) {
1043
- if (elem.kind === "KeyValueExpr") {
1044
- const keyName = elem.key.name;
1045
- const fieldType = base.fields?.get(keyName);
1046
- if (fieldType) {
1047
- const vt = this.checkExpr(elem.value, scope);
1048
- this.assertAssignable(fieldType, vt, elem.value);
1049
- } else {
1050
- // Check if it's an embedded type name (e.g. Dog{Animal: Animal{...}})
1051
- const embed = base._embeds?.find(
1052
- (e) => (e.kind === "named" ? e.name : null) === keyName,
1053
- );
1054
- if (embed) {
1055
- const vt = this.checkExpr(elem.value, scope);
1056
- this.assertAssignable(embed, vt, elem.value);
1057
- elem._isEmbedInit = true;
1058
- } else {
1059
- this.err(`Unknown field '${keyName}'`, elem.key);
1060
- }
1061
- }
1062
- } else {
1063
- this.checkExpr(elem, scope);
1064
- }
1065
- }
1066
- return t;
1067
- }
1068
-
1069
- if (base?.kind === "slice" || base?.kind === "array") {
1070
- for (const elem of expr.elems) {
1071
- if (elem.kind === "CompositeLit" && elem.typeExpr === null) {
1072
- const et = this.checkCompositeLit(elem, scope, base.elem);
1073
- elem._type = et;
1074
- } else if (elem.kind !== "KeyValueExpr") {
1075
- const et = this.checkExpr(elem, scope);
1076
- this.assertAssignable(base.elem, et, elem);
1077
- }
1078
- }
1079
- return t;
1080
- }
1081
-
1082
- if (base?.kind === "map") {
1083
- for (const elem of expr.elems) {
1084
- if (elem.kind === "KeyValueExpr") {
1085
- const kt = this.checkExpr(elem.key, scope);
1086
- // value may be an implicit composite lit
1087
- let vt;
1088
- if (
1089
- elem.value.kind === "CompositeLit" &&
1090
- elem.value.typeExpr === null
1091
- ) {
1092
- vt = this.checkCompositeLit(elem.value, scope, base.value);
1093
- elem.value._type = vt;
1094
- } else {
1095
- vt = this.checkExpr(elem.value, scope);
1096
- }
1097
- this.assertAssignable(base.key, kt, elem.key);
1098
- this.assertAssignable(base.value, vt, elem.value);
1099
- }
1100
- }
1101
- return t;
1102
- }
1103
-
1104
- // Unknown type context — still check elements
1105
- for (const elem of expr.elems) {
1106
- if (elem.kind === "KeyValueExpr") {
1107
- this.checkExpr(elem.value, scope);
1108
- } else {
1109
- this.checkExpr(elem, scope);
1110
- }
1111
- }
1112
- return t ?? ANY;
1113
- }
1114
-
1115
838
  // ── Type resolution ──────────────────────────────────────────
1116
839
 
1117
840
  resolveTypeNode(node, scope) {
@@ -1192,7 +915,11 @@ export class TypeChecker {
1192
915
  const returns = m.returnType
1193
916
  ? [this.resolveTypeNode(m.returnType, scope)]
1194
917
  : [VOID];
1195
- methods.set(m.name, { kind: "func", params, returns });
918
+ const isVariadic =
919
+ m.params.length > 0 && m.params[m.params.length - 1].variadic;
920
+ const mType = { kind: "func", params, returns };
921
+ if (isVariadic) mType.variadic = true;
922
+ methods.set(m.name, mType);
1196
923
  }
1197
924
  // Flatten embedded interface methods
1198
925
  if (node.embeds) {
@@ -1262,6 +989,18 @@ export class TypeChecker {
1262
989
  return this.err(`No field '${field}' on ${typeStr(baseType)}`, node);
1263
990
  }
1264
991
  if (base?.kind === "namespace") {
992
+ // Enforce exported identifier rule for GoFront packages only
993
+ if (
994
+ base._gofront &&
995
+ field.length > 0 &&
996
+ field[0] >= "a" &&
997
+ field[0] <= "z"
998
+ ) {
999
+ return this.err(
1000
+ `cannot refer to unexported name ${base.name}.${field}`,
1001
+ node,
1002
+ );
1003
+ }
1265
1004
  if (field in base.members) return base.members[field];
1266
1005
  return this.err(`No member '${field}' in namespace ${base.name}`, node);
1267
1006
  }
@@ -1285,14 +1024,49 @@ export class TypeChecker {
1285
1024
  }
1286
1025
 
1287
1026
  binaryResultType(op, lt, rt, node) {
1288
- if (CMP_OPS.has(op)) return BOOL;
1027
+ if (CMP_OPS.has(op)) {
1028
+ // Reject slice/map/func comparisons unless one side is nil (Go spec)
1029
+ if (op === "==" || op === "!=") {
1030
+ const lNil = isNil(lt);
1031
+ const rNil = isNil(rt);
1032
+ if (!lNil && !rNil) {
1033
+ for (const t of [lt, rt]) {
1034
+ const base = t?.kind === "named" ? t.underlying : t;
1035
+ if (
1036
+ base?.kind === "slice" ||
1037
+ base?.kind === "map" ||
1038
+ base?.kind === "func"
1039
+ ) {
1040
+ this.err(`operator ${op} not defined on ${typeStr(t)}`, node);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ return BOOL;
1046
+ }
1289
1047
  if (LOG_OPS.has(op)) return BOOL;
1290
1048
  if (isAny(lt) || isAny(rt)) return ANY;
1291
1049
  if (isNumeric(lt) && isNumeric(rt)) {
1292
- // float64 is contagious
1293
- return lt.name === "float64" || rt.name === "float64" ? FLOAT64 : INT;
1050
+ const lFloat = (lt.kind === "untyped" ? lt.base : lt.name) === "float64";
1051
+ const rFloat = (rt.kind === "untyped" ? rt.base : rt.name) === "float64";
1052
+ const isFloat = lFloat || rFloat;
1053
+ // Both untyped → result is untyped
1054
+ if (lt.kind === "untyped" && rt.kind === "untyped")
1055
+ return isFloat ? UNTYPED_FLOAT : UNTYPED_INT;
1056
+ // One typed, one untyped → typed wins
1057
+ if (lt.kind === "untyped")
1058
+ return isFloat && rt.name !== "float64" ? FLOAT64 : rt;
1059
+ if (rt.kind === "untyped")
1060
+ return isFloat && lt.name !== "float64" ? FLOAT64 : lt;
1061
+ // Both typed
1062
+ return isFloat ? FLOAT64 : INT;
1063
+ }
1064
+ if (isString(lt) && isString(rt) && op === "+") {
1065
+ if (lt.kind === "untyped" && rt.kind === "untyped") return lt;
1066
+ if (lt.kind === "untyped") return rt;
1067
+ if (rt.kind === "untyped") return lt;
1068
+ return STRING;
1294
1069
  }
1295
- if (isString(lt) && isString(rt) && op === "+") return STRING;
1296
1070
  if (node)
1297
1071
  this.err(`Invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`, node);
1298
1072
  return ANY;
@@ -1305,6 +1079,29 @@ export class TypeChecker {
1305
1079
  if (isAny(target) || isAny(source)) return;
1306
1080
  if (isNil(source)) return; // nil is assignable to anything nullable
1307
1081
 
1082
+ // Untyped constants coerce to any compatible typed target
1083
+ if (isUntyped(source)) {
1084
+ if (isUntyped(target)) return; // untyped → untyped always OK
1085
+ if (target.kind === "basic") {
1086
+ // untyped int → int or float64
1087
+ if (
1088
+ source.base === "int" &&
1089
+ (target.name === "int" || target.name === "float64")
1090
+ )
1091
+ return;
1092
+ // untyped float → float64 (and int for compatibility)
1093
+ if (
1094
+ source.base === "float64" &&
1095
+ (target.name === "float64" || target.name === "int")
1096
+ )
1097
+ return;
1098
+ // untyped string → string
1099
+ if (source.base === "string" && target.name === "string") return;
1100
+ // untyped bool → bool
1101
+ if (source.base === "bool" && target.name === "bool") return;
1102
+ }
1103
+ }
1104
+
1308
1105
  // Go untyped constant promotion: int literals are assignable to float64
1309
1106
  if (target.kind === "basic" && source.kind === "basic") {
1310
1107
  if (target.name === "float64" && source.name === "int") return;
@@ -1317,6 +1114,8 @@ export class TypeChecker {
1317
1114
  tBase = this.resolveType(tBase);
1318
1115
  sBase = this.resolveType(sBase);
1319
1116
  if (tBase?.kind === "interface") {
1117
+ // Empty interface (interface{}) accepts any type
1118
+ if (tBase.methods.size === 0) return;
1320
1119
  if (!this.implements(source, tBase, node)) {
1321
1120
  this.err(
1322
1121
  `${typeStr(source)} does not implement ${typeStr(target)}`,
@@ -1336,11 +1135,31 @@ export class TypeChecker {
1336
1135
  for (const [name, required] of iface.methods) {
1337
1136
  const actual = base.methods?.get(name);
1338
1137
  if (!actual) return false;
1339
- // Check return type matches (params are not strictly checked — Go allows covariance)
1340
- const reqRet = required.returns?.[0];
1341
- const actRet = actual.returns?.[0];
1342
- if (reqRet && actRet && typeStr(reqRet) !== typeStr(actRet)) return false;
1138
+
1139
+ // Check parameter count matches
1140
+ const reqParams = required.params ?? [];
1141
+ const actParams = actual.params ?? [];
1142
+ if (reqParams.length !== actParams.length) return false;
1143
+
1144
+ // Check each parameter type matches
1145
+ for (let i = 0; i < reqParams.length; i++) {
1146
+ if (typeStr(reqParams[i]) !== typeStr(actParams[i])) return false;
1147
+ }
1148
+
1149
+ // Check variadic flag matches
1150
+ if (!!required.variadic !== !!actual.variadic) return false;
1151
+
1152
+ // Check return types match (count and each type)
1153
+ const reqRets = required.returns ?? [];
1154
+ const actRets = actual.returns ?? [];
1155
+ if (reqRets.length !== actRets.length) return false;
1156
+ for (let i = 0; i < reqRets.length; i++) {
1157
+ if (typeStr(reqRets[i]) !== typeStr(actRets[i])) return false;
1158
+ }
1343
1159
  }
1344
1160
  return true;
1345
1161
  }
1346
1162
  }
1163
+
1164
+ Object.assign(TypeChecker.prototype, statementCheckMethods);
1165
+ Object.assign(TypeChecker.prototype, expressionCheckMethods);