eyeprolog 1.6.40 → 1.6.41
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 +1 -1
- package/src/check-proof.js +30 -3
- package/src/explain.js +36 -4
- package/src/parser.js +14 -5
- package/src/program.js +21 -8
- package/src/result-format.js +24 -3
- package/src/write.js +11 -3
- package/test/regression/cases-regression.mjs +34 -0
package/package.json
CHANGED
package/src/check-proof.js
CHANGED
|
@@ -152,8 +152,19 @@ function isAtom(term, name) {
|
|
|
152
152
|
return term?.type === ATOM && term.name === name;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// While a proof is checked, each term is spelled once: a step's conclusion is
|
|
156
|
+
// also the use of the step that rests on it, and the coverage, relevance and
|
|
157
|
+
// well-foundedness checks each look it up.
|
|
158
|
+
let keyMemo = null;
|
|
159
|
+
|
|
155
160
|
function key(term) {
|
|
156
|
-
return termToString(term, new Env(), true);
|
|
161
|
+
if (keyMemo == null || term === null || typeof term !== 'object') return termToString(term, new Env(), true);
|
|
162
|
+
let spelled = keyMemo.get(term);
|
|
163
|
+
if (spelled === undefined) {
|
|
164
|
+
spelled = termToString(term, new Env(), true);
|
|
165
|
+
keyMemo.set(term, spelled);
|
|
166
|
+
}
|
|
167
|
+
return spelled;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
// A term is identical to another when it needs no further instantiation to
|
|
@@ -455,7 +466,17 @@ export function checkProofDocument(program, text, options = {}) {
|
|
|
455
466
|
// its own proofs this way before writing them (SPEC.md, Section 7.1); that the
|
|
456
467
|
// text it writes reads back as the same structures is what checking the
|
|
457
468
|
// packaged documents establishes.
|
|
458
|
-
export function checkProof(program,
|
|
469
|
+
export function checkProof(program, document, options = {}, failures = []) {
|
|
470
|
+
const outer = keyMemo;
|
|
471
|
+
keyMemo ??= new WeakMap();
|
|
472
|
+
try {
|
|
473
|
+
return checkProofWithKeys(program, document, options, failures);
|
|
474
|
+
} finally {
|
|
475
|
+
keyMemo = outer;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function checkProofWithKeys(program, { claims, records = [], steps: allSteps }, options, failures) {
|
|
459
480
|
const writeOptions = {
|
|
460
481
|
doubleQuotes: program?.doubleQuotes ?? 'chars', doubleBar: true, quoted: true,
|
|
461
482
|
operators: [...(program?.operators?.values() ?? [])],
|
|
@@ -472,7 +493,13 @@ export function checkProof(program, { claims, records = [], steps: allSteps }, o
|
|
|
472
493
|
for (const record of records) {
|
|
473
494
|
const number = record.args[0]?.type === NUMBER ? Number(record.args[0].name) : NaN;
|
|
474
495
|
const clause = Number.isInteger(number) ? numbering.get(number) : null;
|
|
475
|
-
if (!clause
|
|
496
|
+
if (!clause) {
|
|
497
|
+
fail('C1', 'clause record differs from source', record);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// A record that is the rebuilt record itself restates the clause.
|
|
501
|
+
const rebuilt = clauseRecordTerm(number, clause);
|
|
502
|
+
if (record !== rebuilt && !identical(normalizedClauseRecord(record), normalizedClauseRecord(rebuilt))) {
|
|
476
503
|
fail('C1', 'clause record differs from source', record);
|
|
477
504
|
}
|
|
478
505
|
}
|
package/src/explain.js
CHANGED
|
@@ -1009,6 +1009,7 @@ function resolvedSubstitutions(substitutions, env) {
|
|
|
1009
1009
|
// would displace the real clause and every step citing it would stop
|
|
1010
1010
|
// resolving. Such a declaration is never what a rule(N) or fact(N) cites.
|
|
1011
1011
|
function isUnnumberedDeclaration(clause) {
|
|
1012
|
+
if (clause?.compactBinary === true) return false;
|
|
1012
1013
|
const head = clause?.head;
|
|
1013
1014
|
if (head?.type !== COMPOUND || head.name !== ':-' || head.arity !== 1) return false;
|
|
1014
1015
|
const directive = head.args[0];
|
|
@@ -1016,7 +1017,36 @@ function isUnnumberedDeclaration(clause) {
|
|
|
1016
1017
|
return ['module', 'use_module', 'meta_predicate', 'attribute'].includes(directive.name);
|
|
1017
1018
|
}
|
|
1018
1019
|
|
|
1020
|
+
// A Map from small positive integers, kept in an array.
|
|
1021
|
+
class NumberedTable {
|
|
1022
|
+
constructor() {
|
|
1023
|
+
this.items = [];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
get(number) {
|
|
1027
|
+
return Number.isInteger(number) && number >= 0 ? this.items[number] : undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
set(number, value) {
|
|
1031
|
+
this.items[number] = value;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// A proof is numbered when it is flattened and again when it is checked, and
|
|
1036
|
+
// both see the same program; the numbering is kept until the program's
|
|
1037
|
+
// clauses change.
|
|
1038
|
+
const numberings = new WeakMap();
|
|
1039
|
+
|
|
1019
1040
|
export function clauseNumbering(program) {
|
|
1041
|
+
const clauses = program.clauses ?? [];
|
|
1042
|
+
const cached = numberings.get(program);
|
|
1043
|
+
if (cached && cached.clauses === clauses && cached.length === clauses.length) return cached.numbering;
|
|
1044
|
+
const numbering = computeClauseNumbering(program);
|
|
1045
|
+
numberings.set(program, { clauses, length: clauses.length, numbering });
|
|
1046
|
+
return numbering;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function computeClauseNumbering(program) {
|
|
1020
1050
|
const spans = new Map();
|
|
1021
1051
|
for (const clause of program.clauses ?? []) {
|
|
1022
1052
|
if (!isProgramClause(clause)) continue;
|
|
@@ -1032,17 +1062,19 @@ export function clauseNumbering(program) {
|
|
|
1032
1062
|
|
|
1033
1063
|
// Keyed by file, then by clause position: one string key per clause cost
|
|
1034
1064
|
// more than the numbering itself on programs with hundreds of thousands of
|
|
1035
|
-
// clauses.
|
|
1065
|
+
// clauses. Positions and numbers count up from 1, so arrays hold them.
|
|
1036
1066
|
const bySource = new Map();
|
|
1037
|
-
const byNumber = new
|
|
1067
|
+
const byNumber = new NumberedTable();
|
|
1038
1068
|
for (const clause of program.clauses ?? []) {
|
|
1039
1069
|
if (!isProgramClause(clause) || isUnnumberedDeclaration(clause)) continue;
|
|
1040
1070
|
const { filename, clause: position } = clause.source;
|
|
1041
1071
|
const number = (offsets.get(filename) ?? 0) + position;
|
|
1042
1072
|
let numbers = bySource.get(filename);
|
|
1043
|
-
if (!numbers) bySource.set(filename, numbers = new
|
|
1073
|
+
if (!numbers) bySource.set(filename, numbers = new NumberedTable());
|
|
1044
1074
|
numbers.set(position, number);
|
|
1045
|
-
|
|
1075
|
+
// The clause itself, not a copy of its head and body: a compact clause
|
|
1076
|
+
// builds those terms only when a proof cites it.
|
|
1077
|
+
byNumber.set(number, clause.compactBinary === true || clause.body != null ? clause : { head: clause.head, body: [] });
|
|
1046
1078
|
}
|
|
1047
1079
|
return { bySource, byNumber };
|
|
1048
1080
|
}
|
package/src/parser.js
CHANGED
|
@@ -1504,8 +1504,8 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1504
1504
|
let operatorState = null;
|
|
1505
1505
|
// With source metadata, this path numbers clauses exactly as the general
|
|
1506
1506
|
// parser does: one number per clause and per counted directive, none for a
|
|
1507
|
-
// query or a quad.
|
|
1508
|
-
//
|
|
1507
|
+
// query or a quad. A compact binary clause is numbered the same way, through
|
|
1508
|
+
// the clause object its builder hands back.
|
|
1509
1509
|
const trackSource = options.sourceMetadata === true;
|
|
1510
1510
|
const sourceFilename = options.filename ?? '<input>';
|
|
1511
1511
|
let clauseNumber = 0;
|
|
@@ -1774,18 +1774,20 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1774
1774
|
if (rule < 0) {
|
|
1775
1775
|
if (!parseBinaryRawRange(text, start, termEnd, rawHead)) return false;
|
|
1776
1776
|
reportFastBinarySingletonWarnings(rawHead, null, line);
|
|
1777
|
-
emitBinary(rawHead.name,
|
|
1777
|
+
const clause = emitBinary(rawHead.name,
|
|
1778
1778
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1779
1779
|
null, null, null, null, null);
|
|
1780
|
+
if (clause) numbered(clause, line);
|
|
1780
1781
|
return true;
|
|
1781
1782
|
}
|
|
1782
1783
|
if (findRuleInRange(text, rule + 2, termEnd) >= 0 ||
|
|
1783
1784
|
!parseBinaryRawRange(text, start, rule, rawHead) ||
|
|
1784
1785
|
!parseBinaryRawRange(text, rule + 2, termEnd, rawBody)) return false;
|
|
1785
1786
|
reportFastBinarySingletonWarnings(rawHead, rawBody, line);
|
|
1786
|
-
emitBinary(rawHead.name,
|
|
1787
|
+
const clause = emitBinary(rawHead.name,
|
|
1787
1788
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1788
1789
|
rawBody.name, rawBody.arg0Type, rawBody.arg0Name, rawBody.arg1Type, rawBody.arg1Name);
|
|
1790
|
+
if (clause) numbered(clause, line);
|
|
1789
1791
|
return true;
|
|
1790
1792
|
};
|
|
1791
1793
|
|
|
@@ -1851,12 +1853,14 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1851
1853
|
// subsequent source character must pass through the full tokenizer. The
|
|
1852
1854
|
// compact parser deliberately operates on raw source ranges, so using it
|
|
1853
1855
|
// here would silently bypass Convc for otherwise simple clauses.
|
|
1856
|
+
const anonymousBefore = anonymous;
|
|
1854
1857
|
const simple = preparationConversionActive() ? null : parseSimple(text);
|
|
1855
1858
|
if (simple) {
|
|
1856
1859
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', sourceLine);
|
|
1857
1860
|
accept(numbered(simple, sourceLine));
|
|
1858
1861
|
return true;
|
|
1859
1862
|
}
|
|
1863
|
+
anonymous = anonymousBefore;
|
|
1860
1864
|
try {
|
|
1861
1865
|
const nestedWarning = typeof options.onWarning === 'function'
|
|
1862
1866
|
? (warning) => options.onWarning({ ...warning, line: warning.line + sourceLine - 1 })
|
|
@@ -1912,14 +1916,19 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1912
1916
|
contentEnd = trimRangeEnd(source, contentStart, contentEnd);
|
|
1913
1917
|
if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
|
|
1914
1918
|
if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
|
|
1915
|
-
|
|
1919
|
+
// A reader that declines a clause may have named some of its
|
|
1920
|
+
// anonymous variables already; the next one starts the count again.
|
|
1921
|
+
const anonymousBefore = anonymous;
|
|
1922
|
+
if (emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1916
1923
|
// The program builder accepted a compact binary clause directly.
|
|
1917
1924
|
} else {
|
|
1925
|
+
anonymous = anonymousBefore;
|
|
1918
1926
|
const simple = parseFastRange(source, contentStart, contentEnd);
|
|
1919
1927
|
if (simple) {
|
|
1920
1928
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', lineNumber);
|
|
1921
1929
|
accept(numbered(simple, lineNumber));
|
|
1922
1930
|
} else {
|
|
1931
|
+
anonymous = anonymousBefore;
|
|
1923
1932
|
chunkStartLine = lineNumber;
|
|
1924
1933
|
chunk = source.slice(lineStart, lineEnd) + '\n';
|
|
1925
1934
|
if (!flush()) return null;
|
package/src/program.js
CHANGED
|
@@ -1269,18 +1269,27 @@ function collectAutoloadClosureDependencies(closure, extraArguments, out, module
|
|
|
1269
1269
|
|
|
1270
1270
|
function expandAutoloadMetaDependencies(program, dependencies, module = 'user') {
|
|
1271
1271
|
const seen = new WeakMap();
|
|
1272
|
+
// Most dependencies name a predicate without closure arguments; look each
|
|
1273
|
+
// predicate up once instead of once per call site.
|
|
1274
|
+
const closureModes = new Map();
|
|
1272
1275
|
for (let i = 0; i < dependencies.length; i++) {
|
|
1273
1276
|
const dependency = dependencies[i];
|
|
1274
1277
|
if (dependency.goal == null) continue;
|
|
1275
1278
|
const caller = dependency.module ?? module;
|
|
1276
1279
|
const visitKey = `${caller}\u0000${dependency.key}`;
|
|
1280
|
+
let modes = closureModes.get(visitKey);
|
|
1281
|
+
if (modes === undefined) {
|
|
1282
|
+
const group = program.findGroup(dependency.name, dependency.arity, caller);
|
|
1283
|
+
modes = (group?.metaArgumentModes ?? []).filter((mode) =>
|
|
1284
|
+
mode.kind === 'closure' && Number.isSafeInteger(mode.extraArguments) && mode.extraArguments >= 0);
|
|
1285
|
+
closureModes.set(visitKey, modes);
|
|
1286
|
+
}
|
|
1287
|
+
if (modes.length === 0) continue;
|
|
1277
1288
|
const visits = seen.get(dependency.goal) ?? new Set();
|
|
1278
1289
|
if (visits.has(visitKey)) continue;
|
|
1279
1290
|
visits.add(visitKey);
|
|
1280
1291
|
seen.set(dependency.goal, visits);
|
|
1281
|
-
const
|
|
1282
|
-
for (const mode of group?.metaArgumentModes ?? []) {
|
|
1283
|
-
if (mode.kind !== 'closure' || !Number.isSafeInteger(mode.extraArguments) || mode.extraArguments < 0) continue;
|
|
1292
|
+
for (const mode of modes) {
|
|
1284
1293
|
collectAutoloadClosureDependencies(dependency.goal?.args[mode.index], mode.extraArguments, dependencies, caller);
|
|
1285
1294
|
}
|
|
1286
1295
|
}
|
|
@@ -1716,13 +1725,17 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1716
1725
|
if (fast) {
|
|
1717
1726
|
const acceptBinary = (headName, head0Type, head0Name, head1Type, head1Name,
|
|
1718
1727
|
bodyName, body0Type, body0Name, body1Type, body1Name) => {
|
|
1719
|
-
batch.
|
|
1728
|
+
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
1729
|
+
const clause = new CompactBinaryClause(
|
|
1720
1730
|
headName, head0Type, head0Name, head1Type, head1Name,
|
|
1721
1731
|
bodyName, body0Type, body0Name, body1Type, body1Name,
|
|
1722
|
-
)
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1732
|
+
);
|
|
1733
|
+
clause.module = context.module;
|
|
1734
|
+
clause.textUnit = context.textUnit;
|
|
1735
|
+
batch.push(clause);
|
|
1736
|
+
// The parser numbers the clause for its source metadata once it is
|
|
1737
|
+
// returned, so the batch is flushed at the next clause, not here.
|
|
1738
|
+
return clause;
|
|
1726
1739
|
};
|
|
1727
1740
|
const parsed = tryParseClausesFastInto(source, processClause, acceptBinary, options);
|
|
1728
1741
|
if (parsed) flush();
|
package/src/result-format.js
CHANGED
|
@@ -158,11 +158,24 @@ function renameMinted(value, minted) {
|
|
|
158
158
|
if (!minted.has(key)) minted.set(key, minted.size + 1);
|
|
159
159
|
return compound('$stream', [numberTerm(BigInt(minted.get(key)))]);
|
|
160
160
|
}
|
|
161
|
-
return
|
|
161
|
+
return sameOrRebuilt(value, (arg) => renameMinted(arg, minted));
|
|
162
162
|
}
|
|
163
163
|
return value;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// A compound with `rewrite` applied to its arguments: the compound itself
|
|
167
|
+
// when no argument changes, so ground subterms are shared, not copied.
|
|
168
|
+
function sameOrRebuilt(value, rewrite) {
|
|
169
|
+
let args = null;
|
|
170
|
+
for (let i = 0; i < value.args.length; i++) {
|
|
171
|
+
const arg = value.args[i];
|
|
172
|
+
const rewritten = rewrite(arg);
|
|
173
|
+
if (args == null && rewritten !== arg) args = value.args.slice(0, i);
|
|
174
|
+
if (args != null) args.push(rewritten);
|
|
175
|
+
}
|
|
176
|
+
return args == null ? value : compound(value.name, args);
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
function templateTerm(value, anonymous, minted) {
|
|
167
180
|
if (value.type === VAR) {
|
|
168
181
|
if (value.name !== '_' && !value.name.startsWith('_')) {
|
|
@@ -172,7 +185,7 @@ function templateTerm(value, anonymous, minted) {
|
|
|
172
185
|
if (!anonymous.has(value.name)) anonymous.set(value.name, anonymous.size + 1);
|
|
173
186
|
return compound('anonymous', [numberTerm(BigInt(anonymous.get(value.name)))]);
|
|
174
187
|
}
|
|
175
|
-
if (value.type === COMPOUND) return
|
|
188
|
+
if (value.type === COMPOUND) return sameOrRebuilt(value, (arg) => templateTerm(arg, anonymous, minted));
|
|
176
189
|
return value;
|
|
177
190
|
}
|
|
178
191
|
|
|
@@ -192,8 +205,16 @@ function clauseTerm(number, clause, minted) {
|
|
|
192
205
|
|
|
193
206
|
// The `clause/3` record a proof writes for clause `number`, which a checker
|
|
194
207
|
// rebuilds from the source to compare with the document's (C1).
|
|
208
|
+
// The record is kept with the clause, so a proof checked as it is written
|
|
209
|
+
// compares its records with the very terms the checker rebuilds.
|
|
210
|
+
const clauseRecords = new WeakMap();
|
|
211
|
+
|
|
195
212
|
export function clauseRecordTerm(number, clause) {
|
|
196
|
-
|
|
213
|
+
const cached = clauseRecords.get(clause);
|
|
214
|
+
if (cached?.number === number) return cached.record;
|
|
215
|
+
const record = clauseTerm(number, clause, new Map());
|
|
216
|
+
clauseRecords.set(clause, { number, record });
|
|
217
|
+
return record;
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
// A `clause/3` record with its minted variable names renumbered from the
|
package/src/write.js
CHANGED
|
@@ -250,7 +250,11 @@ function printableReadVariableNames(term, env, explicit) {
|
|
|
250
250
|
const used = new Set(names.values());
|
|
251
251
|
const suffixes = new Map();
|
|
252
252
|
const seenVariables = new Set();
|
|
253
|
-
|
|
253
|
+
// A subterm shared many times over is walked once. Most terms are small
|
|
254
|
+
// trees, where walking a subterm again changes nothing, so the set is only
|
|
255
|
+
// kept once a term turns out to be large.
|
|
256
|
+
let seenTerms = null;
|
|
257
|
+
let walked = 0;
|
|
254
258
|
const stack = [term];
|
|
255
259
|
|
|
256
260
|
while (stack.length) {
|
|
@@ -268,8 +272,12 @@ function printableReadVariableNames(term, env, explicit) {
|
|
|
268
272
|
used.add(candidate);
|
|
269
273
|
continue;
|
|
270
274
|
}
|
|
271
|
-
if (current.type !== COMPOUND
|
|
272
|
-
seenTerms
|
|
275
|
+
if (current.type !== COMPOUND) continue;
|
|
276
|
+
if (seenTerms == null && ++walked > 4096) seenTerms = new Set();
|
|
277
|
+
if (seenTerms != null) {
|
|
278
|
+
if (seenTerms.has(current)) continue;
|
|
279
|
+
seenTerms.add(current);
|
|
280
|
+
}
|
|
273
281
|
for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
274
282
|
}
|
|
275
283
|
|
|
@@ -6025,6 +6025,40 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
|
6025
6025
|
}
|
|
6026
6026
|
},
|
|
6027
6027
|
},
|
|
6028
|
+
{
|
|
6029
|
+
name: 'with source metadata, compact binary clauses are numbered and named exactly as the general parser reads them',
|
|
6030
|
+
run: () => {
|
|
6031
|
+
// A proof cites clauses by the numbers source metadata gives them, so
|
|
6032
|
+
// a program loaded through the compact binary-clause path for --proof
|
|
6033
|
+
// must carry the numbers, lines and anonymous-variable names the
|
|
6034
|
+
// general parser gives the same text -- including after a clause the
|
|
6035
|
+
// compact path reads part of and then declines.
|
|
6036
|
+
const source = [
|
|
6037
|
+
'a(ind, n0).',
|
|
6038
|
+
'a(X, n1) :- a(X, n0).',
|
|
6039
|
+
'b(_, _).',
|
|
6040
|
+
'c(X, Y) :- a(X, _), b(Y, _).',
|
|
6041
|
+
'a(_, n2) :- a(_, n1).',
|
|
6042
|
+
'd(f(_), _).',
|
|
6043
|
+
'p(_, x) :- q(f(_), x).',
|
|
6044
|
+
'e(X, _) :- a(X, n2).',
|
|
6045
|
+
'',
|
|
6046
|
+
].join('\n');
|
|
6047
|
+
// Variables by their internal names, which a printed term hides.
|
|
6048
|
+
const named = (term) => term.type === 'var'
|
|
6049
|
+
? term.name
|
|
6050
|
+
: term.type === 'compound' ? `${term.name}(${term.args.map(named).join(',')})` : String(term.name);
|
|
6051
|
+
const spelled = (clause) => [
|
|
6052
|
+
clause.source?.line,
|
|
6053
|
+
clause.source?.clause,
|
|
6054
|
+
named(clause.head),
|
|
6055
|
+
(clause.body ?? []).map(named).join(', '),
|
|
6056
|
+
].join(' | ');
|
|
6057
|
+
const loaded = Program.parse(source, { sourceMetadata: true }).clauses.map(spelled);
|
|
6058
|
+
const general = parseProgramText(source, { sourceMetadata: true, generalParser: true }).map(spelled);
|
|
6059
|
+
assertEqual(loaded.join('\n'), general.join('\n'), 'the loaded clauses');
|
|
6060
|
+
},
|
|
6061
|
+
},
|
|
6028
6062
|
{
|
|
6029
6063
|
// https://github.com/eyereasoner/eyeprolog/issues/114#issuecomment-5663598188
|
|
6030
6064
|
name: 'a genuine IEEE-754 negative zero produced by arithmetic (not just parsed from text) still prints as 0.0',
|