@aster-cloud/aster-lang-ts 1.0.31 → 1.0.33

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.
Files changed (127) hide show
  1. package/dist/scripts/corpus-loader.d.ts +10 -4
  2. package/dist/scripts/corpus-loader.d.ts.map +1 -1
  3. package/dist/scripts/corpus-loader.js +24 -9
  4. package/dist/scripts/corpus-loader.js.map +1 -1
  5. package/dist/scripts/lexicon-completeness.d.ts +42 -0
  6. package/dist/scripts/lexicon-completeness.d.ts.map +1 -0
  7. package/dist/scripts/lexicon-completeness.js +57 -0
  8. package/dist/scripts/lexicon-completeness.js.map +1 -0
  9. package/dist/scripts/lsp-client-helper.d.ts +7 -1
  10. package/dist/scripts/lsp-client-helper.d.ts.map +1 -1
  11. package/dist/scripts/lsp-client-helper.js +6 -1
  12. package/dist/scripts/lsp-client-helper.js.map +1 -1
  13. package/dist/scripts/verify-lexicon-sync.js +14 -44
  14. package/dist/scripts/verify-lexicon-sync.js.map +1 -1
  15. package/dist/src/lsp/codeaction.d.ts.map +1 -1
  16. package/dist/src/lsp/codeaction.js +12 -4
  17. package/dist/src/lsp/codeaction.js.map +1 -1
  18. package/dist/src/lsp/diagnostics.d.ts +2 -0
  19. package/dist/src/lsp/diagnostics.d.ts.map +1 -1
  20. package/dist/src/lsp/diagnostics.js +4 -0
  21. package/dist/src/lsp/diagnostics.js.map +1 -1
  22. package/dist/src/lsp/navigation/hover.d.ts.map +1 -1
  23. package/dist/src/lsp/navigation/hover.js +23 -0
  24. package/dist/src/lsp/navigation/hover.js.map +1 -1
  25. package/dist/src/lsp/navigation/references.d.ts +3 -1
  26. package/dist/src/lsp/navigation/references.d.ts.map +1 -1
  27. package/dist/src/lsp/navigation/references.js +16 -6
  28. package/dist/src/lsp/navigation/references.js.map +1 -1
  29. package/dist/src/lsp/navigation/rename.d.ts +1 -1
  30. package/dist/src/lsp/navigation/rename.d.ts.map +1 -1
  31. package/dist/src/lsp/navigation/rename.js +31 -42
  32. package/dist/src/lsp/navigation/rename.js.map +1 -1
  33. package/dist/src/lsp/navigation/shared.d.ts +34 -2
  34. package/dist/src/lsp/navigation/shared.d.ts.map +1 -1
  35. package/dist/src/lsp/navigation/shared.js +88 -10
  36. package/dist/src/lsp/navigation/shared.js.map +1 -1
  37. package/dist/src/lsp/navigation.js +2 -2
  38. package/dist/src/lsp/navigation.js.map +1 -1
  39. package/dist/src/lsp/parse-document.d.ts +11 -0
  40. package/dist/src/lsp/parse-document.d.ts.map +1 -0
  41. package/dist/src/lsp/parse-document.js +51 -0
  42. package/dist/src/lsp/parse-document.js.map +1 -0
  43. package/dist/src/lsp/server.js +18 -23
  44. package/dist/src/lsp/server.js.map +1 -1
  45. package/dist/src/lsp/settings.d.ts +35 -0
  46. package/dist/src/lsp/settings.d.ts.map +1 -0
  47. package/dist/src/lsp/settings.js +31 -0
  48. package/dist/src/lsp/settings.js.map +1 -0
  49. package/dist/src/lsp/source-positions.d.ts +47 -0
  50. package/dist/src/lsp/source-positions.d.ts.map +1 -0
  51. package/dist/src/lsp/source-positions.js +92 -0
  52. package/dist/src/lsp/source-positions.js.map +1 -0
  53. package/dist/src/lsp/workspace/document-indexer.d.ts +3 -1
  54. package/dist/src/lsp/workspace/document-indexer.d.ts.map +1 -1
  55. package/dist/src/lsp/workspace/document-indexer.js +5 -7
  56. package/dist/src/lsp/workspace/document-indexer.js.map +1 -1
  57. package/dist/src/lsp/workspace/symbol-finder.d.ts +8 -1
  58. package/dist/src/lsp/workspace/symbol-finder.d.ts.map +1 -1
  59. package/dist/src/lsp/workspace/symbol-finder.js +10 -13
  60. package/dist/src/lsp/workspace/symbol-finder.js.map +1 -1
  61. package/dist/src/parser/constraint-parser.d.ts.map +1 -1
  62. package/dist/src/parser/constraint-parser.js +18 -7
  63. package/dist/src/parser/constraint-parser.js.map +1 -1
  64. package/dist/src/parser/decl-parser.d.ts.map +1 -1
  65. package/dist/src/parser/decl-parser.js +6 -5
  66. package/dist/src/parser/decl-parser.js.map +1 -1
  67. package/dist/src/parser/expr-stmt-parser.d.ts.map +1 -1
  68. package/dist/src/parser/expr-stmt-parser.js +95 -38
  69. package/dist/src/parser/expr-stmt-parser.js.map +1 -1
  70. package/dist/src/parser/import-parser.d.ts.map +1 -1
  71. package/dist/src/parser/import-parser.js +2 -1
  72. package/dist/src/parser/import-parser.js.map +1 -1
  73. package/dist/src/parser/keyword-display.d.ts +40 -0
  74. package/dist/src/parser/keyword-display.d.ts.map +1 -0
  75. package/dist/src/parser/keyword-display.js +73 -0
  76. package/dist/src/parser/keyword-display.js.map +1 -0
  77. package/dist/src/parser/type-parser.d.ts.map +1 -1
  78. package/dist/src/parser/type-parser.js +5 -4
  79. package/dist/src/parser/type-parser.js.map +1 -1
  80. package/dist/src/parser.d.ts.map +1 -1
  81. package/dist/src/parser.js +1 -0
  82. package/dist/src/parser.js.map +1 -1
  83. package/dist/test/integration/lsp/lsp-rename-safety.test.d.ts +2 -0
  84. package/dist/test/integration/lsp/lsp-rename-safety.test.d.ts.map +1 -0
  85. package/dist/test/integration/lsp/lsp-rename-safety.test.js +114 -0
  86. package/dist/test/integration/lsp/lsp-rename-safety.test.js.map +1 -0
  87. package/dist/test/integration/lsp/lsp-source-positions.test.d.ts +2 -0
  88. package/dist/test/integration/lsp/lsp-source-positions.test.d.ts.map +1 -0
  89. package/dist/test/integration/lsp/lsp-source-positions.test.js +161 -0
  90. package/dist/test/integration/lsp/lsp-source-positions.test.js.map +1 -0
  91. package/dist/test/integration/lsp/lsp-zh-inmemory-navigation.test.d.ts +2 -0
  92. package/dist/test/integration/lsp/lsp-zh-inmemory-navigation.test.d.ts.map +1 -0
  93. package/dist/test/integration/lsp/lsp-zh-inmemory-navigation.test.js +79 -0
  94. package/dist/test/integration/lsp/lsp-zh-inmemory-navigation.test.js.map +1 -0
  95. package/dist/test/unit/eval-regression-poker-straight-high.test.d.ts +2 -0
  96. package/dist/test/unit/eval-regression-poker-straight-high.test.d.ts.map +1 -0
  97. package/dist/test/unit/eval-regression-poker-straight-high.test.js +61 -0
  98. package/dist/test/unit/eval-regression-poker-straight-high.test.js.map +1 -0
  99. package/dist/test/unit/lexicon-completeness.test.d.ts +2 -0
  100. package/dist/test/unit/lexicon-completeness.test.d.ts.map +1 -0
  101. package/dist/test/unit/lexicon-completeness.test.js +42 -0
  102. package/dist/test/unit/lexicon-completeness.test.js.map +1 -0
  103. package/dist/test/unit/lsp/codeaction.test.js +28 -0
  104. package/dist/test/unit/lsp/codeaction.test.js.map +1 -1
  105. package/dist/test/unit/lsp/lsp-locale-fixes.test.d.ts +2 -0
  106. package/dist/test/unit/lsp/lsp-locale-fixes.test.d.ts.map +1 -0
  107. package/dist/test/unit/lsp/lsp-locale-fixes.test.js +105 -0
  108. package/dist/test/unit/lsp/lsp-locale-fixes.test.js.map +1 -0
  109. package/dist/test/unit/lsp/parse-document.test.d.ts +2 -0
  110. package/dist/test/unit/lsp/parse-document.test.d.ts.map +1 -0
  111. package/dist/test/unit/lsp/parse-document.test.js +30 -0
  112. package/dist/test/unit/lsp/parse-document.test.js.map +1 -0
  113. package/dist/test/unit/mapping/redos-budget.test.js +60 -23
  114. package/dist/test/unit/mapping/redos-budget.test.js.map +1 -1
  115. package/dist/test/unit/parser/keyword-display.test.d.ts +8 -0
  116. package/dist/test/unit/parser/keyword-display.test.d.ts.map +1 -0
  117. package/dist/test/unit/parser/keyword-display.test.js +27 -0
  118. package/dist/test/unit/parser/keyword-display.test.js.map +1 -0
  119. package/dist/test/unit/parser/localized-diagnostic-keywords.test.d.ts +15 -0
  120. package/dist/test/unit/parser/localized-diagnostic-keywords.test.d.ts.map +1 -0
  121. package/dist/test/unit/parser/localized-diagnostic-keywords.test.js +98 -0
  122. package/dist/test/unit/parser/localized-diagnostic-keywords.test.js.map +1 -0
  123. package/dist/test/unit/parser/prefix-operator-call.test.js +119 -1
  124. package/dist/test/unit/parser/prefix-operator-call.test.js.map +1 -1
  125. package/dist/test/unit/scripts/corpus-loader.test.js +44 -9
  126. package/dist/test/unit/scripts/corpus-loader.test.js.map +1 -1
  127. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import { kw, kwKind, kwKindq, kwq } from './keyword-display.js';
