eyeprolog 1.3.39 → 1.3.41

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.
@@ -1333,6 +1333,24 @@ c4 ?- call((!;1)).
1333
1333
  assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1334
1334
  },
1335
1335
  },
1336
+ {
1337
+ name: 'quad recursion-cycle evidence consistently refutes finite failure (issue #58 comment 5381101420)',
1338
+ run: () => {
1339
+ const result = publicApi.runQuads(`inf :- inf, inf.
1340
+
1341
+ 23
1342
+ ?- inf.
1343
+ loops.
1344
+ false.
1345
+ `);
1346
+ assertEqual(result.total, 2, 'quad total');
1347
+ assertEqual(result.passed, 1, 'loops passed');
1348
+ assertEqual(result.failed, 1, 'false failed');
1349
+ assertEqual(result.undecided, 0, 'no undecided result');
1350
+ assertIncludes(result.stdout, 'expected: false.', 'finite failure diagnostic');
1351
+ assertNotIncludes(result.stdout, 'undecided: recursion cycle encountered.', 'cycle is decisive');
1352
+ },
1353
+ },
1336
1354
  {
1337
1355
  name: 'quad search-budget exhaustion is undecided rather than loops or failure (issue #58)',
1338
1356
  run: () => {
@@ -1383,6 +1401,21 @@ c4 ?- call((!;1)).
1383
1401
  assertIncludes(nsto.stdout, 'quads: FAILED 34, <input>:1', 'NSTO diagnostic');
1384
1402
  },
1385
1403
  },
1404
+ {
1405
+ name: 'quads peeks/1 supplies one unconsumed look-ahead character (issue #62)',
1406
+ run: () => {
1407
+ const result = publicApi.runQuads(`29
1408
+ ?- read(X).
1409
+ inputs("1."), X = 1, unexpected.
1410
+ inputs("1."), peeks(" "), X = 1.
1411
+ inputs("1. "), peeks(" "), X = 1, unexpected.
1412
+ `);
1413
+ assertEqual(result.total, 3, 'quad total');
1414
+ assertEqual(result.passed, 3, 'quad passed');
1415
+ assertEqual(result.failed, 0, 'quad failed');
1416
+ assertEqual(result.stdout, 'quads: 3 run, 3 passed, 0 failed.\n', 'quad report');
1417
+ },
1418
+ },
1386
1419
  {
1387
1420
  name: 'outputs/1 accepts DCG bodies over captured characters (issue #59)',
1388
1421
  run: () => {
@@ -3282,6 +3315,28 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3282
3315
  }
3283
3316
  },
3284
3317
  },
3318
+ {
3319
+ name: 'read and read_term report invalid UTF-8 as representation_error(character) (issue #64)',
3320
+ run: () => {
3321
+ const invalidPath = path.join(tmp, `read-invalid-utf8-${++tmpCounter}.bin`);
3322
+ fs.writeFileSync(invalidPath, Buffer.from([0xff]));
3323
+ const quotedPath = sourceAtom(invalidPath);
3324
+ for (const goal of [
3325
+ `open(${quotedPath}, read, S, []), read(S, C)`,
3326
+ `open(${quotedPath}, read, S, []), read_term(S, C, [])`,
3327
+ `open(${quotedPath}, read, S, []), set_input(S), read(C)`,
3328
+ `open(${quotedPath}, read, S, []), set_input(S), read_term(C, [])`,
3329
+ ]) {
3330
+ let caught = null;
3331
+ try {
3332
+ run('', { goal });
3333
+ } catch (error) {
3334
+ caught = error;
3335
+ }
3336
+ assertEqual(caught?.formal, 'representation_error(character)', `${goal} invalid UTF-8`);
3337
+ }
3338
+ },
3339
+ },
3285
3340
  {
3286
3341
  name: 'writeq gives unnamed variables underscore-prefixed names (issue #53)',
3287
3342
  run: () => {
@@ -3310,6 +3365,13 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3310
3365
  );
3311
3366
  },
3312
3367
  },
