gofront 0.0.2 → 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,164 +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
- // ── 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
+ 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 };
162
38
 
163
39
  export class TypeChecker {
164
40
  constructor() {
@@ -169,6 +45,9 @@ export class TypeChecker {
169
45
  this._currentSource = null;
170
46
  this._loopDepth = 0; // for break/continue validation
171
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
172
51
  this._setupGlobals();
173
52
  }
174
53
 
@@ -263,6 +142,210 @@ export class TypeChecker {
263
142
  },
264
143
  });
265
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
+
266
349
  this.globals.define("append", { kind: "builtin", name: "append" });
267
350
  this.globals.define("len", { kind: "builtin", name: "len" });
268
351
  this.globals.define("cap", { kind: "builtin", name: "cap" });
@@ -299,6 +382,27 @@ export class TypeChecker {
299
382
  for (const [name, type] of types) this.types.set(name, type);
300
383
  }
301
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
+
302
406
  // Return a snapshot of all user-defined globals (excludes built-ins).
303
407
  // Used by the compiler to expose a package's exports to importers.
304
408
  getExportedSymbols() {
@@ -326,6 +430,12 @@ export class TypeChecker {
326
430
  return ANY; // recovery type
327
431
  }
328
432
 
433
+ _reportUnused(scope, node) {
434
+ for (const name of scope.unusedLocals()) {
435
+ this.err(`'${name}' declared and not used`, node);
436
+ }
437
+ }
438
+
329
439
  check(program) {
330
440
  this._currentFile = program._filename ?? null;
331
441
  this._currentSource = program._source ?? null;
@@ -517,7 +627,12 @@ export class TypeChecker {
517
627
  outer,
518
628
  );
519
629
  if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
630
+ const savedDefer = this._deferCount;
631
+ this._deferCount = 0;
520
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;
521
636
  decl._returnType = returnType;
522
637
  }
523
638
 
@@ -537,7 +652,12 @@ export class TypeChecker {
537
652
  outer,
538
653
  );
539
654
  if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
655
+ const savedDefer = this._deferCount;
656
+ this._deferCount = 0;
540
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;
541
661
  decl._returnType = returnType;
542
662
  }
543
663
 
@@ -554,20 +674,21 @@ export class TypeChecker {
554
674
  let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
555
675
  if (spec.value) {
556
676
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
557
- if (!type) type = valTypes[0] ?? ANY;
677
+ if (!type) type = defaultType(valTypes[0]) ?? ANY;
558
678
  for (let i = 0; i < spec.names.length; i++) {
559
679
  const vt = valTypes[i] ?? ANY;
560
680
  this.assertAssignable(type, vt, spec.value[i]);
561
681
  }
562
682
  }
563
683
  if (!type) type = ANY;
564
- for (const name of spec.names) scope.define(name, type);
684
+ for (const name of spec.names) scope.defineLocal(name, type);
565
685
  }
566
686
  }
567
687
 
568
688
  checkConstDecl(decl, scope) {
569
689
  for (const spec of decl.decls) {
570
690
  const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
691
+ // Explicit type annotation → typed constant; otherwise preserve untyped
571
692
  const type = spec.type
572
693
  ? this.resolveTypeNode(spec.type, scope)
573
694
  : (valTypes[0] ?? ANY);
@@ -577,541 +698,6 @@ export class TypeChecker {
577
698
 
578
699
  // ── Statement checking ───────────────────────────────────────
579
700
 
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
701
  // ── Type resolution ──────────────────────────────────────────
1116
702
 
1117
703
  resolveTypeNode(node, scope) {
@@ -1192,7 +778,11 @@ export class TypeChecker {
1192
778
  const returns = m.returnType
1193
779
  ? [this.resolveTypeNode(m.returnType, scope)]
1194
780
  : [VOID];
1195
- 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);
1196
786
  }
1197
787
  // Flatten embedded interface methods
1198
788
  if (node.embeds) {
@@ -1289,10 +879,26 @@ export class TypeChecker {
1289
879
  if (LOG_OPS.has(op)) return BOOL;
1290
880
  if (isAny(lt) || isAny(rt)) return ANY;
1291
881
  if (isNumeric(lt) && isNumeric(rt)) {
1292
- // float64 is contagious
1293
- 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;
1294
901
  }
1295
- if (isString(lt) && isString(rt) && op === "+") return STRING;
1296
902
  if (node)
1297
903
  this.err(`Invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`, node);
1298
904
  return ANY;
@@ -1305,6 +911,29 @@ export class TypeChecker {
1305
911
  if (isAny(target) || isAny(source)) return;
1306
912
  if (isNil(source)) return; // nil is assignable to anything nullable
1307
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
+
1308
937
  // Go untyped constant promotion: int literals are assignable to float64
1309
938
  if (target.kind === "basic" && source.kind === "basic") {
1310
939
  if (target.name === "float64" && source.name === "int") return;
@@ -1336,11 +965,31 @@ export class TypeChecker {
1336
965
  for (const [name, required] of iface.methods) {
1337
966
  const actual = base.methods?.get(name);
1338
967
  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;
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
+ }
1343
989
  }
1344
990
  return true;
1345
991
  }
1346
992
  }
993
+
994
+ Object.assign(TypeChecker.prototype, statementCheckMethods);
995
+ Object.assign(TypeChecker.prototype, expressionCheckMethods);