eyeprolog 1.3.69 → 1.3.71

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/src/program.js CHANGED
@@ -576,8 +576,16 @@ class ProgramBuilder {
576
576
  targetSet.add(key);
577
577
  declaredMap.set(`${textUnit}\u0000${key}`, { ...indicator, key, module, textUnit });
578
578
  if (kind === 'dynamic') {
579
- const existing = program.groups.get(key);
580
- if (existing) existing.dynamic = true;
579
+ // A dynamic declaration creates the procedure immediately, not only
580
+ // when ProgramBuilder.finish() runs. Compile-time source-expansion
581
+ // hooks can therefore call an explicitly declared empty dynamic
582
+ // predicate while the remainder of the same file is still loading.
583
+ let existing = program.groups.get(key);
584
+ if (!existing) {
585
+ existing = program.makeGroup(indicator.name, indicator.arity, module);
586
+ program.groups.set(key, existing);
587
+ }
588
+ existing.dynamic = true;
581
589
  }
582
590
  }
583
591
  return true;
@@ -1065,6 +1073,7 @@ function sourcePath(options) {
1065
1073
 
1066
1074
  function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules, fast, context) {
1067
1075
  const batch = [];
1076
+ const deferredGoalExpansions = [];
1068
1077
  const flush = () => {
1069
1078
  if (batch.length === 0) return;
1070
1079
  builder.addClauses(batch);
@@ -1114,10 +1123,23 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1114
1123
  clause = normalizeQualifiedClauseHead(clause, lexicalModule);
1115
1124
 
1116
1125
  if (!builder.program.strictIso && !isDirectiveClause(clause)) {
1117
- expandClauseGoals(builder.program, clause, lexicalModule, {
1118
- module: lexicalModule,
1119
- filename: options.filename ?? '<input>',
1120
- });
1126
+ try {
1127
+ expandClauseGoals(builder.program, clause, lexicalModule, {
1128
+ module: lexicalModule,
1129
+ filename: options.filename ?? '<input>',
1130
+ });
1131
+ } catch (error) {
1132
+ // Scryer-compatible goal-expansion hooks may refer to helper predicates
1133
+ // that occur later in the same source file. Keep loading so those
1134
+ // helpers become visible, then retry this clause at end-of-source.
1135
+ // A still-missing helper (or any other expansion error) is reported on
1136
+ // the final retry rather than being silently ignored.
1137
+ if (!(error instanceof PrologError) || error.formal !== 'existence_error(procedure)') throw error;
1138
+ clause.module ??= lexicalModule;
1139
+ clause.textUnit ??= context.textUnit;
1140
+ deferredGoalExpansions.push({ clause, lexicalModule });
1141
+ return;
1142
+ }
1121
1143
  }
1122
1144
 
1123
1145
  const moduleDeclaration = moduleDirective(clause);
@@ -1207,6 +1229,16 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1207
1229
  }
1208
1230
  parseClausesInto(source, options, processClause);
1209
1231
  flush();
1232
+ if (!builder.program.strictIso && deferredGoalExpansions.length > 0) {
1233
+ for (const { clause, lexicalModule } of deferredGoalExpansions) {
1234
+ expandClauseGoals(builder.program, clause, lexicalModule, {
1235
+ module: lexicalModule,
1236
+ filename: options.filename ?? '<input>',
1237
+ });
1238
+ for (const goal of clause.body) annotateGoalModule(goal, clause.lexicalModule ?? lexicalModule);
1239
+ builder.addClauses([clause]);
1240
+ }
1241
+ }
1210
1242
  return true;
1211
1243
  }
1212
1244
 
package/src/solver.js CHANGED
@@ -10,7 +10,6 @@ import { PrologError, getStrictIsoRegistry } from './iso.js';
10
10
  import { getEyePrologRegistry } from './standard-library.js';
11
11
  import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
12
12
  import { StreamManager } from './io.js';
13
- import { clpzStateConsistent } from './clpz.js';
14
13
  import { hardHeapLimit, softHeapLimit, usedHeapSize } from './platform.js';
15
14
  import { evaluateWfs, relationForGroup, truthOfGroundGoal } from './wfs.js';
