eyeprolog 1.3.55 → 1.3.57

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.3.55",
6
+ "version": "1.3.57",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -393,9 +393,9 @@ function* functorBuiltin({ goal, env }) {
393
393
 
394
394
  const name = deref(goal.args[1], env);
395
395
  const arityTerm = deref(goal.args[2], env);
396
- // ISO 8.5.1.3 orders construction-mode diagnostics. A compound Name is an
397
- // atomic type error (before an Arity type error); only an atomic non-atom
398
- // used for positive arity receives type_error(atom,...).
396
+ // Keep EyeProlog's deterministic construction-mode diagnostics aligned with
397
+ // the individual 8.5.1.3 conditions. Under 7.12, a call satisfying several
398
+ // error conditions at once does not acquire a globally mandated table order.
399
399
  if (name.type === VAR) throw new PrologError('instantiation_error');
400
400
  if (arityTerm.type === VAR) throw new PrologError('instantiation_error');
401
401
  if (name.type === COMPOUND) throw new PrologError('type_error(atomic)', name);
@@ -562,7 +562,9 @@ function* clauseBuiltin({ solver, goal, env }) {
562
562
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
563
563
  const indicator = compound('/', [atom(head.name), numberTerm(head.arity)]);
564
564
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
565
- // ISO 8.8.1.3 places private-procedure access before Body callability.
565
+ // EyeProlog selects private-procedure access before Body callability when both
566
+ // conditions hold. ISO 7.12 permits an implementation-dependent choice when
567
+ // more than one error condition is simultaneously satisfied.
566
568
  if (isProcessorStaticProcedure(solver, head) ||
567
569
  (solver.isoStrict && group && !group.dynamic)) {
568
570
  throw new PrologError('permission_error(access, private_procedure)', indicator);
@@ -590,12 +592,22 @@ function requireClauseHead(head) {
590
592
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
591
593
  }
592
594
 
593
- function convertAssertedBody(term) {
595
+ export function convertClauseBodyTerm(term, culprit = term) {
594
596
  if (term.type === VAR) return compound('call', [term]);
595
- if (term.type === COMPOUND && term.name === ',' && term.arity === 2) {
596
- return compound(',', [convertAssertedBody(term.args[0]), convertAssertedBody(term.args[1])]);
597
+ // ISO 7.6.2 converts each argument of conjunction, disjunction, and if-then
598
+ // recursively when a term is converted to a clause body. This matters for
599
+ // variables (which become call(Var)) and for invalid nested terms, which
600
+ // must be rejected while assert[az]/1 performs the conversion rather than
601
+ // being stored and failing only if that branch is later executed.
602
+ if (term.type === COMPOUND && [',', ';', '->'].includes(term.name) && term.arity === 2) {
603
+ return compound(term.name, [
604
+ convertClauseBodyTerm(term.args[0], culprit),
605
+ convertClauseBodyTerm(term.args[1], culprit),
606
+ ]);
607
+ }
608
+ if (term.type !== ATOM && term.type !== COMPOUND) {
609
+ throw new PrologError('type_error(callable)', culprit);
597
610
  }
598
- if (term.type !== ATOM && term.type !== COMPOUND) throw new PrologError('type_error(callable)', term);
599
611
  return term;
600
612
  }
601
613
 
@@ -633,7 +645,7 @@ function assertBuiltin(atStart) {
633
645
  return function* ({ solver, goal, env }) {
634
646
  const parts = clauseParts(goal.args[0], env);
635
647
  requireClauseHead(parts.head);
636
- const body = convertAssertedBody(parts.body);
648
+ const body = convertClauseBodyTerm(parts.body);
637
649
  assertModifiable(solver, parts.head, goal.module ?? 'user');
638
650
  const copied = freshCopy(compound('$clause', [parts.head, body]), env);
639
651
  solver.program.insertDynamicClause({
@@ -772,9 +784,9 @@ function* opBuiltin({ solver, goal, env }) {
772
784
  const priorityTerm = deref(goal.args[0], env);
773
785
  const specifierTerm = deref(goal.args[1], env);
774
786
 
775
- // ISO 8.14.3.3 prescribes the ordering below. In particular, all three
776
- // instantiation cases precede type/domain errors, and Operator list-shape
777
- // errors precede priority/specifier domain errors.
787
+ // Keep EyeProlog's selected 8.14.3.3 overlap order deterministic. Each
788
+ // individual condition remains observable; ISO 7.12 leaves the choice
789
+ // implementation dependent when several error conditions hold at once.
778
790
  if (priorityTerm.type === VAR) throw new PrologError('instantiation_error');
779
791
  if (specifierTerm.type === VAR) throw new PrologError('instantiation_error');
780
792
  const operator = opListPreflight(goal.args[2], env);
@@ -933,12 +945,11 @@ function optionList(term, env) {
933
945
  return items.map((item) => deref(item, env));
934
946
  }
935
947
 
936
- // read_term/3 and write_term/3 have prescribed error ordering in ISO 8.14.
937
- // In particular, a partial option list (or a variable option element) is
938
- // diagnosed before several stream errors, while the relative order of a
939
- // non-list Options term and an invalid stream-or-alias differs between the
940
- // read and write predicates. Split list-shape inspection from full option
941
- // validation so those predicates can follow the standard order exactly.
948
+ // Keep read_term/3 and write_term/3 diagnostics deterministic and make every
949
+ // 8.14 error condition independently testable. ISO 7.12 leaves the choice
950
+ // implementation dependent when several error conditions hold simultaneously,
951
+ // so this preflight order is an EyeProlog processor choice rather than a claim
952
+ // that the textual order of an error table is globally mandatory.
942
953
  function preflightOptionList(term, env) {
943
954
  const value = deref(term, env);
944
955
  if (value.type === VAR) throw new PrologError('instantiation_error');
@@ -1007,9 +1018,9 @@ function* openBuiltin({ solver, goal, env }) {
1007
1018
  const streamTarget = deref(goal.args[2], env);
1008
1019
  const optionTerm = goal.arity === 3 ? emptyList() : goal.args[3];
1009
1020
 
1010
- // ISO 8.11.5.3 + Corrigenda 2/3. Keep each prescribed condition
1011
- // observable on its own and apply the published ordering for deterministic
1012
- // diagnostics when several arguments are erroneous.
1021
+ // ISO 8.11.5.3 + Corrigenda 2/3. Keep each required condition observable
1022
+ // on its own. When several conditions hold, the order below is EyeProlog's
1023
+ // deterministic 7.12 processor choice unless a procedural rule constrains it.
1013
1024
  if (source.type === VAR) throw new PrologError('instantiation_error');
1014
1025
  if (mode.type === VAR) throw new PrologError('instantiation_error');
1015
1026
  const preflight = preflightOptionList(optionTerm, env);
@@ -1173,16 +1184,21 @@ function isStreamPropertyPattern(value) {
1173
1184
  function* streamPropertyBuiltin({ solver, goal, env }) {
1174
1185
  const reference = deref(goal.args[0], env);
1175
1186
  const propertyPattern = deref(goal.args[1], env);
1187
+ let fixedStreamId = null;
1176
1188
  if (reference.type !== VAR) {
1177
- const id = streamTermReference(goal.args[0], env);
1178
- if (!solver.io.resolve(id)) throw new PrologError('domain_error(stream)', reference);
1189
+ // 8.11.8.1 enumerates (Stream, Property) pairs for currently open
1190
+ // streams. A ground term with the implementation's stream-term shape is
1191
+ // still a stream-term even when its stream is no longer open; in that
1192
+ // case the enumeration simply has no matching pair. domain_error(stream)
1193
+ // is reserved for a term which is not a stream-term at all.
1194
+ fixedStreamId = streamTermReference(goal.args[0], env);
1179
1195
  }
1180
1196
  if (!isStreamPropertyPattern(propertyPattern)) {
1181
1197
  throw new PrologError('domain_error(stream_property)', propertyPattern);
1182
1198
  }
1183
1199
  const streams = reference.type === VAR
1184
1200
  ? [...solver.io.streams.values()]
1185
- : [solver.io.resolve(streamTermReference(goal.args[0], env))];
1201
+ : [solver.io.resolve(fixedStreamId)].filter(Boolean);
1186
1202
  for (const stream of streams) {
1187
1203
  for (const property of streamProperties(stream)) {
1188
1204
  const next = env.clone();
@@ -1222,11 +1238,10 @@ function preflightInputUnitTarget(name, target, env, solver) {
1222
1238
  throw new PrologError('type_error(in_character)', value);
1223
1239
  }
1224
1240
  } else if (name.endsWith('code')) {
1225
- // ISO 8.12.1.3/8.12.2.3 puts the integer type check before stream
1226
- // diagnostics, but the integer-is-not-an-in-character-code
1227
- // representation error comes after the stream existence/mode/type/EOF
1228
- // conditions. Defer that range/repertoire check until the input entity
1229
- // itself has been inspected below.
1241
+ // Keep the selected 8.12 overlap behavior stable: integer type validation
1242
+ // is done here, while the in-character-code repertoire check is deferred
1243
+ // until after stream/entity diagnostics. ISO 7.12 permits this processor
1244
+ // choice when those error conditions overlap.
1230
1245
  if (value.type !== VAR && (value.type !== NUMBER || !isDecimalInteger(value.name))) {
1231
1246
  throw new PrologError('type_error(integer)', value);
1232
1247
  }
@@ -1280,10 +1295,9 @@ function inputUnitBuiltin(name) {
1280
1295
  if (!binary && unit != null && solver.isoStrict && !isStrictIsoPcsCharacter(unit)) {
1281
1296
  throw new PrologError('representation_error(character)');
1282
1297
  }
1283
- // For get_code/peek_code the published error table places an invalid
1284
- // in-character-code target after stream and input-entity errors. Doing
1285
- // this here prevents a fixed bad Code from masking (for example) an
1286
- // output-stream, binary-stream, past-EOF, or invalid-character error.
1298
+ // EyeProlog deliberately defers this representation check until after
1299
+ // stream and input-entity diagnostics. ISO 7.12 permits that deterministic
1300
+ // choice when several 8.12 error conditions hold simultaneously.
1287
1301
  const targetValue = deref(target, env);
1288
1302
  if (name.endsWith('code') && targetValue.type !== VAR &&
1289
1303
  !validStrictInputCharacterCode(targetValue, solver)) {
@@ -1586,9 +1600,9 @@ function* readTermBuiltin({ solver, goal, env }) {
1586
1600
  const optionTerm = goal.args[goal.arity - 1];
1587
1601
  const streamTerm = goal.arity === 3 ? deref(goal.args[0], env) : null;
1588
1602
 
1589
- // ISO 8.14.1.3 error order:
1590
- // a) variable stream; b) partial/options-variable; c) bad stream-or-alias;
1591
- // d) non-list options; e) invalid read option; then stream existence/mode.
1603
+ // EyeProlog's selected 8.14.1.3 overlap order. Each listed error condition
1604
+ // is covered independently; 7.12 makes simultaneous-error selection
1605
+ // implementation dependent unless another procedural requirement constrains it.
1592
1606
  if (streamTerm?.type === VAR) throw new PrologError('instantiation_error');
1593
1607
  const preflight = preflightOptionList(optionTerm, env);
1594
1608
  if (goal.arity === 3) streamReference(goal.args[0], env);
@@ -1702,9 +1716,9 @@ function* writeTermBuiltin({ solver, goal, env }) {
1702
1716
  const optionTerm = goal.args[goal.arity - 1];
1703
1717
  const streamTerm = goal.arity === 3 ? deref(goal.args[0], env) : null;
1704
1718
 
1705
- // ISO 8.14.2.3 error order:
1706
- // a) variable stream; b) partial/options-variable; c) non-list options;
1707
- // d) bad stream-or-alias; e) invalid write option; then stream errors.
1719
+ // EyeProlog's selected 8.14.2.3 overlap order. Each listed error condition
1720
+ // is covered independently; 7.12 makes simultaneous-error selection
1721
+ // implementation dependent unless another procedural requirement constrains it.
1708
1722
  if (streamTerm?.type === VAR) throw new PrologError('instantiation_error');
1709
1723
  const preflight = preflightOptionList(optionTerm, env);
1710
1724
  const optionItems = requireProperOptionList(preflight);
@@ -2177,7 +2191,9 @@ function flatFindallTemplate(template) {
2177
2191
 
2178
2192
  function* findallBuiltin({ solver, goal, env }) {
2179
2193
  const [template, innerGoal, bag] = goal.args;
2180
- // ISO 8.10.1.3 diagnoses Goal before Instances when both are erroneous.
2194
+ // EyeProlog validates Goal before Instances when both are erroneous. ISO 7.12
2195
+ // makes simultaneous-error selection implementation dependent; this order is
2196
+ // a documented/tested processor choice, not a blanket table-order rule.
2181
2197
  const invoked = callable(innerGoal, env);
2182
2198
  assertListOrPartial(bag, env);
2183
2199
  const collector = solver.cloneForInnerGoal(10000000);
@@ -2242,7 +2258,9 @@ function sortedUnique(items) {
2242
2258
 
2243
2259
  function allSolutionsBuiltin(asSet) {
2244
2260
  return function* ({ solver, goal, env }) {
2245
- // ISO 8.10.2.3/8.10.3.3 diagnose the iterated goal before Instances.
2261
+ // EyeProlog validates the iterated goal before Instances. ISO 7.12 makes
2262
+ // simultaneous-error selection implementation dependent; keep this stable as
2263
+ // a processor choice rather than describing it as mandatory table ordering.
2246
2264
  const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2247
2265
  assertListOrPartial(goal.args[2], env);
2248
2266
  const free = freeVariables(iterated, goal.args[0], quantified, env);
package/src/program.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Program representation and clause indexing.
2
2
  // Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
3
- import { ATOM, COMPOUND, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
3
+ import { ATOM, COMPOUND, STRING, VAR, Env, atom, compound, deref, flattenConjunction, isScalar, numberTerm, properListItems, termToString, variable } from './term.js';
4
4
  import { formatTermForWrite } from './write.js';
5
5
  import {
6
6
  ISO_OPERATOR_DEFINITIONS,
@@ -12,7 +12,7 @@ import {
12
12
  parseGoalText,
13
13
  tryParseClausesFastInto,
14
14
  } from './parser.js';
15
- import { PrologError, getStrictIsoRegistry } from './iso.js';
15
+ import { PrologError, convertClauseBodyTerm, getStrictIsoRegistry } from './iso.js';
16
16
  import { currentWorkingDirectory, fs, path } from './platform.js';
17
17
  import {
18
18
  standardLibrarySources,
@@ -33,6 +33,21 @@ import {
33
33
  const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
34
34
  const FAST_PARSE_ABORT = Symbol('fastParseAbort');
35
35
  const PROGRAM_BUILD_BATCH_SIZE = 16384;
36
+
37
+ function termContainsStringExtension(term, seen = new Set()) {
38
+ if (term == null || typeof term !== 'object' || seen.has(term)) return false;
39
+ seen.add(term);
40
+ if (term.type === STRING) return true;
41
+ if (term.type === COMPOUND) return term.args.some((arg) => termContainsStringExtension(arg, seen));
42
+ return false;
43
+ }
44
+
45
+ function clauseContainsStringExtension(clause) {
46
+ if (clause == null) return false;
47
+ if (clause.head0Type === STRING || clause.head1Type === STRING) return true;
48
+ if (termContainsStringExtension(clause.head) || termContainsStringExtension(clause.query)) return true;
49
+ return Array.isArray(clause.body) && clause.body.some((goal) => termContainsStringExtension(goal));
50
+ }
36
51
  export class Program {
37
52
  constructor(clauses = [], options = {}) {
38
53
  this.clauses = [];
@@ -574,6 +589,12 @@ class ProgramBuilder {
574
589
  let lastGroup = this.lastGroup;
575
590
 
576
591
  for (const clause of clauses) {
592
+ if (program.strictIso && clauseContainsStringExtension(clause)) {
593
+ // Programmatic clauses are part of the public embedding surface. The
594
+ // normal profile permits its host string term extension, but the Part 1
595
+ // strict profile must reject that implementation-specific sixth type.
596
+ throw new PrologError('representation_error(term)');
597
+ }
577
598
  if (clause?.kind === 'quad') {
578
599
  const module = clause.module ?? 'user';
579
600
  annotateGoalModule(clause.query, module);
@@ -606,6 +627,14 @@ class ProgramBuilder {
606
627
  }
607
628
 
608
629
  if (!isDirectiveClause(clause)) {
630
+ if (program.strictIso && Array.isArray(clause.body) && clause.body.length > 0) {
631
+ // ISO 7.6.2 applies when source text is prepared as well as when a
632
+ // clause term is asserted dynamically. Keep the original variable
633
+ // objects while wrapping variable goals in call/1 so sharing with
634
+ // the head (and recursively through ;/2 and ->/2) is preserved for
635
+ // later clause/2 and retract/1 conversion back to terms.
636
+ clause.body = clause.body.map((goal) => convertClauseBodyTerm(goal));
637
+ }
609
638
  assertHeadIsDefinable(clause.head, program.strictIso);
610
639
  const head = clause.head;
611
640
  if (head.type !== ATOM && head.type !== COMPOUND) continue;
package/src/solver.js CHANGED
@@ -1,7 +1,7 @@
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
- ATOM, COMPOUND, NUMBER, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
4
+ ATOM, COMPOUND, NUMBER, STRING, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
5
5
  flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
6
6
  numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
@@ -45,6 +45,26 @@ function raiseOccursCheckError(left, right, env) {
45
45
  throw error;
46
46
  }
47
47
 
48
+
49
+ function rejectStrictIsoStringTerms(terms, env) {
50
+ const seen = new Set();
51
+ const visit = (term) => {
52
+ const resolved = deref(term, env);
53
+ if (resolved == null || seen.has(resolved)) return;
54
+ seen.add(resolved);
55
+ if (resolved.type === STRING) {
56
+ // `stringTerm()` is a normal-profile host/API term extension. It has no
57
+ // Part 1 abstract/token syntax, so strict execution must reject it rather
58
+ // than silently introducing a sixth term type through the embedding API.
59
+ throw new PrologError('representation_error(term)');
60
+ }
61
+ if (resolved.type === COMPOUND) {
62
+ for (const arg of resolved.args) visit(arg);
63
+ }
64
+ };
65
+ for (const term of terms) visit(term);
66
+ }
67
+
48
68
  export class Solver {
49
69
  constructor(program, options = {}) {
50
70
  this.program = program;
@@ -301,6 +321,7 @@ export class Solver {
301
321
  *solve(goals, env = new Env(), depth = 0) {
302
322
  if (!Array.isArray(goals)) goals = [goals];
303
323
  env.setOccursCheckHandler(this.occursCheckHandler);
324
+ if (this.isoStrict) rejectStrictIsoStringTerms(goals, env);
304
325
 
305
326
  const writeVariableState = this.writeVariableState;
306
327
  if (writeVariableState.depth === 0) {