3368
+ {
3369
+ name: 'writeq separates operators only where lexical ambiguity requires it (issue #63)',
3370
+ run: () => {
3371
+ const source = "emit :- writeq(1+2), put_char('|'), writeq(a+ -b), put_char('|'), writeq(a+b*c), nl.\n";
3372
+ assertEqual(run(source, { goal: 'emit' }).stdout, '1+2|a+ -b|a+b*c\nemit.\n', 'minimal operator spacing');
3373
+ },
3374
+ },
3313
3375
  {
3314
3376
  name: 'write predicates and write_term options select distinct formats',
3315
3377
  run: () => {
@@ -3332,7 +3394,7 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3332
3394
  ].join('\n');
3333
3395
  assertEqual(
3334
3396
  run(source, { goal: 'emit' }).stdout,
3335
- "hello world|'hello world'|a + b * c|+(a,*(b,c))|hello world|'hello world'|+(a,b)|a + b|A|$VAR(0)|pair(Left,Right)|\"ab\"|[a,b]emit.\n",
3397
+ "hello world|'hello world'|a+b*c|+(a,*(b,c))|hello world|'hello world'|+(a,b)|a+b|A|$VAR(0)|pair(Left,Right)|\"ab\"|[a,b]emit.\n",
3336
3398
  'stdout',
3337
3399
  );
3338
3400
  },
@@ -5412,8 +5412,11 @@ where needed, and `write_canonical/1-2` exposes canonical structure. Dotted
5412
5412
  graphic atoms do not need quotes merely because they contain a period:
5413
5413
  `writeq(./*)`, `writeq(.*)`, and `writeq(...*)` output `./*`, `.*`, and `...*`
5414
5414
  respectively. ISO term output uses only the separator characters needed by the
5415
- syntax, so functional arguments and list elements are emitted compactly; for
5416
- example `writeq([a,b])` outputs `[a,b]`.
5415
+ syntax, so functional arguments, list elements, and operator applications are
5416
+ emitted compactly when no lexical ambiguity would arise. For example,
5417
+ `writeq([a,b])` outputs `[a,b]` and `writeq(1+2)` outputs `1+2`; a separator is
5418
+ still retained where adjacent graphic tokens would otherwise merge, as in
5419
+ `a+ -b`.
5417
5420
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5418
5421
  `variable_names/1`.
5419
5422
 
@@ -6891,31 +6894,88 @@ variable in the renamed exception term. `...` and `ad_infinitum` accept further
6891
6894
  indented descriptions after one query are independent checks: each re-runs the
6892
6895
  query, each is counted in the `quads:` summary, and a failing description does
6893
6896
  not suppress later descriptions for that query. `inputs/1` supplies and checks
