eyeprolog 1.5.6 → 1.5.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 CHANGED
@@ -57,7 +57,7 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
57
57
  ```
58
58
 
59
59
  ## Eyelet forward rules (`:+`)
60
- Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled Prolog `library(eyelet)` driver when no explicit `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a `fuse/1`; JavaScript `run()` follows the same rule. The module exports `:+`, `stable/1`, and `becomes/2`, and implements rule selection, fixed-point rounds, skolemization, state replacement, and duplicate suppression in Prolog. JavaScript only loads the driver and provides private mutability/output adapters. Strict ISO mode disables this extension.
60
+ Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled Prolog `library(eyelet)` driver when no explicit `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a `fuse/1`; JavaScript `run()` follows the same rule. The module exports `:+`, `stable/1`, and `becomes/2`, and implements rule selection, fixed-point rounds, skolemization, state replacement, and duplicate suppression in Prolog. JavaScript retains only syntax recognition, static dependency/autoload planning, driver bootstrap, and private mutability/output adapters; it does not implement the fixed-point semantics. Strict ISO mode disables this extension.
61
61
 
62
62
  Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL; use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
63
63
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.6",
6
+ "version": "1.5.7",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -92,10 +92,7 @@ const graphicAtomChars = '#$&*+-./<=>?@^~\\:';
92
92
  // canonical notation. Commas remain separators except inside parentheses.
93
93
  const INFIX_OPERATORS = new Map([
94
94
  [':-', { precedence: 1, associativity: 'none' }],
95
- [':+', { precedence: 1, associativity: 'none' }], // EyeProlog forward-rule extension
96
- ['?-', { precedence: 1, associativity: 'none' }], // quad label extension
97
95
  ['-->', { precedence: 1, associativity: 'none' }],
98
- ['|', { precedence: 96, associativity: 'right' }],
99
96
  [';', { precedence: 101, associativity: 'right' }],
100
97
  ['->', { precedence: 151, associativity: 'right' }],
101
98
  [',', { precedence: 201, associativity: 'right' }],
@@ -133,7 +130,6 @@ const INFIX_OPERATORS = new Map([
133
130
  ]);
134
131
  const PREFIX_OPERATORS = new Map([
135
132
  ['?-', { precedence: 1, strict: true }],
136
- ['table', { precedence: 51, strict: true }],
137
133
  ['\\+', { precedence: 301, strict: false }],
138
134
  ['+', { precedence: 1001, strict: false }],
139
135
  ['-', { precedence: 1001, strict: false }],
@@ -229,14 +225,10 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
229
225
  prefixOperators: includeDefaults ? new Map(PREFIX_OPERATORS) : new Map(),
230
226
  postfixOperators: new Map(),
231
227
  };
232
- // The infix ?-/2 form is an EyeProlog quad extension. ISO 13211-1
233
- // predefines only the 1200 fx ?- operator; strict core mode starts from that
234
- // table and still permits an explicit op/3 directive to add an infix form.
235
- if (options.isoStrict === true) {
236
- state.infixOperators.delete('?-');
237
- state.infixOperators.delete('|');
238
- state.infixOperators.delete(':+');
239
- state.prefixOperators.delete('table');
228
+ if (includeDefaults && options.isoStrict !== true) {
229
+ for (const [priority, specifier, name] of [...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
230
+ defineParserOperator(state, priority, specifier, name);
231
+ }
240
232
  }
241
233
  for (const definition of definitions) {
242
234
  const [priority, specifier, name] = Array.isArray(definition)
package/src/program.js CHANGED
@@ -65,22 +65,12 @@ function collectMultiplicitySensitiveDependencies(goal, out = []) {
65
65
  }
66
66
 
67
67
 
68
- function scalarNodeKey(term) {
69
- return isScalar(term) ? `${term.type}:${term.name}` : null;
70
- }
71
-
72
- // Some source-level self recursion is structurally finite even though the
73
- // predicate dependency graph contains a self-edge. The deep-taxonomy family
74
- // is the canonical case: p(X,n10000) calls p(X,n9999), eventually reaching a
75
- // fact. When one scalar argument forms an acyclic transition graph and every
76
- // other argument is carried through unchanged, depth-first execution is both
77
- // terminating and much cheaper than constructing the complete Datalog model.
78
68
  // Cut-guided tail recursion is usually a deterministic search loop rather than
79
- // a cyclic relation. Tabling such loops can retain every intermediate numeric
69
+ // a cyclic relation. Tabling such loops can retain every intermediate numeric
80
70
  // state (for example prime scanning in Goldbach) and turn a bounded depth-first
81
- // search into an enormous table. Keep this deliberately narrow: each recursive
71
+ // search into an enormous table. Keep this deliberately narrow: each recursive
82
72
  // clause must contain exactly one direct self-call and that call must be the last
83
- // body goal. Branching recursion such as Takeuchi therefore remains eligible
73
+ // body goal. Branching recursion such as Takeuchi therefore remains eligible
84
74
  // for recursion planning and explicit tabling.
85
75
  function hasDirectCutTailRecursion(group) {
86
76
  if (group.hasCut !== true || group.clauses.length === 0) return false;
@@ -104,96 +94,6 @@ function hasDirectCutTailRecursion(group) {
104
94
  return sawRecursive;
105
95
  }
106
96
 
107
- function hasAcyclicScalarSelfRecursion(group) {
108
- if (group.arity === 0 || group.clauses.length === 0) return false;
109
- for (let position = 0; position < group.arity; position++) {
110
- const edges = new Map();
111
- let sawRecursive = false;
112
- let valid = true;
113
- for (const clause of group.clauses) {
114
- if (isCompactBinaryClause(clause)) {
115
- if (group.arity !== 2) { valid = false; break; }
116
- if (clause.bodyName == null) continue;
117
- if (clause.bodyName !== group.name) { valid = false; break; }
118
- const headType = position === 0 ? clause.head0Type : clause.head1Type;
119
- const headName = position === 0 ? clause.head0Name : clause.head1Name;
120
- const bodyType = position === 0 ? clause.body0Type : clause.body1Type;
121
- const bodyName = position === 0 ? clause.body0Name : clause.body1Name;
122
- if (!['atom', 'string', 'number'].includes(headType) ||
123
- !['atom', 'string', 'number'].includes(bodyType)) { valid = false; break; }
124
- const other = position === 0 ? 1 : 0;
125
- const ohType = other === 0 ? clause.head0Type : clause.head1Type;
126
- const ohName = other === 0 ? clause.head0Name : clause.head1Name;
127
- const obType = other === 0 ? clause.body0Type : clause.body1Type;
128
- const obName = other === 0 ? clause.body0Name : clause.body1Name;
129
- const carried = (ohType === 'var' && obType === 'var' && ohName === obName) ||
130
- (['atom', 'string', 'number'].includes(ohType) && ohType === obType && ohName === obName);
131
- if (!carried) { valid = false; break; }
132
- const from = `${headType}:${headName}`;
133
- const to = `${bodyType}:${bodyName}`;
134
- sawRecursive = true;
135
- let targets = edges.get(from);
136
- if (targets == null) edges.set(from, targets = new Set());
137
- targets.add(to);
138
- continue;
139
- }
140
-
141
- if (clause.head?.type !== COMPOUND || clause.head.name !== group.name || clause.head.arity !== group.arity) {
142
- valid = false; break;
143
- }
144
- if ((clause.body?.length ?? 0) === 0) continue;
145
- if (clause.body.length !== 1) { valid = false; break; }
146
- const body = clause.body[0];
147
- if (body?.type !== COMPOUND || body.name !== group.name || body.arity !== group.arity ||
148
- (body.module ?? group.module) !== group.module) { valid = false; break; }
149
- const from = scalarNodeKey(clause.head.args[position]);
150
- const to = scalarNodeKey(body.args[position]);
151
- if (from == null || to == null) { valid = false; break; }
152
- for (let i = 0; i < group.arity; i++) {
153
- if (i === position) continue;
154
- const h = clause.head.args[i];
155
- const b = body.args[i];
156
- if (h.type === VAR && b.type === VAR && h.name === b.name) continue;
157
- const hk = scalarNodeKey(h);
158
- const bk = scalarNodeKey(b);
159
- if (hk != null && hk === bk) continue;
160
- valid = false;
161
- break;
162
- }
163
- if (!valid) break;
164
- sawRecursive = true;
165
- let targets = edges.get(from);
166
- if (targets == null) edges.set(from, targets = new Set());
167
- targets.add(to);
168
- }
169
- if (!valid || !sawRecursive) continue;
170
-
171
- // Use Kahn's algorithm rather than recursive DFS here. Programs generated
172
- // from large taxonomies can contain hundreds of thousands of scalar
173
- // transitions, and recursing once per edge would overflow the JavaScript
174
- // call stack before Prolog execution even begins.
175
- const indegree = new Map();
176
- for (const [node, targets] of edges) {
177
- if (!indegree.has(node)) indegree.set(node, 0);
178
- for (const next of targets) indegree.set(next, (indegree.get(next) ?? 0) + 1);
179
- }
180
- const ready = [];
181
- for (const [node, degree] of indegree) if (degree === 0) ready.push(node);
182
- let consumed = 0;
183
- for (let cursor = 0; cursor < ready.length; cursor++) {
184
- const node = ready[cursor];
185
- consumed++;
186
- for (const next of edges.get(node) ?? []) {
187
- const degree = indegree.get(next) - 1;
188
- indegree.set(next, degree);
189
- if (degree === 0) ready.push(next);
190
- }
191
- }
192
- if (consumed === indegree.size) return true;
193
- }
194
- return false;
195
- }
196
-
197
97
  function preparedBundledLibraryCacheKey(program, options) {
198
98
  const filename = String(options.filename ?? '');
199
99
  if (filename !== standardLibrarySources.get('clpz')?.filename) return null;
@@ -562,10 +462,8 @@ export class Program {
562
462
  );
563
463
  group.listTailRecursive = directRecursiveComponent && !group.cutRecursive &&
564
464
  hasStrictListTailRecursion(group);
565
- const acyclicScalarRecursion = plannedRecursive && hasAcyclicScalarSelfRecursion(group);
566
465
  const cutTailRecursion = plannedRecursive && hasDirectCutTailRecursion(group);
567
466
  const multiplicitySensitive = [...multiplicitySensitiveRoots].some((root) => reachableIndexes(root, deps).has(start));
568
- group.acyclicScalarRecursion = acyclicScalarRecursion;
569
467
  group.cutTailRecursion = cutTailRecursion;
570
468
  group.multiplicitySensitive = multiplicitySensitive;
571
469
  group.depthFirstRecursive = plannedRecursive && !explicitlyTabled;
@@ -1168,13 +1066,15 @@ function bundledLibraryModule(program, module) {
1168
1066
  function groupDependencies(group) {
1169
1067
  const dependencies = [];
1170
1068
  for (const clause of group.clauses) {
1171
- // Eyelet forward rules store their executable premise in the :+/2 head
1172
- // rather than in the ordinary clause body. Treat that premise as a goal
1173
- // dependency so bundled predicates such as stable/1 and becomes/2 can be
1174
- // autoloaded exactly like calls in ordinary rules. Derived :+ rules may
1175
- // themselves occur in a conclusion, so recurse through that shape too.
1069
+ // Forward rules store executable premises inside their heads instead of in
1070
+ // ordinary clause bodies. Include those premises in static dependency
1071
+ // analysis so portability checks see the same calls that runtime will see.
1176
1072
  if (group.name === ':+') {
1177
- dependencies.push(...forwardRuleDependencies(clause.head));
1073
+ collectForwardRulePremiseDependencies(
1074
+ clause.head,
1075
+ (goal, out) => out.push(...collectGoalDependencies(goal, false, true)),
1076
+ dependencies,
1077
+ );
1178
1078
  }
1179
1079
  if (isCompactBinaryClause(clause)) {
1180
1080
  if (clause.bodyName != null) {
@@ -1194,16 +1094,16 @@ function groupDependencies(group) {
1194
1094
  return dependencies;
1195
1095
  }
1196
1096
 
1197
- function forwardRuleDependencies(term, out = []) {
1097
+ function collectForwardRulePremiseDependencies(term, collect, out) {
1198
1098
  if (term?.type !== COMPOUND) return out;
1199
1099
  if (term.name === ':+' && term.arity === 2) {
1200
- out.push(...collectGoalDependencies(term.args[1], false, true));
1201
- forwardRuleDependencies(term.args[0], out);
1100
+ collect(term.args[1], out);
1101
+ collectForwardRulePremiseDependencies(term.args[0], collect, out);
1202
1102
  return out;
1203
1103
  }
1204
1104
  if (term.name === ',' && term.arity === 2) {
1205
- forwardRuleDependencies(term.args[0], out);
1206
- forwardRuleDependencies(term.args[1], out);
1105
+ collectForwardRulePremiseDependencies(term.args[0], collect, out);
1106
+ collectForwardRulePremiseDependencies(term.args[1], collect, out);
1207
1107
  }
1208
1108
  return out;
1209
1109
  }
@@ -1257,24 +1157,12 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1257
1157
  return out;
1258
1158
  }
1259
1159
 
1260
- function forwardRuleAutoloadDependencies(term, out = []) {
1261
- if (term?.type !== COMPOUND) return out;
1262
- if (term.name === ':+' && term.arity === 2) {
1263
- collectAutoloadGoalDependencies(term.args[1], out);
1264
- forwardRuleAutoloadDependencies(term.args[0], out);
1265
- return out;
1266
- }
1267
- if (term.name === ',' && term.arity === 2) {
1268
- forwardRuleAutoloadDependencies(term.args[0], out);
1269
- forwardRuleAutoloadDependencies(term.args[1], out);
1270
- }
1271
- return out;
1272
- }
1273
-
1274
1160
  function groupAutoloadDependencies(group) {
1275
1161
  const dependencies = [];
1276
1162
  for (const clause of group.clauses) {
1277
- if (group.name === ':+') dependencies.push(...forwardRuleAutoloadDependencies(clause.head));
1163
+ if (group.name === ':+') {
1164
+ collectForwardRulePremiseDependencies(clause.head, collectAutoloadGoalDependencies, dependencies);
1165
+ }
1278
1166
  if (isCompactBinaryClause(clause)) {
1279
1167
  if (clause.bodyName != null) {
1280
1168
  dependencies.push({
@@ -1699,12 +1699,13 @@ EyeProlog source does not need a separate `dynamic/1` declaration merely to use
1699
1699
  that state with `becomes/2`.
1700
1700
 
1701
1701
  The `:+` reasoning algorithm is therefore Prolog code in `src/lib/eyelet.pl`, not
1702
- a second JavaScript implementation. The host boundary is intentionally small:
1703
- JavaScript bootstraps the private `eyelet:eyelet_run/3` entry point and supplies
1704
- two private library adapters—one to make an existing user procedure mutable
1705
- without rewriting its clauses, and one to forward answer/fuse terms to the CLI
1706
- or embedding callbacks. Those adapters are not part of the strict ISO registry
1707
- or the public `library(eyelet)` export surface.
1702
+ a second JavaScript implementation. The remaining JavaScript references have
1703
+ non-semantic roles: the parser declares the normal-profile operator, program
1704
+ analysis scans forward-rule premises for dependency/autoload planning, execution
1705
+ bootstraps the private `eyelet:eyelet_run/3` entry point, and two private library
1706
+ adapters bridge database mutability and answer/fuse events to the host. Those
1707
+ adapters are not part of the strict ISO registry or the public `library(eyelet)`
1708
+ export surface.
1708
1709
 
1709
1710
  The JavaScript convenience `run()` function selects this forward mode when no
1710
1711
  explicit `goal` or `goals` option is supplied. Advanced embedders can inspect a
package/why-eyeprolog.md CHANGED
@@ -147,8 +147,9 @@ and executes those rules with the Prolog fixed-point driver in the bundled
147
147
  `library(eyelet)`. That module owns the Eyelet semantics—rule selection,
148
148
  fixed-point iteration, skolemization, `stable/1`, `becomes/2`, and the
149
149
  `true :+ Goal` / `false :+ Goal` control conventions—rather than duplicating the
150
- reasoning algorithm in JavaScript. The host layer is limited to private database
151
- mutability and output-event adapters. Strict ISO mode removes the operator.
150
+ reasoning algorithm in JavaScript. The remaining JavaScript support is limited to
151
+ syntax recognition, static dependency/autoload planning, driver bootstrap, and
152
+ private database-mutability/output-event adapters. Strict ISO mode removes the operator.
152
153
  Because resource exhaustion is not logical failure, normal execution has no hidden
153
154
  depth cutoff; an explicitly requested depth limit raises `resource_error(depth_limit)`.
154
155