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.
@@ -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.COMMAND:
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.parseIdentifier();
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.eat().value;
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.eat().value;
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.eat().value;
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.eat().value;
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(); // command
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
- const expr = this.parseExpression();
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.parseIdentifier();
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.parseIdentifier();
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.parseIdentifier();
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 only if $ is followed by a valid identifier start (letter or _)
673
- // This prevents SQL JSON paths like "$.bank" from being treated as interpolation
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
- const regex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
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
- parts.push({ kind: AST.NodeType.Identifier, name: match[1] });
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.parseIdentifier();
983
+ const name = this.parseIdentifierName();
799
984
  let typeAnnotation;
800
985
  if (this.match(TokenType.COLON)) {
801
- typeAnnotation = this.eat().value;
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
+ }