6894
- consumed characters. `outputs/1` checks characters emitted while reaching the
6895
- described answer or error, including output produced before a later exception.
6896
- Its argument may be an exact character list/string or a DCG body: terminal
6897
- sequences, conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards,
6898
- and user-defined DCG nonterminals are matched against the captured characters.
6899
- Following Trealla's quad convention, `sto` declares that the query is subject
6900
- to occurs-check; the answer portion of an `sto`-annotated leaf remains
6901
- implementation-dependent and is not compared. EyeProlog can nevertheless check
6902
- some of the declaration without a second execution: the normal finite-tree
6903
- unifier records a concrete occurs-check event as positive STO evidence. A
6904
- naturally completed finite execution with no such event disproves `sto` (so
6905
- `?- true. sto.` fails), while a search/resource boundary leaves the declaration
6906
- conservatively unchecked. When the same quad declares STO and an occurs-check
6907
- event is observed, an unannotated `unexpected` leaf does not reject an outcome
6908
- that is implementation-dependent precisely because the query is STO. This is
6909
- partial STO detection, not a decision procedure for the full STO/NSTO property.
6910
- `loops` explicitly asks for bounded nontermination evidence and accepts direct
6911
- active-variant cycle evidence from EyeProlog's normal recursion guard, with the
6912
- loop depth/inference bounds as a fallback. Ordinary quad descriptions also have
6913
- a finite inference budget (100000 by default); exhausting it does **not** mean
6914
- `loops` or `false`, but produces an `UNDECIDED` result. The JavaScript API may
6915
- override this with `quadMaxInferences`, while `loopMaxDepth` and
6916
- `loopMaxInferences` control the explicit `loops` probe. The advanced stream
6917
- annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
6918
- annotation, are not executed by the current runner.
6897
+ exactly the characters consumed by the query. `peeks/1` may add one character
6898
+ that is available for look-ahead but must remain unconsumed. The runner puts an
6899
+ invalid-character sentinel immediately after the declared input boundary, so a
6900
+ reader cannot accidentally use an artificial end-of-file to decide that a full
6901
+ stop terminates the term. For example:
6902
+
6903
+ ```eyeprolog
6904
+ ?- read(X).
6905
+ inputs("1."), X = 1, unexpected.
6906
+ inputs("1."), peeks(" "), X = 1.
6907
+ inputs("1. "), peeks(" "), X = 1, unexpected.
6908
+ ```
6909
+
6910
+ `outputs/1` checks characters emitted while reaching the described answer or
6911
+ error, including output produced before a later exception. Its argument may be
6912
+ an exact character list/string or a DCG body: terminal sequences,
6913
+ conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards, and
6914
+ user-defined DCG nonterminals are matched against the captured characters.
6915
+ The `waits` and unordered `other_answer_sequence` annotations are not executed
6916
+ by the current runner.
6917
+
6918
+ #### STO, loops, and undecided quad results
6919
+
6920
+ Following Trealla's quad convention, `sto` declares that a query is subject to
6921
+ occurs-check. EyeProlog checks this conservatively rather than attempting a
6922
+ complete STO/NSTO decision procedure. During the query's ordinary execution,
6923
+ the finite-tree unifier records a concrete occurs-check event as positive STO
6924
+ evidence; the query is not run a second time merely to probe STO-ness. A finite
6925
+ execution that completes naturally without such an event disproves `sto`, while
6926
+ a search or resource boundary leaves the declaration conservatively unchecked.
6927
+ The answer portion of an `sto`-annotated leaf remains implementation-dependent
6928
+ and is therefore not compared.
6929
+
6930
+ For example, the cyclic binding in the first query provides positive STO
6931
+ evidence, whereas the second query is finite and cannot be STO:
6932
+
6933
+ ```eyeprolog
6934
+ ?- X = s(X).
6935
+ X = ..., unexpected.
6936
+ false, unexpected.
6937
+ sto, false
6938
+ | sto, true.
6939
+
6940
+ ?- true.
6941
+ sto. % fails: no STO evidence
6942
+ ```
6943
+
6944
+ When a quad declares STO and the execution observes an occurs-check event, an
6945
+ unannotated `unexpected` leaf does not reject the implementation-dependent
6946
+ finite-tree outcome. This is partial STO detection only: EyeProlog makes a
6947
+ definite statement where execution provides definite evidence and otherwise
6948
+ does not guess.
6949
+
6950
+ `loops` is kept distinct from merely exhausting the quad runner's resources.
6951
+ EyeProlog accepts structural nontermination evidence such as an active-variant
6952
+ recursion cycle, with the loop depth/inference bounds as a bounded fallback. A
6953
+ structural cycle is also strong enough to refute a finite `false` description;
6954
+ it is not reported as undecided merely because the same query was checked by a
6955
+ different answer description:
6956
+
6957
+ ```eyeprolog
6958
+ inf :- inf, inf.
6959
+
6960
+ ?- inf.
6961
+ loops.
6962
+ ```
6963
+
6964
+ Ordinary answer descriptions also have a finite inference budget (100000 by
6965
+ default). Exhausting that budget does **not** establish `loops` and does not
6966
+ turn an unfinished search into `false`; instead the description is reported as
6967
+ `UNDECIDED`, for example:
6968
+
6969
+ ```text
6970
+ quads: UNDECIDED expensive_case, program.pl:12
6971
+ undecided: inference limit reached.
6972
+ ```
6973
+
6974
+ Thus quad execution has three useful outcomes: passed, failed, and undecided.
6975
+ When there are no failures but at least one undecided description, the CLI exits
6976
+ with status `2`. The JavaScript API may override the ordinary search budget with
6977
+ `quadMaxInferences`; `loopMaxDepth` and `loopMaxInferences` control the explicit
6978
+ `loops` probe.
6919
6979
 
6920
6980
  The JavaScript API exposes the same operation without process I/O:
6921
6981