eyeprolog 1.2.24 → 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.24",
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
@@ -9,6 +9,7 @@ import { sameNumberValue } from './number-value.js';
9
9
  import { createParserOperatorState, parseClauses, parseGoalText, parseNumberTokenText } from './parser.js';
10
10
  import { formatTermForWrite } from './write.js';
11
11
  import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
12
+ import { characterCodeConstantEnd, quotedEscapeEnd } from './syntax-scan.js';
12
13
 
13
14
  let isoFresh = 0;
14
15
 
@@ -1071,33 +1072,9 @@ function isTerminatingFullStop(source, index) {
1071
1072
  return true;
1072
1073
  }
1073
1074
 
1074
- function quotedEscapeEnd(source, index) {
1075
- const escaped = source[index + 1] ?? '';
1076
- if (!escaped) return index;
1077
-
1078
- // A quoted-token continuation escape consumes its newline. Accept CRLF as
1079
- // the host representation of the same line boundary.
1080
- if (escaped === '\n') return index + 1;
1081
- if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
1082
-
1083
- // ISO 6.4.2.1 numeric escapes include a terminating backslash. Consume
1084
- // that delimiter as part of the quoted character so stream scanning does
1085
- // not mistake it for an escape of the quote which follows.
1086
- if (escaped === 'x') {
1087
- let cursor = index + 2;
1088
- while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
1089
- return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1090
- }
1091
- if (/^[0-9]$/.test(escaped)) {
1092
- let cursor = index + 1;
1093
- // Scan all decimal digits here, including 8 and 9. This scanner only
1094
- // locates the end of a candidate quoted escape; the parser remains
1095
- // authoritative and rejects non-octal digits.
1096
- while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
1097
- return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1098
- }
1099
-
1100
- return index + 1;
1075
+ const termGraphicCharacters = new Set('#$&*+-./<=>@^~\\:');
1076
+ function continuesGraphicToken(source, index) {
1077
+ return index > 0 && termGraphicCharacters.has(source[index - 1]);
1101
1078
  }
1102
1079
 
1103
1080
  function* termTextCandidates(stream) {
@@ -1120,14 +1097,27 @@ function* termTextCandidates(stream) {
1120
1097
  else if (ch === quote) quote = null;
1121
1098
  continue;
1122
1099
  }
1100
+ const characterCodeEnd = characterCodeConstantEnd(source, i);
1101
+ if (characterCodeEnd != null) { i = characterCodeEnd; continue; }
1123
1102
  if (ch === '%') { lineComment = true; continue; }
1124
- 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
+ }
1125
1110
  if (ch === "'" || ch === '"') { quote = ch; continue; }
1126
1111
  if (ch === '.' && isTerminatingFullStop(source, i)) {
1127
1112
  yield { text: source.slice(stream.position, i + 1), end: i + 1 };
1128
1113
  }
1129
1114
  }
1130
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
+ }
1131
1121
  function convertedTermText(text, solver) {
1132
1122
  if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
1133
1123
  let result = '', quote = null;
@@ -1169,6 +1159,7 @@ function readTermFromStream(stream, solver) {
1169
1159
  const clauses = parseClauses(convertedTermText(candidate.text, solver), {
1170
1160
  sourceMetadata: false,
1171
1161
  operatorState,
1162
+ isoStrict: solver.isoStrict,
1172
1163
  doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
1173
1164
  });
1174
1165
  if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
@@ -1196,8 +1187,13 @@ function readTermFromStream(stream, solver) {
1196
1187
  }
1197
1188
  }
1198
1189
 
1199
- stream.position = String(stream.content).length;
1200
- 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
+ }
1201
1197
  throw new PrologError('syntax_error(read_term)');
1202
1198
  }
1203
1199
  }
