eyeprolog 1.3.4 → 1.3.8

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.
@@ -3307,6 +3307,68 @@ check(A, B, C, D, E, F) :-
3307
3307
  assertIncludes(result.stdout, 'check("ab", "a", "ab", [a - 1, b - 2], 6, "a").\n', 'stdout');
3308
3308
  },
3309
3309
  },
3310
+ {
3311
+ name: 'library(lambda) supports Scryer-style maplist lambdas',
3312
+ run: () => {
3313
+ const source = String.raw`:- use_module(library(lambda)).
3314
+ :- use_module(library(lists)).
3315
+ answer :- maplist(\X^(X>3), [4,5,9]).
3316
+ `;
3317
+ const result = run(source, { goal: 'answer' });
3318
+ assertEqual(result.stdout, 'answer.\n', 'stdout');
3319
+ },
3320
+ },
3321
+ {
3322
+ name: 'library(lambda) refreshes local variables on each invocation',
3323
+ run: () => {
3324
+ const source = String.raw`:- use_module(library(lambda)).
3325
+ :- use_module(library(lists)).
3326
+ answer :- maplist(\X^(Y=X), [a,b]).
3327
+ `;
3328
+ const result = run(source, { goal: 'answer' });
3329
+ assertEqual(result.stdout, 'answer.\n', 'fresh local variables');
3330
+ },
3331
+ },
3332
+ {
3333
+ name: 'library(lambda) preserves explicitly free variables with +\\',
3334
+ run: () => {
3335
+ const source = String.raw`:- use_module(library(lambda)).
3336
+ :- use_module(library(lists)).
3337
+ answer(Y) :- maplist(Y+\X^(Y=X), [a,a]).
3338
+ `;
3339
+ const program = Program.parse(source);
3340
+ const imported = [...program.operators.values()].find((operator) => operator.name === '+\\');
3341
+ assertEqual(`${imported?.priority}/${imported?.specifier}`, '201/xfx', '+\\ operator import');
3342
+ const result = run(program, { goal: 'answer(Y)' });
3343
+ assertEqual(result.stdout, 'answer(a).\n', 'free variable sharing');
3344
+ },
3345
+ },
3346
+ {
3347
+ name: 'library(lambda) supports continuations and seven call arguments',
3348
+ run: () => {
3349
+ const source = String.raw`:- use_module(library(lambda)).
3350
+ f(x,y).
3351
+ tuple(a,b,c,d,e,f,g).
3352
+ answer(A,B) :-
3353
+ call(\X^f(X), A, B),
3354
+ call(\X^Y^f(X,Y), A, B),
3355
+ call(\P^Q^R^S^T^U^V^tuple(P,Q,R,S,T,U,V), a,b,c,d,e,f,g).
3356
+ `;
3357
+ const result = run(source, { goal: 'answer(A,B)' });
3358
+ assertEqual(result.stdout, 'answer(x, y).\n', 'continuations');
3359
+ },
3360
+ },
3361
+ {
3362
+ name: 'library(lambda) diagnoses a missing lambda parameter',
3363
+ run: () => {
3364
+ const source = String.raw`:- use_module(library(lambda)).
3365
+ answer(ok) :-
3366
+ catch(call(\X^true), error(existence_error(lambda_parameter,_),_), true).
3367
+ `;
3368
+ const result = run(source, { goal: 'answer(X)' });
3369
+ assertEqual(result.stdout, 'answer(ok).\n', 'lambda parameter error');
3370
+ },
3371
+ },
3310
3372
  {
3311
3373
  name: 'autoload metadata records canonical interop imports',
3312
3374
  run: () => {
@@ -3444,6 +3506,103 @@ check(A, B, C, D, E, F) :-
3444
3506
  assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
3445
3507
  },
3446
3508
  },
