lt-script 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -147
- package/dist/cli/ltc.js +6 -2
- package/dist/cli/utils.d.ts +1 -0
- package/dist/cli/utils.js +21 -0
- package/dist/compiler/codegen/LuaEmitter.js +1 -0
- package/dist/compiler/parser/AST.d.ts +6 -0
- package/dist/compiler/parser/AST.js +1 -0
- package/dist/compiler/parser/Parser.d.ts +2 -0
- package/dist/compiler/parser/Parser.js +77 -7
- package/dist/compiler/semantics/SemanticAnalyzer.d.ts +5 -0
- package/dist/compiler/semantics/SemanticAnalyzer.js +287 -16
- package/package.json +1 -1
package/dist/cli/ltc.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import { compile } from '../index.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import { getAllFiles, ensureDirectoryExistence, style } from './utils.js';
|
|
5
|
+
import { getAllFiles, ensureDirectoryExistence, style, getPackageVersion } from './utils.js';
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
const command = args[0];
|
|
8
8
|
if (!command) {
|
|
9
9
|
printUsage();
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
|
+
if (command === '-v' || command === '--version') {
|
|
13
|
+
console.log(`v${getPackageVersion()}`);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
12
16
|
switch (command) {
|
|
13
17
|
case 'watch':
|
|
14
18
|
handleWatch(args.slice(1));
|
|
@@ -23,7 +27,7 @@ switch (command) {
|
|
|
23
27
|
}
|
|
24
28
|
function printUsage() {
|
|
25
29
|
console.log(`
|
|
26
|
-
${style.bold('LT Language Compiler')} ${style.gray(
|
|
30
|
+
${style.bold('LT Language Compiler')} ${style.gray(`(v${getPackageVersion()})`)}
|
|
27
31
|
${style.gray('----------------------------------------')}
|
|
28
32
|
|
|
29
33
|
${style.bold('Usage:')}
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function getAllFiles(dir: string, extension: string, fileList?: string[]): string[];
|
|
2
2
|
export declare function ensureDirectoryExistence(filePath: string): void;
|
|
3
|
+
export declare function getPackageVersion(): string;
|
|
3
4
|
export declare const colors: {
|
|
4
5
|
reset: string;
|
|
5
6
|
bright: string;
|
package/dist/cli/utils.js
CHANGED
|
@@ -24,6 +24,27 @@ export function ensureDirectoryExistence(filePath) {
|
|
|
24
24
|
ensureDirectoryExistence(dirname);
|
|
25
25
|
fs.mkdirSync(dirname);
|
|
26
26
|
}
|
|
27
|
+
export function getPackageVersion() {
|
|
28
|
+
try {
|
|
29
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
30
|
+
// Navigate up from dist/cli/utils.js to package.json
|
|
31
|
+
// dist/cli -> dist -> root
|
|
32
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
33
|
+
// Handle Windows path issues with URL pathname usually having leading slash
|
|
34
|
+
const adjustedPath = process.platform === 'win32' && packageJsonPath.startsWith('\\')
|
|
35
|
+
? packageJsonPath.substring(1)
|
|
36
|
+
: packageJsonPath;
|
|
37
|
+
if (fs.existsSync(adjustedPath)) {
|
|
38
|
+
const content = fs.readFileSync(adjustedPath, 'utf-8');
|
|
39
|
+
const pkg = JSON.parse(content);
|
|
40
|
+
return pkg.version || '1.0.0';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
// Fallback or dev environment handling
|
|
45
|
+
}
|
|
46
|
+
return '1.0.5'; // Fallback to current known version
|
|
47
|
+
}
|
|
27
48
|
export const colors = {
|
|
28
49
|
reset: "\x1b[0m",
|
|
29
50
|
bright: "\x1b[1m",
|
|
@@ -29,6 +29,7 @@ export declare enum NodeType {
|
|
|
29
29
|
EventHandler = "EventHandler",
|
|
30
30
|
ExportDecl = "ExportDecl",
|
|
31
31
|
TypeDecl = "TypeDecl",
|
|
32
|
+
TypeAliasDecl = "TypeAliasDecl",
|
|
32
33
|
BinaryExpr = "BinaryExpr",
|
|
33
34
|
UnaryExpr = "UnaryExpr",
|
|
34
35
|
UpdateExpr = "UpdateExpr",// ++, --
|
|
@@ -93,6 +94,11 @@ export interface TypeDecl extends Statement {
|
|
|
93
94
|
name: Identifier;
|
|
94
95
|
fields: TypeField[];
|
|
95
96
|
}
|
|
97
|
+
export interface TypeAliasDecl extends Statement {
|
|
98
|
+
kind: NodeType.TypeAliasDecl;
|
|
99
|
+
name: Identifier;
|
|
100
|
+
type: string;
|
|
101
|
+
}
|
|
96
102
|
export interface AssignmentStmt extends Statement {
|
|
97
103
|
kind: NodeType.AssignmentStmt;
|
|
98
104
|
targets: Expression[];
|
|
@@ -35,6 +35,7 @@ export var NodeType;
|
|
|
35
35
|
NodeType["ExportDecl"] = "ExportDecl";
|
|
36
36
|
// Type System
|
|
37
37
|
NodeType["TypeDecl"] = "TypeDecl";
|
|
38
|
+
NodeType["TypeAliasDecl"] = "TypeAliasDecl";
|
|
38
39
|
// Expressions
|
|
39
40
|
NodeType["BinaryExpr"] = "BinaryExpr";
|
|
40
41
|
NodeType["UnaryExpr"] = "UnaryExpr";
|
|
@@ -19,6 +19,18 @@ export class Parser {
|
|
|
19
19
|
}
|
|
20
20
|
// ============ Statement Parsing ============
|
|
21
21
|
parseStatement() {
|
|
22
|
+
// Ambiguity Check: 'type' Identifier = ... VS type(x)
|
|
23
|
+
if (this.at().type === TokenType.IDENTIFIER && this.at().value === 'type') {
|
|
24
|
+
// Lookahead to see if it's a declaration
|
|
25
|
+
const next = this.tokens[this.pos + 1];
|
|
26
|
+
if (next && next.type === TokenType.IDENTIFIER) {
|
|
27
|
+
const nextNext = this.tokens[this.pos + 2];
|
|
28
|
+
if (nextNext && nextNext.type === TokenType.EQUALS) {
|
|
29
|
+
this.eat(); // consume 'type' pseudo-keyword
|
|
30
|
+
return this.parseTypeAlias();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
switch (this.at().type) {
|
|
23
35
|
case TokenType.VAR:
|
|
24
36
|
case TokenType.LET:
|
|
@@ -77,6 +89,65 @@ export class Parser {
|
|
|
77
89
|
return this.parseExpressionStatement();
|
|
78
90
|
}
|
|
79
91
|
}
|
|
92
|
+
// ============ Type Parsing ============
|
|
93
|
+
parseType() {
|
|
94
|
+
// If we're parsing a type and hit a '{', it's an object type literal: value: { a: number }
|
|
95
|
+
if (this.at().type === TokenType.LBRACE) {
|
|
96
|
+
this.eat(); // {
|
|
97
|
+
let typeBody = "{ ";
|
|
98
|
+
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
99
|
+
const key = this.parseIdentifier().name;
|
|
100
|
+
let val = "any";
|
|
101
|
+
// Check for optional property
|
|
102
|
+
if (this.match(TokenType.QUESTION)) {
|
|
103
|
+
key + "?";
|
|
104
|
+
}
|
|
105
|
+
if (this.match(TokenType.COLON)) {
|
|
106
|
+
val = this.parseType();
|
|
107
|
+
}
|
|
108
|
+
typeBody += `${key}: ${val}`;
|
|
109
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
110
|
+
this.match(TokenType.COMMA) || this.match(TokenType.SEMICOLON);
|
|
111
|
+
typeBody += ", ";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.expect(TokenType.RBRACE);
|
|
115
|
+
typeBody += " }";
|
|
116
|
+
// Handle array of objects: { ... }[]
|
|
117
|
+
while (this.match(TokenType.LBRACKET)) {
|
|
118
|
+
this.expect(TokenType.RBRACKET);
|
|
119
|
+
typeBody += "[]";
|
|
120
|
+
}
|
|
121
|
+
return typeBody;
|
|
122
|
+
}
|
|
123
|
+
const typeToken = this.eat();
|
|
124
|
+
let typeName = typeToken.value;
|
|
125
|
+
// Handle generic types: List<string> (Basic support)
|
|
126
|
+
if (this.match(TokenType.LT_ANGLE)) { // <
|
|
127
|
+
typeName += "<";
|
|
128
|
+
typeName += this.parseType();
|
|
129
|
+
while (this.match(TokenType.COMMA)) {
|
|
130
|
+
typeName += ", ";
|
|
131
|
+
typeName += this.parseType();
|
|
132
|
+
}
|
|
133
|
+
this.expect(TokenType.GT_ANGLE); // >
|
|
134
|
+
typeName += ">";
|
|
135
|
+
}
|
|
136
|
+
// Handle array types: string[] or string[][]
|
|
137
|
+
while (this.match(TokenType.LBRACKET)) {
|
|
138
|
+
this.expect(TokenType.RBRACKET);
|
|
139
|
+
typeName += "[]";
|
|
140
|
+
}
|
|
141
|
+
return typeName;
|
|
142
|
+
}
|
|
143
|
+
parseTypeAlias() {
|
|
144
|
+
// 'type' is already consumed or handled by caller
|
|
145
|
+
const name = this.parseIdentifier();
|
|
146
|
+
this.expect(TokenType.EQUALS);
|
|
147
|
+
const typeStr = this.parseType();
|
|
148
|
+
return { kind: AST.NodeType.TypeAliasDecl, name, type: typeStr };
|
|
149
|
+
}
|
|
150
|
+
// ============ Statement Parsing ============
|
|
80
151
|
parseVariableDecl() {
|
|
81
152
|
const scopeToken = this.eat();
|
|
82
153
|
const scope = scopeToken.value;
|
|
@@ -105,12 +176,12 @@ export class Parser {
|
|
|
105
176
|
};
|
|
106
177
|
names.push(parseName());
|
|
107
178
|
if (this.match(TokenType.COLON)) {
|
|
108
|
-
typeAnnotations[0] = this.
|
|
179
|
+
typeAnnotations[0] = this.parseType();
|
|
109
180
|
}
|
|
110
181
|
while (this.match(TokenType.COMMA)) {
|
|
111
182
|
names.push(parseName());
|
|
112
183
|
if (this.match(TokenType.COLON)) {
|
|
113
|
-
typeAnnotations[names.length - 1] = this.
|
|
184
|
+
typeAnnotations[names.length - 1] = this.parseType();
|
|
114
185
|
}
|
|
115
186
|
}
|
|
116
187
|
let values;
|
|
@@ -376,7 +447,7 @@ export class Parser {
|
|
|
376
447
|
this.expect(TokenType.RPAREN);
|
|
377
448
|
let returnType;
|
|
378
449
|
if (this.match(TokenType.COLON)) {
|
|
379
|
-
returnType = this.
|
|
450
|
+
returnType = this.parseType();
|
|
380
451
|
}
|
|
381
452
|
const body = this.parseBlockUntil(TokenType.END);
|
|
382
453
|
this.expect(TokenType.END);
|
|
@@ -399,7 +470,7 @@ export class Parser {
|
|
|
399
470
|
this.expect(TokenType.RPAREN);
|
|
400
471
|
let returnType;
|
|
401
472
|
if (this.match(TokenType.COLON)) {
|
|
402
|
-
returnType = this.
|
|
473
|
+
returnType = this.parseType();
|
|
403
474
|
}
|
|
404
475
|
const body = this.parseBlockUntil(TokenType.END);
|
|
405
476
|
this.expect(TokenType.END);
|
|
@@ -460,13 +531,12 @@ export class Parser {
|
|
|
460
531
|
parseTypeDecl() {
|
|
461
532
|
this.eat(); // interface
|
|
462
533
|
const name = this.parseIdentifier();
|
|
463
|
-
this.expect(TokenType.EQUALS);
|
|
464
534
|
this.expect(TokenType.LBRACE);
|
|
465
535
|
const fields = [];
|
|
466
536
|
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
467
537
|
const fieldName = this.parseIdentifier().name;
|
|
468
538
|
this.expect(TokenType.COLON);
|
|
469
|
-
const fieldType = this.
|
|
539
|
+
const fieldType = this.parseType(); // Get the type as string
|
|
470
540
|
fields.push({ name: fieldName, type: fieldType });
|
|
471
541
|
// Allow trailing comma
|
|
472
542
|
if (!this.check(TokenType.RBRACE)) {
|
|
@@ -913,7 +983,7 @@ export class Parser {
|
|
|
913
983
|
const name = this.parseIdentifierName();
|
|
914
984
|
let typeAnnotation;
|
|
915
985
|
if (this.match(TokenType.COLON)) {
|
|
916
|
-
typeAnnotation = this.
|
|
986
|
+
typeAnnotation = this.parseType();
|
|
917
987
|
}
|
|
918
988
|
let defaultValue;
|
|
919
989
|
if (this.match(TokenType.EQUALS)) {
|
|
@@ -3,7 +3,9 @@ export declare class SemanticAnalyzer {
|
|
|
3
3
|
private globalScope;
|
|
4
4
|
private currentScope;
|
|
5
5
|
private typeRegistry;
|
|
6
|
+
private typeAliasRegistry;
|
|
6
7
|
private memberTypes;
|
|
8
|
+
private functionRegistry;
|
|
7
9
|
analyze(program: AST.Program): void;
|
|
8
10
|
private enterScope;
|
|
9
11
|
private exitScope;
|
|
@@ -18,6 +20,9 @@ export declare class SemanticAnalyzer {
|
|
|
18
20
|
private visitExpression;
|
|
19
21
|
private inferType;
|
|
20
22
|
private stringToType;
|
|
23
|
+
private visitTypeAliasDecl;
|
|
21
24
|
private visitTypeDecl;
|
|
25
|
+
private validateTypeExists;
|
|
22
26
|
private validateTableAgainstType;
|
|
27
|
+
private parseObjectLiteralType;
|
|
23
28
|
}
|
|
@@ -23,14 +23,31 @@ class Scope {
|
|
|
23
23
|
export class SemanticAnalyzer {
|
|
24
24
|
globalScope = new Scope();
|
|
25
25
|
currentScope = this.globalScope;
|
|
26
|
-
typeRegistry = new Map();
|
|
27
|
-
|
|
26
|
+
typeRegistry = new Map(); // Stores interface fields
|
|
27
|
+
typeAliasRegistry = new Map(); // Stores type alias strings
|
|
28
|
+
memberTypes = new Map(); // Tracks Table.Field -> type mappings
|
|
29
|
+
functionRegistry = new Map(); // Stores function signatures
|
|
28
30
|
analyze(program) {
|
|
29
31
|
// Reset scope for fresh analysis (though typically instance is fresh)
|
|
30
32
|
this.currentScope = this.globalScope;
|
|
31
|
-
|
|
33
|
+
this.typeRegistry.clear();
|
|
34
|
+
this.typeAliasRegistry.clear();
|
|
35
|
+
this.memberTypes.clear();
|
|
36
|
+
this.functionRegistry.clear();
|
|
37
|
+
// First pass: Register types (to allow forward references)
|
|
32
38
|
for (const stmt of program.body) {
|
|
33
|
-
|
|
39
|
+
if (stmt.kind === AST.NodeType.TypeDecl) {
|
|
40
|
+
this.visitTypeDecl(stmt);
|
|
41
|
+
}
|
|
42
|
+
else if (stmt.kind === AST.NodeType.TypeAliasDecl) {
|
|
43
|
+
this.visitTypeAliasDecl(stmt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Second pass: Analyze code
|
|
47
|
+
for (const stmt of program.body) {
|
|
48
|
+
if (stmt.kind !== AST.NodeType.TypeDecl && stmt.kind !== AST.NodeType.TypeAliasDecl) {
|
|
49
|
+
this.visitStatement(stmt);
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
}
|
|
36
53
|
enterScope() {
|
|
@@ -117,6 +134,29 @@ export class SemanticAnalyzer {
|
|
|
117
134
|
this.exitScope();
|
|
118
135
|
}
|
|
119
136
|
break;
|
|
137
|
+
case AST.NodeType.EventHandler:
|
|
138
|
+
{
|
|
139
|
+
const s = stmt;
|
|
140
|
+
this.enterScope();
|
|
141
|
+
s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'any', s.line || 0));
|
|
142
|
+
this.visitBlock(s.body);
|
|
143
|
+
this.exitScope();
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case AST.NodeType.TryCatchStmt:
|
|
147
|
+
{
|
|
148
|
+
const s = stmt;
|
|
149
|
+
this.visitBlock(s.tryBody);
|
|
150
|
+
this.enterScope();
|
|
151
|
+
this.currentScope.define(s.catchParam.name, 'let', 'any', s.line || 0);
|
|
152
|
+
this.visitBlock(s.catchBody);
|
|
153
|
+
this.exitScope();
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
// Handle expression statements (standalone function calls like ProcessNumber("hello"))
|
|
157
|
+
case AST.NodeType.CallExpr:
|
|
158
|
+
this.visitExpression(stmt);
|
|
159
|
+
break;
|
|
120
160
|
// Basic fallback recursive visiting
|
|
121
161
|
default:
|
|
122
162
|
// Identify other nodes that contain blocks/expressions to visit
|
|
@@ -171,8 +211,30 @@ export class SemanticAnalyzer {
|
|
|
171
211
|
if (decl.values && decl.values[index]) {
|
|
172
212
|
const value = decl.values[index];
|
|
173
213
|
const inferred = this.inferType(value);
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
const annotation = decl.typeAnnotations && decl.typeAnnotations[index];
|
|
215
|
+
// If it's an array type (e.g., RobberyConfig[] or number[]) and value is a table, validate each element
|
|
216
|
+
if (annotation && annotation.endsWith('[]') && value.kind === AST.NodeType.TableLiteral) {
|
|
217
|
+
const elementTypeName = annotation.slice(0, -2); // Remove []
|
|
218
|
+
const expectedElementType = this.stringToType(elementTypeName);
|
|
219
|
+
const tableValue = value;
|
|
220
|
+
// Iterate through each element and validate
|
|
221
|
+
for (let i = 0; i < tableValue.fields.length; i++) {
|
|
222
|
+
const field = tableValue.fields[i];
|
|
223
|
+
// If element is a TableLiteral and element type is an interface, validate against interface
|
|
224
|
+
if (field.value.kind === AST.NodeType.TableLiteral && this.typeRegistry.has(elementTypeName)) {
|
|
225
|
+
this.validateTableAgainstType(field.value, elementTypeName, decl.line || 0);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// For primitive types (number[], string[], etc.), check each element type
|
|
229
|
+
const actualType = this.inferType(field.value);
|
|
230
|
+
if (expectedElementType !== 'any' && actualType !== 'any' && expectedElementType !== actualType) {
|
|
231
|
+
throw new Error(`Type mismatch in array at line ${decl.line}: Element ${i + 1} expected '${elementTypeName}', got '${actualType}'`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// If we have a custom type annotation and the value is a table (not array), validate it
|
|
237
|
+
else if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
|
|
176
238
|
this.validateTableAgainstType(value, customTypeName, decl.line || 0);
|
|
177
239
|
}
|
|
178
240
|
// If we have an annotation, check compatibility
|
|
@@ -221,8 +283,36 @@ export class SemanticAnalyzer {
|
|
|
221
283
|
}
|
|
222
284
|
}
|
|
223
285
|
}
|
|
224
|
-
// Handle member expression type checking
|
|
286
|
+
// Handle member expression type checking
|
|
225
287
|
if (t.kind === AST.NodeType.MemberExpr) {
|
|
288
|
+
const member = t;
|
|
289
|
+
// 1. Strict Property Existence Check
|
|
290
|
+
if (member.object.kind === AST.NodeType.Identifier && !member.computed) {
|
|
291
|
+
const objName = member.object.name;
|
|
292
|
+
const sym = this.currentScope.lookup(objName);
|
|
293
|
+
const propName = member.property.name;
|
|
294
|
+
if (sym && sym.type !== 'any' && sym.type !== 'table') {
|
|
295
|
+
// Check if it's an interface or alias
|
|
296
|
+
let fields;
|
|
297
|
+
// Remove array suffix for type lookup if needed (though variable shouldn't be array if accessing prop directly)
|
|
298
|
+
const baseType = sym.type.replace('[]', '');
|
|
299
|
+
if (this.typeRegistry.has(baseType)) {
|
|
300
|
+
fields = this.typeRegistry.get(baseType);
|
|
301
|
+
}
|
|
302
|
+
else if (this.typeAliasRegistry.has(baseType)) {
|
|
303
|
+
const aliasStr = this.typeAliasRegistry.get(baseType);
|
|
304
|
+
if (aliasStr.startsWith('{')) {
|
|
305
|
+
fields = this.parseObjectLiteralType(aliasStr);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (fields) {
|
|
309
|
+
const fieldExists = fields.some(f => f.name === propName);
|
|
310
|
+
if (!fieldExists) {
|
|
311
|
+
throw new Error(`Property '${propName}' does not exist on type '${sym.type}' at line ${stmt.line}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
226
316
|
const memberKey = this.getMemberExprKey(t);
|
|
227
317
|
// If this assignment has a type annotation, register it
|
|
228
318
|
if (stmt.typeAnnotation && index === 0) {
|
|
@@ -279,8 +369,22 @@ export class SemanticAnalyzer {
|
|
|
279
369
|
}
|
|
280
370
|
visitFunctionDecl(decl) {
|
|
281
371
|
// Name is defined in CURRENT scope
|
|
372
|
+
let funcName;
|
|
282
373
|
if (decl.name && decl.name.kind === AST.NodeType.Identifier) {
|
|
283
|
-
|
|
374
|
+
funcName = decl.name.name;
|
|
375
|
+
this.currentScope.define(funcName, 'const', 'function', decl.line || 0);
|
|
376
|
+
}
|
|
377
|
+
// Register function signature for call validation
|
|
378
|
+
const paramTypes = [];
|
|
379
|
+
decl.params.forEach(p => {
|
|
380
|
+
let type = 'any';
|
|
381
|
+
if (p.typeAnnotation) {
|
|
382
|
+
type = this.stringToType(p.typeAnnotation);
|
|
383
|
+
}
|
|
384
|
+
paramTypes.push({ name: p.name.name, type });
|
|
385
|
+
});
|
|
386
|
+
if (funcName) {
|
|
387
|
+
this.functionRegistry.set(funcName, { params: paramTypes });
|
|
284
388
|
}
|
|
285
389
|
this.enterScope();
|
|
286
390
|
decl.params.forEach(p => {
|
|
@@ -310,6 +414,41 @@ export class SemanticAnalyzer {
|
|
|
310
414
|
// Mostly just traversing to find nested scopes in functions/arrows
|
|
311
415
|
if (!expr)
|
|
312
416
|
return;
|
|
417
|
+
// Check for undefined variable usage
|
|
418
|
+
if (expr.kind === AST.NodeType.Identifier) {
|
|
419
|
+
const id = expr;
|
|
420
|
+
const sym = this.currentScope.lookup(id.name);
|
|
421
|
+
// Skip globals/builtins - only check if it looks like a user variable
|
|
422
|
+
// FiveM/Lua has many globals, so we only warn for simple lowercase identifiers
|
|
423
|
+
// that are not common Lua globals
|
|
424
|
+
const luaGlobals = ['print', 'type', 'tostring', 'tonumber', 'pairs', 'ipairs',
|
|
425
|
+
'next', 'error', 'assert', 'pcall', 'xpcall', 'require',
|
|
426
|
+
'table', 'string', 'math', 'os', 'io', 'coroutine', 'debug',
|
|
427
|
+
'setmetatable', 'getmetatable', 'rawget', 'rawset', 'rawequal',
|
|
428
|
+
'collectgarbage', 'dofile', 'load', 'loadfile', 'select',
|
|
429
|
+
'unpack', '_G', '_VERSION', 'true', 'false', 'nil',
|
|
430
|
+
// FiveM common globals
|
|
431
|
+
'Citizen', 'CreateThread', 'Wait', 'GetGameTimer', 'vector3',
|
|
432
|
+
'vector2', 'vector4', 'GetCurrentResourceName', 'exports',
|
|
433
|
+
'PlayerPedId', 'GetPlayerPed', 'GetEntityCoords', 'TriggerEvent',
|
|
434
|
+
'TriggerServerEvent', 'TriggerClientEvent', 'RegisterNetEvent',
|
|
435
|
+
'AddEventHandler', 'RegisterCommand', 'IsControlJustPressed',
|
|
436
|
+
'AddEventHandler', 'RegisterCommand', 'LocalPlayer', 'PlayerId',
|
|
437
|
+
'GetPlayerServerId', 'source', 'SetEntityHealth', 'ESX', 'QBCore',
|
|
438
|
+
// Additional FiveM/Lua globals
|
|
439
|
+
'Config', 'Framework', 'MySQL', 'lib', 'cache', 'json',
|
|
440
|
+
'NetworkGetNetworkIdFromEntity', 'NetworkGetEntityFromNetworkId',
|
|
441
|
+
'GetPlayerPed', 'GetVehiclePedIsIn', 'IsPedInAnyVehicle',
|
|
442
|
+
'SetPedCanRagdoll', 'SetTimeout', 'SetInterval', 'ClearTimeout',
|
|
443
|
+
'GetHashKey', 'IsModelValid', 'RequestModel', 'HasModelLoaded'];
|
|
444
|
+
if (!sym && !luaGlobals.includes(id.name)) {
|
|
445
|
+
// Check if it's a type name (not a variable)
|
|
446
|
+
if (!this.typeRegistry.has(id.name) && !this.typeAliasRegistry.has(id.name)) {
|
|
447
|
+
throw new Error(`Undefined variable '${id.name}' at line ${id.line ?? 0}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
313
452
|
if (expr.kind === AST.NodeType.ArrowFunc) {
|
|
314
453
|
const arrow = expr;
|
|
315
454
|
this.enterScope();
|
|
@@ -335,8 +474,57 @@ export class SemanticAnalyzer {
|
|
|
335
474
|
this.visitFunctionDecl(expr);
|
|
336
475
|
return;
|
|
337
476
|
}
|
|
338
|
-
//
|
|
339
|
-
|
|
477
|
+
// Handle TableLiteral - recurse into field values only
|
|
478
|
+
if (expr.kind === AST.NodeType.TableLiteral) {
|
|
479
|
+
const table = expr;
|
|
480
|
+
for (const field of table.fields) {
|
|
481
|
+
// Don't check keys - they are field names, not variable references
|
|
482
|
+
// Only check computed keys like [expression] or string keys
|
|
483
|
+
if (field.key && field.key.kind !== AST.NodeType.Identifier) {
|
|
484
|
+
this.visitExpression(field.key);
|
|
485
|
+
}
|
|
486
|
+
this.visitExpression(field.value);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Handle BinaryExpr
|
|
491
|
+
if (expr.kind === AST.NodeType.BinaryExpr) {
|
|
492
|
+
const bin = expr;
|
|
493
|
+
this.visitExpression(bin.left);
|
|
494
|
+
this.visitExpression(bin.right);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Handle CallExpr - recurse into callee and args, validate argument types
|
|
498
|
+
if (expr.kind === AST.NodeType.CallExpr) {
|
|
499
|
+
const call = expr;
|
|
500
|
+
this.visitExpression(call.callee);
|
|
501
|
+
// Validate argument types if callee is a known function
|
|
502
|
+
if (call.callee.kind === AST.NodeType.Identifier) {
|
|
503
|
+
const funcName = call.callee.name;
|
|
504
|
+
const funcInfo = this.functionRegistry.get(funcName);
|
|
505
|
+
if (funcInfo) {
|
|
506
|
+
// Check each argument against expected param type
|
|
507
|
+
for (let i = 0; i < call.args.length && i < funcInfo.params.length; i++) {
|
|
508
|
+
const expectedType = funcInfo.params[i].type;
|
|
509
|
+
const argType = this.inferType(call.args[i]);
|
|
510
|
+
if (expectedType !== 'any' && argType !== 'any' && expectedType !== argType) {
|
|
511
|
+
throw new Error(`Type mismatch in function call '${funcName}' at line ${call.line || 0}: Argument ${i + 1} expects '${expectedType}', got '${argType}'`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const arg of call.args) {
|
|
517
|
+
this.visitExpression(arg);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Handle MemberExpr - only check the object, not property (obj.property)
|
|
522
|
+
if (expr.kind === AST.NodeType.MemberExpr) {
|
|
523
|
+
const member = expr;
|
|
524
|
+
this.visitExpression(member.object);
|
|
525
|
+
// Don't check member.property - it's a field access, not a variable
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
340
528
|
}
|
|
341
529
|
inferType(expr) {
|
|
342
530
|
if (!expr)
|
|
@@ -361,29 +549,96 @@ export class SemanticAnalyzer {
|
|
|
361
549
|
}
|
|
362
550
|
}
|
|
363
551
|
stringToType(str) {
|
|
364
|
-
|
|
552
|
+
// Array types are tables
|
|
553
|
+
if (str.endsWith('[]'))
|
|
554
|
+
return 'table';
|
|
555
|
+
// Remove array notation for basic type check: string[] -> string
|
|
556
|
+
const baseStr = str.replace(/\[\]/g, '');
|
|
557
|
+
switch (baseStr) {
|
|
365
558
|
case 'string': return 'string';
|
|
366
559
|
case 'number': return 'number';
|
|
367
560
|
case 'boolean': return 'boolean';
|
|
368
|
-
case 'vector':
|
|
561
|
+
case 'vector':
|
|
562
|
+
case 'vector2':
|
|
563
|
+
case 'vector3':
|
|
564
|
+
case 'vector4': return 'vector';
|
|
369
565
|
case 'table': return 'table';
|
|
370
566
|
case 'function': return 'function';
|
|
371
567
|
case 'any': return 'any';
|
|
372
568
|
default:
|
|
373
|
-
// Check if it's a
|
|
374
|
-
if (this.typeRegistry.has(
|
|
375
|
-
return 'table';
|
|
569
|
+
// Check if it's a registered interface
|
|
570
|
+
if (this.typeRegistry.has(baseStr)) {
|
|
571
|
+
return 'table';
|
|
572
|
+
}
|
|
573
|
+
// Check if it's a type alias
|
|
574
|
+
if (this.typeAliasRegistry.has(baseStr)) {
|
|
575
|
+
// If alias resolves to a primitive, return that. If object literal, return 'table'
|
|
576
|
+
const aliased = this.typeAliasRegistry.get(baseStr);
|
|
577
|
+
if (aliased.startsWith('{'))
|
|
578
|
+
return 'table';
|
|
579
|
+
return this.stringToType(aliased);
|
|
376
580
|
}
|
|
377
581
|
return 'any';
|
|
378
582
|
}
|
|
379
583
|
}
|
|
584
|
+
visitTypeAliasDecl(decl) {
|
|
585
|
+
this.typeAliasRegistry.set(decl.name.name, decl.type);
|
|
586
|
+
// If it's an object literal type, validate field types
|
|
587
|
+
if (decl.type.startsWith('{')) {
|
|
588
|
+
const fields = this.parseObjectLiteralType(decl.type);
|
|
589
|
+
for (const field of fields) {
|
|
590
|
+
this.validateTypeExists(field.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// It's a simple alias, validate the aliased type exists
|
|
595
|
+
this.validateTypeExists(decl.type, decl.name.line ?? 0, `type '${decl.name.name}'`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
380
598
|
visitTypeDecl(decl) {
|
|
381
599
|
const typeName = decl.name.name;
|
|
382
600
|
// Register the type
|
|
383
601
|
this.typeRegistry.set(typeName, decl.fields);
|
|
602
|
+
// Validate that all field types exist
|
|
603
|
+
for (const field of decl.fields) {
|
|
604
|
+
this.validateTypeExists(field.type, decl.name.line ?? 0, `interface '${typeName}'`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
validateTypeExists(typeStr, line, context) {
|
|
608
|
+
// Remove array brackets for base type check
|
|
609
|
+
const baseType = typeStr.replace(/\[\]/g, '').trim();
|
|
610
|
+
// Skip object literal types (they are inline definitions)
|
|
611
|
+
if (baseType.startsWith('{'))
|
|
612
|
+
return;
|
|
613
|
+
// Check primitive types (including FiveM vector types)
|
|
614
|
+
const primitives = ['string', 'number', 'boolean', 'nil', 'any', 'table', 'function', 'vector', 'void', 'vector2', 'vector3', 'vector4'];
|
|
615
|
+
if (primitives.includes(baseType))
|
|
616
|
+
return;
|
|
617
|
+
// Check type registry (interfaces)
|
|
618
|
+
if (this.typeRegistry.has(baseType))
|
|
619
|
+
return;
|
|
620
|
+
// Check type alias registry
|
|
621
|
+
if (this.typeAliasRegistry.has(baseType))
|
|
622
|
+
return;
|
|
623
|
+
// Type not found!
|
|
624
|
+
throw new Error(`Unknown type '${baseType}' in ${context} at line ${line}`);
|
|
384
625
|
}
|
|
385
626
|
validateTableAgainstType(table, typeName, line) {
|
|
386
|
-
|
|
627
|
+
let fields;
|
|
628
|
+
// Remove array brackets if present (e.g. validating a single object against MyType[])
|
|
629
|
+
// This shouldn't happen often in this function call context but good for safety
|
|
630
|
+
typeName = typeName.replace(/\[\]/g, '');
|
|
631
|
+
// 1. Check Interface Registry
|
|
632
|
+
if (this.typeRegistry.has(typeName)) {
|
|
633
|
+
fields = this.typeRegistry.get(typeName);
|
|
634
|
+
}
|
|
635
|
+
// 2. Check Type Aliases (specifically object literal aliases)
|
|
636
|
+
else if (this.typeAliasRegistry.has(typeName)) {
|
|
637
|
+
const aliasStr = this.typeAliasRegistry.get(typeName);
|
|
638
|
+
if (aliasStr.startsWith('{')) {
|
|
639
|
+
fields = this.parseObjectLiteralType(aliasStr);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
387
642
|
if (!fields)
|
|
388
643
|
return; // Unknown type, skip validation
|
|
389
644
|
const providedFields = new Map();
|
|
@@ -408,4 +663,20 @@ export class SemanticAnalyzer {
|
|
|
408
663
|
}
|
|
409
664
|
}
|
|
410
665
|
}
|
|
666
|
+
parseObjectLiteralType(typeStr) {
|
|
667
|
+
const content = typeStr.trim().slice(1, -1);
|
|
668
|
+
if (!content)
|
|
669
|
+
return [];
|
|
670
|
+
const fields = [];
|
|
671
|
+
const parts = content.split(/,|;/);
|
|
672
|
+
for (const part of parts) {
|
|
673
|
+
if (!part.trim())
|
|
674
|
+
continue;
|
|
675
|
+
const [key, val] = part.split(':').map(s => s.trim());
|
|
676
|
+
if (key && val) {
|
|
677
|
+
fields.push({ name: key.replace('?', ''), type: val });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return fields;
|
|
681
|
+
}
|
|
411
682
|
}
|