eyeprolog 1.2.25 → 1.2.26

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.25",
6
+ "version": "1.2.26",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -50,9 +50,11 @@
50
50
  "test:eyeprolog": "node test/run-all.mjs",
51
51
  "test:conformance": "node test/run-conformance.mjs",
52
52
  "test:iso-strict": "node test/run-iso-strict.mjs",
53
+ "test:wg17-syntax": "node test/run-wg17-syntax.mjs",
53
54
  "test:examples": "node test/run-examples.mjs",
54
55
  "test:regression": "node test/run-regression.mjs",
55
56
  "test:playground": "node test/run-playground.mjs",
57
+ "report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
56
58
  "preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
57
59
  "postversion": "git push origin HEAD --follow-tags"
58
60
  }
package/src/iso.js CHANGED
@@ -1072,6 +1072,11 @@ function isTerminatingFullStop(source, index) {
1072
1072
  return true;
1073
1073
  }
1074
1074
 
1075
+ const termGraphicCharacters = new Set('#$&*+-./<=>@^~\\:');
1076
+ function continuesGraphicToken(source, index) {
1077
+ return index > 0 && termGraphicCharacters.has(source[index - 1]);
1078
+ }
1079
+
1075
1080
  function* termTextCandidates(stream) {
1076
1081
  const source = String(stream.content);
1077
1082
  let quote = null, lineComment = false, blockComment = false;
@@ -1095,13 +1100,24 @@ function* termTextCandidates(stream) {
1095
1100
  const characterCodeEnd = characterCodeConstantEnd(source, i);
1096
1101
  if (characterCodeEnd != null) { i = characterCodeEnd; continue; }
1097
1102
  if (ch === '%') { lineComment = true; continue; }
1098
- if (ch === '/' && next === '*') { blockComment = true; i++; continue; }
1103
+ // Comment openers are recognized between tokens. Within a maximal
1104
+ // graphic token, as in `//*`, the slash and star remain atom characters.
1105
+ if (ch === '/' && next === '*' && !continuesGraphicToken(source, i)) {
1106
+ blockComment = true;
1107
+ i++;
1108
+ continue;
1109
+ }
1099
1110
  if (ch === "'" || ch === '"') { quote = ch; continue; }
1100
1111
  if (ch === '.' && isTerminatingFullStop(source, i)) {
1101
1112
  yield { text: source.slice(stream.position, i + 1), end: i + 1 };
1102
1113
  }
1103
1114
  }
1104
1115
  }
1116
+ function hasNonLayoutRemainder(source, start) {
1117
+ return source.slice(start)
1118
+ .replace(/[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g, '')
1119
+ .length > 0;
1120
+ }
1105
1121
  function convertedTermText(text, solver) {
1106
1122
  if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
1107
1123
  let result = '', quote = null;
@@ -1143,6 +1159,7 @@ function readTermFromStream(stream, solver) {
1143
1159
  const clauses = parseClauses(convertedTermText(candidate.text, solver), {
1144
1160
  sourceMetadata: false,
1145
1161
  operatorState,
1162
+ isoStrict: solver.isoStrict,
1146
1163
  doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
1147
1164
  });
1148
1165
  if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
@@ -1170,8 +1187,13 @@ function readTermFromStream(stream, solver) {
1170
1187
  }
1171
1188
  }
1172
1189
 
1173
- stream.position = String(stream.content).length;
1174
- if (!sawCandidate) return atom('end_of_file');
1190
+ const source = String(stream.content);
1191
+ const remainderStart = stream.position;
1192
+ stream.position = source.length;
1193
+ if (!sawCandidate) {
1194
+ if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1195
+ return atom('end_of_file');
1196
+ }
1175
1197
  throw new PrologError('syntax_error(read_term)');
1176
1198
  }
1177
1199
  }
