eyeprolog 1.3.3 → 1.3.7
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 +78 -4
- package/examples/book/README.md +2 -1
- package/examples/book/chapter-07/02-move.pl +4 -0
- package/examples/book/chapter-08/01-findall.pl +1 -0
- package/examples/book/chapter-39/04-cost.pl +1 -0
- package/examples/chart-parser.pl +1 -0
- package/examples/dog.pl +1 -0
- package/examples/donald-gerald-robert.pl +1 -0
- package/examples/equality-saturation.pl +1 -0
- package/examples/job-shop-scheduling.pl +1 -0
- package/examples/knapsack-optimization.pl +1 -0
- package/examples/knuth-bendix-completion.pl +1 -0
- package/examples/matrix-chain-order.pl +1 -0
- package/examples/missionaries-cannibals.pl +1 -0
- package/examples/prime-range.pl +1 -0
- package/examples/register-allocation.pl +1 -0
- package/examples/send-more-money.pl +1 -0
- package/examples/stable-marriage.pl +1 -0
- package/examples/totient-summatory.pl +1 -0
- package/examples/weighted-interval-scheduling.pl +1 -0
- package/index.d.ts +7 -0
- package/package.json +1 -1
- package/src/datalog.js +354 -0
- package/src/iso.js +148 -14
- package/src/lib/iso_ext.pl +4 -0
- package/src/lib/lists.pl +0 -3
- package/src/program.js +258 -20
- package/src/solver.js +890 -46
- package/src/standard-library.js +12 -7
- package/src/term.js +1 -1
- package/src/wfs.js +338 -0
- package/test/conformance/ISO-COMPLIANCE.md +2 -1
- package/test/conformance/README.md +4 -1
- package/test/run-conformance.mjs +1 -0
- package/test/run-regression.mjs +230 -3
- package/the-art-of-eyeprolog.md +95 -21
- package/why-eyeprolog.md +17 -0
- /package/examples/book/chapter-07/{02-all_tests_pass.pl → 03-all_tests_pass.pl} +0 -0
package/test/run-regression.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createDefaultRegistry,
|
|
21
21
|
getStrictIsoRegistry,
|
|
22
22
|
getEyePrologRegistry,
|
|
23
|
+
standardLibrarySources,
|
|
23
24
|
eyePrologLibraryIndicators,
|
|
24
25
|
eyePrologNativeLibraryIndicators,
|
|
25
26
|
eyePrologPortableLibraryIndicators,
|
|
@@ -67,6 +68,7 @@ function withStandardModules(source) {
|
|
|
67
68
|
return `:- use_module(library(aggregate)).
|
|
68
69
|
:- use_module(library(comparison)).
|
|
69
70
|
:- use_module(library(dates)).
|
|
71
|
+
:- use_module(library(iso_ext)).
|
|
70
72
|
:- use_module(library(lists)).
|
|
71
73
|
:- use_module(library(primes)).
|
|
72
74
|
:- use_module(library(prologue), [between/3]).
|
|
@@ -2214,6 +2216,22 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
2214
2216
|
assertEqual(result.stderr, '', 'stderr');
|
|
2215
2217
|
},
|
|
2216
2218
|
},
|
|
2219
|
+
{
|
|
2220
|
+
name: 'library(lists) and library(iso_ext) co-import without collisions',
|
|
2221
|
+
run: () => {
|
|
2222
|
+
const input = [
|
|
2223
|
+
':- use_module(library(lists)).',
|
|
2224
|
+
':- use_module(library(iso_ext)).',
|
|
2225
|
+
'%% goal: answer(N)',
|
|
2226
|
+
'answer(N) :- call_nth(member(_, [a,b,c]), N), N = 2.',
|
|
2227
|
+
'',
|
|
2228
|
+
].join('\n');
|
|
2229
|
+
const result = runCli(['--portable', '--no-autoload', '-'], { input });
|
|
2230
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2231
|
+
assertEqual(result.stdout, 'answer(2).\n', 'stdout');
|
|
2232
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2233
|
+
},
|
|
2234
|
+
},
|
|
2217
2235
|
{
|
|
2218
2236
|
name: 'strict ISO mode disables interop autoloading',
|
|
2219
2237
|
run: () => {
|
|
@@ -3426,6 +3444,103 @@ check(A, B, C, D, E, F) :-
|
|
|
3426
3444
|
assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
|
|
3427
3445
|
},
|
|
3428
3446
|
},
|
|
3447
|
+
{
|
|
3448
|
+
name: 'recursive phrase tables stay bounded across distinct inputs (issues #28/#48)',
|
|
3449
|
+
run: () => {
|
|
3450
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
3451
|
+
const programText = `
|
|
3452
|
+
:- use_module(library(prologue)).
|
|
3453
|
+
... --> [].
|
|
3454
|
+
... --> [_], ... .
|
|
3455
|
+
|
|
3456
|
+
vchars("0123456789.").
|
|
3457
|
+
|
|
3458
|
+
ti(Nth:E:Chs, Pot, Start) :-
|
|
3459
|
+
length(Chs, _),
|
|
3460
|
+
call_nth(chs(Chs), Nth), Nth >= Start,
|
|
3461
|
+
Nth mod 10^Pot =:= 0,
|
|
3462
|
+
E = cnt.
|
|
3463
|
+
|
|
3464
|
+
chs(Chs) :-
|
|
3465
|
+
vchars(VChs),
|
|
3466
|
+
maplistch(Chs, VChs),
|
|
3467
|
+
catch(number_chars(_, Chs), error(syntax_error(_), _), false),
|
|
3468
|
+
\\+ phrase((..., ("//"|"++")), Chs).
|
|
3469
|
+
|
|
3470
|
+
maplistch([], _).
|
|
3471
|
+
maplistch([Ch|Chs], VChs) :-
|
|
3472
|
+
member(Ch, VChs),
|
|
3473
|
+
maplistch(Chs, VChs).
|
|
3474
|
+
`;
|
|
3475
|
+
const script = `
|
|
3476
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
3477
|
+
const program = Program.parse(${JSON.stringify(programText)}, { autoloadGoals: ['ti(R,3,0)'] });
|
|
3478
|
+
const listTailGroup = program.findGroup('...', 2, 'user');
|
|
3479
|
+
if (listTailGroup?.listTailRecursive !== true) {
|
|
3480
|
+
throw new Error('recursive DCG tail-consumption was not recognized');
|
|
3481
|
+
}
|
|
3482
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry(), maxMemoryBytes: Infinity });
|
|
3483
|
+
const goal = parseGoalText('ti(R,3,0)', {
|
|
3484
|
+
doubleQuotes: 'chars',
|
|
3485
|
+
operatorDefinitions: [...program.operators.values()],
|
|
3486
|
+
});
|
|
3487
|
+
let count = 0;
|
|
3488
|
+
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
3489
|
+
if (++count === 20) break;
|
|
3490
|
+
}
|
|
3491
|
+
if (count !== 20) throw new Error('unexpected checkpoint count: ' + count);
|
|
3492
|
+
const scope = solver.innerTableScopes.get('phrase');
|
|
3493
|
+
if (scope == null) throw new Error('missing phrase table scope');
|
|
3494
|
+
if (scope.memo.size !== 0) throw new Error('distinct tail-DCG inputs should not retain phrase tables: ' + scope.memo.size);
|
|
3495
|
+
process.stdout.write(count + ':' + scope.memo.size);
|
|
3496
|
+
`;
|
|
3497
|
+
const result = spawnSync(process.execPath, [
|
|
3498
|
+
// This corresponds to 20,000 accepted number candidates. Before the
|
|
3499
|
+
// phrase-local cache, every distinct recursive DCG tail stayed rooted
|
|
3500
|
+
// in the caller memo and this workload exhausted a 32 MiB old space.
|
|
3501
|
+
'--max-old-space-size=32',
|
|
3502
|
+
'--input-type=module',
|
|
3503
|
+
'--eval',
|
|
3504
|
+
script,
|
|
3505
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
3506
|
+
if (result.error) throw result.error;
|
|
3507
|
+
assertEqual(result.status, 0, `bounded phrase-table child status; stderr=${result.stderr}`);
|
|
3508
|
+
const [countText, cacheText] = result.stdout.trim().split(':');
|
|
3509
|
+
assertEqual(countText, '20', 'recursive phrase checkpoints');
|
|
3510
|
+
assertEqual(cacheText, '0', 'distinct tail-DCG phrase cache');
|
|
3511
|
+
},
|
|
3512
|
+
},
|
|
3513
|
+
{
|
|
3514
|
+
name: 'repeated identical phrase invocation reuses a compact table (issue #48)',
|
|
3515
|
+
run: () => {
|
|
3516
|
+
const program = Program.parse(`
|
|
3517
|
+
:- use_module(library(prologue)).
|
|
3518
|
+
... --> [].
|
|
3519
|
+
... --> [_], ... .
|
|
3520
|
+
`, { autoloadGoals: ['phrase((...,("ba"|"gh")),"abcdef")'] });
|
|
3521
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry(), maxMemoryBytes: Infinity });
|
|
3522
|
+
const goal = parseGoalText('phrase((...,("ba"|"gh")),"abcdef")', {
|
|
3523
|
+
doubleQuotes: 'chars',
|
|
3524
|
+
operatorDefinitions: [...program.operators.values()],
|
|
3525
|
+
});
|
|
3526
|
+
const runFailure = () => {
|
|
3527
|
+
let answers = 0;
|
|
3528
|
+
for (const _ of solver.solve([goal], new Env(), 0)) answers++;
|
|
3529
|
+
assertEqual(answers, 0, 'failing phrase answer count');
|
|
3530
|
+
};
|
|
3531
|
+
runFailure();
|
|
3532
|
+
assertEqual(solver.stats.table_fixpoint_rounds, 0, 'first distinct tail-DCG invocation stays untabled');
|
|
3533
|
+
runFailure();
|
|
3534
|
+
const roundsAfterSecond = solver.stats.table_fixpoint_rounds;
|
|
3535
|
+
if (roundsAfterSecond <= 0) throw new Error('repeated phrase invocation did not build reusable table');
|
|
3536
|
+
const scope = solver.innerTableScopes.get('phrase');
|
|
3537
|
+
if (scope == null || scope.memo.size <= 0 || scope.memo.size > 1024) {
|
|
3538
|
+
throw new Error('unexpected repeated phrase table size: ' + (scope?.memo.size ?? 'missing'));
|
|
3539
|
+
}
|
|
3540
|
+
runFailure();
|
|
3541
|
+
assertEqual(solver.stats.table_fixpoint_rounds, roundsAfterSecond, 'third identical phrase call reuses completed table');
|
|
3542
|
+
},
|
|
3543
|
+
},
|
|
3429
3544
|
{
|
|
3430
3545
|
name: 'host Map/Set capacity errors become resource_error(memory)',
|
|
3431
3546
|
run: () => {
|
|
@@ -3608,20 +3723,22 @@ check(A, B, C, D, E, F) :-
|
|
|
3608
3723
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
3609
3724
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
3610
3725
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
3611
|
-
assertEqual(library.defs.size,
|
|
3726
|
+
assertEqual(library.defs.size, 156, 'EyeProlog registry contains ISO definitions, observability extensions, WFS tnot/1, and private library adapters');
|
|
3612
3727
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
3613
3728
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
3614
3729
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
3615
3730
|
assertEqual(registry.get('statistics', 2), null, 'statistics/2 is absent from the ISO registry');
|
|
3616
3731
|
assertEqual(Boolean(library.get('statistics', 0)), true, 'statistics/0 is an EyeProlog observability extension');
|
|
3617
3732
|
assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
|
|
3733
|
+
assertEqual(registry.get('tnot', 1), null, 'tnot/1 is absent from the ISO registry');
|
|
3734
|
+
assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
|
|
3618
3735
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
|
|
3619
3736
|
assertEqual(eyePrologPortableLibraryIndicators.length, 62, 'portable Prolog library count');
|
|
3620
3737
|
assertEqual(eyePrologInteropLibraryIndicators.length, 27, 'cross-implementation interop profile count');
|
|
3621
|
-
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists', 'common explicit library module profile');
|
|
3738
|
+
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext', 'common explicit library module profile');
|
|
3622
3739
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
3623
3740
|
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
3624
|
-
assertEqual(eyePrologInteropAutoload['call_nth/2'], '
|
|
3741
|
+
assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
|
|
3625
3742
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
3626
3743
|
assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
|
|
3627
3744
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
@@ -3671,6 +3788,28 @@ check(A, B, C, D, E, F) :-
|
|
|
3671
3788
|
assertEqual(program.findGroup('random', 3)?.module, 'random', 'random/3 is imported from library(random)');
|
|
3672
3789
|
},
|
|
3673
3790
|
},
|
|
3791
|
+
{
|
|
3792
|
+
name: 'aligned bundled libraries have no overlapping full-module exports',
|
|
3793
|
+
run: () => {
|
|
3794
|
+
const owners = new Map();
|
|
3795
|
+
for (const [moduleName, entry] of standardLibrarySources) {
|
|
3796
|
+
// library(prologue) is the documented legacy compatibility umbrella;
|
|
3797
|
+
// aligned libraries must remain pairwise collision-free so full
|
|
3798
|
+
// use_module/1 imports can be combined safely.
|
|
3799
|
+
if (moduleName === 'prologue') continue;
|
|
3800
|
+
const parsed = Program.parse(entry.source);
|
|
3801
|
+
const definition = parsed.modules.get(moduleName);
|
|
3802
|
+
if (!definition) throw new Error(`missing module declaration for ${moduleName}`);
|
|
3803
|
+
for (const indicator of definition.exports.keys()) {
|
|
3804
|
+
const previous = owners.get(indicator);
|
|
3805
|
+
if (previous != null) throw new Error(`duplicate export ${indicator}: ${previous}, ${moduleName}`);
|
|
3806
|
+
owners.set(indicator, moduleName);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
assertEqual(owners.get('countall/2'), 'iso_ext', 'countall/2 has one aligned owner');
|
|
3810
|
+
assertEqual(owners.get('call_nth/2'), 'iso_ext', 'call_nth/2 has one aligned owner');
|
|
3811
|
+
},
|
|
3812
|
+
},
|
|
3674
3813
|
{
|
|
3675
3814
|
name: 'portable library executes against the ISO-only registry',
|
|
3676
3815
|
run: () => {
|
|
@@ -4226,6 +4365,94 @@ function whiteBoxCases() {
|
|
|
4226
4365
|
assertEqual(program.findGroup('q', 1).tabled, false, 'q/1 not positively tabled');
|
|
4227
4366
|
},
|
|
4228
4367
|
},
|
|
4368
|
+
{
|
|
4369
|
+
name: 'large finite Datalog uses an indexed least model only for broad calls',
|
|
4370
|
+
run: () => {
|
|
4371
|
+
const edges = Array.from({ length: 130 }, (_, i) => `edge(n${i}, n${i + 1}).`).join('\n');
|
|
4372
|
+
const source = `${edges}\npath(X,Y) :- edge(X,Y).\npath(X,Y) :- edge(X,Z), path(Z,Y).\n`;
|
|
4373
|
+
const program = Program.parse(source);
|
|
4374
|
+
const group = program.findGroup('path', 2);
|
|
4375
|
+
assertEqual(group.datalogLeastModel, true, 'large range-restricted Datalog is eligible');
|
|
4376
|
+
|
|
4377
|
+
const broad = run(program, { goal: 'path(X,Y)' });
|
|
4378
|
+
assertEqual(broad.stdout.trim().split('\n').length, 8515, 'semi-naive closure has every chain pair');
|
|
4379
|
+
assertEqual(broad.stats.datalog_evaluations, 1, 'broad query builds one least model');
|
|
4380
|
+
|
|
4381
|
+
const ground = run(program, { goal: 'path(n0,n130)' });
|
|
4382
|
+
assertEqual(ground.stdout, 'path(n0, n130).\n', 'ground chain still succeeds');
|
|
4383
|
+
assertEqual(ground.stats.datalog_evaluations, 0, 'ground query keeps the ordinary indexed chain path');
|
|
4384
|
+
},
|
|
4385
|
+
},
|
|
4386
|
+
{
|
|
4387
|
+
name: 'findall length counting preserves multiplicity and observable bags',
|
|
4388
|
+
run: () => {
|
|
4389
|
+
const source = `
|
|
4390
|
+
p(a).
|
|
4391
|
+
p(b).
|
|
4392
|
+
q(X) :- p(X).
|
|
4393
|
+
q(X) :- p(X).
|
|
4394
|
+
bench(N) :- findall(X, q(X), Bag), length(Bag, N).
|
|
4395
|
+
collect(Bag,N) :- findall(X, q(X), Bag), length(Bag, N).
|
|
4396
|
+
`;
|
|
4397
|
+
assertEqual(run(source, { goal: 'bench(N)' }).stdout, 'bench(4).\n', 'dead bag counts all four proof solutions');
|
|
4398
|
+
assertEqual(run(source, { goal: 'collect(Bag,N)' }).stdout, 'collect("abab", 4).\n', 'bag exposed through the rule head is materialized');
|
|
4399
|
+
},
|
|
4400
|
+
},
|
|
4401
|
+
{
|
|
4402
|
+
name: 'ground negation probes a complete positive Datalog model',
|
|
4403
|
+
run: () => {
|
|
4404
|
+
const edges = Array.from({ length: 130 }, (_, i) => `edge(n${i}, n${i + 1}).`).join('\n');
|
|
4405
|
+
const source = `${edges}
|
|
4406
|
+
path(X,Y) :- edge(X,Y).
|
|
4407
|
+
path(X,Y) :- edge(X,Z), path(Z,Y).
|
|
4408
|
+
blocked(X,Y) :- path(X,Y).
|
|
4409
|
+
candidate(n0,n130).
|
|
4410
|
+
candidate(n130,n0).
|
|
4411
|
+
keep(X,Y) :- candidate(X,Y), \\+ blocked(X,Y).
|
|
4412
|
+
`;
|
|
4413
|
+
const result = run(source, { goal: 'keep(X,Y)' });
|
|
4414
|
+
assertEqual(result.stdout, 'keep(n130, n0).\n', 'negation sees the complete transitive closure');
|
|
4415
|
+
assertEqual(result.stats.datalog_evaluations, 1, 'ground truth probe reuses one complete least model');
|
|
4416
|
+
},
|
|
4417
|
+
},
|
|
4418
|
+
{
|
|
4419
|
+
name: 'tnot enables finite Datalog well-founded semantics without changing ISO negation',
|
|
4420
|
+
run: () => {
|
|
4421
|
+
const program = Program.parse(`
|
|
4422
|
+
move(1, 2).
|
|
4423
|
+
move(2, 3).
|
|
4424
|
+
move(3, 4).
|
|
4425
|
+
move(4, 1).
|
|
4426
|
+
win(X) :- move(X, Y), tnot(win(Y)).
|
|
4427
|
+
`);
|
|
4428
|
+
assertEqual(program.findGroup('win', 1).wfsDatalog, true, 'win/1 uses finite WFS evaluation');
|
|
4429
|
+
assertEqual(program.findGroup('win', 1).tabled, false, 'win/1 is not positive least-model tabled');
|
|
4430
|
+
const result = run(program, { goal: 'win(X)' });
|
|
4431
|
+
assertEqual(result.stdout.trim().split('\n').length, 4, 'negative cycle yields four conditional WFS answers');
|
|
4432
|
+
assertEqual(result.stats.wfs_fixpoint_rounds, 2, 'alternating fixed point converges in two rounds');
|
|
4433
|
+
assertEqual(result.stats.wfs_undefined_answers, 4, 'cycle answers are undefined rather than unconditional truths');
|
|
4434
|
+
|
|
4435
|
+
const nafProgram = Program.parse('p(X) :- \\+ q(X).\nq(X) :- p(X).\n');
|
|
4436
|
+
assertEqual(nafProgram.findGroup('p', 1).wfsDatalog, false, '\\+/1 remains ordinary negation as failure');
|
|
4437
|
+
},
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
name: 'finite WFS distinguishes true false and undefined for ground tnot calls',
|
|
4441
|
+
run: () => {
|
|
4442
|
+
const source = `
|
|
4443
|
+
move(a, b).
|
|
4444
|
+
move(c, d).
|
|
4445
|
+
move(d, c).
|
|
4446
|
+
win(X) :- move(X, Y), tnot(win(Y)).
|
|
4447
|
+
true_case :- win(a).
|
|
4448
|
+
false_case :- tnot(win(a)).
|
|
4449
|
+
undefined_case :- tnot(win(c)).
|
|
4450
|
+
`;
|
|
4451
|
+
assertEqual(run(source, { goal: 'true_case' }).stdout, 'true_case.\n', 'win(a) is true because b is false');
|
|
4452
|
+
assertEqual(run(source, { goal: 'false_case' }).stdout, '', 'tnot(win(a)) fails for a true atom');
|
|
4453
|
+
assertEqual(run(source, { goal: 'undefined_case' }).stdout, 'undefined_case.\n', 'tnot of an undefined WFS atom is a conditional success');
|
|
4454
|
+
},
|
|
4455
|
+
},
|
|
4229
4456
|
{
|
|
4230
4457
|
name: 'cyclic tabling reaches a complete fixed point',
|
|
4231
4458
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1121,13 +1121,36 @@ particular user is unblocked.
|
|
|
1121
1121
|
<figcaption>Absence becomes informative only inside a declared complete boundary: Clara is allowed because the event registry is complete and she is not on its blocked list.</figcaption>
|
|
1122
1122
|
</figure>
|
|
1123
1123
|
|
|
1124
|
-
|
|
1125
|
-
negate it from a higher layer. Use `--warnings`
|
|
1124
|
+
For ordinary `\+/1`, negative dependencies should normally be stratified:
|
|
1125
|
+
compute a lower relation, then negate it from a higher layer. Use `--warnings`
|
|
1126
|
+
to report negative recursion:
|
|
1126
1127
|
|
|
1127
1128
|
```sh
|
|
1128
1129
|
eyeprolog --warnings program.pl
|
|
1129
1130
|
```
|
|
1130
1131
|
|
|
1132
|
+
Some finite rule systems intentionally contain recursion through negation. In
|
|
1133
|
+
normal mode, EyeProlog provides explicit `tnot/1` for that case. When the
|
|
1134
|
+
reachable component is finite, function-free, and range-restricted Datalog,
|
|
1135
|
+
cycles through `tnot/1` are evaluated with the well-founded semantics (WFS)
|
|
1136
|
+
rather than ordinary negation-as-failure. WFS has three truth states: true,
|
|
1137
|
+
false, and undefined. A negative cycle may therefore produce a conditional
|
|
1138
|
+
answer instead of forcing an arbitrary true/false choice.
|
|
1139
|
+
|
|
1140
|
+
```eyeprolog
|
|
1141
|
+
move(a, b).
|
|
1142
|
+
move(b, a).
|
|
1143
|
+
win(X) :- move(X, Y), tnot(win(Y)).
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Here neither `win(a)` nor `win(b)` is unconditionally established; both belong
|
|
1147
|
+
to the undefined part of the well-founded model. EyeProlog exposes undefined
|
|
1148
|
+
WFS answers as conditional successes, including inside finite collectors.
|
|
1149
|
+
Direct calls to `tnot/1` must be ground. In WFS rules, variables occurring in
|
|
1150
|
+
the head or a negated literal must be range-restricted by positive body
|
|
1151
|
+
literals. Ordinary `\+/1` is unchanged, and strict ISO mode does not provide
|
|
1152
|
+
`tnot/1`.
|
|
1153
|
+
|
|
1131
1154
|
Universal checking needs no extension predicate: define the counterexample and
|
|
1132
1155
|
negate it.
|
|
1133
1156
|
|
|
@@ -1155,6 +1178,7 @@ Finite aggregation asks about a solution set:
|
|
|
1155
1178
|
```eyeprolog
|
|
1156
1179
|
:- use_module(library(aggregate)).
|
|
1157
1180
|
:- use_module(library(lists)).
|
|
1181
|
+
:- use_module(library(iso_ext)).
|
|
1158
1182
|
|
|
1159
1183
|
findall(Template, Goal, List).
|
|
1160
1184
|
countall(Goal, Count).
|
|
@@ -1573,9 +1597,20 @@ analysis, and other recursive relations with overlapping subproblems.
|
|
|
1573
1597
|
|
|
1574
1598
|
Ordinary goals use indexed depth-first resolution. Positive recursive groups
|
|
1575
1599
|
are tabled automatically. Bound recursive calls reuse answers and cyclic calls
|
|
1576
|
-
iterate toward a fixed point.
|
|
1577
|
-
|
|
1578
|
-
|
|
1600
|
+
iterate toward a fixed point. For sufficiently large finite, function-free
|
|
1601
|
+
Datalog dependency cones, EyeProlog may share one most-general relation table
|
|
1602
|
+
across call variants. This turns an open closure such as `tc(X, Y)` into one
|
|
1603
|
+
finite relation computation rather than many overlapping bound subcomputations;
|
|
1604
|
+
bound consumers can then use indexes over the stored answers. The exact
|
|
1605
|
+
admission policy is an engine optimization, not part of the language contract.
|
|
1606
|
+
Fully open or structurally unbounded recursive programs may retain ordinary
|
|
1607
|
+
resolution.
|
|
1608
|
+
|
|
1609
|
+
Recursive components with negative dependencies are not positive least-fixed-
|
|
1610
|
+
point problems. When such a component uses explicit `tnot/1` and satisfies the
|
|
1611
|
+
finite, range-restricted, function-free Datalog restrictions, EyeProlog instead
|
|
1612
|
+
computes the alternating fixed point of the well-founded semantics. Existing
|
|
1613
|
+
`\+/1` code remains ordinary negation-as-failure.
|
|
1579
1614
|
|
|
1580
1615
|
### Deeper implementation: how clause indexing stays semantic
|
|
1581
1616
|
|
|
@@ -1611,8 +1646,12 @@ eyeprolog --stats examples/observability-log-correlation.pl
|
|
|
1611
1646
|
The reported counters include completed goal lists, calls to the goal solver
|
|
1612
1647
|
and single-goal solver, unification attempts, maximum depth and goal-list size,
|
|
1613
1648
|
deterministic built-in successes and failures, and table fixed-point rounds.
|
|
1614
|
-
|
|
1615
|
-
|
|
1649
|
+
WFS execution additionally reports `wfs_fixpoint_rounds` and
|
|
1650
|
+
`wfs_undefined_answers`. The latter counts undefined-answer observations made
|
|
1651
|
+
while producing query results; it is an execution statistic, not a declaration
|
|
1652
|
+
that those atoms are true. All statistics describe work performed, not logical
|
|
1653
|
+
truth. Compare counters only across equivalent queries and the same
|
|
1654
|
+
implementation version.
|
|
1616
1655
|
|
|
1617
1656
|
Common sources of nontermination are recursive calls made before constraints,
|
|
1618
1657
|
ever-growing terms, infinite open mathematical queries, negative cycles, and
|
|
@@ -1946,6 +1985,8 @@ Part III moved from obtaining answers to trusting them:
|
|
|
1946
1985
|
- a query selects a question; a proof records one successful justification;
|
|
1947
1986
|
- an explicit integrity query identifies input that a host may reject;
|
|
1948
1987
|
- automatic tabling computes fixed points for eligible positive recursion;
|
|
1988
|
+
- explicit `tnot/1` gives eligible finite Datalog components well-founded,
|
|
1989
|
+
three-valued negation without changing ordinary `\+/1`;
|
|
1949
1990
|
- indexing and ready filters improve control without changing intended meaning;
|
|
1950
1991
|
- knowledge engineering separates sources, concepts, decisions, and reasons;
|
|
1951
1992
|
- explicit host boundaries divide input validation from logical derivation;
|
|
@@ -1964,7 +2005,10 @@ turned fixed-point ideas into a goal-directed technique that shares recursive
|
|
|
1964
2005
|
calls and accumulates answers. EyeProlog's automatic positive tabling is smaller
|
|
1965
2006
|
than the general systems in that literature, but inherits their central
|
|
1966
2007
|
insight: remembering a recursive question can change termination without
|
|
1967
|
-
changing what the relation says.
|
|
2008
|
+
changing what the relation says. For finite Datalog with recursion through
|
|
2009
|
+
explicit `tnot/1`, EyeProlog also uses the alternating-fixed-point account of
|
|
2010
|
+
the well-founded semantics so a negative cycle may remain undefined instead of
|
|
2011
|
+
being collapsed into ordinary negation-as-failure.
|
|
1968
2012
|
|
|
1969
2013
|
In parallel, deductive databases asked where facts come from and how derived
|
|
1970
2014
|
claims retain provenance. EyeProlog adopts the expectation that conclusions
|
|
@@ -3555,6 +3599,25 @@ component grows monotonically with newly discovered answers until no rule adds
|
|
|
3555
3599
|
another. The implementation is performing a local, demand-driven fixed-point
|
|
3556
3600
|
calculation.
|
|
3557
3601
|
|
|
3602
|
+
There is an important lifetime distinction between a table that is needed to
|
|
3603
|
+
finish one fixed point and a cache of tables retained for possible later reuse.
|
|
3604
|
+
For recursive grammars invoked through `phrase/2-3`, EyeProlog uses a separate
|
|
3605
|
+
invocation-keyed table scope rather than the caller's general table map. Static
|
|
3606
|
+
analysis also recognizes the normalized DCG form of strict list-tail recursion
|
|
3607
|
+
(for example `... --> [_], ...`). On a new input, such a grammar runs directly
|
|
3608
|
+
without automatic tabling because every recursive call consumes the input and
|
|
3609
|
+
there is no fixed-point cycle to close. This prevents a stream of distinct
|
|
3610
|
+
sequences from constructing a useless suffix-table family for every input.
|
|
3611
|
+
|
|
3612
|
+
The optimization is deliberately asymmetric. If the same `phrase/2-3`
|
|
3613
|
+
invocation repeats, normal tabling is allowed again so the compact completed
|
|
3614
|
+
table from that invocation can serve subsequent repetitions. That is useful for
|
|
3615
|
+
workloads that probe one grammar/input many thousands of times. Only one
|
|
3616
|
+
invocation signature is retained in the phrase scope, completed entries are
|
|
3617
|
+
still subject to a bound, and active fixed points are never evicted. Thus the
|
|
3618
|
+
policy limits cross-call retention without turning each distinct input into an
|
|
3619
|
+
O(cache-size) eviction pass or changing the declarative answers.
|
|
3620
|
+
|
|
3558
3621
|
**Exercises.**
|
|
3559
3622
|
|
|
3560
3623
|
1. Run the triple program with one, two, and three arguments bound. Compare the
|
|
@@ -6019,8 +6082,8 @@ between solution branches.
|
|
|
6019
6082
|
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
|
|
6020
6083
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6021
6084
|
| `library(dates)` | `difference/3` |
|
|
6022
|
-
| `library(iso_ext)` | `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
|
|
6023
|
-
| `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `
|
|
6085
|
+
| `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
|
|
6086
|
+
| `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
|
|
6024
6087
|
| `library(primes)` | `smallest_divisor_from/3` |
|
|
6025
6088
|
| `library(prologue)` | `member/2`, `append/3`, `length/2`, `between/3`, `select/3`, `succ/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `call_nth/2`, `freeze/2`, `foldl/4`, `foldl/5`, `foldl/6`, `countall/2` |
|
|
6026
6089
|
| `library(random)` | `random/3` |
|
|
@@ -6046,14 +6109,21 @@ This generator mode is important for portable stress and enumeration programs.
|
|
|
6046
6109
|
EyeProlog-only helpers such as `set_nth0/4`, `take/3`, `drop/3`, and `slice/4`
|
|
6047
6110
|
remain available for compatibility but are outside this profile.
|
|
6048
6111
|
|
|
6112
|
+
`library(iso_ext)` is also recognized as a common interop module name.
|
|
6113
|
+
EyeProlog exports `call_nth/2` there, matching Scryer's explicit import
|
|
6114
|
+
organization while still permitting unqualified autoloaded source. The aligned
|
|
6115
|
+
`library(lists)` and `library(iso_ext)` export sets are intentionally disjoint,
|
|
6116
|
+
so a source file may import both without an accidental predicate collision.
|
|
6117
|
+
`library(prologue)` is retained as an EyeProlog compatibility umbrella and may
|
|
6118
|
+
overlap these modules; use selective imports when legacy code combines it with
|
|
6119
|
+
the aligned libraries.
|
|
6120
|
+
|
|
6049
6121
|
Normal EyeProlog execution can autoload an otherwise undefined unqualified call
|
|
6050
6122
|
only when the interop table assigns it one canonical provider. Thus `member/2`
|
|
6051
|
-
autoloads from `library(lists)
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
`--no-autoload`, by the JavaScript option `autoload: false`, and always by
|
|
6056
|
-
`--iso-strict`.
|
|
6123
|
+
autoloads from `library(lists)`, `call_nth/2` from `library(iso_ext)`, and
|
|
6124
|
+
`between/3` from the internal `library(prologue)` implementation. Autoloading is
|
|
6125
|
+
disabled by `--no-autoload`, by the JavaScript option `autoload: false`, and
|
|
6126
|
+
always by `--iso-strict`.
|
|
6057
6127
|
|
|
6058
6128
|
`-w` / `--warnings` reports explicit non-profile library dependencies and calls
|
|
6059
6129
|
to non-profile predicates from common libraries. `--portable` turns those
|
|
@@ -6075,11 +6145,14 @@ cumulative and two-dimensional scheduling constraints, and unbounded-domain
|
|
|
6075
6145
|
propagation that are not yet exported here.
|
|
6076
6146
|
|
|
6077
6147
|
`library(iso_ext)` collects extensions commonly found in mature Prolog
|
|
6078
|
-
systems. `
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
`
|
|
6148
|
+
systems. `call_nth/2` exposes the ordinal number of each solution and supports
|
|
6149
|
+
the explicit import used by Scryer-style source; `forall/2` checks an action for
|
|
6150
|
+
every solution of a condition; `cfor/3` enumerates an inclusive evaluated
|
|
6151
|
+
integer range; `succ/2` relates adjacent nonnegative integers; `findall/4`
|
|
6152
|
+
collects into a difference list; and `variant/2` recognizes terms equal up to
|
|
6153
|
+
variable renaming. Its portable `countall/2` counts solutions without exposing
|
|
6154
|
+
a template. `countall/2` is not exported by `library(lists)`, avoiding a
|
|
6155
|
+
full-module import collision between the two libraries.
|
|
6083
6156
|
|
|
6084
6157
|
`uuid(+Seed0,-UUID,-Seed)` creates a version 4 UUID atom using `random/3`.
|
|
6085
6158
|
Passing the returned seed to the next call produces the next UUID; restarting
|
|
@@ -6272,6 +6345,7 @@ There is no `not/1` alias; use ISO `\+/1`. `forall/2` is available from
|
|
|
6272
6345
|
```eyeprolog
|
|
6273
6346
|
:- use_module(library(aggregate)).
|
|
6274
6347
|
:- use_module(library(lists)).
|
|
6348
|
+
:- use_module(library(iso_ext)).
|
|
6275
6349
|
|
|
6276
6350
|
cost(a, 8).
|
|
6277
6351
|
cost(b, 3).
|
package/why-eyeprolog.md
CHANGED
|
@@ -31,6 +31,8 @@ keeps a narrow architecture:
|
|
|
31
31
|
|
|
32
32
|
- one parser and term model;
|
|
33
33
|
- one solver with automatic tabling for eligible positive recursion;
|
|
34
|
+
- explicit `tnot/1` with well-founded semantics for finite, range-restricted,
|
|
35
|
+
function-free Datalog components;
|
|
34
36
|
- the ISO built-in registry;
|
|
35
37
|
- lean portable ISO Part 2 library modules;
|
|
36
38
|
- ISO Part 3 definite clause grammars and `phrase/2-3`;
|
|
@@ -43,6 +45,21 @@ relations such as list processing remain ordinary Prolog clauses imported with
|
|
|
43
45
|
standard sorting, arithmetic, meta-calls, streams, and database operations use
|
|
44
46
|
their ISO definitions directly.
|
|
45
47
|
|
|
48
|
+
|
|
49
|
+
## Why keep well-founded negation explicit?
|
|
50
|
+
|
|
51
|
+
Ordinary Prolog `\+/1` is negation-as-failure and remains useful when a closed,
|
|
52
|
+
usually stratified relation has already been computed. Non-stratified rule
|
|
53
|
+
systems need a different semantic choice. EyeProlog therefore does not silently
|
|
54
|
+
change `\+/1`; normal mode provides `tnot/1` for finite Datalog components that
|
|
55
|
+
need the three-valued well-founded semantics. This keeps the extension visible
|
|
56
|
+
in source code and lets strict ISO mode remove it cleanly.
|
|
57
|
+
|
|
58
|
+
The same boundary guides performance work. EyeProlog may evaluate large finite
|
|
59
|
+
positive Datalog closures with a shared relation-wide table, but the admission
|
|
60
|
+
heuristics are implementation details. Programs should rely on the documented
|
|
61
|
+
semantics and finiteness conditions, not on a particular internal threshold.
|
|
62
|
+
|
|
46
63
|
## Why proofs?
|
|
47
64
|
|
|
48
65
|
An answer says that a goal succeeded. A proof records one successful route
|
|
File without changes
|