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.
Files changed (81) hide show
  1. package/README.md +22 -43
  2. package/SPEC.md +16 -16
  3. package/bin/eyelang +0 -0
  4. package/conformance/README.md +4 -5
  5. package/conformance/cases/core/001_fact_output.pl +4 -0
  6. package/conformance/cases/core/002_rule_recursion.pl +4 -2
  7. package/conformance/cases/core/003_terms_and_readback.pl +15 -13
  8. package/conformance/cases/core/004_conjunction_and_parentheses.pl +1 -0
  9. package/conformance/cases/core/005_list_deconstruction.pl +1 -0
  10. package/conformance/cases/core/006_comma_formula_data.pl +1 -0
  11. package/conformance/cases/core/007_anonymous_variables.pl +1 -0
  12. package/conformance/cases/core/008_graphic_atoms.pl +5 -3
  13. package/conformance/cases/core/009_comments_and_whitespace.pl +1 -0
  14. package/conformance/cases/core/010_variable_scope_and_reuse.pl +1 -0
  15. package/conformance/cases/core/011_predicate_arity.pl +1 -0
  16. package/conformance/cases/core/012_nested_compound_unification.pl +1 -0
  17. package/conformance/cases/core/013_multiple_clauses_order.pl +1 -0
  18. package/conformance/cases/core/014_failure_filters_answers.pl +2 -0
  19. package/conformance/cases/core/015_improper_list_unification.pl +1 -0
  20. package/conformance/cases/core/016_zero_arity_compound.pl +1 -0
  21. package/conformance/cases/core/017_three_step_recursion.pl +1 -0
  22. package/conformance/cases/core/018_quoted_atom_readback.pl +1 -0
  23. package/conformance/cases/core/019_parenthesized_three_conjuncts.pl +1 -0
  24. package/conformance/cases/core/020_nested_list_terms.pl +1 -0
  25. package/conformance/cases/extension/001_default_derived_output.pl +1 -1
  26. package/conformance/cases/extension/002_materialize_focus.pl +1 -1
  27. package/conformance/cases/extension/003_arithmetic_and_comparison.pl +1 -0
  28. package/conformance/cases/extension/004_strings_and_atoms.pl +1 -0
  29. package/conformance/cases/extension/005_lists_aggregation_ordering.pl +1 -0
  30. package/conformance/cases/extension/006_formula_terms.pl +1 -0
  31. package/conformance/cases/extension/007_negation_once_generators.pl +1 -0
  32. package/conformance/cases/extension/008_equality_and_inequality.pl +1 -0
  33. package/conformance/cases/extension/009_list_relations.pl +1 -0
  34. package/conformance/cases/extension/010_append_splits.pl +1 -0
  35. package/conformance/cases/extension/011_matching_and_comparison.pl +1 -0
  36. package/conformance/cases/extension/012_memoize_declaration.pl +5 -3
  37. package/conformance/cases/extension/013_numeric_functions.pl +1 -0
  38. package/conformance/cases/extension/014_between_enumeration.pl +1 -0
  39. package/conformance/cases/extension/015_smallest_divisor.pl +1 -0
  40. package/conformance/cases/extension/016_negation_filter.pl +1 -0
  41. package/conformance/cases/extension/017_once_user_predicate.pl +1 -0
  42. package/conformance/cases/extension/018_findall_user_goal.pl +1 -0
  43. package/conformance/cases/extension/019_sort_deduplicates_atoms.pl +1 -0
  44. package/conformance/cases/extension/020_append_bound_prefix_suffix.pl +1 -0
  45. package/conformance/cases/extension/021_nth0_index_generation.pl +1 -0
  46. package/conformance/cases/extension/022_set_nth0_edges.pl +1 -0
  47. package/conformance/cases/extension/023_select_duplicate_occurrences.pl +1 -0
  48. package/conformance/cases/extension/024_not_member_filter.pl +1 -0
  49. package/conformance/cases/extension/025_is_list_filter.pl +1 -0
  50. package/conformance/cases/extension/026_nested_formula_terms.pl +1 -0
  51. package/conformance/cases/extension/027_materialize_excludes_source_fact.pl +1 -1
  52. package/conformance/cases/extension/028_numeric_and_lexical_comparison.pl +1 -0
  53. package/conformance/cases/extension/029_string_matching_filters.pl +1 -0
  54. package/conformance/cases/extension/030_string_and_atom_concat.pl +1 -0
  55. package/conformance/cases/extension/031_countall_empty_and_nonempty.pl +2 -0
  56. package/conformance/cases/extension/032_sumall_numeric_template.pl +2 -0
  57. package/conformance/cases/extension/033_aggregate_min_template.pl +2 -0
  58. package/conformance/cases/extension/034_aggregate_max_compound_key.pl +2 -0
  59. package/conformance/expected/extension/031_countall_empty_and_nonempty.out +1 -1
  60. package/conformance/expected/extension/032_sumall_numeric_template.out +1 -1
  61. package/conformance/expected/extension/033_aggregate_min_template.out +1 -1
  62. package/conformance/expected/extension/034_aggregate_max_compound_key.out +1 -1
  63. package/examples/basic-monadic.pl +1 -1
  64. package/examples/monkey-bananas.pl +1 -1
  65. package/examples/path-discovery.pl +3 -3
  66. package/examples/peano-arithmetic.pl +1 -1
  67. package/package.json +1 -1
  68. package/playground-worker.mjs +2 -15
  69. package/playground.html +5 -25
  70. package/src/builtins/aggregation.js +5 -5
  71. package/src/builtins/control.js +3 -3
  72. package/src/builtins/registry.js +7 -0
  73. package/src/cli.js +36 -38
  74. package/src/index.js +21 -28
  75. package/src/parser.js +3 -3
  76. package/src/solver.js +2 -2
  77. package/test/run-conformance.js +20 -45
  78. package/test/run-examples.js +42 -50
  79. package/test/run-regression.js +38 -59
  80. package/conformance/cases/core/001_fact_query.pl +0 -2
  81. /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, parseQueryGoal } from './parser.js';
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 solver = new Solver(program, options);
19
+ const runOptions = options.registry ? options : { ...options, registry: getDefaultRegistry() };
20
+ const solver = new Solver(program, runOptions);
20
21
  const output = [];