3509
+ {
3510
+ name: 'recursive phrase tables stay bounded across distinct inputs (issues #28/#48)',
3511
+ run: () => {
3512
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
3513
+ const programText = `
3514
+ :- use_module(library(prologue)).
3515
+ ... --> [].
3516
+ ... --> [_], ... .
3517
+
3518
+ vchars("0123456789.").
3519
+
3520
+ ti(Nth:E:Chs, Pot, Start) :-
3521
+ length(Chs, _),
3522
+ call_nth(chs(Chs), Nth), Nth >= Start,
3523
+ Nth mod 10^Pot =:= 0,
3524
+ E = cnt.
3525
+
3526
+ chs(Chs) :-
3527
+ vchars(VChs),
3528
+ maplistch(Chs, VChs),
3529
+ catch(number_chars(_, Chs), error(syntax_error(_), _), false),
3530
+ \\+ phrase((..., ("//"|"++")), Chs).
3531
+
3532
+ maplistch([], _).
3533
+ maplistch([Ch|Chs], VChs) :-
3534
+ member(Ch, VChs),
3535
+ maplistch(Chs, VChs).
3536
+ `;
3537
+ const script = `
3538
+ import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
3539
+ const program = Program.parse(${JSON.stringify(programText)}, { autoloadGoals: ['ti(R,3,0)'] });
3540
+ const listTailGroup = program.findGroup('...', 2, 'user');
3541
+ if (listTailGroup?.listTailRecursive !== true) {
3542
+ throw new Error('recursive DCG tail-consumption was not recognized');
3543
+ }
3544
+ const solver = new Solver(program, { registry: getEyePrologRegistry(), maxMemoryBytes: Infinity });
3545
+ const goal = parseGoalText('ti(R,3,0)', {
3546
+ doubleQuotes: 'chars',
3547
+ operatorDefinitions: [...program.operators.values()],
3548
+ });
3549
+ let count = 0;
3550
+ for (const _ of solver.solve([goal], new Env(), 0)) {
3551
+ if (++count === 20) break;
3552
+ }
3553
+ if (count !== 20) throw new Error('unexpected checkpoint count: ' + count);
3554
+ const scope = solver.innerTableScopes.get('phrase');
3555
+ if (scope == null) throw new Error('missing phrase table scope');
3556
+ if (scope.memo.size !== 0) throw new Error('distinct tail-DCG inputs should not retain phrase tables: ' + scope.memo.size);
3557
+ process.stdout.write(count + ':' + scope.memo.size);
3558
+ `;
3559
+ const result = spawnSync(process.execPath, [
3560
+ // This corresponds to 20,000 accepted number candidates. Before the
3561
+ // phrase-local cache, every distinct recursive DCG tail stayed rooted
3562
+ // in the caller memo and this workload exhausted a 32 MiB old space.
3563
+ '--max-old-space-size=32',
3564
+ '--input-type=module',
3565
+ '--eval',
3566
+ script,
3567
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
3568
+ if (result.error) throw result.error;
3569
+ assertEqual(result.status, 0, `bounded phrase-table child status; stderr=${result.stderr}`);
3570
+ const [countText, cacheText] = result.stdout.trim().split(':');
3571
+ assertEqual(countText, '20', 'recursive phrase checkpoints');
3572
+ assertEqual(cacheText, '0', 'distinct tail-DCG phrase cache');
3573
+ },
3574
+ },
3575
+ {
3576
+ name: 'repeated identical phrase invocation reuses a compact table (issue #48)',
3577
+ run: () => {
3578
+ const program = Program.parse(`
3579
+ :- use_module(library(prologue)).
3580
+ ... --> [].
3581
+ ... --> [_], ... .
3582
+ `, { autoloadGoals: ['phrase((...,("ba"|"gh")),"abcdef")'] });
3583
+ const solver = new Solver(program, { registry: getEyePrologRegistry(), maxMemoryBytes: Infinity });
3584
+ const goal = parseGoalText('phrase((...,("ba"|"gh")),"abcdef")', {
3585
+ doubleQuotes: 'chars',
3586
+ operatorDefinitions: [...program.operators.values()],
3587
+ });
3588
+ const runFailure = () => {
3589
+ let answers = 0;
3590
+ for (const _ of solver.solve([goal], new Env(), 0)) answers++;
3591
+ assertEqual(answers, 0, 'failing phrase answer count');
3592
+ };
3593
+ runFailure();
3594
+ assertEqual(solver.stats.table_fixpoint_rounds, 0, 'first distinct tail-DCG invocation stays untabled');
3595
+ runFailure();
3596
+ const roundsAfterSecond = solver.stats.table_fixpoint_rounds;
3597
+ if (roundsAfterSecond <= 0) throw new Error('repeated phrase invocation did not build reusable table');
3598
+ const scope = solver.innerTableScopes.get('phrase');
3599
+ if (scope == null || scope.memo.size <= 0 || scope.memo.size > 1024) {
3600
+ throw new Error('unexpected repeated phrase table size: ' + (scope?.memo.size ?? 'missing'));
3601
+ }
3602
+ runFailure();
3603
+ assertEqual(solver.stats.table_fixpoint_rounds, roundsAfterSecond, 'third identical phrase call reuses completed table');
3604
+ },
3605
+ },
3447
3606
  {
3448
3607
  name: 'host Map/Set capacity errors become resource_error(memory)',
3449
3608
  run: () => {
@@ -3626,24 +3785,26 @@ check(A, B, C, D, E, F) :-
3626
3785
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
3627
3786
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
3628
3787
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
3629
- assertEqual(library.defs.size, 155, 'EyeProlog registry contains ISO definitions, observability extensions, and private library adapters');
3788
+ assertEqual(library.defs.size, 156, 'EyeProlog registry contains ISO definitions, observability extensions, WFS tnot/1, and private library adapters');
3630
3789
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
3631
3790
  assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
3632
3791
  assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
3633
3792
  assertEqual(registry.get('statistics', 2), null, 'statistics/2 is absent from the ISO registry');
3634
3793
  assertEqual(Boolean(library.get('statistics', 0)), true, 'statistics/0 is an EyeProlog observability extension');
3635
3794
  assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
3795
+ assertEqual(registry.get('tnot', 1), null, 'tnot/1 is absent from the ISO registry');
3796
+ assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
3636
3797
  assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
3637
- assertEqual(eyePrologPortableLibraryIndicators.length, 62, 'portable Prolog library count');
3798
+ assertEqual(eyePrologPortableLibraryIndicators.length, 86, 'portable Prolog library count');
3638
3799
  assertEqual(eyePrologInteropLibraryIndicators.length, 27, 'cross-implementation interop profile count');
3639
- assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext', 'common explicit library module profile');
3800
+ assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
3640
3801
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
3641
3802
  assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
3642
3803
  assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
3643
3804
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
3644
3805
  assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
3645
3806
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
3646
- assertEqual(eyePrologLibraryIndicators.length, 102, 'complete EyeProlog library surface');
3807
+ assertEqual(eyePrologLibraryIndicators.length, 126, 'complete EyeProlog library surface');
3647
3808
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
3648
3809
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
3649
3810
  assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
@@ -4266,6 +4427,94 @@ function whiteBoxCases() {
4266
4427
  assertEqual(program.findGroup('q', 1).tabled, false, 'q/1 not positively tabled');
4267
4428
  },
4268
4429
  },
4430
+ {
4431
+ name: 'large finite Datalog uses an indexed least model only for broad calls',
4432
+ run: () => {
4433
+ const edges = Array.from({ length: 130 }, (_, i) => `edge(n${i}, n${i + 1}).`).join('\n');
4434
+ const source = `${edges}\npath(X,Y) :- edge(X,Y).\npath(X,Y) :- edge(X,Z), path(Z,Y).\n`;
4435
+ const program = Program.parse(source);
4436
+ const group = program.findGroup('path', 2);
4437
+ assertEqual(group.datalogLeastModel, true, 'large range-restricted Datalog is eligible');
4438
+
4439
+ const broad = run(program, { goal: 'path(X,Y)' });
4440
+ assertEqual(broad.stdout.trim().split('\n').length, 8515, 'semi-naive closure has every chain pair');
4441
+ assertEqual(broad.stats.datalog_evaluations, 1, 'broad query builds one least model');
4442
+
4443
+ const ground = run(program, { goal: 'path(n0,n130)' });
4444
+ assertEqual(ground.stdout, 'path(n0, n130).\n', 'ground chain still succeeds');
4445
+ assertEqual(ground.stats.datalog_evaluations, 0, 'ground query keeps the ordinary indexed chain path');
4446
+ },
4447
+ },
4448
+ {
4449
+ name: 'findall length counting preserves multiplicity and observable bags',
4450
+ run: () => {
4451
+ const source = `
4452
+ p(a).
4453
+ p(b).
4454
+ q(X) :- p(X).
4455
+ q(X) :- p(X).
4456
+ bench(N) :- findall(X, q(X), Bag), length(Bag, N).
4457
+ collect(Bag,N) :- findall(X, q(X), Bag), length(Bag, N).
4458
+ `;
4459
+ assertEqual(run(source, { goal: 'bench(N)' }).stdout, 'bench(4).\n', 'dead bag counts all four proof solutions');
4460
+ assertEqual(run(source, { goal: 'collect(Bag,N)' }).stdout, 'collect("abab", 4).\n', 'bag exposed through the rule head is materialized');
4461
+ },
4462
+ },
4463
+ {
4464
+ name: 'ground negation probes a complete positive Datalog model',
4465
+ run: () => {
4466
+ const edges = Array.from({ length: 130 }, (_, i) => `edge(n${i}, n${i + 1}).`).join('\n');
4467
+ const source = `${edges}
4468
+ path(X,Y) :- edge(X,Y).
4469
+ path(X,Y) :- edge(X,Z), path(Z,Y).
4470
+ blocked(X,Y) :- path(X,Y).
4471
+ candidate(n0,n130).
4472
+ candidate(n130,n0).
4473
+ keep(X,Y) :- candidate(X,Y), \\+ blocked(X,Y).
4474
+ `;
4475
+ const result = run(source, { goal: 'keep(X,Y)' });
4476
+ assertEqual(result.stdout, 'keep(n130, n0).\n', 'negation sees the complete transitive closure');
4477
+ assertEqual(result.stats.datalog_evaluations, 1, 'ground truth probe reuses one complete least model');
4478
+ },
4479
+ },
4480
+ {
4481
+ name: 'tnot enables finite Datalog well-founded semantics without changing ISO negation',
4482
+ run: () => {
4483
+ const program = Program.parse(`
4484
+ move(1, 2).
4485
+ move(2, 3).
4486
+ move(3, 4).
4487
+ move(4, 1).
4488
+ win(X) :- move(X, Y), tnot(win(Y)).
4489
+ `);
4490
+ assertEqual(program.findGroup('win', 1).wfsDatalog, true, 'win/1 uses finite WFS evaluation');
4491
+ assertEqual(program.findGroup('win', 1).tabled, false, 'win/1 is not positive least-model tabled');
4492
+ const result = run(program, { goal: 'win(X)' });
4493
+ assertEqual(result.stdout.trim().split('\n').length, 4, 'negative cycle yields four conditional WFS answers');
4494
+ assertEqual(result.stats.wfs_fixpoint_rounds, 2, 'alternating fixed point converges in two rounds');
4495
+ assertEqual(result.stats.wfs_undefined_answers, 4, 'cycle answers are undefined rather than unconditional truths');
4496
+
4497
+ const nafProgram = Program.parse('p(X) :- \\+ q(X).\nq(X) :- p(X).\n');
4498
+ assertEqual(nafProgram.findGroup('p', 1).wfsDatalog, false, '\\+/1 remains ordinary negation as failure');
4499
+ },
4500
+ },
4501
+ {
4502
+ name: 'finite WFS distinguishes true false and undefined for ground tnot calls',
4503
+ run: () => {
4504
+ const source = `
4505
+ move(a, b).
4506
+ move(c, d).
4507
+ move(d, c).
4508
+ win(X) :- move(X, Y), tnot(win(Y)).
4509
+ true_case :- win(a).
4510
+ false_case :- tnot(win(a)).
4511
+ undefined_case :- tnot(win(c)).
4512
+ `;
4513
+ assertEqual(run(source, { goal: 'true_case' }).stdout, 'true_case.\n', 'win(a) is true because b is false');
4514
+ assertEqual(run(source, { goal: 'false_case' }).stdout, '', 'tnot(win(a)) fails for a true atom');
4515
+ assertEqual(run(source, { goal: 'undefined_case' }).stdout, 'undefined_case.\n', 'tnot of an undefined WFS atom is a conditional success');
4516
+ },
4517
+ },
4269
4518
  {
4270
4519
  name: 'cyclic tabling reaches a complete fixed point',
4271
4520
  run: () => {
@@ -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
- Negative dependencies should be stratified: compute a lower relation, then
1125
- negate it from a higher layer. Use `--warnings` to report negative recursion:
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
 
@@ -1574,9 +1597,20 @@ analysis, and other recursive relations with overlapping subproblems.
1574
1597
 
1575
1598
  Ordinary goals use indexed depth-first resolution. Positive recursive groups
1576
1599
  are tabled automatically. Bound recursive calls reuse answers and cyclic calls
1577
- iterate toward a fixed point. Fully open or structurally unbounded calls may
1578
- retain ordinary resolution. Recursive components with negative dependencies
1579
- are not positive fixed points.
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.
1580
1614
 
1581
1615
  ### Deeper implementation: how clause indexing stays semantic
1582
1616
 
@@ -1612,8 +1646,12 @@ eyeprolog --stats examples/observability-log-correlation.pl
1612
1646
  The reported counters include completed goal lists, calls to the goal solver
1613
1647
  and single-goal solver, unification attempts, maximum depth and goal-list size,
1614
1648
  deterministic built-in successes and failures, and table fixed-point rounds.
1615
- They describe work performed, not logical truth. Compare counters only across
1616
- equivalent queries and the same implementation version.
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.
1617
1655
 
1618
1656
  Common sources of nontermination are recursive calls made before constraints,
1619
1657
  ever-growing terms, infinite open mathematical queries, negative cycles, and
@@ -1947,6 +1985,8 @@ Part III moved from obtaining answers to trusting them:
1947
1985
  - a query selects a question; a proof records one successful justification;
1948
1986
  - an explicit integrity query identifies input that a host may reject;
1949
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`;
1950
1990
  - indexing and ready filters improve control without changing intended meaning;
1951
1991
  - knowledge engineering separates sources, concepts, decisions, and reasons;
1952
1992
  - explicit host boundaries divide input validation from logical derivation;
@@ -1965,7 +2005,10 @@ turned fixed-point ideas into a goal-directed technique that shares recursive
1965
2005
  calls and accumulates answers. EyeProlog's automatic positive tabling is smaller
1966
2006
  than the general systems in that literature, but inherits their central
1967
2007
  insight: remembering a recursive question can change termination without
1968
- 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.
1969
2012
 
1970
2013
  In parallel, deductive databases asked where facts come from and how derived
1971
2014
  claims retain provenance. EyeProlog adopts the expectation that conclusions
@@ -3556,6 +3599,25 @@ component grows monotonically with newly discovered answers until no rule adds
3556
3599
  another. The implementation is performing a local, demand-driven fixed-point
3557
3600
  calculation.
3558
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
+
3559
3621
  **Exercises.**
3560
3622
 
3561
3623
  1. Run the triple program with one, two, and three arguments bound. Compare the
@@ -5977,19 +6039,19 @@ so side effects occur in Prolog execution order.
5977
6039
 
5978
6040
  ### The EyeProlog library
5979
6041
 
5980
- EyeProlog exposes **102 library predicate indicators** in addition to the 129
5981
- indicators in its isolated ISO profile. **63 are defined as ordinary Prolog
6042
+ EyeProlog exposes **126 library predicate indicators** in addition to the 129
6043
+ indicators in its isolated ISO profile. **87 are defined as ordinary Prolog
5982
6044
  clauses** in focused modules under `src/lib/`. The remaining 39
5983
6045
  are public wrappers around backtrackable host support: Prologue `call_nth/2` and
5984
6046
  `freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
5985
- resulting normal EyeProlog language surface is therefore **228 public predicate
6047
+ resulting normal EyeProlog language surface is therefore **255 public predicate
5986
6048
  indicators**. Internally, the runtime registry contains the 129 ISO definitions
5987
6049
  plus 23 private library adapters; public relations remain module source clauses.
5988
6050
 
5989
6051
  The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
5990
- `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lists.pl`,
5991
- `src/lib/primes.pl`, `src/lib/prologue.pl`, `src/lib/random.pl`,
5992
- `src/lib/strings.pl`, and `src/lib/uuid.pl`. Each declares a same-named module
6052
+ `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lambda.pl`,
6053
+ `src/lib/lists.pl`, `src/lib/primes.pl`, `src/lib/prologue.pl`,
6054
+ `src/lib/random.pl`, `src/lib/strings.pl`, and `src/lib/uuid.pl`. Each declares a same-named module
5993
6055
  with `module/2`; there is no catch-all `library(eyeprolog)`. A program imports
5994
6056
  only the modules it needs, and
5995
6057
  `use_module/2` can select an even smaller indicator list. The Prologue
@@ -6021,6 +6083,7 @@ between solution branches.
6021
6083
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
6022
6084
  | `library(dates)` | `difference/3` |
6023
6085
  | `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
6086
+ | `library(lambda)` | `^/3`, `^/4`, `^/5`, `^/6`, `^/7`, `^/8`, `^/9`, `^/10`, `\/1`, `\/2`, `\/3`, `\/4`, `\/5`, `\/6`, `\/7`, `\/8`, `+\/2`, `+\/3`, `+\/4`, `+\/5`, `+\/6`, `+\/7`, `+\/8`, `+\/9` |
6024
6087
  | `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` |
6025
6088
  | `library(primes)` | `smallest_divisor_from/3` |
6026
6089
  | `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` |
@@ -6056,6 +6119,39 @@ so a source file may import both without an accidental predicate collision.
6056
6119
  overlap these modules; use selective imports when legacy code combines it with
6057
6120
  the aligned libraries.
6058
6121
 
6122
+ `library(lambda)` is an explicitly imported Scryer-aligned module for
6123
+ higher-order programming. Its implementation is adapted from Ulrich Neumerkel's
6124
+ `library(lambda)` as distributed by Scryer Prolog, retaining the upstream
6125
+ copyright and redistribution notice. The public syntax is:
6126
+
6127
+ ```text
6128
+ \X1^X2^...^XN^Goal
6129
+ Free+\X1^X2^...^XN^Goal
6130
+ ```
6131
+
6132
+ The first form has no explicitly shared free variables. Before each invocation
6133
+ EyeProlog copies the closure term, so local variables are fresh on successive
6134
+ `maplist/2-8`, `foldl/4-6`, or direct `call/N` uses. In the second form, the
6135
+ variables contained in `Free` remain shared with the surrounding goal. Importing
6136
+ the library installs `+\` as a priority-201 `xfx` operator; `\` and `^` use
6137
+ their existing ISO operator definitions. Parenthesize lower-priority goal
6138
+ operators after `^`, for example `\X^(X > 3)`.
6139
+
6140
+ A continuation lambda may leave arguments for a later call:
6141
+
6142
+ ```text
6143
+ f(x, y).
6144
+
6145
+ answer(A, B) :- call(\X^f(X), A, B).
6146
+ ```
6147
+
6148
+ This is equivalent to supplying both arguments directly. A lambda that is
6149
+ called with too few parameters raises `existence_error(lambda_parameter, ...)`,
6150
+ matching the diagnostic intent of the Scryer library. EyeProlog uses its ISO
6151
+ `copy_term/2` implementation for the fresh-copy step; in EyeProlog this gives
6152
+ the natural-copy behavior needed by the library without requiring a separate
6153
+ `copy_term_nat/2` predicate.
6154
+
6059
6155
  Normal EyeProlog execution can autoload an otherwise undefined unqualified call
6060
6156
  only when the interop table assigns it one canonical provider. Thus `member/2`
6061
6157
  autoloads from `library(lists)`, `call_nth/2` from `library(iso_ext)`, and
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