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.
@@ -0,0 +1,359 @@
1
+ // TypeChecker expression-checking methods — installed as a mixin on TypeChecker.prototype.
2
+
3
+ import {
4
+ ANY,
5
+ BOOL,
6
+ ERROR,
7
+ INT,
8
+ isAny,
9
+ isBool,
10
+ isNumeric,
11
+ isString,
12
+ NIL,
13
+ Scope,
14
+ STRING,
15
+ typeStr,
16
+ UNTYPED_BOOL,
17
+ UNTYPED_FLOAT,
18
+ UNTYPED_INT,
19
+ UNTYPED_STRING,
20
+ VOID,
21
+ } from "./types.js";
22
+
23
+ export const expressionCheckMethods = {
24
+ checkExpr(expr, scope) {
25
+ const t = this._checkExpr(expr, scope);
26
+ expr._type = t;
27
+ return t;
28
+ },
29
+
30
+ _checkExpr(expr, scope) {
31
+ switch (expr.kind) {
32
+ case "BasicLit": {
33
+ switch (expr.litKind) {
34
+ case "INT":
35
+ return UNTYPED_INT;
36
+ case "FLOAT":
37
+ return UNTYPED_FLOAT;
38
+ case "STRING":
39
+ return UNTYPED_STRING;
40
+ case "BOOL":
41
+ return UNTYPED_BOOL;
42
+ case "NIL":
43
+ return NIL;
44
+ }
45
+ break;
46
+ }
47
+
48
+ case "Ident": {
49
+ const t = scope.lookup(expr.name);
50
+ if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
51
+ return t;
52
+ }
53
+
54
+ case "UnaryExpr": {
55
+ const ot = this.checkExpr(expr.operand, scope);
56
+ if (expr.op === "!") {
57
+ if (!isBool(ot) && !isAny(ot)) this.err("! requires bool", expr);
58
+ return BOOL;
59
+ }
60
+ if (expr.op === "-" || expr.op === "+") {
61
+ if (!isNumeric(ot) && !isAny(ot))
62
+ this.err(`${expr.op} requires numeric`, expr);
63
+ return ot;
64
+ }
65
+ return ot;
66
+ }
67
+
68
+ case "BinaryExpr": {
69
+ const lt = this.checkExpr(expr.left, scope);
70
+ const rt = this.checkExpr(expr.right, scope);
71
+ if (isAny(lt) || isAny(rt)) return this.binaryResultType(expr.op, ANY);
72
+ return this.binaryResultType(expr.op, lt, rt, expr);
73
+ }
74
+
75
+ case "CallExpr": {
76
+ return this.checkCall(expr, scope);
77
+ }
78
+
79
+ case "SelectorExpr": {
80
+ const baseType = this.checkExpr(expr.expr, scope);
81
+ return this.fieldType(baseType, expr.field, expr);
82
+ }
83
+
84
+ case "IndexExpr": {
85
+ const bt = this.checkExpr(expr.expr, scope);
86
+ this.checkExpr(expr.index, scope);
87
+ if (isAny(bt)) return ANY;
88
+ if (bt.kind === "slice" || bt.kind === "array") return bt.elem;
89
+ if (bt.kind === "map") {
90
+ expr._mapValueType = bt.value; // for codegen zero-value fallback
91
+ return bt.value;
92
+ }
93
+ if (isString(bt)) return INT; // byte
94
+ return this.err(`Cannot index type ${typeStr(bt)}`, expr);
95
+ }
96
+
97
+ case "SliceExpr": {
98
+ const bt = this.checkExpr(expr.expr, scope);
99
+ if (expr.low) this.checkExpr(expr.low, scope);
100
+ if (expr.high) this.checkExpr(expr.high, scope);
101
+ if (expr.max) this.checkExpr(expr.max, scope);
102
+ if (isAny(bt)) return ANY;
103
+ if (bt.kind === "slice" || bt.kind === "array")
104
+ return { kind: "slice", elem: bt.elem };
105
+ if (isString(bt)) return STRING;
106
+ return this.err(`Cannot slice type ${typeStr(bt)}`, expr);
107
+ }
108
+
109
+ case "CompositeLit": {
110
+ return this.checkCompositeLit(expr, scope);
111
+ }
112
+
113
+ case "FuncLit": {
114
+ const inner = new Scope(scope);
115
+ for (const p of expr.params)
116
+ inner.define(
117
+ p.name,
118
+ p.type ? this.resolveTypeNode(p.type, scope) : ANY,
119
+ );
120
+ const ret = expr.returnType
121
+ ? this.resolveTypeNode(expr.returnType, scope)
122
+ : VOID;
123
+ const savedDefer = this._deferCount;
124
+ this._deferCount = 0;
125
+ this.checkBlock(expr.body, inner, ret);
126
+ this._reportUnused(inner, expr);
127
+ if (this._deferCount > 0) expr.body._hasDefer = true;
128
+ this._deferCount = savedDefer;
129
+ const paramTypes = expr.params.map((p) =>
130
+ p.type ? this.resolveTypeNode(p.type, scope) : ANY,
131
+ );
132
+ return {
133
+ kind: "func",
134
+ params: paramTypes,
135
+ returns: [ret],
136
+ async: expr.async,
137
+ };
138
+ }
139
+
140
+ case "AwaitExpr": {
141
+ // await unwraps whatever the expression produces — no Promise type modelling
142
+ return this.checkExpr(expr.expr, scope);
143
+ }
144
+
145
+ case "TypeConversion": {
146
+ this.checkExpr(expr.expr, scope);
147
+ return this.resolveTypeNode(expr.targetType, scope);
148
+ }
149
+
150
+ case "TypeAssertExpr": {
151
+ const srcType = this.checkExpr(expr.expr, scope);
152
+ const targetType = this.resolveTypeNode(expr.type, scope);
153
+
154
+ // In Go, the source of a type assertion must be an interface type.
155
+ // We allow `any` (which is GoFront's basic any) and interface types.
156
+ if (srcType && !isAny(srcType)) {
157
+ let underlying =
158
+ srcType.kind === "named" ? srcType.underlying : srcType;
159
+ underlying = this.resolveType(underlying);
160
+ if (underlying?.kind !== "interface") {
161
+ this.err(
162
+ `invalid type assertion: ${typeStr(srcType)} is not an interface`,
163
+ expr,
164
+ );
165
+ }
166
+ }
167
+
168
+ return targetType;
169
+ }
170
+
171
+ case "RangeExpr": {
172
+ // Type-check the iterated expression so its _type is annotated
173
+ return this.checkExpr(expr.expr, scope);
174
+ }
175
+
176
+ default:
177
+ return ANY;
178
+ }
179
+ return ANY;
180
+ },
181
+
182
+ checkCall(expr, scope) {
183
+ const fnType = this.checkExpr(expr.func, scope);
184
+
185
+ // Handle built-ins before evaluating args; some (e.g. new) take type names as
186
+ // arguments, which would otherwise produce false "Undefined" errors.
187
+ if (fnType.kind === "builtin") {
188
+ const argTypes = expr.args.map((a, i) => {
189
+ // new(T) — first arg is a type name, not a value expression
190
+ if (fnType.name === "new" && i === 0) return ANY;
191
+ return this.checkExpr(a, scope);
192
+ });
193
+ return this.checkBuiltin(fnType.name, expr, argTypes, scope);
194
+ }
195
+
196
+ const argTypes = expr.args.map((a) => this.checkExpr(a, scope));
197
+
198
+ if (isAny(fnType)) return ANY;
199
+
200
+ if (fnType.kind !== "func") {
201
+ return this.err(`Cannot call non-function type ${typeStr(fnType)}`, expr);
202
+ }
203
+
204
+ // Check arg count (allow variadic slack)
205
+ const minArgs = fnType.params.filter(
206
+ (_, i) => !fnType.variadic || i < fnType.params.length - 1,
207
+ ).length;
208
+ if (argTypes.length < minArgs) {
209
+ this.err(
210
+ `Too few arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
211
+ expr,
212
+ );
213
+ }
214
+ if (!fnType.variadic && argTypes.length > fnType.params.length) {
215
+ this.err(
216
+ `Too many arguments: expected ${fnType.params.length}, got ${argTypes.length}`,
217
+ expr,
218
+ );
219
+ }
220
+
221
+ // Check each argument is assignable to its parameter type
222
+ for (let i = 0; i < fnType.params.length && i < argTypes.length; i++) {
223
+ const paramIdx =
224
+ fnType.variadic && i >= fnType.params.length - 1
225
+ ? fnType.params.length - 1
226
+ : i;
227
+ // Spread arg (f(slice...)) passes a whole slice into a variadic — skip per-element check
228
+ if (expr.args[i]?._spread) continue;
229
+ this.assertAssignable(fnType.params[paramIdx], argTypes[i], expr.args[i]);
230
+ }
231
+
232
+ const ret = fnType.returns;
233
+ if (!ret || ret.length === 0) return VOID;
234
+ if (ret.length === 1) return ret[0];
235
+ return { kind: "tuple", types: ret };
236
+ },
237
+
238
+ checkBuiltin(name, expr, argTypes, scope) {
239
+ switch (name) {
240
+ case "len":
241
+ case "cap":
242
+ return INT;
243
+ case "append":
244
+ return argTypes[0] ?? ANY;
245
+ case "copy":
246
+ return INT;
247
+ case "delete":
248
+ return VOID;
249
+ case "make": {
250
+ if (expr.args.length < 1) return ANY;
251
+ const typeArg = expr.args[0];
252
+ const typeNode = typeArg.kind === "TypeExpr" ? typeArg.type : typeArg;
253
+ return this.resolveTypeNode(typeNode, scope) ?? ANY;
254
+ }
255
+ case "new": {
256
+ if (expr.args.length < 1) return ANY;
257
+ const inner = this.resolveTypeNode(expr.args[0], scope);
258
+ return { kind: "pointer", base: inner };
259
+ }
260
+ case "print":
261
+ case "println":
262
+ case "panic":
263
+ return VOID;
264
+ case "recover":
265
+ return ANY;
266
+ case "error":
267
+ return ERROR;
268
+ case "min":
269
+ case "max":
270
+ return argTypes[0] ?? ANY;
271
+ case "clear":
272
+ return VOID;
273
+ default:
274
+ return ANY;
275
+ }
276
+ },
277
+
278
+ checkCompositeLit(expr, scope, hintType = null) {
279
+ // null typeExpr = implicit lit inside a slice/map: {X:1} in []Point{{X:1}}
280
+ const t =
281
+ expr.typeExpr === null
282
+ ? (hintType ?? ANY)
283
+ : this.resolveTypeNode(expr.typeExpr, scope);
284
+ const base = t.kind === "named" ? t.underlying : t;
285
+
286
+ if (base?.kind === "struct") {
287
+ for (const elem of expr.elems) {
288
+ if (elem.kind === "KeyValueExpr") {
289
+ const keyName = elem.key.name;
290
+ const fieldType = base.fields?.get(keyName);
291
+ if (fieldType) {
292
+ const vt = this.checkExpr(elem.value, scope);
293
+ this.assertAssignable(fieldType, vt, elem.value);
294
+ } else {
295
+ // Check if it's an embedded type name (e.g. Dog{Animal: Animal{...}})
296
+ const embed = base._embeds?.find(
297
+ (e) => (e.kind === "named" ? e.name : null) === keyName,
298
+ );
299
+ if (embed) {
300
+ const vt = this.checkExpr(elem.value, scope);
301
+ this.assertAssignable(embed, vt, elem.value);
302
+ elem._isEmbedInit = true;
303
+ } else {
304
+ this.err(`Unknown field '${keyName}'`, elem.key);
305
+ }
306
+ }
307
+ } else {
308
+ this.checkExpr(elem, scope);
309
+ }
310
+ }
311
+ return t;
312
+ }
313
+
314
+ if (base?.kind === "slice" || base?.kind === "array") {
315
+ for (const elem of expr.elems) {
316
+ if (elem.kind === "CompositeLit" && elem.typeExpr === null) {
317
+ const et = this.checkCompositeLit(elem, scope, base.elem);
318
+ elem._type = et;
319
+ } else if (elem.kind !== "KeyValueExpr") {
320
+ const et = this.checkExpr(elem, scope);
321
+ this.assertAssignable(base.elem, et, elem);
322
+ }
323
+ }
324
+ return t;
325
+ }
326
+
327
+ if (base?.kind === "map") {
328
+ for (const elem of expr.elems) {
329
+ if (elem.kind === "KeyValueExpr") {
330
+ const kt = this.checkExpr(elem.key, scope);
331
+ // value may be an implicit composite lit
332
+ let vt;
333
+ if (
334
+ elem.value.kind === "CompositeLit" &&
335
+ elem.value.typeExpr === null
336
+ ) {
337
+ vt = this.checkCompositeLit(elem.value, scope, base.value);
338
+ elem.value._type = vt;
339
+ } else {
340
+ vt = this.checkExpr(elem.value, scope);
341
+ }
342
+ this.assertAssignable(base.key, kt, elem.key);
343
+ this.assertAssignable(base.value, vt, elem.value);
344
+ }
345
+ }
346
+ return t;
347
+ }
348
+
349
+ // Unknown type context — still check elements
350
+ for (const elem of expr.elems) {
351
+ if (elem.kind === "KeyValueExpr") {
352
+ this.checkExpr(elem.value, scope);
353
+ } else {
354
+ this.checkExpr(elem, scope);
355
+ }
356
+ }
357
+ return t ?? ANY;
358
+ },
359
+ };
@@ -0,0 +1,250 @@
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
+ // Check for const reassignment before evaluating RHS
76
+ for (const lhs of stmt.lhs) {
77
+ if (lhs.kind === "Ident") {
78
+ const ownerScope = scope.lookupScope(lhs.name);
79
+ if (ownerScope?.isConst(lhs.name))
80
+ this.err(`cannot assign to const '${lhs.name}'`, lhs);
81
+ }
82
+ }
83
+ const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
84
+ const rhsFlat =
85
+ rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
86
+ const lhsTypes = stmt.lhs.map((e) => this.checkExpr(e, scope));
87
+ for (let i = 0; i < lhsTypes.length; i++) {
88
+ const r = rhsFlat[i] ?? ANY;
89
+ if (!isAny(lhsTypes[i]) && !isAny(r)) {
90
+ this.assertAssignable(lhsTypes[i], r, stmt.lhs[i]);
91
+ }
92
+ }
93
+ break;
94
+ }
95
+
96
+ case "IncDecStmt":
97
+ this.checkExpr(stmt.expr, scope);
98
+ break;
99
+
100
+ case "ExprStmt":
101
+ this.checkExpr(stmt.expr, scope);
102
+ break;
103
+
104
+ case "ReturnStmt": {
105
+ const types = stmt.values.map((v) => this.checkExpr(v, scope));
106
+ stmt._types = types;
107
+ if (
108
+ !isVoid(returnType) &&
109
+ types.length === 0 &&
110
+ !returnType?._hasNamedReturns
111
+ ) {
112
+ this.err("Missing return value", stmt);
113
+ }
114
+ // Check each returned value is assignable to its declared type
115
+ if (!isVoid(returnType) && types.length > 0) {
116
+ const expected =
117
+ returnType.kind === "tuple" ? returnType.types : [returnType];
118
+ for (let i = 0; i < expected.length; i++) {
119
+ if (types[i])
120
+ this.assertAssignable(
121
+ expected[i],
122
+ types[i],
123
+ stmt.values[i] ?? stmt,
124
+ );
125
+ }
126
+ }
127
+ break;
128
+ }
129
+
130
+ case "IfStmt": {
131
+ const inner = new Scope(scope);
132
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
133
+ const ct = this.checkExpr(stmt.cond, inner);
134
+ if (!isBool(ct) && !isAny(ct))
135
+ this.err("If condition must be bool", stmt);
136
+ const bodyScope = new Scope(inner);
137
+ this.checkBlock(stmt.body, bodyScope, returnType);
138
+ this._reportUnused(bodyScope, stmt);
139
+ if (stmt.elseBody) {
140
+ if (stmt.elseBody.kind === "Block") {
141
+ const elseScope = new Scope(inner);
142
+ this.checkBlock(stmt.elseBody, elseScope, returnType);
143
+ this._reportUnused(elseScope, stmt);
144
+ } else this.checkStmt(stmt.elseBody, inner, returnType);
145
+ }
146
+ this._reportUnused(inner, stmt);
147
+ break;
148
+ }
149
+
150
+ case "ForStmt": {
151
+ const inner = new Scope(scope);
152
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
153
+ if (stmt.cond) {
154
+ // for range N { } — RangeExpr as condition is valid (Go 1.22 int range)
155
+ if (stmt.cond.kind !== "RangeExpr") {
156
+ const ct = this.checkExpr(stmt.cond, inner);
157
+ if (!isBool(ct) && !isAny(ct))
158
+ this.err("For condition must be bool", stmt);
159
+ } else {
160
+ this.checkExpr(stmt.cond.expr, inner);
161
+ }
162
+ }
163
+ if (stmt.post) this.checkStmt(stmt.post, inner, returnType);
164
+ this._loopDepth++;
165
+ const bodyScope = new Scope(inner);
166
+ this.checkBlock(stmt.body, bodyScope, returnType);
167
+ this._reportUnused(bodyScope, stmt);
168
+ this._loopDepth--;
169
+ this._reportUnused(inner, stmt);
170
+ break;
171
+ }
172
+
173
+ case "SwitchStmt": {
174
+ const inner = new Scope(scope);
175
+ if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
176
+ if (stmt.tag) this.checkExpr(stmt.tag, inner);
177
+ this._switchDepth++;
178
+ for (const c of stmt.cases) {
179
+ const caseScope = new Scope(inner);
180
+ if (c.list) for (const e of c.list) this.checkExpr(e, caseScope);
181
+ for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
182
+ this._reportUnused(caseScope, stmt);
183
+ }
184
+ this._switchDepth--;
185
+ this._reportUnused(inner, stmt);
186
+ break;
187
+ }
188
+
189
+ case "TypeSwitchStmt": {
190
+ const inner = new Scope(scope);
191
+ this.checkExpr(stmt.expr, inner);
192
+ this._switchDepth++;
193
+ this._typeSwitchDepth++;
194
+ for (const c of stmt.cases) {
195
+ const caseScope = new Scope(inner);
196
+ if (stmt.assign) {
197
+ // Bind the variable to the single case type, or any for multi/default
198
+ const bindType =
199
+ c.types?.length === 1
200
+ ? this.resolveTypeNode(c.types[0], inner)
201
+ : ANY;
202
+ caseScope.defineLocal(stmt.assign, bindType);
203
+ }
204
+ for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
205
+ this._reportUnused(caseScope, stmt);
206
+ }
207
+ this._typeSwitchDepth--;
208
+ this._switchDepth--;
209
+ break;
210
+ }
211
+
212
+ case "DeferStmt": {
213
+ if (stmt.call.kind !== "CallExpr")
214
+ this.err("defer requires a function call", stmt.call);
215
+ this.checkExpr(stmt.call, scope);
216
+ this._deferCount++;
217
+ break;
218
+ }
219
+
220
+ case "LabeledStmt":
221
+ this.checkStmt(stmt.body, scope, returnType);
222
+ break;
223
+
224
+ case "BranchStmt": {
225
+ const kw = stmt.keyword;
226
+ if (kw === "continue" && this._loopDepth === 0)
227
+ this.err("continue statement outside for loop", stmt);
228
+ else if (
229
+ kw === "break" &&
230
+ this._loopDepth === 0 &&
231
+ this._switchDepth === 0
232
+ )
233
+ this.err("break statement outside for loop or switch", stmt);
234
+ else if (kw === "fallthrough" && this._switchDepth === 0)
235
+ this.err("fallthrough statement outside switch", stmt);
236
+ else if (kw === "fallthrough" && this._typeSwitchDepth > 0)
237
+ this.err("cannot fallthrough in type switch", stmt);
238
+ break;
239
+ }
240
+ case "Block": {
241
+ const blockScope = new Scope(scope);
242
+ this.checkBlock(stmt, blockScope, returnType);
243
+ this._reportUnused(blockScope, stmt);
244
+ break;
245
+ }
246
+ default:
247
+ break;
248
+ }
249
+ },
250
+ };