@@ -1605,6 +1601,11 @@ function sameNumber(left, right) {
1605
1601
  function canonicalNumberText(value) {
1606
1602
  if (isDecimalInteger(value.name)) return BigInt(value.name).toString();
1607
1603
  const finite = Number(value.name);
1604
+ // ISO 13211-1 has a single floating-point zero: a source spelling with a
1605
+ // minus sign does not denote a distinct -0.0 value. Generate the same
1606
+ // character sequence for both spellings so converting that sequence back
1607
+ // cannot make number_chars/2 change its own subsequent output.
1608
+ if (finite === 0) return '0.0';
1608
1609
  if (Number.isFinite(finite) && /^-?\d+\.\d+(?:[eE][+-]?\d+)?$/.test(value.name)) {
1609
1610
  return value.name;
1610
1611
  }
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
@@ -342,7 +355,9 @@ class Parser {
342
355
  nextToken() {
343
356
  // The tokenizer keeps just enough state for useful parse-line errors and
344
357
  // treats quoted atoms and quoted strings differently, as Prolog syntax does.
358
+ const beforeLayout = this.pos;
345
359
  this.skipWhitespaceAndComments();
360
+ const precededByLayout = this.pos !== beforeLayout;
346
361
  const line = this.line;
347
362
  const ch = this.peek();
348
363
  if (!ch) return { type: TOK.EOF, text: '', line };
@@ -377,7 +392,7 @@ class Parser {
377
392
  };
378
393
  if (punct[ch]) {
379
394
  this.take();
380
- return { type: punct[ch], text: ch, line };
395
+ return { type: punct[ch], text: ch, line, precededByLayout };
381
396
  }
382
397
  if (ch === ':' && this.peek(1) === '-') {
383
398
  this.pos += 2;
@@ -420,16 +435,22 @@ class Parser {
420
435
  const previousEndsTerm = this.previousToken && (
421
436
  [TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
422
437
  (this.previousToken.type === TOK.ATOM &&
423
- !this.infixOperators.has(this.previousToken.text) &&
424
- !this.prefixOperators.has(this.previousToken.text) &&
425
- !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))))
426
441
  );
427
442
  if (isDigitCode(ch.charCodeAt(0)) ||
428
443
  (ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
429
444
  const start = this.pos;
430
445
  const negative = this.peek() === '-';
431
446
  if (negative) this.take();
432
- 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) {
433
454
  this.take();
434
455
  this.take();
435
456
  let value = this.take();
@@ -461,7 +482,12 @@ class Parser {
461
482
  const code = value.codePointAt(0);
462
483
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
463
484
  }
464
- 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) {
465
491
  this.take();
466
492
  const kind = this.take();
467
493
  const radix = kind === 'b' ? 2 : kind === 'o' ? 8 : 16;
@@ -475,11 +501,16 @@ class Parser {
475
501
  return { type: TOK.NUMBER, text: integer.toString(), line };
476
502
  }
477
503
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
504
+ let hasFraction = false;
478
505
  if (this.peek() === '.' && isDigitCode(this.peek(1).charCodeAt(0))) {
506
+ hasFraction = true;
479
507
  this.take();
480
508
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
481
509
  }
482
- 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')) {
483
514
  let idx = this.pos + 1;
484
515
  if (this.source[idx] === '+' || this.source[idx] === '-') idx++;
485
516
  if (isDigitCode((this.source[idx] ?? '').charCodeAt(0))) {
@@ -488,7 +519,10 @@ class Parser {
488
519
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
489
520
  }
490
521
  }
491
- 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 };
492
526
  }
493
527
 
494
528
  if (isVariableStartCode(ch.charCodeAt(0))) {
@@ -525,7 +559,9 @@ class Parser {
525
559
  parseParenthesizedTerm() {
526
560
  this.expect(TOK.LPAREN, '(');
527
561
  this.advance();
528
- 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);
529
565
  this.expect(TOK.RPAREN, ')');
530
566
  this.advance();
531
567
  return term;
@@ -542,14 +578,14 @@ class Parser {
542
578
  const items = [];
543
579
  let tail = null;
544
580
  while (true) {
545
- items.push(this.parseTerm(0, false, false));
581
+ items.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
546
582
  if (this.token.type === TOK.COMMA) {
547
583
  this.advance();
548
584
  continue;
549
585
  }
550
586
  if (this.token.type === TOK.BAR) {
551
587
  this.advance();
552
- tail = this.parseTerm(0, false, false);
588
+ tail = this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true);
553
589
  this.expect(TOK.RBRACKET, ']');
554
590
  this.advance();
555
591
  break;
@@ -574,8 +610,33 @@ class Parser {
574
610
  this.advance();
575
611
  return compound('{}', [term]);
576
612
  }
577
- parseTerm(minPrecedence = 0, allowComma = false, allowBar = true) {
578
- 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;
579
640
  let strictPostfixPrecedence = null;
580
641
  while (true) {
581
642
  const op = this.token.type === TOK.COMMA && allowComma
@@ -589,15 +650,26 @@ class Parser {
589
650
  const postfix = postfixName == null ? null : this.postfixOperators.get(postfixName);
590
651
  if (!postfix || postfix.precedence < minPrecedence ||
591
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
+ }
592
656
  const name = postfixName;
593
657
  this.advance();
594
658
  left = compound(name, [left]);
595
659
  strictPostfixPrecedence = postfix.strict ? postfix.precedence : null;
596
660
  continue;
597
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
+ }
598
665
  strictPostfixPrecedence = null;
599
666
  this.advance();
600
- 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
+ );
601
673
  left = compound(op, [left, right]);
602
674
  if (info.associativity === 'none') {
603
675
  const nextOp = this.token.type === TOK.COMMA && allowComma
@@ -610,18 +682,46 @@ class Parser {
610
682
  }
611
683
  }
612
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
+ }
613
688
  return left;
614
689
  }
615
- parsePrefixTerm(minPrecedence = 0, allowBar = true) {
690
+ parsePrefixTerm(minPrecedence = 0, allowBar = true, allowOperatorAtom = false) {
616
691
  // `:-` is tokenized specially so the program grammar can recognize clause
617
692
  // and directive markers. In term argument position, however, ISO 6.3.3.1
618
693
  // permits an operator atom directly as an `arg`; a leading `:-` cannot be
619
694
  // prefix operator notation at argument priority, so it denotes the atom.
620
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
+ }
621
699
  this.advance();
622
700
  return atom(':-');
623
701
  }
624
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
+ }
625
725
  if (operatorName != null && this.prefixOperators.get(operatorName)?.precedence >= minPrecedence) {
626
726
  const op = operatorName;
627
727
  const info = this.prefixOperators.get(op);
@@ -631,26 +731,30 @@ class Parser {
631
731
  // an argument delimiter there is no operand for prefix syntax, so keep
632
732
  // the operator as an atom instead of reporting a misleading bad-term
633
733
  // error.
634
- 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
+ }
635
738
  return atom(op);
636
739
  }
637
- if (this.token.type === TOK.LPAREN) {
638
- this.advance();
639
- const args = [];
640
- while (true) {
641
- args.push(this.parseTerm(0, false, false));
642
- if (this.token.type !== TOK.COMMA) break;
643
- this.advance();
644
- }
645
- this.expect(TOK.RPAREN, ')');
646
- this.advance();
647
- return compound(op, args);
740
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
741
+ return this.parseFunctionalNotation(op);
648
742
  }
649
- 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)]);
650
744
  }
651
745
  if (this.token.type === TOK.LPAREN) return this.parseParenthesizedTerm();
652
- if (this.token.type === TOK.LBRACKET) return this.parseList();
653
- 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
+ }
654
758
  if (this.token.type === TOK.VAR) {
655
759
  const name = this.token.text;
656
760
  this.advance();
@@ -666,21 +770,7 @@ class Parser {
666
770
  const value = this.token.text;
667
771
  this.advance();
668
772
  if (this.parserFlagState.doubleQuotes === 'atom') {
669
- if (this.token.type === TOK.LPAREN) {
670
- this.advance();
671
- const args = [];
672
- if (this.token.type === TOK.RPAREN) {
673
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(value)} for arity zero data`);
674
- }
675
- while (true) {
676
- args.push(this.parseTerm(0, false, false));
677
- if (this.token.type !== TOK.COMMA) break;
678
- this.advance();
679
- }
680
- this.expect(TOK.RPAREN, ')');
681
- this.advance();
682
- return compound(value, args);
683
- }
773
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) return this.parseFunctionalNotation(value);
684
774
  return atom(value);
685
775
  }
686
776
  const items = Array.from(value, (character) =>
@@ -699,23 +789,12 @@ class Parser {
699
789
  if (this.token.type === TOK.ATOM) {
700
790
  const name = this.token.text;
701
791
  this.advance();
702
- if (this.token.type === TOK.LPAREN) {
703
- this.advance();
704
- const args = [];
705
- if (this.token.type === TOK.RPAREN) {
706
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
707
- }
708
- while (true) {
709
- args.push(this.parseTerm(0, false, false));
710
- if (this.token.type === TOK.COMMA) {
711
- this.advance();
712
- continue;
713
- }
714
- break;
715
- }
716
- this.expect(TOK.RPAREN, ')');
717
- this.advance();
718
- 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`);
719
798
  }
720
799
  return atom(name);
721
800
  }
@@ -840,7 +919,7 @@ class Parser {
840
919
  this.token = headState.token;
841
920
  };
842
921
 
843
- let head = this.parseTerm(3);
922
+ let head = this.parseTerm(3, false, true, true);
844
923
  if (this.token.type === TOK.COMMA && !this.strictIso) {
845
924
  restoreHeadState();
846
925
  const quadId = this.parseTerm(3, true);
@@ -850,7 +929,7 @@ class Parser {
850
929
  continue;
851
930
  }
852
931
  restoreHeadState();
853
- head = this.parseTerm(3);
932
+ head = this.parseTerm(3, false, true, true);
854
933
  }
855
934
 
856
935
  // Outside quad syntax, preserve the existing program-level comma rule,
@@ -1308,18 +1387,100 @@ export function parseProgramText(source, options = {}) {
1308
1387
 
1309
1388
  export function parseNumberTokenText(text) {
1310
1389
  const source = String(text ?? '');
1311
- const parser = new Parser(source, {
1312
- includeDefaultOperators: false,
1313
- sourceMetadata: false,
1314
- });
1315
- // Inspect the tokenizer position before requesting another token: advancing
1316
- // would skip trailing layout and make `3 ` or `3/**/` look complete. A
1317
- // literal space consumed by the character-code token `0' ` is already part
1318
- // of parser.pos and is therefore correctly accepted.
1319
- if (parser.token.type !== TOK.NUMBER || parser.pos !== source.length) {
1320
- throw new Error('not exactly one number token');
1390
+ let position = 0;
1391
+ let negative = false;
1392
+ if (source[position] === '-') {
1393
+ negative = true;
1394
+ position++;
1395
+ }
1396
+
1397
+ // number_chars/2 and number_codes/2 need the lexical number production,
1398
+ // not a general term reader. Keeping this scanner number-only avoids
1399
+ // allocating a Parser and three operator tables for every conversion.
1400
+ if (source.startsWith("0'", position)) {
1401
+ position += 2;
1402
+ let value = source[position] ?? '';
1403
+ if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
1404
+ throw new Error('not exactly one number token');
1405
+ }
1406
+ position++;
1407
+ const firstCode = value.charCodeAt(0);
1408
+ if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
1409
+ const secondCode = source.charCodeAt(position);
1410
+ if (secondCode < 0xdc00 || secondCode > 0xdfff) throw new Error('not exactly one number token');
1411
+ value += source[position++];
1412
+ } else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
1413
+ throw new Error('not exactly one number token');
1414
+ }
1415
+
1416
+ if (value === "'") {
1417
+ if (source[position] !== "'") throw new Error('not exactly one number token');
1418
+ position++;
1419
+ } else if (value === '\\') {
1420
+ const escaped = source[position++] ?? '';
1421
+ const controls = { a: '\x07', b: '\b', r: '\r', f: '\f', t: '\t', n: '\n', v: '\v' };
1422
+ if (controls[escaped] != null) {
1423
+ value = controls[escaped];
1424
+ } else if (escaped === 'x') {
1425
+ let digits = '';
1426
+ while (/^[0-9A-Fa-f]$/.test(source[position] ?? '')) digits += source[position++];
1427
+ if (!digits || source[position++] !== '\\') throw new Error('not exactly one number token');
1428
+ const code = Number.parseInt(digits, 16);
1429
+ if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw new Error('not exactly one number token');
1430
+ value = String.fromCodePoint(code);
1431
+ } else if (/^[0-7]$/.test(escaped)) {
1432
+ let digits = escaped;
1433
+ while (/^[0-7]$/.test(source[position] ?? '')) digits += source[position++];
1434
+ if (source[position++] !== '\\') throw new Error('not exactly one number token');
1435
+ const code = Number.parseInt(digits, 8);
1436
+ if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw new Error('not exactly one number token');
1437
+ value = String.fromCodePoint(code);
1438
+ } else if (escaped === '\\' || escaped === "'" || escaped === '"' || escaped === '`') {
1439
+ value = escaped;
1440
+ } else {
1441
+ throw new Error('not exactly one number token');
1442
+ }
1443
+ }
1444
+
1445
+ if (position !== source.length) throw new Error('not exactly one number token');
1446
+ const code = value.codePointAt(0);
1447
+ return numberTerm(String(negative ? -code : code));
1448
+ }
1449
+
1450
+ if (source[position] === '0' && ['b', 'o', 'x'].includes(source[position + 1])) {
1451
+ const kind = source[position + 1];
1452
+ const radix = kind === 'b' ? 2 : kind === 'o' ? 8 : 16;
1453
+ const digitPattern = radix === 2 ? /^[01]$/ : radix === 8 ? /^[0-7]$/ : /^[0-9A-Fa-f]$/;
1454
+ position += 2;
1455
+ let digits = '';
1456
+ while (digitPattern.test(source[position] ?? '')) digits += source[position++];
1457
+ if (!digits || position !== source.length) throw new Error('not exactly one number token');
1458
+ let integer = 0n;
1459
+ for (const digit of digits) integer = integer * BigInt(radix) + BigInt(Number.parseInt(digit, radix));
1460
+ if (negative) integer = -integer;
1461
+ return numberTerm(integer.toString());
1462
+ }
1463
+
1464
+ const digitsStart = position;
1465
+ while (isDigitCode(source.charCodeAt(position))) position++;
1466
+ if (position === digitsStart) throw new Error('not exactly one number token');
1467
+ let hasFraction = false;
1468
+ if (source[position] === '.' && isDigitCode(source.charCodeAt(position + 1))) {
1469
+ hasFraction = true;
1470
+ position++;
1471
+ while (isDigitCode(source.charCodeAt(position))) position++;
1472
+ }
1473
+ if (hasFraction && (source[position] === 'e' || source[position] === 'E')) {
1474
+ position++;
1475
+ if (source[position] === '+' || source[position] === '-') position++;
1476
+ const exponentStart = position;
1477
+ while (isDigitCode(source.charCodeAt(position))) position++;
1478
+ if (position === exponentStart) throw new Error('not exactly one number token');
1321
1479
  }
1322
- return numberTerm(parser.token.text);
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');
1483
+ return numberTerm(source);
1323
1484
  }
1324
1485
 
1325
1486
  export function parseGoalText(text, options = {}) {
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);