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.
@@ -0,0 +1,287 @@
1
+ // TypeChecker statement-checking methods — installed as a mixin on TypeChecker.prototype.
2
+
3
+ import {
4
+ ANY,
5
+ BOOL,
6
+ defaultType,
7
+ isAny,
8
+ isBool,
9
+ isVoid,
10
+ Scope,
11
+ } from "./types.js";
12
+
13
+ export const statementCheckMethods = {
14
+ checkBlock(block, scope, returnType) {
15
+ for (const stmt of block.stmts) {
16
+ this.checkStmt(stmt, scope, returnType);
17
+ }
18
+ },
19
+
20
+ checkStmt(stmt, scope, returnType) {
21
+ switch (stmt.kind) {
22
+ case "VarDecl":
23
+ this.checkVarDecl(stmt, scope);
24
+ break;
25
+ case "ConstDecl":
26
+ this.checkConstDecl(stmt, scope);
27
+ break;
28
+ case "TypeDecl":
29
+ this.collectType(stmt);
30
+ break;
31
+
32
+ case "DefineStmt": {
33
+ // comma-ok type assertion: n, ok := x.(T)
34
+ if (
35
+ stmt.lhs.length === 2 &&
36
+ stmt.rhs.length === 1 &&
37
+ stmt.rhs[0].kind === "TypeAssertExpr"
38
+ ) {
39
+ stmt.rhs[0]._commaOk = true;
40
+ }
41
+ const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
42
+ // Flatten tuple returns
43
+ const rhsFlat =
44
+ rhs.length === 1 && rhs[0].kind === "tuple" ? rhs[0].types : rhs;
45
+ // comma-ok type assertion returns [assertedType, bool]
46
+ if (stmt.rhs[0]?._commaOk) {
47
+ const lhsNames = stmt.lhs.map((e) => e.name ?? e);
48
+ if (lhsNames[0] !== "_") scope.defineLocal(lhsNames[0], rhs[0]);
49
+ if (lhsNames[1] !== "_") scope.defineLocal(lhsNames[1], BOOL);
50
+ stmt._rhsTypes = [rhs[0], BOOL];
51
+ break;
52
+ }
53
+ for (let i = 0; i < stmt.lhs.length; i++) {
54
+ const name = stmt.lhs[i].name ?? stmt.lhs[i];
55
+ if (name === "_") continue;
56
+ // Short variable re-declaration: mark vars that already exist
57
+ // in the current (not parent) scope as redeclared.
58
+ if (scope.symbols.has(name)) {
59
+ stmt.lhs[i]._redecl = true;
60
+ }
61
+ scope.defineLocal(name, defaultType(rhsFlat[i]) ?? ANY);
62
+ }
63
+ // Go requires at least one new variable in :=
64
+ const allRedecl = stmt.lhs.every(
65
+ (e) => e._redecl || (e.name ?? e) === "_",
66
+ );
67
+ if (allRedecl && stmt.lhs.length > 0) {
68
+ this.err("no new variables on left side of :=", stmt.lhs[0]);
69
+ }
70
+ stmt._rhsTypes = rhsFlat.map((t) => defaultType(t) ?? ANY);
71
+ break;
72
+ }
73
+
74
+ case "AssignStmt": {
75
+ // comma-ok map index: v, ok = m["key"]
76
+ // Evaluate map expr first so _type is populated, then detect the pattern.
77
+ if (
78
+ stmt.lhs.length === 2 &&
79
+ stmt.rhs.length === 1 &&
80
+ stmt.rhs[0].kind === "IndexExpr"
81
+ ) {
82
+ const mapBaseType = this.checkExpr(stmt.rhs[0].expr, scope);
83
+ const resolvedBase =
84
+ mapBaseType?.kind === "named"
85
+ ? mapBaseType.underlying
86
+ : mapBaseType;
87
+ if (resolvedBase?.kind === "map") {
88
+ this.checkExpr(stmt.rhs[0].index, scope);
89
+ stmt._commaOkMap = true;
90
+ // Store the map value type so codegen can emit the zero-value fallback
91
+ stmt.rhs[0]._mapValueType = resolvedBase.value;
92
+ break;
93
+ }
94
+ }
95
+ // comma-ok type assertion: v, ok = x.(T)
96
+ if (
97
+ stmt.lhs.length === 2 &&
98
+ stmt.rhs.length === 1 &&
99
+ stmt.rhs[0].kind === "TypeAssertExpr"
100
+ ) {
101
+ stmt.rhs[0]._commaOk = true;
102
+ }
103
+ // Check for const reassignment before evaluating RHS
104
+ for (const lhs of stmt.lhs) {
105
+ if (lhs.kind === "Ident" && lhs.name !== "_") {
106
+ const ownerScope = scope.lookupScope(lhs.name);
107
+ if (ownerScope?.isConst(lhs.name))
108
+ this.err(`cannot assign to const '${lhs.name}'`, lhs);
109
+ }
110
+ }
111
+ const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
112
+ const rhsFlat =
113
+ rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
114
+ const lhsTypes = stmt.lhs.map((e) => {
115
+ if (e.kind === "Ident" && e.name === "_") return ANY;
116
+ return this.checkExpr(e, scope);
117
+ });
118
+ for (let i = 0; i < lhsTypes.length; i++) {
119
+ if (stmt.lhs[i]?.kind === "Ident" && stmt.lhs[i]?.name === "_")
120
+ continue;
121
+ const r = rhsFlat[i] ?? ANY;
122
+ if (!isAny(lhsTypes[i]) && !isAny(r)) {
123
+ this.assertAssignable(lhsTypes[i], r, stmt.lhs[i]);
124
+ }
125
+ }
126
+ break;
127
+ }
128
+
129
+ case "IncDecStmt":
130
+ this.checkExpr(stmt.expr, scope);
131
+ break;
132
+
133
+ case "ExprStmt":
134
+ this.checkExpr(stmt.expr, scope);
135
+ break;
136
+
137
+ case "ReturnStmt": {
138
+ const types = stmt.values.map((v) => this.checkExpr(v, scope));
139
+ stmt._types = types;
140
+ if (
141
+ !isVoid(returnType) &&
142
+ types.length === 0 &&
143
+ !returnType?._hasNamedReturns
144
+ ) {
145
+ this.err("Missing return value", stmt);
146
+ }
147
+ // Check each returned value is assignable to its declared type
148
+ if (!isVoid(returnType) && types.length > 0) {
149
+ const expected =
150
+ returnType.kind === "tuple" ? returnType.types : [returnType];
151
+ for (let i = 0; i < expected.length; i++) {
152
+ if (types[i])
153
+ this.assertAssignable(
154
+ expected[i],
155
+ types[i],
156
+ stmt.values[i] ?? stmt,
157
+ );
158
+ }
159
+ }
160
+ break;
161
+ }
162
+
163
+ case "IfStmt": {
164
+ const inner = new Scope(scope);
165
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
166
+ const ct = this.checkExpr(stmt.cond, inner);
167
+ if (!isBool(ct) && !isAny(ct))
168
+ this.err("If condition must be bool", stmt);
169
+ const bodyScope = new Scope(inner);
170
+ this.checkBlock(stmt.body, bodyScope, returnType);
171
+ this._reportUnused(bodyScope, stmt);
172
+ if (stmt.elseBody) {
173
+ if (stmt.elseBody.kind === "Block") {
174
+ const elseScope = new Scope(inner);
175
+ this.checkBlock(stmt.elseBody, elseScope, returnType);
176
+ this._reportUnused(elseScope, stmt);
177
+ } else this.checkStmt(stmt.elseBody, inner, returnType);
178
+ }
179
+ this._reportUnused(inner, stmt);
180
+ break;
181
+ }
182
+
183
+ case "ForStmt": {
184
+ const inner = new Scope(scope);
185
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
186
+ if (stmt.cond) {
187
+ // for range N { } — RangeExpr as condition is valid (Go 1.22 int range)
188
+ if (stmt.cond.kind !== "RangeExpr") {
189
+ const ct = this.checkExpr(stmt.cond, inner);
190
+ if (!isBool(ct) && !isAny(ct))
191
+ this.err("For condition must be bool", stmt);
192
+ } else {
193
+ this.checkExpr(stmt.cond.expr, inner);
194
+ }
195
+ }
196
+ if (stmt.post) this.checkStmt(stmt.post, inner, returnType);
197
+ this._loopDepth++;
198
+ const bodyScope = new Scope(inner);
199
+ this.checkBlock(stmt.body, bodyScope, returnType);
200
+ this._reportUnused(bodyScope, stmt);
201
+ this._loopDepth--;
202
+ this._reportUnused(inner, stmt);
203
+ break;
204
+ }
205
+
206
+ case "SwitchStmt": {
207
+ const inner = new Scope(scope);
208
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
209
+ if (stmt.tag) this.checkExpr(stmt.tag, inner);
210
+ this._switchDepth++;
211
+ for (const c of stmt.cases) {
212
+ const caseScope = new Scope(inner);
213
+ if (c.list) for (const e of c.list) this.checkExpr(e, caseScope);
214
+ for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
215
+ this._reportUnused(caseScope, stmt);
216
+ }
217
+ this._switchDepth--;
218
+ this._reportUnused(inner, stmt);
219
+ break;
220
+ }
221
+
222
+ case "TypeSwitchStmt": {
223
+ const inner = new Scope(scope);
224
+ this.checkExpr(stmt.expr, inner);
225
+ this._switchDepth++;
226
+ this._typeSwitchDepth++;
227
+ for (const c of stmt.cases) {
228
+ const caseScope = new Scope(inner);
229
+ if (stmt.assign) {
230
+ // Bind the variable to the single case type, or any for multi/default
231
+ const bindType =
232
+ c.types?.length === 1
233
+ ? this.resolveTypeNode(c.types[0], inner)
234
+ : ANY;
235
+ caseScope.defineLocal(stmt.assign, bindType);
236
+ // The capture variable is always considered used — it's the whole
237
+ // point of the switch-with-assign form. Each case body uses its own
238
+ // typed copy of the variable.
239
+ caseScope.lookup(stmt.assign);
240
+ }
241
+ for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
242
+ this._reportUnused(caseScope, stmt);
243
+ }
244
+ this._typeSwitchDepth--;
245
+ this._switchDepth--;
246
+ break;
247
+ }
248
+
249
+ case "DeferStmt": {
250
+ if (stmt.call.kind !== "CallExpr")
251
+ this.err("defer requires a function call", stmt.call);
252
+ this.checkExpr(stmt.call, scope);
253
+ this._deferCount++;
254
+ break;
255
+ }
256
+
257
+ case "LabeledStmt":
258
+ this.checkStmt(stmt.body, scope, returnType);
259
+ break;
260
+
261
+ case "BranchStmt": {
262
+ const kw = stmt.keyword;
263
+ if (kw === "continue" && this._loopDepth === 0)
264
+ this.err("continue statement outside for loop", stmt);
265
+ else if (
266
+ kw === "break" &&
267
+ this._loopDepth === 0 &&
268
+ this._switchDepth === 0
269
+ )
270
+ this.err("break statement outside for loop or switch", stmt);
271
+ else if (kw === "fallthrough" && this._switchDepth === 0)
272
+ this.err("fallthrough statement outside switch", stmt);
273
+ else if (kw === "fallthrough" && this._typeSwitchDepth > 0)
274
+ this.err("cannot fallthrough in type switch", stmt);
275
+ break;
276
+ }
277
+ case "Block": {
278
+ const blockScope = new Scope(scope);
279
+ this.checkBlock(stmt, blockScope, returnType);
280
+ this._reportUnused(blockScope, stmt);
281
+ break;
282
+ }
283
+ default:
284
+ break;
285
+ }
286
+ },
287
+ };
@@ -0,0 +1,208 @@
1
+ // GoFront type system — shared type constants, predicates, and utilities
2
+ // used across the type-checker sub-modules.
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
+ export const CMP_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
39
+ export const LOG_OPS = new Set(["&&", "||"]);
40
+
41
+ // ── Built-in types ───────────────────────────────────────────
42
+
43
+ export const INT = { kind: "basic", name: "int" };
44
+ export const FLOAT64 = { kind: "basic", name: "float64" };
45
+ export const STRING = { kind: "basic", name: "string" };
46
+ export const BOOL = { kind: "basic", name: "bool" };
47
+ export const ANY = { kind: "basic", name: "any" };
48
+ export const VOID = { kind: "basic", name: "void" };
49
+ export const NIL = { kind: "basic", name: "nil" };
50
+ export const ERROR = { kind: "basic", name: "error" };
51
+
52
+ // ── Untyped constant types (Go spec §Constants) ─────────────
53
+ // Untyped constants coerce to any compatible typed context.
54
+ export const UNTYPED_INT = { kind: "untyped", base: "int" };
55
+ export const UNTYPED_FLOAT = { kind: "untyped", base: "float64" };
56
+ export const UNTYPED_STRING = { kind: "untyped", base: "string" };
57
+ export const UNTYPED_BOOL = { kind: "untyped", base: "bool" };
58
+
59
+ export const BASIC_TYPES = {
60
+ int: INT,
61
+ float64: FLOAT64,
62
+ string: STRING,
63
+ bool: BOOL,
64
+ any: ANY,
65
+ byte: INT,
66
+ rune: INT,
67
+ error: ERROR,
68
+ // Sized integer / float aliases — all map to int or float64 at runtime
69
+ uint: INT,
70
+ int8: INT,
71
+ int16: INT,
72
+ int32: INT,
73
+ int64: INT,
74
+ uint8: INT,
75
+ uint16: INT,
76
+ uint32: INT,
77
+ uint64: INT,
78
+ uintptr: INT,
79
+ float32: FLOAT64,
80
+ };
81
+
82
+ export function isNumeric(t) {
83
+ if (t?.kind === "untyped") return t.base === "int" || t.base === "float64";
84
+ return t?.kind === "basic" && (t.name === "int" || t.name === "float64");
85
+ }
86
+ export function isString(t) {
87
+ if (t?.kind === "untyped") return t.base === "string";
88
+ return t?.kind === "basic" && t.name === "string";
89
+ }
90
+ export function isBool(t) {
91
+ if (t?.kind === "untyped") return t.base === "bool";
92
+ return t?.kind === "basic" && t.name === "bool";
93
+ }
94
+ export function isAny(t) {
95
+ return t?.kind === "basic" && t.name === "any";
96
+ }
97
+ export function isNil(t) {
98
+ return t?.kind === "basic" && t.name === "nil";
99
+ }
100
+ export function isVoid(t) {
101
+ return t?.kind === "basic" && t.name === "void";
102
+ }
103
+ export function isError(t) {
104
+ return t?.kind === "basic" && t.name === "error";
105
+ }
106
+ export function isUntyped(t) {
107
+ return t?.kind === "untyped";
108
+ }
109
+
110
+ /** Materialize an untyped type to its default concrete type. */
111
+ export function defaultType(t) {
112
+ if (t?.kind !== "untyped") return t;
113
+ switch (t.base) {
114
+ case "int":
115
+ return INT;
116
+ case "float64":
117
+ return FLOAT64;
118
+ case "string":
119
+ return STRING;
120
+ case "bool":
121
+ return BOOL;
122
+ default:
123
+ return ANY;
124
+ }
125
+ }
126
+
127
+ export function typeStr(t) {
128
+ if (!t) return "void";
129
+ if (t.kind === "basic" && t.alias) return t.alias;
130
+ switch (t.kind) {
131
+ case "basic":
132
+ return t.name;
133
+ case "untyped":
134
+ return `untyped ${t.base}`;
135
+ case "slice":
136
+ return `[]${typeStr(t.elem)}`;
137
+ case "array":
138
+ return `[${t.size}]${typeStr(t.elem)}`;
139
+ case "map":
140
+ return `map[${typeStr(t.key)}]${typeStr(t.value)}`;
141
+ case "struct":
142
+ return t.name || "struct{...}";
143
+ case "interface":
144
+ return t.name || "interface{...}";
145
+ case "namespace":
146
+ return t.name || "namespace{...}";
147
+ case "func":
148
+ return `func(${t.params.map(typeStr).join(", ")}) ${typeStr(t.returns[0] ?? VOID)}`;
149
+ case "tuple":
150
+ return `(${t.types.map(typeStr).join(", ")})`;
151
+ case "named":
152
+ return t.name;
153
+ case "pointer":
154
+ return `*${typeStr(t.base)}`;
155
+ default:
156
+ return "?";
157
+ }
158
+ }
159
+
160
+ // ── Scope / environment ──────────────────────────────────────
161
+
162
+ export class Scope {
163
+ constructor(parent = null) {
164
+ this.parent = parent;
165
+ this.symbols = new Map();
166
+ this._consts = new Set(); // names declared as const in this scope
167
+ this._locals = new Set(); // names declared as local variables (var / :=)
168
+ this._used = new Set(); // names referenced in this scope
169
+ }
170
+ define(name, type) {
171
+ this.symbols.set(name, type);
172
+ }
173
+ defineLocal(name, type) {
174
+ this.symbols.set(name, type);
175
+ if (name !== "_") this._locals.add(name);
176
+ }
177
+ defineConst(name, type) {
178
+ this.symbols.set(name, type);
179
+ this._consts.add(name);
180
+ }
181
+ isConst(name) {
182
+ if (this._consts.has(name)) return true;
183
+ // Don't walk parent — shadowing a const with a var in a child scope is valid
184
+ return false;
185
+ }
186
+ lookup(name) {
187
+ if (this.symbols.has(name)) {
188
+ this._used.add(name);
189
+ return this.symbols.get(name);
190
+ }
191
+ if (this.parent) return this.parent.lookup(name);
192
+ return null;
193
+ }
194
+ // Lookup which scope owns the name (to check const flag)
195
+ lookupScope(name) {
196
+ if (this.symbols.has(name)) return this;
197
+ if (this.parent) return this.parent.lookupScope(name);
198
+ return null;
199
+ }
200
+ // Returns local variable names that were never referenced
201
+ unusedLocals() {
202
+ const unused = [];
203
+ for (const name of this._locals) {
204
+ if (!this._used.has(name)) unused.push(name);
205
+ }
206
+ return unused;
207
+ }
208
+ }