eyeprolog 1.2.25 → 1.2.27
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 +71 -20
- package/src/parser.js +146 -65
- package/src/program.js +2 -1
- package/src/syntax-scan.js +5 -0
- package/src/term.js +19 -2
- package/src/write.js +55 -4
- 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 +44 -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.27",
|
|
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;
|
|
@@ -1128,6 +1144,43 @@ function convertedTermText(text, solver) {
|
|
|
1128
1144
|
}
|
|
1129
1145
|
return result;
|
|
1130
1146
|
}
|
|
1147
|
+
|
|
1148
|
+
function scopeReadTerm(term) {
|
|
1149
|
+
// A term read from a stream has its own variable set (ISO 7.10.3). Parser
|
|
1150
|
+
// variable names cannot be used as environment identities here: a caller
|
|
1151
|
+
// such as read(X) and an input term X=a would otherwise share the same `X`
|
|
1152
|
+
// and incorrectly attempt the cyclic unification X=(X=a). Use an internal
|
|
1153
|
+
// name containing NUL, which cannot occur in Prolog source, while retaining
|
|
1154
|
+
// the spelling and occurrence count required by read_term/3 metadata.
|
|
1155
|
+
const scope = ++isoFresh;
|
|
1156
|
+
const bySourceName = new Map();
|
|
1157
|
+
const variables = [];
|
|
1158
|
+
|
|
1159
|
+
const copy = (item) => {
|
|
1160
|
+
if (item.type === VAR) {
|
|
1161
|
+
let record = bySourceName.get(item.name);
|
|
1162
|
+
if (record == null) {
|
|
1163
|
+
const scoped = variable(`\u0000read:${scope}:${variables.length}`);
|
|
1164
|
+
scoped.displayName = item.name;
|
|
1165
|
+
record = {
|
|
1166
|
+
sourceName: item.name,
|
|
1167
|
+
term: scoped,
|
|
1168
|
+
count: 0,
|
|
1169
|
+
anonymous: item.name.startsWith('__anon'),
|
|
1170
|
+
};
|
|
1171
|
+
bySourceName.set(item.name, record);
|
|
1172
|
+
variables.push(record);
|
|
1173
|
+
}
|
|
1174
|
+
record.count++;
|
|
1175
|
+
return record.term;
|
|
1176
|
+
}
|
|
1177
|
+
if (item.type !== COMPOUND) return item;
|
|
1178
|
+
return compound(item.name, item.args.map(copy));
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
return { term: copy(term), variables };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1131
1184
|
function readTermFromStream(stream, solver) {
|
|
1132
1185
|
let requestedInteractiveTerm = false;
|
|
1133
1186
|
while (true) {
|
|
@@ -1143,11 +1196,12 @@ function readTermFromStream(stream, solver) {
|
|
|
1143
1196
|
const clauses = parseClauses(convertedTermText(candidate.text, solver), {
|
|
1144
1197
|
sourceMetadata: false,
|
|
1145
1198
|
operatorState,
|
|
1199
|
+
isoStrict: solver.isoStrict,
|
|
1146
1200
|
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1147
1201
|
});
|
|
1148
1202
|
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1149
1203
|
stream.position = candidate.end;
|
|
1150
|
-
return clauses[0].head;
|
|
1204
|
+
return scopeReadTerm(clauses[0].head);
|
|
1151
1205
|
} catch (_) {
|
|
1152
1206
|
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1153
1207
|
// terminator. Keep scanning until a complete term parses.
|
|
@@ -1170,8 +1224,13 @@ function readTermFromStream(stream, solver) {
|
|
|
1170
1224
|
}
|
|
1171
1225
|
}
|
|
1172
1226
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1227
|
+
const source = String(stream.content);
|
|
1228
|
+
const remainderStart = stream.position;
|
|
1229
|
+
stream.position = source.length;
|
|
1230
|
+
if (!sawCandidate) {
|
|
1231
|
+
if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
|
|
1232
|
+
return { term: atom('end_of_file'), variables: [] };
|
|
1233
|
+
}
|
|
1175
1234
|
throw new PrologError('syntax_error(read_term)');
|
|
1176
1235
|
}
|
|
1177
1236
|
}
|
|
@@ -1180,39 +1239,31 @@ function* readBuiltin({ solver, goal, env }) {
|
|
|
1180
1239
|
const stream = inputStreamFor(solver, goal, env);
|
|
1181
1240
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
|
1182
1241
|
const next = env.clone();
|
|
1183
|
-
|
|
1242
|
+
const { term } = readTermFromStream(stream, solver);
|
|
1243
|
+
if (unify(goal.args[goal.arity - 1], term, next)) yield next;
|
|
1184
1244
|
}
|
|
1185
1245
|
function* readTermBuiltin({ solver, goal, env }) {
|
|
1186
1246
|
const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
|
|
1187
1247
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
|
1188
1248
|
const options = optionList(goal.args[goal.arity - 1], env);
|
|
1189
1249
|
const target = goal.args[goal.arity - 2];
|
|
1190
|
-
const term = readTermFromStream(stream, solver);
|
|
1250
|
+
const { term, variables } = readTermFromStream(stream, solver);
|
|
1191
1251
|
const next = env.clone();
|
|
1192
1252
|
if (!unify(target, term, next)) return;
|
|
1193
|
-
const variables = [];
|
|
1194
|
-
const counts = new Map();
|
|
1195
|
-
const visit = (item) => {
|
|
1196
|
-
if (item.type === VAR) {
|
|
1197
|
-
counts.set(item.name, (counts.get(item.name) ?? 0) + 1);
|
|
1198
|
-
if (!variables.some((entry) => entry.name === item.name)) variables.push(item);
|
|
1199
|
-
} else for (const arg of item.args) visit(arg);
|
|
1200
|
-
};
|
|
1201
|
-
visit(term);
|
|
1202
1253
|
for (const option of options) {
|
|
1203
1254
|
if (option.type === VAR) throw new PrologError('instantiation_error');
|
|
1204
1255
|
if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(read_option)', option);
|
|
1205
1256
|
let value;
|
|
1206
1257
|
if (option.name === 'variables') {
|
|
1207
|
-
value = listFromItems(variables);
|
|
1258
|
+
value = listFromItems(variables.map((item) => item.term));
|
|
1208
1259
|
} else if (option.name === 'variable_names') {
|
|
1209
1260
|
value = listFromItems(variables
|
|
1210
|
-
.filter((item) => !item.
|
|
1211
|
-
.map((item) => compound('=', [atom(item.
|
|
1261
|
+
.filter((item) => !item.anonymous)
|
|
1262
|
+
.map((item) => compound('=', [atom(item.sourceName), item.term])));
|
|
1212
1263
|
} else if (option.name === 'singletons') {
|
|
1213
1264
|
value = listFromItems(variables
|
|
1214
|
-
.filter((item) => !item.
|
|
1215
|
-
.map((item) => compound('=', [atom(item.
|
|
1265
|
+
.filter((item) => !item.anonymous && item.count === 1)
|
|
1266
|
+
.map((item) => compound('=', [atom(item.sourceName), item.term])));
|
|
1216
1267
|
} else {
|
|
1217
1268
|
throw new PrologError('domain_error(read_option)', option);
|
|
1218
1269
|
}
|
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/term.js
CHANGED
|
@@ -413,9 +413,26 @@ function writeList(term, env, options) {
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
export function termToString(term, env = new Env(), quoteStrings = true, options = {}) {
|
|
416
|
-
options = {
|
|
416
|
+
options = {
|
|
417
|
+
...options,
|
|
418
|
+
doubleQuotes: options.doubleQuotes ?? 'chars',
|
|
419
|
+
readVariableNames: options.readVariableNames instanceof Map ? options.readVariableNames : new Map(),
|
|
420
|
+
usedReadVariableNames: options.usedReadVariableNames instanceof Set ? options.usedReadVariableNames : new Set(),
|
|
421
|
+
};
|
|
417
422
|
const resolved = deref(term, env);
|
|
418
|
-
if (resolved.type === VAR)
|
|
423
|
+
if (resolved.type === VAR) {
|
|
424
|
+
if (resolved.displayName == null) return writeVariable(resolved.name);
|
|
425
|
+
let printed = options.readVariableNames.get(resolved.name);
|
|
426
|
+
if (printed == null) {
|
|
427
|
+
const base = writeVariable(resolved.displayName);
|
|
428
|
+
printed = base;
|
|
429
|
+
let suffix = 1;
|
|
430
|
+
while (options.usedReadVariableNames.has(printed)) printed = `${base}_${suffix++}`;
|
|
431
|
+
options.readVariableNames.set(resolved.name, printed);
|
|
432
|
+
options.usedReadVariableNames.add(printed);
|
|
433
|
+
}
|
|
434
|
+
return printed;
|
|
435
|
+
}
|
|
419
436
|
if (isCons(resolved)) return writeList(resolved, env, options);
|
|
420
437
|
if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
|
|
421
438
|
if (resolved.type === ATOM) return writeAtom(resolved.name);
|