eyeprolog 1.2.23 → 1.2.24

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.23",
6
+ "version": "1.2.24",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/program.js CHANGED
@@ -159,6 +159,9 @@ export class Program {
159
159
  scalarFactsOnly: true,
160
160
  dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
161
161
  negationStratum: null,
162
+ hasCut: false,
163
+ cutReachable: null,
164
+ bundledLibrary: false,
162
165
  };
163
166
  return group;
164
167
  }
@@ -186,6 +189,7 @@ export class Program {
186
189
  group.rejectedDemandIndexes.clear();
187
190
  }
188
191
  group.clauses.push(clause);
192
+ if (clauseHasCut(clause)) group.hasCut = true;
189
193
  const clausePosition = group.clauses.length - 1;
190
194
  for (let i = 0; i < head.arity; i++) indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
191
195
  }
@@ -292,6 +296,7 @@ export class Program {
292
296
  const groups = [...this.groups.values()];
293
297
  const indexByGroup = new Map(groups.map((group, i) => [group, i]));
294
298
  const deps = groups.map(() => new Set());
299
+ const cutDeps = groups.map(() => new Set());
295
300
  const negativeEdges = [];
296
301
  for (const group of groups) {
297
302
  const groupIndex = indexByGroup.get(group);
@@ -299,11 +304,18 @@ export class Program {
299
304
  if (isCompactBinaryClause(clause)) {
300
305
  if (clause.bodyName != null) {
301
306
  const dep = this.findGroup(clause.bodyName, 2, group.module);
302
- if (dep) deps[groupIndex].add(indexByGroup.get(dep));
307
+ if (dep) {
308
+ deps[groupIndex].add(indexByGroup.get(dep));
309
+ cutDeps[groupIndex].add(indexByGroup.get(dep));
310
+ }
303
311
  }
304
312
  continue;
305
313
  }
306
314
  for (const goal of clause.body) {
315
+ for (const dependency of collectGoalDependencies(goal, false, true)) {
316
+ const dep = this.findGroup(dependency.name, dependency.arity, dependency.module ?? group.module);
317
+ if (dep) cutDeps[groupIndex].add(indexByGroup.get(dep));
318
+ }
307
319
  const directKey = directGoalDependencyKey(goal);
308
320
  if (directKey) {
309
321
  const dep = this.findGroup(goal.name, goal.arity, goal.module ?? group.module);
@@ -325,6 +337,8 @@ export class Program {
325
337
  const start = indexByGroup.get(group);
326
338
  const standardLibraryModule = group.module !== 'user' &&
327
339
  this.modules.get(group.module)?.filename?.startsWith('src/lib/');
340
+ group.bundledLibrary = standardLibraryModule === true;
341
+ group.cutReachable = [...reachableIndexes(start, cutDeps)].some((index) => groups[index].hasCut);
328
342
  const seen = new Set();
329
343
  const stack = [start];
330
344
  let recursive = false;
@@ -533,6 +547,7 @@ class ProgramBuilder {
533
547
  group.clauses.push(clause);
534
548
  clause.groundHead = termHasNoVariables(head);
535
549
  clause.scalarHead = head.type === COMPOUND && head.args.every(isScalar);
550
+ if (clauseHasCut(clause)) group.hasCut = true;
536
551
  if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
537
552
  for (let i = 0; i < head.arity; i++) {
538
553
  indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
@@ -1114,33 +1129,41 @@ function directGoalDependencyKey(goal) {
1114
1129
  return `${goal.name}/${goal.arity}`;
1115
1130
  }
1116
1131
 
1117
- function collectGoalDependencies(goal, negated) {
1132
+ function collectGoalDependencies(goal, negated, traverseConditionals = false) {
1118
1133
  if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated }];
1119
1134
  if (goal.type !== COMPOUND) return [];
1120
1135
  if (goal.name === ',' && goal.arity === 2) {
1121
1136
  return [
1122
- ...collectGoalDependencies(goal.args[0], negated),
1123
- ...collectGoalDependencies(goal.args[1], negated),
1137
+ ...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
1138
+ ...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
1139
+ ];
1140
+ }
1141
+ if (traverseConditionals && (goal.name === ';' || goal.name === '->') && goal.arity === 2) {
1142
+ return [
1143
+ ...collectGoalDependencies(goal.args[0], negated, true),
1144
+ ...collectGoalDependencies(goal.args[1], negated, true),
1124
1145
  ];
1125
1146
  }
1126
1147
  if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) {
1127
- return collectGoalDependencies(goal.args[0], !negated);
1148
+ return collectGoalDependencies(goal.args[0], !negated, traverseConditionals);
1128
1149
  }
1129
1150
  if (goal.name === 'once' && goal.arity === 1) {
1130
- return collectGoalDependencies(goal.args[0], negated);
1151
+ return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
1131
1152
  }
1132
1153
  if (goal.name === 'forall' && goal.arity === 2) {
1133
1154
  return [
1134
- ...collectGoalDependencies(goal.args[0], negated),
1135
- ...collectGoalDependencies(goal.args[1], negated),
1155
+ ...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
1156
+ ...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
1136
1157
  ];
1137
1158
  }
1138
1159
  if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
1139
- return collectGoalDependencies(goal.args[1], negated);
1160
+ return collectGoalDependencies(goal.args[1], negated, traverseConditionals);
1161
+ }
1162
+ if (goal.name === 'countall' && goal.arity === 2) {
1163
+ return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
1140
1164
  }
1141
- if (goal.name === 'countall' && goal.arity === 2) return collectGoalDependencies(goal.args[0], negated);
1142
1165
  if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
1143
- return collectGoalDependencies(goal.args[2], negated);
1166
+ return collectGoalDependencies(goal.args[2], negated, traverseConditionals);
1144
1167
  }
1145
1168
  return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated }];
1146
1169
  }
@@ -1308,6 +1331,7 @@ function rebuildGroupIndexes(group) {
1308
1331
  group.demandIndexes.clear();
1309
1332
  group.rejectedDemandIndexes.clear();
1310
1333
  group.scalarFactsOnly = true;
1334
+ group.hasCut = false;
1311
1335
  for (let clausePosition = 0; clausePosition < group.clauses.length; clausePosition++) {
1312
1336
  const clause = group.clauses[clausePosition];
1313
1337
  if (isCompactBinaryClause(clause)) {
@@ -1320,6 +1344,7 @@ function rebuildGroupIndexes(group) {
1320
1344
  }
1321
1345
  clause.groundHead = termHasNoVariables(clause.head);
1322
1346
  clause.scalarHead = clause.head.type === COMPOUND && clause.head.args.every(isScalar);
1347
+ if (clauseHasCut(clause)) group.hasCut = true;
1323
1348
  if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
1324
1349
  for (let i = 0; i < group.arity; i++) indexOne(group.argIndexes[i], clause.head.args[i], clause, group.clauses, clausePosition);
1325
1350
  }
package/src/solver.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
2
2
  // Most semantic decisions still flow through unification; optimizations only select candidates earlier.
3
3
  import {
4
- COMPOUND, Env, compound, copyResolved, deref, emptyList, flattenConjunction, freshTerm,
5
- numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variantTerms,
4
+ COMPOUND, NUMBER, VAR, Env, compound, cons, copyResolved, deref, emptyList,
5
+ flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
6
+ numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
6
7
  } from './term.js';
7
8
  import { sameNumberValue } from './number-value.js';
8
9
  import { PrologError, getStrictIsoRegistry } from './iso.js';
@@ -367,6 +368,23 @@ export class Solver {
367
368
  }
368
369
  qualifyMetaArguments(goal, group);
369
370
 
371
+ const lengthIterator = prologueLengthIterator(this, group, goal, env);
372
+ if (lengthIterator != null) {
373
+ const firstResult = lengthIterator.next();
374
+ if (firstResult.done) break;
375
+ stack.push({
376
+ kind: 'resumeBuiltin',
377
+ iterator: lengthIterator,
378
+ goals: rest,
379
+ depth: depth + 1,
380
+ active,
381
+ });
382
+ goals = rest;
383
+ env = firstResult.value;
384
+ depth++;
385
+ continue;
386
+ }
387
+
370
388
  if (group.tabled) {
371
389
  const key = memoKey(goal, env, group);
372
390
  if (key.hasBound) {
@@ -418,8 +436,8 @@ export class Solver {
418
436
  return activeVariantIn(goal, env, this.active);
419
437
  }
420
438
 
421
- checkMemoryLimit() {
422
- if (this.inferences < this.nextMemoryCheck) return;
439
+ checkMemoryLimit(force = false) {
440
+ if (!force && this.inferences < this.nextMemoryCheck) return;
423
441
  this.nextMemoryCheck = this.inferences + 256;
424
442
  if (!Number.isFinite(this.maxMemoryBytes)) return;
425
443
  const used = usedHeapSize();
@@ -477,6 +495,12 @@ export class Solver {
477
495
  if (!unify(goal, freshHead, next)) continue;
478
496
  if (freshBody.length === 0) {
479
497
  yield* this.solve(rest, next, depth + 1);
498
+ } else if (!groupNeedsActiveFrame(group)) {
499
+ for (const bodyEnv of this.solve(freshBody, next, depth + 1)) {
500
+ if (this.solutionsSeen > 0) this.solutionsSeen--;
501
+ yield* this.solve(rest, bodyEnv, depth + 1);
502
+ if (this.solutionsSeen >= this.solutionLimit) break;
503
+ }
480
504
  } else {
481
505
  yield* this.solveRuleBodyThenRest(goal, env, freshBody, rest, next, depth);
482
506
  }
@@ -613,7 +637,11 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
613
637
  const candidates = selectClauseCandidates(group, goal, env);
614
638
  const frames = [];
615
639
  const invocation = { goal, env };
616
- const guarded = !group.linearNumeric;
640
+ // Active frames serve two purposes: they delimit cut and detect variants in
641
+ // recursive user predicates. Cut-free, non-recursive library helpers need
642
+ // neither. Copying their full active stack at every recursive step made
643
+ // otherwise linear relations such as length/2 retain O(depth^2) references.
644
+ const guarded = groupNeedsActiveFrame(group);
617
645
  const release = guarded ? [{ kind: 'releaseActive' }] : [];
618
646
  const nextActive = guarded ? [...active, invocation] : active;
619
647
  for (const pass of [candidates.primary, candidates.fallback]) {
@@ -663,6 +691,141 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
663
691
  for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
664
692
  }
665
693
 
694
+ function groupNeedsActiveFrame(group) {
695
+ // User code may observe the surrounding control context through later cuts,
696
+ // so only apply this planning shortcut to the fixed bundled-library graph.
697
+ if (group.bundledLibrary !== true) return true;
698
+ // A frame is also required above a cut-bearing callee. The disjunction
699
+ // builtin uses the caller marker to distinguish a callee-local cut from a
700
+ // cut in its own branch. null means dependency analysis was intentionally
701
+ // disabled (strict mode or a newly mutated group), so remain conservative.
702
+ return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
703
+ }
704
+
705
+ function prologueLengthIterator(solver, group, goal, env) {
706
+ if (solver.registry.eyePrologLibrary !== true ||
707
+ group.module !== 'prologue' || group.name !== 'length' || group.arity !== 2 ||
708
+ group.bundledLibrary !== true || group.clauses.length !== 2) {
709
+ return null;
710
+ }
711
+
712
+ // Delayed and constrained variables need the ordinary solver's wake-up
713
+ // points. The fast path is deliberately limited to plain finite-tree terms.
714
+ if (env._clpz != null) return null;
715
+ const length = deref(goal.args[1], env);
716
+ if (length.type === VAR && env._delays?.has(length.name)) return null;
717
+
718
+ let cursor = deref(goal.args[0], env);
719
+ while (isCons(cursor)) {
720
+ cursor = deref(cursor.args[1], env);
721
+ }
722
+ if (cursor.type === VAR) {
723
+ if (env._delays?.has(cursor.name)) return null;
724
+ if (length.type === VAR && cursor.name === length.name) return null;
725
+ }
726
+ return prologueLengthSolutions(solver, goal, env);
727
+ }
728
+
729
+ function* prologueLengthSolutions(solver, goal, env) {
730
+ const requestedLength = deref(goal.args[1], env);
731
+ if (requestedLength.type !== VAR) {
732
+ if (requestedLength.type !== NUMBER || !isDecimalInteger(requestedLength.name)) {
733
+ throw new PrologError('type_error(integer)', requestedLength);
734
+ }
735
+ const length = BigInt(requestedLength.name);
736
+ if (length < 0n) throw new PrologError('domain_error(not_less_than_zero)', requestedLength);
737
+ yield* fixedLengthSolutions(solver, goal.args[0], length, env);
738
+ return;
739
+ }
740
+
741
+ yield* generatedLengthSolutions(solver, goal.args[0], goal.args[1], env);
742
+ }
743
+
744
+ function* fixedLengthSolutions(solver, list, length, env) {
745
+ let cursor = deref(list, env);
746
+ let remaining = length;
747
+ let steps = 0n;
748
+ while (isCons(cursor)) {
749
+ if (remaining === 0n) return;
750
+ remaining--;
751
+ cursor = deref(cursor.args[1], env);
752
+ lengthAllocationCheckpoint(solver, ++steps);
753
+ }
754
+ if (isEmptyList(cursor)) {
755
+ if (remaining === 0n) yield env;
756
+ return;
757
+ }
758
+ if (cursor.type !== VAR) return;
759
+
760
+ // A source-level anonymous variable occurs nowhere else, so materializing
761
+ // its list cannot affect any subsequent goal or answer substitution.
762
+ if (isAnonymousVariable(cursor)) {
763
+ yield env;
764
+ return;
765
+ }
766
+
767
+ const id = nextFreshId();
768
+ let suffix = emptyList();
769
+ for (let index = 0n; index < remaining; index++) {
770
+ suffix = cons(variable(`__length${id}_${index}`), suffix);
771
+ lengthAllocationCheckpoint(solver, ++steps);
772
+ }
773
+ const next = env.clone();
774
+ solver.stats.unify_calls++;
775
+ if (unify(cursor, suffix, next)) yield next;
776
+ }
777
+
778
+ function* generatedLengthSolutions(solver, list, length, env) {
779
+ let cursor = deref(list, env);
780
+ let count = 0n;
781
+ let steps = 0n;
782
+ while (isCons(cursor)) {
783
+ count++;
784
+ cursor = deref(cursor.args[1], env);
785
+ lengthAllocationCheckpoint(solver, ++steps);
786
+ }
787
+ if (isEmptyList(cursor)) {
788
+ const next = bindGeneratedLength(solver, length, count, env);
789
+ if (next != null) yield next;
790
+ return;
791
+ }
792
+ if (cursor.type !== VAR) return;
793
+
794
+ if (isAnonymousVariable(cursor)) {
795
+ for (let value = count; ; value++) {
796
+ const next = bindGeneratedLength(solver, length, value, env);
797
+ if (next != null) yield next;
798
+ }
799
+ }
800
+
801
+ const id = nextFreshId();
802
+ let suffix = emptyList();
803
+ for (let extra = 0n; ; extra++) {
804
+ const next = env.clone();
805
+ solver.stats.unify_calls++;
806
+ if (unify(cursor, suffix, next)) {
807
+ const answer = bindGeneratedLength(solver, length, count + extra, next);
808
+ if (answer != null) yield answer;
809
+ }
810
+ suffix = cons(variable(`__length${id}_${extra}`), suffix);
811
+ lengthAllocationCheckpoint(solver, ++steps);
812
+ }
813
+ }
814
+
815
+ function bindGeneratedLength(solver, length, value, env) {
816
+ const next = env.clone();
817
+ solver.stats.unify_calls++;
818
+ return unify(length, numberTerm(value), next) ? next : null;
819
+ }
820
+
821
+ function isAnonymousVariable(term) {
822
+ return term.type === VAR && term.name.startsWith('__anon');
823
+ }
824
+
825
+ function lengthAllocationCheckpoint(solver, steps) {
826
+ if ((steps & 255n) === 0n) solver.checkMemoryLimit(true);
827
+ }
828
+
666
829
  function pushFastPiFrames(stack, goal, rest, env, depth, active) {
667
830
  const values = goal.args.map((arg) => deref(arg, env));
668
831
  if ([0, 1, 2, 4].some((index) => values[index].type !== 'number')) return false;
@@ -681,6 +681,21 @@ c4 ?- call((!;1)).
681
681
  assertEqual(result.stderr, '', 'quad stderr');
682
682
  },
683
683
  },
684
+ {
685
+ name: 'REPL advances anonymous Prologue length checks through I = 28',
686
+ run: () => {
687
+ const result = runCli([], {
688
+ input:
689
+ 'use_module(library(prologue)).\n' +
690
+ 'length(_,I),I>9,N is 2^I,\\+ \\+ length(_,N).\n' +
691
+ 'f\nf\nf\n;\n;\n;\n;\n\nhalt.\n',
692
+ });
693
+ assertEqual(result.status, 0, 'exit status');
694
+ assertIncludes(result.stdout, '; I = 28, N = 268435456\n; ... .\n?- ', 'large anonymous length answer');
695
+ assertNotIncludes(result.stdout, 'resource_error(memory)', 'stdout');
696
+ assertEqual(result.stderr, '', 'stderr');
697
+ },
698
+ },
684
699
  {
685
700
  name: 'Prologue freeze wakes delayed goals with their bindings',
686
701
  run: () => {
@@ -2472,14 +2487,14 @@ open(X) :- candidate(X), \\+ closed(X).
2472
2487
  },
2473
2488
  },
2474
2489
  {
2475
- name: 'list allocation heap pressure becomes resource_error(memory)',
2490
+ name: 'named list allocation heap pressure becomes resource_error(memory)',
2476
2491
  run: () => {
2477
2492
  const engineUrl = new URL('../src/index.js', import.meta.url).href;
2478
2493
  const programText = ':- use_module(library(prologue)).\n';
2479
- const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
2494
+ const goalText = '\\+ \\+ length(List, 1000000)';
2480
2495
  const script = `
2481
2496
  import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
2482
- const program = Program.parse(${JSON.stringify(programText)});
2497
+ const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
2483
2498
  const solver = new Solver(program, { registry: getEyePrologRegistry() });
2484
2499
  const goal = parseGoalText(${JSON.stringify(goalText)}, {
2485
2500
  operatorDefinitions: [...program.operators.values()],
@@ -2500,6 +2515,37 @@ open(X) :- candidate(X), \\+ closed(X).
2500
2515
  assertEqual(result.stdout, 'resource_error(memory)', 'heap pressure resource error');
2501
2516
  },
2502
2517
  },
2518
+ {
2519
+ name: 'anonymous Prologue length checks avoid materializing discarded lists',
2520
+ run: () => {
2521
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
2522
+ const programText = ':- use_module(library(prologue)).\n';
2523
+ const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
2524
+ const script = `
2525
+ import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
2526
+ const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
2527
+ const solver = new Solver(program, {
2528
+ registry: getEyePrologRegistry(),
2529
+ solutionLimit: 19,
2530
+ });
2531
+ const goal = parseGoalText(${JSON.stringify(goalText)}, {
2532
+ operatorDefinitions: [...program.operators.values()],
2533
+ });
2534
+ let answers = 0;
2535
+ for (const _ of solver.solve([goal], new Env(), 0)) answers++;
2536
+ process.stdout.write(String(answers));
2537
+ `;
2538
+ const result = spawnSync(process.execPath, [
2539
+ '--max-old-space-size=64',
2540
+ '--input-type=module',
2541
+ '--eval',
2542
+ script,
2543
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
2544
+ if (result.error) throw result.error;
2545
+ assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
2546
+ assertEqual(result.stdout, '19', 'answers through I = 28');
2547
+ },
2548
+ },
2503
2549
  {
2504
2550
  name: 'solver honors solution limits',
2505
2551
  run: () => {
@@ -1843,6 +1843,17 @@ ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
1843
1843
  `finite_memory` spelling for the distinct convention where no finite amount of
1844
1844
  memory could complete the computation.
1845
1845
 
1846
+ The iterative solver keeps active-call frames only where they are semantically
1847
+ needed for cut scope or recursive variant guards. Bundled-library helpers whose
1848
+ callable dependency region is cut-free and which need no recursive variant
1849
+ guard therefore do not copy a growing active-call sequence at every step.
1850
+ Under the normal EyeProlog registry, the bundled Prologue `length/2` also has a
1851
+ scoped iterative execution path: named lists are counted or constructed without
1852
+ recursive interpreter frames, and an anonymous list is not materialized because
1853
+ its binding cannot be observed. The ordinary clauses remain the authoritative
1854
+ module definition and are used unchanged by the ISO-only registry and whenever
1855
+ delays or finite-domain constraints require their normal wake-up points.
1856
+
1846
1857
  ### Implementation boundary
1847
1858
 
1848
1859
  The source layout mirrors the language boundary. `src/iso.js` contains the
@@ -5918,8 +5929,8 @@ so side effects occur in Prolog execution order.
5918
5929
  ### The EyeProlog library
5919
5930
 
5920
5931
  EyeProlog exposes **99 library predicate indicators** in addition to the 129
5921
- indicators in its isolated ISO profile. **60 are implemented entirely as
5922
- ordinary Prolog clauses** in focused modules under `src/lib/`. The remaining 39
5932
+ indicators in its isolated ISO profile. **60 are defined as ordinary Prolog
5933
+ clauses** in focused modules under `src/lib/`. The remaining 39
5923
5934
  are public wrappers around backtrackable host support: Prologue `call_nth/2` and
5924
5935
  `freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
5925
5936
  resulting normal EyeProlog language surface is therefore **228 public predicate