gofront 0.0.9 → 1.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.
- package/README.md +5 -5
- package/package.json +7 -4
- package/src/cli-core.js +65 -0
- package/src/codegen/expressions.js +523 -1717
- package/src/codegen/index.js +576 -0
- package/src/codegen/runtime.js +151 -0
- package/src/codegen/statements.js +308 -296
- package/src/codegen/stdlib/builder.js +39 -0
- package/src/codegen/stdlib/bytes.js +65 -0
- package/src/codegen/stdlib/errors.js +20 -0
- package/src/codegen/stdlib/fmt.js +104 -0
- package/src/codegen/stdlib/gom.js +155 -0
- package/src/codegen/stdlib/html.js +20 -0
- package/src/codegen/stdlib/index.js +98 -0
- package/src/codegen/stdlib/io.js +36 -0
- package/src/codegen/stdlib/maps.js +45 -0
- package/src/codegen/stdlib/math.js +56 -0
- package/src/codegen/stdlib/os.js +13 -0
- package/src/codegen/stdlib/path.js +34 -0
- package/src/codegen/stdlib/rand.js +27 -0
- package/src/codegen/stdlib/regexp.js +54 -0
- package/src/codegen/stdlib/slices.js +51 -0
- package/src/codegen/stdlib/sort.js +31 -0
- package/src/codegen/stdlib/strconv.js +33 -0
- package/src/codegen/stdlib/strings.js +86 -0
- package/src/codegen/stdlib/time.js +57 -0
- package/src/codegen/stdlib/unicode.js +25 -0
- package/src/codegen/stdlib/utf8.js +26 -0
- package/src/codegen/templ.js +284 -0
- package/src/compiler.js +142 -77
- package/src/dts-parser.js +355 -371
- package/src/index.js +32 -162
- package/src/lexer.js +186 -272
- package/src/minifier.js +305 -318
- package/src/{parser.js → parser/declarations.js} +18 -175
- package/src/parser/expressions.js +51 -34
- package/src/parser/index.js +161 -0
- package/src/parser/statements.js +24 -34
- package/src/parser/types.js +31 -29
- package/src/templ-parser.js +2 -2
- package/src/tokens.js +109 -0
- package/src/typechecker/assignability.js +261 -0
- package/src/typechecker/expressions.js +568 -549
- package/src/typechecker/index.js +556 -0
- package/src/typechecker/resolve.js +428 -0
- package/src/typechecker/statements.js +270 -280
- package/src/typechecker/stdlib/core.js +592 -0
- package/src/typechecker/stdlib/extended.js +332 -0
- package/src/typechecker/stdlib.js +9 -1060
- package/src/typechecker/termination.js +86 -0
- package/src/typechecker/types.js +1 -0
- package/src/codegen.js +0 -882
- package/src/typechecker.js +0 -1299
package/src/typechecker.js
DELETED
|
@@ -1,1299 +0,0 @@
|
|
|
1
|
-
// Type checker: walks the AST, resolves types for all expressions,
|
|
2
|
-
// and reports type errors at compile time.
|
|
3
|
-
//
|
|
4
|
-
// Split into sub-modules under typechecker/:
|
|
5
|
-
// types.js — shared type constants, predicates, Scope
|
|
6
|
-
// stdlib.js — browser globals + all built-in package registrations
|
|
7
|
-
// statements.js — checkBlock, checkStmt
|
|
8
|
-
// expressions.js — checkExpr, checkCall, checkBuiltin, checkCompositeLit
|
|
9
|
-
|
|
10
|
-
import { expressionCheckMethods } from "./typechecker/expressions.js";
|
|
11
|
-
import { statementCheckMethods } from "./typechecker/statements.js";
|
|
12
|
-
import { setupGlobals } from "./typechecker/stdlib.js";
|
|
13
|
-
import {
|
|
14
|
-
ANY,
|
|
15
|
-
BASIC_TYPES,
|
|
16
|
-
BOOL,
|
|
17
|
-
CMP_OPS,
|
|
18
|
-
COMPLEX128,
|
|
19
|
-
defaultType,
|
|
20
|
-
ERROR,
|
|
21
|
-
FLOAT64,
|
|
22
|
-
INT,
|
|
23
|
-
isAny,
|
|
24
|
-
isComplex,
|
|
25
|
-
isComplexOrNumeric,
|
|
26
|
-
isNil,
|
|
27
|
-
isNumeric,
|
|
28
|
-
isPointer,
|
|
29
|
-
isString,
|
|
30
|
-
isUntyped,
|
|
31
|
-
isVoid,
|
|
32
|
-
LOG_OPS,
|
|
33
|
-
Scope,
|
|
34
|
-
STRING,
|
|
35
|
-
TypeCheckError,
|
|
36
|
-
typeStr,
|
|
37
|
-
UNTYPED_COMPLEX,
|
|
38
|
-
UNTYPED_FLOAT,
|
|
39
|
-
UNTYPED_INT,
|
|
40
|
-
VOID,
|
|
41
|
-
} from "./typechecker/types.js";
|
|
42
|
-
|
|
43
|
-
// Re-export for consumers that import from typechecker.js
|
|
44
|
-
export { TypeCheckError, typeStr };
|
|
45
|
-
|
|
46
|
-
export class TypeChecker {
|
|
47
|
-
constructor() {
|
|
48
|
-
this.types = new Map(); // named types
|
|
49
|
-
this.globals = new Scope();
|
|
50
|
-
this.errors = [];
|
|
51
|
-
this._currentFile = null; // filename of file currently being checked
|
|
52
|
-
this._currentSource = null;
|
|
53
|
-
this._loopDepth = 0; // for break/continue validation
|
|
54
|
-
this._switchDepth = 0; // for break/fallthrough validation
|
|
55
|
-
this._typeSwitchDepth = 0; // for rejecting fallthrough in type switch
|
|
56
|
-
this._deferCount = 0; // tracks defer usage in current function body
|
|
57
|
-
this._imports = []; // tracked imports for unused-import detection
|
|
58
|
-
this._setupGlobals();
|
|
59
|
-
// Register error as a named type (interface)
|
|
60
|
-
this.types.set("error", ERROR);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
_setupGlobals() {
|
|
64
|
-
setupGlobals(this.globals, this.types);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
addDefinitions(types, values) {
|
|
68
|
-
for (const [name, type] of types) {
|
|
69
|
-
this.types.set(name, type);
|
|
70
|
-
}
|
|
71
|
-
for (const [name, type] of values) {
|
|
72
|
-
this.globals.define(name, type);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Add an imported GoFront package as a qualified namespace.
|
|
77
|
-
// e.g. addPackageNamespace('utils', symbolsMap, typesMap)
|
|
78
|
-
// lets callers type-check `utils.Foo` via SelectorExpr.
|
|
79
|
-
addPackageNamespace(pkgName, symbols, types) {
|
|
80
|
-
const members = {};
|
|
81
|
-
for (const [name, type] of symbols) members[name] = type;
|
|
82
|
-
// _gofront: true marks this as a GoFront package — exported identifier rules apply
|
|
83
|
-
this.globals.define(pkgName, {
|
|
84
|
-
kind: "namespace",
|
|
85
|
-
name: pkgName,
|
|
86
|
-
members,
|
|
87
|
-
_gofront: true,
|
|
88
|
-
});
|
|
89
|
-
for (const [name, type] of types) {
|
|
90
|
-
this.types.set(name, type);
|
|
91
|
-
this.types.set(`${pkgName}.${name}`, type);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Register an import for unused-import detection.
|
|
96
|
-
// `name` is the package name or alias used in code.
|
|
97
|
-
// `node` carries _line for error reporting.
|
|
98
|
-
// `filename` / `source` identify the file containing the import.
|
|
99
|
-
trackImport(name, node, filename, source) {
|
|
100
|
-
this._imports.push({ name, node, filename, source });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Report any tracked imports whose package name was never referenced.
|
|
104
|
-
reportUnusedImports() {
|
|
105
|
-
for (const { name, node, filename, source } of this._imports) {
|
|
106
|
-
if (!this.globals._used.has(name)) {
|
|
107
|
-
const saved = [this._currentFile, this._currentSource];
|
|
108
|
-
this._currentFile = filename;
|
|
109
|
-
this._currentSource = source;
|
|
110
|
-
this.err(`'${name}' imported and not used`, node);
|
|
111
|
-
[this._currentFile, this._currentSource] = saved;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Return a snapshot of all user-defined globals (excludes built-ins).
|
|
117
|
-
// Used by the compiler to expose a package's exports to importers.
|
|
118
|
-
getExportedSymbols() {
|
|
119
|
-
const out = new Map();
|
|
120
|
-
for (const [name, type] of this.globals.symbols) {
|
|
121
|
-
if (type?.kind === "builtin") continue; // skip built-ins
|
|
122
|
-
if (type === ANY && !name[0].match(/[A-Z]/)) continue; // skip browser globals
|
|
123
|
-
out.set(name, type);
|
|
124
|
-
}
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
getExportedTypes() {
|
|
129
|
-
return new Map(this.types);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
err(msg, node) {
|
|
133
|
-
const e = new TypeCheckError(
|
|
134
|
-
msg,
|
|
135
|
-
node,
|
|
136
|
-
this._currentFile,
|
|
137
|
-
this._currentSource,
|
|
138
|
-
);
|
|
139
|
-
this.errors.push(e);
|
|
140
|
-
return ANY; // recovery type
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
_reportUnused(scope, node) {
|
|
144
|
-
for (const name of scope.unusedLocals()) {
|
|
145
|
-
this.err(`'${name}' declared and not used`, node);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
check(program) {
|
|
150
|
-
this._currentFile = program._filename ?? null;
|
|
151
|
-
this._currentSource = program._source ?? null;
|
|
152
|
-
// Pass 1: collect type declarations
|
|
153
|
-
for (const decl of program.decls) {
|
|
154
|
-
if (decl.kind === "TypeDecl") this.collectType(decl);
|
|
155
|
-
}
|
|
156
|
-
// Pass 2: collect function / method signatures
|
|
157
|
-
for (const decl of program.decls) {
|
|
158
|
-
if (decl.kind === "FuncDecl" || decl.kind === "MethodDecl") {
|
|
159
|
-
this.collectFunc(decl);
|
|
160
|
-
} else if (decl.kind === "TemplDecl") {
|
|
161
|
-
this._collectTemplDecl(decl);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Pass 2.1: Promote embedded methods
|
|
165
|
-
this._promoteEmbeddedMethods();
|
|
166
|
-
// Pass 3: check bodies
|
|
167
|
-
for (const decl of program.decls) {
|
|
168
|
-
this.checkTopDecl(decl, this.globals);
|
|
169
|
-
}
|
|
170
|
-
return this.errors;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Pre-declare a package-level var so other files can reference its name
|
|
174
|
-
// before we've type-checked its initializer. Called in pass 2.5.
|
|
175
|
-
collectVar(decl) {
|
|
176
|
-
for (const spec of decl.decls) {
|
|
177
|
-
const type = spec.type
|
|
178
|
-
? this.resolveTypeNode(spec.type, this.globals)
|
|
179
|
-
: ANY;
|
|
180
|
-
for (const name of spec.names) this.globals.define(name, type);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
collectConst(decl) {
|
|
185
|
-
for (const spec of decl.decls) {
|
|
186
|
-
const type = spec.type
|
|
187
|
-
? this.resolveTypeNode(spec.type, this.globals)
|
|
188
|
-
: ANY;
|
|
189
|
-
for (const name of spec.names) this.globals.defineConst(name, type);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Like check() but operates over multiple programs (same-package multi-file).
|
|
194
|
-
// All passes run across all files before moving to the next pass,
|
|
195
|
-
// so every file sees every other file's declarations.
|
|
196
|
-
checkAll(programs) {
|
|
197
|
-
// Pass 1: collect type declarations
|
|
198
|
-
for (const p of programs) {
|
|
199
|
-
this._currentFile = p._filename ?? null;
|
|
200
|
-
this._currentSource = p._source ?? null;
|
|
201
|
-
for (const d of p.decls) if (d.kind === "TypeDecl") this.collectType(d);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Pass 2: collect function / method signatures
|
|
205
|
-
for (const p of programs) {
|
|
206
|
-
this._currentFile = p._filename ?? null;
|
|
207
|
-
this._currentSource = p._source ?? null;
|
|
208
|
-
for (const d of p.decls) {
|
|
209
|
-
if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
|
|
210
|
-
this.collectFunc(d);
|
|
211
|
-
else if (d.kind === "TemplDecl") this._collectTemplDecl(d);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Pass 2.1: Promote embedded methods
|
|
216
|
-
this._promoteEmbeddedMethods();
|
|
217
|
-
|
|
218
|
-
// Pass 2.5: pre-declare package-level vars and consts
|
|
219
|
-
// This allows any file's function body to reference vars from other files.
|
|
220
|
-
for (const p of programs) {
|
|
221
|
-
this._currentFile = p._filename ?? null;
|
|
222
|
-
this._currentSource = p._source ?? null;
|
|
223
|
-
for (const d of p.decls) {
|
|
224
|
-
if (d.kind === "VarDecl") this.collectVar(d);
|
|
225
|
-
if (d.kind === "ConstDecl") this.collectConst(d);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Pass 3: check bodies (re-checks var/const decls with proper types)
|
|
230
|
-
for (const p of programs) {
|
|
231
|
-
this._currentFile = p._filename ?? null;
|
|
232
|
-
this._currentSource = p._source ?? null;
|
|
233
|
-
for (const d of p.decls) this.checkTopDecl(d, this.globals);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return this.errors;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ── Shared pass helpers ──────────────────────────────────────
|
|
240
|
-
|
|
241
|
-
_promoteEmbeddedMethods() {
|
|
242
|
-
for (const type of this.types.values()) {
|
|
243
|
-
const struct = type.underlying;
|
|
244
|
-
if (struct?.kind === "struct" && struct._embeds) {
|
|
245
|
-
for (const embed of struct._embeds) {
|
|
246
|
-
const base = embed.kind === "named" ? embed.underlying : embed;
|
|
247
|
-
if (base?.kind !== "struct" || !base.methods) continue;
|
|
248
|
-
for (const [mName, mType] of base.methods.entries()) {
|
|
249
|
-
if (!struct.methods.has(mName)) struct.methods.set(mName, mType);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ── Type collection ──────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
collectType(decl) {
|
|
259
|
-
if (decl.typeParams) {
|
|
260
|
-
// Generic type — resolve underlying with type params as ANY for struct fields
|
|
261
|
-
const typeScope = new Scope(this.globals);
|
|
262
|
-
const typeParamTypes = decl.typeParams.map((tp) => {
|
|
263
|
-
const t = {
|
|
264
|
-
kind: "typeParam",
|
|
265
|
-
name: tp.name,
|
|
266
|
-
constraint: tp.constraint,
|
|
267
|
-
};
|
|
268
|
-
typeScope.define(tp.name, t);
|
|
269
|
-
return t;
|
|
270
|
-
});
|
|
271
|
-
const underlying = this.resolveTypeNode(decl.type, typeScope);
|
|
272
|
-
const named = {
|
|
273
|
-
kind: "named",
|
|
274
|
-
name: decl.name,
|
|
275
|
-
underlying,
|
|
276
|
-
_generic: {
|
|
277
|
-
typeParams: typeParamTypes,
|
|
278
|
-
declNode: decl,
|
|
279
|
-
},
|
|
280
|
-
};
|
|
281
|
-
if (underlying.kind === "struct") {
|
|
282
|
-
underlying.name = decl.name;
|
|
283
|
-
underlying.methods = new Map();
|
|
284
|
-
}
|
|
285
|
-
this.types.set(decl.name, named);
|
|
286
|
-
this.globals.define(decl.name, named);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const underlying = this.resolveTypeNode(decl.type, this.globals);
|
|
290
|
-
if (decl.isAlias) {
|
|
291
|
-
// type A = B — transparent alias, A and B are identical types
|
|
292
|
-
this.types.set(decl.name, underlying);
|
|
293
|
-
this.globals.define(decl.name, underlying);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const named = { kind: "named", name: decl.name, underlying };
|
|
297
|
-
if (underlying.kind === "struct") {
|
|
298
|
-
underlying.name = decl.name;
|
|
299
|
-
underlying.methods = new Map();
|
|
300
|
-
} else if (underlying.kind !== "interface") {
|
|
301
|
-
named.methods = new Map();
|
|
302
|
-
}
|
|
303
|
-
if (underlying.kind === "interface") {
|
|
304
|
-
underlying.name = decl.name;
|
|
305
|
-
}
|
|
306
|
-
this.types.set(decl.name, named);
|
|
307
|
-
this.globals.define(decl.name, named);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
collectFunc(decl) {
|
|
311
|
-
// For generic functions, create type param scope for resolving param/return types
|
|
312
|
-
let resolveScope = this.globals;
|
|
313
|
-
let typeParamTypes = null;
|
|
314
|
-
if (decl.typeParams) {
|
|
315
|
-
resolveScope = new Scope(this.globals);
|
|
316
|
-
typeParamTypes = decl.typeParams.map((tp) => {
|
|
317
|
-
const t = {
|
|
318
|
-
kind: "typeParam",
|
|
319
|
-
name: tp.name,
|
|
320
|
-
constraint: tp.constraint,
|
|
321
|
-
};
|
|
322
|
-
resolveScope.define(tp.name, t);
|
|
323
|
-
return t;
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
// For methods on generic types, inject the receiver's type params
|
|
327
|
-
if (decl.kind === "MethodDecl") {
|
|
328
|
-
const recvTypeName =
|
|
329
|
-
decl.recvType.kind === "GenericTypeName"
|
|
330
|
-
? decl.recvType.name
|
|
331
|
-
: decl.recvType.name;
|
|
332
|
-
const recvNamedType = this.types.get(recvTypeName);
|
|
333
|
-
if (recvNamedType?._generic) {
|
|
334
|
-
resolveScope = new Scope(this.globals);
|
|
335
|
-
for (const tp of recvNamedType._generic.typeParams) {
|
|
336
|
-
resolveScope.define(tp.name, tp);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
const paramTypes = decl.params.map((p) =>
|
|
341
|
-
this.resolveTypeNode(p.type, resolveScope),
|
|
342
|
-
);
|
|
343
|
-
const returnType = decl.returnType
|
|
344
|
-
? this.resolveTypeNode(decl.returnType, resolveScope)
|
|
345
|
-
: VOID;
|
|
346
|
-
const isVariadic =
|
|
347
|
-
decl.params.length > 0 && decl.params[decl.params.length - 1].variadic;
|
|
348
|
-
const funcType = {
|
|
349
|
-
kind: "func",
|
|
350
|
-
params: paramTypes,
|
|
351
|
-
returns: [returnType],
|
|
352
|
-
variadic: isVariadic,
|
|
353
|
-
async: decl.async ?? false,
|
|
354
|
-
};
|
|
355
|
-
if (decl.kind === "FuncDecl") {
|
|
356
|
-
if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
|
|
357
|
-
this.err(`${decl.name} redeclared in this block`, decl);
|
|
358
|
-
}
|
|
359
|
-
if (typeParamTypes) {
|
|
360
|
-
this.globals.define(decl.name, {
|
|
361
|
-
kind: "generic",
|
|
362
|
-
name: decl.name,
|
|
363
|
-
typeParams: typeParamTypes,
|
|
364
|
-
underlying: funcType,
|
|
365
|
-
});
|
|
366
|
-
} else {
|
|
367
|
-
this.globals.define(decl.name, funcType);
|
|
368
|
-
}
|
|
369
|
-
} else {
|
|
370
|
-
// Method: attach to struct or named non-struct type
|
|
371
|
-
const recvTypeName =
|
|
372
|
-
decl.recvType.kind === "GenericTypeName"
|
|
373
|
-
? decl.recvType.name
|
|
374
|
-
: decl.recvType.name;
|
|
375
|
-
const recvNamedType = this.types.get(recvTypeName);
|
|
376
|
-
if (recvNamedType?._generic) {
|
|
377
|
-
// Store method on the generic metadata and on the underlying struct
|
|
378
|
-
if (!recvNamedType._generic.methods)
|
|
379
|
-
recvNamedType._generic.methods = new Map();
|
|
380
|
-
recvNamedType._generic.methods.set(decl.name, funcType);
|
|
381
|
-
const base = recvNamedType.underlying;
|
|
382
|
-
if (base?.kind === "struct") {
|
|
383
|
-
base.methods.set(decl.name, funcType);
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
const recvType = this.resolveTypeNodeName(decl.recvType, this.globals);
|
|
387
|
-
const base = recvType?.underlying ?? recvType;
|
|
388
|
-
if (base?.kind === "struct") {
|
|
389
|
-
base.methods.set(decl.name, funcType);
|
|
390
|
-
} else if (recvType?.kind === "named" && recvType.methods) {
|
|
391
|
-
recvType.methods.set(decl.name, funcType);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── Top-level checker ────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
checkTopDecl(decl, scope) {
|
|
400
|
-
switch (decl.kind) {
|
|
401
|
-
case "FuncDecl":
|
|
402
|
-
this.checkFuncDecl(decl, scope);
|
|
403
|
-
break;
|
|
404
|
-
case "MethodDecl":
|
|
405
|
-
this.checkMethodDecl(decl, scope);
|
|
406
|
-
break;
|
|
407
|
-
case "VarDecl":
|
|
408
|
-
this.checkVarDecl(decl, scope);
|
|
409
|
-
break;
|
|
410
|
-
case "ConstDecl":
|
|
411
|
-
this.checkConstDecl(decl, scope);
|
|
412
|
-
break;
|
|
413
|
-
case "TypeDecl":
|
|
414
|
-
case "TemplDecl":
|
|
415
|
-
break; // already collected / no body to check
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
_collectTemplDecl(decl) {
|
|
420
|
-
const paramTypes = decl.params.map((p) =>
|
|
421
|
-
this.resolveTypeNode(p.type, this.globals),
|
|
422
|
-
);
|
|
423
|
-
const gomNodeType = this.types.get("gom.Node") ?? {
|
|
424
|
-
kind: "basic",
|
|
425
|
-
name: "any",
|
|
426
|
-
};
|
|
427
|
-
const isVariadic =
|
|
428
|
-
decl.params.length > 0 && decl.params[decl.params.length - 1].variadic;
|
|
429
|
-
const funcType = {
|
|
430
|
-
kind: "func",
|
|
431
|
-
params: paramTypes,
|
|
432
|
-
returns: [gomNodeType],
|
|
433
|
-
variadic: isVariadic,
|
|
434
|
-
async: false,
|
|
435
|
-
};
|
|
436
|
-
if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
|
|
437
|
-
this.err(`${decl.name} redeclared in this block`, decl);
|
|
438
|
-
}
|
|
439
|
-
this.globals.define(decl.name, funcType);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
checkFuncDecl(decl, outer) {
|
|
443
|
-
const inner = new Scope(outer);
|
|
444
|
-
// Inject type params into scope for generic functions
|
|
445
|
-
if (decl.typeParams) {
|
|
446
|
-
for (const tp of decl.typeParams) {
|
|
447
|
-
const constraint = tp.constraint
|
|
448
|
-
? this.resolveTypeNode(tp.constraint, outer)
|
|
449
|
-
: ANY;
|
|
450
|
-
inner.define(tp.name, { kind: "typeParam", name: tp.name, constraint });
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
for (const p of decl.params) {
|
|
454
|
-
inner.define(p.name, this.resolveTypeNode(p.type, inner));
|
|
455
|
-
}
|
|
456
|
-
let returnType = decl.returnType
|
|
457
|
-
? this.resolveTypeNode(decl.returnType, inner)
|
|
458
|
-
: VOID;
|
|
459
|
-
const hasNamedReturns = this._injectNamedReturns(
|
|
460
|
-
decl.returnType,
|
|
461
|
-
inner,
|
|
462
|
-
outer,
|
|
463
|
-
);
|
|
464
|
-
// Wrap the return type to avoid mutating the shared singleton
|
|
465
|
-
if (hasNamedReturns && returnType)
|
|
466
|
-
returnType = { ...returnType, _hasNamedReturns: true };
|
|
467
|
-
const savedDefer = this._deferCount;
|
|
468
|
-
this._deferCount = 0;
|
|
469
|
-
this.checkBlock(decl.body, inner, returnType);
|
|
470
|
-
this._reportUnused(inner, decl);
|
|
471
|
-
if (this._deferCount > 0) decl.body._hasDefer = true;
|
|
472
|
-
this._deferCount = savedDefer;
|
|
473
|
-
decl._returnType = returnType;
|
|
474
|
-
// Missing return check: non-void functions must terminate on all paths
|
|
475
|
-
if (!isVoid(returnType) && !returnType._hasNamedReturns) {
|
|
476
|
-
if (!this._isTerminating(decl.body)) {
|
|
477
|
-
this.err(`missing return at end of function '${decl.name}'`, decl);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
checkMethodDecl(decl, outer) {
|
|
483
|
-
const inner = new Scope(outer);
|
|
484
|
-
// For generic receiver types (e.g. Stack[T]), inject type params
|
|
485
|
-
const recvTypeName =
|
|
486
|
-
decl.recvType.kind === "GenericTypeName"
|
|
487
|
-
? decl.recvType.name
|
|
488
|
-
: decl.recvType.name;
|
|
489
|
-
const recvNamedType = this.types.get(recvTypeName);
|
|
490
|
-
if (recvNamedType?._generic) {
|
|
491
|
-
for (const tp of recvNamedType._generic.typeParams) {
|
|
492
|
-
inner.define(tp.name, tp);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const recvType = this.resolveTypeNodeName(decl.recvType, outer);
|
|
496
|
-
inner.define(decl.recvName, recvType);
|
|
497
|
-
for (const p of decl.params) {
|
|
498
|
-
inner.define(p.name, this.resolveTypeNode(p.type, inner));
|
|
499
|
-
}
|
|
500
|
-
let returnType = decl.returnType
|
|
501
|
-
? this.resolveTypeNode(decl.returnType, inner)
|
|
502
|
-
: VOID;
|
|
503
|
-
const hasNamedReturns = this._injectNamedReturns(
|
|
504
|
-
decl.returnType,
|
|
505
|
-
inner,
|
|
506
|
-
inner,
|
|
507
|
-
);
|
|
508
|
-
if (hasNamedReturns && returnType)
|
|
509
|
-
returnType = { ...returnType, _hasNamedReturns: true };
|
|
510
|
-
const savedDefer = this._deferCount;
|
|
511
|
-
this._deferCount = 0;
|
|
512
|
-
this.checkBlock(decl.body, inner, returnType);
|
|
513
|
-
this._reportUnused(inner, decl);
|
|
514
|
-
if (this._deferCount > 0) decl.body._hasDefer = true;
|
|
515
|
-
this._deferCount = savedDefer;
|
|
516
|
-
decl._returnType = returnType;
|
|
517
|
-
// Missing return check
|
|
518
|
-
if (!isVoid(returnType) && !returnType._hasNamedReturns) {
|
|
519
|
-
if (!this._isTerminating(decl.body)) {
|
|
520
|
-
this.err(`missing return at end of method '${decl.name}'`, decl);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Go spec §Terminating statements — returns true if the statement always terminates.
|
|
526
|
-
_isTerminating(stmt) {
|
|
527
|
-
if (!stmt) return false;
|
|
528
|
-
switch (stmt.kind) {
|
|
529
|
-
case "ReturnStmt":
|
|
530
|
-
return true;
|
|
531
|
-
case "BranchStmt":
|
|
532
|
-
// panic() is in ExprStmt, not BranchStmt — but "goto" could be here
|
|
533
|
-
return false;
|
|
534
|
-
case "ExprStmt":
|
|
535
|
-
// panic(...) is a terminating call
|
|
536
|
-
if (
|
|
537
|
-
stmt.expr?.kind === "CallExpr" &&
|
|
538
|
-
stmt.expr?.func?.kind === "Ident" &&
|
|
539
|
-
stmt.expr?.func?.name === "panic"
|
|
540
|
-
)
|
|
541
|
-
return true;
|
|
542
|
-
return false;
|
|
543
|
-
case "Block":
|
|
544
|
-
return this._isTerminatingBlock(stmt);
|
|
545
|
-
case "IfStmt":
|
|
546
|
-
// Terminating if: has else, and both branches terminate
|
|
547
|
-
if (!stmt.elseBody) return false;
|
|
548
|
-
return (
|
|
549
|
-
this._isTerminatingBlock(stmt.body) &&
|
|
550
|
-
(stmt.elseBody.kind === "Block"
|
|
551
|
-
? this._isTerminatingBlock(stmt.elseBody)
|
|
552
|
-
: this._isTerminating(stmt.elseBody))
|
|
553
|
-
);
|
|
554
|
-
case "ForStmt":
|
|
555
|
-
// Infinite loop (no condition) with no break — terminating
|
|
556
|
-
if (!stmt.cond && !this._blockHasBreak(stmt.body)) return true;
|
|
557
|
-
return false;
|
|
558
|
-
case "SwitchStmt":
|
|
559
|
-
// Terminating switch: has default and every case terminates
|
|
560
|
-
return this._isTerminatingSwitch(stmt);
|
|
561
|
-
case "TypeSwitchStmt":
|
|
562
|
-
return this._isTerminatingTypeSwitch(stmt);
|
|
563
|
-
case "LabeledStmt":
|
|
564
|
-
return this._isTerminating(stmt.body);
|
|
565
|
-
default:
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
_isTerminatingBlock(block) {
|
|
571
|
-
if (!block?.stmts?.length) return false;
|
|
572
|
-
return this._isTerminating(block.stmts[block.stmts.length - 1]);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
_blockHasBreak(block) {
|
|
576
|
-
if (!block?.stmts) return false;
|
|
577
|
-
for (const s of block.stmts) {
|
|
578
|
-
if (s.kind === "BranchStmt" && s.keyword === "break") return true;
|
|
579
|
-
}
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
_isTerminatingSwitch(stmt) {
|
|
584
|
-
let hasDefault = false;
|
|
585
|
-
for (const c of stmt.cases ?? []) {
|
|
586
|
-
if (!c.list) hasDefault = true;
|
|
587
|
-
if (!c.stmts?.length) return false;
|
|
588
|
-
const last = c.stmts[c.stmts.length - 1];
|
|
589
|
-
if (!this._isTerminating(last)) return false;
|
|
590
|
-
}
|
|
591
|
-
return hasDefault;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
_isTerminatingTypeSwitch(stmt) {
|
|
595
|
-
let hasDefault = false;
|
|
596
|
-
for (const c of stmt.cases ?? []) {
|
|
597
|
-
if (!c.types) hasDefault = true;
|
|
598
|
-
if (!c.stmts?.length) return false;
|
|
599
|
-
const last = c.stmts[c.stmts.length - 1];
|
|
600
|
-
if (!this._isTerminating(last)) return false;
|
|
601
|
-
}
|
|
602
|
-
return hasDefault;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
_injectNamedReturns(returnTypeNode, scope, outer) {
|
|
606
|
-
if (!returnTypeNode?._namedReturns) return false;
|
|
607
|
-
for (const { name, type } of returnTypeNode._namedReturns) {
|
|
608
|
-
if (name) scope.define(name, this.resolveTypeNode(type, outer));
|
|
609
|
-
}
|
|
610
|
-
return true;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
checkVarDecl(decl, scope) {
|
|
614
|
-
for (const spec of decl.decls) {
|
|
615
|
-
let type = spec.type ? this.resolveTypeNode(spec.type, scope) : null;
|
|
616
|
-
if (spec.value) {
|
|
617
|
-
const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
|
|
618
|
-
if (!type) type = defaultType(valTypes[0]) ?? ANY;
|
|
619
|
-
for (let i = 0; i < spec.names.length; i++) {
|
|
620
|
-
const vt = valTypes[i] ?? ANY;
|
|
621
|
-
this.assertAssignable(type, vt, spec.value[i]);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (!type) type = ANY;
|
|
625
|
-
for (const name of spec.names) scope.defineLocal(name, type);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
checkConstDecl(decl, scope) {
|
|
630
|
-
for (const spec of decl.decls) {
|
|
631
|
-
const valTypes = spec.value.map((v) => this.checkExpr(v, scope));
|
|
632
|
-
// Explicit type annotation → typed constant; otherwise preserve untyped
|
|
633
|
-
const type = spec.type
|
|
634
|
-
? this.resolveTypeNode(spec.type, scope)
|
|
635
|
-
: (valTypes[0] ?? ANY);
|
|
636
|
-
for (const name of spec.names) scope.defineConst(name, type);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// ── Statement checking ───────────────────────────────────────
|
|
641
|
-
|
|
642
|
-
// ── Type resolution ──────────────────────────────────────────
|
|
643
|
-
|
|
644
|
-
resolveTypeNode(node, scope) {
|
|
645
|
-
if (!node) return ANY;
|
|
646
|
-
switch (node.kind) {
|
|
647
|
-
case "TypeName": {
|
|
648
|
-
if (BASIC_TYPES[node.name]) return BASIC_TYPES[node.name];
|
|
649
|
-
if (node.name === "comparable")
|
|
650
|
-
return { kind: "basic", name: "comparable" };
|
|
651
|
-
// Check scope for type params before checking this.types
|
|
652
|
-
if (scope) {
|
|
653
|
-
const fromScope = scope.lookup(node.name);
|
|
654
|
-
if (fromScope?.kind === "typeParam") return fromScope;
|
|
655
|
-
}
|
|
656
|
-
const named = this.types.get(node.name);
|
|
657
|
-
if (named) return named;
|
|
658
|
-
return this.err(`Unknown type '${node.name}'`, node);
|
|
659
|
-
}
|
|
660
|
-
case "GenericTypeName": {
|
|
661
|
-
const base = this.types.get(node.name);
|
|
662
|
-
if (!base) return this.err(`Unknown type '${node.name}'`, node);
|
|
663
|
-
if (base.kind === "named" && base._generic) {
|
|
664
|
-
return this.instantiateGenericType(
|
|
665
|
-
base._generic,
|
|
666
|
-
node.typeArgs,
|
|
667
|
-
scope,
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
return this.err(`Type '${node.name}' is not generic`, node);
|
|
671
|
-
}
|
|
672
|
-
case "TypeParam": {
|
|
673
|
-
const constraint = node.constraint
|
|
674
|
-
? this.resolveTypeNode(node.constraint, scope)
|
|
675
|
-
: ANY;
|
|
676
|
-
return { kind: "typeParam", name: node.name, constraint };
|
|
677
|
-
}
|
|
678
|
-
case "UnionConstraint":
|
|
679
|
-
return node;
|
|
680
|
-
case "SliceType":
|
|
681
|
-
return {
|
|
682
|
-
kind: "slice",
|
|
683
|
-
elem: this.resolveTypeNode(node.elem, scope),
|
|
684
|
-
};
|
|
685
|
-
case "ArrayType":
|
|
686
|
-
return {
|
|
687
|
-
kind: "array",
|
|
688
|
-
// inferLen: true means [...]T — size is determined by composite literal
|
|
689
|
-
size: node.inferLen
|
|
690
|
-
? null
|
|
691
|
-
: node.size?.value !== undefined
|
|
692
|
-
? Number(node.size.value)
|
|
693
|
-
: node.size,
|
|
694
|
-
elem: this.resolveTypeNode(node.elem, scope),
|
|
695
|
-
};
|
|
696
|
-
case "MapType":
|
|
697
|
-
return {
|
|
698
|
-
kind: "map",
|
|
699
|
-
key: this.resolveTypeNode(node.key, scope),
|
|
700
|
-
value: this.resolveTypeNode(node.value, scope),
|
|
701
|
-
};
|
|
702
|
-
case "PointerType":
|
|
703
|
-
return {
|
|
704
|
-
kind: "pointer",
|
|
705
|
-
base: this.resolveTypeNode(node.base, scope),
|
|
706
|
-
};
|
|
707
|
-
case "FuncType": {
|
|
708
|
-
const params = node.params.map((p) =>
|
|
709
|
-
this.resolveTypeNode(p.type, scope),
|
|
710
|
-
);
|
|
711
|
-
const returns = node.returnType
|
|
712
|
-
? [this.resolveTypeNode(node.returnType, scope)]
|
|
713
|
-
: [VOID];
|
|
714
|
-
return { kind: "func", params, returns };
|
|
715
|
-
}
|
|
716
|
-
case "StructType": {
|
|
717
|
-
const fields = new Map();
|
|
718
|
-
const embeds = []; // track embedded types to merge methods later
|
|
719
|
-
for (const f of node.fields) {
|
|
720
|
-
const ft = this.resolveTypeNode(f.type, scope);
|
|
721
|
-
if (f.embedded) {
|
|
722
|
-
// Flatten embedded struct fields
|
|
723
|
-
const base = ft.kind === "named" ? ft.underlying : ft;
|
|
724
|
-
if (base?.kind === "struct") {
|
|
725
|
-
for (const [k, v] of base.fields.entries()) fields.set(k, v);
|
|
726
|
-
}
|
|
727
|
-
embeds.push(ft);
|
|
728
|
-
} else {
|
|
729
|
-
for (const n of f.names) fields.set(n, ft);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const structType = {
|
|
733
|
-
kind: "struct",
|
|
734
|
-
fields,
|
|
735
|
-
methods: new Map(),
|
|
736
|
-
_embeds: embeds,
|
|
737
|
-
};
|
|
738
|
-
return structType;
|
|
739
|
-
}
|
|
740
|
-
case "InterfaceType": {
|
|
741
|
-
const methods = new Map();
|
|
742
|
-
for (const m of node.methods) {
|
|
743
|
-
const params = m.params.map((p) =>
|
|
744
|
-
this.resolveTypeNode(p.type, scope),
|
|
745
|
-
);
|
|
746
|
-
const returns = m.returnType
|
|
747
|
-
? [this.resolveTypeNode(m.returnType, scope)]
|
|
748
|
-
: [VOID];
|
|
749
|
-
const isVariadic =
|
|
750
|
-
m.params.length > 0 && m.params[m.params.length - 1].variadic;
|
|
751
|
-
const mType = { kind: "func", params, returns };
|
|
752
|
-
if (isVariadic) mType.variadic = true;
|
|
753
|
-
methods.set(m.name, mType);
|
|
754
|
-
}
|
|
755
|
-
// Flatten embedded interface methods
|
|
756
|
-
if (node.embeds) {
|
|
757
|
-
for (const embed of node.embeds) {
|
|
758
|
-
const resolved = this.resolveTypeNode(embed, scope);
|
|
759
|
-
const base =
|
|
760
|
-
resolved?.kind === "named" ? resolved.underlying : resolved;
|
|
761
|
-
if (base?.kind === "interface") {
|
|
762
|
-
for (const [mName, mType] of base.methods) {
|
|
763
|
-
if (!methods.has(mName)) methods.set(mName, mType);
|
|
764
|
-
}
|
|
765
|
-
} else if (!isAny(resolved)) {
|
|
766
|
-
this.err(
|
|
767
|
-
`cannot embed non-interface type ${typeStr(resolved)}`,
|
|
768
|
-
embed,
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
return { kind: "interface", methods };
|
|
774
|
-
}
|
|
775
|
-
case "TupleType": {
|
|
776
|
-
return {
|
|
777
|
-
kind: "tuple",
|
|
778
|
-
types: node.types.map((t) => this.resolveTypeNode(t, scope)),
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
// Expression used as type (e.g. in make/new calls)
|
|
782
|
-
case "Ident":
|
|
783
|
-
return this.resolveTypeNode(
|
|
784
|
-
{ kind: "TypeName", name: node.name },
|
|
785
|
-
scope,
|
|
786
|
-
);
|
|
787
|
-
default:
|
|
788
|
-
return ANY;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
resolveTypeNodeName(node, scope) {
|
|
793
|
-
if (!node) return ANY;
|
|
794
|
-
if (node.kind === "TypeName") {
|
|
795
|
-
const named = this.types.get(node.name);
|
|
796
|
-
if (named) return named;
|
|
797
|
-
}
|
|
798
|
-
if (node.kind === "GenericTypeName") {
|
|
799
|
-
const named = this.types.get(node.name);
|
|
800
|
-
if (named) return named;
|
|
801
|
-
}
|
|
802
|
-
return this.resolveTypeNode(node, scope);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// ── Helpers ───────────────────────────────────────────────────
|
|
806
|
-
|
|
807
|
-
// ── Generics — instantiation, inference, substitution ────────
|
|
808
|
-
|
|
809
|
-
substituteType(type, map) {
|
|
810
|
-
if (!type) return type;
|
|
811
|
-
if (type.kind === "typeParam") {
|
|
812
|
-
const bound = map.get(type.name);
|
|
813
|
-
return bound ?? type;
|
|
814
|
-
}
|
|
815
|
-
switch (type.kind) {
|
|
816
|
-
case "basic":
|
|
817
|
-
case "untyped":
|
|
818
|
-
return type;
|
|
819
|
-
case "slice":
|
|
820
|
-
return { kind: "slice", elem: this.substituteType(type.elem, map) };
|
|
821
|
-
case "array":
|
|
822
|
-
return {
|
|
823
|
-
kind: "array",
|
|
824
|
-
size: type.size,
|
|
825
|
-
elem: this.substituteType(type.elem, map),
|
|
826
|
-
};
|
|
827
|
-
case "map":
|
|
828
|
-
return {
|
|
829
|
-
kind: "map",
|
|
830
|
-
key: this.substituteType(type.key, map),
|
|
831
|
-
value: this.substituteType(type.value, map),
|
|
832
|
-
};
|
|
833
|
-
case "func": {
|
|
834
|
-
return {
|
|
835
|
-
kind: "func",
|
|
836
|
-
params: type.params.map((p) => this.substituteType(p, map)),
|
|
837
|
-
returns: type.returns.map((r) => this.substituteType(r, map)),
|
|
838
|
-
variadic: type.variadic,
|
|
839
|
-
async: type.async,
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
case "struct": {
|
|
843
|
-
const fields = new Map();
|
|
844
|
-
for (const [k, v] of type.fields)
|
|
845
|
-
fields.set(k, this.substituteType(v, map));
|
|
846
|
-
const methods = new Map();
|
|
847
|
-
for (const [k, v] of type.methods)
|
|
848
|
-
methods.set(k, this.substituteType(v, map));
|
|
849
|
-
return {
|
|
850
|
-
kind: "struct",
|
|
851
|
-
name: type.name,
|
|
852
|
-
fields,
|
|
853
|
-
methods,
|
|
854
|
-
_embeds: type._embeds,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
case "interface": {
|
|
858
|
-
const methods = new Map();
|
|
859
|
-
for (const [k, v] of type.methods)
|
|
860
|
-
methods.set(k, this.substituteType(v, map));
|
|
861
|
-
return { kind: "interface", name: type.name, methods };
|
|
862
|
-
}
|
|
863
|
-
case "named":
|
|
864
|
-
return {
|
|
865
|
-
kind: "named",
|
|
866
|
-
name: type.name,
|
|
867
|
-
underlying: this.substituteType(type.underlying, map),
|
|
868
|
-
};
|
|
869
|
-
case "pointer":
|
|
870
|
-
return { kind: "pointer", base: this.substituteType(type.base, map) };
|
|
871
|
-
case "tuple":
|
|
872
|
-
return {
|
|
873
|
-
kind: "tuple",
|
|
874
|
-
types: type.types.map((t) => this.substituteType(t, map)),
|
|
875
|
-
};
|
|
876
|
-
default:
|
|
877
|
-
return type;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
instantiateGenericFunc(genericType, typeArgs) {
|
|
882
|
-
const map = new Map();
|
|
883
|
-
for (let i = 0; i < genericType.typeParams.length; i++) {
|
|
884
|
-
map.set(genericType.typeParams[i].name, typeArgs[i] ?? ANY);
|
|
885
|
-
}
|
|
886
|
-
return this.substituteType(genericType.underlying, map);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
instantiateGenericType(generic, typeArgNodes, scope) {
|
|
890
|
-
const map = new Map();
|
|
891
|
-
const typeArgs = typeArgNodes.map((n) => this.resolveTypeNode(n, scope));
|
|
892
|
-
for (let i = 0; i < generic.typeParams.length; i++) {
|
|
893
|
-
map.set(generic.typeParams[i].name, typeArgs[i] ?? ANY);
|
|
894
|
-
}
|
|
895
|
-
// Resolve the underlying type from the decl node using a scope with type params bound
|
|
896
|
-
const typeScope = new Scope(this.globals);
|
|
897
|
-
for (const [name, type] of map) typeScope.define(name, type);
|
|
898
|
-
const underlying = this.resolveTypeNode(generic.declNode.type, typeScope);
|
|
899
|
-
const instantiated = this.substituteType(underlying, map);
|
|
900
|
-
if (instantiated.kind === "struct") {
|
|
901
|
-
instantiated.name = generic.declNode.name;
|
|
902
|
-
if (!instantiated.methods) instantiated.methods = new Map();
|
|
903
|
-
// Copy and substitute methods from the generic
|
|
904
|
-
if (generic.methods) {
|
|
905
|
-
for (const [mName, mType] of generic.methods) {
|
|
906
|
-
instantiated.methods.set(mName, this.substituteType(mType, map));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
return {
|
|
911
|
-
kind: "named",
|
|
912
|
-
name: generic.declNode.name,
|
|
913
|
-
underlying: instantiated,
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
inferTypeArgs(genericType, argTypes) {
|
|
918
|
-
const map = new Map();
|
|
919
|
-
const params = genericType.underlying.params;
|
|
920
|
-
for (let i = 0; i < params.length && i < argTypes.length; i++) {
|
|
921
|
-
this._inferFromPair(params[i], argTypes[i], map);
|
|
922
|
-
}
|
|
923
|
-
// Check all type params are resolved
|
|
924
|
-
for (const tp of genericType.typeParams) {
|
|
925
|
-
if (!map.has(tp.name)) return null;
|
|
926
|
-
}
|
|
927
|
-
// Apply defaultType to resolve untyped constants
|
|
928
|
-
const result = [];
|
|
929
|
-
for (const tp of genericType.typeParams) {
|
|
930
|
-
result.push(defaultType(map.get(tp.name)));
|
|
931
|
-
}
|
|
932
|
-
return result;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
_inferFromPair(paramType, argType, map) {
|
|
936
|
-
if (!paramType || !argType) return;
|
|
937
|
-
if (paramType.kind === "typeParam") {
|
|
938
|
-
if (!map.has(paramType.name)) {
|
|
939
|
-
map.set(paramType.name, argType);
|
|
940
|
-
}
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
if (paramType.kind === "slice" && argType.kind === "slice") {
|
|
944
|
-
this._inferFromPair(paramType.elem, argType.elem, map);
|
|
945
|
-
}
|
|
946
|
-
if (paramType.kind === "map" && argType.kind === "map") {
|
|
947
|
-
this._inferFromPair(paramType.key, argType.key, map);
|
|
948
|
-
this._inferFromPair(paramType.value, argType.value, map);
|
|
949
|
-
}
|
|
950
|
-
if (paramType.kind === "func" && argType.kind === "func") {
|
|
951
|
-
for (
|
|
952
|
-
let i = 0;
|
|
953
|
-
i < paramType.params.length && i < argType.params.length;
|
|
954
|
-
i++
|
|
955
|
-
) {
|
|
956
|
-
this._inferFromPair(paramType.params[i], argType.params[i], map);
|
|
957
|
-
}
|
|
958
|
-
for (
|
|
959
|
-
let i = 0;
|
|
960
|
-
i < paramType.returns.length && i < argType.returns.length;
|
|
961
|
-
i++
|
|
962
|
-
) {
|
|
963
|
-
this._inferFromPair(paramType.returns[i], argType.returns[i], map);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
checkConstraint(typeArg, constraint, node) {
|
|
969
|
-
if (!constraint) return;
|
|
970
|
-
if (isAny(constraint)) return;
|
|
971
|
-
if (constraint.kind === "basic" && constraint.name === "comparable") return;
|
|
972
|
-
if (constraint.kind === "UnionConstraint") {
|
|
973
|
-
// Check if typeArg matches any term; ~terms always pass (v1)
|
|
974
|
-
for (const term of constraint.terms) {
|
|
975
|
-
if (term.approx) return; // ~ means approximate, always passes
|
|
976
|
-
const termType = this.resolveTypeNode(term.type, this.globals);
|
|
977
|
-
if (typeStr(typeArg) === typeStr(termType)) return;
|
|
978
|
-
}
|
|
979
|
-
this.err(`type ${typeStr(typeArg)} does not satisfy constraint`, node);
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
// Interface constraint — unwrap named types
|
|
983
|
-
const base =
|
|
984
|
-
constraint.kind === "named" ? constraint.underlying : constraint;
|
|
985
|
-
if (base?.kind === "interface") {
|
|
986
|
-
if (base.methods.size === 0 && !base.unionConstraint) return;
|
|
987
|
-
if (base.unionConstraint) {
|
|
988
|
-
return this.checkConstraint(typeArg, base.unionConstraint, node);
|
|
989
|
-
}
|
|
990
|
-
if (!this.implements(typeArg, base, node)) {
|
|
991
|
-
this.err(
|
|
992
|
-
`type ${typeStr(typeArg)} does not satisfy constraint ${typeStr(constraint)}`,
|
|
993
|
-
node,
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
resolveType(t) {
|
|
1000
|
-
if (!t) return ANY;
|
|
1001
|
-
if (t.kind === "basic" && t.alias) {
|
|
1002
|
-
const found = this.types.get(t.alias);
|
|
1003
|
-
if (found) return found;
|
|
1004
|
-
}
|
|
1005
|
-
return t;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
fieldType(baseType, field, node) {
|
|
1009
|
-
baseType = this.resolveType(baseType);
|
|
1010
|
-
if (!baseType || isAny(baseType)) return ANY;
|
|
1011
|
-
// Transparent pointer dereference for named-type method calls: (*T).Method works like T.Method
|
|
1012
|
-
// (only unwrap when the pointer's base is a named type that has methods)
|
|
1013
|
-
if (
|
|
1014
|
-
baseType.kind === "pointer" &&
|
|
1015
|
-
field !== "value" &&
|
|
1016
|
-
baseType.base?.kind === "named"
|
|
1017
|
-
) {
|
|
1018
|
-
baseType = this.resolveType(baseType.base);
|
|
1019
|
-
}
|
|
1020
|
-
// Check methods on named type itself (for non-struct named types)
|
|
1021
|
-
if (baseType.kind === "named" && baseType.methods?.has(field)) {
|
|
1022
|
-
return baseType.methods.get(field);
|
|
1023
|
-
}
|
|
1024
|
-
let base = baseType.kind === "named" ? baseType.underlying : baseType;
|
|
1025
|
-
base = this.resolveType(base);
|
|
1026
|
-
if (base?.kind === "struct") {
|
|
1027
|
-
if (base.fields?.has(field)) return base.fields.get(field);
|
|
1028
|
-
if (base.methods?.has(field)) return base.methods.get(field);
|
|
1029
|
-
return this.err(`No field '${field}' on ${typeStr(baseType)}`, node);
|
|
1030
|
-
}
|
|
1031
|
-
if (base?.kind === "interface") {
|
|
1032
|
-
if (base.methods?.has(field)) return base.methods.get(field);
|
|
1033
|
-
return this.err(`No method '${field}' on ${typeStr(baseType)}`, node);
|
|
1034
|
-
}
|
|
1035
|
-
if (base?.kind === "namespace") {
|
|
1036
|
-
// Enforce exported identifier rule for GoFront packages only
|
|
1037
|
-
if (
|
|
1038
|
-
base._gofront &&
|
|
1039
|
-
field.length > 0 &&
|
|
1040
|
-
field[0] >= "a" &&
|
|
1041
|
-
field[0] <= "z"
|
|
1042
|
-
) {
|
|
1043
|
-
return this.err(
|
|
1044
|
-
`cannot refer to unexported name ${base.name}.${field}`,
|
|
1045
|
-
node,
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1048
|
-
if (field in base.members) return base.members[field];
|
|
1049
|
-
return this.err(`No member '${field}' in namespace ${base.name}`, node);
|
|
1050
|
-
}
|
|
1051
|
-
// For explicitly typed non-any values, report the bad access rather than
|
|
1052
|
-
// silently returning any — this catches typos on structs and primitive misuse.
|
|
1053
|
-
if (base && !isAny(base)) {
|
|
1054
|
-
// pointer.value is the GoFront new(T) pattern
|
|
1055
|
-
if (base.kind === "pointer" && field === "value") return base.base ?? ANY;
|
|
1056
|
-
const badKinds = ["basic", "slice", "array", "map", "func", "tuple"];
|
|
1057
|
-
if (badKinds.includes(base.kind)) {
|
|
1058
|
-
return this.err(
|
|
1059
|
-
`${typeStr(baseType ?? base)} has no field or method '${field}'`,
|
|
1060
|
-
node,
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return ANY; // unknown types (e.g. external .d.ts) — allow any access
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
binaryResultType(op, lt, rt, node) {
|
|
1068
|
-
// Type parameters are permissive — allow operations and return the typeParam
|
|
1069
|
-
if (lt?.kind === "typeParam" || rt?.kind === "typeParam") {
|
|
1070
|
-
if (CMP_OPS.has(op)) return BOOL;
|
|
1071
|
-
if (LOG_OPS.has(op)) return BOOL;
|
|
1072
|
-
return lt?.kind === "typeParam" ? lt : rt;
|
|
1073
|
-
}
|
|
1074
|
-
if (CMP_OPS.has(op)) {
|
|
1075
|
-
// Complex only supports == and !=
|
|
1076
|
-
if (isComplex(lt) || isComplex(rt)) {
|
|
1077
|
-
if (op !== "==" && op !== "!=") {
|
|
1078
|
-
this.err(
|
|
1079
|
-
`invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`,
|
|
1080
|
-
node,
|
|
1081
|
-
);
|
|
1082
|
-
return ANY;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Reject slice/map/func comparisons unless one side is nil (Go spec)
|
|
1086
|
-
if (op === "==" || op === "!=") {
|
|
1087
|
-
const lNil = isNil(lt);
|
|
1088
|
-
const rNil = isNil(rt);
|
|
1089
|
-
if (!lNil && !rNil) {
|
|
1090
|
-
for (const t of [lt, rt]) {
|
|
1091
|
-
const base = t?.kind === "named" ? t.underlying : t;
|
|
1092
|
-
if (
|
|
1093
|
-
base?.kind === "slice" ||
|
|
1094
|
-
base?.kind === "map" ||
|
|
1095
|
-
base?.kind === "func"
|
|
1096
|
-
) {
|
|
1097
|
-
this.err(`operator ${op} not defined on ${typeStr(t)}`, node);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return BOOL;
|
|
1103
|
-
}
|
|
1104
|
-
if (LOG_OPS.has(op)) return BOOL;
|
|
1105
|
-
if (isAny(lt) || isAny(rt)) return ANY;
|
|
1106
|
-
// Complex arithmetic: +, -, *, /
|
|
1107
|
-
if (isComplex(lt) || isComplex(rt)) {
|
|
1108
|
-
if (!isComplexOrNumeric(lt) || !isComplexOrNumeric(rt)) {
|
|
1109
|
-
this.err(
|
|
1110
|
-
`invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`,
|
|
1111
|
-
node,
|
|
1112
|
-
);
|
|
1113
|
-
return ANY;
|
|
1114
|
-
}
|
|
1115
|
-
if (op !== "+" && op !== "-" && op !== "*" && op !== "/") {
|
|
1116
|
-
this.err(
|
|
1117
|
-
`invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`,
|
|
1118
|
-
node,
|
|
1119
|
-
);
|
|
1120
|
-
return ANY;
|
|
1121
|
-
}
|
|
1122
|
-
if (lt.kind === "untyped" && rt.kind === "untyped")
|
|
1123
|
-
return UNTYPED_COMPLEX;
|
|
1124
|
-
return COMPLEX128;
|
|
1125
|
-
}
|
|
1126
|
-
if (isNumeric(lt) && isNumeric(rt)) {
|
|
1127
|
-
const lFloat = (lt.kind === "untyped" ? lt.base : lt.name) === "float64";
|
|
1128
|
-
const rFloat = (rt.kind === "untyped" ? rt.base : rt.name) === "float64";
|
|
1129
|
-
const isFloat = lFloat || rFloat;
|
|
1130
|
-
// Both untyped → result is untyped
|
|
1131
|
-
if (lt.kind === "untyped" && rt.kind === "untyped")
|
|
1132
|
-
return isFloat ? UNTYPED_FLOAT : UNTYPED_INT;
|
|
1133
|
-
// One typed, one untyped → typed wins
|
|
1134
|
-
if (lt.kind === "untyped")
|
|
1135
|
-
return isFloat && rt.name !== "float64" ? FLOAT64 : rt;
|
|
1136
|
-
if (rt.kind === "untyped")
|
|
1137
|
-
return isFloat && lt.name !== "float64" ? FLOAT64 : lt;
|
|
1138
|
-
// Both typed
|
|
1139
|
-
return isFloat ? FLOAT64 : INT;
|
|
1140
|
-
}
|
|
1141
|
-
if (isString(lt) && isString(rt) && op === "+") {
|
|
1142
|
-
if (lt.kind === "untyped" && rt.kind === "untyped") return lt;
|
|
1143
|
-
if (lt.kind === "untyped") return rt;
|
|
1144
|
-
if (rt.kind === "untyped") return lt;
|
|
1145
|
-
return STRING;
|
|
1146
|
-
}
|
|
1147
|
-
if (node)
|
|
1148
|
-
this.err(`Invalid operation: ${typeStr(lt)} ${op} ${typeStr(rt)}`, node);
|
|
1149
|
-
return ANY;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
assertAssignable(target, source, node) {
|
|
1153
|
-
if (!target || !source) return;
|
|
1154
|
-
target = this.resolveType(target);
|
|
1155
|
-
source = this.resolveType(source);
|
|
1156
|
-
// Type params are permissive — allow any assignment
|
|
1157
|
-
if (target?.kind === "typeParam" || source?.kind === "typeParam") return;
|
|
1158
|
-
if (isAny(target) || isAny(source)) return;
|
|
1159
|
-
if (isNil(source)) return; // nil is assignable to anything nullable
|
|
1160
|
-
// Pointer type: nil → *T is allowed (handled above); *T → *T check base types
|
|
1161
|
-
if (isPointer(target) && isPointer(source)) {
|
|
1162
|
-
this.assertAssignable(
|
|
1163
|
-
target.base ?? target.underlying?.base,
|
|
1164
|
-
source.base ?? source.underlying?.base,
|
|
1165
|
-
node,
|
|
1166
|
-
);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// Untyped constants coerce to any compatible typed target
|
|
1171
|
-
if (isUntyped(source)) {
|
|
1172
|
-
if (isUntyped(target)) return; // untyped → untyped always OK
|
|
1173
|
-
if (target.kind === "basic") {
|
|
1174
|
-
// untyped int → int or float64
|
|
1175
|
-
if (
|
|
1176
|
-
source.base === "int" &&
|
|
1177
|
-
(target.name === "int" || target.name === "float64")
|
|
1178
|
-
)
|
|
1179
|
-
return;
|
|
1180
|
-
// untyped float → float64 (and int for compatibility)
|
|
1181
|
-
if (
|
|
1182
|
-
source.base === "float64" &&
|
|
1183
|
-
(target.name === "float64" || target.name === "int")
|
|
1184
|
-
)
|
|
1185
|
-
return;
|
|
1186
|
-
// untyped string → string
|
|
1187
|
-
if (source.base === "string" && target.name === "string") return;
|
|
1188
|
-
// untyped bool → bool
|
|
1189
|
-
if (source.base === "bool" && target.name === "bool") return;
|
|
1190
|
-
// untyped complex → complex128 or complex64
|
|
1191
|
-
if (
|
|
1192
|
-
source.base === "complex128" &&
|
|
1193
|
-
(target.name === "complex128" || target.name === "complex64")
|
|
1194
|
-
)
|
|
1195
|
-
return;
|
|
1196
|
-
// untyped int/float → complex (Go allows: var z complex128 = 5)
|
|
1197
|
-
if (
|
|
1198
|
-
isComplex(target) &&
|
|
1199
|
-
(source.base === "int" || source.base === "float64")
|
|
1200
|
-
)
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Go untyped constant promotion: int literals are assignable to float64
|
|
1206
|
-
if (target.kind === "basic" && source.kind === "basic") {
|
|
1207
|
-
if (target.name === "float64" && source.name === "int") return;
|
|
1208
|
-
if (target.name === "int" && source.name === "float64") return; // truncating
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Array-specific checks: size matching and array/slice incompatibility
|
|
1212
|
-
if (target.kind === "array" && source.kind === "array") {
|
|
1213
|
-
if (
|
|
1214
|
-
target.size != null &&
|
|
1215
|
-
source.size != null &&
|
|
1216
|
-
target.size !== source.size
|
|
1217
|
-
) {
|
|
1218
|
-
this.err(
|
|
1219
|
-
`Cannot assign ${typeStr(source)} to ${typeStr(target)} (different array lengths)`,
|
|
1220
|
-
node,
|
|
1221
|
-
);
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
// Check element type compatibility
|
|
1225
|
-
this.assertAssignable(target.elem, source.elem, node);
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
if (target.kind === "array" && source.kind === "slice") {
|
|
1229
|
-
this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
if (target.kind === "slice" && source.kind === "array") {
|
|
1233
|
-
this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
if (typeStr(target) !== typeStr(source)) {
|
|
1238
|
-
// Interface satisfaction check
|
|
1239
|
-
let tBase = target.kind === "named" ? target.underlying : target;
|
|
1240
|
-
let sBase = source.kind === "named" ? source.underlying : source;
|
|
1241
|
-
tBase = this.resolveType(tBase);
|
|
1242
|
-
sBase = this.resolveType(sBase);
|
|
1243
|
-
if (tBase?.kind === "interface") {
|
|
1244
|
-
// Empty interface (interface{}) accepts any type
|
|
1245
|
-
if (tBase.methods.size === 0) return;
|
|
1246
|
-
if (!this.implements(source, tBase, node)) {
|
|
1247
|
-
this.err(
|
|
1248
|
-
`${typeStr(source)} does not implement ${typeStr(target)}`,
|
|
1249
|
-
node,
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
implements(srcType, iface, _node) {
|
|
1259
|
-
let base = srcType.kind === "named" ? srcType.underlying : srcType;
|
|
1260
|
-
base = this.resolveType(base);
|
|
1261
|
-
// For non-struct named types, methods live on the named type itself
|
|
1262
|
-
const methodMap =
|
|
1263
|
-
base?.kind === "struct"
|
|
1264
|
-
? base.methods
|
|
1265
|
-
: srcType.kind === "named"
|
|
1266
|
-
? srcType.methods
|
|
1267
|
-
: null;
|
|
1268
|
-
if (!methodMap) return false;
|
|
1269
|
-
for (const [name, required] of iface.methods) {
|
|
1270
|
-
const actual = methodMap.get(name);
|
|
1271
|
-
if (!actual) return false;
|
|
1272
|
-
|
|
1273
|
-
// Check parameter count matches
|
|
1274
|
-
const reqParams = required.params ?? [];
|
|
1275
|
-
const actParams = actual.params ?? [];
|
|
1276
|
-
if (reqParams.length !== actParams.length) return false;
|
|
1277
|
-
|
|
1278
|
-
// Check each parameter type matches
|
|
1279
|
-
for (let i = 0; i < reqParams.length; i++) {
|
|
1280
|
-
if (typeStr(reqParams[i]) !== typeStr(actParams[i])) return false;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// Check variadic flag matches
|
|
1284
|
-
if (!!required.variadic !== !!actual.variadic) return false;
|
|
1285
|
-
|
|
1286
|
-
// Check return types match (count and each type)
|
|
1287
|
-
const reqRets = required.returns ?? [];
|
|
1288
|
-
const actRets = actual.returns ?? [];
|
|
1289
|
-
if (reqRets.length !== actRets.length) return false;
|
|
1290
|
-
for (let i = 0; i < reqRets.length; i++) {
|
|
1291
|
-
if (typeStr(reqRets[i]) !== typeStr(actRets[i])) return false;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
return true;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
Object.assign(TypeChecker.prototype, statementCheckMethods);
|
|
1299
|
-
Object.assign(TypeChecker.prototype, expressionCheckMethods);
|