lt-script 1.0.1 → 1.0.3

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.
@@ -0,0 +1,411 @@
1
+ import * as AST from '../parser/AST.js';
2
+ class Scope {
3
+ symbols = new Map();
4
+ parent;
5
+ constructor(parent) {
6
+ this.parent = parent;
7
+ }
8
+ define(name, kind, type, line) {
9
+ // In strict mode we might check for redeclaration here
10
+ this.symbols.set(name, { name, kind, type, definedAtLine: line });
11
+ }
12
+ lookup(name) {
13
+ let current = this;
14
+ while (current) {
15
+ if (current.symbols.has(name)) {
16
+ return current.symbols.get(name);
17
+ }
18
+ current = current.parent;
19
+ }
20
+ return undefined;
21
+ }
22
+ }
23
+ export class SemanticAnalyzer {
24
+ globalScope = new Scope();
25
+ currentScope = this.globalScope;
26
+ typeRegistry = new Map();
27
+ memberTypes = new Map(); // Tracks Table.Field -> type mapping
28
+ analyze(program) {
29
+ // Reset scope for fresh analysis (though typically instance is fresh)
30
+ this.currentScope = this.globalScope;
31
+ // Visit all statements
32
+ for (const stmt of program.body) {
33
+ this.visitStatement(stmt);
34
+ }
35
+ }
36
+ enterScope() {
37
+ this.currentScope = new Scope(this.currentScope);
38
+ }
39
+ exitScope() {
40
+ if (this.currentScope.parent) {
41
+ this.currentScope = this.currentScope.parent;
42
+ }
43
+ }
44
+ visitStatement(stmt) {
45
+ switch (stmt.kind) {
46
+ case AST.NodeType.VariableDecl:
47
+ this.visitVariableDecl(stmt);
48
+ break;
49
+ case AST.NodeType.TypeDecl:
50
+ this.visitTypeDecl(stmt);
51
+ break;
52
+ case AST.NodeType.AssignmentStmt:
53
+ this.visitAssignmentStmt(stmt);
54
+ break;
55
+ case AST.NodeType.CompoundAssignment:
56
+ this.visitCompoundAssignment(stmt);
57
+ break;
58
+ case AST.NodeType.FunctionDecl:
59
+ this.visitFunctionDecl(stmt);
60
+ break;
61
+ case AST.NodeType.Block:
62
+ this.visitBlock(stmt);
63
+ break;
64
+ case AST.NodeType.IfStmt:
65
+ {
66
+ const s = stmt;
67
+ this.visitExpression(s.condition);
68
+ this.visitBlock(s.thenBody);
69
+ s.elseIfClauses?.forEach(c => {
70
+ this.visitExpression(c.condition);
71
+ this.visitBlock(c.body);
72
+ });
73
+ if (s.elseBody)
74
+ this.visitBlock(s.elseBody);
75
+ }
76
+ break;
77
+ case AST.NodeType.WhileStmt:
78
+ {
79
+ const s = stmt;
80
+ this.visitExpression(s.condition);
81
+ this.visitBlock(s.body);
82
+ }
83
+ break;
84
+ case AST.NodeType.ForStmt:
85
+ {
86
+ const s = stmt;
87
+ this.enterScope(); // For loop creates scope for iterators
88
+ s.iterators.forEach(i => this.currentScope.define(i.name, 'let', 'any', s.line || 0)); // Iterators can be anything in generic for
89
+ this.visitExpression(s.iterable);
90
+ // We can treat the body as part of this scope or nested.
91
+ // AST.Block usually suggests its own scope, but let's reuse this one or just visit statements.
92
+ // Since AST.Block logic below creates a NEW scope, we have: Scope(Iterators) -> Scope(Body). This is correct.
93
+ this.visitBlock(s.body);
94
+ this.exitScope();
95
+ }
96
+ break;
97
+ case AST.NodeType.RangeForStmt:
98
+ {
99
+ const s = stmt;
100
+ this.enterScope();
101
+ this.currentScope.define(s.counter.name, 'let', 'number', s.line || 0); // Range for is always number
102
+ this.visitExpression(s.start);
103
+ this.visitExpression(s.end);
104
+ if (s.step)
105
+ this.visitExpression(s.step);
106
+ this.visitBlock(s.body);
107
+ this.exitScope();
108
+ }
109
+ break;
110
+ // ... Handle other control flows that have blocks ...
111
+ case AST.NodeType.CommandStmt:
112
+ {
113
+ const s = stmt;
114
+ this.enterScope();
115
+ s.params.forEach(p => this.currentScope.define(p.name.name, 'param', 'string', s.line || 0)); // Command params are strings (usually)
116
+ this.visitBlock(s.body);
117
+ this.exitScope();
118
+ }
119
+ break;
120
+ // Basic fallback recursive visiting
121
+ default:
122
+ // Identify other nodes that contain blocks/expressions to visit
123
+ this.visitChildren(stmt);
124
+ }
125
+ }
126
+ visitChildren(node) {
127
+ if (!node || typeof node !== 'object')
128
+ return;
129
+ for (const key in node) {
130
+ if (key === 'kind')
131
+ continue;
132
+ const val = node[key];
133
+ if (Array.isArray(val)) {
134
+ val.forEach(v => {
135
+ if (v && typeof v === 'object' && 'kind' in v) {
136
+ if (v.kind.endsWith('Stmt') || v.kind === 'VariableDecl' || v.kind === 'Block') {
137
+ this.visitStatement(v);
138
+ }
139
+ else {
140
+ this.visitExpression(v);
141
+ }
142
+ }
143
+ });
144
+ }
145
+ else if (val && typeof val === 'object' && 'kind' in val) {
146
+ if (val.kind.endsWith('Stmt') || val.kind === 'VariableDecl' || val.kind === 'Block') {
147
+ this.visitStatement(val);
148
+ }
149
+ else {
150
+ this.visitExpression(val);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ visitVariableDecl(decl) {
156
+ // Visit values first
157
+ if (decl.values) {
158
+ decl.values.forEach(v => this.visitExpression(v));
159
+ }
160
+ const kind = decl.scope === 'const' ? 'const' : 'let';
161
+ decl.names.forEach((n, index) => {
162
+ let type = 'any';
163
+ let customTypeName;
164
+ // 1. Try to get type from annotation
165
+ if (decl.typeAnnotations && decl.typeAnnotations[index]) {
166
+ const annotation = decl.typeAnnotations[index];
167
+ customTypeName = this.typeRegistry.has(annotation) ? annotation : undefined;
168
+ type = this.stringToType(annotation);
169
+ }
170
+ // 2. Try to infer from value if available
171
+ if (decl.values && decl.values[index]) {
172
+ const value = decl.values[index];
173
+ 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) {
176
+ this.validateTableAgainstType(value, customTypeName, decl.line || 0);
177
+ }
178
+ // If we have an annotation, check compatibility
179
+ if (type !== 'any' && inferred !== 'any' && type !== inferred) {
180
+ throw new Error(`Type mismatch at line ${decl.line}: Expected '${type}', but got '${inferred}'`);
181
+ }
182
+ // If no annotation, keep type as 'any' (default)
183
+ // if (type === 'any' && (!decl.typeAnnotations || !decl.typeAnnotations[index])) {
184
+ // type = inferred;
185
+ // }
186
+ }
187
+ if (n.kind === AST.NodeType.Identifier) {
188
+ this.currentScope.define(n.name, kind, type, decl.line || 0);
189
+ }
190
+ else if (n.kind === AST.NodeType.ObjectDestructure) {
191
+ n.properties.forEach(p => {
192
+ this.currentScope.define(p.name, kind, 'any', decl.line || 0);
193
+ });
194
+ }
195
+ else if (n.kind === AST.NodeType.ArrayDestructure) {
196
+ n.elements.forEach(el => {
197
+ this.currentScope.define(el.name, kind, 'any', decl.line || 0);
198
+ });
199
+ }
200
+ });
201
+ }
202
+ visitAssignmentStmt(stmt) {
203
+ // Check RHS
204
+ stmt.values.forEach(v => this.visitExpression(v));
205
+ // Check LHS
206
+ stmt.targets.forEach((t, index) => {
207
+ // If it's a simple identifier, check const and TYPE
208
+ if (t.kind === AST.NodeType.Identifier) {
209
+ const name = t.name;
210
+ const sym = this.currentScope.lookup(name);
211
+ if (sym) {
212
+ if (sym.kind === 'const') {
213
+ throw new Error(`Assignment to constant variable '${name}' at line ${stmt.line}`);
214
+ }
215
+ // Type Checking
216
+ if (stmt.values[index]) {
217
+ const valueType = this.inferType(stmt.values[index]);
218
+ if (sym.type !== 'any' && valueType !== 'any' && sym.type !== valueType) {
219
+ throw new Error(`Type mismatch at line ${stmt.line}: Variable '${name}' is type '${sym.type}', but assigned '${valueType}'`);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Handle member expression type checking (Config.Field: type = value)
225
+ if (t.kind === AST.NodeType.MemberExpr) {
226
+ const memberKey = this.getMemberExprKey(t);
227
+ // If this assignment has a type annotation, register it
228
+ if (stmt.typeAnnotation && index === 0) {
229
+ const type = this.stringToType(stmt.typeAnnotation);
230
+ this.memberTypes.set(memberKey, type);
231
+ // Also check that the value matches the declared type
232
+ if (stmt.values[index]) {
233
+ const valueType = this.inferType(stmt.values[index]);
234
+ if (type !== 'any' && valueType !== 'any' && type !== valueType) {
235
+ throw new Error(`Type mismatch at line ${stmt.line}: '${memberKey}' is declared as '${stmt.typeAnnotation}', but assigned '${valueType}'`);
236
+ }
237
+ }
238
+ }
239
+ else {
240
+ // Check if this member has a registered type
241
+ const registeredType = this.memberTypes.get(memberKey);
242
+ if (registeredType && stmt.values[index]) {
243
+ const valueType = this.inferType(stmt.values[index]);
244
+ if (registeredType !== 'any' && valueType !== 'any' && registeredType !== valueType) {
245
+ throw new Error(`Type mismatch at line ${stmt.line}: '${memberKey}' is type '${registeredType}', but assigned '${valueType}'`);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ this.visitExpression(t);
251
+ });
252
+ }
253
+ // Helper to get a string key for a member expression (e.g., "Config.Locale")
254
+ getMemberExprKey(expr) {
255
+ let parts = [];
256
+ let current = expr;
257
+ while (current.kind === AST.NodeType.MemberExpr) {
258
+ const memberExpr = current;
259
+ if (memberExpr.property.kind === AST.NodeType.Identifier) {
260
+ parts.unshift(memberExpr.property.name);
261
+ }
262
+ current = memberExpr.object;
263
+ }
264
+ if (current.kind === AST.NodeType.Identifier) {
265
+ parts.unshift(current.name);
266
+ }
267
+ return parts.join('.');
268
+ }
269
+ visitCompoundAssignment(stmt) {
270
+ this.visitExpression(stmt.value);
271
+ if (stmt.target.kind === AST.NodeType.Identifier) {
272
+ const name = stmt.target.name;
273
+ const sym = this.currentScope.lookup(name);
274
+ if (sym && sym.kind === 'const') {
275
+ throw new Error(`Assignment to constant variable '${name}' at line ${stmt.line}`);
276
+ }
277
+ }
278
+ this.visitExpression(stmt.target);
279
+ }
280
+ visitFunctionDecl(decl) {
281
+ // Name is defined in CURRENT scope
282
+ if (decl.name && decl.name.kind === AST.NodeType.Identifier) {
283
+ this.currentScope.define(decl.name.name, 'const', 'function', decl.line || 0);
284
+ }
285
+ this.enterScope();
286
+ decl.params.forEach(p => {
287
+ // Parse param type annotation
288
+ let type = 'any';
289
+ if (p.typeAnnotation) {
290
+ type = this.stringToType(p.typeAnnotation);
291
+ }
292
+ this.currentScope.define(p.name.name, 'param', type, decl.line || 0);
293
+ if (p.defaultValue) {
294
+ const defType = this.inferType(p.defaultValue);
295
+ if (type !== 'any' && defType !== 'any' && type !== defType) {
296
+ throw new Error(`Type mismatch in parameter default value at line ${decl.line}`);
297
+ }
298
+ this.visitExpression(p.defaultValue);
299
+ }
300
+ });
301
+ this.visitBlock(decl.body);
302
+ this.exitScope();
303
+ }
304
+ visitBlock(block) {
305
+ this.enterScope();
306
+ block.statements.forEach(s => this.visitStatement(s));
307
+ this.exitScope();
308
+ }
309
+ visitExpression(expr) {
310
+ // Mostly just traversing to find nested scopes in functions/arrows
311
+ if (!expr)
312
+ return;
313
+ if (expr.kind === AST.NodeType.ArrowFunc) {
314
+ const arrow = expr;
315
+ this.enterScope();
316
+ arrow.params.forEach(p => {
317
+ let type = 'any';
318
+ if (p.typeAnnotation) {
319
+ type = this.stringToType(p.typeAnnotation);
320
+ }
321
+ this.currentScope.define(p.name.name, 'param', type, expr.line || 0);
322
+ if (p.defaultValue)
323
+ this.visitExpression(p.defaultValue);
324
+ });
325
+ if ('kind' in arrow.body && arrow.body.kind === AST.NodeType.Block) {
326
+ this.visitBlock(arrow.body);
327
+ }
328
+ else {
329
+ this.visitExpression(arrow.body);
330
+ }
331
+ this.exitScope();
332
+ return;
333
+ }
334
+ if (expr.kind === AST.NodeType.FunctionDecl) {
335
+ this.visitFunctionDecl(expr);
336
+ return;
337
+ }
338
+ // Recurse
339
+ this.visitChildren(expr);
340
+ }
341
+ inferType(expr) {
342
+ if (!expr)
343
+ return 'any';
344
+ switch (expr.kind) {
345
+ case AST.NodeType.NumberLiteral: return 'number';
346
+ case AST.NodeType.StringLiteral: return 'string';
347
+ case AST.NodeType.InterpolatedString: return 'string';
348
+ case AST.NodeType.BooleanLiteral: return 'boolean';
349
+ case AST.NodeType.NilLiteral: return 'nil';
350
+ case AST.NodeType.VectorLiteral: return 'vector';
351
+ case AST.NodeType.TableLiteral: return 'table';
352
+ case AST.NodeType.FunctionDecl: return 'function';
353
+ case AST.NodeType.ArrowFunc: return 'function';
354
+ case AST.NodeType.Identifier:
355
+ const sym = this.currentScope.lookup(expr.name);
356
+ return sym ? sym.type : 'any';
357
+ // TODO: Improve binary expr inference (e.g. number + number = number)
358
+ case AST.NodeType.BinaryExpr: return 'any';
359
+ default:
360
+ return 'any';
361
+ }
362
+ }
363
+ stringToType(str) {
364
+ switch (str) {
365
+ case 'string': return 'string';
366
+ case 'number': return 'number';
367
+ case 'boolean': return 'boolean';
368
+ case 'vector': return 'vector';
369
+ case 'table': return 'table';
370
+ case 'function': return 'function';
371
+ case 'any': return 'any';
372
+ default:
373
+ // Check if it's a custom type
374
+ if (this.typeRegistry.has(str)) {
375
+ return 'table'; // Custom types are tables internally
376
+ }
377
+ return 'any';
378
+ }
379
+ }
380
+ visitTypeDecl(decl) {
381
+ const typeName = decl.name.name;
382
+ // Register the type
383
+ this.typeRegistry.set(typeName, decl.fields);
384
+ }
385
+ validateTableAgainstType(table, typeName, line) {
386
+ const fields = this.typeRegistry.get(typeName);
387
+ if (!fields)
388
+ return; // Unknown type, skip validation
389
+ const providedFields = new Map();
390
+ for (const field of table.fields) {
391
+ if (field.key && field.key.kind === AST.NodeType.Identifier) {
392
+ providedFields.set(field.key.name, field.value);
393
+ }
394
+ else if (field.key && field.key.kind === AST.NodeType.StringLiteral) {
395
+ providedFields.set(field.key.value, field.value);
396
+ }
397
+ }
398
+ // Check each expected field
399
+ for (const expected of fields) {
400
+ const provided = providedFields.get(expected.name);
401
+ if (!provided) {
402
+ throw new Error(`Type '${typeName}' requires field '${expected.name}' at line ${line}`);
403
+ }
404
+ const providedType = this.inferType(provided);
405
+ const expectedType = this.stringToType(expected.type);
406
+ if (expectedType !== 'any' && providedType !== 'any' && expectedType !== providedType) {
407
+ throw new Error(`Type mismatch: field '${expected.name}' should be '${expected.type}', got '${providedType}' at line ${line}`);
408
+ }
409
+ }
410
+ }
411
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Lexer } from './compiler/lexer/Lexer.js';
2
2
  import { Parser } from './compiler/parser/Parser.js';
3
3
  import { LuaEmitter } from './compiler/codegen/LuaEmitter.js';
4
+ import { SemanticAnalyzer } from './compiler/semantics/SemanticAnalyzer.js';
4
5
  export declare function compile(source: string): string;
5
- export { Lexer, Parser, LuaEmitter };
6
+ export { Lexer, Parser, LuaEmitter, SemanticAnalyzer };
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
1
  import { Lexer } from './compiler/lexer/Lexer.js';
2
2
  import { Parser } from './compiler/parser/Parser.js';
3
3
  import { LuaEmitter } from './compiler/codegen/LuaEmitter.js';
4
+ import { SemanticAnalyzer } from './compiler/semantics/SemanticAnalyzer.js';
4
5
  export function compile(source) {
5
6
  const lexer = new Lexer(source);
6
7
  const tokens = lexer.tokenize();
8
+ // DEBUG: Check first token to see if it has line info
9
+ // if (tokens.length > 0) {
10
+ // console.log('[DEBUG] First Token:', JSON.stringify(tokens[0]));
11
+ // console.log('[DEBUG] Token count:', tokens.length);
12
+ // }
7
13
  const parser = new Parser(tokens);
8
14
  const ast = parser.parse();
15
+ const analyzer = new SemanticAnalyzer();
16
+ analyzer.analyze(ast);
9
17
  const emitter = new LuaEmitter();
10
18
  return emitter.emit(ast);
11
19
  }
12
- export { Lexer, Parser, LuaEmitter };
20
+ export { Lexer, Parser, LuaEmitter, SemanticAnalyzer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lt-script",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "LT Language Compiler for FiveM",
6
6
  "main": "dist/index.js",
@@ -18,16 +18,25 @@
18
18
  "laot",
19
19
  "lt"
20
20
  ],
21
- "author": "https://github.com/laot7490",
21
+ "author": "laot",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/laot7490/lt-script.git"
25
+ },
22
26
  "license": "MIT",
23
27
  "scripts": {
24
28
  "build": "tsc",
25
29
  "dev": "tsc --watch",
26
30
  "test": "node dist/tests/run.js",
27
- "prepublishOnly": "npm run build"
31
+ "build:lt": "npm run build",
32
+ "build:vscode": "cd vscode-extension && npx vsce package",
33
+ "watch:playground": "tsx src/cli/ltc.ts watch playground",
34
+ "watch:testing": "tsx src/cli/ltc.ts watch .testing",
35
+ "github:push": "node tools/push.js"
28
36
  },
29
37
  "devDependencies": {
30
38
  "@types/node": "^25.0.10",
39
+ "tsx": "^4.21.0",
31
40
  "typescript": "^5.3.0"
32
41
  }
33
42
  }