lt-script 1.0.0 → 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.
- package/README.md +257 -16
- package/dist/cli/ltc.js +44 -34
- package/dist/cli/utils.d.ts +40 -0
- package/dist/cli/utils.js +40 -0
- package/dist/compiler/codegen/LuaEmitter.js +19 -3
- package/dist/compiler/lexer/Lexer.js +7 -2
- package/dist/compiler/lexer/Token.d.ts +2 -1
- package/dist/compiler/lexer/Token.js +6 -2
- package/dist/compiler/parser/AST.d.ts +12 -0
- package/dist/compiler/parser/AST.js +2 -0
- package/dist/compiler/parser/Parser.d.ts +4 -0
- package/dist/compiler/parser/Parser.js +141 -26
- package/dist/compiler/semantics/SemanticAnalyzer.d.ts +23 -0
- package/dist/compiler/semantics/SemanticAnalyzer.js +411 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/package.json +12 -3
|
@@ -33,6 +33,8 @@ export var NodeType;
|
|
|
33
33
|
NodeType["EmitStmt"] = "EmitStmt";
|
|
34
34
|
NodeType["EventHandler"] = "EventHandler";
|
|
35
35
|
NodeType["ExportDecl"] = "ExportDecl";
|
|
36
|
+
// Type System
|
|
37
|
+
NodeType["TypeDecl"] = "TypeDecl";
|
|
36
38
|
// Expressions
|
|
37
39
|
NodeType["BinaryExpr"] = "BinaryExpr";
|
|
38
40
|
NodeType["UnaryExpr"] = "UnaryExpr";
|
|
@@ -30,6 +30,7 @@ export declare class Parser {
|
|
|
30
30
|
private parseSwitchStmt;
|
|
31
31
|
private parseCommandStmt;
|
|
32
32
|
private parseExportDecl;
|
|
33
|
+
private parseTypeDecl;
|
|
33
34
|
private parseExpressionStatement;
|
|
34
35
|
private parseExpression;
|
|
35
36
|
private parseAssignment;
|
|
@@ -44,15 +45,18 @@ export declare class Parser {
|
|
|
44
45
|
private parseMultiplicative;
|
|
45
46
|
private parseUnary;
|
|
46
47
|
private parseCallMember;
|
|
48
|
+
private parseMemberSuffix;
|
|
47
49
|
private parseCallExpr;
|
|
48
50
|
private parsePrimary;
|
|
49
51
|
private parseStringLiteral;
|
|
50
52
|
private parseInterpolatedString;
|
|
53
|
+
private parseEmbeddedExpression;
|
|
51
54
|
private parseParenOrArrow;
|
|
52
55
|
private parseArrayLiteral;
|
|
53
56
|
private parseTableLiteral;
|
|
54
57
|
private parseVectorLiteral;
|
|
55
58
|
private parseIdentifier;
|
|
59
|
+
private parseIdentifierName;
|
|
56
60
|
private parseParameter;
|
|
57
61
|
private parseObjectDestructure;
|
|
58
62
|
private parseArrayDestructure;
|
|
@@ -69,8 +69,10 @@ export class Parser {
|
|
|
69
69
|
return this.parseSwitchStmt();
|
|
70
70
|
case TokenType.EXPORT:
|
|
71
71
|
return this.parseExportDecl();
|
|
72
|
-
case TokenType.
|
|
72
|
+
case TokenType.ADDCMD:
|
|
73
73
|
return this.parseCommandStmt();
|
|
74
|
+
case TokenType.INTERFACE:
|
|
75
|
+
return this.parseTypeDecl();
|
|
74
76
|
default:
|
|
75
77
|
return this.parseExpressionStatement();
|
|
76
78
|
}
|
|
@@ -91,7 +93,7 @@ export class Parser {
|
|
|
91
93
|
return this.parseObjectDestructure();
|
|
92
94
|
if (this.at().type === TokenType.LBRACKET)
|
|
93
95
|
return this.parseArrayDestructure();
|
|
94
|
-
const id = this.
|
|
96
|
+
const id = this.parseIdentifierName();
|
|
95
97
|
// Lua 5.4 Attributes: <const>, <close>
|
|
96
98
|
if (this.match(TokenType.LT)) {
|
|
97
99
|
// Consume the attribute name (usually 'const' or 'close')
|
|
@@ -119,7 +121,7 @@ export class Parser {
|
|
|
119
121
|
values.push(this.parseExpression(true));
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
|
-
return { kind: AST.NodeType.VariableDecl, scope, names, typeAnnotations, values };
|
|
124
|
+
return { kind: AST.NodeType.VariableDecl, scope, names, typeAnnotations, values, line: scopeToken.line, column: scopeToken.column };
|
|
123
125
|
}
|
|
124
126
|
parseIfStmt() {
|
|
125
127
|
this.eat(); // if
|
|
@@ -224,6 +226,7 @@ export class Parser {
|
|
|
224
226
|
this.eat(); // guard
|
|
225
227
|
const condition = this.parseExpression();
|
|
226
228
|
let elseBody;
|
|
229
|
+
let returnValue;
|
|
227
230
|
if (this.match(TokenType.ELSE)) {
|
|
228
231
|
elseBody = [];
|
|
229
232
|
while (!this.check(TokenType.RETURN) && !this.isEOF()) {
|
|
@@ -233,9 +236,29 @@ export class Parser {
|
|
|
233
236
|
this.match(TokenType.END); // Consume optional END
|
|
234
237
|
}
|
|
235
238
|
else {
|
|
236
|
-
this.expect(TokenType.RETURN);
|
|
239
|
+
const returnToken = this.expect(TokenType.RETURN);
|
|
240
|
+
const nextToken = this.at();
|
|
241
|
+
// Check if there's a return value on the same line
|
|
242
|
+
if (nextToken.line === returnToken.line && !this.isStatementEnd()) {
|
|
243
|
+
// Check if this is a simple expression (nil, number, string, identifier, etc.)
|
|
244
|
+
// rather than a statement that starts with these tokens
|
|
245
|
+
if (this.check(TokenType.NIL) ||
|
|
246
|
+
this.check(TokenType.NUMBER) ||
|
|
247
|
+
this.check(TokenType.STRING) ||
|
|
248
|
+
this.check(TokenType.BOOLEAN) ||
|
|
249
|
+
this.check(TokenType.IDENTIFIER) ||
|
|
250
|
+
this.check(TokenType.LBRACE) ||
|
|
251
|
+
this.check(TokenType.LBRACKET)) {
|
|
252
|
+
// Try to parse as expression first for simple return values
|
|
253
|
+
returnValue = this.parseExpression();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// For function calls like print("Fail"), parse as statement
|
|
257
|
+
elseBody = [this.parseStatement()];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
237
260
|
}
|
|
238
|
-
return { kind: AST.NodeType.GuardStmt, condition, elseBody };
|
|
261
|
+
return { kind: AST.NodeType.GuardStmt, condition, elseBody, returnValue };
|
|
239
262
|
}
|
|
240
263
|
parseSafeCall() {
|
|
241
264
|
this.eat(); // safe
|
|
@@ -405,7 +428,7 @@ export class Parser {
|
|
|
405
428
|
return { kind: AST.NodeType.SwitchStmt, discriminant, cases, defaultCase };
|
|
406
429
|
}
|
|
407
430
|
parseCommandStmt() {
|
|
408
|
-
this.eat(); //
|
|
431
|
+
this.eat(); // addcmd
|
|
409
432
|
let nameVal;
|
|
410
433
|
if (this.at().type === TokenType.STRING) {
|
|
411
434
|
nameVal = this.eat().value;
|
|
@@ -434,32 +457,79 @@ export class Parser {
|
|
|
434
457
|
}
|
|
435
458
|
throw new Error(`Expected function declaration after export at ${this.at().line}:${this.at().column}`);
|
|
436
459
|
}
|
|
460
|
+
parseTypeDecl() {
|
|
461
|
+
this.eat(); // interface
|
|
462
|
+
const name = this.parseIdentifier();
|
|
463
|
+
this.expect(TokenType.EQUALS);
|
|
464
|
+
this.expect(TokenType.LBRACE);
|
|
465
|
+
const fields = [];
|
|
466
|
+
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
467
|
+
const fieldName = this.parseIdentifier().name;
|
|
468
|
+
this.expect(TokenType.COLON);
|
|
469
|
+
const fieldType = this.eat().value; // Get the type as string
|
|
470
|
+
fields.push({ name: fieldName, type: fieldType });
|
|
471
|
+
// Allow trailing comma
|
|
472
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
473
|
+
this.match(TokenType.COMMA);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.expect(TokenType.RBRACE);
|
|
477
|
+
return { kind: AST.NodeType.TypeDecl, name, fields };
|
|
478
|
+
}
|
|
437
479
|
parseExpressionStatement() {
|
|
438
|
-
|
|
480
|
+
// Parse expression but don't allow colon for type annotation detection
|
|
481
|
+
let expr = this.parseExpression(false);
|
|
482
|
+
// Type annotation for table field assignment: Config.Field: type = value
|
|
483
|
+
// When we have an expression followed by COLON + IDENTIFIER + EQUALS
|
|
484
|
+
if (this.check(TokenType.COLON) && expr.kind === AST.NodeType.MemberExpr) {
|
|
485
|
+
const colonPos = this.pos;
|
|
486
|
+
this.eat(); // consume :
|
|
487
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
488
|
+
const typeToken = this.eat(); // consume type
|
|
489
|
+
if (this.check(TokenType.EQUALS)) {
|
|
490
|
+
// This is a typed assignment: Config.Field: type = value
|
|
491
|
+
const typeAnnotation = typeToken.value;
|
|
492
|
+
this.eat(); // consume =
|
|
493
|
+
const value = this.parseExpression();
|
|
494
|
+
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value], typeAnnotation, line: expr.line, column: expr.column };
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Not a typed assignment, backtrack
|
|
498
|
+
this.pos = colonPos;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// Not a type annotation, backtrack
|
|
503
|
+
this.pos = colonPos;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Continue parsing member expression (method calls were disabled in initial parse)
|
|
507
|
+
// This allows: Entity(vehicle).state:set(...)
|
|
508
|
+
expr = this.parseMemberSuffix(expr, true);
|
|
439
509
|
// Compound assignment
|
|
440
510
|
if (this.match(TokenType.PLUS_EQ)) {
|
|
441
511
|
const value = this.parseExpression();
|
|
442
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "+=", value };
|
|
512
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "+=", value, line: expr.line, column: expr.column };
|
|
443
513
|
}
|
|
444
514
|
if (this.match(TokenType.MINUS_EQ)) {
|
|
445
515
|
const value = this.parseExpression();
|
|
446
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "-=", value };
|
|
516
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "-=", value, line: expr.line, column: expr.column };
|
|
447
517
|
}
|
|
448
518
|
if (this.match(TokenType.STAR_EQ)) {
|
|
449
519
|
const value = this.parseExpression();
|
|
450
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "*=", value };
|
|
520
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "*=", value, line: expr.line, column: expr.column };
|
|
451
521
|
}
|
|
452
522
|
if (this.match(TokenType.SLASH_EQ)) {
|
|
453
523
|
const value = this.parseExpression();
|
|
454
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "/=", value };
|
|
524
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "/=", value, line: expr.line, column: expr.column };
|
|
455
525
|
}
|
|
456
526
|
if (this.match(TokenType.PERCENT_EQ)) {
|
|
457
527
|
const value = this.parseExpression();
|
|
458
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "%=", value };
|
|
528
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "%=", value, line: expr.line, column: expr.column };
|
|
459
529
|
}
|
|
460
530
|
if (this.match(TokenType.CONCAT_EQ)) {
|
|
461
531
|
const value = this.parseExpression();
|
|
462
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "..=", value };
|
|
532
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "..=", value, line: expr.line, column: expr.column };
|
|
463
533
|
}
|
|
464
534
|
// Multiple targets: a, b = 1, 2
|
|
465
535
|
if (this.at().type === TokenType.COMMA) {
|
|
@@ -473,12 +543,12 @@ export class Parser {
|
|
|
473
543
|
while (this.match(TokenType.COMMA)) {
|
|
474
544
|
values.push(this.parseExpression());
|
|
475
545
|
}
|
|
476
|
-
return { kind: AST.NodeType.AssignmentStmt, targets, values };
|
|
546
|
+
return { kind: AST.NodeType.AssignmentStmt, targets, values, line: expr.line, column: expr.column };
|
|
477
547
|
}
|
|
478
548
|
// Regular assignment
|
|
479
549
|
if (this.match(TokenType.EQUALS)) {
|
|
480
550
|
const value = this.parseExpression();
|
|
481
|
-
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value] };
|
|
551
|
+
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value], line: expr.line, column: expr.column };
|
|
482
552
|
}
|
|
483
553
|
return expr;
|
|
484
554
|
}
|
|
@@ -585,9 +655,12 @@ export class Parser {
|
|
|
585
655
|
}
|
|
586
656
|
parseCallMember(allowColon = true) {
|
|
587
657
|
let expr = this.parsePrimary();
|
|
658
|
+
return this.parseMemberSuffix(expr, allowColon);
|
|
659
|
+
}
|
|
660
|
+
parseMemberSuffix(expr, allowColon) {
|
|
588
661
|
while (true) {
|
|
589
662
|
if (this.match(TokenType.OPT_DOT)) {
|
|
590
|
-
const property = this.
|
|
663
|
+
const property = this.parseIdentifierName();
|
|
591
664
|
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: false };
|
|
592
665
|
}
|
|
593
666
|
else if (this.match(TokenType.OPT_BRACKET)) {
|
|
@@ -596,11 +669,11 @@ export class Parser {
|
|
|
596
669
|
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: true };
|
|
597
670
|
}
|
|
598
671
|
else if (this.match(TokenType.DOT)) {
|
|
599
|
-
const property = this.
|
|
600
|
-
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false };
|
|
672
|
+
const property = this.parseIdentifierName();
|
|
673
|
+
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false, line: expr.line, column: expr.column };
|
|
601
674
|
}
|
|
602
675
|
else if (allowColon && this.match(TokenType.COLON)) {
|
|
603
|
-
const property = this.
|
|
676
|
+
const property = this.parseIdentifierName();
|
|
604
677
|
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false, isMethod: true };
|
|
605
678
|
}
|
|
606
679
|
else if (this.match(TokenType.LBRACKET)) {
|
|
@@ -669,9 +742,8 @@ export class Parser {
|
|
|
669
742
|
const value = tk.value;
|
|
670
743
|
const isLong = tk.type === TokenType.LONG_STRING;
|
|
671
744
|
const quote = tk.quote || '"'; // Default to double quote for long strings
|
|
672
|
-
// Check for interpolation
|
|
673
|
-
|
|
674
|
-
const hasInterpolation = /\$[a-zA-Z_]/.test(value);
|
|
745
|
+
// Check for interpolation: ${...} syntax
|
|
746
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(value);
|
|
675
747
|
if (hasInterpolation) {
|
|
676
748
|
return this.parseInterpolatedString(value, quote);
|
|
677
749
|
}
|
|
@@ -679,21 +751,48 @@ export class Parser {
|
|
|
679
751
|
}
|
|
680
752
|
parseInterpolatedString(str, quote = '"') {
|
|
681
753
|
const parts = [];
|
|
682
|
-
|
|
754
|
+
// Match ${expression} pattern - captures everything between ${ and }
|
|
755
|
+
const regex = /\$\{([^}]+)\}/g;
|
|
683
756
|
let lastIndex = 0;
|
|
684
757
|
let match;
|
|
685
758
|
while ((match = regex.exec(str)) !== null) {
|
|
759
|
+
// Add text before the interpolation
|
|
686
760
|
if (match.index > lastIndex) {
|
|
687
761
|
parts.push(str.slice(lastIndex, match.index));
|
|
688
762
|
}
|
|
689
|
-
|
|
763
|
+
// Parse the expression inside ${...}
|
|
764
|
+
const exprStr = match[1].trim();
|
|
765
|
+
const exprAst = this.parseEmbeddedExpression(exprStr);
|
|
766
|
+
parts.push(exprAst);
|
|
690
767
|
lastIndex = regex.lastIndex;
|
|
691
768
|
}
|
|
769
|
+
// Add remaining text after last interpolation
|
|
692
770
|
if (lastIndex < str.length) {
|
|
693
771
|
parts.push(str.slice(lastIndex));
|
|
694
772
|
}
|
|
695
773
|
return { kind: AST.NodeType.InterpolatedString, parts, quote };
|
|
696
774
|
}
|
|
775
|
+
// Parse an embedded expression string (for ${...} interpolation)
|
|
776
|
+
parseEmbeddedExpression(exprStr) {
|
|
777
|
+
// Import Lexer dynamically to avoid circular deps - use simple parsing
|
|
778
|
+
// For member expressions like Config.ServerName, parse manually
|
|
779
|
+
const parts = exprStr.split('.');
|
|
780
|
+
if (parts.length === 1) {
|
|
781
|
+
// Simple identifier
|
|
782
|
+
return { kind: AST.NodeType.Identifier, name: parts[0] };
|
|
783
|
+
}
|
|
784
|
+
// Build member expression chain: Config.ServerName.foo
|
|
785
|
+
let expr = { kind: AST.NodeType.Identifier, name: parts[0] };
|
|
786
|
+
for (let i = 1; i < parts.length; i++) {
|
|
787
|
+
expr = {
|
|
788
|
+
kind: AST.NodeType.MemberExpr,
|
|
789
|
+
object: expr,
|
|
790
|
+
property: { kind: AST.NodeType.Identifier, name: parts[i] },
|
|
791
|
+
computed: false
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return expr;
|
|
795
|
+
}
|
|
697
796
|
parseParenOrArrow() {
|
|
698
797
|
const checkpoint = this.pos;
|
|
699
798
|
this.eat(); // (
|
|
@@ -792,10 +891,26 @@ export class Parser {
|
|
|
792
891
|
}
|
|
793
892
|
parseIdentifier() {
|
|
794
893
|
const token = this.expect(TokenType.IDENTIFIER);
|
|
795
|
-
return { kind: AST.NodeType.Identifier, name: token.value };
|
|
894
|
+
return { kind: AST.NodeType.Identifier, name: token.value, line: token.line, column: token.column };
|
|
895
|
+
}
|
|
896
|
+
parseIdentifierName() {
|
|
897
|
+
const token = this.at();
|
|
898
|
+
// Allow identifier or any token that looks like an identifier (including keywords)
|
|
899
|
+
// Exclude literals that definitely aren't identifiers
|
|
900
|
+
if (token.type === TokenType.STRING || token.type === TokenType.LONG_STRING ||
|
|
901
|
+
token.type === TokenType.NUMBER || token.type === TokenType.EOF) {
|
|
902
|
+
throw new Error(`Expected identifier name but got ${token.type} at ${token.line}:${token.column}`);
|
|
903
|
+
}
|
|
904
|
+
// Check regex to ensure it's alphanumeric (handles keywords like 'default', 'if', etc.)
|
|
905
|
+
if (/^[a-zA-Z_]\w*$/.test(token.value)) {
|
|
906
|
+
this.eat();
|
|
907
|
+
return { kind: AST.NodeType.Identifier, name: token.value, line: token.line, column: token.column };
|
|
908
|
+
}
|
|
909
|
+
// Fallback to standard expect for clear error message
|
|
910
|
+
return this.parseIdentifier();
|
|
796
911
|
}
|
|
797
912
|
parseParameter() {
|
|
798
|
-
const name = this.
|
|
913
|
+
const name = this.parseIdentifierName();
|
|
799
914
|
let typeAnnotation;
|
|
800
915
|
if (this.match(TokenType.COLON)) {
|
|
801
916
|
typeAnnotation = this.eat().value;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as AST from '../parser/AST.js';
|
|
2
|
+
export declare class SemanticAnalyzer {
|
|
3
|
+
private globalScope;
|
|
4
|
+
private currentScope;
|
|
5
|
+
private typeRegistry;
|
|
6
|
+
private memberTypes;
|
|
7
|
+
analyze(program: AST.Program): void;
|
|
8
|
+
private enterScope;
|
|
9
|
+
private exitScope;
|
|
10
|
+
private visitStatement;
|
|
11
|
+
private visitChildren;
|
|
12
|
+
private visitVariableDecl;
|
|
13
|
+
private visitAssignmentStmt;
|
|
14
|
+
private getMemberExprKey;
|
|
15
|
+
private visitCompoundAssignment;
|
|
16
|
+
private visitFunctionDecl;
|
|
17
|
+
private visitBlock;
|
|
18
|
+
private visitExpression;
|
|
19
|
+
private inferType;
|
|
20
|
+
private stringToType;
|
|
21
|
+
private visitTypeDecl;
|
|
22
|
+
private validateTableAgainstType;
|
|
23
|
+
}
|