eyeprolog 1.3.7 → 1.3.9

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 CHANGED
@@ -189,12 +189,13 @@ noun --> [world] | [prolog].
189
189
  %% goal: phrase(sentence, Words)
190
190
  ```
191
191
 
192
- EyeProlog also adds 102 public library predicate indicators to its 129-entry ISO
193
- profile. **63 are implemented entirely as ordinary Prolog clauses** in focused
194
- modules under `src/lib/`; the control predicates and finite-domain
192
+ EyeProlog also adds 126 public library predicate indicators to its 129-entry ISO
193
+ profile. **87 are implemented entirely as ordinary Prolog clauses** in focused
194
+ modules under `src/lib/`; the remaining control predicates and finite-domain
195
195
  `library(clpz)` kernel use backtrackable host support.
196
196
  They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
197
- `library(lists)`, `library(strings)`, `library(aggregate)`, or `library(clpz)`.
197
+ `library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`, or
198
+ `library(clpz)`.
198
199
  Portable text
199
200
  predicates use ISO atoms or character lists. The old catch-all
200
201
  `library(eyeprolog)` module is no longer needed.
@@ -207,7 +208,9 @@ Trealla/Scryer organization for predicates such as `member/2`, `memberchk/2`,
207
208
  `append/2-3`, `nth0/3-4`, `nth1/3-4`, `length/2`, `maplist/2-8`, and
208
209
  `foldl/4-6`. Its `length/2` remains relational: with both arguments variable,
209
210
  `length(Xs, N)` enumerates lists of increasing length together with `N = 0, 1,
210
- 2, ...`.
211
+ 2, ...`. Open-ended generation uses the normal memory guard with recovery
212
+ headroom, so an exhausted finite heap is reported as a catchable
213
+ `resource_error(memory)` instead of degenerating into quadratic list checks.
211
214
 
212
215
  `library(iso_ext)` is also accepted as a common interop module name.
213
216
  EyeProlog exports `call_nth/2` there, so Scryer-style source can explicitly use
@@ -217,6 +220,24 @@ they can be imported together without an accidental import conflict. EyeProlog's
217
220
  legacy `library(prologue)` remains a compatibility umbrella and should be
218
221
  selectively imported when mixed with the aligned modules.
219
222
 
