eyelang 1.4.0 → 1.5.0
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/README.md +22 -43
- package/SPEC.md +16 -16
- package/bin/eyelang +0 -0
- package/conformance/README.md +4 -5
- package/conformance/cases/core/001_fact_output.pl +4 -0
- package/conformance/cases/core/002_rule_recursion.pl +4 -2
- package/conformance/cases/core/003_terms_and_readback.pl +15 -13
- package/conformance/cases/core/004_conjunction_and_parentheses.pl +1 -0
- package/conformance/cases/core/005_list_deconstruction.pl +1 -0
- package/conformance/cases/core/006_comma_formula_data.pl +1 -0
- package/conformance/cases/core/007_anonymous_variables.pl +1 -0
- package/conformance/cases/core/008_graphic_atoms.pl +5 -3
- package/conformance/cases/core/009_comments_and_whitespace.pl +1 -0
- package/conformance/cases/core/010_variable_scope_and_reuse.pl +1 -0
- package/conformance/cases/core/011_predicate_arity.pl +1 -0
- package/conformance/cases/core/012_nested_compound_unification.pl +1 -0
- package/conformance/cases/core/013_multiple_clauses_order.pl +1 -0
- package/conformance/cases/core/014_failure_filters_answers.pl +2 -0
- package/conformance/cases/core/015_improper_list_unification.pl +1 -0
- package/conformance/cases/core/016_zero_arity_compound.pl +1 -0
- package/conformance/cases/core/017_three_step_recursion.pl +1 -0
- package/conformance/cases/core/018_quoted_atom_readback.pl +1 -0
- package/conformance/cases/core/019_parenthesized_three_conjuncts.pl +1 -0
- package/conformance/cases/core/020_nested_list_terms.pl +1 -0
- package/conformance/cases/extension/001_default_derived_output.pl +1 -1
- package/conformance/cases/extension/002_materialize_focus.pl +1 -1
- package/conformance/cases/extension/003_arithmetic_and_comparison.pl +1 -0
- package/conformance/cases/extension/004_strings_and_atoms.pl +1 -0
- package/conformance/cases/extension/005_lists_aggregation_ordering.pl +1 -0
- package/conformance/cases/extension/006_formula_terms.pl +1 -0
- package/conformance/cases/extension/007_negation_once_generators.pl +1 -0
- package/conformance/cases/extension/008_equality_and_inequality.pl +1 -0
- package/conformance/cases/extension/009_list_relations.pl +1 -0
- package/conformance/cases/extension/010_append_splits.pl +1 -0
- package/conformance/cases/extension/011_matching_and_comparison.pl +1 -0
- package/conformance/cases/extension/012_memoize_declaration.pl +5 -3
- package/conformance/cases/extension/013_numeric_functions.pl +1 -0
- package/conformance/cases/extension/014_between_enumeration.pl +1 -0
- package/conformance/cases/extension/015_smallest_divisor.pl +1 -0
- package/conformance/cases/extension/016_negation_filter.pl +1 -0
- package/conformance/cases/extension/017_once_user_predicate.pl +1 -0
- package/conformance/cases/extension/018_findall_user_goal.pl +1 -0
- package/conformance/cases/extension/019_sort_deduplicates_atoms.pl +1 -0
- package/conformance/cases/extension/020_append_bound_prefix_suffix.pl +1 -0
- package/conformance/cases/extension/021_nth0_index_generation.pl +1 -0
- package/conformance/cases/extension/022_set_nth0_edges.pl +1 -0
- package/conformance/cases/extension/023_select_duplicate_occurrences.pl +1 -0
- package/conformance/cases/extension/024_not_member_filter.pl +1 -0
- package/conformance/cases/extension/025_is_list_filter.pl +1 -0
- package/conformance/cases/extension/026_nested_formula_terms.pl +1 -0
- package/conformance/cases/extension/027_materialize_excludes_source_fact.pl +1 -1
- package/conformance/cases/extension/028_numeric_and_lexical_comparison.pl +1 -0
- package/conformance/cases/extension/029_string_matching_filters.pl +1 -0
- package/conformance/cases/extension/030_string_and_atom_concat.pl +1 -0
- package/conformance/cases/extension/031_countall_empty_and_nonempty.pl +2 -0
- package/conformance/cases/extension/032_sumall_numeric_template.pl +2 -0
- package/conformance/cases/extension/033_aggregate_min_template.pl +2 -0
- package/conformance/cases/extension/034_aggregate_max_compound_key.pl +2 -0
- package/conformance/expected/extension/031_countall_empty_and_nonempty.out +1 -1
- package/conformance/expected/extension/032_sumall_numeric_template.out +1 -1
- package/conformance/expected/extension/033_aggregate_min_template.out +1 -1
- package/conformance/expected/extension/034_aggregate_max_compound_key.out +1 -1
- package/examples/basic-monadic.pl +1 -1
- package/examples/monkey-bananas.pl +1 -1
- package/examples/path-discovery.pl +3 -3
- package/examples/peano-arithmetic.pl +1 -1
- package/package.json +1 -1
- package/playground-worker.mjs +2 -15
- package/playground.html +5 -25
- package/src/builtins/aggregation.js +5 -5
- package/src/builtins/control.js +3 -3
- package/src/builtins/registry.js +7 -0
- package/src/cli.js +36 -38
- package/src/index.js +21 -28
- package/src/parser.js +3 -3
- package/src/solver.js +2 -2
- package/test/run-conformance.js +20 -45
- package/test/run-examples.js +42 -50
- package/test/run-regression.js +38 -59
- package/conformance/cases/core/001_fact_query.pl +0 -2
- /package/conformance/expected/core/{001_fact_query.out → 001_fact_output.out} +0 -0
package/src/index.js
CHANGED
|
@@ -1,52 +1,45 @@
|
|
|
1
1
|
// Public JavaScript API surface for embedders and the browser playground.
|
|
2
2
|
// The CLI imports the same parser, program, solver, and term primitives from here.
|
|
3
3
|
export { Program, makeProgram } from './program.js';
|
|
4
|
-
export { parseClauses, parseProgramText
|
|
4
|
+
export { parseClauses, parseProgramText } from './parser.js';
|
|
5
5
|
export { Solver } from './solver.js';
|
|
6
6
|
export * from './term.js';
|
|
7
|
-
export { BuiltinRegistry, createDefaultRegistry } from './builtins/registry.js';
|
|
7
|
+
export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './builtins/registry.js';
|
|
8
8
|
|
|
9
9
|
import { Env, copyResolved, termIsGround, termToString } from './term.js';
|
|
10
10
|
import { Program } from './program.js';
|
|
11
11
|
import { Solver } from './solver.js';
|
|
12
|
-
import { parseQueryGoal } from './parser.js';
|
|
13
12
|
import { whyNoProof, whyProof } from './explain.js';
|
|
13
|
+
import { getDefaultRegistry } from './builtins/registry.js';
|
|
14
14
|
|
|
15
15
|
export function run(source, options = {}) {
|
|
16
16
|
const includeWhy = options.proof === true || options.why === true || options.explain === true;
|
|
17
17
|
const parseOptions = { ...options, sourceMetadata: includeWhy, markRecursive: includeWhy };
|
|
18
18
|
const program = source instanceof Program ? source : Program.parse(source, parseOptions);
|
|
19
|
-
const
|
|
19
|
+
const runOptions = options.registry ? options : { ...options, registry: getDefaultRegistry() };
|
|
20
|
+
const solver = new Solver(program, runOptions);
|
|
20
21
|
const output = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const resolved = copyResolved(goal, env);
|
|
36
|
-
if (!termIsGround(resolved)) continue;
|
|
37
|
-
const line = `${termToString(resolved, new Env(), true)}.\n`;
|
|
38
|
-
if (facts.has(line) || seen.has(line)) continue;
|
|
39
|
-
seen.add(line);
|
|
40
|
-
output.push(line);
|
|
41
|
-
if (includeWhy) appendExplanation(output, program, resolved);
|
|
42
|
-
}
|
|
22
|
+
const goals = program.materializationGoals();
|
|
23
|
+
const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
|
|
24
|
+
const facts = program.sourceFactLines(materializedKeys);
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const goal of goals) {
|
|
27
|
+
const localSolver = new Solver(program, runOptions);
|
|
28
|
+
for (const env of localSolver.solve([goal], new Env(), 0)) {
|
|
29
|
+
const resolved = copyResolved(goal, env);
|
|
30
|
+
if (!termIsGround(resolved)) continue;
|
|
31
|
+
const line = `${termToString(resolved, new Env(), true)}.\n`;
|
|
32
|
+
if (facts.has(line) || seen.has(line)) continue;
|
|
33
|
+
seen.add(line);
|
|
34
|
+
output.push(line);
|
|
35
|
+
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
return { stdout: output.join(''), stats: solver.stats };
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
function appendExplanation(output, program, resolved) {
|
|
49
|
-
const proof = whyProof(program, resolved);
|
|
41
|
+
function appendExplanation(output, program, resolved, registry) {
|
|
42
|
+
const proof = whyProof(program, resolved, { registry });
|
|
50
43
|
output.push(proof.text);
|
|
51
44
|
if (!proof.ok) output.push(whyNoProof(resolved));
|
|
52
45
|
}
|
package/src/parser.js
CHANGED
|
@@ -399,9 +399,9 @@ export function parseProgramText(source) {
|
|
|
399
399
|
return parseClauses(source);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
export function
|
|
403
|
-
const clauses = parseClauses(`
|
|
402
|
+
export function parseGoalText(text) {
|
|
403
|
+
const clauses = parseClauses(`zz_goal(${text}).`);
|
|
404
404
|
const head = clauses[0]?.head;
|
|
405
|
-
if (!head || head.args.length < 1) throw new Error('bad
|
|
405
|
+
if (!head || head.args.length < 1) throw new Error('bad goal');
|
|
406
406
|
return head.args[0];
|
|
407
407
|
}
|
package/src/solver.js
CHANGED
|
@@ -31,7 +31,7 @@ export class Solver {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
cloneForInnerGoal(solutionLimit = this.solutionLimit) {
|
|
35
35
|
const solver = new Solver(this.program, { registry: this.registry, maxDepth: this.maxDepth, solutionLimit });
|
|
36
36
|
solver.memo = this.memo;
|
|
37
37
|
return solver;
|
|
@@ -114,7 +114,7 @@ export class Solver {
|
|
|
114
114
|
}
|
|
115
115
|
if (!entry.complete && !entry.computing) {
|
|
116
116
|
entry.computing = true;
|
|
117
|
-
const collector = this.
|
|
117
|
+
const collector = this.cloneForInnerGoal();
|
|
118
118
|
for (const answerEnv of collector.solveUserGoalUncached(group, goal, [], env.clone(), depth)) {
|
|
119
119
|
entry.answers.push(goal.args.map((arg) => importResolved(arg, answerEnv)));
|
|
120
120
|
}
|
package/test/run-conformance.js
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Conformance test runner.
|
|
3
|
-
// It executes
|
|
3
|
+
// It executes cases in-process so the conformance corpus measures engine behavior instead of Node process startup.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { Program, run } from '../src/index.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { TestReporter, isMainModule } from './test-style.js';
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
-
const bin = path.join(root, 'bin', 'eyelang');
|
|
13
12
|
const profileArg = process.argv[2] ?? 'conformance';
|
|
14
13
|
|
|
15
14
|
export function runConformance(reporter = new TestReporter(), requestedProfiles = null) {
|
|
16
15
|
const profiles = requestedProfiles ?? (profileArg === 'conformance' ? ['core', 'extension'] : [profileArg]);
|
|
17
|
-
const
|
|
18
|
-
const actualFile = path.join(tmp, 'actual.out');
|
|
19
|
-
const errFile = path.join(tmp, 'stderr.out');
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
for (const profile of profiles) runProfile(reporter, profile, actualFile, errFile);
|
|
23
|
-
} finally {
|
|
24
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
25
|
-
}
|
|
16
|
+
for (const profile of profiles) runProfile(reporter, profile);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
function runProfile(reporter, profile
|
|
19
|
+
function runProfile(reporter, profile) {
|
|
29
20
|
const casesDir = path.join(root, 'conformance', 'cases', profile);
|
|
30
21
|
const expectedDir = path.join(root, 'conformance', 'expected', profile);
|
|
31
22
|
|
|
@@ -38,56 +29,40 @@ function runProfile(reporter, profile, actualFile, errFile) {
|
|
|
38
29
|
for (const file of files) {
|
|
39
30
|
const name = file.slice(0, -3);
|
|
40
31
|
const label = `${profile}/${name}`;
|
|
41
|
-
reporter.test(label, () => runCase(profile, name, file, casesDir, expectedDir
|
|
32
|
+
reporter.test(label, () => runCase(profile, name, file, casesDir, expectedDir));
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
reporter.sectionTotal(`conformance ${profile}`);
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
function runCase(profile, name, file, casesDir, expectedDir
|
|
48
|
-
const
|
|
49
|
-
const queryFile = path.join(casesDir, `${name}.query`);
|
|
38
|
+
function runCase(profile, name, file, casesDir, expectedDir) {
|
|
39
|
+
const programFile = path.join(casesDir, file);
|
|
50
40
|
const expected = path.join(expectedDir, `${name}.out`);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const outFd = fs.openSync(actualFile, 'w');
|
|
56
|
-
const errFd = fs.openSync(errFile, 'w');
|
|
57
|
-
const result = spawnSync(process.execPath, args, {
|
|
58
|
-
cwd: root,
|
|
59
|
-
stdio: ['ignore', outFd, errFd],
|
|
60
|
-
});
|
|
61
|
-
fs.closeSync(outFd);
|
|
62
|
-
fs.closeSync(errFd);
|
|
63
|
-
|
|
64
|
-
if (result.status !== 0) {
|
|
65
|
-
const stderr = fs.readFileSync(errFile, 'utf8');
|
|
66
|
-
const stdout = fs.readFileSync(actualFile, 'utf8');
|
|
67
|
-
throw new Error(`case ${profile}/${name} exited with ${result.status}\n${stderr}${stdout}`.trimEnd());
|
|
68
|
-
}
|
|
41
|
+
const text = fs.readFileSync(programFile, 'utf8');
|
|
42
|
+
const program = Program.parseSources([{ text, filename: file }], { sourceMetadata: false, markRecursive: false });
|
|
43
|
+
const actual = run(program).stdout;
|
|
69
44
|
|
|
70
45
|
if (!fs.existsSync(expected)) {
|
|
71
46
|
throw new Error(`missing expected file: ${path.relative(root, expected)}`);
|
|
72
47
|
}
|
|
73
48
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
const expectedText = fs.readFileSync(expected, 'utf8');
|
|
50
|
+
if (expectedText !== actual) {
|
|
51
|
+
throw new Error(`output mismatch for ${profile}/${name}
|
|
52
|
+
${diffText(expected, actual)}`.trimEnd());
|
|
78
53
|
}
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
function diffText(expected,
|
|
82
|
-
const diff = spawnSync('diff', ['-u', expected,
|
|
56
|
+
function diffText(expected, actualText) {
|
|
57
|
+
const diff = spawnSync('diff', ['-u', expected, '-'], { input: actualText, encoding: 'utf8' });
|
|
83
58
|
if (diff.stdout) return diff.stdout;
|
|
84
59
|
|
|
85
60
|
const expectedText = fs.readFileSync(expected, 'utf8').split('\n');
|
|
86
|
-
const
|
|
87
|
-
const limit = Math.max(expectedText.length,
|
|
61
|
+
const actualLines = actualText.split('\n');
|
|
62
|
+
const limit = Math.max(expectedText.length, actualLines.length);
|
|
88
63
|
for (let i = 0; i < limit; i++) {
|
|
89
|
-
if (expectedText[i] !==
|
|
90
|
-
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${
|
|
64
|
+
if (expectedText[i] !== actualLines[i]) {
|
|
65
|
+
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualLines[i] ?? '<missing>'}`;
|
|
91
66
|
}
|
|
92
67
|
}
|
|
93
68
|
|
package/test/run-examples.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
// Example-output test runner.
|
|
3
3
|
// It compares examples byte-for-byte against golden output so answer and proof changes cannot silently alter results.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { Program, run } from '../src/index.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { TestReporter, isMainModule } from './test-style.js';
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
-
const bin = path.join(root, 'bin', 'eyelang');
|
|
13
12
|
const examplesDir = path.join(root, 'examples');
|
|
14
13
|
const expectedDir = path.join(examplesDir, 'output');
|
|
15
14
|
const expectedProofDir = path.join(examplesDir, 'proof');
|
|
@@ -39,77 +38,70 @@ const proofExamples = [
|
|
|
39
38
|
];
|
|
40
39
|
|
|
41
40
|
export function runExamples(reporter = new TestReporter()) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const files = fs.readdirSync(examplesDir)
|
|
42
|
+
.filter((name) => name.endsWith('.pl'))
|
|
43
|
+
.sort();
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.sort();
|
|
50
|
-
|
|
51
|
-
reporter.section('Examples');
|
|
52
|
-
for (const name of files) reporter.test(name, () => runExample(name, actualFile, errFile));
|
|
53
|
-
reporter.sectionTotal('examples');
|
|
45
|
+
reporter.section('Examples');
|
|
46
|
+
for (const name of files) reporter.test(name, () => runExample(name));
|
|
47
|
+
reporter.sectionTotal('examples');
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} finally {
|
|
59
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
60
|
-
}
|
|
49
|
+
reporter.section('Proof examples');
|
|
50
|
+
for (const name of proofExamples) reporter.test(name, () => runProofExample(name));
|
|
51
|
+
reporter.sectionTotal('proof examples');
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
function runExample(name
|
|
64
|
-
const
|
|
54
|
+
function runExample(name) {
|
|
55
|
+
const programFile = path.join(examplesDir, name);
|
|
65
56
|
const expected = path.join(expectedDir, name);
|
|
66
|
-
|
|
57
|
+
const actual = runProgramExample(programFile, name, { proof: false });
|
|
58
|
+
compareOutput(name, expected, actual, 'output');
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
function runProofExample(name
|
|
70
|
-
const
|
|
61
|
+
function runProofExample(name) {
|
|
62
|
+
const programFile = path.join(examplesDir, name);
|
|
71
63
|
const expected = path.join(expectedProofDir, name);
|
|
72
|
-
|
|
64
|
+
const actual = runProgramExample(programFile, name, { proof: true });
|
|
65
|
+
compareOutput(name, expected, actual, 'proof output');
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const stdout = fs.readFileSync(actualFile, 'utf8');
|
|
89
|
-
throw new Error(`example ${name} ${label} exited with ${result.status}\n${stderr}${stdout}`.trimEnd());
|
|
68
|
+
function runProgramExample(programFile, filename, options) {
|
|
69
|
+
const oldLocalTime = process.env.EYELANG_LOCAL_TIME;
|
|
70
|
+
process.env.EYELANG_LOCAL_TIME = fixedExampleDate;
|
|
71
|
+
try {
|
|
72
|
+
const text = fs.readFileSync(programFile, 'utf8');
|
|
73
|
+
const program = Program.parseSources([{ text, filename }], {
|
|
74
|
+
sourceMetadata: options.proof,
|
|
75
|
+
markRecursive: options.proof,
|
|
76
|
+
});
|
|
77
|
+
return run(program, options).stdout;
|
|
78
|
+
} finally {
|
|
79
|
+
if (oldLocalTime == null) delete process.env.EYELANG_LOCAL_TIME;
|
|
80
|
+
else process.env.EYELANG_LOCAL_TIME = oldLocalTime;
|
|
90
81
|
}
|
|
82
|
+
}
|
|
91
83
|
|
|
84
|
+
function compareOutput(name, expected, actual, label) {
|
|
92
85
|
if (!fs.existsSync(expected)) {
|
|
93
86
|
throw new Error(`missing expected ${label} file: ${path.relative(root, expected)}`);
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new Error(`${label} mismatch for ${name}\n${diffText(expected, actualFile)}`.trimEnd());
|
|
89
|
+
const expectedText = fs.readFileSync(expected, 'utf8');
|
|
90
|
+
if (expectedText !== actual) {
|
|
91
|
+
throw new Error(`${label} mismatch for ${name}\n${diffText(expected, actual)}`.trimEnd());
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
94
|
|
|
103
|
-
function diffText(expected,
|
|
104
|
-
const diff = spawnSync('diff', ['-u', expected,
|
|
95
|
+
function diffText(expected, actualText) {
|
|
96
|
+
const diff = spawnSync('diff', ['-u', expected, '-'], { input: actualText, encoding: 'utf8' });
|
|
105
97
|
if (diff.stdout) return diff.stdout;
|
|
106
98
|
|
|
107
99
|
const expectedText = fs.readFileSync(expected, 'utf8').split('\n');
|
|
108
|
-
const
|
|
109
|
-
const limit = Math.max(expectedText.length,
|
|
100
|
+
const actualLines = actualText.split('\n');
|
|
101
|
+
const limit = Math.max(expectedText.length, actualLines.length);
|
|
110
102
|
for (let i = 0; i < limit; i++) {
|
|
111
|
-
if (expectedText[i] !==
|
|
112
|
-
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${
|
|
103
|
+
if (expectedText[i] !== actualLines[i]) {
|
|
104
|
+
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualLines[i] ?? '<missing>'}`;
|
|
113
105
|
}
|
|
114
106
|
}
|
|
115
107
|
|
package/test/run-regression.js
CHANGED
|
@@ -30,8 +30,8 @@ import {
|
|
|
30
30
|
unify,
|
|
31
31
|
variantTerms,
|
|
32
32
|
parseProgramText,
|
|
33
|
-
parseQueryGoal,
|
|
34
33
|
} from '../src/index.js';
|
|
34
|
+
import { parseGoalText } from '../src/parser.js';
|
|
35
35
|
import { selectClauseCandidates } from '../src/program.js';
|
|
36
36
|
import { TestReporter, isMainModule } from './test-style.js';
|
|
37
37
|
|
|
@@ -61,7 +61,7 @@ function regressionCases() {
|
|
|
61
61
|
name: '--proof rule fact explanation output',
|
|
62
62
|
run: () => runWhy({
|
|
63
63
|
program: 'type(socrates, man).\ntype(X, mortal) :- type(X, man).\n',
|
|
64
|
-
|
|
64
|
+
goalText: 'type(socrates, mortal)',
|
|
65
65
|
expected: `type(socrates, mortal).
|
|
66
66
|
why(
|
|
67
67
|
type(socrates, mortal),
|
|
@@ -84,8 +84,8 @@ why(
|
|
|
84
84
|
{
|
|
85
85
|
name: '--proof numeric builtin explanation output',
|
|
86
86
|
run: () => runWhy({
|
|
87
|
-
program: 'p(X) :- between(
|
|
88
|
-
|
|
87
|
+
program: 'p(X) :- between(536, 536, X).\n',
|
|
88
|
+
goalText: 'p(536)',
|
|
89
89
|
expected: `p(536).
|
|
90
90
|
why(
|
|
91
91
|
p(536),
|
|
@@ -95,7 +95,7 @@ why(
|
|
|
95
95
|
bindings([binding("X", 536)]),
|
|
96
96
|
uses([
|
|
97
97
|
proof(
|
|
98
|
-
goal(between(
|
|
98
|
+
goal(between(536, 536, 536)),
|
|
99
99
|
by(builtin(between, 3))
|
|
100
100
|
)
|
|
101
101
|
])
|
|
@@ -108,8 +108,8 @@ why(
|
|
|
108
108
|
{
|
|
109
109
|
name: '--proof list builtin explanation output',
|
|
110
110
|
run: () => runWhy({
|
|
111
|
-
program: 'p(X) :- member(X, [a
|
|
112
|
-
|
|
111
|
+
program: 'p(X) :- member(X, [a]).\n',
|
|
112
|
+
goalText: 'p(a)',
|
|
113
113
|
expected: `p(a).
|
|
114
114
|
why(
|
|
115
115
|
p(a),
|
|
@@ -119,7 +119,7 @@ why(
|
|
|
119
119
|
bindings([binding("X", a)]),
|
|
120
120
|
uses([
|
|
121
121
|
proof(
|
|
122
|
-
goal(member(a, [a
|
|
122
|
+
goal(member(a, [a])),
|
|
123
123
|
by(builtin(member, 2))
|
|
124
124
|
)
|
|
125
125
|
])
|
|
@@ -134,7 +134,7 @@ why(
|
|
|
134
134
|
run: () => {
|
|
135
135
|
const result = runWhyLoose({
|
|
136
136
|
program: 'p(ok) :- q(X), r(X).\nq(a).\nq(b).\nr(b).\n',
|
|
137
|
-
|
|
137
|
+
goalText: 'p(ok)',
|
|
138
138
|
});
|
|
139
139
|
assertIncludes(result.stdout, 'goal(q(b)),\n by(fact("', 'stdout');
|
|
140
140
|
assertIncludes(result.stdout, 'goal(r(b)),\n by(fact("', 'stdout');
|
|
@@ -146,7 +146,7 @@ why(
|
|
|
146
146
|
run: () => {
|
|
147
147
|
const result = runWhyLoose({
|
|
148
148
|
program: 'p(ok) :- q(1), q(1).\nq(0).\nq(1) :- q(0).\n',
|
|
149
|
-
|
|
149
|
+
goalText: 'p(ok)',
|
|
150
150
|
});
|
|
151
151
|
assertIncludes(result.stdout, 'goal(p(ok)),\n by(rule("', 'stdout');
|
|
152
152
|
assertIncludes(result.stdout, 'goal(q(1)),\n by(rule("', 'stdout');
|
|
@@ -156,12 +156,12 @@ why(
|
|
|
156
156
|
{
|
|
157
157
|
name: 'EYELANG_LOCAL_TIME fixes local_time builtin',
|
|
158
158
|
run: () => {
|
|
159
|
-
const result = runCli(['
|
|
160
|
-
input: '',
|
|
159
|
+
const result = runCli(['-'], {
|
|
160
|
+
input: 'materialize(local_time_answer, 1).\nlocal_time_answer(D) :- local_time(D).\n',
|
|
161
161
|
env: { EYELANG_LOCAL_TIME: '2024-01-02' },
|
|
162
162
|
});
|
|
163
163
|
assertEqual(result.status, 0, 'exit status');
|
|
164
|
-
assertEqual(result.stdout, '
|
|
164
|
+
assertEqual(result.stdout, 'local_time_answer("2024-01-02").\n', 'stdout');
|
|
165
165
|
assertEqual(result.stderr, '', 'stderr');
|
|
166
166
|
},
|
|
167
167
|
},
|
|
@@ -171,7 +171,6 @@ why(
|
|
|
171
171
|
const result = runCli([]);
|
|
172
172
|
assertEqual(result.status, 0, 'exit status');
|
|
173
173
|
assertIncludes(result.stdout, 'Usage:\n eyelang [options] [file-or-url.pl|- ...]', 'stdout');
|
|
174
|
-
assertIncludes(result.stdout, '--query GOAL', 'stdout');
|
|
175
174
|
assertIncludes(result.stdout, '--proof', 'stdout');
|
|
176
175
|
assertEqual(result.stderr, '', 'stderr');
|
|
177
176
|
},
|
|
@@ -198,21 +197,20 @@ why(
|
|
|
198
197
|
{
|
|
199
198
|
name: 'stdin input is accepted',
|
|
200
199
|
run: () => {
|
|
201
|
-
const result = runCli(['
|
|
200
|
+
const result = runCli(['-'], { input: 'p(a, b).\nq(X, Y) :- p(X, Y).\n' });
|
|
202
201
|
assertEqual(result.status, 0, 'exit status');
|
|
203
|
-
assertEqual(result.stdout, '
|
|
202
|
+
assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
|
|
204
203
|
assertEqual(result.stderr, '', 'stderr');
|
|
205
204
|
},
|
|
206
205
|
},
|
|
207
206
|
|
|
208
207
|
|
|
209
208
|
{
|
|
210
|
-
name: '--proof enables
|
|
209
|
+
name: '--proof enables materialization explanations',
|
|
211
210
|
run: () => {
|
|
212
|
-
const result = runCli(['--proof', '
|
|
211
|
+
const result = runCli(['--proof', '-'], { input: 'p(a, b).\nq(X, Y) :- p(X, Y).\n' });
|
|
213
212
|
assertEqual(result.status, 0, 'exit status');
|
|
214
|
-
assertIncludes(result.stdout, '
|
|
215
|
-
assertIncludes(result.stdout, 'p(b).\nwhy(', 'stdout');
|
|
213
|
+
assertIncludes(result.stdout, 'q(a, b).\nwhy(', 'stdout');
|
|
216
214
|
assertEqual(result.stderr, '', 'stderr');
|
|
217
215
|
},
|
|
218
216
|
},
|
|
@@ -238,26 +236,11 @@ why(
|
|
|
238
236
|
assertEqual(result.stderr, '', 'stderr');
|
|
239
237
|
},
|
|
240
238
|
},
|
|
241
|
-
{
|
|
242
|
-
name: 'missing query argument fails clearly',
|
|
243
|
-
run: () => {
|
|
244
|
-
const result = runCli(['--query']);
|
|
245
|
-
assertEqual(result.status, 1, 'exit status');
|
|
246
|
-
assertIncludes(result.stderr, 'eyelang: --query requires an argument', 'stderr');
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
239
|
];
|
|
250
240
|
}
|
|
251
241
|
|
|
252
242
|
function apiCases() {
|
|
253
243
|
return [
|
|
254
|
-
{
|
|
255
|
-
name: 'run query through public API without proof by default',
|
|
256
|
-
run: () => {
|
|
257
|
-
const result = run('parent(pat, jan).\nancestor(X, Y) :- parent(X, Y).\n', { query: 'ancestor(pat, Y)' });
|
|
258
|
-
assertEqual(result.stdout, 'ancestor(pat, jan).\n', 'stdout');
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
244
|
{
|
|
262
245
|
name: 'run materialization through public API without proof by default',
|
|
263
246
|
run: () => {
|
|
@@ -267,14 +250,6 @@ function apiCases() {
|
|
|
267
250
|
},
|
|
268
251
|
|
|
269
252
|
|
|
270
|
-
{
|
|
271
|
-
name: 'run query can enable proof explanations',
|
|
272
|
-
run: () => {
|
|
273
|
-
const result = run('p(a).\np(b).\n', { query: 'p(X)', proof: true });
|
|
274
|
-
assertIncludes(result.stdout, 'p(a).\nwhy(', 'stdout');
|
|
275
|
-
assertIncludes(result.stdout, 'p(b).\nwhy(', 'stdout');
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
253
|
{
|
|
279
254
|
name: 'run materialization can enable proof explanations',
|
|
280
255
|
run: () => {
|
|
@@ -286,9 +261,9 @@ function apiCases() {
|
|
|
286
261
|
{
|
|
287
262
|
name: 'run accepts Program instances',
|
|
288
263
|
run: () => {
|
|
289
|
-
const program = Program.parse('p(a).\
|
|
290
|
-
const result = run(program
|
|
291
|
-
assertEqual(result.stdout, '
|
|
264
|
+
const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n');
|
|
265
|
+
const result = run(program);
|
|
266
|
+
assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
|
|
292
267
|
},
|
|
293
268
|
},
|
|
294
269
|
{
|
|
@@ -306,7 +281,7 @@ function apiCases() {
|
|
|
306
281
|
run: () => {
|
|
307
282
|
const program = Program.parse('p(a).\np(b).\n');
|
|
308
283
|
const solver = new Solver(program);
|
|
309
|
-
const goal =
|
|
284
|
+
const goal = parseGoalText('p(X)');
|
|
310
285
|
const answers = [...solver.solve([goal], new Env(), 0)].map((env) => termToString(goal, env, true));
|
|
311
286
|
assertEqual(answers.join('\n'), 'p(a)\np(b)', 'answers');
|
|
312
287
|
},
|
|
@@ -316,7 +291,7 @@ function apiCases() {
|
|
|
316
291
|
run: () => {
|
|
317
292
|
const program = Program.parse('p(a).\np(b).\np(c).\n');
|
|
318
293
|
const solver = new Solver(program, { solutionLimit: 2 });
|
|
319
|
-
const goal =
|
|
294
|
+
const goal = parseGoalText('p(X)');
|
|
320
295
|
const answers = [...solver.solve([goal], new Env(), 0)].map((env) => termToString(goal, env, true));
|
|
321
296
|
assertEqual(answers.join('\n'), 'p(a)\np(b)', 'answers');
|
|
322
297
|
},
|
|
@@ -331,7 +306,7 @@ function apiCases() {
|
|
|
331
306
|
});
|
|
332
307
|
const program = Program.parse('answer(X) :- hello(X).\n');
|
|
333
308
|
const solver = new Solver(program, { registry });
|
|
334
|
-
const goal =
|
|
309
|
+
const goal = parseGoalText('answer(X)');
|
|
335
310
|
const answers = [...solver.solve([goal], new Env(), 0)].map((env) => termToString(goal, env, true));
|
|
336
311
|
assertEqual(answers.join('\n'), 'answer(world)', 'answers');
|
|
337
312
|
},
|
|
@@ -376,7 +351,7 @@ function whiteBoxCases() {
|
|
|
376
351
|
{
|
|
377
352
|
name: 'parser preserves list syntax readback',
|
|
378
353
|
run: () => {
|
|
379
|
-
const goal =
|
|
354
|
+
const goal = parseGoalText('member(X, [a, b])');
|
|
380
355
|
assertEqual(termToString(goal, new Env(), true), 'member(X, [a, b])', 'goal');
|
|
381
356
|
},
|
|
382
357
|
},
|
|
@@ -392,9 +367,9 @@ function whiteBoxCases() {
|
|
|
392
367
|
{
|
|
393
368
|
name: 'variantTerms recognizes alpha-equivalent goals',
|
|
394
369
|
run: () => {
|
|
395
|
-
const left =
|
|
396
|
-
const right =
|
|
397
|
-
const nonVariant =
|
|
370
|
+
const left = parseGoalText('edge(X, Y)');
|
|
371
|
+
const right = parseGoalText('edge(A, B)');
|
|
372
|
+
const nonVariant = parseGoalText('edge(A, A)');
|
|
398
373
|
assertEqual(variantTerms(left, new Env(), right, new Env()), true, 'variant');
|
|
399
374
|
assertEqual(variantTerms(left, new Env(), nonVariant, new Env()), false, 'non-variant');
|
|
400
375
|
},
|
|
@@ -402,7 +377,7 @@ function whiteBoxCases() {
|
|
|
402
377
|
{
|
|
403
378
|
name: 'flattenConjunction preserves left-to-right order',
|
|
404
379
|
run: () => {
|
|
405
|
-
const goal =
|
|
380
|
+
const goal = parseGoalText('(a, b, c)');
|
|
406
381
|
const parts = flattenConjunction(goal).map((part) => termToString(part, new Env(), true));
|
|
407
382
|
assertEqual(parts.join(' | '), 'a | b | c', 'order');
|
|
408
383
|
},
|
|
@@ -421,7 +396,7 @@ function whiteBoxCases() {
|
|
|
421
396
|
run: () => {
|
|
422
397
|
const program = Program.parse('edge(a, b).\nedge(c, d).\nedge(X, z).\n');
|
|
423
398
|
const group = program.findGroup('edge', 2);
|
|
424
|
-
const goal =
|
|
399
|
+
const goal = parseGoalText('edge(a, Y)');
|
|
425
400
|
const candidates = selectClauseCandidates(group, goal, new Env());
|
|
426
401
|
assertEqual(candidates.primary.length, 1, 'primary bucket length');
|
|
427
402
|
assertEqual(candidates.fallback.length, 1, 'fallback length');
|
|
@@ -443,10 +418,12 @@ function sectionLabel(name) {
|
|
|
443
418
|
return name.toLowerCase();
|
|
444
419
|
}
|
|
445
420
|
|
|
446
|
-
function runWhy({ program,
|
|
421
|
+
function runWhy({ program, goalText, expected }) {
|
|
447
422
|
const programFile = path.join(tmp, `${++tmpCounter}.pl`);
|
|
448
423
|
fs.writeFileSync(programFile, program);
|
|
449
|
-
const
|
|
424
|
+
const goal = parseGoalText(goalText);
|
|
425
|
+
fs.appendFileSync(programFile, `\nmaterialize(${goal.name}, ${goal.arity}).\n`);
|
|
426
|
+
const result = runCli(['--proof', programFile]);
|
|
450
427
|
assertEqual(result.status, 0, 'exit status');
|
|
451
428
|
assertEqual(result.stderr, '', 'stderr');
|
|
452
429
|
const expectedText = expected.replaceAll('__FILE__', path.basename(programFile));
|
|
@@ -460,10 +437,12 @@ function runWhy({ program, query, expected }) {
|
|
|
460
437
|
assertIncludes(result.stdout, '\n).\n\n', 'stdout');
|
|
461
438
|
}
|
|
462
439
|
|
|
463
|
-
function runWhyLoose({ program,
|
|
440
|
+
function runWhyLoose({ program, goalText }) {
|
|
464
441
|
const programFile = path.join(tmp, `${++tmpCounter}.pl`);
|
|
465
442
|
fs.writeFileSync(programFile, program);
|
|
466
|
-
const
|
|
443
|
+
const goal = parseGoalText(goalText);
|
|
444
|
+
fs.appendFileSync(programFile, `\nmaterialize(${goal.name}, ${goal.arity}).\n`);
|
|
445
|
+
const result = runCli(['--proof', programFile]);
|
|
467
446
|
assertEqual(result.status, 0, 'exit status');
|
|
468
447
|
assertEqual(result.stderr, '', 'stderr');
|
|
469
448
|
Program.parse(result.stdout);
|
|
File without changes
|