@katnip-org/compiler 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 +202 -0
- package/NOTICE +5 -0
- package/README.md +42 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +141 -0
- package/build/cli.js.map +1 -0
- package/build/codegen/SB3Generator.d.ts +1 -0
- package/build/codegen/SB3Generator.js +2 -0
- package/build/codegen/SB3Generator.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +26 -0
- package/build/index.js.map +1 -0
- package/build/ir/IRGenerator.d.ts +1 -0
- package/build/ir/IRGenerator.js +2 -0
- package/build/ir/IRGenerator.js.map +1 -0
- package/build/ir/IRNode.d.ts +1 -0
- package/build/ir/IRNode.js +2 -0
- package/build/ir/IRNode.js.map +1 -0
- package/build/lexer/Lexer.d.ts +69 -0
- package/build/lexer/Lexer.js +441 -0
- package/build/lexer/Lexer.js.map +1 -0
- package/build/lexer/LexerState.d.ts +14 -0
- package/build/lexer/LexerState.js +16 -0
- package/build/lexer/LexerState.js.map +1 -0
- package/build/lexer/Token.d.ts +105 -0
- package/build/lexer/Token.js +114 -0
- package/build/lexer/Token.js.map +1 -0
- package/build/parser/AST-nodes.d.ts +230 -0
- package/build/parser/AST-nodes.js +10 -0
- package/build/parser/AST-nodes.js.map +1 -0
- package/build/parser/BindingPowerTable.d.ts +8 -0
- package/build/parser/BindingPowerTable.js +37 -0
- package/build/parser/BindingPowerTable.js.map +1 -0
- package/build/parser/Parser.d.ts +208 -0
- package/build/parser/Parser.js +1215 -0
- package/build/parser/Parser.js.map +1 -0
- package/build/semantic/InternalTypes.d.ts +43 -0
- package/build/semantic/InternalTypes.js +95 -0
- package/build/semantic/InternalTypes.js.map +1 -0
- package/build/semantic/SemanticAnalyzer.d.ts +109 -0
- package/build/semantic/SemanticAnalyzer.js +998 -0
- package/build/semantic/SemanticAnalyzer.js.map +1 -0
- package/build/semantic/StdlibLoader.d.ts +18 -0
- package/build/semantic/StdlibLoader.js +42 -0
- package/build/semantic/StdlibLoader.js.map +1 -0
- package/build/semantic/SymbolTable.d.ts +64 -0
- package/build/semantic/SymbolTable.js +28 -0
- package/build/semantic/SymbolTable.js.map +1 -0
- package/build/utils/ErrorReporter.d.ts +29 -0
- package/build/utils/ErrorReporter.js +84 -0
- package/build/utils/ErrorReporter.js.map +1 -0
- package/build/utils/Logger.d.ts +32 -0
- package/build/utils/Logger.js +62 -0
- package/build/utils/Logger.js.map +1 -0
- package/build/utils/colors.d.ts +8 -0
- package/build/utils/colors.js +14 -0
- package/build/utils/colors.js.map +1 -0
- package/package.json +58 -0
- package/stdlib/clone.knip +5 -0
- package/stdlib/console.knip +6 -0
- package/stdlib/control.knip +3 -0
- package/stdlib/dict.knip +7 -0
- package/stdlib/events.knip +3 -0
- package/stdlib/list.knip +10 -0
- package/stdlib/looks.knip +4 -0
- package/stdlib/math.knip +7 -0
- package/stdlib/motion.knip +25 -0
- package/stdlib/pen.knip +14 -0
- package/stdlib/prelude.knip +25 -0
- package/stdlib/str.knip +3 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Contains the semantic analysis logic for the Katnip compiler, including type checking and symbol resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorReporter, KatnipError } from "../utils/ErrorReporter.js";
|
|
5
|
+
import { KatnipLog, KatnipLogType, Logger } from "../utils/Logger.js";
|
|
6
|
+
import { Scope, } from "./SymbolTable.js";
|
|
7
|
+
import { bindReceiver, isAssignable, isStr, substitute, typeToString, } from "./InternalTypes.js";
|
|
8
|
+
/** A placeholder declNode for symbols with no real source location (stdlib namespaces). */
|
|
9
|
+
function syntheticNode(type) {
|
|
10
|
+
return { type, loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } } };
|
|
11
|
+
}
|
|
12
|
+
export class SemanticAnalyzer {
|
|
13
|
+
reporter;
|
|
14
|
+
logger;
|
|
15
|
+
/** Current lexical scope. Changed by exit() and enter() */
|
|
16
|
+
current;
|
|
17
|
+
/** Declared return type of the procedure being visited, or null at the top level. */
|
|
18
|
+
currentReturnType = null;
|
|
19
|
+
/** Keeps global scope clean- is the parent of the global scope */
|
|
20
|
+
stdlibScope;
|
|
21
|
+
/** True only while resolving stdlib signatures, where T/K/V typevars are legal. */
|
|
22
|
+
allowTypevars = false;
|
|
23
|
+
constructor(reporter, logger = new Logger()) {
|
|
24
|
+
this.reporter = reporter;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
this.stdlibScope = new Scope("stdlib");
|
|
27
|
+
this.current = new Scope("global", this.stdlibScope);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs semantic analysis over a parsed AST.
|
|
31
|
+
* Pass 1 hoists top-level declarations (so forward references resolve);
|
|
32
|
+
* Pass 2 walks the tree resolving identifiers and enforcing scope rules.
|
|
33
|
+
* @returns The global scope, for downstream passes (IR/codegen).
|
|
34
|
+
*/
|
|
35
|
+
analyze(ast) {
|
|
36
|
+
this.logger.print(new KatnipLog(KatnipLogType.Info, `--- Semantic analysis started with ${ast.body.length} top-level statements ---`));
|
|
37
|
+
this.hoist(ast.body);
|
|
38
|
+
for (const stmt of ast.body)
|
|
39
|
+
this.visit(stmt);
|
|
40
|
+
return this.current;
|
|
41
|
+
}
|
|
42
|
+
// -- Stdlib loading --
|
|
43
|
+
/**
|
|
44
|
+
* Ingests pre-parsed stdlib modules before user analysis.
|
|
45
|
+
* Each file becomes a namespace named after its basename (motion.knip -> motion.*).
|
|
46
|
+
*/
|
|
47
|
+
loadStdlib(modules) {
|
|
48
|
+
for (const module of modules) {
|
|
49
|
+
const savedReporter = this.reporter;
|
|
50
|
+
const savedScope = this.current;
|
|
51
|
+
this.reporter = module.reporter;
|
|
52
|
+
this.allowTypevars = true;
|
|
53
|
+
const isPrelude = module.namespace === "prelude";
|
|
54
|
+
const target = isPrelude ? this.stdlibScope : new Scope("namespace", this.stdlibScope);
|
|
55
|
+
this.current = target;
|
|
56
|
+
this.hoist(module.ast.body);
|
|
57
|
+
this.finalizeStdlibScope(module.ast.body);
|
|
58
|
+
this.current = savedScope;
|
|
59
|
+
this.allowTypevars = false;
|
|
60
|
+
this.reporter = savedReporter;
|
|
61
|
+
if (!isPrelude) {
|
|
62
|
+
this.stdlibScope.declare({
|
|
63
|
+
kind: "namespace",
|
|
64
|
+
name: module.namespace,
|
|
65
|
+
declNode: syntheticNode("NamespaceDeclaration"),
|
|
66
|
+
scope: target,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (module.reporter.hasErrors()) {
|
|
70
|
+
console.error(`Internal compiler error in stdlib file '${module.sourcePath}' (this is a Katnip bug):`);
|
|
71
|
+
module.reporter.print();
|
|
72
|
+
throw new Error(`stdlib module '${module.namespace}' failed to load`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Resolve hoisted stdlib so they can be earglerly loaded. */
|
|
77
|
+
finalizeStdlibScope(body) {
|
|
78
|
+
for (const stmt of body) {
|
|
79
|
+
if (stmt.type === "ProcedureDeclaration") {
|
|
80
|
+
const procs = this.current
|
|
81
|
+
.lookupLocal(stmt.name)
|
|
82
|
+
?.filter((s) => s.kind === "procedure");
|
|
83
|
+
const sig = procs
|
|
84
|
+
?.flatMap((p) => p.signatures)
|
|
85
|
+
.find((s) => s.params === stmt.parameters);
|
|
86
|
+
if (!sig)
|
|
87
|
+
continue; // hoist already reported a redeclaration error
|
|
88
|
+
sig.resolvedParamTypes = stmt.parameters.map((p) => this.typeFromNode(p.paramType));
|
|
89
|
+
sig.resolvedReturn = stmt.returnType
|
|
90
|
+
? this.typeFromNode(stmt.returnType)
|
|
91
|
+
: { kind: "primitive", name: "void" };
|
|
92
|
+
}
|
|
93
|
+
else if (stmt.type === "VariableDeclaration") {
|
|
94
|
+
if (!stmt.varType) {
|
|
95
|
+
this.error(`Stdlib constant '${stmt.name}' must have a type annotation`, stmt);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const sym = this.current.lookupLocal(stmt.name)?.find((s) => s.declNode === stmt);
|
|
99
|
+
if (sym)
|
|
100
|
+
sym.cachedType = this.typeFromNode(stmt.varType);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// -- Scope stack --
|
|
105
|
+
/** Pushes a fresh child scope and makes it current. */
|
|
106
|
+
enter(kind) {
|
|
107
|
+
this.current = new Scope(kind, this.current);
|
|
108
|
+
}
|
|
109
|
+
/** Pops back to the parent scope. */
|
|
110
|
+
exit() {
|
|
111
|
+
if (this.current.parent)
|
|
112
|
+
this.current = this.current.parent;
|
|
113
|
+
}
|
|
114
|
+
/** Whether the current scope is inside a procedure body (where `return` is valid). */
|
|
115
|
+
inProcedure() {
|
|
116
|
+
for (let scope = this.current; scope; scope = scope.parent) {
|
|
117
|
+
if (scope.kind === "procedure")
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/** Whether the current scope is inside a sprite body (where `self` is valid). */
|
|
123
|
+
inSprite() {
|
|
124
|
+
for (let scope = this.current; scope; scope = scope.parent) {
|
|
125
|
+
if (scope.kind === "sprite")
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
/** Declares a symbol in the current scope, reporting on illegal redeclaration. */
|
|
131
|
+
declare(sym, node) {
|
|
132
|
+
const conflict = this.current.declare(sym);
|
|
133
|
+
if (conflict)
|
|
134
|
+
this.error(`'${sym.name}' is already declared in this scope`, node);
|
|
135
|
+
}
|
|
136
|
+
// -- Pass 1: hoist top-level declarations --
|
|
137
|
+
/**
|
|
138
|
+
* Declares procedures, enums, sprites, and top-level variables from a statement list into the current scope before bodies are walked.
|
|
139
|
+
* This is so that they can be referenced early. Top-level variables are the file's shared namespace, visible to every sprite regardless of declaration order.
|
|
140
|
+
*/
|
|
141
|
+
hoist(body) {
|
|
142
|
+
for (const stmt of body) {
|
|
143
|
+
switch (stmt.type) {
|
|
144
|
+
case "ProcedureDeclaration":
|
|
145
|
+
this.hoistProcedure(stmt);
|
|
146
|
+
break;
|
|
147
|
+
case "EnumDeclaration":
|
|
148
|
+
this.hoistEnum(stmt);
|
|
149
|
+
break;
|
|
150
|
+
case "SpriteDeclaration":
|
|
151
|
+
this.hoistSprite(stmt);
|
|
152
|
+
break;
|
|
153
|
+
case "VariableDeclaration":
|
|
154
|
+
this.hoistVariable(stmt);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
hoistProcedure(node) {
|
|
160
|
+
this.declare({
|
|
161
|
+
kind: "procedure",
|
|
162
|
+
name: node.name,
|
|
163
|
+
declNode: node,
|
|
164
|
+
signatures: [
|
|
165
|
+
{
|
|
166
|
+
params: node.parameters,
|
|
167
|
+
returnType: node.returnType,
|
|
168
|
+
meta: this.extractMeta(node.decorators),
|
|
169
|
+
decorators: node.decorators,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}, node);
|
|
173
|
+
}
|
|
174
|
+
/** Pulls codegen-relevant decorators (@opcode, @hat) out of a proc's decorator list. */
|
|
175
|
+
extractMeta(decorators) {
|
|
176
|
+
const meta = {};
|
|
177
|
+
for (const decorator of decorators) {
|
|
178
|
+
const value = decorator.value.type === "Literal" ? decorator.value.value : undefined;
|
|
179
|
+
// A bare `@hat` parses as the string literal "true".
|
|
180
|
+
if (decorator.name === "opcode" && typeof value === "string")
|
|
181
|
+
meta.opcode = value;
|
|
182
|
+
if (decorator.name === "hat")
|
|
183
|
+
meta.hat = value === "true" || value === true;
|
|
184
|
+
}
|
|
185
|
+
return meta;
|
|
186
|
+
}
|
|
187
|
+
hoistEnum(node) {
|
|
188
|
+
this.declare({ kind: "enum", name: node.name, declNode: node, members: node.members }, node);
|
|
189
|
+
}
|
|
190
|
+
hoistSprite(node) {
|
|
191
|
+
this.declare({ kind: "sprite", name: node.name, declNode: node }, node);
|
|
192
|
+
}
|
|
193
|
+
hoistVariable(node) {
|
|
194
|
+
this.declare({
|
|
195
|
+
kind: "variable",
|
|
196
|
+
name: node.name,
|
|
197
|
+
declNode: node,
|
|
198
|
+
type: node.varType,
|
|
199
|
+
access: node.access,
|
|
200
|
+
}, node);
|
|
201
|
+
}
|
|
202
|
+
// -- Pass 2: resolve --
|
|
203
|
+
/** Dispatches a statement to its handler. */
|
|
204
|
+
visit(node) {
|
|
205
|
+
switch (node.type) {
|
|
206
|
+
case "VariableDeclaration": {
|
|
207
|
+
// `private` at the top scope is valid: the variable is visible to
|
|
208
|
+
// sprites in this file but cannot be imported into other files.
|
|
209
|
+
const initType = node.initializer ? this.inferType(node.initializer) : null;
|
|
210
|
+
const varType = node.varType ? this.typeFromNode(node.varType) : null;
|
|
211
|
+
if (!initType && !varType) {
|
|
212
|
+
this.error(`Variable '${node.name}' must have either a type annotation or an initial value`, node);
|
|
213
|
+
}
|
|
214
|
+
if (initType && varType && !isAssignable(initType, varType)) {
|
|
215
|
+
this.error(`Variable '${node.name}' of type '${typeToString(varType)}' cannot be initialized with value of type '${typeToString(initType)}'`, node);
|
|
216
|
+
}
|
|
217
|
+
if (this.current.kind !== "global") {
|
|
218
|
+
this.declare({
|
|
219
|
+
kind: "variable",
|
|
220
|
+
name: node.name,
|
|
221
|
+
declNode: node,
|
|
222
|
+
type: node.varType,
|
|
223
|
+
access: node.access,
|
|
224
|
+
}, node);
|
|
225
|
+
}
|
|
226
|
+
const sym = this.current.lookupLocal(node.name)?.find(s => s.declNode === node);
|
|
227
|
+
if (sym)
|
|
228
|
+
sym.cachedType = varType ?? initType ?? { kind: "unknown" };
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "VariableAssignment": {
|
|
232
|
+
const target = this.inferType(node.left);
|
|
233
|
+
const value = this.inferType(node.right);
|
|
234
|
+
if (!isAssignable(value, target)) {
|
|
235
|
+
this.error(`Cannot assign value of type '${typeToString(value)}' to target of type '${typeToString(target)}'`, node);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "ProcedureDeclaration": {
|
|
240
|
+
if (node.access === "temp") {
|
|
241
|
+
this.error("Procedures cannot be declared 'temp'", node, node.access.length);
|
|
242
|
+
}
|
|
243
|
+
const previousReturnType = this.currentReturnType;
|
|
244
|
+
this.currentReturnType = node.returnType
|
|
245
|
+
? this.typeFromNode(node.returnType)
|
|
246
|
+
: { kind: "primitive", name: "void" };
|
|
247
|
+
this.enter("procedure");
|
|
248
|
+
for (const param of node.parameters) {
|
|
249
|
+
this.declare({
|
|
250
|
+
kind: "parameter",
|
|
251
|
+
name: param.name,
|
|
252
|
+
declNode: param,
|
|
253
|
+
type: param.paramType,
|
|
254
|
+
}, param);
|
|
255
|
+
}
|
|
256
|
+
for (const stmt of node.body.body)
|
|
257
|
+
this.visit(stmt);
|
|
258
|
+
this.exit();
|
|
259
|
+
this.currentReturnType = previousReturnType;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "SpriteDeclaration":
|
|
263
|
+
this.visitBlock(node.body, "sprite");
|
|
264
|
+
break;
|
|
265
|
+
case "HandlerStatement": {
|
|
266
|
+
if (this.current.kind !== "sprite") {
|
|
267
|
+
this.error("Event handlers can only appear at the top level of a sprite", node.call);
|
|
268
|
+
}
|
|
269
|
+
const resolved = this.resolveCall(node.call, true);
|
|
270
|
+
if (resolved.signature && !resolved.signature.meta?.hat) {
|
|
271
|
+
this.error(`'${this.calleeName(node.call)}' is not a hat block and cannot be used as an event handler`, node.call);
|
|
272
|
+
}
|
|
273
|
+
this.visitBlock(node.body);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "IfStatement":
|
|
277
|
+
this.checkCondition(node.condition, "'if' statement");
|
|
278
|
+
this.visitBlock(node.thenBlock);
|
|
279
|
+
for (const elif of node.elifs) {
|
|
280
|
+
this.checkCondition(elif.condition, "'elif' clause");
|
|
281
|
+
this.visitBlock(elif.block);
|
|
282
|
+
}
|
|
283
|
+
if (node.elseBlock)
|
|
284
|
+
this.visitBlock(node.elseBlock);
|
|
285
|
+
break;
|
|
286
|
+
case "WhileStatement":
|
|
287
|
+
this.checkCondition(node.condition, "'while' loop");
|
|
288
|
+
this.visitBlock(node.body);
|
|
289
|
+
break;
|
|
290
|
+
case "DoWhileStatement":
|
|
291
|
+
this.checkCondition(node.condition, "'do-while' loop");
|
|
292
|
+
this.visitBlock(node.body);
|
|
293
|
+
break;
|
|
294
|
+
case "ForStatement": {
|
|
295
|
+
const elementType = this.iterationType(this.inferType(node.iterable));
|
|
296
|
+
const isTuple = node.pattern.type === "TupleExpression";
|
|
297
|
+
const loopVars = node.pattern.type === "TupleExpression"
|
|
298
|
+
? node.pattern.elements
|
|
299
|
+
: [node.pattern];
|
|
300
|
+
let partTypes;
|
|
301
|
+
if (!isTuple) {
|
|
302
|
+
partTypes = [elementType];
|
|
303
|
+
}
|
|
304
|
+
else if (elementType.kind === "tuple" &&
|
|
305
|
+
elementType.elements.length === loopVars.length) {
|
|
306
|
+
partTypes = elementType.elements;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
if (elementType.kind !== "unknown") {
|
|
310
|
+
this.error(`Cannot destructure '${typeToString(elementType)}' into ${loopVars.length} loop variables`, node.pattern);
|
|
311
|
+
}
|
|
312
|
+
partTypes = loopVars.map(() => ({ kind: "unknown" }));
|
|
313
|
+
}
|
|
314
|
+
this.enter("block");
|
|
315
|
+
loopVars.forEach((loopVar, i) => {
|
|
316
|
+
if (loopVar.type !== "Identifier") {
|
|
317
|
+
this.error("Loop variable must be an identifier", loopVar);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
this.declare({
|
|
321
|
+
kind: "loopVar",
|
|
322
|
+
name: loopVar.name,
|
|
323
|
+
declNode: loopVar,
|
|
324
|
+
type: null,
|
|
325
|
+
cachedType: partTypes[i],
|
|
326
|
+
}, loopVar);
|
|
327
|
+
});
|
|
328
|
+
for (const stmt of node.body.body)
|
|
329
|
+
this.visit(stmt);
|
|
330
|
+
this.exit();
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "ExpressionStatement":
|
|
334
|
+
this.inferType(node.expression);
|
|
335
|
+
break;
|
|
336
|
+
case "ReturnStatement": {
|
|
337
|
+
if (!this.inProcedure()) {
|
|
338
|
+
this.error("'return' can only be used inside a procedure", node, "return".length);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
const returned = node.argument
|
|
342
|
+
? this.inferType(node.argument)
|
|
343
|
+
: { kind: "primitive", name: "void" };
|
|
344
|
+
const expected = this.currentReturnType ?? { kind: "primitive", name: "void" };
|
|
345
|
+
if (!isAssignable(returned, expected)) {
|
|
346
|
+
this.error(`Return value of type '${typeToString(returned)}' is not assignable to return type '${typeToString(expected)}'`, node.argument ?? node);
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "SwitchDeclaration": {
|
|
351
|
+
// TODO: Check for all cases covered or default case for enum; falthrough kward not in default and used correctly
|
|
352
|
+
const defaultCases = node.body.filter((caseEntry) => caseEntry.type === "DefaultCaseDeclaration");
|
|
353
|
+
if (defaultCases.length > 1) {
|
|
354
|
+
this.error(`2 or more 'default' cases found in switch-case statement`, defaultCases[1], "default".length);
|
|
355
|
+
}
|
|
356
|
+
if (node.body.length > 0
|
|
357
|
+
&& defaultCases.length === 1
|
|
358
|
+
&& node.body.at(-1)?.type !== "DefaultCaseDeclaration") {
|
|
359
|
+
this.error(`'default' case not located at bottom of switch-case statement`, defaultCases[0], "default".length);
|
|
360
|
+
}
|
|
361
|
+
this.inferType(node.value);
|
|
362
|
+
for (const caseEntry of node.body)
|
|
363
|
+
this.visit(caseEntry);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case "CaseDeclaration":
|
|
367
|
+
for (const caseExpr of node.values)
|
|
368
|
+
this.inferType(caseExpr);
|
|
369
|
+
this.visitBlock(node.body);
|
|
370
|
+
break;
|
|
371
|
+
case "DefaultCaseDeclaration":
|
|
372
|
+
this.visitBlock(node.body);
|
|
373
|
+
break;
|
|
374
|
+
case "EnumDeclaration":
|
|
375
|
+
// Enum body already hoisted; just validate the modifier.
|
|
376
|
+
if (node.access === "temp") {
|
|
377
|
+
this.error("Enums cannot be declared 'temp'", node, node.access.length);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case "ErrorStatement":
|
|
381
|
+
// Error statements come from the parser.
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/** Three-step reusable block visitor. */
|
|
386
|
+
visitBlock(block, kind = "block") {
|
|
387
|
+
this.enter(kind);
|
|
388
|
+
for (const statement of block.body)
|
|
389
|
+
this.visit(statement);
|
|
390
|
+
this.exit();
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Reports an error if a condition expression is not of type 'bool'.
|
|
394
|
+
* `unknown` passes: it is the escape hatch for not-yet-typed calls/members,
|
|
395
|
+
* so a single missing type does not cascade into false positives.
|
|
396
|
+
*/
|
|
397
|
+
checkCondition(condition, context) {
|
|
398
|
+
const type = this.inferType(condition);
|
|
399
|
+
if (type.kind === "unknown")
|
|
400
|
+
return;
|
|
401
|
+
if (type.kind !== "primitive" || type.name !== "bool") {
|
|
402
|
+
this.error(`${context} condition must be of type 'bool', not '${typeToString(type)}'`, condition);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** The type produced by one step of iterating over the given iterable type. */
|
|
406
|
+
iterationType(iterable) {
|
|
407
|
+
switch (iterable.kind) {
|
|
408
|
+
case "list":
|
|
409
|
+
return iterable.element;
|
|
410
|
+
case "dict":
|
|
411
|
+
return { kind: "tuple", elements: [iterable.key, iterable.value] };
|
|
412
|
+
case "primitive":
|
|
413
|
+
// str -> characters, num -> counter value
|
|
414
|
+
return iterable.name === "str" || iterable.name === "num"
|
|
415
|
+
? iterable
|
|
416
|
+
: { kind: "unknown" };
|
|
417
|
+
default:
|
|
418
|
+
return { kind: "unknown" };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Converts a written type annotation (TypeNode) into an InternalType. */
|
|
422
|
+
typeFromNode(node) {
|
|
423
|
+
if (node.type === "UnionType") {
|
|
424
|
+
return {
|
|
425
|
+
kind: "union",
|
|
426
|
+
left: this.typeFromNode(node.left),
|
|
427
|
+
right: this.typeFromNode(node.right),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (node.type === "TupleType") {
|
|
431
|
+
return {
|
|
432
|
+
kind: "tuple",
|
|
433
|
+
elements: node.elements.map((element) => this.typeFromNode(element)),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
// SingleTypeNode
|
|
437
|
+
const params = node.typeParams ?? [];
|
|
438
|
+
switch (node.typeName) {
|
|
439
|
+
case "num":
|
|
440
|
+
case "str":
|
|
441
|
+
case "bool":
|
|
442
|
+
case "void":
|
|
443
|
+
return { kind: "primitive", name: node.typeName };
|
|
444
|
+
case "list":
|
|
445
|
+
return {
|
|
446
|
+
kind: "list",
|
|
447
|
+
element: params[0]
|
|
448
|
+
? this.typeFromNode(params[0])
|
|
449
|
+
: { kind: "unknown" },
|
|
450
|
+
};
|
|
451
|
+
case "dict":
|
|
452
|
+
return {
|
|
453
|
+
kind: "dict",
|
|
454
|
+
key: params[0]
|
|
455
|
+
? this.typeFromNode(params[0])
|
|
456
|
+
: { kind: "unknown" },
|
|
457
|
+
value: params[1]
|
|
458
|
+
? this.typeFromNode(params[1])
|
|
459
|
+
: { kind: "unknown" },
|
|
460
|
+
};
|
|
461
|
+
default: {
|
|
462
|
+
// Any other stuff must resolve to an enum
|
|
463
|
+
const symbols = this.current.lookup(node.typeName);
|
|
464
|
+
if (node.typeName === "any")
|
|
465
|
+
return { kind: "unknown" };
|
|
466
|
+
if (this.allowTypevars && /^[A-Z]$/.test(node.typeName)) {
|
|
467
|
+
return { kind: "typevar", name: node.typeName };
|
|
468
|
+
}
|
|
469
|
+
if (symbols?.some((s) => s.kind === "enum")) {
|
|
470
|
+
return { kind: "enum", name: node.typeName };
|
|
471
|
+
}
|
|
472
|
+
this.error(`Unknown type '${node.typeName}'`, node);
|
|
473
|
+
return { kind: "unknown" };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Returns a symbol's resolved type, computing and caching it on first access
|
|
479
|
+
* For inferred (un-annotated) variables, the analyzer is expected to have already written `cachedType` during inference
|
|
480
|
+
*/
|
|
481
|
+
typeOf(sym) {
|
|
482
|
+
if (sym.cachedType)
|
|
483
|
+
return sym.cachedType;
|
|
484
|
+
let resolved;
|
|
485
|
+
switch (sym.kind) {
|
|
486
|
+
case "variable":
|
|
487
|
+
case "parameter":
|
|
488
|
+
case "loopVar":
|
|
489
|
+
resolved = sym.type
|
|
490
|
+
? this.typeFromNode(sym.type)
|
|
491
|
+
: { kind: "unknown" };
|
|
492
|
+
break;
|
|
493
|
+
case "enum":
|
|
494
|
+
resolved = { kind: "enum", name: sym.name };
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
resolved = { kind: "unknown" }; // procedures/sprites aren't values
|
|
498
|
+
}
|
|
499
|
+
sym.cachedType = resolved;
|
|
500
|
+
return resolved;
|
|
501
|
+
}
|
|
502
|
+
inferType(expression) {
|
|
503
|
+
switch (expression.type) {
|
|
504
|
+
case "Identifier": {
|
|
505
|
+
const sym = this.current.lookup(expression.name)?.[0];
|
|
506
|
+
if (!sym) {
|
|
507
|
+
this.error(`'${expression.name}' is not defined`, expression);
|
|
508
|
+
return { kind: "unknown" };
|
|
509
|
+
}
|
|
510
|
+
return this.typeOf(sym);
|
|
511
|
+
}
|
|
512
|
+
case "BinaryExpression": {
|
|
513
|
+
let l = this.inferType(expression.left);
|
|
514
|
+
let r = this.inferType(expression.right);
|
|
515
|
+
const bothList = l.kind === "list" && r.kind === "list" && isAssignable(l, r);
|
|
516
|
+
const bothEnum = l.kind === "enum" && r.kind === "enum" && isAssignable(l, r);
|
|
517
|
+
const isComparison = expression.operator === "==" ||
|
|
518
|
+
expression.operator === "<" ||
|
|
519
|
+
expression.operator === ">" ||
|
|
520
|
+
expression.operator === "<=" ||
|
|
521
|
+
expression.operator === ">=";
|
|
522
|
+
const matchedPair = bothList || (bothEnum && isComparison);
|
|
523
|
+
// `unknown` is the escape hatch (not-yet-typed calls/members);
|
|
524
|
+
// let it pass so one missing type doesn't cascade into errors.
|
|
525
|
+
if (!matchedPair &&
|
|
526
|
+
l.kind !== "unknown" &&
|
|
527
|
+
r.kind !== "unknown" &&
|
|
528
|
+
!(l.kind === "primitive" && r.kind === "primitive")) {
|
|
529
|
+
this.error(`Cannot use binop between two variables that are not both of type: 'primitive', 'list', or 'enum'`, expression);
|
|
530
|
+
}
|
|
531
|
+
switch (expression.operator) {
|
|
532
|
+
// arithmetic
|
|
533
|
+
case "+":
|
|
534
|
+
if (l.kind === "list" && matchedPair)
|
|
535
|
+
return l; // concatenation
|
|
536
|
+
return isStr(l) || isStr(r)
|
|
537
|
+
? { kind: "primitive", name: "str" }
|
|
538
|
+
: { kind: "primitive", name: "num" };
|
|
539
|
+
case "-":
|
|
540
|
+
case "*":
|
|
541
|
+
case "/":
|
|
542
|
+
case "%":
|
|
543
|
+
case "**":
|
|
544
|
+
return { kind: "primitive", name: "num" };
|
|
545
|
+
// comparison
|
|
546
|
+
case "==":
|
|
547
|
+
case "<":
|
|
548
|
+
case ">":
|
|
549
|
+
case "<=":
|
|
550
|
+
case ">=":
|
|
551
|
+
return { kind: "primitive", name: "bool" };
|
|
552
|
+
// logical
|
|
553
|
+
case "&&":
|
|
554
|
+
case "||":
|
|
555
|
+
case "!&":
|
|
556
|
+
case "!|":
|
|
557
|
+
case "!^":
|
|
558
|
+
case "^":
|
|
559
|
+
return { kind: "primitive", name: "bool" };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
case "CallExpression":
|
|
563
|
+
return this.resolveCall(expression).type;
|
|
564
|
+
case "DictExpression": {
|
|
565
|
+
if (expression.entries.length == 0)
|
|
566
|
+
return { kind: "dict", key: { kind: "unknown" }, value: { kind: "unknown" } };
|
|
567
|
+
const firstKeyType = this.inferType(expression.entries[0].key);
|
|
568
|
+
const firstValueType = this.inferType(expression.entries[0].value);
|
|
569
|
+
for (const [i, entry] of expression.entries.entries()) {
|
|
570
|
+
const keyType = this.inferType(entry.key);
|
|
571
|
+
const valueType = this.inferType(entry.value);
|
|
572
|
+
if (!isAssignable(keyType, firstKeyType)) {
|
|
573
|
+
this.error(`Expected key of type ${typeToString(firstKeyType)}, got ${typeToString(keyType)}`, entry.key);
|
|
574
|
+
return {
|
|
575
|
+
kind: "dict",
|
|
576
|
+
key: { kind: "unknown" },
|
|
577
|
+
value: { kind: "unknown" },
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (!isAssignable(valueType, firstValueType)) {
|
|
581
|
+
this.error(`Expected value of type ${typeToString(firstValueType)}, got ${typeToString(valueType)}`, entry.value);
|
|
582
|
+
return {
|
|
583
|
+
kind: "dict",
|
|
584
|
+
key: { kind: "unknown" },
|
|
585
|
+
value: { kind: "unknown" },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
kind: "dict",
|
|
591
|
+
key: firstKeyType,
|
|
592
|
+
value: firstValueType,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
case "IndexerAccess": {
|
|
596
|
+
const objectType = this.inferType(expression.object);
|
|
597
|
+
this.inferType(expression.index);
|
|
598
|
+
switch (objectType.kind) {
|
|
599
|
+
case "list":
|
|
600
|
+
return objectType.element;
|
|
601
|
+
case "dict":
|
|
602
|
+
return objectType.value;
|
|
603
|
+
case "tuple":
|
|
604
|
+
return objectType.elements[0]; // TODO: naive. Doesn't really work
|
|
605
|
+
case "primitive":
|
|
606
|
+
if (objectType.name === "str")
|
|
607
|
+
return objectType;
|
|
608
|
+
break;
|
|
609
|
+
case "unknown":
|
|
610
|
+
return { kind: "unknown" };
|
|
611
|
+
}
|
|
612
|
+
this.error(`Cannot index object of type ${typeToString(objectType)}`, expression);
|
|
613
|
+
return { kind: "unknown" };
|
|
614
|
+
}
|
|
615
|
+
case "InterpolatedString":
|
|
616
|
+
for (const entry of expression.parts) {
|
|
617
|
+
if (!(typeof entry === "string")) {
|
|
618
|
+
this.inferType(entry);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return { kind: "primitive", name: "str" };
|
|
622
|
+
case "ListExpression": {
|
|
623
|
+
if (expression.elements.length == 0) {
|
|
624
|
+
return { kind: "list", element: { kind: "unknown" } };
|
|
625
|
+
}
|
|
626
|
+
const types = [];
|
|
627
|
+
for (const element of expression.elements) {
|
|
628
|
+
const t = this.inferType(element);
|
|
629
|
+
const seen = types.some((existing) => isAssignable(existing, t) && isAssignable(t, existing));
|
|
630
|
+
if (!seen)
|
|
631
|
+
types.push(t);
|
|
632
|
+
}
|
|
633
|
+
let element = types[0];
|
|
634
|
+
for (let i = 1; i < types.length; i++) {
|
|
635
|
+
element = { kind: "union", left: element, right: types[i] };
|
|
636
|
+
}
|
|
637
|
+
return { kind: "list", element };
|
|
638
|
+
}
|
|
639
|
+
case "Literal":
|
|
640
|
+
switch (expression.valueType) {
|
|
641
|
+
case "Number":
|
|
642
|
+
return { kind: "primitive", name: "num" };
|
|
643
|
+
case "String":
|
|
644
|
+
return { kind: "primitive", name: "str" };
|
|
645
|
+
case "Boolean":
|
|
646
|
+
return { kind: "primitive", name: "bool" };
|
|
647
|
+
case "Null":
|
|
648
|
+
return { kind: "unknown" };
|
|
649
|
+
}
|
|
650
|
+
case "MemberExpression":
|
|
651
|
+
return this.resolveMemberAccess(expression);
|
|
652
|
+
case "SliceAccess": {
|
|
653
|
+
const objectType = this.inferType(expression.object);
|
|
654
|
+
if (expression.start)
|
|
655
|
+
this.inferType(expression.start);
|
|
656
|
+
if (expression.end)
|
|
657
|
+
this.inferType(expression.end);
|
|
658
|
+
if (expression.step)
|
|
659
|
+
this.inferType(expression.step);
|
|
660
|
+
return objectType;
|
|
661
|
+
}
|
|
662
|
+
case "TupleExpression": {
|
|
663
|
+
const elements = expression.elements.map(item => this.inferType(item));
|
|
664
|
+
return { kind: "tuple", elements };
|
|
665
|
+
}
|
|
666
|
+
case "UnaryExpression":
|
|
667
|
+
const right = this.inferType(expression.argument);
|
|
668
|
+
switch (expression.operator) {
|
|
669
|
+
case "!":
|
|
670
|
+
if (!(right.kind === "primitive" && right.name === "bool")) {
|
|
671
|
+
this.error(`Must use '!' unop with a boolean expression type`, expression);
|
|
672
|
+
}
|
|
673
|
+
return { kind: "primitive", name: "bool" };
|
|
674
|
+
case "-":
|
|
675
|
+
if (!(right.kind === "primitive" && right.name === "num")) {
|
|
676
|
+
this.error(`Must use '-' unop with a numeric expression type`, expression);
|
|
677
|
+
}
|
|
678
|
+
return { kind: "primitive", name: "num" };
|
|
679
|
+
}
|
|
680
|
+
case "EmptyExpression":
|
|
681
|
+
return { kind: "unknown" };
|
|
682
|
+
case "ErrorToken":
|
|
683
|
+
return { kind: "unknown" };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Resolves call by shape of callee.
|
|
688
|
+
* Hat blocks are rejected here unless resolving on behalf of a handler.
|
|
689
|
+
*/
|
|
690
|
+
resolveCall(call, asHandler = false) {
|
|
691
|
+
const args = this.collectArguments(call);
|
|
692
|
+
let resolved;
|
|
693
|
+
if (call.object.type === "Identifier") {
|
|
694
|
+
resolved = this.resolveProcedureCall(call, call.object.name, args);
|
|
695
|
+
}
|
|
696
|
+
else if (call.object.type === "MemberExpression") {
|
|
697
|
+
resolved = this.resolveMemberCall(call, call.object, args);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
this.inferType(call.object);
|
|
701
|
+
this.error(`This expression is not callable`, call.object);
|
|
702
|
+
resolved = { type: { kind: "unknown" } };
|
|
703
|
+
}
|
|
704
|
+
if (!asHandler && resolved.signature?.meta?.hat) {
|
|
705
|
+
this.error(`'${this.calleeName(call)}' is a hat block and can only be used as an event handler`, call);
|
|
706
|
+
}
|
|
707
|
+
return resolved;
|
|
708
|
+
}
|
|
709
|
+
/** Display name of a call's callee for error messages ("add", "motion.move") */
|
|
710
|
+
calleeName(call) {
|
|
711
|
+
const callee = call.object;
|
|
712
|
+
if (callee.type === "Identifier")
|
|
713
|
+
return callee.name;
|
|
714
|
+
if (callee.type === "MemberExpression") {
|
|
715
|
+
const prefix = callee.object.type === "Identifier" ? `${callee.object.name}.` : "";
|
|
716
|
+
return `${prefix}${callee.property.name}`;
|
|
717
|
+
}
|
|
718
|
+
return "<expression>";
|
|
719
|
+
}
|
|
720
|
+
/** Parse and enforce argument */
|
|
721
|
+
collectArguments(call) {
|
|
722
|
+
const positional = [];
|
|
723
|
+
const named = new Map();
|
|
724
|
+
for (const arg of call.arguments) {
|
|
725
|
+
if (arg.type === "NamedArgument") {
|
|
726
|
+
// TODO: a repeated name (same named arg supplied twice) could be reported here before overwriting the map entry.
|
|
727
|
+
named.set(arg.name, { type: this.inferType(arg.value), node: arg.value });
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
if (named.size > 0) {
|
|
731
|
+
this.error(`Positional argument cannot follow a named argument`, arg);
|
|
732
|
+
}
|
|
733
|
+
positional.push({ type: this.inferType(arg), node: arg });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return { positional, named };
|
|
737
|
+
}
|
|
738
|
+
/** Resolves name of call and finds corresponding override */
|
|
739
|
+
resolveProcedureCall(call, name, args) {
|
|
740
|
+
const candidates = this.current.lookup(name);
|
|
741
|
+
if (!candidates) {
|
|
742
|
+
this.error(`'${name}' is not defined`, call.object);
|
|
743
|
+
return { type: { kind: "unknown" } };
|
|
744
|
+
}
|
|
745
|
+
// A name can carry several procedure overloads; anything else isn't callable.
|
|
746
|
+
const overloads = candidates
|
|
747
|
+
.filter((s) => s.kind === "procedure")
|
|
748
|
+
.flatMap((s) => s.signatures);
|
|
749
|
+
if (overloads.length === 0) {
|
|
750
|
+
this.error(`'${name}' is not a procedure and cannot be called`, call.object);
|
|
751
|
+
return { type: { kind: "unknown" } };
|
|
752
|
+
}
|
|
753
|
+
return this.callProcedure(call, name, overloads, args);
|
|
754
|
+
}
|
|
755
|
+
/** Resolves a member-callee call: `self.*`, a namespace bltn (motion.move(...)), or a method on a value's type (li.contains(...)) */
|
|
756
|
+
resolveMemberCall(call, callee, args) {
|
|
757
|
+
const objNode = callee.object;
|
|
758
|
+
const propName = callee.property.name;
|
|
759
|
+
const head = this.resolveMemberHead(objNode);
|
|
760
|
+
switch (head.kind) {
|
|
761
|
+
case "self":
|
|
762
|
+
case "error": return { type: { kind: "unknown" } };
|
|
763
|
+
case "namespace": {
|
|
764
|
+
const members = head.symbol.scope.lookupLocal(propName);
|
|
765
|
+
if (!members) {
|
|
766
|
+
this.error(`Namespace '${head.name}' has no member '${propName}'`, callee.property);
|
|
767
|
+
return { type: { kind: "unknown" } };
|
|
768
|
+
}
|
|
769
|
+
const overloads = members
|
|
770
|
+
.filter((s) => s.kind === "procedure")
|
|
771
|
+
.flatMap((s) => s.signatures);
|
|
772
|
+
if (overloads.length === 0) {
|
|
773
|
+
this.error(`'${head.name}.${propName}' is not a procedure and cannot be called`, callee);
|
|
774
|
+
return { type: { kind: "unknown" } };
|
|
775
|
+
}
|
|
776
|
+
return this.callProcedure(call, `${head.name}.${propName}`, overloads, args);
|
|
777
|
+
}
|
|
778
|
+
case "enum":
|
|
779
|
+
this.error(`'${head.name}.${propName}' is an enum member and cannot be called`, callee);
|
|
780
|
+
return { type: { kind: "unknown" } };
|
|
781
|
+
case "value": break; // fall through to value-method resolution
|
|
782
|
+
}
|
|
783
|
+
const receiver = this.inferType(objNode);
|
|
784
|
+
if (receiver.kind === "unknown")
|
|
785
|
+
return { type: { kind: "unknown" } };
|
|
786
|
+
const nsName = this.typeHeadNamespace(receiver);
|
|
787
|
+
if (!nsName) {
|
|
788
|
+
this.error(`Type '${typeToString(receiver)}' has no methods`, objNode);
|
|
789
|
+
return { type: { kind: "unknown" } };
|
|
790
|
+
}
|
|
791
|
+
// Stdlib self calls get special case resolving.
|
|
792
|
+
const overloads = (this.stdlibNamespace(nsName)?.scope.lookupLocal(propName) ?? [])
|
|
793
|
+
.filter((s) => s.kind === "procedure")
|
|
794
|
+
.flatMap((s) => s.signatures)
|
|
795
|
+
.filter((sig) => sig.params[0]?.name === "self");
|
|
796
|
+
if (overloads.length === 0) {
|
|
797
|
+
this.error(`'${propName}' is not a method of type '${typeToString(receiver)}'`, callee.property);
|
|
798
|
+
return { type: { kind: "unknown" } };
|
|
799
|
+
}
|
|
800
|
+
const methodArgs = {
|
|
801
|
+
positional: [{ type: receiver, node: objNode }, ...args.positional],
|
|
802
|
+
named: args.named,
|
|
803
|
+
};
|
|
804
|
+
return this.callProcedure(call, propName, overloads, methodArgs);
|
|
805
|
+
}
|
|
806
|
+
/** Resolves a non-call member access: namespace consts (math.pi) etc. */
|
|
807
|
+
resolveMemberAccess(expr) {
|
|
808
|
+
const objNode = expr.object;
|
|
809
|
+
const propName = expr.property.name;
|
|
810
|
+
const head = this.resolveMemberHead(objNode);
|
|
811
|
+
switch (head.kind) {
|
|
812
|
+
case "self":
|
|
813
|
+
case "error": return { kind: "unknown" };
|
|
814
|
+
case "namespace": {
|
|
815
|
+
const members = head.symbol.scope.lookupLocal(propName);
|
|
816
|
+
if (!members) {
|
|
817
|
+
this.error(`Namespace '${head.name}' has no member '${propName}'`, expr.property);
|
|
818
|
+
return { kind: "unknown" };
|
|
819
|
+
}
|
|
820
|
+
const member = members[0];
|
|
821
|
+
if (member.kind === "procedure") {
|
|
822
|
+
this.error(`'${head.name}.${propName}' is a procedure — call it with (...)`, expr);
|
|
823
|
+
return { kind: "unknown" };
|
|
824
|
+
}
|
|
825
|
+
return this.typeOf(member);
|
|
826
|
+
}
|
|
827
|
+
case "enum":
|
|
828
|
+
if (!head.symbol.members.includes(propName)) {
|
|
829
|
+
this.error(`Enum '${head.symbol.name}' has no member '${propName}'`, expr.property);
|
|
830
|
+
return { kind: "unknown" };
|
|
831
|
+
}
|
|
832
|
+
return { kind: "enum", name: head.symbol.name };
|
|
833
|
+
case "value": break; // fall through to value-member resolution
|
|
834
|
+
}
|
|
835
|
+
const receiver = this.inferType(objNode);
|
|
836
|
+
if (receiver.kind === "unknown")
|
|
837
|
+
return { kind: "unknown" };
|
|
838
|
+
const nsName = this.typeHeadNamespace(receiver);
|
|
839
|
+
const members = nsName
|
|
840
|
+
? this.stdlibNamespace(nsName)?.scope.lookupLocal(propName)
|
|
841
|
+
: null;
|
|
842
|
+
if (members?.some((s) => s.kind === "procedure")) {
|
|
843
|
+
this.error(`'${propName}' is a method — call it with (...)`, expr.property);
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
this.error(`Type '${typeToString(receiver)}' has no member '${propName}'`, expr.property);
|
|
847
|
+
}
|
|
848
|
+
return { kind: "unknown" };
|
|
849
|
+
}
|
|
850
|
+
/** Classifies a member expression's object identifier, emitting the shared self/undefined errors. */
|
|
851
|
+
resolveMemberHead(objNode) {
|
|
852
|
+
if (objNode.type !== "Identifier")
|
|
853
|
+
return { kind: "value" };
|
|
854
|
+
if (objNode.name === "self") {
|
|
855
|
+
if (!this.inSprite())
|
|
856
|
+
this.error(`'self' can only be used inside a sprite`, objNode);
|
|
857
|
+
return { kind: "self" };
|
|
858
|
+
}
|
|
859
|
+
const symbols = this.current.lookup(objNode.name);
|
|
860
|
+
const namespace = symbols?.find((s) => s.kind === "namespace");
|
|
861
|
+
if (namespace)
|
|
862
|
+
return { kind: "namespace", symbol: namespace, name: objNode.name };
|
|
863
|
+
const enumSym = symbols?.find((s) => s.kind === "enum");
|
|
864
|
+
if (enumSym)
|
|
865
|
+
return { kind: "enum", symbol: enumSym, name: objNode.name };
|
|
866
|
+
if (!symbols) {
|
|
867
|
+
this.error(`'${objNode.name}' is not defined`, objNode);
|
|
868
|
+
return { kind: "error" };
|
|
869
|
+
}
|
|
870
|
+
return { kind: "value" }; // a plain variable: resolve via its type below
|
|
871
|
+
}
|
|
872
|
+
/** The stdlib namespace that holds methods for a receiver type, if any. */
|
|
873
|
+
typeHeadNamespace(type) {
|
|
874
|
+
switch (type.kind) {
|
|
875
|
+
case "list": return "list";
|
|
876
|
+
case "dict": return "dict";
|
|
877
|
+
case "primitive":
|
|
878
|
+
return type.name === "void" ? null : type.name;
|
|
879
|
+
default:
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** Looks a namespace up directly in the stdlib scope (immune to user shadowing). */
|
|
884
|
+
stdlibNamespace(name) {
|
|
885
|
+
return (this.stdlibScope
|
|
886
|
+
.lookupLocal(name)
|
|
887
|
+
?.find((s) => s.kind === "namespace") ?? null);
|
|
888
|
+
}
|
|
889
|
+
/** Overload selection. */
|
|
890
|
+
callProcedure(call, name, overloads, args) {
|
|
891
|
+
const instantiated = overloads.map((sig) => {
|
|
892
|
+
if (!sig.resolvedParamTypes)
|
|
893
|
+
return sig;
|
|
894
|
+
const bindings = new Map();
|
|
895
|
+
sig.resolvedParamTypes.forEach((paramType, i) => {
|
|
896
|
+
const arg = args.positional[i] ?? args.named.get(sig.params[i]?.name);
|
|
897
|
+
if (arg)
|
|
898
|
+
bindReceiver(paramType, arg.type, bindings);
|
|
899
|
+
});
|
|
900
|
+
return {
|
|
901
|
+
...sig,
|
|
902
|
+
resolvedParamTypes: sig.resolvedParamTypes.map((t) => substitute(t, bindings)),
|
|
903
|
+
resolvedReturn: sig.resolvedReturn
|
|
904
|
+
? substitute(sig.resolvedReturn, bindings)
|
|
905
|
+
: undefined,
|
|
906
|
+
};
|
|
907
|
+
});
|
|
908
|
+
const match = this.selectOverload(instantiated, args, call, name);
|
|
909
|
+
if (!match)
|
|
910
|
+
return { type: { kind: "unknown" } };
|
|
911
|
+
const type = match.resolvedReturn ??
|
|
912
|
+
(match.returnType
|
|
913
|
+
? this.typeFromNode(match.returnType)
|
|
914
|
+
: { kind: "primitive", name: "void" });
|
|
915
|
+
return { type, signature: match };
|
|
916
|
+
}
|
|
917
|
+
/** Choose the first overload that matches the signiture of the function call */
|
|
918
|
+
selectOverload(overloads, args, call, name) {
|
|
919
|
+
const single = overloads.length === 1;
|
|
920
|
+
for (const sig of overloads) {
|
|
921
|
+
if (this.bindArguments(sig, args, call, name, single))
|
|
922
|
+
return sig;
|
|
923
|
+
}
|
|
924
|
+
if (!single) {
|
|
925
|
+
this.error(`No overload of '${name}' matches these arguments`, call);
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
/** Checks whether 'args' satisfy one signature. */
|
|
930
|
+
bindArguments(sig, args, call, name, report) {
|
|
931
|
+
const { params } = sig;
|
|
932
|
+
let ok = true;
|
|
933
|
+
if (args.positional.length > params.length) {
|
|
934
|
+
if (report) {
|
|
935
|
+
this.error(`'${name}' takes at most ${params.length} positional argument${params.length === 1 ? "" : "s"}, got ${args.positional.length}`, call);
|
|
936
|
+
}
|
|
937
|
+
ok = false;
|
|
938
|
+
}
|
|
939
|
+
for (const [i, param] of params.entries()) {
|
|
940
|
+
const positional = args.positional[i];
|
|
941
|
+
const named = args.named.get(param.name);
|
|
942
|
+
if (positional && named) {
|
|
943
|
+
if (report) {
|
|
944
|
+
this.error(`'${param.name}' is supplied both positionally and by name`, named.node);
|
|
945
|
+
}
|
|
946
|
+
ok = false;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const provided = positional ?? named;
|
|
950
|
+
if (!provided) {
|
|
951
|
+
if (!param.default) {
|
|
952
|
+
if (report) {
|
|
953
|
+
this.error(`Missing argument '${param.name}' in call to '${name}'`, call);
|
|
954
|
+
}
|
|
955
|
+
ok = false;
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
// Stdlib signatures carry pre-resolved types (typevars already
|
|
960
|
+
// substituted); their TypeNodes must not reach typeFromNode here.
|
|
961
|
+
const paramType = sig.resolvedParamTypes?.[i] ?? this.typeFromNode(param.paramType);
|
|
962
|
+
if (!isAssignable(provided.type, paramType)) {
|
|
963
|
+
if (report) {
|
|
964
|
+
this.error(`Argument '${param.name}' expects ${typeToString(paramType)}, got ${typeToString(provided.type)}`, provided.node);
|
|
965
|
+
}
|
|
966
|
+
ok = false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (const [argName, arg] of args.named) {
|
|
970
|
+
if (!params.some((p) => p.name === argName)) {
|
|
971
|
+
if (report) {
|
|
972
|
+
this.error(`'${name}' has no parameter named '${argName}'`, arg.node);
|
|
973
|
+
}
|
|
974
|
+
ok = false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return ok;
|
|
978
|
+
}
|
|
979
|
+
// -- Helpers --
|
|
980
|
+
/**
|
|
981
|
+
* Reports a semantic error at a node's start location.
|
|
982
|
+
* By default the caret spans the whole node; pass `length` to underline only
|
|
983
|
+
* the first N columns (e.g. just an offending keyword).
|
|
984
|
+
*/
|
|
985
|
+
error(message, node, length) {
|
|
986
|
+
this.reporter.add(new KatnipError("Semantic", message, {
|
|
987
|
+
line: node.loc.start.line,
|
|
988
|
+
column: node.loc.start.column,
|
|
989
|
+
...(length != null
|
|
990
|
+
? { length }
|
|
991
|
+
: {
|
|
992
|
+
endLine: node.loc.end.line,
|
|
993
|
+
endColumn: node.loc.end.column,
|
|
994
|
+
}),
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
//# sourceMappingURL=SemanticAnalyzer.js.map
|