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 +3 -1
- package/src/iso.js +25 -3
- package/src/parser.js +146 -65
- package/src/program.js +2 -1
- package/src/syntax-scan.js +5 -0
- package/src/write.js +19 -2
- package/test/conformance/ISO-COMPLIANCE.md +6 -5
- package/test/conformance/WG17-SYNTAX-STATUS.md +42 -0
- package/test/conformance/wg17-syntax-cases.json +5564 -0
- package/test/conformance/wg17-syntax-coverage.json +18 -0
- package/test/run-all.mjs +2 -0
- package/test/run-regression.mjs +15 -3
- package/test/run-wg17-syntax.mjs +106 -0
- package/tools/report-wg17-syntax-coverage.mjs +141 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.2.
|
|
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
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
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)
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
580
|
-
|
|
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(
|
|
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.
|
|
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)
|
|
655
|
-
|
|
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.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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);
|
package/src/syntax-scan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
92
|
-
|
|
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.
|