kopscript 0.1.0
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/LICENSE +21 -0
- package/README.md +588 -0
- package/bin/ks.js +17 -0
- package/dist/ast.js +2 -0
- package/dist/checker.js +1334 -0
- package/dist/cli.js +100 -0
- package/dist/codegen.js +339 -0
- package/dist/diagnostics.js +26 -0
- package/dist/lexer.js +301 -0
- package/dist/modules.js +133 -0
- package/dist/parser.js +995 -0
- package/dist/tokens.js +81 -0
- package/dist/types.js +108 -0
- package/package.json +43 -0
package/dist/checker.js
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
import * as T from "./types.js";
|
|
2
|
+
export function emptyModuleExports() {
|
|
3
|
+
return { namedTypes: new Map(), classes: new Map(), interfaces: new Map(), enums: new Map(), functions: new Map(), externValues: new Map() };
|
|
4
|
+
}
|
|
5
|
+
const STRING_METHODS = {
|
|
6
|
+
Contains: { params: [T.STRING], returnType: T.BOOL, visibility: "public", isVirtual: false, isOverride: false },
|
|
7
|
+
StartsWith: { params: [T.STRING], returnType: T.BOOL, visibility: "public", isVirtual: false, isOverride: false },
|
|
8
|
+
EndsWith: { params: [T.STRING], returnType: T.BOOL, visibility: "public", isVirtual: false, isOverride: false },
|
|
9
|
+
Replace: { params: [T.STRING, T.STRING], returnType: T.STRING, visibility: "public", isVirtual: false, isOverride: false },
|
|
10
|
+
Split: { params: [T.STRING], returnType: T.arrayOf(T.STRING), visibility: "public", isVirtual: false, isOverride: false },
|
|
11
|
+
Trim: { params: [], returnType: T.STRING, visibility: "public", isVirtual: false, isOverride: false },
|
|
12
|
+
ToUpper: { params: [], returnType: T.STRING, visibility: "public", isVirtual: false, isOverride: false },
|
|
13
|
+
ToLower: { params: [], returnType: T.STRING, visibility: "public", isVirtual: false, isOverride: false },
|
|
14
|
+
};
|
|
15
|
+
class Scope {
|
|
16
|
+
constructor(parent = null) {
|
|
17
|
+
this.parent = parent;
|
|
18
|
+
this.vars = new Map();
|
|
19
|
+
}
|
|
20
|
+
declare(name, type, isConst) {
|
|
21
|
+
this.vars.set(name, { type, isConst });
|
|
22
|
+
}
|
|
23
|
+
resolve(name) {
|
|
24
|
+
return this.vars.get(name) ?? this.parent?.resolve(name) ?? null;
|
|
25
|
+
}
|
|
26
|
+
child() {
|
|
27
|
+
return new Scope(this);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class Checker {
|
|
31
|
+
constructor(program, diagnostics, imports = emptyModuleExports()) {
|
|
32
|
+
this.program = program;
|
|
33
|
+
this.diagnostics = diagnostics;
|
|
34
|
+
this.imports = imports;
|
|
35
|
+
this.classes = new Map();
|
|
36
|
+
this.interfaces = new Map();
|
|
37
|
+
this.enums = new Map();
|
|
38
|
+
this.functions = new Map();
|
|
39
|
+
this.externValues = new Map();
|
|
40
|
+
this.namedTypes = new Map();
|
|
41
|
+
this.importedNames = new Set();
|
|
42
|
+
}
|
|
43
|
+
check() {
|
|
44
|
+
for (const [name, kind] of this.imports.namedTypes) {
|
|
45
|
+
this.namedTypes.set(name, kind);
|
|
46
|
+
this.importedNames.add(name);
|
|
47
|
+
}
|
|
48
|
+
for (const [name, info] of this.imports.classes)
|
|
49
|
+
this.classes.set(name, info);
|
|
50
|
+
for (const [name, info] of this.imports.interfaces)
|
|
51
|
+
this.interfaces.set(name, info);
|
|
52
|
+
for (const [name, info] of this.imports.enums)
|
|
53
|
+
this.enums.set(name, info);
|
|
54
|
+
for (const [name, info] of this.imports.functions) {
|
|
55
|
+
this.functions.set(name, info);
|
|
56
|
+
this.importedNames.add(name);
|
|
57
|
+
}
|
|
58
|
+
for (const [name, type] of this.imports.externValues) {
|
|
59
|
+
this.externValues.set(name, type);
|
|
60
|
+
this.importedNames.add(name);
|
|
61
|
+
}
|
|
62
|
+
const classDecls = this.program.statements.filter((s) => s.kind === "ClassDecl");
|
|
63
|
+
const interfaceDecls = this.program.statements.filter((s) => s.kind === "InterfaceDecl");
|
|
64
|
+
const enumDecls = this.program.statements.filter((s) => s.kind === "EnumDecl");
|
|
65
|
+
const functionDecls = this.program.statements.filter((s) => s.kind === "FunctionDecl");
|
|
66
|
+
const externFunctionDecls = this.program.statements.filter((s) => s.kind === "ExternFunctionDecl");
|
|
67
|
+
const externClassDecls = this.program.statements.filter((s) => s.kind === "ExternClassDecl");
|
|
68
|
+
const externValueDecls = this.program.statements.filter((s) => s.kind === "ExternValueDecl");
|
|
69
|
+
this.checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls);
|
|
70
|
+
for (const c of classDecls)
|
|
71
|
+
this.namedTypes.set(c.name, "class");
|
|
72
|
+
for (const i of interfaceDecls)
|
|
73
|
+
this.namedTypes.set(i.name, "interface");
|
|
74
|
+
for (const e of enumDecls)
|
|
75
|
+
this.namedTypes.set(e.name, "enum");
|
|
76
|
+
for (const c of externClassDecls)
|
|
77
|
+
this.namedTypes.set(c.name, "class");
|
|
78
|
+
for (const e of enumDecls)
|
|
79
|
+
this.registerEnum(e);
|
|
80
|
+
for (const i of interfaceDecls)
|
|
81
|
+
this.registerInterface(i);
|
|
82
|
+
for (const i of interfaceDecls)
|
|
83
|
+
this.checkInterfaceHierarchy(i);
|
|
84
|
+
for (const i of interfaceDecls)
|
|
85
|
+
this.checkExportedInterfaceSurface(i);
|
|
86
|
+
// Extern classes register before regular ones so a class extending an
|
|
87
|
+
// extern class (e.g. `class Counter : Component` where Component comes
|
|
88
|
+
// from another package) has its base already in `this.classes` by the
|
|
89
|
+
// time hierarchy/export/virtual-override checks below need to resolve it.
|
|
90
|
+
for (const c of externClassDecls)
|
|
91
|
+
this.registerExternClass(c);
|
|
92
|
+
for (const c of classDecls)
|
|
93
|
+
this.registerClass(c);
|
|
94
|
+
for (const c of classDecls)
|
|
95
|
+
this.checkClassHierarchy(c);
|
|
96
|
+
for (const c of classDecls)
|
|
97
|
+
this.checkExportedClassSurface(c);
|
|
98
|
+
for (const c of classDecls)
|
|
99
|
+
this.checkVirtualOverride(c);
|
|
100
|
+
for (const c of classDecls)
|
|
101
|
+
this.checkInterfaceConformance(c);
|
|
102
|
+
for (const f of functionDecls)
|
|
103
|
+
this.registerFunction(f);
|
|
104
|
+
for (const f of externFunctionDecls)
|
|
105
|
+
this.registerExternFunction(f);
|
|
106
|
+
for (const v of externValueDecls)
|
|
107
|
+
this.registerExternValue(v);
|
|
108
|
+
const globalScope = new Scope();
|
|
109
|
+
for (const stmt of this.program.statements) {
|
|
110
|
+
if (stmt.kind === "ClassDecl" ||
|
|
111
|
+
stmt.kind === "InterfaceDecl" ||
|
|
112
|
+
stmt.kind === "EnumDecl" ||
|
|
113
|
+
stmt.kind === "ExternFunctionDecl" ||
|
|
114
|
+
stmt.kind === "ExternClassDecl" ||
|
|
115
|
+
stmt.kind === "ExternValueDecl") {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
this.checkTopLevelStatement(stmt, globalScope);
|
|
119
|
+
}
|
|
120
|
+
for (const c of classDecls)
|
|
121
|
+
this.checkClassBody(c);
|
|
122
|
+
}
|
|
123
|
+
registerExternFunction(decl) {
|
|
124
|
+
this.functions.set(decl.name, {
|
|
125
|
+
params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
126
|
+
returnType: this.resolveType(decl.returnType, decl.line, decl.col),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
registerExternClass(decl) {
|
|
130
|
+
const fields = new Map();
|
|
131
|
+
const staticFields = new Map();
|
|
132
|
+
for (const p of decl.properties) {
|
|
133
|
+
const info = { type: this.resolveType(p.type, decl.line, decl.col), visibility: "public", hasSetter: p.hasSetter };
|
|
134
|
+
(p.isStatic ? staticFields : fields).set(p.name, info);
|
|
135
|
+
}
|
|
136
|
+
const methods = new Map();
|
|
137
|
+
const staticMethods = new Map();
|
|
138
|
+
for (const m of decl.methods) {
|
|
139
|
+
const info = {
|
|
140
|
+
params: m.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
141
|
+
returnType: this.resolveType(m.returnType, decl.line, decl.col),
|
|
142
|
+
visibility: "public",
|
|
143
|
+
isVirtual: m.isVirtual,
|
|
144
|
+
isOverride: false,
|
|
145
|
+
};
|
|
146
|
+
(m.isStatic ? staticMethods : methods).set(m.name, info);
|
|
147
|
+
}
|
|
148
|
+
const ownCtorParams = decl.hasConstructor ? decl.ctorParams.map((p) => this.resolveType(p.type, decl.line, decl.col)) : null;
|
|
149
|
+
this.classes.set(decl.name, {
|
|
150
|
+
name: decl.name,
|
|
151
|
+
superclass: null,
|
|
152
|
+
interfaces: [],
|
|
153
|
+
fields,
|
|
154
|
+
methods,
|
|
155
|
+
staticFields,
|
|
156
|
+
staticMethods,
|
|
157
|
+
ownCtorParams,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
registerExternValue(decl) {
|
|
161
|
+
this.externValues.set(decl.name, this.resolveType(decl.type, decl.line, decl.col));
|
|
162
|
+
}
|
|
163
|
+
// The subset of this module's registered declarations visible to a file
|
|
164
|
+
// that `using`s it. Call only after check() has run.
|
|
165
|
+
getExports() {
|
|
166
|
+
const exports = emptyModuleExports();
|
|
167
|
+
for (const stmt of this.program.statements) {
|
|
168
|
+
if (stmt.kind === "ClassDecl" && stmt.isExported) {
|
|
169
|
+
exports.namedTypes.set(stmt.name, "class");
|
|
170
|
+
exports.classes.set(stmt.name, this.classes.get(stmt.name));
|
|
171
|
+
}
|
|
172
|
+
else if (stmt.kind === "InterfaceDecl" && stmt.isExported) {
|
|
173
|
+
exports.namedTypes.set(stmt.name, "interface");
|
|
174
|
+
exports.interfaces.set(stmt.name, this.interfaces.get(stmt.name));
|
|
175
|
+
}
|
|
176
|
+
else if (stmt.kind === "EnumDecl" && stmt.isExported) {
|
|
177
|
+
exports.namedTypes.set(stmt.name, "enum");
|
|
178
|
+
exports.enums.set(stmt.name, this.enums.get(stmt.name));
|
|
179
|
+
}
|
|
180
|
+
else if (stmt.kind === "FunctionDecl" && stmt.isExported) {
|
|
181
|
+
exports.functions.set(stmt.name, this.functions.get(stmt.name));
|
|
182
|
+
}
|
|
183
|
+
else if (stmt.kind === "ExternFunctionDecl" && stmt.isExported) {
|
|
184
|
+
exports.functions.set(stmt.name, this.functions.get(stmt.name));
|
|
185
|
+
}
|
|
186
|
+
else if (stmt.kind === "ExternClassDecl" && stmt.isExported) {
|
|
187
|
+
exports.namedTypes.set(stmt.name, "class");
|
|
188
|
+
exports.classes.set(stmt.name, this.classes.get(stmt.name));
|
|
189
|
+
}
|
|
190
|
+
else if (stmt.kind === "ExternValueDecl" && stmt.isExported) {
|
|
191
|
+
exports.externValues.set(stmt.name, this.externValues.get(stmt.name));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return exports;
|
|
195
|
+
}
|
|
196
|
+
checkNoDuplicateTopLevelNames(classDecls, interfaceDecls, enumDecls, functionDecls, externFunctionDecls, externClassDecls, externValueDecls) {
|
|
197
|
+
const seen = new Map();
|
|
198
|
+
const declare = (name, line, col) => {
|
|
199
|
+
if (this.importedNames.has(name)) {
|
|
200
|
+
this.diagnostics.error(`Declaration '${name}' conflicts with a name brought in by 'using'`, line, col);
|
|
201
|
+
}
|
|
202
|
+
else if (seen.has(name)) {
|
|
203
|
+
this.diagnostics.error(`Duplicate top-level declaration '${name}'`, line, col);
|
|
204
|
+
}
|
|
205
|
+
seen.set(name, true);
|
|
206
|
+
};
|
|
207
|
+
for (const c of classDecls)
|
|
208
|
+
declare(c.name, c.line, c.col);
|
|
209
|
+
for (const i of interfaceDecls)
|
|
210
|
+
declare(i.name, i.line, i.col);
|
|
211
|
+
for (const e of enumDecls)
|
|
212
|
+
declare(e.name, e.line, e.col);
|
|
213
|
+
for (const f of functionDecls)
|
|
214
|
+
declare(f.name, f.line, f.col);
|
|
215
|
+
for (const f of externFunctionDecls)
|
|
216
|
+
declare(f.name, f.line, f.col);
|
|
217
|
+
for (const c of externClassDecls)
|
|
218
|
+
declare(c.name, c.line, c.col);
|
|
219
|
+
for (const v of externValueDecls)
|
|
220
|
+
declare(v.name, v.line, v.col);
|
|
221
|
+
}
|
|
222
|
+
// An exported class's superclass and directly-implemented interfaces must
|
|
223
|
+
// also be exported — otherwise a file that imports this class would have
|
|
224
|
+
// no way to even name its base type, and (more importantly) the exported
|
|
225
|
+
// slice handed to importers wouldn't be self-contained for chain-walking.
|
|
226
|
+
checkExportedClassSurface(decl) {
|
|
227
|
+
if (!decl.isExported)
|
|
228
|
+
return;
|
|
229
|
+
const info = this.classes.get(decl.name);
|
|
230
|
+
if (info.superclass && !this.isExportedName(info.superclass)) {
|
|
231
|
+
this.diagnostics.error(`Exported class '${decl.name}' has a base class '${info.superclass}' that isn't exported`, decl.line, decl.col);
|
|
232
|
+
}
|
|
233
|
+
for (const ifaceName of info.interfaces) {
|
|
234
|
+
if (!this.isExportedName(ifaceName)) {
|
|
235
|
+
this.diagnostics.error(`Exported class '${decl.name}' implements interface '${ifaceName}', which isn't exported`, decl.line, decl.col);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
checkExportedInterfaceSurface(decl) {
|
|
240
|
+
if (!decl.isExported)
|
|
241
|
+
return;
|
|
242
|
+
const info = this.interfaces.get(decl.name);
|
|
243
|
+
if (!info)
|
|
244
|
+
return;
|
|
245
|
+
for (const baseName of info.bases) {
|
|
246
|
+
if (!this.isExportedName(baseName)) {
|
|
247
|
+
this.diagnostics.error(`Exported interface '${decl.name}' extends interface '${baseName}', which isn't exported`, decl.line, decl.col);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Whether `name` is visible to another file that `using`s this one: either
|
|
252
|
+
// it was itself imported (already public by construction) or it's a local
|
|
253
|
+
// top-level declaration explicitly marked `public` (or left unmarked).
|
|
254
|
+
isExportedName(name) {
|
|
255
|
+
if (this.importedNames.has(name))
|
|
256
|
+
return true;
|
|
257
|
+
const decl = this.program.statements.find((s) => (s.kind === "ClassDecl" || s.kind === "InterfaceDecl" || s.kind === "EnumDecl" || s.kind === "ExternClassDecl") && s.name === name);
|
|
258
|
+
return decl?.isExported ?? false;
|
|
259
|
+
}
|
|
260
|
+
// ---------- registration ----------
|
|
261
|
+
resolveType(node, line, col) {
|
|
262
|
+
const resolved = T.resolveTypeNode(node, this.namedTypes);
|
|
263
|
+
if (!resolved) {
|
|
264
|
+
const name = node.kind === "NamedType" ? node.name : "[]";
|
|
265
|
+
this.diagnostics.error(`Unknown type '${name}'`, line, col);
|
|
266
|
+
return T.UNKNOWN;
|
|
267
|
+
}
|
|
268
|
+
return resolved;
|
|
269
|
+
}
|
|
270
|
+
registerEnum(decl) {
|
|
271
|
+
const members = new Map();
|
|
272
|
+
decl.members.forEach((name, index) => {
|
|
273
|
+
if (members.has(name)) {
|
|
274
|
+
this.diagnostics.error(`Duplicate enum member '${name}'`, decl.line, decl.col);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
members.set(name, index);
|
|
278
|
+
});
|
|
279
|
+
this.enums.set(decl.name, { name: decl.name, members });
|
|
280
|
+
}
|
|
281
|
+
registerInterface(decl) {
|
|
282
|
+
const methods = decl.methods.map((m) => ({
|
|
283
|
+
name: m.name,
|
|
284
|
+
params: m.params.map((p) => this.resolveType(p.type, m.line, m.col)),
|
|
285
|
+
returnType: this.resolveType(m.returnType, m.line, m.col),
|
|
286
|
+
}));
|
|
287
|
+
const bases = [];
|
|
288
|
+
for (const baseName of decl.baseList) {
|
|
289
|
+
if (this.namedTypes.get(baseName) !== "interface") {
|
|
290
|
+
this.diagnostics.error(`Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
bases.push(baseName);
|
|
294
|
+
}
|
|
295
|
+
this.interfaces.set(decl.name, { name: decl.name, bases, methods });
|
|
296
|
+
}
|
|
297
|
+
checkInterfaceHierarchy(decl) {
|
|
298
|
+
const info = this.interfaces.get(decl.name);
|
|
299
|
+
if (!info)
|
|
300
|
+
return;
|
|
301
|
+
const seen = new Set();
|
|
302
|
+
const stack = [...info.bases];
|
|
303
|
+
while (stack.length > 0) {
|
|
304
|
+
const current = stack.pop();
|
|
305
|
+
if (current === decl.name) {
|
|
306
|
+
this.diagnostics.error(`Circular interface inheritance detected involving '${decl.name}'`, decl.line, decl.col);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (seen.has(current))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(current);
|
|
312
|
+
const currentInfo = this.interfaces.get(current);
|
|
313
|
+
if (currentInfo)
|
|
314
|
+
stack.push(...currentInfo.bases);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// All methods required to satisfy an interface: its own plus every
|
|
318
|
+
// transitively-inherited parent interface's (cycle-safe).
|
|
319
|
+
collectInterfaceMethods(interfaceName, seen = new Set()) {
|
|
320
|
+
if (seen.has(interfaceName))
|
|
321
|
+
return [];
|
|
322
|
+
seen.add(interfaceName);
|
|
323
|
+
const info = this.interfaces.get(interfaceName);
|
|
324
|
+
if (!info)
|
|
325
|
+
return [];
|
|
326
|
+
const inherited = info.bases.flatMap((b) => this.collectInterfaceMethods(b, seen));
|
|
327
|
+
return [...inherited, ...info.methods];
|
|
328
|
+
}
|
|
329
|
+
interfaceExtends(sub, sup) {
|
|
330
|
+
if (sub === sup)
|
|
331
|
+
return true;
|
|
332
|
+
const info = this.interfaces.get(sub);
|
|
333
|
+
if (!info)
|
|
334
|
+
return false;
|
|
335
|
+
return info.bases.some((b) => this.interfaceExtends(b, sup));
|
|
336
|
+
}
|
|
337
|
+
registerClass(decl) {
|
|
338
|
+
const fields = new Map();
|
|
339
|
+
const staticFields = new Map();
|
|
340
|
+
for (const f of decl.fields) {
|
|
341
|
+
const info = { type: this.resolveType(f.type, f.line, f.col), visibility: f.visibility, hasSetter: true };
|
|
342
|
+
(f.isStatic ? staticFields : fields).set(f.name, info);
|
|
343
|
+
}
|
|
344
|
+
for (const p of decl.properties) {
|
|
345
|
+
fields.set(p.name, { type: this.resolveType(p.type, p.line, p.col), visibility: p.visibility, hasSetter: p.hasSetter });
|
|
346
|
+
}
|
|
347
|
+
const methods = new Map();
|
|
348
|
+
const staticMethods = new Map();
|
|
349
|
+
for (const m of decl.methods) {
|
|
350
|
+
const info = {
|
|
351
|
+
params: m.params.map((p) => this.resolveType(p.type, m.line, m.col)),
|
|
352
|
+
returnType: this.resolveType(m.returnType, m.line, m.col),
|
|
353
|
+
visibility: m.visibility,
|
|
354
|
+
isVirtual: m.isVirtual,
|
|
355
|
+
isOverride: m.isOverride,
|
|
356
|
+
};
|
|
357
|
+
(m.isStatic ? staticMethods : methods).set(m.name, info);
|
|
358
|
+
}
|
|
359
|
+
const ownCtorParams = decl.constructor
|
|
360
|
+
? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
|
|
361
|
+
: null;
|
|
362
|
+
let superclass = null;
|
|
363
|
+
const interfaces = [];
|
|
364
|
+
for (const baseName of decl.baseList) {
|
|
365
|
+
const kind = this.namedTypes.get(baseName);
|
|
366
|
+
if (kind === "class") {
|
|
367
|
+
if (superclass !== null) {
|
|
368
|
+
this.diagnostics.error(`Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${baseName}')`, decl.line, decl.col);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
superclass = baseName;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (kind === "interface") {
|
|
375
|
+
interfaces.push(baseName);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
this.diagnostics.error(`Unknown base class or interface '${baseName}'`, decl.line, decl.col);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this.classes.set(decl.name, {
|
|
382
|
+
name: decl.name,
|
|
383
|
+
superclass,
|
|
384
|
+
interfaces,
|
|
385
|
+
fields,
|
|
386
|
+
methods,
|
|
387
|
+
staticFields,
|
|
388
|
+
staticMethods,
|
|
389
|
+
ownCtorParams,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
checkClassHierarchy(decl) {
|
|
393
|
+
const info = this.classes.get(decl.name);
|
|
394
|
+
this.checkBaseCall(decl, info);
|
|
395
|
+
// Detect cycles.
|
|
396
|
+
const seen = new Set([decl.name]);
|
|
397
|
+
let current = info.superclass;
|
|
398
|
+
while (current) {
|
|
399
|
+
if (seen.has(current)) {
|
|
400
|
+
this.diagnostics.error(`Circular inheritance detected involving '${decl.name}'`, decl.line, decl.col);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
seen.add(current);
|
|
404
|
+
current = this.classes.get(current)?.superclass ?? null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Structural validity of `: base(...)` — whether it's present/absent
|
|
408
|
+
// appropriately for whether the class has a superclass. Argument count
|
|
409
|
+
// and types are checked later, in checkClassBody, once a scope exists.
|
|
410
|
+
checkBaseCall(decl, info) {
|
|
411
|
+
if (!decl.constructor)
|
|
412
|
+
return;
|
|
413
|
+
if (info.superclass) {
|
|
414
|
+
if (!decl.constructor.baseArgs) {
|
|
415
|
+
this.diagnostics.error(`Class '${decl.name}' extends '${info.superclass}' and must call the base constructor via ': base(...)'`, decl.constructor.line, decl.constructor.col);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (decl.constructor.baseArgs) {
|
|
419
|
+
this.diagnostics.error(`Class '${decl.name}' has no base class; ': base(...)' is not valid here`, decl.constructor.line, decl.constructor.col);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Walks up the inheritance chain to find the nearest ancestor that already
|
|
423
|
+
// declares a method with this name (used for virtual/override validation).
|
|
424
|
+
findNearestMethodInChain(className, methodName) {
|
|
425
|
+
let current = this.classes.get(className)?.superclass ?? null;
|
|
426
|
+
while (current) {
|
|
427
|
+
const info = this.classes.get(current);
|
|
428
|
+
const m = info?.methods.get(methodName);
|
|
429
|
+
if (m)
|
|
430
|
+
return { info: m, owner: current };
|
|
431
|
+
current = info?.superclass ?? null;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
checkVirtualOverride(decl) {
|
|
436
|
+
const classInfo = this.classes.get(decl.name);
|
|
437
|
+
for (const method of decl.methods) {
|
|
438
|
+
if (method.isStatic)
|
|
439
|
+
continue; // static methods don't participate in virtual dispatch
|
|
440
|
+
const methodInfo = classInfo.methods.get(method.name);
|
|
441
|
+
if (methodInfo.isVirtual && methodInfo.isOverride) {
|
|
442
|
+
this.diagnostics.error(`Method '${method.name}' cannot be both 'virtual' and 'override'`, method.line, method.col);
|
|
443
|
+
}
|
|
444
|
+
const ancestor = this.findNearestMethodInChain(decl.name, method.name);
|
|
445
|
+
if (ancestor) {
|
|
446
|
+
const overridable = ancestor.info.isVirtual || ancestor.info.isOverride;
|
|
447
|
+
if (!methodInfo.isOverride) {
|
|
448
|
+
this.diagnostics.error(`Method '${method.name}' hides inherited member '${ancestor.owner}.${method.name}'; add 'override' (and mark the base member 'virtual')`, method.line, method.col);
|
|
449
|
+
}
|
|
450
|
+
else if (!overridable) {
|
|
451
|
+
this.diagnostics.error(`Cannot override non-virtual method '${ancestor.owner}.${method.name}'; mark it 'virtual' in '${ancestor.owner}'`, method.line, method.col);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
const paramsMatch = methodInfo.params.length === ancestor.info.params.length &&
|
|
455
|
+
methodInfo.params.every((p, i) => T.typesEqual(p, ancestor.info.params[i]));
|
|
456
|
+
const returnMatches = T.typesEqual(methodInfo.returnType, ancestor.info.returnType);
|
|
457
|
+
if (!paramsMatch || !returnMatches) {
|
|
458
|
+
this.diagnostics.error(`Method '${method.name}' does not match the signature of overridden method '${ancestor.owner}.${method.name}'`, method.line, method.col);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else if (methodInfo.isOverride) {
|
|
463
|
+
this.diagnostics.error(`Method '${method.name}' marked 'override' but no matching method was found in a base class`, method.line, method.col);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
checkInterfaceConformance(decl) {
|
|
468
|
+
const classInfo = this.classes.get(decl.name);
|
|
469
|
+
for (const ifaceName of classInfo.interfaces) {
|
|
470
|
+
if (!this.interfaces.has(ifaceName))
|
|
471
|
+
continue; // already reported as an unknown base type
|
|
472
|
+
for (const sig of this.collectInterfaceMethods(ifaceName)) {
|
|
473
|
+
const found = this.lookupMethod(decl.name, sig.name);
|
|
474
|
+
if (!found) {
|
|
475
|
+
this.diagnostics.error(`Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const paramsMatch = found.info.params.length === sig.params.length && found.info.params.every((p, i) => T.typesEqual(p, sig.params[i]));
|
|
479
|
+
if (!paramsMatch || !T.typesEqual(found.info.returnType, sig.returnType)) {
|
|
480
|
+
this.diagnostics.error(`Class '${decl.name}' member '${sig.name}' does not match the signature required by interface '${ifaceName}'`, decl.line, decl.col);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
registerFunction(decl) {
|
|
486
|
+
this.functions.set(decl.name, {
|
|
487
|
+
params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
|
|
488
|
+
returnType: this.resolveType(decl.returnType, decl.line, decl.col),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
isSubclass(sub, sup) {
|
|
492
|
+
let current = sub;
|
|
493
|
+
while (current) {
|
|
494
|
+
if (current === sup)
|
|
495
|
+
return true;
|
|
496
|
+
current = this.classes.get(current)?.superclass ?? null;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
classImplementsInterface(className, interfaceName) {
|
|
501
|
+
let current = className;
|
|
502
|
+
while (current) {
|
|
503
|
+
const info = this.classes.get(current);
|
|
504
|
+
if (!info)
|
|
505
|
+
return false;
|
|
506
|
+
if (info.interfaces.some((i) => this.interfaceExtends(i, interfaceName)))
|
|
507
|
+
return true;
|
|
508
|
+
current = info.superclass;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
lookupField(className, fieldName) {
|
|
513
|
+
let current = className;
|
|
514
|
+
while (current) {
|
|
515
|
+
const info = this.classes.get(current);
|
|
516
|
+
if (!info)
|
|
517
|
+
return null;
|
|
518
|
+
const field = info.fields.get(fieldName);
|
|
519
|
+
if (field)
|
|
520
|
+
return { info: field, owner: current };
|
|
521
|
+
current = info.superclass;
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
lookupMethod(className, methodName) {
|
|
526
|
+
let current = className;
|
|
527
|
+
while (current) {
|
|
528
|
+
const info = this.classes.get(current);
|
|
529
|
+
if (!info)
|
|
530
|
+
return null;
|
|
531
|
+
const method = info.methods.get(methodName);
|
|
532
|
+
if (method)
|
|
533
|
+
return { info: method, owner: current };
|
|
534
|
+
current = info.superclass;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
lookupStaticField(className, fieldName) {
|
|
539
|
+
let current = className;
|
|
540
|
+
while (current) {
|
|
541
|
+
const info = this.classes.get(current);
|
|
542
|
+
if (!info)
|
|
543
|
+
return null;
|
|
544
|
+
const field = info.staticFields.get(fieldName);
|
|
545
|
+
if (field)
|
|
546
|
+
return { info: field, owner: current };
|
|
547
|
+
current = info.superclass;
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
lookupStaticMethod(className, methodName) {
|
|
552
|
+
let current = className;
|
|
553
|
+
while (current) {
|
|
554
|
+
const info = this.classes.get(current);
|
|
555
|
+
if (!info)
|
|
556
|
+
return null;
|
|
557
|
+
const method = info.staticMethods.get(methodName);
|
|
558
|
+
if (method)
|
|
559
|
+
return { info: method, owner: current };
|
|
560
|
+
current = info.superclass;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
// Enforces C#-style private/protected/public access from the current class context.
|
|
565
|
+
checkAccessibility(visibility, owner, ctx, memberName, line, col) {
|
|
566
|
+
if (visibility === "public")
|
|
567
|
+
return;
|
|
568
|
+
if (visibility === "private") {
|
|
569
|
+
if (ctx.currentClass?.name !== owner) {
|
|
570
|
+
this.diagnostics.error(`'${memberName}' is private and not accessible outside class '${owner}'`, line, col);
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// protected
|
|
575
|
+
if (!ctx.currentClass || !(ctx.currentClass.name === owner || this.isSubclass(ctx.currentClass.name, owner))) {
|
|
576
|
+
this.diagnostics.error(`'${memberName}' is protected and only accessible within '${owner}' or its subclasses`, line, col);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// Assignability: is a value of type `from` usable where type `to` is expected?
|
|
580
|
+
isAssignableType(from, to) {
|
|
581
|
+
if (from.kind === "unknown" || to.kind === "unknown")
|
|
582
|
+
return true;
|
|
583
|
+
if (to.kind === "interface") {
|
|
584
|
+
if (from.kind === "class")
|
|
585
|
+
return this.classImplementsInterface(from.name, to.name);
|
|
586
|
+
if (from.kind === "interface")
|
|
587
|
+
return this.interfaceExtends(from.name, to.name);
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (from.kind === "class" && to.kind === "class") {
|
|
591
|
+
return from.name === to.name || this.isSubclass(from.name, to.name);
|
|
592
|
+
}
|
|
593
|
+
if (from.kind === "array" && to.kind === "array") {
|
|
594
|
+
return this.isAssignableType(from.element, to.element);
|
|
595
|
+
}
|
|
596
|
+
return T.typesEqual(from, to);
|
|
597
|
+
}
|
|
598
|
+
// ---------- top-level ----------
|
|
599
|
+
// Validates that `isAsync` and the declared return type agree — `async`
|
|
600
|
+
// requires `task`/`task<T>`, and `task`/`task<T>` requires `async` (there's
|
|
601
|
+
// no way in v1 to construct a task value by hand) — and returns the type
|
|
602
|
+
// `return` statements inside the body should be checked against: the
|
|
603
|
+
// unwrapped result type for async, the declared type unchanged otherwise.
|
|
604
|
+
resolveBodyReturnType(declaredReturnType, isAsync, line, col) {
|
|
605
|
+
if (isAsync) {
|
|
606
|
+
if (declaredReturnType.kind !== "task") {
|
|
607
|
+
this.diagnostics.error(`'async' functions/methods must return 'task' or 'task<T>', got '${T.typeToString(declaredReturnType)}'`, line, col);
|
|
608
|
+
return T.UNKNOWN;
|
|
609
|
+
}
|
|
610
|
+
return declaredReturnType.resultType;
|
|
611
|
+
}
|
|
612
|
+
if (declaredReturnType.kind === "task") {
|
|
613
|
+
this.diagnostics.error(`A function/method returning 'task'/'task<T>' must be marked 'async'`, line, col);
|
|
614
|
+
}
|
|
615
|
+
return declaredReturnType;
|
|
616
|
+
}
|
|
617
|
+
checkTopLevelStatement(stmt, scope) {
|
|
618
|
+
if (stmt.kind === "FunctionDecl") {
|
|
619
|
+
this.checkFunctionBody(stmt, scope);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Top-level code allows `await`, matching real top-level await in an ES module.
|
|
623
|
+
this.checkStatement(stmt, scope, { returnType: T.VOID, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: true });
|
|
624
|
+
}
|
|
625
|
+
checkFunctionBody(decl, parentScope) {
|
|
626
|
+
const info = this.functions.get(decl.name);
|
|
627
|
+
const scope = parentScope.child();
|
|
628
|
+
decl.params.forEach((p, i) => scope.declare(p.name, info.params[i], false));
|
|
629
|
+
const returnType = this.resolveBodyReturnType(info.returnType, decl.isAsync, decl.line, decl.col);
|
|
630
|
+
this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
|
|
631
|
+
}
|
|
632
|
+
checkClassBody(decl) {
|
|
633
|
+
const info = this.classes.get(decl.name);
|
|
634
|
+
if (decl.constructor) {
|
|
635
|
+
const paramScope = new Scope();
|
|
636
|
+
decl.constructor.params.forEach((p, i) => paramScope.declare(p.name, info.ownCtorParams[i], false));
|
|
637
|
+
// `base(...)` args are checked in a scope with the constructor's own
|
|
638
|
+
// params but no `this` — matching real base()/super() semantics,
|
|
639
|
+
// which must run before `this` becomes available.
|
|
640
|
+
if (decl.constructor.baseArgs && info.superclass) {
|
|
641
|
+
const baseCtorParams = this.lookupCtorParams(info.superclass);
|
|
642
|
+
const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
|
|
643
|
+
if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
|
|
644
|
+
this.diagnostics.error(`Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
|
|
645
|
+
}
|
|
646
|
+
decl.constructor.baseArgs.forEach((arg, i) => {
|
|
647
|
+
const expected = baseCtorParams[i] ?? T.UNKNOWN;
|
|
648
|
+
const argType = this.checkExpressionExpecting(arg, expected, paramScope, baseCtx);
|
|
649
|
+
if (!this.isAssignableType(argType, expected)) {
|
|
650
|
+
this.diagnostics.error(`Base constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const scope = paramScope.child();
|
|
655
|
+
scope.declare("this", T.classType(decl.name), false);
|
|
656
|
+
this.checkBlock(decl.constructor.body, scope, { returnType: T.VOID, currentClass: info, inConstructor: true, loopDepth: 0, isAsync: false });
|
|
657
|
+
}
|
|
658
|
+
for (const method of decl.methods) {
|
|
659
|
+
const methodInfo = (method.isStatic ? info.staticMethods : info.methods).get(method.name);
|
|
660
|
+
const scope = new Scope();
|
|
661
|
+
if (!method.isStatic)
|
|
662
|
+
scope.declare("this", T.classType(decl.name), false);
|
|
663
|
+
method.params.forEach((p, i) => scope.declare(p.name, methodInfo.params[i], false));
|
|
664
|
+
const returnType = this.resolveBodyReturnType(methodInfo.returnType, method.isAsync, method.line, method.col);
|
|
665
|
+
this.checkBlock(method.body, scope, { returnType, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: method.isAsync });
|
|
666
|
+
}
|
|
667
|
+
// Static field initializers run in a static context (no `this`).
|
|
668
|
+
for (const field of decl.fields) {
|
|
669
|
+
if (!field.isStatic || !field.initializer)
|
|
670
|
+
continue;
|
|
671
|
+
const declaredType = info.staticFields.get(field.name).type;
|
|
672
|
+
const initType = this.checkExpressionExpecting(field.initializer, declaredType, new Scope(), {
|
|
673
|
+
returnType: T.VOID,
|
|
674
|
+
currentClass: info,
|
|
675
|
+
inConstructor: false,
|
|
676
|
+
loopDepth: 0,
|
|
677
|
+
isAsync: false,
|
|
678
|
+
});
|
|
679
|
+
if (!this.isAssignableType(initType, declaredType)) {
|
|
680
|
+
this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to static field of type '${T.typeToString(declaredType)}'`, field.line, field.col);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// ---------- statements ----------
|
|
685
|
+
checkBlock(block, parentScope, ctx) {
|
|
686
|
+
const scope = parentScope.child();
|
|
687
|
+
for (const stmt of block.statements)
|
|
688
|
+
this.checkStatement(stmt, scope, ctx);
|
|
689
|
+
}
|
|
690
|
+
checkStatement(stmt, scope, ctx) {
|
|
691
|
+
switch (stmt.kind) {
|
|
692
|
+
case "VarDecl": {
|
|
693
|
+
const declaredType = this.resolveType(stmt.type, stmt.line, stmt.col);
|
|
694
|
+
const initType = this.checkExpressionExpecting(stmt.init, declaredType, scope, ctx);
|
|
695
|
+
if (!this.isAssignableType(initType, declaredType)) {
|
|
696
|
+
this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to variable of type '${T.typeToString(declaredType)}'`, stmt.line, stmt.col);
|
|
697
|
+
}
|
|
698
|
+
scope.declare(stmt.name, declaredType, stmt.isConst);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
case "Block":
|
|
702
|
+
this.checkBlock(stmt, scope, ctx);
|
|
703
|
+
return;
|
|
704
|
+
case "IfStatement": {
|
|
705
|
+
const condType = this.checkExpression(stmt.condition, scope, ctx);
|
|
706
|
+
this.expectType(condType, T.BOOL, stmt.line, stmt.col, "if condition");
|
|
707
|
+
this.checkBlock(stmt.thenBranch, scope, ctx);
|
|
708
|
+
if (stmt.elseBranch) {
|
|
709
|
+
if (stmt.elseBranch.kind === "IfStatement")
|
|
710
|
+
this.checkStatement(stmt.elseBranch, scope, ctx);
|
|
711
|
+
else
|
|
712
|
+
this.checkBlock(stmt.elseBranch, scope, ctx);
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
case "WhileStatement": {
|
|
717
|
+
const condType = this.checkExpression(stmt.condition, scope, ctx);
|
|
718
|
+
this.expectType(condType, T.BOOL, stmt.line, stmt.col, "while condition");
|
|
719
|
+
this.checkBlock(stmt.body, scope, { ...ctx, loopDepth: ctx.loopDepth + 1 });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
case "ForStatement": {
|
|
723
|
+
const forScope = scope.child();
|
|
724
|
+
if (stmt.init) {
|
|
725
|
+
if (stmt.init.kind === "VarDecl")
|
|
726
|
+
this.checkStatement(stmt.init, forScope, ctx);
|
|
727
|
+
else
|
|
728
|
+
this.checkExpression(stmt.init.expression, forScope, ctx);
|
|
729
|
+
}
|
|
730
|
+
if (stmt.condition) {
|
|
731
|
+
const condType = this.checkExpression(stmt.condition, forScope, ctx);
|
|
732
|
+
this.expectType(condType, T.BOOL, stmt.line, stmt.col, "for condition");
|
|
733
|
+
}
|
|
734
|
+
if (stmt.update)
|
|
735
|
+
this.checkExpression(stmt.update, forScope, ctx);
|
|
736
|
+
this.checkBlock(stmt.body, forScope, { ...ctx, loopDepth: ctx.loopDepth + 1 });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
case "ForInStatement": {
|
|
740
|
+
const iterableType = this.checkExpression(stmt.iterable, scope, ctx);
|
|
741
|
+
const declaredVarType = this.resolveType(stmt.varType, stmt.line, stmt.col);
|
|
742
|
+
if (iterableType.kind === "array") {
|
|
743
|
+
if (!this.isAssignableType(iterableType.element, declaredVarType)) {
|
|
744
|
+
this.diagnostics.error(`Cannot use loop variable of type '${T.typeToString(declaredVarType)}' for array of '${T.typeToString(iterableType.element)}'`, stmt.line, stmt.col);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (iterableType.kind !== "unknown") {
|
|
748
|
+
this.diagnostics.error(`Cannot iterate over non-array type '${T.typeToString(iterableType)}'`, stmt.line, stmt.col);
|
|
749
|
+
}
|
|
750
|
+
const forScope = scope.child();
|
|
751
|
+
forScope.declare(stmt.varName, declaredVarType, false);
|
|
752
|
+
this.checkBlock(stmt.body, forScope, { ...ctx, loopDepth: ctx.loopDepth + 1 });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
case "ReturnStatement": {
|
|
756
|
+
const actual = stmt.value ? this.checkExpressionExpecting(stmt.value, ctx.returnType, scope, ctx) : T.VOID;
|
|
757
|
+
if (!this.isAssignableType(actual, ctx.returnType)) {
|
|
758
|
+
this.diagnostics.error(`Return type '${T.typeToString(actual)}' does not match declared return type '${T.typeToString(ctx.returnType)}'`, stmt.line, stmt.col);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
case "BreakStatement":
|
|
763
|
+
case "ContinueStatement":
|
|
764
|
+
if (ctx.loopDepth === 0) {
|
|
765
|
+
this.diagnostics.error(`'${stmt.kind === "BreakStatement" ? "break" : "continue"}' used outside of a loop`, stmt.line, stmt.col);
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
case "ExpressionStatement":
|
|
769
|
+
this.checkExpression(stmt.expression, scope, ctx);
|
|
770
|
+
return;
|
|
771
|
+
case "TryStatement": {
|
|
772
|
+
this.checkBlock(stmt.tryBlock, scope, ctx);
|
|
773
|
+
if (stmt.catchBlock && stmt.catchParam) {
|
|
774
|
+
const catchScope = scope.child();
|
|
775
|
+
const paramType = this.resolveType(stmt.catchParam.type, stmt.line, stmt.col);
|
|
776
|
+
catchScope.declare(stmt.catchParam.name, paramType, false);
|
|
777
|
+
this.checkBlock(stmt.catchBlock, catchScope, ctx);
|
|
778
|
+
}
|
|
779
|
+
if (stmt.finallyBlock)
|
|
780
|
+
this.checkBlock(stmt.finallyBlock, scope, ctx);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
case "ThrowStatement":
|
|
784
|
+
// No restriction on what can be thrown, matching JS's own looseness
|
|
785
|
+
// — KopScript has no base exception/error type to require conformance to.
|
|
786
|
+
this.checkExpression(stmt.expression, scope, ctx);
|
|
787
|
+
return;
|
|
788
|
+
case "FunctionDecl":
|
|
789
|
+
this.diagnostics.error(`Nested function declarations are not supported`, stmt.line, stmt.col);
|
|
790
|
+
return;
|
|
791
|
+
case "ClassDecl":
|
|
792
|
+
this.diagnostics.error(`Nested class declarations are not supported`, stmt.line, stmt.col);
|
|
793
|
+
return;
|
|
794
|
+
case "InterfaceDecl":
|
|
795
|
+
this.diagnostics.error(`Nested interface declarations are not supported`, stmt.line, stmt.col);
|
|
796
|
+
return;
|
|
797
|
+
case "EnumDecl":
|
|
798
|
+
this.diagnostics.error(`Nested enum declarations are not supported`, stmt.line, stmt.col);
|
|
799
|
+
return;
|
|
800
|
+
case "ExternFunctionDecl":
|
|
801
|
+
case "ExternClassDecl":
|
|
802
|
+
case "ExternValueDecl":
|
|
803
|
+
this.diagnostics.error(`'extern' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
expectType(actual, expected, line, col, context) {
|
|
808
|
+
if (!T.typesEqual(actual, expected)) {
|
|
809
|
+
this.diagnostics.error(`Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// ---------- expressions ----------
|
|
813
|
+
checkExpression(expr, scope, ctx) {
|
|
814
|
+
switch (expr.kind) {
|
|
815
|
+
case "NumberLiteral":
|
|
816
|
+
return T.NUMBER;
|
|
817
|
+
case "StringLiteral":
|
|
818
|
+
return T.STRING;
|
|
819
|
+
case "BoolLiteral":
|
|
820
|
+
return T.BOOL;
|
|
821
|
+
case "InterpolatedStringLiteral":
|
|
822
|
+
for (const part of expr.parts) {
|
|
823
|
+
if (part.kind === "Expr")
|
|
824
|
+
this.checkExpression(part.expression, scope, ctx);
|
|
825
|
+
}
|
|
826
|
+
return T.STRING;
|
|
827
|
+
case "ArrayLiteral": {
|
|
828
|
+
if (expr.elements.length === 0)
|
|
829
|
+
return T.arrayOf(T.UNKNOWN);
|
|
830
|
+
const elementTypes = expr.elements.map((e) => this.checkExpression(e, scope, ctx));
|
|
831
|
+
const first = elementTypes[0];
|
|
832
|
+
for (let i = 1; i < elementTypes.length; i++) {
|
|
833
|
+
if (!T.typesEqual(elementTypes[i], first)) {
|
|
834
|
+
this.diagnostics.error(`Array elements must all have the same type`, expr.line, expr.col);
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return T.arrayOf(first);
|
|
839
|
+
}
|
|
840
|
+
case "Identifier": {
|
|
841
|
+
const found = scope.resolve(expr.name);
|
|
842
|
+
if (found)
|
|
843
|
+
return found.type;
|
|
844
|
+
const externValue = this.externValues.get(expr.name);
|
|
845
|
+
if (externValue)
|
|
846
|
+
return externValue;
|
|
847
|
+
// A free function referenced by name as a value (e.g. passed as a
|
|
848
|
+
// callback) has a real function type — direct calls (`Foo(1)`)
|
|
849
|
+
// don't reach this path at all; checkCall resolves those itself.
|
|
850
|
+
const fnInfo = this.functions.get(expr.name);
|
|
851
|
+
if (fnInfo)
|
|
852
|
+
return T.functionType(fnInfo.params, fnInfo.returnType);
|
|
853
|
+
this.diagnostics.error(`Undefined identifier '${expr.name}'`, expr.line, expr.col);
|
|
854
|
+
return T.UNKNOWN;
|
|
855
|
+
}
|
|
856
|
+
case "ThisExpr": {
|
|
857
|
+
const found = scope.resolve("this");
|
|
858
|
+
if (found)
|
|
859
|
+
return found.type;
|
|
860
|
+
this.diagnostics.error(`'this' used outside of a class method`, expr.line, expr.col);
|
|
861
|
+
return T.UNKNOWN;
|
|
862
|
+
}
|
|
863
|
+
case "UnaryExpr": {
|
|
864
|
+
const operandType = this.checkExpression(expr.operand, scope, ctx);
|
|
865
|
+
if (expr.op === "-")
|
|
866
|
+
this.expectType(operandType, T.NUMBER, expr.line, expr.col, "unary '-'");
|
|
867
|
+
if (expr.op === "!")
|
|
868
|
+
this.expectType(operandType, T.BOOL, expr.line, expr.col, "unary '!'");
|
|
869
|
+
return operandType.kind === "unknown" ? T.UNKNOWN : expr.op === "-" ? T.NUMBER : T.BOOL;
|
|
870
|
+
}
|
|
871
|
+
case "BinaryExpr": {
|
|
872
|
+
const leftType = this.checkExpression(expr.left, scope, ctx);
|
|
873
|
+
const rightType = this.checkExpression(expr.right, scope, ctx);
|
|
874
|
+
return this.checkBinaryOp(expr, leftType, rightType);
|
|
875
|
+
}
|
|
876
|
+
case "LogicalExpr": {
|
|
877
|
+
const leftType = this.checkExpression(expr.left, scope, ctx);
|
|
878
|
+
const rightType = this.checkExpression(expr.right, scope, ctx);
|
|
879
|
+
this.expectType(leftType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
880
|
+
this.expectType(rightType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
881
|
+
return T.BOOL;
|
|
882
|
+
}
|
|
883
|
+
case "AssignExpr": {
|
|
884
|
+
let targetType;
|
|
885
|
+
if (expr.target.kind === "MemberExpr") {
|
|
886
|
+
targetType = this.checkMember(expr.target, scope, ctx, true).type;
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
targetType = this.checkExpression(expr.target, scope, ctx);
|
|
890
|
+
if (expr.target.kind === "Identifier") {
|
|
891
|
+
const found = scope.resolve(expr.target.name);
|
|
892
|
+
if (found?.isConst) {
|
|
893
|
+
this.diagnostics.error(`Cannot assign to const variable '${expr.target.name}'`, expr.line, expr.col);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const valueType = this.checkExpressionExpecting(expr.value, targetType, scope, ctx);
|
|
898
|
+
if (!this.isAssignableType(valueType, targetType)) {
|
|
899
|
+
this.diagnostics.error(`Cannot assign value of type '${T.typeToString(valueType)}' to target of type '${T.typeToString(targetType)}'`, expr.line, expr.col);
|
|
900
|
+
}
|
|
901
|
+
return targetType;
|
|
902
|
+
}
|
|
903
|
+
case "CallExpr":
|
|
904
|
+
return this.checkCall(expr, scope, ctx);
|
|
905
|
+
case "NewExpr":
|
|
906
|
+
return this.checkNew(expr, scope, ctx);
|
|
907
|
+
case "MemberExpr":
|
|
908
|
+
return this.checkMember(expr, scope, ctx).type;
|
|
909
|
+
case "IndexExpr": {
|
|
910
|
+
const objType = this.checkExpression(expr.object, scope, ctx);
|
|
911
|
+
const indexType = this.checkExpression(expr.index, scope, ctx);
|
|
912
|
+
this.expectType(indexType, T.NUMBER, expr.line, expr.col, "array index");
|
|
913
|
+
if (objType.kind === "array")
|
|
914
|
+
return objType.element;
|
|
915
|
+
if (objType.kind !== "unknown") {
|
|
916
|
+
this.diagnostics.error(`Cannot index into non-array type '${T.typeToString(objType)}'`, expr.line, expr.col);
|
|
917
|
+
}
|
|
918
|
+
return T.UNKNOWN;
|
|
919
|
+
}
|
|
920
|
+
case "MatchExpr":
|
|
921
|
+
return this.checkMatch(expr, scope, ctx);
|
|
922
|
+
case "LambdaExpr":
|
|
923
|
+
// Reached only when a lambda appears somewhere with no contextual
|
|
924
|
+
// expected type available (e.g. inside a match arm or array
|
|
925
|
+
// literal) — still checked, just without validating its params
|
|
926
|
+
// against anything. checkExpressionExpecting is the normal path.
|
|
927
|
+
return this.checkLambda(expr, T.UNKNOWN, scope, ctx);
|
|
928
|
+
case "AwaitExpr": {
|
|
929
|
+
if (!ctx.isAsync) {
|
|
930
|
+
this.diagnostics.error(`'await' can only be used inside an 'async' function or method`, expr.line, expr.col);
|
|
931
|
+
}
|
|
932
|
+
const operandType = this.checkExpression(expr.operand, scope, ctx);
|
|
933
|
+
if (operandType.kind === "task")
|
|
934
|
+
return operandType.resultType;
|
|
935
|
+
if (operandType.kind !== "unknown") {
|
|
936
|
+
this.diagnostics.error(`Cannot 'await' a value of type '${T.typeToString(operandType)}'`, expr.line, expr.col);
|
|
937
|
+
}
|
|
938
|
+
return T.UNKNOWN;
|
|
939
|
+
}
|
|
940
|
+
case "StateExpr": {
|
|
941
|
+
const valueType = this.checkExpression(expr.initializer, scope, ctx);
|
|
942
|
+
return T.stateType(valueType);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Like checkExpression, but for a position with a known expected type —
|
|
947
|
+
// every position a lambda can appear in has one, since KopScript requires an
|
|
948
|
+
// explicit type everywhere (the declared type of a variable/field/param,
|
|
949
|
+
// a function's declared return type, ...). Lambdas are checked against
|
|
950
|
+
// that expected type; everything else behaves exactly like
|
|
951
|
+
// checkExpression (the caller still does its own assignability check
|
|
952
|
+
// against `expected` afterward, same as before this existed).
|
|
953
|
+
checkExpressionExpecting(expr, expected, scope, ctx) {
|
|
954
|
+
if (expr.kind === "LambdaExpr")
|
|
955
|
+
return this.checkLambda(expr, expected, scope, ctx);
|
|
956
|
+
return this.checkExpression(expr, scope, ctx);
|
|
957
|
+
}
|
|
958
|
+
checkLambda(expr, expected, scope, ctx) {
|
|
959
|
+
if (expected.kind !== "function") {
|
|
960
|
+
if (expected.kind !== "unknown") {
|
|
961
|
+
this.diagnostics.error(`Lambda expression is not valid where type '${T.typeToString(expected)}' is expected`, expr.line, expr.col);
|
|
962
|
+
}
|
|
963
|
+
const paramTypes = expr.params.map((p) => this.resolveType(p.type, expr.line, expr.col));
|
|
964
|
+
const lambdaScope = scope.child();
|
|
965
|
+
expr.params.forEach((p, i) => lambdaScope.declare(p.name, paramTypes[i], false));
|
|
966
|
+
// No expected type at all: infer the return type from the body itself
|
|
967
|
+
// (expression-bodied only — see checkLambdaBody) rather than giving up.
|
|
968
|
+
const actualReturnType = this.checkLambdaBody(expr, T.UNKNOWN, lambdaScope, ctx);
|
|
969
|
+
return T.functionType(paramTypes, actualReturnType);
|
|
970
|
+
}
|
|
971
|
+
if (expr.params.length !== expected.params.length) {
|
|
972
|
+
this.diagnostics.error(`Lambda has ${expr.params.length} parameter(s), but ${expected.params.length} were expected`, expr.line, expr.col);
|
|
973
|
+
}
|
|
974
|
+
const paramTypes = expr.params.map((p, i) => {
|
|
975
|
+
const declared = this.resolveType(p.type, expr.line, expr.col);
|
|
976
|
+
const expectedParamType = expected.params[i];
|
|
977
|
+
if (expectedParamType && !T.typesEqual(declared, expectedParamType)) {
|
|
978
|
+
this.diagnostics.error(`Lambda parameter '${p.name}' has type '${T.typeToString(declared)}', expected '${T.typeToString(expectedParamType)}'`, expr.line, expr.col);
|
|
979
|
+
}
|
|
980
|
+
return declared;
|
|
981
|
+
});
|
|
982
|
+
const lambdaScope = scope.child();
|
|
983
|
+
expr.params.forEach((p, i) => lambdaScope.declare(p.name, paramTypes[i], false));
|
|
984
|
+
const actualReturnType = this.checkLambdaBody(expr, expected.returnType, lambdaScope, ctx);
|
|
985
|
+
// The expected return type might itself be UNKNOWN (e.g. Array.Map's
|
|
986
|
+
// callback: the param type is known, but the result type is exactly
|
|
987
|
+
// what we're trying to learn) — fall back to whatever the body actually
|
|
988
|
+
// produced rather than propagating UNKNOWN needlessly.
|
|
989
|
+
const resultReturnType = expected.returnType.kind === "unknown" ? actualReturnType : expected.returnType;
|
|
990
|
+
return T.functionType(paramTypes, resultReturnType);
|
|
991
|
+
}
|
|
992
|
+
// Returns the type `return` statements/the body expression actually
|
|
993
|
+
// produced. For a block body this is just `expectedReturnType` echoed
|
|
994
|
+
// back (v1 doesn't scan a block's `return` statements to infer a result
|
|
995
|
+
// type when none is expected) — inference only works for the common case
|
|
996
|
+
// of an expression-bodied lambda.
|
|
997
|
+
checkLambdaBody(expr, expectedReturnType, scope, ctx) {
|
|
998
|
+
// A lambda is a closure, not a method — it never has its own `this`
|
|
999
|
+
// (it inherits the enclosing one, matching JS arrow-function semantics,
|
|
1000
|
+
// which is exactly what it compiles to) and it's never "in a
|
|
1001
|
+
// constructor": even one created inside a constructor might be called
|
|
1002
|
+
// long after construction finishes, so allowing a get-only property
|
|
1003
|
+
// assignment inside it would be unsound. There's no async-lambda syntax
|
|
1004
|
+
// in v1 either, so `await` is never valid inside one, regardless of
|
|
1005
|
+
// whether the enclosing function is itself async.
|
|
1006
|
+
const lambdaCtx = { returnType: expectedReturnType, currentClass: ctx.currentClass, inConstructor: false, loopDepth: 0, isAsync: false };
|
|
1007
|
+
if (expr.body.kind === "Block") {
|
|
1008
|
+
this.checkBlock(expr.body, scope, lambdaCtx);
|
|
1009
|
+
return expectedReturnType;
|
|
1010
|
+
}
|
|
1011
|
+
const actual = this.checkExpressionExpecting(expr.body, expectedReturnType, scope, lambdaCtx);
|
|
1012
|
+
if (expectedReturnType.kind !== "unknown" && !this.isAssignableType(actual, expectedReturnType)) {
|
|
1013
|
+
this.diagnostics.error(`Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col);
|
|
1014
|
+
}
|
|
1015
|
+
return actual;
|
|
1016
|
+
}
|
|
1017
|
+
checkBinaryOp(expr, leftType, rightType) {
|
|
1018
|
+
const { op, line, col } = expr;
|
|
1019
|
+
if (op === "+") {
|
|
1020
|
+
if (leftType.kind === "string" || rightType.kind === "string") {
|
|
1021
|
+
return T.STRING;
|
|
1022
|
+
}
|
|
1023
|
+
this.expectType(leftType, T.NUMBER, line, col, "'+' operand");
|
|
1024
|
+
this.expectType(rightType, T.NUMBER, line, col, "'+' operand");
|
|
1025
|
+
return T.NUMBER;
|
|
1026
|
+
}
|
|
1027
|
+
if (op === "-" || op === "*" || op === "/" || op === "%") {
|
|
1028
|
+
this.expectType(leftType, T.NUMBER, line, col, `'${op}' operand`);
|
|
1029
|
+
this.expectType(rightType, T.NUMBER, line, col, `'${op}' operand`);
|
|
1030
|
+
return T.NUMBER;
|
|
1031
|
+
}
|
|
1032
|
+
if (op === "==" || op === "!=") {
|
|
1033
|
+
if (!T.typesEqual(leftType, rightType)) {
|
|
1034
|
+
this.diagnostics.error(`Cannot compare '${T.typeToString(leftType)}' with '${T.typeToString(rightType)}'`, line, col);
|
|
1035
|
+
}
|
|
1036
|
+
return T.BOOL;
|
|
1037
|
+
}
|
|
1038
|
+
// <, >, <=, >=
|
|
1039
|
+
this.expectType(leftType, T.NUMBER, line, col, `'${op}' operand`);
|
|
1040
|
+
this.expectType(rightType, T.NUMBER, line, col, `'${op}' operand`);
|
|
1041
|
+
return T.BOOL;
|
|
1042
|
+
}
|
|
1043
|
+
checkCall(expr, scope, ctx) {
|
|
1044
|
+
// Extern values are excluded here even though they're not scope-resolved,
|
|
1045
|
+
// so a function-typed extern value (rare, but valid) falls through to the
|
|
1046
|
+
// generic "callee is some function-typed expression" handling below
|
|
1047
|
+
// instead of hitting the "undefined function" error meant for plain names.
|
|
1048
|
+
if (expr.callee.kind === "Identifier" && !scope.resolve(expr.callee.name) && !this.externValues.has(expr.callee.name)) {
|
|
1049
|
+
const info = this.functions.get(expr.callee.name);
|
|
1050
|
+
if (!info) {
|
|
1051
|
+
if (expr.callee.name === "print") {
|
|
1052
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1053
|
+
return T.VOID;
|
|
1054
|
+
}
|
|
1055
|
+
this.diagnostics.error(`Undefined function '${expr.callee.name}'`, expr.line, expr.col);
|
|
1056
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1057
|
+
return T.UNKNOWN;
|
|
1058
|
+
}
|
|
1059
|
+
this.checkArgs(expr, info.params, scope, ctx);
|
|
1060
|
+
return info.returnType;
|
|
1061
|
+
}
|
|
1062
|
+
if (expr.callee.kind === "MemberExpr") {
|
|
1063
|
+
// Array.Map's result type is polymorphic (derived from the callback),
|
|
1064
|
+
// which doesn't fit checkMember's fixed-signature methodInfo shape —
|
|
1065
|
+
// handled here instead, before the generic member/call path below.
|
|
1066
|
+
if (expr.callee.property === "Map") {
|
|
1067
|
+
const objectType = this.checkExpression(expr.callee.object, scope, ctx);
|
|
1068
|
+
if (objectType.kind === "array") {
|
|
1069
|
+
return this.checkArrayMap(expr, objectType.element, scope, ctx);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const { type: methodType, methodInfo } = this.checkMember(expr.callee, scope, ctx);
|
|
1073
|
+
if (methodInfo) {
|
|
1074
|
+
this.checkArgs(expr, methodInfo.params, scope, ctx);
|
|
1075
|
+
return methodInfo.returnType;
|
|
1076
|
+
}
|
|
1077
|
+
if (methodType.kind === "function") {
|
|
1078
|
+
this.checkArgs(expr, methodType.params, scope, ctx);
|
|
1079
|
+
return methodType.returnType;
|
|
1080
|
+
}
|
|
1081
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1082
|
+
return methodType;
|
|
1083
|
+
}
|
|
1084
|
+
// Any other callee expression: a variable/parameter holding a function
|
|
1085
|
+
// value, a call that itself returns a function, an immediately-invoked
|
|
1086
|
+
// lambda, etc. — anything whose static type is a function type.
|
|
1087
|
+
const calleeType = this.checkExpression(expr.callee, scope, ctx);
|
|
1088
|
+
if (calleeType.kind === "function") {
|
|
1089
|
+
this.checkArgs(expr, calleeType.params, scope, ctx);
|
|
1090
|
+
return calleeType.returnType;
|
|
1091
|
+
}
|
|
1092
|
+
if (calleeType.kind !== "unknown") {
|
|
1093
|
+
this.diagnostics.error(`Cannot call a value of type '${T.typeToString(calleeType)}'`, expr.line, expr.col);
|
|
1094
|
+
}
|
|
1095
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1096
|
+
return T.UNKNOWN;
|
|
1097
|
+
}
|
|
1098
|
+
checkArgs(expr, paramTypes, scope, ctx) {
|
|
1099
|
+
if (expr.args.length !== paramTypes.length) {
|
|
1100
|
+
this.diagnostics.error(`Expected ${paramTypes.length} argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1101
|
+
}
|
|
1102
|
+
expr.args.forEach((arg, i) => {
|
|
1103
|
+
const expected = paramTypes[i] ?? T.UNKNOWN;
|
|
1104
|
+
const argType = this.checkExpressionExpecting(arg, expected, scope, ctx);
|
|
1105
|
+
if (!this.isAssignableType(argType, expected)) {
|
|
1106
|
+
this.diagnostics.error(`Argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
checkNew(expr, scope, ctx) {
|
|
1111
|
+
if (this.interfaces.has(expr.className)) {
|
|
1112
|
+
this.diagnostics.error(`Cannot instantiate interface '${expr.className}'`, expr.line, expr.col);
|
|
1113
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1114
|
+
return T.UNKNOWN;
|
|
1115
|
+
}
|
|
1116
|
+
if (this.enums.has(expr.className)) {
|
|
1117
|
+
this.diagnostics.error(`Cannot instantiate enum '${expr.className}'`, expr.line, expr.col);
|
|
1118
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1119
|
+
return T.UNKNOWN;
|
|
1120
|
+
}
|
|
1121
|
+
const info = this.classes.get(expr.className);
|
|
1122
|
+
if (!info) {
|
|
1123
|
+
this.diagnostics.error(`Unknown class '${expr.className}'`, expr.line, expr.col);
|
|
1124
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1125
|
+
return T.UNKNOWN;
|
|
1126
|
+
}
|
|
1127
|
+
const ctorParams = this.lookupCtorParams(expr.className);
|
|
1128
|
+
if (expr.args.length !== ctorParams.length) {
|
|
1129
|
+
this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1130
|
+
}
|
|
1131
|
+
expr.args.forEach((arg, i) => {
|
|
1132
|
+
const expected = ctorParams[i] ?? T.UNKNOWN;
|
|
1133
|
+
const argType = this.checkExpressionExpecting(arg, expected, scope, ctx);
|
|
1134
|
+
if (!this.isAssignableType(argType, expected)) {
|
|
1135
|
+
this.diagnostics.error(`Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
return T.classType(expr.className);
|
|
1139
|
+
}
|
|
1140
|
+
lookupCtorParams(className) {
|
|
1141
|
+
let current = className;
|
|
1142
|
+
while (current) {
|
|
1143
|
+
const info = this.classes.get(current);
|
|
1144
|
+
if (!info)
|
|
1145
|
+
return [];
|
|
1146
|
+
if (info.ownCtorParams !== null)
|
|
1147
|
+
return info.ownCtorParams;
|
|
1148
|
+
current = info.superclass;
|
|
1149
|
+
}
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
// Array stdlib with a fixed (non-polymorphic) signature, given the
|
|
1153
|
+
// array's own element type — everything except Map, which checkCall
|
|
1154
|
+
// handles separately since its result type depends on the callback
|
|
1155
|
+
// actually passed at the call site, not just the receiver.
|
|
1156
|
+
arrayMethod(elementType, name) {
|
|
1157
|
+
const method = { visibility: "public", isVirtual: false, isOverride: false };
|
|
1158
|
+
if (name === "ForEach") {
|
|
1159
|
+
return { ...method, params: [T.functionType([elementType], T.VOID)], returnType: T.VOID };
|
|
1160
|
+
}
|
|
1161
|
+
if (name === "Filter") {
|
|
1162
|
+
return { ...method, params: [T.functionType([elementType], T.BOOL)], returnType: T.arrayOf(elementType) };
|
|
1163
|
+
}
|
|
1164
|
+
if (name === "Push") {
|
|
1165
|
+
// Non-mutating by design, unlike JS's own Array.push — consistent
|
|
1166
|
+
// with Map/Filter, and avoids introducing aliasing surprises that
|
|
1167
|
+
// nothing else in the type system models. Compiles to `[...arr, x]`.
|
|
1168
|
+
return { ...method, params: [elementType], returnType: T.arrayOf(elementType) };
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
// Array.Map: `arr.Map(f)` where f: (T) => U, result: U[]. U is whatever
|
|
1173
|
+
// the callback actually returns — checkLambda infers it from the body
|
|
1174
|
+
// when given a partially-known expected type (params known, return
|
|
1175
|
+
// UNKNOWN); a plain function-valued argument (a named function, or a
|
|
1176
|
+
// variable already holding one) already carries its own real type, so no
|
|
1177
|
+
// inference is needed there at all.
|
|
1178
|
+
checkArrayMap(expr, elementType, scope, ctx) {
|
|
1179
|
+
if (expr.args.length !== 1) {
|
|
1180
|
+
this.diagnostics.error(`Map expects exactly 1 argument, got ${expr.args.length}`, expr.line, expr.col);
|
|
1181
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1182
|
+
return T.arrayOf(T.UNKNOWN);
|
|
1183
|
+
}
|
|
1184
|
+
const arg = expr.args[0];
|
|
1185
|
+
const expectedCallbackType = T.functionType([elementType], T.UNKNOWN);
|
|
1186
|
+
const argType = this.checkExpressionExpecting(arg, expectedCallbackType, scope, ctx);
|
|
1187
|
+
if (argType.kind !== "function") {
|
|
1188
|
+
if (argType.kind !== "unknown") {
|
|
1189
|
+
this.diagnostics.error(`Map expects a function as its argument, got '${T.typeToString(argType)}'`, arg.line, arg.col);
|
|
1190
|
+
}
|
|
1191
|
+
return T.arrayOf(T.UNKNOWN);
|
|
1192
|
+
}
|
|
1193
|
+
if (argType.params.length !== 1 || !this.isAssignableType(elementType, argType.params[0])) {
|
|
1194
|
+
this.diagnostics.error(`Map callback must take a single '${T.typeToString(elementType)}' parameter, got '${T.typeToString(argType)}'`, arg.line, arg.col);
|
|
1195
|
+
}
|
|
1196
|
+
return T.arrayOf(argType.returnType);
|
|
1197
|
+
}
|
|
1198
|
+
checkMember(expr, scope, ctx, isAssignTarget = false) {
|
|
1199
|
+
// A bare type name as the "object" — Color.Red (enum) or Dog.Count (static) —
|
|
1200
|
+
// is a type reference, not a value, so it's handled before the general expression check.
|
|
1201
|
+
if (expr.object.kind === "Identifier" && !scope.resolve(expr.object.name)) {
|
|
1202
|
+
const objName = expr.object.name;
|
|
1203
|
+
if (this.enums.has(objName)) {
|
|
1204
|
+
const enumInfo = this.enums.get(objName);
|
|
1205
|
+
if (!enumInfo.members.has(expr.property)) {
|
|
1206
|
+
this.diagnostics.error(`Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1207
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1208
|
+
}
|
|
1209
|
+
return { type: T.enumType(objName), methodInfo: null };
|
|
1210
|
+
}
|
|
1211
|
+
if (this.classes.has(objName)) {
|
|
1212
|
+
const field = this.lookupStaticField(objName, expr.property);
|
|
1213
|
+
if (field) {
|
|
1214
|
+
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
1215
|
+
return { type: field.info.type, methodInfo: null };
|
|
1216
|
+
}
|
|
1217
|
+
const method = this.lookupStaticMethod(objName, expr.property);
|
|
1218
|
+
if (method) {
|
|
1219
|
+
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
1220
|
+
return { type: method.info.returnType, methodInfo: method.info };
|
|
1221
|
+
}
|
|
1222
|
+
this.diagnostics.error(`Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col);
|
|
1223
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const objectType = this.checkExpression(expr.object, scope, ctx);
|
|
1227
|
+
if (objectType.kind === "string") {
|
|
1228
|
+
if (expr.property === "Length")
|
|
1229
|
+
return { type: T.NUMBER, methodInfo: null };
|
|
1230
|
+
const method = STRING_METHODS[expr.property];
|
|
1231
|
+
if (method)
|
|
1232
|
+
return { type: method.returnType, methodInfo: method };
|
|
1233
|
+
this.diagnostics.error(`Unknown string member '${expr.property}'`, expr.line, expr.col);
|
|
1234
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1235
|
+
}
|
|
1236
|
+
if (objectType.kind === "array") {
|
|
1237
|
+
if (expr.property === "Length")
|
|
1238
|
+
return { type: T.NUMBER, methodInfo: null };
|
|
1239
|
+
const method = this.arrayMethod(objectType.element, expr.property);
|
|
1240
|
+
if (method)
|
|
1241
|
+
return { type: method.returnType, methodInfo: method };
|
|
1242
|
+
// Map isn't handled here at all — its result type is polymorphic
|
|
1243
|
+
// (derived from the callback passed at the call site), which doesn't
|
|
1244
|
+
// fit this fixed-signature lookup. checkCall special-cases it before
|
|
1245
|
+
// ever reaching checkMember, so `arr.Map(f)` works; `arr.Map` used as
|
|
1246
|
+
// a bare value (not called) falls through to this error, same as an
|
|
1247
|
+
// unknown member — a known v1 restriction.
|
|
1248
|
+
this.diagnostics.error(`Unknown array member '${expr.property}'`, expr.line, expr.col);
|
|
1249
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1250
|
+
}
|
|
1251
|
+
if (objectType.kind === "state") {
|
|
1252
|
+
if (expr.property === "Value")
|
|
1253
|
+
return { type: objectType.valueType, methodInfo: null };
|
|
1254
|
+
if (expr.property === "Subscribe") {
|
|
1255
|
+
const method = {
|
|
1256
|
+
params: [T.functionType([objectType.valueType], T.VOID)],
|
|
1257
|
+
returnType: T.VOID,
|
|
1258
|
+
visibility: "public",
|
|
1259
|
+
isVirtual: false,
|
|
1260
|
+
isOverride: false,
|
|
1261
|
+
};
|
|
1262
|
+
return { type: method.returnType, methodInfo: method };
|
|
1263
|
+
}
|
|
1264
|
+
this.diagnostics.error(`Unknown state member '${expr.property}'`, expr.line, expr.col);
|
|
1265
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1266
|
+
}
|
|
1267
|
+
if (objectType.kind === "class") {
|
|
1268
|
+
const field = this.lookupField(objectType.name, expr.property);
|
|
1269
|
+
if (field) {
|
|
1270
|
+
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
1271
|
+
if (isAssignTarget && !field.info.hasSetter) {
|
|
1272
|
+
const inOwnCtor = ctx.inConstructor && ctx.currentClass?.name === field.owner;
|
|
1273
|
+
if (!inOwnCtor) {
|
|
1274
|
+
this.diagnostics.error(`'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return { type: field.info.type, methodInfo: null };
|
|
1278
|
+
}
|
|
1279
|
+
const method = this.lookupMethod(objectType.name, expr.property);
|
|
1280
|
+
if (method) {
|
|
1281
|
+
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
1282
|
+
return { type: method.info.returnType, methodInfo: method.info };
|
|
1283
|
+
}
|
|
1284
|
+
this.diagnostics.error(`Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1285
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1286
|
+
}
|
|
1287
|
+
if (objectType.kind === "interface") {
|
|
1288
|
+
const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
|
|
1289
|
+
if (sig) {
|
|
1290
|
+
return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
|
|
1291
|
+
}
|
|
1292
|
+
this.diagnostics.error(`Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1293
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1294
|
+
}
|
|
1295
|
+
if (objectType.kind !== "unknown") {
|
|
1296
|
+
this.diagnostics.error(`Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
|
|
1297
|
+
}
|
|
1298
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1299
|
+
}
|
|
1300
|
+
checkMatch(expr, scope, ctx) {
|
|
1301
|
+
const subjectType = this.checkExpression(expr.subject, scope, ctx);
|
|
1302
|
+
this.expectType(subjectType, T.STRING, expr.line, expr.col, "match subject");
|
|
1303
|
+
if (expr.arms.length === 0) {
|
|
1304
|
+
this.diagnostics.error(`match expression must have at least one arm`, expr.line, expr.col);
|
|
1305
|
+
return T.UNKNOWN;
|
|
1306
|
+
}
|
|
1307
|
+
const lastArm = expr.arms[expr.arms.length - 1];
|
|
1308
|
+
if (lastArm.pattern.kind !== "WildcardPattern") {
|
|
1309
|
+
this.diagnostics.error(`match expression must end with a wildcard '_' arm`, lastArm.line, lastArm.col);
|
|
1310
|
+
}
|
|
1311
|
+
let resultType = null;
|
|
1312
|
+
for (const arm of expr.arms) {
|
|
1313
|
+
if (arm.pattern.kind === "LiteralPattern") {
|
|
1314
|
+
for (const value of arm.pattern.values) {
|
|
1315
|
+
if (value.kind !== "StringLiteral") {
|
|
1316
|
+
this.diagnostics.error(`match patterns must be string literals`, value.line, value.col);
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
this.checkExpression(value, scope, ctx);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
// RegexPattern and WildcardPattern need no further checking here.
|
|
1324
|
+
const armResultType = this.checkExpression(arm.result, scope, ctx);
|
|
1325
|
+
if (resultType === null) {
|
|
1326
|
+
resultType = armResultType;
|
|
1327
|
+
}
|
|
1328
|
+
else if (!this.isAssignableType(armResultType, resultType)) {
|
|
1329
|
+
this.diagnostics.error(`match arm result type '${T.typeToString(armResultType)}' does not match preceding arms' type '${T.typeToString(resultType)}'`, arm.line, arm.col);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return resultType ?? T.UNKNOWN;
|
|
1333
|
+
}
|
|
1334
|
+
}
|