package/src/parser.js CHANGED
@@ -87,7 +87,6 @@ const PREFIX_OPERATORS = new Map([
87
87
 
88
88
  export const ISO_OPERATOR_DEFINITIONS = [
89
89
  [1200, 'xfx', ':-'], [1200, 'fx', ':-'], [1200, 'fx', '?-'], [1200, 'xfx', '-->'],
90
- [1105, 'xfy', '|'],
91
90
  [1100, 'xfy', ';'], [1050, 'xfy', '->'], [1000, 'xfy', ','],
92
91
  [900, 'fy', '\\+'],
93
92
  ...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
@@ -99,6 +98,13 @@ export const ISO_OPERATOR_DEFINITIONS = [
99
98
  [200, 'fy', '+'], [200, 'fy', '-'], [200, 'fy', '\\'],
100
99
  ];
101
100
 
101
+ // The alternative operator belongs to the Part 3 grammar-rule profile. Part 1
102
+ // reserves `|` as list punctuation but permits a program to declare it as an
103
+ // infix operator at priority 1001 or greater (Corrigendum 2).
104
+ export const PART3_OPERATOR_DEFINITIONS = [
105
+ [1105, 'xfy', '|'],
106
+ ];
107
+
102
108
  // EyeProlog's embedded quad syntax permits an optional label before `?-`.
103
109
  // That makes `?-` an implementation-specific xfx operator in addition to its
104
110
  // ISO 1200 fx definition.
@@ -118,6 +124,10 @@ function operatorStrength(priority) {
118
124
  return 1201 - priority;
119
125
  }
120
126
 
127
+ // ISO 6.3.3 arguments have maximum priority 999. Parenthesized terms and
128
+ // curly-bracket contents may contain a full priority-1200 term instead.
129
+ const ARG_MIN_PRECEDENCE = operatorStrength(999);
130
+
121
131
  function isGraphicAtomCode(code) {
122
132
  return graphicAtomChars.includes(String.fromCharCode(code));
123
133
  }
@@ -148,7 +158,10 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
148
158
  // The infix ?-/2 form is an EyeProlog quad extension. ISO 13211-1
149
159
  // predefines only the 1200 fx ?- operator; strict core mode starts from that
150
160
  // table and still permits an explicit op/3 directive to add an infix form.
151
- if (options.isoStrict === true) state.infixOperators.delete('?-');
161
+ if (options.isoStrict === true) {
162
+ state.infixOperators.delete('?-');
163
+ state.infixOperators.delete('|');
164
+ }
152
165
  for (const definition of definitions) {
153
166
  const [priority, specifier, name] = Array.isArray(definition)
154
167
  ? definition
@@ -422,16 +435,22 @@ class Parser {
422
435
  const previousEndsTerm = this.previousToken && (
423
436
  [TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
424
437
  (this.previousToken.type === TOK.ATOM &&
425
- !this.infixOperators.has(this.previousToken.text) &&
426
- !this.prefixOperators.has(this.previousToken.text) &&
427
- !this.postfixOperators.has(this.previousToken.text))
438
+ (this.postfixOperators.has(this.previousToken.text) ||
439
+ (!this.infixOperators.has(this.previousToken.text) &&
440
+ !this.prefixOperators.has(this.previousToken.text))))
428
441
  );
429
442
  if (isDigitCode(ch.charCodeAt(0)) ||
430
443
  (ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
431
444
  const start = this.pos;
432
445
  const negative = this.peek() === '-';
433
446
  if (negative) this.take();
434
- if (this.peek() === '0' && this.peek(1) === "'") {
447
+ const startsQuotedCharacter = this.peek() === '0' && this.peek(1) === "'" &&
448
+ // `0''` is the integer 0 followed by the empty atom, whereas `0'''`
449
+ // is the character-code constant for an apostrophe. A continuation
450
+ // after the integer likewise belongs to the following quoted atom.
451
+ (this.peek(2) !== "'" || this.peek(3) === "'") &&
452
+ !(this.peek(2) === '\\' && this.peek(3) === '\n');
453
+ if (startsQuotedCharacter) {
435
454
  this.take();
436
455
  this.take();
437
456
  let value = this.take();
@@ -463,7 +482,12 @@ class Parser {
463
482
  const code = value.codePointAt(0);
464
483
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
465
484
  }
466
- if (this.peek() === '0' && ['b', 'o', 'x'].includes(this.peek(1))) {
485
+ const radixKind = this.peek() === '0' ? this.peek(1) : '';
486
+ const radixHasDigit = radixKind === 'b' ? /^[01]$/.test(this.peek(2))
487
+ : radixKind === 'o' ? /^[0-7]$/.test(this.peek(2))
488
+ : radixKind === 'x' ? /^[0-9A-Fa-f]$/.test(this.peek(2))
489
+ : false;
490
+ if (radixHasDigit) {
467
491
  this.take();
468
492
  const kind = this.take();
469
493
  const radix = kind === 'b' ? 2 : kind === 'o' ? 8 : 16;
@@ -477,11 +501,16 @@ class Parser {
477
501
  return { type: TOK.NUMBER, text: integer.toString(), line };
478
502
  }
479
503
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
504
+ let hasFraction = false;
480
505
  if (this.peek() === '.' && isDigitCode(this.peek(1).charCodeAt(0))) {
506
+ hasFraction = true;
481
507
  this.take();
482
508
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
483
509
  }
484
- if ((this.peek() === 'e' || this.peek() === 'E')) {
510
+ // ISO floating-point syntax requires a fractional part before an
511
+ // exponent. Thus 1.0e9 is one number token, while 1E9 is the integer 1
512
+ // followed by the name E9 and is not a valid term without an operator.
513
+ if (hasFraction && (this.peek() === 'e' || this.peek() === 'E')) {
485
514
  let idx = this.pos + 1;
486
515
  if (this.source[idx] === '+' || this.source[idx] === '-') idx++;
487
516
  if (isDigitCode((this.source[idx] ?? '').charCodeAt(0))) {
@@ -490,7 +519,10 @@ class Parser {
490
519
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
491
520
  }
492
521
  }
493
- return { type: TOK.NUMBER, text: this.source.slice(start, this.pos), line };
522
+ let text = this.source.slice(start, this.pos);
523
+ if (!hasFraction) text = BigInt(text).toString();
524
+ else if (Object.is(Number(text), -0)) text = '0.0';
525
+ return { type: TOK.NUMBER, text, line };
494
526
  }
495
527
 
496
528
  if (isVariableStartCode(ch.charCodeAt(0))) {
@@ -527,7 +559,9 @@ class Parser {
527
559
  parseParenthesizedTerm() {
528
560
  this.expect(TOK.LPAREN, '(');
529
561
  this.advance();
530
- const term = this.parseTerm(0, true);
562
+ // A current operator atom may be the complete parenthesized term, e.g.
563
+ // (+), but it cannot silently become an operand in a larger expression.
564
+ const term = this.parseTerm(0, true, true, true);
531
565
  this.expect(TOK.RPAREN, ')');
532
566
  this.advance();
533
567
  return term;
@@ -544,14 +578,14 @@ class Parser {
544
578
  const items = [];
545
579
  let tail = null;
546
580
  while (true) {
547
- items.push(this.parseTerm(0, false, false));
581
+ items.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
548
582
  if (this.token.type === TOK.COMMA) {
549
583
  this.advance();
550
584
  continue;
551
585
  }
552
586
  if (this.token.type === TOK.BAR) {
553
587
  this.advance();
554
- tail = this.parseTerm(0, false, false);
588
+ tail = this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true);
555
589
  this.expect(TOK.RBRACKET, ']');
556
590
  this.advance();
557
591
  break;
@@ -576,8 +610,33 @@ class Parser {
576
610
  this.advance();
577
611
  return compound('{}', [term]);
578
612
  }
579
- parseTerm(minPrecedence = 0, allowComma = false, allowBar = true) {
580
- let left = this.parsePrefixTerm(minPrecedence, allowBar);
613
+ parseFunctionalNotation(name) {
614
+ this.expect(TOK.LPAREN, '(');
615
+ this.advance();
616
+ const args = [];
617
+ if (this.token.type === TOK.RPAREN) {
618
+ throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
619
+ }
620
+ while (true) {
621
+ args.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
622
+ if (this.token.type !== TOK.COMMA) break;
623
+ this.advance();
624
+ }
625
+ this.expect(TOK.RPAREN, ')');
626
+ this.advance();
627
+ return compound(name, args);
628
+ }
629
+ parseTerm(minPrecedence = 0, allowComma = false, allowBar = true, allowOperatorAtom = false) {
630
+ const initialOperatorName = this.operatorTokenName();
631
+ // An atom whose only operator declaration is postfix can still seed a
632
+ // postfix expression: after `op(100,xf,a), op(200,xf,b)`, `a b` denotes
633
+ // b(a). Prefix and infix operator atoms cannot be bare operands.
634
+ const initialWasCurrentOperator = initialOperatorName != null &&
635
+ (this.infixOperators.has(initialOperatorName) ||
636
+ this.prefixOperators.has(initialOperatorName));
637
+ let left = this.parsePrefixTerm(minPrecedence, allowBar, allowOperatorAtom);
638
+ const leftIsBareOperatorAtom = initialWasCurrentOperator &&
639
+ left.type === ATOM && left.name === initialOperatorName;
581
640
  let strictPostfixPrecedence = null;
582
641
  while (true) {
583
642
  const op = this.token.type === TOK.COMMA && allowComma
@@ -591,15 +650,26 @@ class Parser {
591
650
  const postfix = postfixName == null ? null : this.postfixOperators.get(postfixName);
592
651
  if (!postfix || postfix.precedence < minPrecedence ||
593
652
  (strictPostfixPrecedence === postfix.precedence)) break;
653
+ if (this.strictIso && leftIsBareOperatorAtom) {
654
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
655
+ }
594
656
  const name = postfixName;
595
657
  this.advance();
596
658
  left = compound(name, [left]);
597
659
  strictPostfixPrecedence = postfix.strict ? postfix.precedence : null;
598
660
  continue;
599
661
  }
662
+ if (this.strictIso && leftIsBareOperatorAtom) {
663
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
664
+ }
600
665
  strictPostfixPrecedence = null;
601
666
  this.advance();
602
- const right = this.parseTerm(info.associativity === 'right' ? info.precedence : info.precedence + 1, allowComma, allowBar);
667
+ const right = this.parseTerm(
668
+ info.associativity === 'right' ? info.precedence : info.precedence + 1,
669
+ allowComma,
670
+ allowBar,
671
+ false,
672
+ );
603
673
  left = compound(op, [left, right]);
604
674
  if (info.associativity === 'none') {
605
675
  const nextOp = this.token.type === TOK.COMMA && allowComma
@@ -612,18 +682,46 @@ class Parser {
612
682
  }
613
683
  }
614
684
  }
685
+ if (this.strictIso && leftIsBareOperatorAtom && left.type === ATOM && !allowOperatorAtom) {
686
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
687
+ }
615
688
  return left;
616
689
  }
617
- parsePrefixTerm(minPrecedence = 0, allowBar = true) {
690
+ parsePrefixTerm(minPrecedence = 0, allowBar = true, allowOperatorAtom = false) {
618
691
  // `:-` is tokenized specially so the program grammar can recognize clause
619
692
  // and directive markers. In term argument position, however, ISO 6.3.3.1
620
693
  // permits an operator atom directly as an `arg`; a leading `:-` cannot be
621
694
  // prefix operator notation at argument priority, so it denotes the atom.
622
695
  if (this.token.type === TOK.IF) {
696
+ if (this.strictIso && !allowOperatorAtom) {
697
+ throw new Error(`parse line ${this.token.line}: operator atom :- requires argument context or parentheses`);
698
+ }
623
699
  this.advance();
624
700
  return atom(':-');
625
701
  }
626
702
  const operatorName = this.operatorTokenName();
703
+ // A negative number consists of a minus name token followed by a numeric
704
+ // token, with layout permitted between them. It is lexical number syntax,
705
+ // not an application of the current prefix `-` operator, and therefore
706
+ // remains valid even after op(0, fy, -).
707
+ if (operatorName === '-' && this.token.type === TOK.ATOM) {
708
+ const state = {
709
+ pos: this.pos,
710
+ line: this.line,
711
+ previousToken: this.previousToken,
712
+ token: this.token,
713
+ };
714
+ this.advance();
715
+ if (this.token.type === TOK.NUMBER && !this.token.text.startsWith('-')) {
716
+ const value = this.token.text;
717
+ this.advance();
718
+ return numberTerm(`-${value}`);
719
+ }
720
+ this.pos = state.pos;
721
+ this.line = state.line;
722
+ this.previousToken = state.previousToken;
723
+ this.token = state.token;
724
+ }
627
725
  if (operatorName != null && this.prefixOperators.get(operatorName)?.precedence >= minPrecedence) {
628
726
  const op = operatorName;
629
727
  const info = this.prefixOperators.get(op);
@@ -633,26 +731,30 @@ class Parser {
633
731
  // an argument delimiter there is no operand for prefix syntax, so keep
634
732
  // the operator as an atom instead of reporting a misleading bad-term
635
733
  // error.
636
- if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR].includes(this.token.type)) {
734
+ if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR, TOK.DOT].includes(this.token.type)) {
735
+ if (this.strictIso && !allowOperatorAtom && this.token.type !== TOK.DOT) {
736
+ throw new Error(`parse line ${this.token.line}: operator atom ${op} requires argument context or parentheses`);
737
+ }
637
738
  return atom(op);
638
739
  }
639
740
  if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
640
- this.advance();
641
- const args = [];
642
- while (true) {
643
- args.push(this.parseTerm(0, false, false));
644
- if (this.token.type !== TOK.COMMA) break;
645
- this.advance();
646
- }
647
- this.expect(TOK.RPAREN, ')');
648
- this.advance();
649
- return compound(op, args);
741
+ return this.parseFunctionalNotation(op);
650
742
  }
651
- return compound(op, [this.parseTerm(info.precedence + (info.strict ? 1 : 0), false, allowBar)]);
743
+ return compound(op, [this.parseTerm(info.precedence + (info.strict ? 1 : 0), false, allowBar, false)]);
652
744
  }
653
745
  if (this.token.type === TOK.LPAREN) return this.parseParenthesizedTerm();
654
- if (this.token.type === TOK.LBRACKET) return this.parseList();
655
- if (this.token.type === TOK.LBRACE) return this.parseCurly();
746
+ if (this.token.type === TOK.LBRACKET) {
747
+ const list = this.parseList();
748
+ if (list.type === ATOM && list.name === '[]' && this.token.type === TOK.LPAREN &&
749
+ this.token.precededByLayout !== true) return this.parseFunctionalNotation('[]');
750
+ return list;
751
+ }
752
+ if (this.token.type === TOK.LBRACE) {
753
+ const curly = this.parseCurly();
754
+ if (curly.type === ATOM && curly.name === '{}' && this.token.type === TOK.LPAREN &&
755
+ this.token.precededByLayout !== true) return this.parseFunctionalNotation('{}');
756
+ return curly;
757
+ }
656
758
  if (this.token.type === TOK.VAR) {
657
759
  const name = this.token.text;
658
760
  this.advance();
@@ -668,21 +770,7 @@ class Parser {
668
770
  const value = this.token.text;
669
771
  this.advance();
670
772
  if (this.parserFlagState.doubleQuotes === 'atom') {
671
- if (this.token.type === TOK.LPAREN) {
672
- this.advance();
673
- const args = [];
674
- if (this.token.type === TOK.RPAREN) {
675
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(value)} for arity zero data`);
676
- }
677
- while (true) {
678
- args.push(this.parseTerm(0, false, false));
679
- if (this.token.type !== TOK.COMMA) break;
680
- this.advance();
681
- }
682
- this.expect(TOK.RPAREN, ')');
683
- this.advance();
684
- return compound(value, args);
685
- }
773
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) return this.parseFunctionalNotation(value);
686
774
  return atom(value);
687
775
  }
688
776
  const items = Array.from(value, (character) =>
@@ -701,23 +789,12 @@ class Parser {
701
789
  if (this.token.type === TOK.ATOM) {
702
790
  const name = this.token.text;
703
791
  this.advance();
704
- if (this.token.type === TOK.LPAREN) {
705
- this.advance();
706
- const args = [];
707
- if (this.token.type === TOK.RPAREN) {
708
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
709
- }
710
- while (true) {
711
- args.push(this.parseTerm(0, false, false));
712
- if (this.token.type === TOK.COMMA) {
713
- this.advance();
714
- continue;
715
- }
716
- break;
717
- }
718
- this.expect(TOK.RPAREN, ')');
719
- this.advance();
720
- return compound(name, args);
792
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
793
+ return this.parseFunctionalNotation(name);
794
+ }
795
+ if (this.strictIso && !allowOperatorAtom &&
796
+ (this.infixOperators.has(name) || this.prefixOperators.has(name) || this.postfixOperators.has(name))) {
797
+ throw new Error(`parse line ${this.token.line}: operator atom ${name} requires argument context or parentheses`);
721
798
  }
722
799
  return atom(name);
723
800
  }
@@ -842,7 +919,7 @@ class Parser {
842
919
  this.token = headState.token;
843
920
  };
844
921
 
845
- let head = this.parseTerm(3);
922
+ let head = this.parseTerm(3, false, true, true);
846
923
  if (this.token.type === TOK.COMMA && !this.strictIso) {
847
924
  restoreHeadState();
848
925
  const quadId = this.parseTerm(3, true);
@@ -852,7 +929,7 @@ class Parser {
852
929
  continue;
853
930
  }
854
931
  restoreHeadState();
855
- head = this.parseTerm(3);
932
+ head = this.parseTerm(3, false, true, true);
856
933
  }
857
934
 
858
935
  // Outside quad syntax, preserve the existing program-level comma rule,
@@ -1387,11 +1464,13 @@ export function parseNumberTokenText(text) {
1387
1464
  const digitsStart = position;
1388
1465
  while (isDigitCode(source.charCodeAt(position))) position++;
1389
1466
  if (position === digitsStart) throw new Error('not exactly one number token');
1467
+ let hasFraction = false;
1390
1468
  if (source[position] === '.' && isDigitCode(source.charCodeAt(position + 1))) {
1469
+ hasFraction = true;
1391
1470
  position++;
1392
1471
  while (isDigitCode(source.charCodeAt(position))) position++;
1393
1472
  }
1394
- if (source[position] === 'e' || source[position] === 'E') {
1473
+ if (hasFraction && (source[position] === 'e' || source[position] === 'E')) {
1395
1474
  position++;
1396
1475
  if (source[position] === '+' || source[position] === '-') position++;
1397
1476
  const exponentStart = position;
@@ -1399,6 +1478,8 @@ export function parseNumberTokenText(text) {
1399
1478
  if (position === exponentStart) throw new Error('not exactly one number token');
1400
1479
  }
1401
1480
  if (position !== source.length) throw new Error('not exactly one number token');
1481
+ if (/^-?\d+$/.test(source)) return numberTerm(BigInt(source).toString());
1482
+ if (Object.is(Number(source), -0)) return numberTerm('0.0');
1402
1483
  return numberTerm(source);
1403
1484
  }
1404
1485
 
package/src/program.js CHANGED
@@ -5,6 +5,7 @@ import { numberValueKey } from './number-value.js';
5
5
  import { formatTermForWrite } from './write.js';
6
6
  import {
7
7
  ISO_OPERATOR_DEFINITIONS,
8
+ PART3_OPERATOR_DEFINITIONS,
8
9
  QUAD_OPERATOR_DEFINITIONS,
9
10
  createParserOperatorState,
10
11
  parseClauses,
@@ -106,7 +107,7 @@ export class Program {
106
107
  this.operators = new Map();
107
108
  const predefinedOperatorSets = this.strictIso
108
109
  ? [ISO_OPERATOR_DEFINITIONS]
109
- : [ISO_OPERATOR_DEFINITIONS, QUAD_OPERATOR_DEFINITIONS];
110
+ : [ISO_OPERATOR_DEFINITIONS, PART3_OPERATOR_DEFINITIONS, QUAD_OPERATOR_DEFINITIONS];
110
111
  for (const definitions of predefinedOperatorSets) {
111
112
  for (const [priority, specifier, name] of definitions) {
112
113
  this.defineOperator(priority, specifier, name);
@@ -34,6 +34,11 @@ export function characterCodeConstantEnd(source, apostropheIndex) {
34
34
  const characterIndex = apostropheIndex + 1;
35
35
  const character = source[characterIndex] ?? '';
36
36
  if (!character) return apostropheIndex;
37
+ // `0''` is two tokens (0 and the empty atom), and `0'\\\n...'
38
+ // likewise starts a quoted atom containing a continuation. Only `0'''`
39
+ // denotes the character-code constant for an apostrophe.
40
+ if (character === "'" && source[characterIndex + 1] !== "'") return null;
41
+ if (character === '\\' && ['\n', '\r'].includes(source[characterIndex + 1])) return null;
37
42
  if (character === '\\') return quotedEscapeEnd(source, characterIndex);
38
43
 
39
44
  // An apostrophe character is doubled in 0''' exactly as it is in a quoted
package/src/write.js CHANGED
@@ -28,6 +28,7 @@ function atomNeedsQuotes(name) {
28
28
  if (!name) return true;
29
29
  if (name === '[]' || name === '{}') return false;
30
30
  if (name === '...') return false;
31
+ if (name.startsWith('/*')) return true;
31
32
  if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
32
33
  if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
33
34
  for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
@@ -104,6 +105,7 @@ function writeNumberedVariable(index) {
104
105
  }
105
106
 
106
107
  function operatorName(name) {
108
+ if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
107
109
  if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return name;
108
110
  if (/^[!#$&*+\-./<=>@^~\\;:]+$/.test(name)) return name;
109
111
  return quoteAtom(name);
@@ -142,6 +144,12 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
142
144
  // `arg` production: an atom that is a current operator is valid there
143
145
  // without quoting. Keep lexical exceptions such as `|` quoted.
144
146
  if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
147
+ if (!options.ignoreOps && context !== 'argument' && table.has(resolved.name)) {
148
+ const definitions = table.get(resolved.name);
149
+ const requiresParentheses = definitions.some(({ specifier }) =>
150
+ ['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(specifier));
151
+ if (requiresParentheses) return `(${operatorName(resolved.name)})`;
152
+ }
145
153
  return writeAtom(resolved.name);
146
154
  }
147
155
  if (resolved.type === NUMBER) return resolved.name;
@@ -186,11 +194,20 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
186
194
  const argumentPriority = specifier === 'fx' ? priority - 1 : priority;
187
195
  text = `${token} ${format(resolved.args[0], env, options, table, argumentPriority)}`;
188
196
  } else if (specifier === 'xf' || specifier === 'yf') {
189
- const argumentPriority = specifier === 'xf' ? priority - 1 : priority;
197
+ let argumentPriority = specifier === 'xf' ? priority - 1 : priority;
198
+ const childDefinition = chooseOperator(deref(resolved.args[0], env), table);
199
+ if (childDefinition?.priority === priority &&
200
+ ['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(childDefinition.specifier)) {
201
+ argumentPriority = priority - 1;
202
+ }
190
203
  text = `${format(resolved.args[0], env, options, table, argumentPriority)} ${token}`;
191
204
  } else {
192
- const leftPriority = specifier === 'yfx' ? priority : priority - 1;
205
+ let leftPriority = specifier === 'yfx' ? priority : priority - 1;
193
206
  const rightPriority = specifier === 'xfy' ? priority : priority - 1;
207
+ const leftDefinition = chooseOperator(deref(resolved.args[0], env), table);
208
+ if (leftDefinition?.priority === priority && ['fx', 'fy'].includes(leftDefinition.specifier)) {
209
+ leftPriority = priority - 1;
210
+ }
194
211
  const left = format(resolved.args[0], env, options, table, leftPriority);
195
212
  const right = format(resolved.args[1], env, options, table, rightPriority);
196
213
  text = resolved.name === ',' ? `${left}, ${right}`
@@ -18,7 +18,7 @@ error-ordering alternative to an individual executable assertion.
18
18
 
19
19
  | Requirement | Status | EyeProlog evidence / remaining work |
20
20
  | --- | --- | --- |
21
- | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the selected WG17 high-risk syntax case. Complete character-by-character external syntax sweep remains open. |
21
+ | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete 366-case WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
22
22
  | 5.1(b) execute conforming Prolog goals | audit | Clause 7-9 conformance corpus plus regression/API/example gates. A normative goal-semantics ledger is still being expanded. |
23
23
  | 5.1(c) reject nonconforming text/read-terms | audit | Dedicated syntax-error cases and strict-core extension rejection. Exhaustive lexical rejection coverage remains open. |
24
24
  | 5.1(d) document permitted variations | audit | Major implementation-defined choices are documented in *The Art of EyeProlog*. Every occurrence of “implementation defined/dependent/specific” in Part 1 still needs a final documentation cross-check. |
@@ -30,7 +30,7 @@ error-ordering alternative to an individual executable assertion.
30
30
 
31
31
  | Standard area | Status | Current evidence |
32
32
  | --- | --- | --- |
33
- | Clause 6 — tokens, terms, lists, operators, quoted text | audit | `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, `wg17_syntax_high_risk`, quoted-layout/escape error cases, writer/read-back regressions. |
33
+ | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete 366-case WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
35
35
  | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
36
36
  | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
@@ -75,6 +75,7 @@ A release intended to advance ISO conformance must pass all of:
75
75
  npm test
76
76
  npm run test:iso-strict
77
77
  npm run test:conformance
78
+ npm run test:wg17-syntax
78
79
  ```
79
80
 
80
81
  The unified `npm test` gate includes the strict-core suite. Expected conformance
@@ -88,9 +89,9 @@ ISO/IEC 13211-1 processor” until all of the following are true:
88
89
  1. every normative Part 1 processor requirement is represented in this ledger;
89
90
  2. every `audit` row above has been reduced to explicit pass/not-applicable or
90
91
  documented implementation-defined choices;
91
- 3. the complete external WG17 syntax/conversion/variable-name conformity
92
- corpus has been run against the strict core mode, with every difference
93
- explained or fixed;
92
+ 3. the external WG17 conversion and variable-name conformity corpora have
93
+ joined the now-complete syntax corpus in strict core mode, with every
94
+ difference explained or fixed;
94
95
  4. prescribed modes, errors, side effects, and relevant error precedence for
95
96
  every Part 1 built-in have executable coverage;
96
97
  5. every implementation-defined/dependent/specific choice required to be
@@ -0,0 +1,42 @@
1
+ # WG17 syntax traceability status
2
+
3
+ Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
4
+ Upstream inventory checked: 2026-08-15
5
+
6
+ This ledger counts an upstream case only when its WG17 identifier, query,
7
+ expected ISO disposition, and observed EyeProlog outcome are stored in the
8
+ offline executable matrix. Semantically similar parser tests are not inferred
9
+ as coverage.
10
+
11
+ ## Current standing
12
+
13
+ | Measure | Count |
14
+ | --- | ---: |
15
+ | Active upstream cases | 366 |
16
+ | Executable EyeProlog dispositions | 366 (100.0%) |
17
+ | Not yet traced one-by-one | 0 |
18
+ | Deleted upstream identifiers | #20, #273 |
19
+
20
+ The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
21
+ three upstream `waits` cases are checked through EyeProlog's interactive input
22
+ hook; the other 363 cases are checked for their exact stored
23
+ success output, bindings, failure, or ISO error category.
24
+
25
+ ## Traceable evidence
26
+
27
+ | Executable evidence | Referenced IDs | WG17 cases |
28
+ | --- | ---: | --- |
29
+ | [complete offline executable matrix](../run-wg17-syntax.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
30
+
31
+ The evidence groups overlap. Their union is **366** active cases:
32
+ #1–#19, #21–#272, #274–#368.
33
+
34
+ ## Untraced upstream identifiers
35
+
36
+ None.
37
+
38
+ ## Maintenance
39
+
40
+ 1. Refresh the dated fixture when the upstream table changes.
41
+ 2. Review any changed ISO expectation before updating an observed snapshot.
42
+ 3. Keep this generated status page synchronized in the release gate.