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/examples/output/access-control-policy.pl +2 -0
- package/examples/output/d3-group.pl +2 -2
- package/examples/output/map-four-color-search.pl +1 -0
- package/examples/output/partial-evaluator.pl +3 -0
- package/examples/output/weighted-interval-scheduling.pl +4 -0
- package/examples/proof/access-control-policy.pl +158 -0
- package/examples/proof/d3-group.pl +17 -17
- package/examples/proof/partial-evaluator.pl +359 -0
- package/examples/proof/weighted-interval-scheduling.pl +8388 -0
- package/package.json +3 -1
- package/src/iso.js +31 -30
- package/src/parser.js +238 -77
- package/src/program.js +2 -1
- package/src/repl.js +10 -32
- package/src/syntax-scan.js +54 -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/cases/iso/wg17_syntax_high_risk.pl +5 -1
- package/test/conformance/expected/iso/wg17_syntax_high_risk.pl +1 -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 +96 -1
- 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
|
@@ -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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
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)
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
578
|
-
|
|
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(
|
|
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.
|
|
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)
|
|
653
|
-
|
|
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.
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
//
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
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);
|