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/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('(v1.0.0)')}
30
+ ${style.bold('LT Language Compiler')} ${style.gray(`(v${getPackageVersion()})`)}
27
31
  ${style.gray('----------------------------------------')}
28
32
 
29
33
  ${style.bold('Usage:')}
@@ -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",
@@ -149,6 +149,7 @@ export class LuaEmitter {
149
149
  this.emitCommandStmt(stmt);
150
150
  break;
151
151
  case AST.NodeType.TypeDecl:
152
+ case AST.NodeType.TypeAliasDecl:
152
153
  // Type declarations are compile-time only, no Lua output
153
154
  break;
154
155
  default:
@@ -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";
@@ -10,6 +10,8 @@ export declare class Parser {
10
10
  constructor(tokens: Token[]);
11
11
  parse(): AST.Program;
12
12
  private parseStatement;
13
+ private parseType;
14
+ private parseTypeAlias;
13
15
  private parseVariableDecl;
14
16
  private parseIfStmt;
15
17
  private parseForStmt;
@@ -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.eat().value;
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.eat().value;
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.eat().value;
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.eat().value;
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.eat().value; // Get the type as string
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.eat().value;
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
- memberTypes = new Map(); // Tracks Table.Field -> type mapping
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
- // Visit all statements
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
- this.visitStatement(stmt);
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
- // If we have a custom type annotation and the value is a table, validate it
175
- if (customTypeName && value.kind === AST.NodeType.TableLiteral) {
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 (Config.Field: type = value)
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
- this.currentScope.define(decl.name.name, 'const', 'function', decl.line || 0);
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
- // Recurse
339
- this.visitChildren(expr);
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
- switch (str) {
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': return '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 custom type
374
- if (this.typeRegistry.has(str)) {
375
- return 'table'; // Custom types are tables internally
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
- const fields = this.typeRegistry.get(typeName);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lt-script",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "LT Language Compiler for FiveM",
6
6
  "main": "dist/index.js",