gofront 0.0.1

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.
@@ -0,0 +1,1252 @@
1
+ // Type checker: walks the AST, resolves types for all expressions,
2
+ // and reports type errors at compile time.
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 ─────────────────────────────────────────────
146
+
147
+ export class TypeChecker {
148
+ constructor() {
149
+ this.types = new Map(); // named types
150
+ this.globals = new Scope();
151
+ this.errors = [];
152
+ this._currentFile = null; // filename of file currently being checked
153
+ this._currentSource = null;
154
+ this._loopDepth = 0; // for break/continue validation
155
+ this._switchDepth = 0; // for break/fallthrough validation
156
+ this._setupGlobals();
157
+ }
158
+
159
+ _setupGlobals() {
160
+ // Browser globals — typed as 'any' so any access/call is permitted
161
+ const browserGlobals = [
162
+ "console",
163
+ "document",
164
+ "window",
165
+ "navigator",
166
+ "location",
167
+ "history",
168
+ "screen",
169
+ "performance",
170
+ "crypto",
171
+ "indexedDB",
172
+ "fetch",
173
+ "setTimeout",
174
+ "setInterval",
175
+ "clearTimeout",
176
+ "clearInterval",
177
+ "requestAnimationFrame",
178
+ "cancelAnimationFrame",
179
+ "Math",
180
+ "JSON",
181
+ "Date",
182
+ "RegExp",
183
+ "Promise",
184
+ "Error",
185
+ "Symbol",
186
+ "String",
187
+ "Number",
188
+ "Boolean",
189
+ "Array",
190
+ "Object",
191
+ "JSON",
192
+ "parseInt",
193
+ "parseFloat",
194
+ "isNaN",
195
+ "isFinite",
196
+ "encodeURIComponent",
197
+ "decodeURIComponent",
198
+ "atob",
199
+ "btoa",
200
+ "alert",
201
+ "confirm",
202
+ "prompt",
203
+ "localStorage",
204
+ "sessionStorage",
205
+ "WebSocket",
206
+ "Worker",
207
+ "HTMLElement",
208
+ "Element",
209
+ "Event",
210
+ "CustomEvent",
211
+ "URL",
212
+ "URLSearchParams",
213
+ "FormData",
214
+ "Headers",
215
+ "Request",
216
+ "Response",
217
+ "Blob",
218
+ "File",
219
+ "FileReader",
220
+ "ArrayBuffer",
221
+ "Uint8Array",
222
+ "WebGLRenderingContext",
223
+ "WebGL2RenderingContext",
224
+ "GPUDevice",
225
+ "GPUAdapter",
226
+ ];
227
+ for (const g of browserGlobals) this.globals.define(g, ANY);
228
+
229
+ // Built-in functions
230
+ // fmt package — string formatting (variadic: format string + any args)
231
+ const fmtVariadic = (ret) => ({
232
+ kind: "func",
233
+ params: [STRING, ANY],
234
+ returns: [ret],
235
+ variadic: true,
236
+ });
237
+ this.globals.define("fmt", {
238
+ kind: "namespace",
239
+ name: "fmt",
240
+ members: {
241
+ Sprintf: fmtVariadic(STRING),
242
+ Errorf: fmtVariadic(ERROR),
243
+ Printf: fmtVariadic(VOID),
244
+ Println: fmtVariadic(VOID),
245
+ Print: fmtVariadic(VOID),
246
+ },
247
+ });
248
+
249
+ this.globals.define("append", { kind: "builtin", name: "append" });
250
+ this.globals.define("len", { kind: "builtin", name: "len" });
251
+ this.globals.define("cap", { kind: "builtin", name: "cap" });
252
+ this.globals.define("make", { kind: "builtin", name: "make" });
253
+ this.globals.define("delete", { kind: "builtin", name: "delete" });
254
+ this.globals.define("print", { kind: "builtin", name: "print" });
255
+ this.globals.define("println", { kind: "builtin", name: "println" });
256
+ this.globals.define("panic", { kind: "builtin", name: "panic" });
257
+ this.globals.define("new", { kind: "builtin", name: "new" });
258
+ this.globals.define("copy", { kind: "builtin", name: "copy" });
259
+ this.globals.define("error", { kind: "builtin", name: "error" });
260
+ }
261
+
262
+ addDefinitions(types, values) {
263
+ for (const [name, type] of types) {
264
+ this.types.set(name, type);
265
+ }
266
+ for (const [name, type] of values) {
267
+ this.globals.define(name, type);
268
+ }
269
+ }
270
+
271
+ // Add an imported GoFront package as a qualified namespace.
272
+ // e.g. addPackageNamespace('utils', symbolsMap, typesMap)
273
+ // lets callers type-check `utils.Foo` via SelectorExpr.
274
+ addPackageNamespace(pkgName, symbols, types) {
275
+ const members = {};
276
+ for (const [name, type] of symbols) members[name] = type;
277
+ this.globals.define(pkgName, { kind: "namespace", name: pkgName, members });
278
+ for (const [name, type] of types) this.types.set(name, type);
279
+ }
280
+
281
+ // Return a snapshot of all user-defined globals (excludes built-ins).
282
+ // Used by the compiler to expose a package's exports to importers.
283
+ getExportedSymbols() {
284
+ const out = new Map();
285
+ for (const [name, type] of this.globals.symbols) {
286
+ if (type?.kind === "builtin") continue; // skip built-ins
287
+ if (type === ANY && !name[0].match(/[A-Z]/)) continue; // skip browser globals
288
+ out.set(name, type);
289
+ }
290
+ return out;
291
+ }
292
+
293
+ getExportedTypes() {
294
+ return new Map(this.types);
295
+ }
296
+
297
+ err(msg, node) {
298
+ const e = new TypeCheckError(
299
+ msg,
300
+ node,
301
+ this._currentFile,
302
+ this._currentSource,
303
+ );
304
+ this.errors.push(e);
305
+ return ANY; // recovery type
306
+ }
307
+
308
+ check(program) {
309
+ this._currentFile = program._filename ?? null;
310
+ this._currentSource = program._source ?? null;
311
+ // Pass 1: collect type declarations
312
+ for (const decl of program.decls) {
313
+ if (decl.kind === "TypeDecl") this.collectType(decl);
314
+ }
315
+ // Pass 2: collect function / method signatures
316
+ for (const decl of program.decls) {
317
+ if (decl.kind === "FuncDecl" || decl.kind === "MethodDecl") {
318
+ this.collectFunc(decl);
319
+ }
320
+ }
321
+ // Pass 2.1: Promote embedded methods
322
+ this._promoteEmbeddedMethods();
323
+ // Pass 3: check bodies
324
+ for (const decl of program.decls) {
325
+ this.checkTopDecl(decl, this.globals);
326
+ }
327
+ return this.errors;
328
+ }
329
+
330
+ // Pre-declare a package-level var so other files can reference its name
331
+ // before we've type-checked its initializer. Called in pass 2.5.
332
+ collectVar(decl) {
333
+ for (const spec of decl.decls) {
334
+ const type = spec.type
335
+ ? this.resolveTypeNode(spec.type, this.globals)
336
+ : ANY;
337
+ for (const name of spec.names) this.globals.define(name, type);
338
+ }
339
+ }
340
+
341
+ collectConst(decl) {
342
+ for (const spec of decl.decls) {
343
+ const type = spec.type
344
+ ? this.resolveTypeNode(spec.type, this.globals)
345
+ : ANY;
346
+ for (const name of spec.names) this.globals.defineConst(name, type);
347
+ }
348
+ }
349
+
350
+ // Like check() but operates over multiple programs (same-package multi-file).
351
+ // All passes run across all files before moving to the next pass,
352
+ // so every file sees every other file's declarations.
353
+ checkAll(programs) {
354
+ // Pass 1: collect type declarations
355
+ for (const p of programs) {
356
+ this._currentFile = p._filename ?? null;
357
+ this._currentSource = p._source ?? null;
358
+ for (const d of p.decls) if (d.kind === "TypeDecl") this.collectType(d);
359
+ }
360
+
361
+ // Pass 2: collect function / method signatures
362
+ for (const p of programs) {
363
+ this._currentFile = p._filename ?? null;
364
+ this._currentSource = p._source ?? null;
365
+ for (const d of p.decls)
366
+ if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
367
+ this.collectFunc(d);
368
+ }
369
+
370
+ // Pass 2.1: Promote embedded methods
371
+ this._promoteEmbeddedMethods();
372
+
373
+ // Pass 2.5: pre-declare package-level vars and consts
374
+ // This allows any file's function body to reference vars from other files.
375
+ for (const p of programs) {
376
+ this._currentFile = p._filename ?? null;
377
+ this._currentSource = p._source ?? null;
378
+ for (const d of p.decls) {
379
+ if (d.kind === "VarDecl") this.collectVar(d);
380
+ if (d.kind === "ConstDecl") this.collectConst(d);
381
+ }
382
+ }
383
+
384
+ // Pass 3: check bodies (re-checks var/const decls with proper types)
385
+ for (const p of programs) {
386
+ this._currentFile = p._filename ?? null;
387
+ this._currentSource = p._source ?? null;
388
+ for (const d of p.decls) this.checkTopDecl(d, this.globals);
389
+ }
390
+
391
+ return this.errors;
392
+ }
393
+
394
+ // ── Shared pass helpers ──────────────────────────────────────
395
+
396
+ _promoteEmbeddedMethods() {
397
+ for (const type of this.types.values()) {
398
+ const struct = type.underlying;
399
+ if (struct?.kind === "struct" && struct._embeds) {
400
+ for (const embed of struct._embeds) {
401
+ const base = embed.kind === "named" ? embed.underlying : embed;
402
+ if (base?.kind !== "struct" || !base.methods) continue;
403
+ for (const [mName, mType] of base.methods.entries()) {
404
+ if (!struct.methods.has(mName)) struct.methods.set(mName, mType);
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ // ── Type collection ──────────────────────────────────────────
412
+
413
+ collectType(decl) {
414
+ const underlying = this.resolveTypeNode(decl.type, this.globals);
415
+ const named = { kind: "named", name: decl.name, underlying };
416
+ if (underlying.kind === "struct") {
417
+ underlying.name = decl.name;
418
+ underlying.methods = new Map();
419
+ }
420
+ if (underlying.kind === "interface") {
421
+ underlying.name = decl.name;
422
+ }
423
+ this.types.set(decl.name, named);
424
+ this.globals.define(decl.name, named);
425
+ }
426
+
427
+ collectFunc(decl) {
428
+ const paramTypes = decl.params.map((p) =>
429
+ this.resolveTypeNode(p.type, this.globals),
430
+ );
431
+ const returnType = decl.returnType
432
+ ? this.resolveTypeNode(decl.returnType, this.globals)
433
+ : VOID;
434
+ const isVariadic =
435
+ decl.params.length > 0 && decl.params[decl.params.length - 1].variadic;
436
+ const funcType = {
437
+ kind: "func",
438
+ params: paramTypes,
439
+ returns: [returnType],
440
+ variadic: isVariadic,
441
+ async: decl.async ?? false,
442
+ };
443
+ if (decl.kind === "FuncDecl") {
444
+ if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
445
+ this.err(`${decl.name} redeclared in this block`, decl);
446
+ }
447
+ this.globals.define(decl.name, funcType);
448
+ } else {
449
+ // Method: attach to struct type
450
+ const recvType = this.resolveTypeNodeName(decl.recvType, this.globals);
451
+ const base = recvType?.underlying ?? recvType;
452
+ if (base?.kind === "struct") {
453
+ base.methods.set(decl.name, funcType);
454
+ }
455
+ }
456
+ }
457
+
458
+ // ── Top-level checker ────────────────────────────────────────
459
+
460
+ checkTopDecl(decl, scope) {
461
+ switch (decl.kind) {
462
+ case "FuncDecl":
463
+ this.checkFuncDecl(decl, scope);
464
+ break;
465
+ case "MethodDecl":
466
+ this.checkMethodDecl(decl, scope);
467
+ break;
468
+ case "VarDecl":
469
+ this.checkVarDecl(decl, scope);
470
+ break;
471
+ case "ConstDecl":
472
+ this.checkConstDecl(decl, scope);
473
+ break;
474
+ case "TypeDecl":
475
+ break; // already collected
476
+ }
477
+ }
478
+
479
+ checkFuncDecl(decl, outer) {
480
+ const inner = new Scope(outer);
481
+ for (const p of decl.params) {
482
+ inner.define(p.name, this.resolveTypeNode(p.type, outer));
483
+ }
484
+ const returnType = decl.returnType
485
+ ? this.resolveTypeNode(decl.returnType, outer)
486
+ : VOID;
487
+ const hasNamedReturns = this._injectNamedReturns(
488
+ decl.returnType,
489
+ inner,
490
+ outer,
491
+ );
492
+ if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
493
+ this.checkBlock(decl.body, inner, returnType);
494
+ decl._returnType = returnType;
495
+ }
496
+
497
+ checkMethodDecl(decl, outer) {
498
+ const inner = new Scope(outer);
499
+ const recvType = this.resolveTypeNodeName(decl.recvType, outer);
500
+ inner.define(decl.recvName, recvType);
501
+ for (const p of decl.params) {
502
+ inner.define(p.name, this.resolveTypeNode(p.type, outer));
503
+ }
504
+ const returnType = decl.returnType
505
+ ? this.resolveTypeNode(decl.returnType, outer)
506
+ : VOID;
507
+ const hasNamedReturns = this._injectNamedReturns(
508
+ decl.returnType,
509
+ inner,
510
+ outer,
511
+ );
512
+ if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
513
+ this.checkBlock(decl.body, inner, returnType);
514
+ decl._returnType = returnType;
515
+ }
516
+
517
+ _injectNamedReturns(returnTypeNode, scope, outer) {
518
+ if (!returnTypeNode?._namedReturns) return false;
519
+ for (const { name, type } of returnTypeNode._namedReturns) {
520
+ if (name) scope.define(name, this.resolveTypeNode(type, outer));
521
+ }
522
+ return true;
523
+ }
524
+
525
+ checkVarDecl(decl, scope) {
526
+ for (const spec of decl.decls) {
527
+ let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
528
+ if (spec.value) {
529
+ const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
530
+ if (!type) type = valTypes[0] ?? ANY;
531
+ for (let i = 0; i < spec.names.length; i++) {
532
+ const vt = valTypes[i] ?? ANY;
533
+ this.assertAssignable(type, vt, spec.value[i]);
534
+ }
535
+ }
536
+ if (!type) type = ANY;
537
+ for (const name of spec.names) scope.define(name, type);
538
+ }
539
+ }
540
+
541
+ checkConstDecl(decl, scope) {
542
+ for (const spec of decl.decls) {
543
+ const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
544
+ const type = spec.type
545
+ ? this.resolveTypeNode(spec.type, scope)
546
+ : (valTypes[0] ?? ANY);
547
+ for (const name of spec.names) scope.defineConst(name, type);
548
+ }
549
+ }
550
+
551
+ // ── Statement checking ───────────────────────────────────────
552
+
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
+ // ── Type resolution ──────────────────────────────────────────
1043
+
1044
+ resolveTypeNode(node, scope) {
1045
+ if (!node) return ANY;
1046
+ switch (node.kind) {
1047
+ case "TypeName": {
1048
+ if (BASIC_TYPES[node.name]) return BASIC_TYPES[node.name];
1049
+ const named = this.types.get(node.name);
1050
+ if (named) return named;
1051
+ return this.err(`Unknown type '${node.name}'`, node);
1052
+ }
1053
+ case "SliceType":
1054
+ return {
1055
+ kind: "slice",
1056
+ elem: this.resolveTypeNode(node.elem, scope),
1057
+ };
1058
+ case "ArrayType":
1059
+ return {
1060
+ kind: "array",
1061
+ size: node.size,
1062
+ elem: this.resolveTypeNode(node.elem, scope),
1063
+ };
1064
+ case "MapType":
1065
+ return {
1066
+ kind: "map",
1067
+ key: this.resolveTypeNode(node.key, scope),
1068
+ value: this.resolveTypeNode(node.value, scope),
1069
+ };
1070
+ case "PointerType":
1071
+ return {
1072
+ kind: "pointer",
1073
+ base: this.resolveTypeNode(node.base, scope),
1074
+ };
1075
+ case "FuncType": {
1076
+ const params = node.params.map((p) =>
1077
+ this.resolveTypeNode(p.type, scope),
1078
+ );
1079
+ const returns = node.returnType
1080
+ ? [this.resolveTypeNode(node.returnType, scope)]
1081
+ : [VOID];
1082
+ return { kind: "func", params, returns };
1083
+ }
1084
+ case "StructType": {
1085
+ const fields = new Map();
1086
+ const embeds = []; // track embedded types to merge methods later
1087
+ for (const f of node.fields) {
1088
+ const ft = this.resolveTypeNode(f.type, scope);
1089
+ if (f.embedded) {
1090
+ // Flatten embedded struct fields
1091
+ const base = ft.kind === "named" ? ft.underlying : ft;
1092
+ if (base?.kind === "struct") {
1093
+ for (const [k, v] of base.fields.entries()) fields.set(k, v);
1094
+ }
1095
+ embeds.push(ft);
1096
+ } else {
1097
+ for (const n of f.names) fields.set(n, ft);
1098
+ }
1099
+ }
1100
+ const structType = {
1101
+ kind: "struct",
1102
+ fields,
1103
+ methods: new Map(),
1104
+ _embeds: embeds,
1105
+ };
1106
+ return structType;
1107
+ }
1108
+ case "InterfaceType": {
1109
+ const methods = new Map();
1110
+ for (const m of node.methods) {
1111
+ const params = m.params.map((p) =>
1112
+ this.resolveTypeNode(p.type, scope),
1113
+ );
1114
+ const returns = m.returnType
1115
+ ? [this.resolveTypeNode(m.returnType, scope)]
1116
+ : [VOID];
1117
+ methods.set(m.name, { kind: "func", params, returns });
1118
+ }
1119
+ return { kind: "interface", methods };
1120
+ }
1121
+ case "TupleType": {
1122
+ return {
1123
+ kind: "tuple",
1124
+ types: node.types.map((t) => this.resolveTypeNode(t, scope)),
1125
+ };
1126
+ }
1127
+ // Expression used as type (e.g. in make/new calls)
1128
+ case "Ident":
1129
+ return this.resolveTypeNode(
1130
+ { kind: "TypeName", name: node.name },
1131
+ scope,
1132
+ );
1133
+ default:
1134
+ return ANY;
1135
+ }
1136
+ }
1137
+
1138
+ resolveTypeNodeName(node, scope) {
1139
+ if (!node) return ANY;
1140
+ if (node.kind === "TypeName") {
1141
+ const named = this.types.get(node.name);
1142
+ if (named) return named;
1143
+ }
1144
+ return this.resolveTypeNode(node, scope);
1145
+ }
1146
+
1147
+ // ── Helpers ───────────────────────────────────────────────────
1148
+
1149
+ resolveType(t) {
1150
+ if (!t) return ANY;
1151
+ if (t.kind === "basic" && t.alias) {
1152
+ const found = this.types.get(t.alias);
1153
+ if (found) return found;
1154
+ }
1155
+ return t;
1156
+ }
1157
+
1158
+ fieldType(baseType, field, node) {
1159
+ baseType = this.resolveType(baseType);
1160
+ if (!baseType || isAny(baseType)) return ANY;
1161
+ let base = baseType.kind === "named" ? baseType.underlying : baseType;
1162
+ base = this.resolveType(base);
1163
+ if (base?.kind === "struct") {
1164
+ if (base.fields?.has(field)) return base.fields.get(field);
1165
+ if (base.methods?.has(field)) return base.methods.get(field);
1166
+ return this.err(`No field '${field}' on ${typeStr(baseType)}`, node);
1167
+ }
1168
+ if (base?.kind === "namespace") {
1169
+ if (field in base.members) return base.members[field];
1170
+ return this.err(`No member '${field}' in namespace ${base.name}`, node);
1171
+ }
1172
+ // For explicitly typed non-any values, report the bad access rather than
1173
+ // silently returning any — this catches typos on structs and primitive misuse.
1174
+ if (base && !isAny(base)) {
1175
+ // error.Error() is valid — it returns the error string
1176
+ if (isError(base) && field === "Error")
1177
+ return { kind: "func", params: [], returns: [STRING], async: false };
1178
+ // pointer.value is the GoFront new(T) pattern
1179
+ if (base.kind === "pointer" && field === "value") return base.base ?? ANY;
1180
+ const badKinds = ["basic", "slice", "array", "map", "func", "tuple"];
1181
+ if (badKinds.includes(base.kind)) {
1182
+ return this.err(
1183
+ `${typeStr(baseType ?? base)} has no field or method '${field}'`,
1184
+ node,
1185
+ );
1186
+ }
1187
+ }
1188
+ return ANY; // unknown types (e.g. external .d.ts) — allow any access
1189
+ }
1190
+
1191
+ 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;
1196
+ if (isAny(lt) || isAny(rt)) return ANY;
1197
+ if (isNumeric(lt) && isNumeric(rt)) {
1198
+ // float64 is contagious
1199
+ return lt.name === "float64" || rt.name === "float64" ? FLOAT64 : INT;
1200
+ }
1201
+ if (isString(lt) && isString(rt) && op === "+") return STRING;
1202
+ if (node)
1203
+ this.err(`Invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`, node);
1204
+ return ANY;
1205
+ }
1206
+
1207
+ assertAssignable(target, source, node) {
1208
+ if (!target || !source) return;
1209
+ target = this.resolveType(target);
1210
+ source = this.resolveType(source);
1211
+ if (isAny(target) || isAny(source)) return;
1212
+ if (isNil(source)) return; // nil is assignable to anything nullable
1213
+
1214
+ // Go untyped constant promotion: int literals are assignable to float64
1215
+ if (target.kind === "basic" && source.kind === "basic") {
1216
+ if (target.name === "float64" && source.name === "int") return;
1217
+ if (target.name === "int" && source.name === "float64") return; // truncating
1218
+ }
1219
+ if (typeStr(target) !== typeStr(source)) {
1220
+ // Interface satisfaction check
1221
+ let tBase = target.kind === "named" ? target.underlying : target;
1222
+ let sBase = source.kind === "named" ? source.underlying : source;
1223
+ tBase = this.resolveType(tBase);
1224
+ sBase = this.resolveType(sBase);
1225
+ if (tBase?.kind === "interface") {
1226
+ if (!this.implements(source, tBase, node)) {
1227
+ this.err(
1228
+ `${typeStr(source)} does not implement ${typeStr(target)}`,
1229
+ node,
1230
+ );
1231
+ }
1232
+ return;
1233
+ }
1234
+ this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
1235
+ }
1236
+ }
1237
+
1238
+ implements(srcType, iface, _node) {
1239
+ let base = srcType.kind === "named" ? srcType.underlying : srcType;
1240
+ base = this.resolveType(base);
1241
+ if (base?.kind !== "struct") return false;
1242
+ for (const [name, required] of iface.methods) {
1243
+ const actual = base.methods?.get(name);
1244
+ 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;
1249
+ }
1250
+ return true;
1251
+ }
1252
+ }