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.
@@ -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.COMMAND:
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.parseIdentifier();
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(); // command
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
- const expr = this.parseExpression();
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.parseIdentifier();
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.parseIdentifier();
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.parseIdentifier();
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 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);
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
- const regex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
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
- parts.push({ kind: AST.NodeType.Identifier, name: match[1] });
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.parseIdentifier();
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
+ }