eyeprolog 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1063,16 +1063,36 @@ function isTerminatingFullStop(source, index) {
|
|
|
1063
1063
|
return true;
|
|
1064
1064
|
}
|
|
1065
1065
|
|
|
1066
|
+
function quotedEscapeEnd(source, index) {
|
|
1067
|
+
const escaped = source[index + 1] ?? '';
|
|
1068
|
+
if (!escaped) return index;
|
|
1069
|
+
|
|
1070
|
+
// ISO 6.4.2.1 numeric escapes include a terminating backslash. Consume
|
|
1071
|
+
// that delimiter as part of the quoted character so stream scanning does
|
|
1072
|
+
// not mistake it for an escape of the quote which follows.
|
|
1073
|
+
if (escaped === 'x') {
|
|
1074
|
+
let cursor = index + 2;
|
|
1075
|
+
while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
|
|
1076
|
+
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
1077
|
+
}
|
|
1078
|
+
if (/^[0-7]$/.test(escaped)) {
|
|
1079
|
+
let cursor = index + 1;
|
|
1080
|
+
while (/^[0-7]$/.test(source[cursor] ?? '')) cursor++;
|
|
1081
|
+
return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return index + 1;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1066
1087
|
function* termTextCandidates(stream) {
|
|
1067
1088
|
const source = String(stream.content);
|
|
1068
|
-
let quote = null,
|
|
1089
|
+
let quote = null, lineComment = false, blockComment = false;
|
|
1069
1090
|
for (let i = stream.position; i < source.length; i++) {
|
|
1070
1091
|
const ch = source[i], next = source[i + 1];
|
|
1071
1092
|
if (lineComment) { if (ch === '\n') lineComment = false; continue; }
|
|
1072
1093
|
if (blockComment) { if (ch === '*' && next === '/') { blockComment = false; i++; } continue; }
|
|
1073
1094
|
if (quote) {
|
|
1074
|
-
if (
|
|
1075
|
-
else if (ch === '\\') escaped = true;
|
|
1095
|
+
if (ch === '\\') i = quotedEscapeEnd(source, i);
|
|
1076
1096
|
else if (ch === quote && next === quote) i++;
|
|
1077
1097
|
else if (ch === quote) quote = null;
|
|
1078
1098
|
continue;
|
|
@@ -1087,13 +1107,21 @@ function* termTextCandidates(stream) {
|
|
|
1087
1107
|
}
|
|
1088
1108
|
function convertedTermText(text, solver) {
|
|
1089
1109
|
if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
|
|
1090
|
-
let result = '', quote = null
|
|
1091
|
-
for (
|
|
1110
|
+
let result = '', quote = null;
|
|
1111
|
+
for (let i = 0; i < text.length; i++) {
|
|
1112
|
+
const ch = text[i], next = text[i + 1];
|
|
1092
1113
|
if (quote) {
|
|
1093
1114
|
result += ch;
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1115
|
+
if (ch === '\\') {
|
|
1116
|
+
const end = quotedEscapeEnd(text, i);
|
|
1117
|
+
if (end > i) result += text.slice(i + 1, end + 1);
|
|
1118
|
+
i = end;
|
|
1119
|
+
} else if (ch === quote && next === quote) {
|
|
1120
|
+
result += next;
|
|
1121
|
+
i++;
|
|
1122
|
+
} else if (ch === quote) {
|
|
1123
|
+
quote = null;
|
|
1124
|
+
}
|
|
1097
1125
|
} else if (ch === "'" || ch === '"') {
|
|
1098
1126
|
quote = ch;
|
|
1099
1127
|
result += ch;
|
package/src/parser.js
CHANGED
|
@@ -663,8 +663,7 @@ class Parser {
|
|
|
663
663
|
}
|
|
664
664
|
return this.source[start] === ' ' || this.source[start] === '\t';
|
|
665
665
|
}
|
|
666
|
-
|
|
667
|
-
const query = this.parseTerm(0, true);
|
|
666
|
+
parseQuadAnswers(id, query, line, accept) {
|
|
668
667
|
this.expect(TOK.DOT, '.');
|
|
669
668
|
this.advance();
|
|
670
669
|
|
|
@@ -684,28 +683,38 @@ class Parser {
|
|
|
684
683
|
source: { filename: this.filename, line },
|
|
685
684
|
});
|
|
686
685
|
}
|
|
686
|
+
parseQuad(id, line, accept) {
|
|
687
|
+
const query = this.parseTerm(0, true);
|
|
688
|
+
this.parseQuadAnswers(id, query, line, accept);
|
|
689
|
+
}
|
|
690
|
+
parseQuadTerm(term, line, accept) {
|
|
691
|
+
if (term.type !== COMPOUND || term.name !== '?-' || ![1, 2].includes(term.arity)) return false;
|
|
692
|
+
const id = term.arity === 2 ? term.args[0] : null;
|
|
693
|
+
const query = term.args[term.arity - 1];
|
|
694
|
+
this.parseQuadAnswers(id, query, line, accept);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
687
697
|
parseProgram(emit = null) {
|
|
688
698
|
const clauses = emit ? null : [];
|
|
689
699
|
let clauseNumber = 0;
|
|
690
700
|
const accept = emit ?? ((clause) => clauses.push(clause));
|
|
691
701
|
while (this.token.type !== TOK.EOF) {
|
|
692
702
|
const line = this.token.line;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
703
|
+
// In the normal EyeProlog profile, canonical functional ?-/1 and
|
|
704
|
+
// ?-/2 notation denotes the same quad marker as the corresponding
|
|
705
|
+
// operator notation. Keep the existing operator-form query parsing so
|
|
706
|
+
// a query containing comma remains wholly to the right of `?-`, while
|
|
707
|
+
// functional notation is parsed as a term and then decomposed by arity.
|
|
708
|
+
if (this.operatorTokenName() === '?-' && !this.strictIso) {
|
|
709
|
+
if (this.peek() === '(') {
|
|
710
|
+
const quadTerm = this.parseTerm(0, true);
|
|
711
|
+
if (!this.parseQuadTerm(quadTerm, line, accept)) {
|
|
712
|
+
throw new Error(`parse line ${line}: bad quad term`);
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
700
715
|
this.advance();
|
|
701
|
-
|
|
702
|
-
clauseNumber++;
|
|
703
|
-
if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauseNumber };
|
|
704
|
-
accept(clause);
|
|
705
|
-
continue;
|
|
716
|
+
this.parseQuad(null, line, accept);
|
|
706
717
|
}
|
|
707
|
-
this.advance();
|
|
708
|
-
this.parseQuad(null, line, accept);
|
|
709
718
|
continue;
|
|
710
719
|
}
|
|
711
720
|
if (this.token.type === TOK.IF) {
|
|
@@ -740,6 +749,13 @@ class Parser {
|
|
|
740
749
|
continue;
|
|
741
750
|
}
|
|
742
751
|
let head = this.parseTerm(3);
|
|
752
|
+
// Both a quad label and a TS 13211-3 semicontext may contain an
|
|
753
|
+
// unparenthesized comma before their priority-1200 operator. Assemble
|
|
754
|
+
// that left operand before deciding whether the marker is ?- or -->.
|
|
755
|
+
if (this.token.type === TOK.COMMA) {
|
|
756
|
+
this.advance();
|
|
757
|
+
head = compound(',', [head, this.parseTerm(3)]);
|
|
758
|
+
}
|
|
743
759
|
if (this.operatorTokenName() === '?-') {
|
|
744
760
|
if (this.strictIso) {
|
|
745
761
|
// There is no predefined infix ?-/2 in strict core mode. If a
|
|
@@ -761,13 +777,6 @@ class Parser {
|
|
|
761
777
|
this.parseQuad(head, line, accept);
|
|
762
778
|
continue;
|
|
763
779
|
}
|
|
764
|
-
// ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
|
|
765
|
-
// priority as clauses. The left side may include an unparenthesized
|
|
766
|
-
// semicontext: NonTerminal, Terminals --> Body.
|
|
767
|
-
if (this.token.type === TOK.COMMA) {
|
|
768
|
-
this.advance();
|
|
769
|
-
head = compound(',', [head, this.parseTerm(3)]);
|
|
770
|
-
}
|
|
771
780
|
if (this.operatorTokenName() === '-->') {
|
|
772
781
|
this.advance();
|
|
773
782
|
const grammarBody = this.parseTerm(0, true);
|
|
@@ -65,3 +65,23 @@ default_streams(ok) :-
|
|
|
65
65
|
standard_write(ok) :-
|
|
66
66
|
write(io_marker),
|
|
67
67
|
nl.
|
|
68
|
+
|
|
69
|
+
%% goal: numeric_escape_term_input(ok)
|
|
70
|
+
|
|
71
|
+
numeric_escape_term_input(ok) :-
|
|
72
|
+
open('/tmp/eyeprolog-iso-read-escapes.txt', write, Output, []),
|
|
73
|
+
put_code(Output, 39), put_code(Output, 92), put_code(Output, 55),
|
|
74
|
+
put_code(Output, 92), put_code(Output, 39), put_code(Output, 46), nl(Output),
|
|
75
|
+
put_code(Output, 39), put_code(Output, 92), put_code(Output, 120), put_code(Output, 55),
|
|
76
|
+
put_code(Output, 92), put_code(Output, 39), put_code(Output, 46), nl(Output),
|
|
77
|
+
close(Output),
|
|
78
|
+
current_input(Old),
|
|
79
|
+
open('/tmp/eyeprolog-iso-read-escapes.txt', read, Input, []),
|
|
80
|
+
set_input(Input),
|
|
81
|
+
read(A),
|
|
82
|
+
read_term(B, []),
|
|
83
|
+
set_input(Old),
|
|
84
|
+
close(Input),
|
|
85
|
+
A == '\a',
|
|
86
|
+
B == '\a'.
|
|
87
|
+
|
package/test/run-regression.mjs
CHANGED
|
@@ -221,6 +221,43 @@ why(
|
|
|
221
221
|
assertEqual(program.quads[1].query.args[1].name, '.', 'dot atom');
|
|
222
222
|
},
|
|
223
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: 'quad parser treats operator and functional ?- notation equivalently',
|
|
226
|
+
run: () => {
|
|
227
|
+
const labelled = Program.parse(
|
|
228
|
+
`0,passes
|
|
229
|
+
?- X = 1.
|
|
230
|
+
X = 1.
|
|
231
|
+
`,
|
|
232
|
+
);
|
|
233
|
+
assertEqual(labelled.quads.length, 1, 'labelled quad count');
|
|
234
|
+
assertEqual(labelled.clauses.length, 0, 'labelled quad clause count');
|
|
235
|
+
assertEqual(labelled.quads[0].id.name, ',', 'comma label functor');
|
|
236
|
+
assertEqual(labelled.quads[0].query.name, '=', 'labelled quad query');
|
|
237
|
+
const labelledReport = publicApi.runQuads(labelled);
|
|
238
|
+
assertEqual(labelledReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'labelled quad report');
|
|
239
|
+
|
|
240
|
+
const functional = Program.parse(
|
|
241
|
+
`?-(','(0,passes),=(X,1)).
|
|
242
|
+
X = 1.
|
|
243
|
+
`,
|
|
244
|
+
);
|
|
245
|
+
assertEqual(functional.quads.length, 1, 'functional quad count');
|
|
246
|
+
assertEqual(functional.clauses.length, 0, 'functional quad clause count');
|
|
247
|
+
assertEqual(functional.quads[0].id.name, ',', 'functional comma label functor');
|
|
248
|
+
assertEqual(functional.quads[0].query.name, '=', 'functional quad query');
|
|
249
|
+
assertEqual(termToString(functional.quads[0].id), termToString(labelled.quads[0].id),
|
|
250
|
+
'operator and functional labels are equivalent');
|
|
251
|
+
assertEqual(termToString(functional.quads[0].query), termToString(labelled.quads[0].query),
|
|
252
|
+
'operator and functional queries are equivalent');
|
|
253
|
+
const functionalReport = publicApi.runQuads(functional);
|
|
254
|
+
assertEqual(functionalReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'functional quad report');
|
|
255
|
+
|
|
256
|
+
const strict = Program.parse(`?-(','(0,passes),=(X,1)).\n`, { isoStrict: true });
|
|
257
|
+
assertEqual(strict.quads.length, 0, 'strict mode has no quads');
|
|
258
|
+
assertEqual(strict.clauses.length, 1, 'strict functional ?-/2 remains an ordinary term');
|
|
259
|
+
},
|
|
260
|
+
},
|
|
224
261
|
{
|
|
225
262
|
name: 'runQuads checks portable answer descriptions',
|
|
226
263
|
run: () => {
|
|
@@ -1169,6 +1206,44 @@ c4 ?- call((!;1)).
|
|
|
1169
1206
|
assertIncludes(result.stderr, 'implementation-specific directive use_module/1', 'stderr');
|
|
1170
1207
|
},
|
|
1171
1208
|
},
|
|
1209
|
+
{
|
|
1210
|
+
name: 'term input accepts ISO numeric escapes through read predicates',
|
|
1211
|
+
run: () => {
|
|
1212
|
+
const escapePath = path.join(tmp, `read-escapes-${++tmpCounter}.term`);
|
|
1213
|
+
// Two raw read-terms: '\7\'. and '\x7\'. Build the file from
|
|
1214
|
+
// character codes so this regression tests stream parsing rather than
|
|
1215
|
+
// the parser which reads this JavaScript fixture.
|
|
1216
|
+
fs.writeFileSync(escapePath, String.fromCharCode(
|
|
1217
|
+
39, 92, 55, 92, 39, 46, 10,
|
|
1218
|
+
39, 92, 120, 55, 92, 39, 46, 10,
|
|
1219
|
+
));
|
|
1220
|
+
const source = [
|
|
1221
|
+
`read_escapes(A, B) :-`,
|
|
1222
|
+
` current_input(Old),`,
|
|
1223
|
+
` open(${sourceAtom(escapePath)}, read, Input, []),`,
|
|
1224
|
+
` set_input(Input),`,
|
|
1225
|
+
` read(A),`,
|
|
1226
|
+
` read_term(B, []),`,
|
|
1227
|
+
` set_input(Old),`,
|
|
1228
|
+
` close(Input).`,
|
|
1229
|
+
'',
|
|
1230
|
+
].join('\n');
|
|
1231
|
+
assertEqual(
|
|
1232
|
+
run(source, { goal: 'read_escapes(A, B)' }).stdout,
|
|
1233
|
+
"read_escapes('\\a', '\\a').\n",
|
|
1234
|
+
'read/1 and read_term/2 numeric escapes',
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
assertEqual(
|
|
1238
|
+
run('answer(T) :- read(T).\n', {
|
|
1239
|
+
goal: 'answer(T)',
|
|
1240
|
+
ioOptions: { input: "'\\7\\'." },
|
|
1241
|
+
}).stdout,
|
|
1242
|
+
"answer('\\a').\n",
|
|
1243
|
+
'read/1 user_input numeric escape',
|
|
1244
|
+
);
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1172
1247
|
{
|
|
1173
1248
|
name: 'term input keeps dotted operators intact and uses program operators',
|
|
1174
1249
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5146,7 +5146,11 @@ contains `?-` at priority 1200 with specifier `fx`, so
|
|
|
5146
5146
|
an optional label before the query marker (`Label ?- Query.`), so while quad
|
|
5147
5147
|
syntax is supported it additionally exposes `?-` at priority 1200 with
|
|
5148
5148
|
specifier `xfx` as an implementation-specific operator. Consequently
|
|
5149
|
-
`current_op(Priority, Specifier, ?-)` enumerates both definitions.
|
|
5149
|
+
`current_op(Priority, Specifier, ?-)` enumerates both definitions. At top level in the normal EyeProlog profile, the quad marker is recognized
|
|
5150
|
+
after term parsing, so equivalent syntax stays equivalent: `Label ?- Query.`
|
|
5151
|
+
and canonical `?-(Label, Query).` denote the same labelled quad when followed
|
|
5152
|
+
by its indented answer descriptions. In `--iso-strict` mode this quad
|
|
5153
|
+
interpretation is disabled, and `?-/2` remains ordinary Prolog term syntax.
|
|
5150
5154
|
|
|
5151
5155
|
Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
|
|
5152
5156
|
for an explicitly stateful queue and
|
|
@@ -5747,9 +5751,11 @@ the beginning. A peek does not mark the stream as past-end.
|
|
|
5747
5751
|
| `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
|
|
5748
5752
|
| `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. |
|
|
5749
5753
|
|
|
5750
|
-
Term input uses the program's current operator table and
|
|
5751
|
-
|
|
5752
|
-
|
|
5754
|
+
Term input uses the program's current operator table and the same ISO quoted-character
|
|
5755
|
+
syntax as source text, including backslash-terminated octal and hexadecimal
|
|
5756
|
+
escapes such as `'\7\'` and `'\x7\'`. This applies equally to `read/1-2` and
|
|
5757
|
+
`read_term/2-3`. Installed character conversions are applied outside quoted text
|
|
5758
|
+
when the `char_conversion` flag is `on`. `variable_names/1` and `singletons/1` omit anonymous variables. Output
|
|
5753
5759
|
predicates do not append a period or newline; call `write/1`, then `write('.')`
|
|
5754
5760
|
and `nl/0` when emitting a complete source term manually.
|
|
5755
5761
|
|
|
@@ -6361,7 +6367,10 @@ console.log(report.passed, report.failed, report.stdout);
|
|
|
6361
6367
|
The syntax follows the “queries using answer descriptions” convention used by
|
|
6362
6368
|
Trealla and the ISO Prolog working examples. Because answer descriptions are
|
|
6363
6369
|
layout-sensitive, indent every description while keeping ordinary clause heads
|
|
6364
|
-
and the next quad query at the left margin.
|
|
6370
|
+
and the next quad query at the left margin. A comma may be part of a quad label,
|
|
6371
|
+
including across layout before `?-`. Canonical functional notation is
|
|
6372
|
+
semantically equivalent: `?-(Label, Query).` followed by the same indented
|
|
6373
|
+
answer descriptions creates the same labelled quad as `Label ?- Query.`.
|
|
6365
6374
|
|
|
6366
6375
|
Statistics are comparative evidence, not a score in isolation. Preserve the
|
|
6367
6376
|
program, input, runtime version, selected query, answers, and counters together.
|