223
+ `library(lambda)` follows Scryer's higher-order lambda notation, adapted from
224
+ Ulrich Neumerkel's permissively licensed implementation. Importing it installs
225
+ the `+\` operator and enables closures such as `\X^Goal` and
226
+ `Free+\X^Goal`. Parameters are supplied by `call/N`; variables not listed in
227
+ `Free` are copied afresh for each invocation, while explicitly free variables
228
+ remain shared. For example:
229
+
230
+ ```prolog
231
+ :- use_module(library(lambda)).
232
+ :- use_module(library(lists)).
233
+
234
+ all_positive(Xs) :- maplist(\X^(X > 0), Xs).
235
+ all_equal(Y, Xs) :- maplist(Y+\X^(X = Y), Xs).
236
+ ```
237
+
238
+ The explicit import is intentional: unlike ordinary predicate autoloading, the
239
+ lambda syntax also changes the active operator table.
240
+
220
241
  Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
221
242
  predicate only when the interop profile has one canonical EyeProlog provider.
222
243
  For example, `member/2` autoloads from `library(lists)`, `call_nth/2` from
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.7",
6
+ "version": "1.3.9",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -0,0 +1,116 @@
1
+ /*
2
+ Author: Ulrich Neumerkel
3
+ E-mail: ulrich@complang.tuwien.ac.at
4
+ Copyright (C): 2009 Ulrich Neumerkel. All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are
8
+ met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in the
15
+ documentation and/or other materials provided with the distribution.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY Ulrich Neumerkel ``AS IS'' AND ANY
18
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ulrich Neumerkel OR
21
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ The views and conclusions contained in the software and documentation
30
+ are those of the authors and should not be interpreted as representing
31
+ official policies, either expressed or implied, of Ulrich Neumerkel.
32
+
33
+ Adapted for EyeProlog from Scryer Prolog library(lambda):
34
+ https://github.com/mthom/scryer-prolog/blob/master/src/lib/lambda.pl
35
+ */
36
+
37
+ /** Lambda expressions for higher-order programming based on call/N.
38
+
39
+ Supported forms follow Scryer Prolog library(lambda):
40
+
41
+ Free+\X1^X2^...^XN^Goal
42
+ \X1^X2^...^XN^Goal
43
+
44
+ The second form is shorthand for a lambda with no explicitly shared free
45
+ variables. Variables not listed in Free are copied afresh for each call.
46
+ */
47
+
48
+ :- module(lambda, [
49
+ (^)/3, (^)/4, (^)/5, (^)/6, (^)/7, (^)/8, (^)/9, (^)/10,
50
+ (\)/1, (\)/2, (\)/3, (\)/4, (\)/5, (\)/6, (\)/7, (\)/8,
51
+ (+\)/2, (+\)/3, (+\)/4, (+\)/5, (+\)/6, (+\)/7, (+\)/8, (+\)/9
52
+ ]).
53
+
54
+ % Scryer exports this operator from library(lambda). EyeProlog applies module
55
+ % operator directives while loading the library, so importing the module makes
56
+ % the same syntax available to the importing compilation unit and REPL state.
57
+ :- op(201, xfx, +\).
58
+
59
+ :- meta_predicate(^(?, 0, ?)).
60
+ :- meta_predicate(^(?, 1, ?, ?)).
61
+ :- meta_predicate(^(?, 2, ?, ?, ?)).
62
+ :- meta_predicate(^(?, 3, ?, ?, ?, ?)).
63
+ :- meta_predicate(^(?, 4, ?, ?, ?, ?, ?)).
64
+ :- meta_predicate(^(?, 5, ?, ?, ?, ?, ?, ?)).
65
+ :- meta_predicate(^(?, 6, ?, ?, ?, ?, ?, ?, ?)).
66
+ :- meta_predicate(^(?, 7, ?, ?, ?, ?, ?, ?, ?, ?)).
67
+ :- meta_predicate(\(0)).
68
+ :- meta_predicate(\(1, ?)).
69
+ :- meta_predicate(\(2, ?, ?)).
70
+ :- meta_predicate(\(3, ?, ?, ?)).
71
+ :- meta_predicate(\(4, ?, ?, ?, ?)).
72
+ :- meta_predicate(\(5, ?, ?, ?, ?, ?)).
73
+ :- meta_predicate(\(6, ?, ?, ?, ?, ?, ?)).
74
+ :- meta_predicate(\(7, ?, ?, ?, ?, ?, ?, ?)).
75
+ :- meta_predicate(+\(?, 0)).
76
+ :- meta_predicate(+\(?, 1, ?)).
77
+ :- meta_predicate(+\(?, 2, ?, ?)).
78
+ :- meta_predicate(+\(?, 3, ?, ?, ?)).
79
+ :- meta_predicate(+\(?, 4, ?, ?, ?, ?)).
80
+ :- meta_predicate(+\(?, 5, ?, ?, ?, ?, ?)).
81
+ :- meta_predicate(+\(?, 6, ?, ?, ?, ?, ?, ?)).
82
+ :- meta_predicate(+\(?, 7, ?, ?, ?, ?, ?, ?, ?)).
83
+
84
+ ^(V1, C0, V1) :- lambda__no_hat_call(C0).
85
+ ^(V1, C1, V1, V2) :- call(C1, V2).
86
+ ^(V1, C2, V1, V2, V3) :- call(C2, V2, V3).
87
+ ^(V1, C3, V1, V2, V3, V4) :- call(C3, V2, V3, V4).
88
+ ^(V1, C4, V1, V2, V3, V4, V5) :- call(C4, V2, V3, V4, V5).
89
+ ^(V1, C5, V1, V2, V3, V4, V5, V6) :- call(C5, V2, V3, V4, V5, V6).
90
+ ^(V1, C6, V1, V2, V3, V4, V5, V6, V7) :- call(C6, V2, V3, V4, V5, V6, V7).
91
+ ^(V1, C7, V1, V2, V3, V4, V5, V6, V7, V8) :- call(C7, V2, V3, V4, V5, V6, V7, V8).
92
+
93
+ \(FC0) :- copy_term(FC0, C0), lambda__no_hat_call(C0).
94
+ \(FC1, V1) :- copy_term(FC1, C1), call(C1, V1).
95
+ \(FC2, V1, V2) :- copy_term(FC2, C2), call(C2, V1, V2).
96
+ \(FC3, V1, V2, V3) :- copy_term(FC3, C3), call(C3, V1, V2, V3).
97
+ \(FC4, V1, V2, V3, V4) :- copy_term(FC4, C4), call(C4, V1, V2, V3, V4).
98
+ \(FC5, V1, V2, V3, V4, V5) :- copy_term(FC5, C5), call(C5, V1, V2, V3, V4, V5).
99
+ \(FC6, V1, V2, V3, V4, V5, V6) :- copy_term(FC6, C6), call(C6, V1, V2, V3, V4, V5, V6).
100
+ \(FC7, V1, V2, V3, V4, V5, V6, V7) :- copy_term(FC7, C7), call(C7, V1, V2, V3, V4, V5, V6, V7).
101
+
102
+ +\(GV, FC0) :- copy_term(GV+FC0, GV+C0), lambda__no_hat_call(C0).
103
+ +\(GV, FC1, V1) :- copy_term(GV+FC1, GV+C1), call(C1, V1).
104
+ +\(GV, FC2, V1, V2) :- copy_term(GV+FC2, GV+C2), call(C2, V1, V2).
105
+ +\(GV, FC3, V1, V2, V3) :- copy_term(GV+FC3, GV+C3), call(C3, V1, V2, V3).
106
+ +\(GV, FC4, V1, V2, V3, V4) :- copy_term(GV+FC4, GV+C4), call(C4, V1, V2, V3, V4).
107
+ +\(GV, FC5, V1, V2, V3, V4, V5) :- copy_term(GV+FC5, GV+C5), call(C5, V1, V2, V3, V4, V5).
108
+ +\(GV, FC6, V1, V2, V3, V4, V5, V6) :- copy_term(GV+FC6, GV+C6), call(C6, V1, V2, V3, V4, V5, V6).
109
+ +\(GV, FC7, V1, V2, V3, V4, V5, V6, V7) :- copy_term(GV+FC7, GV+C7), call(C7, V1, V2, V3, V4, V5, V6, V7).
110
+
111
+ lambda__no_hat_call(Goal) :-
112
+ nonvar(Goal),
113
+ Goal = (_^_),
114
+ !,
115
+ throw(error(existence_error(lambda_parameter, Goal), _)).
116
+ lambda__no_hat_call(Goal) :- call(Goal).
package/src/solver.js CHANGED
@@ -17,6 +17,12 @@ import { evaluatePositiveDatalog, relationForDatalogGroup, datalogCandidateIndex
17
17
 
18
18
  let freshCounter = 0;
19
19
  const DEFAULT_INNER_TABLE_SCOPE_LIMIT = 1024;
20
+ // Conservative live-storage estimate for one generated length/2 list cell
21
+ // (cons object, argument vector, fresh variable, and its generated name).
22
+ const GENERATED_LENGTH_CELL_RESERVE_BYTES = 256;
23
+ const MAX_GENERATED_LENGTH_RESERVE_STEPS = BigInt(
24
+ Math.floor(Number.MAX_SAFE_INTEGER / GENERATED_LENGTH_CELL_RESERVE_BYTES),
25
+ );
20
26
 
21
27
  function qualifyTerm(term, module) {
22
28
  if (!term || (term.type !== COMPOUND && term.type !== 'atom')) return term;
@@ -1239,13 +1245,17 @@ function* generatedLengthSolutions(solver, list, length, env) {
1239
1245
  let suffix = emptyList();
1240
1246
  for (let extra = 0n; ; extra++) {
1241
1247
  const next = env.clone();
1242
- solver.stats.unify_calls++;
1243
- if (unify(cursor, suffix, next)) {
1244
- const answer = bindGeneratedLength(solver, length, count + extra, next);
1245
- if (answer != null) yield answer;
1246
- }
1248
+ // cursor is a dereferenced plain variable and suffix is made only from
1249
+ // freshly generated variables, so this binding cannot create a cycle.
1250
+ // Binding directly avoids an O(extra) occurs-check over the complete
1251
+ // growing suffix for every answer; without this, unbounded length/2
1252
+ // generation becomes quadratic and can spend hours before reaching the
1253
+ // normal memory resource guard (issue #49).
1254
+ next.bind(cursor.name, suffix);
1255
+ const answer = bindGeneratedLength(solver, length, count + extra, next);
1256
+ if (answer != null) yield answer;
1247
1257
  suffix = cons(variable(`__length${id}_${extra}`), suffix);
1248
- lengthAllocationCheckpoint(solver, ++steps);
1258
+ generatedLengthAllocationCheckpoint(solver, extra + 1n);
1249
1259
  }
1250
1260
  }
1251
1261
 
@@ -1263,6 +1273,19 @@ function lengthAllocationCheckpoint(solver, steps) {
1263
1273
  if ((steps & 255n) === 0n) solver.checkMemoryLimit(true);
1264
1274
  }
1265
1275
 
1276
+ function generatedLengthAllocationCheckpoint(solver, steps) {
1277
+ if ((steps & 255n) !== 0n) return;
1278
+ // The open-ended generator retains its current list spine between answers.
1279
+ // Reserve room proportional to that live spine so the protected length/2
1280
+ // call raises resource_error(memory) before its caller's outer solver hits
1281
+ // the same heap limit. This makes the error catchable by catch/3.
1282
+ const estimatedSpineBytes = steps > MAX_GENERATED_LENGTH_RESERVE_STEPS
1283
+ ? Number.MAX_SAFE_INTEGER
1284
+ : Number(steps) * GENERATED_LENGTH_CELL_RESERVE_BYTES;
1285
+ solver.checkMemoryReservation(estimatedSpineBytes);
1286
+ solver.checkMemoryLimit(true);
1287
+ }
1288
+
1266
1289
  function pushFastPiFrames(stack, goal, rest, env, depth, active) {
1267
1290
  const values = goal.args.map((arg) => deref(arg, env));
1268
1291
  if ([0, 1, 2, 4].some((index) => values[index].type !== 'number')) return false;
@@ -13,6 +13,7 @@ const moduleFiles = Object.freeze({
13
13
  comparison: 'comparison.pl',
14
14
  dates: 'dates.pl',
15
15
  iso_ext: 'iso_ext.pl',
16
+ lambda: 'lambda.pl',
16
17
  lists: 'lists.pl',
17
18
  primes: 'primes.pl',
18
19
  prologue: 'prologue.pl',
@@ -68,6 +69,9 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
68
69
  'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
69
70
  'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
70
71
  'forall/2', 'cfor/3', 'findall/4', 'variant/2', 'uuid/3',
72
+ '^/3', '^/4', '^/5', '^/6', '^/7', '^/8', '^/9', '^/10',
73
+ '\\/1', '\\/2', '\\/3', '\\/4', '\\/5', '\\/6', '\\/7', '\\/8',
74
+ '+\\/2', '+\\/3', '+\\/4', '+\\/5', '+\\/6', '+\\/7', '+\\/8', '+\\/9',
71
75
  ]);
72
76
  export const eyePrologLibraryIndicators = Object.freeze([
73
77
  ...eyePrologPortableLibraryIndicators,
@@ -127,7 +131,7 @@ export const eyePrologInteropLibraryIndicators = Object.freeze(
127
131
  // Libraries whose *name* is part of the current interop profile. A program
128
132
  // may freely use_module/1 with these common module names; predicates in those
129
133
  // modules outside eyePrologInteropLibraryIndicators are still diagnosed when used.
130
- export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext']);
134
+ export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext', 'lambda']);
131
135
 
132
136
  function* tabledNegationBuiltin({ solver, goal, env }) {
133
137
  yield* solver.solveTabledNegation(goal.args[0], env);
@@ -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: () => {
@@ -3565,6 +3627,45 @@ check(A, B, C, D, E, F) :-
3565
3627
  }
3566
3628
  },
3567
3629
  },
3630
+ {
3631
+ name: 'unbounded length/2 reaches a catchable memory resource error (issue #49)',
3632
+ run: () => {
3633
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
3634
+ const script = `
3635
+ import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
3636
+ const program = Program.parse(${JSON.stringify(':- use_module(library(lists)).\n')}, { sourceMetadata: false });
3637
+ const solver = new Solver(program, { registry: getEyePrologRegistry() });
3638
+ const goal = parseGoalText('catch(length(L,N),error(E,_),true),L=N', {
3639
+ operatorDefinitions: [...program.operators.values()],
3640
+ });
3641
+ let answer = null;
3642
+ for (const env of solver.solve([goal], new Env(), 0)) {
3643
+ answer = env;
3644
+ break;
3645
+ }
3646
+ if (answer == null) throw new Error('issue #49 query produced no caught answer');
3647
+ const formal = deref(variable('E'), answer);
3648
+ if (formal?.name !== 'resource_error' || deref(formal.args?.[0], answer)?.name !== 'memory') {
3649
+ throw new Error('unexpected caught error: ' + JSON.stringify(formal));
3650
+ }
3651
+ const left = deref(variable('L'), answer);
3652
+ const right = deref(variable('N'), answer);
3653
+ if (left.type !== 'var' || right.type !== 'var' || left.name !== right.name) {
3654
+ throw new Error('recovery did not leave L=N');
3655
+ }
3656
+ process.stdout.write('caught');
3657
+ `;
3658
+ const result = spawnSync(process.execPath, [
3659
+ '--max-old-space-size=64',
3660
+ '--input-type=module',
3661
+ '--eval',
3662
+ script,
3663
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 5000 });
3664
+ if (result.error) throw result.error;
3665
+ assertEqual(result.status, 0, `issue #49 bounded-heap child status; stderr=${result.stderr}`);
3666
+ assertEqual(result.stdout, 'caught', 'issue #49 resource error is caught by catch/3');
3667
+ },
3668
+ },
3568
3669
  {
3569
3670
  name: 'discarded fixed-length lists stay compact under a bounded heap',
3570
3671
  run: () => {
@@ -3733,16 +3834,16 @@ check(A, B, C, D, E, F) :-
3733
3834
  assertEqual(registry.get('tnot', 1), null, 'tnot/1 is absent from the ISO registry');
3734
3835
  assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
3735
3836
  assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
3736
- assertEqual(eyePrologPortableLibraryIndicators.length, 62, 'portable Prolog library count');
3837
+ assertEqual(eyePrologPortableLibraryIndicators.length, 86, 'portable Prolog library count');
3737
3838
  assertEqual(eyePrologInteropLibraryIndicators.length, 27, 'cross-implementation interop profile count');
3738
- assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext', 'common explicit library module profile');
3839
+ assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
3739
3840
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
3740
3841
  assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
3741
3842
  assertEqual(eyePrologInteropAutoload['call_nth/2'], 'iso_ext', 'call_nth/2 canonical interop autoload');
3742
3843
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
3743
3844
  assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
3744
3845
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
3745
- assertEqual(eyePrologLibraryIndicators.length, 102, 'complete EyeProlog library surface');
3846
+ assertEqual(eyePrologLibraryIndicators.length, 126, 'complete EyeProlog library surface');
3746
3847
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
3747
3848
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
3748
3849
  assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
@@ -1900,9 +1900,15 @@ unification, another list predicate, or answer readback inspects it. This is a
1900
1900
  storage optimization, not a distinct Prolog term or list semantics. Embedders
1901
1901
  that inspect the JavaScript term model can recognize this representation with
1902
1902
  `CompactListTerm`, `isCompactList`, and `compactListLength`, or construct one
1903
- with `compactVariableList`. The ordinary clauses remain the authoritative module
1904
- definition and are used unchanged by the ISO-only registry and whenever delays
1905
- or finite-domain constraints require their normal wake-up points.
1903
+ with `compactVariableList`. For open-ended `length(List, N)` generation, the
1904
+ bundled path binds each fresh generated spine directly instead of re-running an
1905
+ occurs-check over the whole growing list. It also reserves recovery headroom
1906
+ proportional to the retained spine, so a finite heap limit is raised inside the
1907
+ `length/2` search as a catchable `resource_error(memory)` rather than allowing
1908
+ an outer solver frame to encounter the limit first. The ordinary clauses remain
1909
+ the authoritative module definition and are used unchanged by the ISO-only
1910
+ registry and whenever delays or finite-domain constraints require their normal
1911
+ wake-up points.
1906
1912
 
1907
1913
  ### Implementation boundary
1908
1914
 
@@ -6039,19 +6045,19 @@ so side effects occur in Prolog execution order.
6039
6045
 
6040
6046
  ### The EyeProlog library
6041
6047
 
6042
- EyeProlog exposes **102 library predicate indicators** in addition to the 129
6043
- indicators in its isolated ISO profile. **63 are defined as ordinary Prolog
6048
+ EyeProlog exposes **126 library predicate indicators** in addition to the 129
6049
+ indicators in its isolated ISO profile. **87 are defined as ordinary Prolog
6044
6050
  clauses** in focused modules under `src/lib/`. The remaining 39
6045
6051
  are public wrappers around backtrackable host support: Prologue `call_nth/2` and
6046
6052
  `freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
6047
- resulting normal EyeProlog language surface is therefore **228 public predicate
6053
+ resulting normal EyeProlog language surface is therefore **255 public predicate
6048
6054
  indicators**. Internally, the runtime registry contains the 129 ISO definitions
6049
6055
  plus 23 private library adapters; public relations remain module source clauses.
6050
6056
 
6051
6057
  The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
6052
- `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lists.pl`,
6053
- `src/lib/primes.pl`, `src/lib/prologue.pl`, `src/lib/random.pl`,
6054
- `src/lib/strings.pl`, and `src/lib/uuid.pl`. Each declares a same-named module
6058
+ `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lambda.pl`,
6059
+ `src/lib/lists.pl`, `src/lib/primes.pl`, `src/lib/prologue.pl`,
6060
+ `src/lib/random.pl`, `src/lib/strings.pl`, and `src/lib/uuid.pl`. Each declares a same-named module
6055
6061
  with `module/2`; there is no catch-all `library(eyeprolog)`. A program imports
6056
6062
  only the modules it needs, and
6057
6063
  `use_module/2` can select an even smaller indicator list. The Prologue
@@ -6083,6 +6089,7 @@ between solution branches.
6083
6089
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
6084
6090
  | `library(dates)` | `difference/3` |
6085
6091
  | `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
6092
+ | `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` |
6086
6093
  | `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` |
6087
6094
  | `library(primes)` | `smallest_divisor_from/3` |
6088
6095
  | `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` |
@@ -6118,6 +6125,39 @@ so a source file may import both without an accidental predicate collision.
6118
6125
  overlap these modules; use selective imports when legacy code combines it with
6119
6126
  the aligned libraries.
6120
6127
 
6128
+ `library(lambda)` is an explicitly imported Scryer-aligned module for
6129
+ higher-order programming. Its implementation is adapted from Ulrich Neumerkel's
6130
+ `library(lambda)` as distributed by Scryer Prolog, retaining the upstream
6131
+ copyright and redistribution notice. The public syntax is:
6132
+
6133
+ ```text
6134
+ \X1^X2^...^XN^Goal
6135
+ Free+\X1^X2^...^XN^Goal
6136
+ ```
6137
+
6138
+ The first form has no explicitly shared free variables. Before each invocation
6139
+ EyeProlog copies the closure term, so local variables are fresh on successive
6140
+ `maplist/2-8`, `foldl/4-6`, or direct `call/N` uses. In the second form, the
6141
+ variables contained in `Free` remain shared with the surrounding goal. Importing
6142
+ the library installs `+\` as a priority-201 `xfx` operator; `\` and `^` use
6143
+ their existing ISO operator definitions. Parenthesize lower-priority goal
6144
+ operators after `^`, for example `\X^(X > 3)`.
6145
+
6146
+ A continuation lambda may leave arguments for a later call:
6147
+
6148
+ ```text
6149
+ f(x, y).
6150
+
6151
+ answer(A, B) :- call(\X^f(X), A, B).
6152
+ ```
6153
+
6154
+ This is equivalent to supplying both arguments directly. A lambda that is
6155
+ called with too few parameters raises `existence_error(lambda_parameter, ...)`,
6156
+ matching the diagnostic intent of the Scryer library. EyeProlog uses its ISO
6157
+ `copy_term/2` implementation for the fresh-copy step; in EyeProlog this gives
6158
+ the natural-copy behavior needed by the library without requiring a separate
6159
+ `copy_term_nat/2` predicate.
6160
+
6121
6161
  Normal EyeProlog execution can autoload an otherwise undefined unqualified call
6122
6162
  only when the interop table assigns it one canonical provider. Thus `member/2`
6123
6163
  autoloads from `library(lists)`, `call_nth/2` from `library(iso_ext)`, and