2
+ import { SemanticTokenKind } from '../config/token-kind.js';
1
3
  import { kwParts, tokLowerAt, tryReadTranslatedIdent } from './context.js';
2
4
  import { TokenKind, KW } from '../frontend/tokens.js';
3
5
  import { Node } from '../ast/ast.js';
@@ -225,11 +227,11 @@ function skipInlineElseLayout(ctx) {
225
227
  function parseInlineIf(ctx, error, cond) {
226
228
  skipInlineThenLayout(ctx);
227
229
  if (!ctx.isKeyword(KW.THEN))
228
- error("Expected 'then' in inline if");
230
+ error(`Expected ${kwq(ctx, KW.THEN)} in inline if`);
229
231
  ctx.nextWord();
230
232
  // then 分支:单个 return(内联,不带句点;句点由整条 inline-if 末尾的 return 承担)。
231
233
  if (!ctx.isKeyword(KW.RETURN))
232
- error("Expected 'return' after 'then'");
234
+ error(`Expected ${kwq(ctx, KW.RETURN)} after ${kwq(ctx, KW.THEN)}`);
233
235
  const thenRetTok = ctx.peek();
234
236
  ctx.nextWord();
235
237
  const thenExpr = parseExpr(ctx, error);
@@ -258,7 +260,7 @@ function parseInlineIf(ctx, error, cond) {
258
260
  else {
259
261
  // else return X.(末尾 return,带句点)。
260
262
  if (!ctx.isKeyword(KW.RETURN))
261
- error("Expected 'return' or 'if' after 'else'");
263
+ error(`Expected ${kwq(ctx, KW.RETURN)} or ${kwq(ctx, KW.IF)} after ${kwKindq(ctx, SemanticTokenKind.OTHERWISE, 'else')}`);
262
264
  const elseRetTok = ctx.peek();
263
265
  ctx.nextWord();
264
266
  const elseExpr = parseExpr(ctx, error);
@@ -359,7 +361,7 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
359
361
  ctx.nextWord();
360
362
  const nameTok = ctx.peek();
361
363
  const name = parseIdent(ctx, error);
362
- expectKeyword(ctx, error, KW.BE, "Use 'be' in bindings: 'Let x be ...'.");
364
+ expectKeyword(ctx, error, KW.BE, `Use ${kwq(ctx, KW.BE)} in bindings: '${kw(ctx, KW.LET, 'Let')} x ${kw(ctx, KW.BE)} ...'.`);
363
365
  // Special-case lambda block form to avoid trailing '.'
364
366
  if ((ctx.isKeyword('a') && tokLowerAt(ctx, ctx.index + 1) === 'function') || ctx.isKeyword('function')) {
365
367
  if (ctx.isKeyword('a'))
@@ -367,7 +369,7 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
367
369
  const functionTok = ctx.nextWord(); // 'function'
368
370
  const params = parseParamList(ctx, error);
369
371
  expectCommaOr(ctx);
370
- expectKeyword(ctx, error, KW.PRODUCE, "Expected 'produce' and return type");
372
+ expectKeyword(ctx, error, KW.PRODUCE, `Expected ${kwq(ctx, KW.PRODUCE)} and return type`);
371
373
  let retType;
372
374
  if (ctx.at(TokenKind.COLON)) {
373
375
  retType = Node.TypeName('Unknown');
@@ -403,7 +405,7 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
403
405
  const setTok = ctx.peek();
404
406
  ctx.nextWord();
405
407
  const name = parseIdent(ctx, error);
406
- expectKeyword(ctx, error, KW.TO_WORD, "Use 'to' in assignments: 'Set x to ...'.");
408
+ expectKeyword(ctx, error, KW.TO_WORD, `Use ${kwq(ctx, KW.TO_WORD)} in assignments: '${kw(ctx, KW.SET, 'Set')} x ${kw(ctx, KW.TO_WORD)} ...'.`);
407
409
  const expr = parseExpr(ctx, error);
408
410
  expectPeriodEnd(ctx, error);
409
411
  const nd = Node.Set(name, expr);
@@ -521,9 +523,9 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
521
523
  }
522
524
  if (ctx.isKeyword(KW.WITHIN)) {
523
525
  ctx.nextWord();
524
- expectKeyword(ctx, error, KW.SCOPE, "Expected 'scope' after 'Within'");
526
+ expectKeyword(ctx, error, KW.SCOPE, `Expected ${kwq(ctx, KW.SCOPE)} after ${kwq(ctx, KW.WITHIN, 'Within')}`);
525
527
  if (!ctx.at(TokenKind.COLON))
526
- error("Expected ':' after 'scope'");
528
+ error(`Expected ':' after ${kwq(ctx, KW.SCOPE)}`);
527
529
  ctx.next();
528
530
  expectNewline(ctx, error);
529
531
  return parseBlock(ctx, error); // Lowering later
@@ -531,8 +533,8 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
531
533
  if (ctx.isKeyword(KW.START)) {
532
534
  const startTok = ctx.nextWord();
533
535
  const name = parseIdent(ctx, error);
534
- expectKeyword(ctx, error, KW.AS, "Expected 'as' after name");
535
- expectKeyword(ctx, error, KW.ASYNC, "Expected 'async'");
536
+ expectKeyword(ctx, error, KW.AS, `Expected ${kwq(ctx, KW.AS)} after name`);
537
+ expectKeyword(ctx, error, KW.ASYNC, `Expected ${kwq(ctx, KW.ASYNC)}`);
536
538
  const expr = parseExpr(ctx, error);
537
539
  expectPeriodEnd(ctx, error);
538
540
  const nd = Node.Start(name, expr);
@@ -565,9 +567,9 @@ export function parseStatement(ctx, error, reportOutcomeError = error) {
565
567
  }
566
568
  function parseWorkflow(ctx, error) {
567
569
  const workflowTok = ctx.peek();
568
- expectKeyword(ctx, error, KW.WORKFLOW, "Expected 'workflow'");
570
+ expectKeyword(ctx, error, KW.WORKFLOW, `Expected ${kwq(ctx, KW.WORKFLOW)}`);
569
571
  if (!ctx.at(TokenKind.COLON))
570
- error("Expected ':' after 'workflow'");
572
+ error(`Expected ':' after ${kwq(ctx, KW.WORKFLOW)}`);
571
573
  ctx.next();
572
574
  expectNewline(ctx, error);
573
575
  if (!ctx.at(TokenKind.INDENT))
@@ -603,14 +605,14 @@ function parseWorkflow(ctx, error) {
603
605
  }
604
606
  function parseStep(ctx, error) {
605
607
  const stepTok = ctx.peek();
606
- expectKeyword(ctx, error, KW.STEP, "Expected 'step'");
608
+ expectKeyword(ctx, error, KW.STEP, `Expected ${kwq(ctx, KW.STEP)}`);
607
609
  const name = parseIdent(ctx, error);
608
610
  const dependencies = [];
609
611
  if (ctx.isKeyword(KW.DEPENDS)) {
610
612
  ctx.nextWord();
611
- expectKeyword(ctx, error, KW.ON, "Expected 'on' after 'depends'");
613
+ expectKeyword(ctx, error, KW.ON, `Expected ${kwq(ctx, KW.ON)} after ${kwq(ctx, KW.DEPENDS)}`);
612
614
  if (!ctx.at(TokenKind.LBRACKET))
613
- error("Expected '[' after 'depends on'");
615
+ error(`Expected '[' after '${kw(ctx, KW.DEPENDS)} ${kw(ctx, KW.ON)}'`);
614
616
  ctx.next();
615
617
  if (!ctx.at(TokenKind.RBRACKET)) {
616
618
  while (true) {
@@ -642,7 +644,7 @@ function parseStep(ctx, error) {
642
644
  if (ctx.isKeyword(KW.COMPENSATE)) {
643
645
  ctx.nextWord();
644
646
  if (!ctx.at(TokenKind.COLON))
645
- error("Expected ':' after 'compensate'");
647
+ error(`Expected ':' after ${kwq(ctx, KW.COMPENSATE)}`);
646
648
  ctx.next();
647
649
  expectNewline(ctx, error);
648
650
  compensate = parseBlock(ctx, error);
@@ -654,9 +656,9 @@ function parseStep(ctx, error) {
654
656
  return step;
655
657
  }
656
658
  function parseRetryPolicy(ctx, error) {
657
- expectKeyword(ctx, error, KW.RETRY, "Expected 'retry'");
659
+ expectKeyword(ctx, error, KW.RETRY, `Expected ${kwq(ctx, KW.RETRY)}`);
658
660
  if (!ctx.at(TokenKind.COLON))
659
- error("Expected ':' after 'retry'");
661
+ error(`Expected ':' after ${kwq(ctx, KW.RETRY)}`);
660
662
  ctx.next();
661
663
  expectNewline(ctx, error);
662
664
  if (!ctx.at(TokenKind.INDENT))
@@ -669,20 +671,20 @@ function parseRetryPolicy(ctx, error) {
669
671
  if (ctx.isKeywordSeq(MAX_ATTEMPTS_PARTS)) {
670
672
  ctx.nextWords(MAX_ATTEMPTS_PARTS);
671
673
  if (!ctx.at(TokenKind.COLON))
672
- error("Expected ':' after 'max attempts'");
674
+ error(`Expected ':' after ${kwq(ctx, KW.MAX_ATTEMPTS)}`);
673
675
  ctx.next();
674
676
  if (!ctx.at(TokenKind.INT))
675
- error("Expected integer after 'max attempts'");
677
+ error(`Expected integer after ${kwq(ctx, KW.MAX_ATTEMPTS)}`);
676
678
  const attempts = ctx.next().value;
677
679
  if (attempts <= 0)
678
- error("'max attempts' must be greater than zero");
680
+ error(`${kwq(ctx, KW.MAX_ATTEMPTS)} must be greater than zero`);
679
681
  maxAttempts = attempts;
680
682
  expectPeriodEnd(ctx, error);
681
683
  }
682
684
  else if (ctx.isKeyword(KW.BACKOFF)) {
683
685
  ctx.nextWord();
684
686
  if (!ctx.at(TokenKind.COLON))
685
- error("Expected ':' after 'backoff'");
687
+ error(`Expected ':' after ${kwq(ctx, KW.BACKOFF)}`);
686
688
  ctx.next();
687
689
  const modeTok = ctx.nextWord();
688
690
  const mode = String(modeTok.value ?? '').toLowerCase();
@@ -699,15 +701,15 @@ function parseRetryPolicy(ctx, error) {
699
701
  }
700
702
  ctx.next();
701
703
  if (maxAttempts === null)
702
- error("Retry section must include 'max attempts'");
704
+ error(`Retry section must include ${kwq(ctx, KW.MAX_ATTEMPTS)}`);
703
705
  if (backoff === null)
704
- error("Retry section must include 'backoff'");
706
+ error(`Retry section must include ${kwq(ctx, KW.BACKOFF)}`);
705
707
  return { maxAttempts, backoff };
706
708
  }
707
709
  function parseTimeout(ctx, error) {
708
- expectKeyword(ctx, error, KW.TIMEOUT, "Expected 'timeout'");
710
+ expectKeyword(ctx, error, KW.TIMEOUT, `Expected ${kwq(ctx, KW.TIMEOUT)}`);
709
711
  if (!ctx.at(TokenKind.COLON))
710
- error("Expected ':' after 'timeout'");
712
+ error(`Expected ':' after ${kwq(ctx, KW.TIMEOUT)}`);
711
713
  ctx.next();
712
714
  if (!ctx.at(TokenKind.INT))
713
715
  error('Expected integer timeout value');
@@ -716,6 +718,8 @@ function parseTimeout(ctx, error) {
716
718
  if (seconds < 0)
717
719
  error('Timeout value must be non-negative');
718
720
  if (!(ctx.isKeyword('seconds') || ctx.isKeyword('second'))) {
721
+ // 'seconds' 不做本地化:所有 locale 的词表都没有时间单位的本地拼写,
722
+ // zh/de 作者同样只能写英文 seconds,报错里的 'seconds' 正是作者该写的词。
719
723
  error("Timeout must specify time unit 'seconds'");
720
724
  }
721
725
  ctx.nextWord();
@@ -739,7 +743,7 @@ function parseCases(ctx, error) {
739
743
  while (!ctx.at(TokenKind.DEDENT)) {
740
744
  if (!ctx.isKeyword(KW.WHEN)) {
741
745
  // 改进的错误消息,提示复合模式关系
742
- error("Expected 'When' (若...为 pattern requires '为' after '若')");
746
+ error(`Expected ${kwq(ctx, KW.WHEN, 'When')} (若...为 pattern requires '为' after '若')`);
743
747
  }
744
748
  const whenTok = ctx.nextWord();
745
749
  const pat = parsePattern(ctx, error);
@@ -816,12 +820,13 @@ function parseIfExpr(ctx, error) {
816
820
  const cond = parseOr(ctx, error);
817
821
  skipInlineThenLayout(ctx);
818
822
  if (!ctx.isKeyword(KW.THEN))
819
- error("Expected 'then' in if-expression");
823
+ error(`Expected ${kwq(ctx, KW.THEN)} in if-expression`);
820
824
  ctx.nextWord(); // 消费 THEN
821
825
  const thenE = parseExpr(ctx, error);
822
826
  skipInlineElseLayout(ctx);
823
827
  if (!ctx.isKeyword(KW.ELSE) && !ctx.isKeyword(KW.OTHERWISE)) {
824
- error("Expected 'else' in if-expression (else is mandatory for expression-level if)");
828
+ error(`Expected ${kwKindq(ctx, SemanticTokenKind.OTHERWISE, 'else')} in if-expression ` +
829
+ `(${kwKind(ctx, SemanticTokenKind.OTHERWISE, 'else')} is mandatory for expression-level if)`);
825
830
  }
826
831
  ctx.nextWord(); // 消费 ELSE / OTHERWISE
827
832
  const elseE = parseExpr(ctx, error);
@@ -1293,7 +1298,7 @@ function parseApplyOrPrimary(ctx, error) {
1293
1298
  if (ctx.isKeyword(KW.APPLY) && applyLooksLikeCall(ctx)) {
1294
1299
  const applyTok = ctx.nextWord();
1295
1300
  const fn = parseCallTargetName(ctx, error);
1296
- expectKeyword(ctx, error, KW.TO_WORD, "Use 'apply <fn> to <arg>' (expected 'to').");
1301
+ expectKeyword(ctx, error, KW.TO_WORD, `Use '${kw(ctx, KW.APPLY)} <fn> ${kw(ctx, KW.TO_WORD)} <arg>' (expected ${kwq(ctx, KW.TO_WORD)}).`);
1297
1302
  const arg = parseExpr(ctx, error);
1298
1303
  const call = Node.Call(fn, [arg]);
1299
1304
  assignSpan(call, spanFromSources(applyTok, arg));
@@ -1332,7 +1337,7 @@ function applyLooksLikeCall(ctx) {
1332
1337
  function parseCallTargetName(ctx, error) {
1333
1338
  const startTok = ctx.peek();
1334
1339
  if (!ctx.at(TokenKind.IDENT) && !ctx.at(TokenKind.TYPE_IDENT)) {
1335
- error("Expected a function name after 'apply'.");
1340
+ error(`Expected a function name after ${kwq(ctx, KW.APPLY)}.`);
1336
1341
  }
1337
1342
  const consumed = [ctx.peek()];
1338
1343
  let full = ctx.at(TokenKind.IDENT) ? parseIdent(ctx, error) : ctx.next().value;
@@ -1361,9 +1366,56 @@ const PREFIX_OPERATORS = new Map([
1361
1366
  [TokenKind.STAR, '*'],
1362
1367
  [TokenKind.SLASH, '/'],
1363
1368
  ]);
1364
- /** 前缀运算符调用:恰好 2 个实参,降为与中缀相同的 Call(Name(op), [a, b])。 */
1365
- function parsePrefixOperatorCall(ctx, error, op) {
1369
+ // 运算符词 → 非英语词法表下 Java Canonicalizer(OPERATOR_SYMBOL_MAP)把它翻成的符号;
1370
+ // null 表示不翻译,词仍是 Java lexer 的硬 token,不能作前缀调用目标。
1371
+ const OPERATOR_WORD_SYMBOLS = new Map([
1372
+ [KW.AND, null],
1373
+ [KW.OR, null],
1374
+ [KW.MODULO, null],
1375
+ [KW.INTEGER_DIVIDED_BY, null],
1376
+ [KW.PLUS, '+'],
1377
+ [KW.MINUS, '-'],
1378
+ [KW.TIMES, '*'],
1379
+ [KW.DIVIDED_BY, '/'],
1380
+ [KW.LESS_THAN, '<'],
1381
+ [KW.GREATER_THAN, '>'],
1382
+ [KW.MORE_THAN, '>'],
1383
+ [KW.EQUALS_TO, '=='],
1384
+ [KW.AT_LEAST, '>='],
1385
+ [KW.AT_MOST, '<='],
1386
+ [KW.UNDER, '<'],
1387
+ [KW.OVER, '>'],
1388
+ ]);
1389
+ // en-US 下 under/over 是软关键词(Java 当普通 IDENT),前缀位置就是普通函数调用
1390
+ const EN_SOFT_OPERATOR_WORDS = new Set([KW.UNDER, KW.OVER]);
1391
+ const PREFIX_SYMBOL_LIST = '< > <= >= != == = + - * /';
1392
+ /** 当前位置是否为「运算符词 + (」;按原文大小写精确匹配(Java lexer 区分大小写)。 */
1393
+ function operatorWordBeforeParen(ctx, english) {
1394
+ for (const word of OPERATOR_WORD_SYMBOLS.keys()) {
1395
+ const parts = kwParts(word);
1396
+ const matches = parts.every((part, k) => ctx.peek(k).kind === TokenKind.IDENT && ctx.peek(k).value === part);
1397
+ if (matches && ctx.peek(parts.length).kind === TokenKind.LPAREN) {
1398
+ return english && EN_SOFT_OPERATOR_WORDS.has(word) ? undefined : word;
1399
+ }
1400
+ }
1401
+ return undefined;
1402
+ }
1403
+ /**
1404
+ * 运算符词前缀调用,与 Java 一致:en-US 规范拼写不翻译,词是硬 token → 拒绝;
1405
+ * 非英语词法表下可映射为符号的词按符号前缀调用处理,其余拒绝。
1406
+ */
1407
+ function parseOperatorWordCall(ctx, error, word, english) {
1408
+ const symbol = english ? null : OPERATOR_WORD_SYMBOLS.get(word);
1409
+ if (!symbol) {
1410
+ error(`运算符词 ${kwq(ctx, word)} 不能用作前缀调用;只有符号运算符 ${PREFIX_SYMBOL_LIST} 可以写成 op(a, b)`);
1411
+ }
1412
+ return parsePrefixOperatorCall(ctx, error, symbol, kwParts(word).length);
1413
+ }
1414
+ /** 前缀运算符调用:恰好 2 个实参,降为与中缀相同的 Call(Name(op), [a, b])。运算符可占多个 token。 */
1415
+ function parsePrefixOperatorCall(ctx, error, op, opTokenCount = 1) {
1366
1416
  const opTok = ctx.next();
1417
+ for (let k = 1; k < opTokenCount; k++)
1418
+ ctx.next();
1367
1419
  const args = parseArgList(ctx, error);
1368
1420
  if (args.length !== 2)
1369
1421
  error(`前缀操作符调用需要 2 个参数,但实际为 ${args.length}`);
@@ -1375,6 +1427,11 @@ function parsePrimary(ctx, error) {
1375
1427
  if (prefixOp !== undefined && ctx.peek(1).kind === TokenKind.LPAREN) {
1376
1428
  return parsePrefixOperatorCall(ctx, error, prefixOp);
1377
1429
  }
1430
+ const english = ctx.lexicon === undefined || ctx.lexicon.id === 'en-US';
1431
+ const operatorWord = operatorWordBeforeParen(ctx, english);
1432
+ if (operatorWord !== undefined) {
1433
+ return parseOperatorWordCall(ctx, error, operatorWord, english);
1434
+ }
1378
1435
  // Minimal: construction, literals, names, Ok/Err/Some/None, call with dotted names and parens args
1379
1436
  // Lambda (block form): 'a function' (or 'function') ... 'produce' Type ':' \n Block
1380
1437
  if ((ctx.isKeyword('a') && tokLowerAt(ctx, ctx.index + 1) === 'function') || ctx.isKeyword('function')) {
@@ -1382,7 +1439,7 @@ function parsePrimary(ctx, error) {
1382
1439
  const functionTok = ctx.nextWord(); // 'function'
1383
1440
  const params = parseParamList(ctx, error);
1384
1441
  expectCommaOr(ctx);
1385
- expectKeyword(ctx, error, KW.PRODUCE, "Expected 'produce' and return type");
1442
+ expectKeyword(ctx, error, KW.PRODUCE, `Expected ${kwq(ctx, KW.PRODUCE)} and return type`);
1386
1443
  let retType;
1387
1444
  if (ctx.at(TokenKind.COLON)) {
1388
1445
  retType = Node.TypeName('Unknown');
@@ -1606,11 +1663,11 @@ function parsePrimary(ctx, error) {
1606
1663
  const nameTok = ctx.peek();
1607
1664
  const name = parseIdent(ctx, error);
1608
1665
  if (!ctx.isKeyword(KW.SET))
1609
- error("Expected 'set to' in construction");
1666
+ error(`Expected '${kw(ctx, KW.SET)} ${kw(ctx, KW.TO_WORD)}' in construction`);
1610
1667
  const equalsTok = ctx.peek();
1611
1668
  ctx.nextWord();
1612
1669
  if (!ctx.isKeyword(KW.TO_WORD))
1613
- error("Expected 'to' after 'set' in construction");
1670
+ error(`Expected ${kwq(ctx, KW.TO_WORD)} after ${kwq(ctx, KW.SET)} in construction`);
1614
1671
  ctx.nextWord();
1615
1672
  const e = parseExpr(ctx, error);
1616
1673
  const fld = {
@@ -1666,7 +1723,7 @@ function parsePrimary(ctx, error) {
1666
1723
  // 仅拦截裸类型名(无点链后缀),点链形态(如 SomeType.method(...))不受影响。
1667
1724
  if (full === typeName && ctx.declaredRecordTypes.has(typeName)) {
1668
1725
  error(`Cannot construct '${typeName}' with positional arguments. ` +
1669
- `Use named fields instead: '${typeName} with <field> set to <value> and ...'`);
1726
+ `Use named fields instead: '${typeName} ${kw(ctx, KW.WITH)} <field> ${kw(ctx, KW.SET)} ${kw(ctx, KW.TO_WORD)} <value> ${kw(ctx, KW.AND)} ...'`);
1670
1727
  }
1671
1728
  const args = parseArgList(ctx, error);
1672
1729
  const call = Node.Call(target, args);
@@ -1704,7 +1761,7 @@ function parsePrimary(ctx, error) {
1704
1761
  // 同 TYPE_IDENT 分支:拦截对已声明类型的位置式构造(无大小写语言里类型名是 IDENT)。
1705
1762
  if (full === name && ctx.declaredRecordTypes.has(name)) {
1706
1763
  error(`Cannot construct '${name}' with positional arguments. ` +
1707
- `Use named fields instead: '${name} with <field> set to <value> and ...'`);
1764
+ `Use named fields instead: '${name} ${kw(ctx, KW.WITH)} <field> ${kw(ctx, KW.SET)} ${kw(ctx, KW.TO_WORD)} <value> ${kw(ctx, KW.AND)} ...'`);
1708
1765
  }
1709
1766
  const args = parseArgList(ctx, error);
1710
1767
  const call = Node.Call(target, args);