lt-script 1.0.1 → 1.0.5
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 +548 -15
- 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 +20 -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 +18 -0
- package/dist/compiler/parser/AST.js +3 -0
- package/dist/compiler/parser/Parser.d.ts +6 -0
- package/dist/compiler/parser/Parser.js +216 -31
- package/dist/compiler/semantics/SemanticAnalyzer.d.ts +28 -0
- package/dist/compiler/semantics/SemanticAnalyzer.js +682 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/package.json +12 -3
|
@@ -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:
|
|
@@ -69,12 +81,73 @@ export class Parser {
|
|
|
69
81
|
return this.parseSwitchStmt();
|
|
70
82
|
case TokenType.EXPORT:
|
|
71
83
|
return this.parseExportDecl();
|
|
72
|
-
case TokenType.
|
|
84
|
+
case TokenType.ADDCMD:
|
|
73
85
|
return this.parseCommandStmt();
|
|
86
|
+
case TokenType.INTERFACE:
|
|
87
|
+
return this.parseTypeDecl();
|
|
74
88
|
default:
|
|
75
89
|
return this.parseExpressionStatement();
|
|
76
90
|
}
|
|
77
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 ============
|
|
78
151
|
parseVariableDecl() {
|
|
79
152
|
const scopeToken = this.eat();
|
|
80
153
|
const scope = scopeToken.value;
|
|
@@ -91,7 +164,7 @@ export class Parser {
|
|
|
91
164
|
return this.parseObjectDestructure();
|
|
92
165
|
if (this.at().type === TokenType.LBRACKET)
|
|
93
166
|
return this.parseArrayDestructure();
|
|
94
|
-
const id = this.
|
|
167
|
+
const id = this.parseIdentifierName();
|
|
95
168
|
// Lua 5.4 Attributes: <const>, <close>
|
|
96
169
|
if (this.match(TokenType.LT)) {
|
|
97
170
|
// Consume the attribute name (usually 'const' or 'close')
|
|
@@ -103,12 +176,12 @@ export class Parser {
|
|
|
103
176
|
};
|
|
104
177
|
names.push(parseName());
|
|
105
178
|
if (this.match(TokenType.COLON)) {
|
|
106
|
-
typeAnnotations[0] = this.
|
|
179
|
+
typeAnnotations[0] = this.parseType();
|
|
107
180
|
}
|
|
108
181
|
while (this.match(TokenType.COMMA)) {
|
|
109
182
|
names.push(parseName());
|
|
110
183
|
if (this.match(TokenType.COLON)) {
|
|
111
|
-
typeAnnotations[names.length - 1] = this.
|
|
184
|
+
typeAnnotations[names.length - 1] = this.parseType();
|
|
112
185
|
}
|
|
113
186
|
}
|
|
114
187
|
let values;
|
|
@@ -119,7 +192,7 @@ export class Parser {
|
|
|
119
192
|
values.push(this.parseExpression(true));
|
|
120
193
|
}
|
|
121
194
|
}
|
|
122
|
-
return { kind: AST.NodeType.VariableDecl, scope, names, typeAnnotations, values };
|
|
195
|
+
return { kind: AST.NodeType.VariableDecl, scope, names, typeAnnotations, values, line: scopeToken.line, column: scopeToken.column };
|
|
123
196
|
}
|
|
124
197
|
parseIfStmt() {
|
|
125
198
|
this.eat(); // if
|
|
@@ -224,6 +297,7 @@ export class Parser {
|
|
|
224
297
|
this.eat(); // guard
|
|
225
298
|
const condition = this.parseExpression();
|
|
226
299
|
let elseBody;
|
|
300
|
+
let returnValue;
|
|
227
301
|
if (this.match(TokenType.ELSE)) {
|
|
228
302
|
elseBody = [];
|
|
229
303
|
while (!this.check(TokenType.RETURN) && !this.isEOF()) {
|
|
@@ -233,9 +307,29 @@ export class Parser {
|
|
|
233
307
|
this.match(TokenType.END); // Consume optional END
|
|
234
308
|
}
|
|
235
309
|
else {
|
|
236
|
-
this.expect(TokenType.RETURN);
|
|
310
|
+
const returnToken = this.expect(TokenType.RETURN);
|
|
311
|
+
const nextToken = this.at();
|
|
312
|
+
// Check if there's a return value on the same line
|
|
313
|
+
if (nextToken.line === returnToken.line && !this.isStatementEnd()) {
|
|
314
|
+
// Check if this is a simple expression (nil, number, string, identifier, etc.)
|
|
315
|
+
// rather than a statement that starts with these tokens
|
|
316
|
+
if (this.check(TokenType.NIL) ||
|
|
317
|
+
this.check(TokenType.NUMBER) ||
|
|
318
|
+
this.check(TokenType.STRING) ||
|
|
319
|
+
this.check(TokenType.BOOLEAN) ||
|
|
320
|
+
this.check(TokenType.IDENTIFIER) ||
|
|
321
|
+
this.check(TokenType.LBRACE) ||
|
|
322
|
+
this.check(TokenType.LBRACKET)) {
|
|
323
|
+
// Try to parse as expression first for simple return values
|
|
324
|
+
returnValue = this.parseExpression();
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// For function calls like print("Fail"), parse as statement
|
|
328
|
+
elseBody = [this.parseStatement()];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
237
331
|
}
|
|
238
|
-
return { kind: AST.NodeType.GuardStmt, condition, elseBody };
|
|
332
|
+
return { kind: AST.NodeType.GuardStmt, condition, elseBody, returnValue };
|
|
239
333
|
}
|
|
240
334
|
parseSafeCall() {
|
|
241
335
|
this.eat(); // safe
|
|
@@ -353,7 +447,7 @@ export class Parser {
|
|
|
353
447
|
this.expect(TokenType.RPAREN);
|
|
354
448
|
let returnType;
|
|
355
449
|
if (this.match(TokenType.COLON)) {
|
|
356
|
-
returnType = this.
|
|
450
|
+
returnType = this.parseType();
|
|
357
451
|
}
|
|
358
452
|
const body = this.parseBlockUntil(TokenType.END);
|
|
359
453
|
this.expect(TokenType.END);
|
|
@@ -376,7 +470,7 @@ export class Parser {
|
|
|
376
470
|
this.expect(TokenType.RPAREN);
|
|
377
471
|
let returnType;
|
|
378
472
|
if (this.match(TokenType.COLON)) {
|
|
379
|
-
returnType = this.
|
|
473
|
+
returnType = this.parseType();
|
|
380
474
|
}
|
|
381
475
|
const body = this.parseBlockUntil(TokenType.END);
|
|
382
476
|
this.expect(TokenType.END);
|
|
@@ -405,7 +499,7 @@ export class Parser {
|
|
|
405
499
|
return { kind: AST.NodeType.SwitchStmt, discriminant, cases, defaultCase };
|
|
406
500
|
}
|
|
407
501
|
parseCommandStmt() {
|
|
408
|
-
this.eat(); //
|
|
502
|
+
this.eat(); // addcmd
|
|
409
503
|
let nameVal;
|
|
410
504
|
if (this.at().type === TokenType.STRING) {
|
|
411
505
|
nameVal = this.eat().value;
|
|
@@ -434,32 +528,78 @@ export class Parser {
|
|
|
434
528
|
}
|
|
435
529
|
throw new Error(`Expected function declaration after export at ${this.at().line}:${this.at().column}`);
|
|
436
530
|
}
|
|
531
|
+
parseTypeDecl() {
|
|
532
|
+
this.eat(); // interface
|
|
533
|
+
const name = this.parseIdentifier();
|
|
534
|
+
this.expect(TokenType.LBRACE);
|
|
535
|
+
const fields = [];
|
|
536
|
+
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
537
|
+
const fieldName = this.parseIdentifier().name;
|
|
538
|
+
this.expect(TokenType.COLON);
|
|
539
|
+
const fieldType = this.parseType(); // Get the type as string
|
|
540
|
+
fields.push({ name: fieldName, type: fieldType });
|
|
541
|
+
// Allow trailing comma
|
|
542
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
543
|
+
this.match(TokenType.COMMA);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
this.expect(TokenType.RBRACE);
|
|
547
|
+
return { kind: AST.NodeType.TypeDecl, name, fields };
|
|
548
|
+
}
|
|
437
549
|
parseExpressionStatement() {
|
|
438
|
-
|
|
550
|
+
// Parse expression but don't allow colon for type annotation detection
|
|
551
|
+
let expr = this.parseExpression(false);
|
|
552
|
+
// Type annotation for table field assignment: Config.Field: type = value
|
|
553
|
+
// When we have an expression followed by COLON + IDENTIFIER + EQUALS
|
|
554
|
+
if (this.check(TokenType.COLON) && expr.kind === AST.NodeType.MemberExpr) {
|
|
555
|
+
const colonPos = this.pos;
|
|
556
|
+
this.eat(); // consume :
|
|
557
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
558
|
+
const typeToken = this.eat(); // consume type
|
|
559
|
+
if (this.check(TokenType.EQUALS)) {
|
|
560
|
+
// This is a typed assignment: Config.Field: type = value
|
|
561
|
+
const typeAnnotation = typeToken.value;
|
|
562
|
+
this.eat(); // consume =
|
|
563
|
+
const value = this.parseExpression();
|
|
564
|
+
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value], typeAnnotation, line: expr.line, column: expr.column };
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// Not a typed assignment, backtrack
|
|
568
|
+
this.pos = colonPos;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Not a type annotation, backtrack
|
|
573
|
+
this.pos = colonPos;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Continue parsing member expression (method calls were disabled in initial parse)
|
|
577
|
+
// This allows: Entity(vehicle).state:set(...)
|
|
578
|
+
expr = this.parseMemberSuffix(expr, true);
|
|
439
579
|
// Compound assignment
|
|
440
580
|
if (this.match(TokenType.PLUS_EQ)) {
|
|
441
581
|
const value = this.parseExpression();
|
|
442
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "+=", value };
|
|
582
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "+=", value, line: expr.line, column: expr.column };
|
|
443
583
|
}
|
|
444
584
|
if (this.match(TokenType.MINUS_EQ)) {
|
|
445
585
|
const value = this.parseExpression();
|
|
446
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "-=", value };
|
|
586
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "-=", value, line: expr.line, column: expr.column };
|
|
447
587
|
}
|
|
448
588
|
if (this.match(TokenType.STAR_EQ)) {
|
|
449
589
|
const value = this.parseExpression();
|
|
450
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "*=", value };
|
|
590
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "*=", value, line: expr.line, column: expr.column };
|
|
451
591
|
}
|
|
452
592
|
if (this.match(TokenType.SLASH_EQ)) {
|
|
453
593
|
const value = this.parseExpression();
|
|
454
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "/=", value };
|
|
594
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "/=", value, line: expr.line, column: expr.column };
|
|
455
595
|
}
|
|
456
596
|
if (this.match(TokenType.PERCENT_EQ)) {
|
|
457
597
|
const value = this.parseExpression();
|
|
458
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "%=", value };
|
|
598
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "%=", value, line: expr.line, column: expr.column };
|
|
459
599
|
}
|
|
460
600
|
if (this.match(TokenType.CONCAT_EQ)) {
|
|
461
601
|
const value = this.parseExpression();
|
|
462
|
-
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "..=", value };
|
|
602
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "..=", value, line: expr.line, column: expr.column };
|
|
463
603
|
}
|
|
464
604
|
// Multiple targets: a, b = 1, 2
|
|
465
605
|
if (this.at().type === TokenType.COMMA) {
|
|
@@ -473,12 +613,12 @@ export class Parser {
|
|
|
473
613
|
while (this.match(TokenType.COMMA)) {
|
|
474
614
|
values.push(this.parseExpression());
|
|
475
615
|
}
|
|
476
|
-
return { kind: AST.NodeType.AssignmentStmt, targets, values };
|
|
616
|
+
return { kind: AST.NodeType.AssignmentStmt, targets, values, line: expr.line, column: expr.column };
|
|
477
617
|
}
|
|
478
618
|
// Regular assignment
|
|
479
619
|
if (this.match(TokenType.EQUALS)) {
|
|
480
620
|
const value = this.parseExpression();
|
|
481
|
-
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value] };
|
|
621
|
+
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value], line: expr.line, column: expr.column };
|
|
482
622
|
}
|
|
483
623
|
return expr;
|
|
484
624
|
}
|
|
@@ -585,9 +725,12 @@ export class Parser {
|
|
|
585
725
|
}
|
|
586
726
|
parseCallMember(allowColon = true) {
|
|
587
727
|
let expr = this.parsePrimary();
|
|
728
|
+
return this.parseMemberSuffix(expr, allowColon);
|
|
729
|
+
}
|
|
730
|
+
parseMemberSuffix(expr, allowColon) {
|
|
588
731
|
while (true) {
|
|
589
732
|
if (this.match(TokenType.OPT_DOT)) {
|
|
590
|
-
const property = this.
|
|
733
|
+
const property = this.parseIdentifierName();
|
|
591
734
|
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: false };
|
|
592
735
|
}
|
|
593
736
|
else if (this.match(TokenType.OPT_BRACKET)) {
|
|
@@ -596,11 +739,11 @@ export class Parser {
|
|
|
596
739
|
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: true };
|
|
597
740
|
}
|
|
598
741
|
else if (this.match(TokenType.DOT)) {
|
|
599
|
-
const property = this.
|
|
600
|
-
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false };
|
|
742
|
+
const property = this.parseIdentifierName();
|
|
743
|
+
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false, line: expr.line, column: expr.column };
|
|
601
744
|
}
|
|
602
745
|
else if (allowColon && this.match(TokenType.COLON)) {
|
|
603
|
-
const property = this.
|
|
746
|
+
const property = this.parseIdentifierName();
|
|
604
747
|
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false, isMethod: true };
|
|
605
748
|
}
|
|
606
749
|
else if (this.match(TokenType.LBRACKET)) {
|
|
@@ -669,9 +812,8 @@ export class Parser {
|
|
|
669
812
|
const value = tk.value;
|
|
670
813
|
const isLong = tk.type === TokenType.LONG_STRING;
|
|
671
814
|
const quote = tk.quote || '"'; // Default to double quote for long strings
|
|
672
|
-
// Check for interpolation
|
|
673
|
-
|
|
674
|
-
const hasInterpolation = /\$[a-zA-Z_]/.test(value);
|
|
815
|
+
// Check for interpolation: ${...} syntax
|
|
816
|
+
const hasInterpolation = /\$\{[^}]+\}/.test(value);
|
|
675
817
|
if (hasInterpolation) {
|
|
676
818
|
return this.parseInterpolatedString(value, quote);
|
|
677
819
|
}
|
|
@@ -679,21 +821,48 @@ export class Parser {
|
|
|
679
821
|
}
|
|
680
822
|
parseInterpolatedString(str, quote = '"') {
|
|
681
823
|
const parts = [];
|
|
682
|
-
|
|
824
|
+
// Match ${expression} pattern - captures everything between ${ and }
|
|
825
|
+
const regex = /\$\{([^}]+)\}/g;
|
|
683
826
|
let lastIndex = 0;
|
|
684
827
|
let match;
|
|
685
828
|
while ((match = regex.exec(str)) !== null) {
|
|
829
|
+
// Add text before the interpolation
|
|
686
830
|
if (match.index > lastIndex) {
|
|
687
831
|
parts.push(str.slice(lastIndex, match.index));
|
|
688
832
|
}
|
|
689
|
-
|
|
833
|
+
// Parse the expression inside ${...}
|
|
834
|
+
const exprStr = match[1].trim();
|
|
835
|
+
const exprAst = this.parseEmbeddedExpression(exprStr);
|
|
836
|
+
parts.push(exprAst);
|
|
690
837
|
lastIndex = regex.lastIndex;
|
|
691
838
|
}
|
|
839
|
+
// Add remaining text after last interpolation
|
|
692
840
|
if (lastIndex < str.length) {
|
|
693
841
|
parts.push(str.slice(lastIndex));
|
|
694
842
|
}
|
|
695
843
|
return { kind: AST.NodeType.InterpolatedString, parts, quote };
|
|
696
844
|
}
|
|
845
|
+
// Parse an embedded expression string (for ${...} interpolation)
|
|
846
|
+
parseEmbeddedExpression(exprStr) {
|
|
847
|
+
// Import Lexer dynamically to avoid circular deps - use simple parsing
|
|
848
|
+
// For member expressions like Config.ServerName, parse manually
|
|
849
|
+
const parts = exprStr.split('.');
|
|
850
|
+
if (parts.length === 1) {
|
|
851
|
+
// Simple identifier
|
|
852
|
+
return { kind: AST.NodeType.Identifier, name: parts[0] };
|
|
853
|
+
}
|
|
854
|
+
// Build member expression chain: Config.ServerName.foo
|
|
855
|
+
let expr = { kind: AST.NodeType.Identifier, name: parts[0] };
|
|
856
|
+
for (let i = 1; i < parts.length; i++) {
|
|
857
|
+
expr = {
|
|
858
|
+
kind: AST.NodeType.MemberExpr,
|
|
859
|
+
object: expr,
|
|
860
|
+
property: { kind: AST.NodeType.Identifier, name: parts[i] },
|
|
861
|
+
computed: false
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
return expr;
|
|
865
|
+
}
|
|
697
866
|
parseParenOrArrow() {
|
|
698
867
|
const checkpoint = this.pos;
|
|
699
868
|
this.eat(); // (
|
|
@@ -792,13 +961,29 @@ export class Parser {
|
|
|
792
961
|
}
|
|
793
962
|
parseIdentifier() {
|
|
794
963
|
const token = this.expect(TokenType.IDENTIFIER);
|
|
795
|
-
return { kind: AST.NodeType.Identifier, name: token.value };
|
|
964
|
+
return { kind: AST.NodeType.Identifier, name: token.value, line: token.line, column: token.column };
|
|
965
|
+
}
|
|
966
|
+
parseIdentifierName() {
|
|
967
|
+
const token = this.at();
|
|
968
|
+
// Allow identifier or any token that looks like an identifier (including keywords)
|
|
969
|
+
// Exclude literals that definitely aren't identifiers
|
|
970
|
+
if (token.type === TokenType.STRING || token.type === TokenType.LONG_STRING ||
|
|
971
|
+
token.type === TokenType.NUMBER || token.type === TokenType.EOF) {
|
|
972
|
+
throw new Error(`Expected identifier name but got ${token.type} at ${token.line}:${token.column}`);
|
|
973
|
+
}
|
|
974
|
+
// Check regex to ensure it's alphanumeric (handles keywords like 'default', 'if', etc.)
|
|
975
|
+
if (/^[a-zA-Z_]\w*$/.test(token.value)) {
|
|
976
|
+
this.eat();
|
|
977
|
+
return { kind: AST.NodeType.Identifier, name: token.value, line: token.line, column: token.column };
|
|
978
|
+
}
|
|
979
|
+
// Fallback to standard expect for clear error message
|
|
980
|
+
return this.parseIdentifier();
|
|
796
981
|
}
|
|
797
982
|
parseParameter() {
|
|
798
|
-
const name = this.
|
|
983
|
+
const name = this.parseIdentifierName();
|
|
799
984
|
let typeAnnotation;
|
|
800
985
|
if (this.match(TokenType.COLON)) {
|
|
801
|
-
typeAnnotation = this.
|
|
986
|
+
typeAnnotation = this.parseType();
|
|
802
987
|
}
|
|
803
988
|
let defaultValue;
|
|
804
989
|
if (this.match(TokenType.EQUALS)) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as AST from '../parser/AST.js';
|
|
2
|
+
export declare class SemanticAnalyzer {
|
|
3
|
+
private globalScope;
|
|
4
|
+
private currentScope;
|
|
5
|
+
private typeRegistry;
|
|
6
|
+
private typeAliasRegistry;
|
|
7
|
+
private memberTypes;
|
|
8
|
+
private functionRegistry;
|
|
9
|
+
analyze(program: AST.Program): void;
|
|
10
|
+
private enterScope;
|
|
11
|
+
private exitScope;
|
|
12
|
+
private visitStatement;
|
|
13
|
+
private visitChildren;
|
|
14
|
+
private visitVariableDecl;
|
|
15
|
+
private visitAssignmentStmt;
|
|
16
|
+
private getMemberExprKey;
|
|
17
|
+
private visitCompoundAssignment;
|
|
18
|
+
private visitFunctionDecl;
|
|
19
|
+
private visitBlock;
|
|
20
|
+
private visitExpression;
|
|
21
|
+
private inferType;
|
|
22
|
+
private stringToType;
|
|
23
|
+
private visitTypeAliasDecl;
|
|
24
|
+
private visitTypeDecl;
|
|
25
|
+
private validateTypeExists;
|
|
26
|
+
private validateTableAgainstType;
|
|
27
|
+
private parseObjectLiteralType;
|
|
28
|
+
}
|