21
- if (options.query) {
22
- const goal = typeof options.query === 'string' ? parseQueryGoal(options.query) : options.query;
23
- for (const env of solver.solve([goal], new Env(), 0)) {
24
- output.push(`${termToString(goal, env, true)}.\n`);
25
- if (includeWhy) appendExplanation(output, program, copyResolved(goal, env));
26
- }
27
- } else {
28
- const goals = program.materializationGoals();
29
- const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
30
- const facts = program.sourceFactLines(materializedKeys);
31
- const seen = new Set();
32
- for (const goal of goals) {
33
- const localSolver = new Solver(program, options);
34
- for (const env of localSolver.solve([goal], new Env(), 0)) {
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 parseQueryGoal(query) {
403
- const clauses = parseClauses(`q(${query}).`);
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 query');
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
- cloneForSubquery(solutionLimit = this.solutionLimit) {
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.cloneForSubquery();
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
  }
@@ -1,31 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  // Conformance test runner.
3
- // It executes each case through the CLI so tests cover the same path users run from the shell.
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 tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyelang-conformance.'));
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, actualFile, errFile) {
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, actualFile, errFile));
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, actualFile, errFile) {
48
- const program = path.join(casesDir, file);
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 args = [bin];
52
- if (fs.existsSync(queryFile)) args.push('--query', fs.readFileSync(queryFile, 'utf8').trim());
53
- args.push(program);
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 expectedBuffer = fs.readFileSync(expected);
75
- const actualBuffer = fs.readFileSync(actualFile);
76
- if (!expectedBuffer.equals(actualBuffer)) {
77
- throw new Error(`output mismatch for ${profile}/${name}\n${diffText(expected, actualFile)}`.trimEnd());
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, actual) {
82
- const diff = spawnSync('diff', ['-u', expected, actual], { encoding: 'utf8' });
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 actualText = fs.readFileSync(actual, 'utf8').split('\n');
87
- const limit = Math.max(expectedText.length, actualText.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] !== actualText[i]) {
90
- return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualText[i] ?? '<missing>'}`;
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
 
@@ -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 tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyelang-examples.'));
43
- const actualFile = path.join(tmp, 'actual.out');
44
- const errFile = path.join(tmp, 'stderr.out');
41
+ const files = fs.readdirSync(examplesDir)
42
+ .filter((name) => name.endsWith('.pl'))
43
+ .sort();
45
44
 
46
- try {
47
- const files = fs.readdirSync(examplesDir)
48
- .filter((name) => name.endsWith('.pl'))
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
- reporter.section('Proof examples');
56
- for (const name of proofExamples) reporter.test(name, () => runProofExample(name, actualFile, errFile));
57
- reporter.sectionTotal('proof examples');
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, actualFile, errFile) {
64
- const program = path.join(examplesDir, name);
54
+ function runExample(name) {
55
+ const programFile = path.join(examplesDir, name);
65
56
  const expected = path.join(expectedDir, name);
66
- runAndCompare(name, program, expected, [], actualFile, errFile, 'output');
57
+ const actual = runProgramExample(programFile, name, { proof: false });
58
+ compareOutput(name, expected, actual, 'output');
67
59
  }
68
60
 
69
- function runProofExample(name, actualFile, errFile) {
70
- const program = path.join(examplesDir, name);
61
+ function runProofExample(name) {
62
+ const programFile = path.join(examplesDir, name);
71
63
  const expected = path.join(expectedProofDir, name);
72
- runAndCompare(name, program, expected, ['--proof'], actualFile, errFile, 'proof output');
64
+ const actual = runProgramExample(programFile, name, { proof: true });
65
+ compareOutput(name, expected, actual, 'proof output');
73
66
  }
74
67
 
75
- function runAndCompare(name, program, expected, args, actualFile, errFile, label) {
76
- const outFd = fs.openSync(actualFile, 'w');
77
- const errFd = fs.openSync(errFile, 'w');
78
- const result = spawnSync(process.execPath, [bin, ...args, program], {
79
- cwd: root,
80
- env: { ...process.env, EYELANG_LOCAL_TIME: fixedExampleDate },
81
- stdio: ['ignore', outFd, errFd],
82
- });
83
- fs.closeSync(outFd);
84
- fs.closeSync(errFd);
85
-
86
- if (result.status !== 0) {
87
- const stderr = fs.readFileSync(errFile, 'utf8');
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 expectedBuffer = fs.readFileSync(expected);
97
- const actualBuffer = fs.readFileSync(actualFile);
98
- if (!expectedBuffer.equals(actualBuffer)) {
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, actual) {
104
- const diff = spawnSync('diff', ['-u', expected, actual], { encoding: 'utf8' });
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 actualText = fs.readFileSync(actual, 'utf8').split('\n');
109
- const limit = Math.max(expectedText.length, actualText.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] !== actualText[i]) {
112
- return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualText[i] ?? '<missing>'}`;
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
 
@@ -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
- query: 'type(socrates, mortal)',
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(4, 1000, X).\n',
88
- query: 'p(536)',
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(4, 1000, 536)),
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, b]).\n',
112
- query: 'p(a)',
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, b])),
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
- query: 'p(ok)',
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
- query: 'p(ok)',
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(['--query', 'local_time(D)', '-'], {
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, 'local_time("2024-01-02").\n', '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(['--query', 'p(X)', '-'], { input: 'p(a).\np(b).\n' });
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, 'p(a).\np(b).\n', '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 query explanations',
209
+ name: '--proof enables materialization explanations',
211
210
  run: () => {
212
- const result = runCli(['--proof', '--query', 'p(X)', '-'], { input: 'p(a).\np(b).\n' });
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, 'p(a).\nwhy(', '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).\np(b).\n');
290
- const result = run(program, { query: 'p(X)' });
291
- assertEqual(result.stdout, 'p(a).\np(b).\n', '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 = parseQueryGoal('p(X)');
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 = parseQueryGoal('p(X)');
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 = parseQueryGoal('answer(X)');
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 = parseQueryGoal('member(X, [a, b])');
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 = parseQueryGoal('edge(X, Y)');
396
- const right = parseQueryGoal('edge(A, B)');
397
- const nonVariant = parseQueryGoal('edge(A, A)');
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 = parseQueryGoal('(a, b, c)');
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 = parseQueryGoal('edge(a, Y)');
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, query, expected }) {
421
+ function runWhy({ program, goalText, expected }) {
447
422
  const programFile = path.join(tmp, `${++tmpCounter}.pl`);
448
423
  fs.writeFileSync(programFile, program);
449
- const result = runCli(['--proof', '--query', query, programFile]);
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, query }) {
440
+ function runWhyLoose({ program, goalText }) {
464
441
  const programFile = path.join(tmp, `${++tmpCounter}.pl`);
465
442
  fs.writeFileSync(programFile, program);
466
- const result = runCli(['--proof', '--query', query, programFile]);
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);
@@ -1,2 +0,0 @@
1
- % SPEC 6, 7, 11: facts are queryable and printed as resolved terms.
2
- parent(pat, jan).