@velarscript/compiler 0.11.0 → 0.12.0

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/dist/parser.js CHANGED
@@ -162,9 +162,7 @@ export class Parser {
162
162
  return expression;
163
163
  const operatorToken = this.advance();
164
164
  this.diagnostics.push(recoveredDiagnostic("VEL2028", "Assignment is a statement, not an expression; write it on its own line inside a function, action, or handler body", operatorToken.span));
165
- if (expression.kind !== "IdentifierExpression" && expression.kind !== "MemberExpression" && expression.kind !== "IndexExpression") {
166
- this.diagnostics.push(diagnostic("VEL2005", "Assignment target must be a name, member, or index", expression.span));
167
- }
165
+ this.reportInvalidAssignmentTarget(expression);
168
166
  const value = this.recoverExpressionAssignment(this.parseExpression());
169
167
  return { kind: "AssignmentExpression", target: expression, operator, value, span: span(expression.span.start, value.span.end) };
170
168
  }
@@ -445,9 +443,7 @@ export class Parser {
445
443
  const operator = assignmentOperators[this.current().kind];
446
444
  if (operator) {
447
445
  this.advance();
448
- if (expression.kind !== "IdentifierExpression" && expression.kind !== "MemberExpression" && expression.kind !== "IndexExpression") {
449
- this.diagnostics.push(diagnostic("VEL2005", "Assignment target must be a name, member, or index", expression.span));
450
- }
446
+ this.reportInvalidAssignmentTarget(expression);
451
447
  const value = this.parseExpression();
452
448
  return {
453
449
  kind: "AssignmentStatement",
@@ -1512,18 +1508,16 @@ export class Parser {
1512
1508
  this.consumeNewlines();
1513
1509
  while (!this.check("dedent") && !this.check("eof")) {
1514
1510
  const methodStart = this.current().span.start;
1515
- // D43 item 67/69 + D68 rule 177: an `@name` member belongs to the
1516
- // language. `@dispose:` and `@iterate:` are the two a class declares;
1517
- // anything else gets the closed vocabulary named back. Both are parsed
1518
- // and validated on one path because they are one idea — a question the
1519
- // language asks the type, answered in a block that is not a method.
1511
+ // `@` always selects the contextual compiler namespace. In a class that
1512
+ // closed namespace contains `dispose` and `iterate`; their behavior
1513
+ // differs, but their resolution, rejection, and collision rules do not.
1520
1514
  if (this.check("at")) {
1521
1515
  const marker = this.advance();
1522
- const memberName = this.expect("identifier", "Expected a compiler-known class member name after '@'");
1516
+ const memberName = this.expect("identifier", "Expected a compiler-owned class name after '@'");
1523
1517
  const keywordSpan = span(marker.span.start, memberName.span.end);
1524
1518
  const known = memberName.value === "dispose" || memberName.value === "iterate";
1525
1519
  if (!known) {
1526
- this.diagnostics.push(diagnostic("VEL2022", `Unknown language member '@${memberName.value}'; a class declares '@dispose:' and '@iterate:', and no other '@' member`, keywordSpan));
1520
+ this.diagnostics.push(diagnostic("VEL2022", `Unknown compiler-owned name '@${memberName.value}' in a class; the class namespace contains only '@dispose:' and '@iterate:'`, keywordSpan));
1527
1521
  }
1528
1522
  const body = this.parseBlock();
1529
1523
  const blockSpan = span(methodStart, body.at(-1)?.span.end ?? this.previous().span.end);
@@ -2590,6 +2584,17 @@ export class Parser {
2590
2584
  this.advance();
2591
2585
  return this.withParseDepth(() => this.parseUnary());
2592
2586
  }
2587
+ if (this.match("bang")) {
2588
+ // D86 rule 212: a `!` reached here stands before its operand, so it is
2589
+ // the JavaScript negation, not the required-value unwrap the postfix
2590
+ // loop reads. D54 rule 118 keeps that reading a teaching diagnostic.
2591
+ this.reportPrefixBang(this.previous());
2592
+ const operator = this.previous();
2593
+ return this.withParseDepth(() => {
2594
+ const operand = this.parseUnary();
2595
+ return { kind: "UnaryExpression", operator: "not", operand, span: span(operator.span.start, operand.span.end) };
2596
+ });
2597
+ }
2593
2598
  if (this.match("not") || this.match("plus") || this.match("minus") || this.match("tilde")) {
2594
2599
  const operator = this.previous();
2595
2600
  return this.withParseDepth(() => {
@@ -2635,7 +2640,7 @@ export class Parser {
2635
2640
  return this.parsePostfix();
2636
2641
  const operator = this.previous();
2637
2642
  return this.withParseDepth(() => {
2638
- const operand = this.check("not") || this.check("plus") || this.check("minus") || this.check("tilde")
2643
+ const operand = this.check("not") || this.check("bang") || this.check("plus") || this.check("minus") || this.check("tilde")
2639
2644
  ? this.parseUnary()
2640
2645
  : this.parsePowerBase();
2641
2646
  return {
@@ -2648,6 +2653,7 @@ export class Parser {
2648
2653
  }
2649
2654
  parsePostfix() {
2650
2655
  let expression = this.parsePrimary();
2656
+ let typeArgumentsRemoved = false;
2651
2657
  while (true) {
2652
2658
  const explicitTypeArgumentsEnd = this.explicitTypeArgumentsEnd(expression);
2653
2659
  if (explicitTypeArgumentsEnd !== null) {
@@ -2657,7 +2663,26 @@ export class Parser {
2657
2663
  const name = expression.kind === "IdentifierExpression" ? expression.name
2658
2664
  : expression.kind === "MemberExpression" ? expression.property
2659
2665
  : "function";
2660
- this.diagnostics.push(recoveredDiagnostic("VEL2031", `Type arguments are inferred at each call site; write '${name}(...)' without '<...>'`, span(start, this.previous().span.end), mechanicalFix(span(start, this.previous().span.end), "", "Remove the explicit type arguments")));
2666
+ // D85 rule 207: an empty `Set<string>()` has no argument to infer from,
2667
+ // so "remove the type arguments" alone would leave the author with
2668
+ // code the analyzer rejects. Name where the type belongs instead, and
2669
+ // withhold the mechanical fix that would not reach working source.
2670
+ const emptyCollection = (name === "Set" || name === "Map")
2671
+ && this.check("leftParen") && this.peekKind(1) === "rightParen";
2672
+ this.diagnostics.push(recoveredDiagnostic("VEL2031", emptyCollection
2673
+ ? `Type arguments are inferred at each call site; an empty '${name}()' takes its type from the binding — write 'const values: ${name === "Set" ? "Set<string>" : "Map<string, number>"} = ${name}()'`
2674
+ : `Type arguments are inferred at each call site; write '${name}(...)' without '<...>'`, span(start, this.previous().span.end), ...(emptyCollection
2675
+ ? []
2676
+ : [mechanicalFix(span(start, this.previous().span.end), "", "Remove the explicit type arguments")])));
2677
+ typeArgumentsRemoved = true;
2678
+ continue;
2679
+ }
2680
+ // D86 rule 212: a `!` that follows an operand is the required-value
2681
+ // unwrap. It binds with the rest of the postfix chain, so `a!.b` unwraps
2682
+ // `a` and then reads `b`, and `a.b!` unwraps the field.
2683
+ if (this.match("bang")) {
2684
+ const bang = this.previous();
2685
+ expression = { kind: "RequiredExpression", value: expression, span: span(expression.span.start, bang.span.end) };
2661
2686
  continue;
2662
2687
  }
2663
2688
  let call = false;
@@ -2717,8 +2742,10 @@ export class Parser {
2717
2742
  arguments: arguments_,
2718
2743
  ...(sawNamed ? { argumentNames } : {}),
2719
2744
  optional: optionalCall,
2745
+ ...(typeArgumentsRemoved ? { typeArgumentsRemoved: true } : {}),
2720
2746
  span: span(expression.span.start, close.span.end),
2721
2747
  };
2748
+ typeArgumentsRemoved = false;
2722
2749
  continue;
2723
2750
  }
2724
2751
  if (this.check("optionalDot") && this.peekKind(1) === "leftBracket") {
@@ -3031,13 +3058,12 @@ export class Parser {
3031
3058
  }
3032
3059
  return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
3033
3060
  }
3034
- // D43 item 67: '@name' is the language's own namespace for members that
3035
- // sit where user names also sit. Outside a declaration body it is not an
3036
- // expression, so the reader is told what the marker means instead of
3037
- // receiving a bare 'Expected an expression'.
3061
+ // `@name` is resolved only by a compiler-owned syntax context. Here it
3062
+ // cannot become an expression or runtime value, so name the namespace
3063
+ // rule instead of reporting a bare 'Expected an expression'.
3038
3064
  if (token.kind === "at") {
3039
3065
  const name = this.check("identifier") ? this.advance().value : "";
3040
- this.diagnostics.push(diagnostic("VEL2002", `'@${name}' names a language-owned member and appears only inside a declaration body, such as a component's '@mounted:' block`, span(token.span.start, this.previous().span.end)));
3066
+ this.diagnostics.push(diagnostic("VEL2002", `'@${name}' is a compiler-owned contextual name and is not valid here; use it only in a context that defines it, such as a component's '@mounted:' block`, span(token.span.start, this.previous().span.end)));
3041
3067
  this.skipMistypedDeclaration();
3042
3068
  return { kind: "LiteralExpression", value: null, raw: "null", span: token.span };
3043
3069
  }
@@ -3593,6 +3619,33 @@ export class Parser {
3593
3619
  }
3594
3620
  return token;
3595
3621
  }
3622
+ /**
3623
+ * D54 rule 118 keeps prefix `!` a teaching diagnostic rather than a second
3624
+ * spelling of `not`; D86 rule 212 moved the report here because only the
3625
+ * parser knows the `!` stood before its operand. The rewrite carries the
3626
+ * spacing the word form needs, exactly as the lexer's own word-operator
3627
+ * fixes do: `!ready` becomes `not ready`, `and!ready` becomes `and not ready`.
3628
+ */
3629
+ /**
3630
+ * D86 rule 212: `value! = next` names the unwrap on the left of a write. The
3631
+ * unwrap reads a value and proves it present; a write has neither a result
3632
+ * to unwrap nor a fact to prove, and assigning `null` back into an optional
3633
+ * is legitimate — so the target is the location itself.
3634
+ */
3635
+ reportInvalidAssignmentTarget(expression) {
3636
+ if (expression.kind === "IdentifierExpression" || expression.kind === "MemberExpression" || expression.kind === "IndexExpression")
3637
+ return;
3638
+ if (expression.kind === "RequiredExpression") {
3639
+ this.diagnostics.push(diagnostic("VEL2005", "'!' unwraps a value that is read, so it cannot stand on an assignment target; assign to the location itself", expression.span, mechanicalFix(span(expression.span.end - 1, expression.span.end), "", "Remove the '!'")));
3640
+ return;
3641
+ }
3642
+ this.diagnostics.push(diagnostic("VEL2005", "Assignment target must be a name, member, or index", expression.span));
3643
+ }
3644
+ reportPrefixBang(bang) {
3645
+ const spaceBefore = this.tokens[this.index - 2]?.span.end === bang.span.start ? " " : "";
3646
+ const spaceAfter = this.current().span.start === bang.span.end ? " " : "";
3647
+ this.diagnostics.push(recoveredDiagnostic("VEL1005", "Use 'not'; VelarScript uses readable logical operators", bang.span, mechanicalFix(bang.span, `${spaceBefore}not${spaceAfter}`, "Use readable 'not'")));
3648
+ }
3596
3649
  current() {
3597
3650
  return this.tokens[this.index] ?? this.tokens[this.tokens.length - 1];
3598
3651
  }