16
15
  import { ISO_MAX_ARITY } from './iso-limits.js';
@@ -428,6 +427,45 @@ export class Solver {
428
427
  });
429
428
  continue;
430
429
  }
430
+ if (frame.kind === 'userClause') {
431
+ if (this.solutionsSeen >= this.solutionLimit) continue;
432
+ const { clause, goal: clauseGoal, rest: clauseRest, env: clauseEnv, depth: clauseDepth,
433
+ active: clauseActive, release } = frame;
434
+ let next;
435
+ if (clause.body.length === 0 && clause.scalarHead) {
436
+ next = matchScalarFact(clauseGoal, clause.head, clauseEnv);
437
+ if (!next) continue;
438
+ this.stats.unify_calls++;
439
+ stack.push({
440
+ kind: 'goals',
441
+ goals: [...release, ...clauseRest],
442
+ env: next,
443
+ depth: clauseDepth + 1,
444
+ active: clauseActive,
445
+ });
446
+ continue;
447
+ }
448
+ const id = nextFreshId();
449
+ const freshVariables = new Map();
450
+ const freshHead = freshTerm(clause.head, id, freshVariables);
451
+ const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
452
+ const localFreshPlan = clauseLocalFreshPlan(clause);
453
+ const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
454
+ attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
455
+ next = clauseEnv.clone();
456
+ this.stats.unify_calls++;
457
+ if (!unify(clauseGoal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
458
+ stack.push({
459
+ kind: 'goals',
460
+ goals: freshBody.length === 0
461
+ ? [...release, ...clauseRest]
462
+ : [...freshBody, ...release, ...clauseRest],
463
+ env: next,
464
+ depth: clauseDepth + 1,
465
+ active: clauseActive,
466
+ });
467
+ continue;
468
+ }
431
469
  if (frame.kind === 'completeTableFixpointRound') {
432
470
  if (frame.revision !== this.programRevision) continue;
433
471
  frame.entry.computing = false;
@@ -491,7 +529,6 @@ export class Solver {
491
529
  }
492
530
 
493
531
  if (goals.length === 0) {
494
- if (!clpzStateConsistent(env)) break;
495
532
  this.solutionsSeen++;
496
533
  this.stats.completed_goal_lists++;
497
534
  this.active = active;
@@ -1182,53 +1219,27 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
1182
1219
  for (const pass of [candidates.primary, candidates.fallback]) {
1183
1220
  for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
1184
1221
  const clause = clauseCandidateAt(pass, candidateIndex);
1185
- if (clause.body.length === 0 && clause.scalarHead) {
1186
- const next = matchScalarFact(goal, clause.head, env);
1187
- if (next) {
1188
- solver.stats.unify_calls++;
1189
- frames.push({
1190
- kind: 'goals',
1191
- goals: [...release, ...rest],
1192
- env: next,
1193
- depth: depth + 1,
1194
- active: nextActive,
1195
- });
1196
- }
1197
- continue;
1198
- }
1199
1222
  if (headCannotMatch(goal, clause.head, env)) continue;
1200
- const id = nextFreshId();
1201
- const freshVariables = new Map();
1202
- const freshHead = freshTerm(clause.head, id, freshVariables);
1203
- const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
1204
- const localFreshPlan = clauseLocalFreshPlan(clause);
1205
- const headLocalFresh = freshVariableSet(localFreshPlan.head, freshVariables);
1206
- attachBodyLocalFreshVariables(freshBody, localFreshPlan.body, freshVariables);
1207
- const next = env.clone();
1208
- solver.stats.unify_calls++;
1209
- if (!unify(goal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
1210
- if (freshBody.length === 0) {
1211
- frames.push({
1212
- kind: 'goals',
1213
- goals: [...release, ...rest],
1214
- env: next,
1215
- depth: depth + 1,
1216
- active: nextActive,
1217
- });
1218
- } else {
1219
- frames.push({
1220
- kind: 'goals',
1221
- goals: [...freshBody, ...release, ...rest],
1222
- env: next,
1223
- depth: depth + 1,
1224
- active: nextActive,
1225
- });
1226
- }
1223
+ // Do not speculatively unify alternative heads. With attributed variables,
1224
+ // head unification can run verify_attributes/3, queue wakeups, fail, or
1225
+ // throw. Those effects are observable and must occur only if Prolog search
1226
+ // actually reaches this clause (in particular after earlier cuts).
1227
+ frames.push({
1228
+ kind: 'userClause',
1229
+ clause,
1230
+ goal,
1231
+ rest,
1232
+ env,
1233
+ depth,
1234
+ active: nextActive,
1235
+ release,
1236
+ });
1227
1237
  }
1228
1238
  }
1229
1239
  for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
1230
1240
  }
1231
1241
 
1242
+
1232
1243
  function clauseLocalFreshPlan(clause) {
1233
1244
  if (clause._localFreshPlan != null) return clause._localFreshPlan;
1234
1245
  const seen = new Set();
@@ -1273,19 +1284,7 @@ function attachBodyLocalFreshVariables(freshBody, plan, freshVariables) {
1273
1284
  if (knownNonoccurringVariables == null || goal?.type !== COMPOUND) continue;
1274
1285
  if (goal.name === '=' && goal.arity === 2) {
1275
1286
  goal._knownNonoccurringVariables = knownNonoccurringVariables;
1276
- continue;
1277
- }
1278
- // Only a compiler-generated DCG state first handed as a complete argument
1279
- // to another callable receives a longer-lived local marker. Equality-created
1280
- // sequence variables keep the existing one-unification proof instead.
1281
- const localFirstUseVariables = new Set();
1282
- for (const argument of goal.args) {
1283
- if (argument?.type === VAR && argument.name.startsWith('\u0000dcg') &&
1284
- knownNonoccurringVariables.has(argument.name)) {
1285
- localFirstUseVariables.add(argument.name);
1286
- }
1287
1287
  }
1288
- if (localFirstUseVariables.size !== 0) goal._localFirstUseVariables = localFirstUseVariables;
1289
1288
  }
1290
1289
  }
1291
1290
 
@@ -1538,7 +1537,7 @@ function bundledLengthIterator(solver, group, goal, env) {
1538
1537
 
1539
1538
  // Delayed and constrained variables need the ordinary solver's wake-up
1540
1539
  // points. The fast path is deliberately limited to plain finite-tree terms.
1541
- if (env._clpz != null) return null;
1540
+ if (env._prologAttributes != null) return null;
1542
1541
  const length = deref(goal.args[1], env);
1543
1542
  if (length.type === VAR && env._delays?.has(length.name)) return null;
1544
1543
 
@@ -2043,7 +2042,7 @@ function fastCountBranch(branch, env) {
2043
2042
 
2044
2043
  function fastCountPureGoal(solver, goal, env) {
2045
2044
  if (solver.isoStrict || solver.solutionLimit !== Infinity || solver.maxInferences !== Infinity ||
2046
- env._clpz != null || env._variableConstraints != null || env._prologAttributes != null || (env._delays != null && env._delays.size !== 0)) return null;
2045
+ env._variableConstraints != null || env._prologAttributes != null || (env._delays != null && env._delays.size !== 0)) return null;
2047
2046
  const budget = { remaining: 8192 };
2048
2047
  const branches = expandPureCountGoal(solver, goal, goal.module ?? 'user', new Set(), budget);
2049
2048
  if (branches == null) return null;
@@ -2681,7 +2680,8 @@ function activeVariantIn(goal, env, active) {
2681
2680
  // Variant calls must have the same predicate indicator. Avoid walking
2682
2681
  // large matrix/list arguments for every unrelated active predicate.
2683
2682
  if (candidate?.type !== goal.type || candidate?.name !== goal.name ||
2684
- candidate?.arity !== goal.arity) continue;
2683
+ candidate?.arity !== goal.arity ||
2684
+ (candidate?.module ?? 'user') !== (goal.module ?? 'user')) continue;
2685
2685
  goalShape ??= variantShape(goal, env);
2686
2686
  entry.variantShape ??= variantShape(candidate, entry.env);
2687
2687
  if (goalShape !== entry.variantShape) continue;
@@ -71,7 +71,7 @@ function runHook(program, module, name, input, loadContext = null) {
71
71
  const goal = compound(name, [input, output]);
72
72
  goal.module = owner;
73
73
  const solver = new Solver(program, {
74
- solutionLimit: 1,
74
+ solutionLimit: Infinity,
75
75
  maxInferences: MAX_EXPANSION_INFERENCES,
76
76
  sourceLoadContext: loadContext,
77
77
  });
@@ -3,7 +3,6 @@
3
3
  // Modules are loaded on demand by explicit use_module/1-2 or by the conservative
4
4
  // source-level interop autoloader declared below.
5
5
  import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
6
- import { clpzBuiltins } from './clpz.js';
7
6
  import { attsBuiltins } from './atts.js';
8
7
  import { expansionBuiltins } from './expansion-builtins.js';
9
8
  import { scryerCompatibilityBuiltins } from './scryer-compat.js';
@@ -203,7 +202,6 @@ export function createEyePrologRegistry() {
203
202
  attsBuiltins.register(registry);
204
203
  expansionBuiltins.register(registry);
205
204
  scryerCompatibilityBuiltins.register(registry);
206
- clpzBuiltins.register(registry);
207
205
  registry.eyePrologLibrary = true;
208
206
  return registry;
209
207
  }
package/src/term.js CHANGED
@@ -112,7 +112,6 @@ export class Env {
112
112
  // Backtrackable blackboard entries used by Scryer-compatible libraries.
113
113
  // Values are logical terms and the map is copy-on-write across Env clones.
114
114
  this._backtrackableBlackboard = null;
115
- this._clpz = null;
116
115
  this._occursCheckHandler = null;
117
116
  this._localVariables = null;
118
117
  }
@@ -131,7 +130,6 @@ export class Env {
131
130
  clone._attributeHookRunner = this._attributeHookRunner;
132
131
  clone._pendingAttributeGoals = this._pendingAttributeGoals;
133
132
  clone._backtrackableBlackboard = this._backtrackableBlackboard;
134
- clone._clpz = this._clpz;
135
133
  clone._occursCheckHandler = this._occursCheckHandler;
136
134
  clone._localVariables = this._localVariables;
137
135
  return clone;
@@ -153,7 +151,6 @@ export class Env {
153
151
  this._prologAttributes = other._prologAttributes;
154
152
  this._pendingAttributeGoals = other._pendingAttributeGoals;
155
153
  this._backtrackableBlackboard = other._backtrackableBlackboard;
156
- this._clpz = other._clpz;
157
154
  // Execution callbacks belong to the Solver driving this Env, not to the
158
155
  // logical branch being adopted from an inner attribute-hook call.
159
156
  this._localVariables = other._localVariables;
package/src/write.js CHANGED
@@ -86,7 +86,12 @@ function isWordOperatorToken(token) {
86
86
 
87
87
  function compactPrefixOperator(token, argument) {
88
88
  if (isWordOperatorToken(token)) return `${token} ${argument}`;
89
- return `${token}${compactBoundaryNeedsSpace(token, argument) ? ' ' : ''}${argument}`;
89
+ // Without layout, `op(...)` is functional notation rather than prefix-operator
90
+ // notation. ISO 7.10.5 h requires operator-form output and layout whenever
91
+ // ambiguity could otherwise arise, so a parenthesized prefix argument must
92
+ // never be glued directly to the operator token.
93
+ const needsLayout = argument.startsWith('(') || compactBoundaryNeedsSpace(token, argument);
94
+ return `${token}${needsLayout ? ' ' : ''}${argument}`;
90
95
  }
91
96
 
92
97
  function compactPostfixOperator(argument, token) {
@@ -188,28 +193,6 @@ function chooseOperator(term, table) {
188
193
  return null;
189
194
  }
190
195
 
191
- function startsWithUnsignedNumber(term, env, table, options) {
192
- const resolved = deref(term, env);
193
- if (resolved.type === NUMBER) return !resolved.name.startsWith('-');
194
- if (resolved.type !== COMPOUND || isCons(resolved)) return false;
195
- if (options.numbervars && resolved.name === '$VAR' && resolved.arity === 1) {
196
- const index = deref(resolved.args[0], env);
197
- if (index.type === NUMBER && /^\d+$/.test(index.name) &&
198
- writeNumberedVariable(Number(index.name)) != null) {
199
- return false;
200
- }
201
- }
202
- const definition = chooseOperator(resolved, table);
203
- if (definition == null) return false;
204
- if (definition.specifier === 'xf' || definition.specifier === 'yf') {
205
- return startsWithUnsignedNumber(resolved.args[0], env, table, options);
206
- }
207
- if (definition.specifier === 'xfx' || definition.specifier === 'xfy' || definition.specifier === 'yfx') {
208
- return startsWithUnsignedNumber(resolved.args[0], env, table, options);
209
- }
210
- return false;
211
- }
212
-
213
196
  function printableReadVariableNames(term, env, explicit) {
214
197
  const names = new Map(explicit);
215
198
  const used = new Set(names.values());
@@ -353,13 +336,30 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
353
336
  let text;
354
337
  if (specifier === 'fx' || specifier === 'fy') {
355
338
  const argumentPriority = specifier === 'fx' ? priority - 1 : priority;
356
- // A separated `- 1` is still read as the negative number -1. When
357
- // the source term is the unary -/1 compound, parenthesize a positive
358
- // numeric-leading argument so writeq/write_term remain read-back safe
359
- // (WG17 #135, #183, #215, #216, and #248).
360
- if (resolved.name === '-' && startsWithUnsignedNumber(resolved.args[0], env, table, options)) {
339
+ // Corrigendum 3 adds mandatory parentheses for prefix - when its
340
+ // argument is a non-negative number or is written in infix/postfix
341
+ // operator form (ISO 7.10.5 h 2(iii-iv)).
342
+ const child = deref(resolved.args[0], env);
343
+ const childNumbervar = options.numbervars && child.type === COMPOUND &&
344
+ child.name === '$VAR' && child.arity === 1 && (() => {
345
+ const index = deref(child.args[0], env);
346
+ return index.type === NUMBER && /^\d+$/.test(index.name) &&
347
+ writeNumberedVariable(Number(index.name)) != null;
348
+ })();
349
+ const childUsesSpecialNotation = isCons(child) ||
350
+ (child.type === COMPOUND && child.name === '{}' && child.arity === 1) || childNumbervar;
351
+ const childDefinition = childUsesSpecialNotation ? null : chooseOperator(child, table);
352
+ const negativeNeedsParentheses = resolved.name === '-' && (
353
+ (child.type === NUMBER && !child.name.startsWith('-')) ||
354
+ ['xf', 'yf', 'xfx', 'xfy', 'yfx'].includes(childDefinition?.specifier)
355
+ );
356
+ if (negativeNeedsParentheses) {
357
+ // ISO 7.10.5 h 2(iii-iv): prefix - requires parentheses around a
358
+ // non-negative number and around an argument written in infix or
359
+ // postfix operator form. Keep layout before `(` so the result is
360
+ // operator notation, not functional notation such as `-(...)`.
361
361
  const argument = format(resolved.args[0], env, options, table, 1200);
362
- text = options.minimalOperatorSpacing ? `${token}(${argument})` : `${token} (${argument})`;
362
+ text = `${token} (${argument})`;
363
363
  } else {
364
364
  const argument = format(resolved.args[0], env, options, table, argumentPriority);
365
365
  text = options.minimalOperatorSpacing ? compactPrefixOperator(token, argument) : `${token} ${argument}`;
@@ -11,10 +11,14 @@ upstream assertion.
11
11
 
12
12
  ## Current standing
13
13
 
14
+ The percentage below is **traceability coverage**, not a conformance pass rate.
15
+ A represented case still has to pass the executable upstream assertion in
16
+ `test/run-wg17.mjs`.
17
+
14
18
  | Measure | Count |
15
19
  | --- | ---: |
16
20
  | Active upstream cases | 366 |
17
- | Executable EyeProlog dispositions | 366 (100.0%) |
21
+ | Upstream cases represented in the executable matrix | 366 (100.0%) |
18
22
  | Not yet traced one-by-one | 0 |
19
23
  | Deleted upstream identifiers | #20, #273 |
20
24