eyeprolog 1.5.11 → 1.5.12

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.5.11",
6
+ "version": "1.5.12",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/execute.js CHANGED
@@ -51,6 +51,7 @@ export function executeGoals(program, solver, goals, { onAnswer = () => {} } = {
51
51
  function currentWriteOptions(program, solver) {
52
52
  return {
53
53
  doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
54
+ doubleBar: !solver.isoStrict,
54
55
  operators: [...program.operators.values()],
55
56
  quoted: true,
56
57
  };
package/src/iso.js CHANGED
@@ -1988,6 +1988,7 @@ function writeBuiltin(mode) {
1988
1988
  generateVariableNames: true,
1989
1989
  variableNameState: solver.writeVariableState,
1990
1990
  operators: solver.program.operators.values(),
1991
+ doubleBar: !solver.isoStrict,
1991
1992
  }));
1992
1993
  yield env;
1993
1994
  };
@@ -2019,6 +2020,7 @@ function* writeTermBuiltin({ solver, goal, env }) {
2019
2020
  generateVariableNames: true,
2020
2021
  variableNameState: solver.writeVariableState,
2021
2022
  operators: solver.program.operators.values(),
2023
+ doubleBar: !solver.isoStrict,
2022
2024
  }));
2023
2025
  yield env;
2024
2026
  }
package/src/parser.js CHANGED
@@ -937,9 +937,37 @@ class Parser {
937
937
  this.parserFlagState.doubleQuotes === 'chars'
938
938
  ? atom(character)
939
939
  : numberTerm(character.codePointAt(0)));
940
- let list = emptyList();
941
- for (let i = items.length - 1; i >= 0; i--) list = cons(items[i], list);
942
- return list;
940
+ let tail = emptyList();
941
+
942
+ // Trealla-compatible double-bar right splice (issue #88): in normal
943
+ // mode, a double-quoted chars/codes prefix may be followed by `||Tail`.
944
+ // It is equivalent to the corresponding list prefix with Tail as its
945
+ // final list tail, e.g. "ab"||T is [a,b|T] with double_quotes(chars).
946
+ // The extension binds at priority 1 (tighter than ordinary operators)
947
+ // and is deliberately absent from the strict ISO profile.
948
+ if (!this.strictIso && this.token.type === TOK.BAR) {
949
+ const state = {
950
+ pos: this.pos,
951
+ line: this.line,
952
+ previousToken: this.previousToken,
953
+ token: this.token,
954
+ };
955
+ this.advance();
956
+ if (this.token.type === TOK.BAR) {
957
+ this.advance();
958
+ tail = this.parseTerm(operatorStrength(1), false, allowBar, false);
959
+ } else {
960
+ // A single bar remains ordinary bar syntax. We had to advance once
961
+ // to distinguish it from `||`, so restore the tokenizer state.
962
+ this.pos = state.pos;
963
+ this.line = state.line;
964
+ this.previousToken = state.previousToken;
965
+ this.token = state.token;
966
+ }
967
+ }
968
+
969
+ for (let i = items.length - 1; i >= 0; i--) tail = cons(items[i], tail);
970
+ return tail;
943
971
  }
944
972
  if (this.token.type === TOK.NUMBER) {
945
973
  const value = this.token.text;
package/src/repl.js CHANGED
@@ -746,6 +746,7 @@ function formatAnswer(engine, state, variables, env) {
746
746
  operatorAtomsAsArgs: true,
747
747
  dottedGraphicAtoms: true,
748
748
  doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
749
+ doubleBar: !state.strictIso,
749
750
  };
750
751
  let generated = 0;
751
752
 
@@ -844,6 +845,7 @@ function formatError(engine, state, error) {
844
845
  quoted: true,
845
846
  operators: [...state.program.operators.values()],
846
847
  variableNames,
848
+ doubleBar: !state.strictIso,
847
849
  })}.`;
848
850
  }
849
851
  const message = error?.message ?? String(error);
package/src/term.js CHANGED
@@ -1067,14 +1067,18 @@ function writeString(value, quoteStrings) {
1067
1067
  return out + '"';
1068
1068
  }
1069
1069
 
1070
- function quotedListText(term, env, doubleQuotes) {
1070
+ function quotedListSplice(term, env, doubleQuotes) {
1071
1071
  if (doubleQuotes !== 'chars' && doubleQuotes !== 'codes') return null;
1072
1072
  const characters = [];
1073
1073
  let cursor = term;
1074
1074
  while (true) {
1075
1075
  cursor = deref(cursor, env);
1076
- if (isEmptyList(cursor)) return characters.length === 0 ? null : characters.join('');
1077
- if (!isCons(cursor)) return null;
1076
+ if (isEmptyList(cursor)) {
1077
+ return characters.length === 0 ? null : { text: characters.join(''), tail: null };
1078
+ }
1079
+ if (!isCons(cursor)) {
1080
+ return characters.length === 0 ? null : { text: characters.join(''), tail: cursor };
1081
+ }
1078
1082
  const item = deref(cursor.args[0], env);
1079
1083
  if (doubleQuotes === 'chars') {
1080
1084
  if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
@@ -1090,8 +1094,12 @@ function quotedListText(term, env, doubleQuotes) {
1090
1094
  }
1091
1095
 
1092
1096
  function writeList(term, env, options) {
1093
- const quotedText = quotedListText(term, env, options.doubleQuotes);
1094
- if (quotedText != null) return writeString(quotedText, true);
1097
+ const quotedSplice = quotedListSplice(term, env, options.doubleQuotes);
1098
+ if (quotedSplice != null && (quotedSplice.tail == null || options.doubleBar === true)) {
1099
+ const prefix = writeString(quotedSplice.text, true);
1100
+ if (quotedSplice.tail == null) return prefix;
1101
+ return `${prefix}||${termToString(quotedSplice.tail, env, true, options)}`;
1102
+ }
1095
1103
  const parts = [];
1096
1104
  let cursor = term;
1097
1105
  while (true) {
@@ -1110,6 +1118,10 @@ export function termToString(term, env = new Env(), quoteStrings = true, options
1110
1118
  options = {
1111
1119
  ...options,
1112
1120
  doubleQuotes: options.doubleQuotes ?? 'chars',
1121
+ // termToString is also used for context-free processor-error messages.
1122
+ // Keep the normal-profile `||` extension opt-in here so strict errors do
1123
+ // not accidentally emit syntax that the strict parser rejects.
1124
+ doubleBar: options.doubleBar === true,
1113
1125
  readVariableNames: options.readVariableNames instanceof Map ? options.readVariableNames : new Map(),
1114
1126
  usedReadVariableNames: options.usedReadVariableNames instanceof Set ? options.usedReadVariableNames : new Set(),
1115
1127
  };
package/src/write.js CHANGED
@@ -152,14 +152,18 @@ function writeString(value) {
152
152
  return out + '"';
153
153
  }
154
154
 
155
- function quotedListText(term, env, doubleQuotes) {
155
+ function quotedListSplice(term, env, doubleQuotes) {
156
156
  if (doubleQuotes !== 'chars' && doubleQuotes !== 'codes') return null;
157
157
  const characters = [];
158
158
  let cursor = term;
159
159
  while (true) {
160
160
  cursor = deref(cursor, env);
161
- if (isEmptyList(cursor)) return characters.length === 0 ? null : characters.join('');
162
- if (!isCons(cursor)) return null;
161
+ if (isEmptyList(cursor)) {
162
+ return characters.length === 0 ? null : { text: characters.join(''), tail: null };
163
+ }
164
+ if (!isCons(cursor)) {
165
+ return characters.length === 0 ? null : { text: characters.join(''), tail: cursor };
166
+ }
163
167
  const item = deref(cursor.args[0], env);
164
168
  if (doubleQuotes === 'chars') {
165
169
  if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
@@ -331,8 +335,15 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
331
335
  }
332
336
 
333
337
  if (!options.ignoreOps && isCons(resolved)) {
334
- const quotedText = quotedListText(resolved, env, options.doubleQuotes);
335
- if (quotedText != null) return writeString(quotedText);
338
+ const quotedSplice = quotedListSplice(resolved, env, options.doubleQuotes);
339
+ if (quotedSplice != null && (quotedSplice.tail == null || options.doubleBar)) {
340
+ const prefix = writeString(quotedSplice.text);
341
+ if (quotedSplice.tail == null) return prefix;
342
+ // `||` is a normal-profile syntax extension with priority 1, so a
343
+ // looser operator in the right operand must be parenthesized to keep the
344
+ // emitted text readable as the same term.
345
+ return `${prefix}||${format(quotedSplice.tail, env, options, table, 1, 'term')}`;
346
+ }
336
347
  const parts = [];
337
348
  let cursor = resolved;
338
349
  while (true) {
@@ -442,6 +453,9 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
442
453
  ignoreOps: options.ignoreOps === true,
443
454
  numbervars: options.numbervars !== false,
444
455
  doubleQuotes: options.doubleQuotes,
456
+ // `||` is a normal-profile syntax extension. Public normal-mode writers
457
+ // enable it by default; strict callers explicitly turn it off.
458
+ doubleBar: options.doubleBar !== false,
445
459
  variableNames: options.generateVariableNames === true
446
460
  ? printableGeneratedVariableNames(term, env, explicitVariableNames, options.variableNameState)
447
461
  : printableReadVariableNames(term, env, explicitVariableNames),
@@ -113,7 +113,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
113
113
 
114
114
  | Part 1 extension hook | EyeProlog normal-profile feature | Strict-core disposition |
115
115
  | --- | --- | --- |
116
- | 5.5.1 Syntax | Part 2 modules, Part 3 grammar-rule expansion, and embedded quad syntax | Module directives are rejected; grammar rules remain ordinary `-->/2` terms rather than being expanded; quad syntax is rejected. Unicode PCS membership/classification is implementation defined and therefore shared with normal mode rather than treated as an extension. |
116
+ | 5.5.1 Syntax | Part 2 modules, Part 3 grammar-rule expansion, embedded quad syntax, and Trealla-compatible `"text"||Tail` right-splicing for double-quoted `chars`/`codes` lists | Module directives are rejected; grammar rules remain ordinary `-->/2` terms rather than being expanded; quad syntax and double-bar right-splicing are rejected. Unicode PCS membership/classification is implementation defined and therefore shared with normal mode rather than treated as an extension. |
117
117
  | 5.5.2 Predefined operators | Part 3 `|` and EyeProlog's labelable infix `(?-)/2`; CLP(Z) operators when that library is imported | Only the Part 1 operator table is predefined; a conforming `op/3` may still add permitted operators. |
118
118
  | 5.5.3 Character-conversion mapping | No non-identity initial `Convc` extension | Identity initial mapping. |
119
119
  | 5.5.4 Types | The normal JavaScript API exposes an implementation-specific string term `stringTerm(Text)`. It is disjoint from the five Part 1 term types; normal term order places it after atoms and before compound terms, and normal `atomic/1` treats it as atomic. It has no Prolog source token syntax (double-quoted source still follows `double_quotes`), is non-callable for term-to-clause conversion, is not evaluable as an arithmetic expression, and writes as a double-quoted host string. | Strict mode rejects a programmatic string term at program/goal entry with `representation_error(term)`, so the additional type cannot enter the Part 1 execution domain. **defined** — `src/term.js`, `src/program.js`, `src/solver.js`; strict API-boundary regression. |
@@ -1,4 +1,4 @@
1
1
  scryer_escapes(ok).
2
2
  scryer_numbers(-1, 1, 1, 1, 1).
3
3
  scryer_comments(7, 7).
4
- scryer_terms({1}, "a", [a, b | c], []).
4
+ scryer_terms({1}, "a", "ab"||c, []).
@@ -1 +1 @@
1
- answer(append_to_improper_tail, [a, b | tail]).
1
+ answer(append_to_improper_tail, "ab"||tail).
@@ -1 +1 @@
1
- answer([a, b | tail]).
1
+ answer("ab"||tail).
@@ -10,4 +10,4 @@ value(compound, pair(3, nested(atom, "xy"))).
10
10
  value(arity_zero_atom, nil).
11
11
  value(empty_list, []).
12
12
  value(proper_list, "abc").
13
- value(improper_list, [a, b | tail]).
13
+ value(improper_list, "ab"||tail).
@@ -2441,7 +2441,7 @@ c4 ?- call((!;1)).
2441
2441
  '?- Xs = [].\n' +
2442
2442
  '?- true.\n' +
2443
2443
  '?- true.\n' +
2444
- '?- Xs0 = [a | Xs].\n' +
2444
+ '?- Xs0 = "a"||Xs.\n' +
2445
2445
  '?- ',
2446
2446
  'stdout');
2447
2447
  assertEqual(result.stderr, '', 'stderr');
@@ -6947,6 +6947,44 @@ function whiteBoxCases() {
6947
6947
  assertEqual(`${quotedAtom.type}:${quotedAtom.name}`, 'atom:aλ', 'atom');
6948
6948
  },
6949
6949
  },
6950
+ {
6951
+ name: 'double-bar syntax splices double-quoted list prefixes (issue #88)',
6952
+ run: () => {
6953
+ const chars = parseGoalText('p("ab"||Tail)').args[0];
6954
+ assertEqual(chars.args[0].name, 'a', 'chars first element');
6955
+ assertEqual(chars.args[1].args[0].name, 'b', 'chars second element');
6956
+ assertEqual(chars.args[1].args[1].type, 'var', 'chars partial tail type');
6957
+ assertEqual(chars.args[1].args[1].name, 'Tail', 'chars partial tail name');
6958
+ assertEqual(termToString(chars, new Env(), true, { doubleBar: true }), '"ab"||Tail', 'chars readback');
6959
+
6960
+ const codes = parseGoalText('p("aλ" || Tail)', { doubleQuotes: 'codes' }).args[0];
6961
+ assertEqual(codes.args[0].name, '97', 'codes first element');
6962
+ assertEqual(codes.args[1].args[0].name, '955', 'codes second element');
6963
+ assertEqual(termToString(codes, new Env(), true, { doubleQuotes: 'codes', doubleBar: true }), '"aλ"||Tail', 'codes readback');
6964
+
6965
+ const nested = parseGoalText('p(["ab"|Tail])');
6966
+ assertEqual(termToString(nested), 'p(["ab" | Tail])', 'single bar remains list syntax');
6967
+
6968
+ for (const options of [{ isoStrict: true }, { doubleQuotes: 'atom' }]) {
6969
+ let threw = false;
6970
+ try { parseGoalText('p("ab"||Tail)', options); } catch (_) { threw = true; }
6971
+ assertEqual(threw, true, `double-bar rejected for ${JSON.stringify(options)}`);
6972
+ }
6973
+ },
6974
+ },
6975
+ {
6976
+ name: 'REPL prints partial character lists with double-bar syntax (issue #88)',
6977
+ run: () => {
6978
+ const result = runCli([], {
6979
+ input: 'phrase("Think of this text, and much more.", S0,S).\nhalt.\n',
6980
+ });
6981
+ assertEqual(result.status, 0, 'REPL exit status');
6982
+ assertIncludes(result.stdout,
6983
+ 'S0 = "Think of this text, and much more."||S.',
6984
+ 'Trealla-compatible partial string answer');
6985
+ assertNotIncludes(result.stdout, "S0 = ['T'", 'legacy expanded list answer');
6986
+ },
6987
+ },
6950
6988
  {
6951
6989
  name: 'double_quotes directives affect subsequent source text',
6952
6990
  run: () => {
@@ -474,6 +474,12 @@ Prolog programs accepted by EyeProlog are built from terms:
474
474
  - compound terms: `point(3, 4)`, `reading(temp, 91)`;
475
475
  - lists: `[]`, `[red, green, blue]`, `[Head | Tail]`.
476
476
 
477
+ In normal mode, double-quoted character/code lists also support Trealla's
478
+ right-splice notation. With `double_quotes(chars)`, `"ab"||Tail` is shorthand
479
+ for `[a,b|Tail]`; with `double_quotes(codes)`, it denotes `[97,98|Tail]`. The
480
+ splice is not available when `double_quotes(atom)` is active, and
481
+ `--iso-strict` rejects it as an implementation-specific syntax extension.
482
+
477
483
  Plain atom constants begin with a lowercase ASCII letter. Variables begin with
478
484
  an uppercase letter or underscore. The bare `_` is anonymous and every
479
485
  occurrence is fresh. `_Name` is a named variable; repeated occurrences refer to
@@ -5492,8 +5498,10 @@ still retained where adjacent graphic tokens would otherwise merge, as in
5492
5498
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5493
5499
  `variable_names/1`. Normal mode additionally accepts the EyeProlog extension
5494
5500
  `double_quotes(true|false)`: `true` lets eligible character/code lists use the
5495
- current `double_quotes` representation, while strict ISO mode rejects this
5496
- implementation-specific write option. Normal mode also accepts the
5501
+ current `double_quotes` representation. Proper lists can therefore be written
5502
+ as `"text"`, while a partial list such as `[a,b|Tail]` is written as
5503
+ `"ab"||Tail`. Strict ISO mode rejects this implementation-specific write
5504
+ option. Normal mode also accepts the
5497
5505
  implementation-specific boolean `spacing(true|false)` option: `false` emits
5498
5506
  only separators required to avoid lexical ambiguity, while `true` adds
5499
5507
  conventional layout around operators. For example,
@@ -5588,6 +5596,8 @@ lowercase ASCII letter. Variables begin with uppercase or underscore. The bare
5588
5596
  `_` is fresh each time. Single quotes delimit quoted atoms; double quotes use
5589
5597
  ISO double-quoted-list notation. Integers, decimals, scientific notation,
5590
5598
  binary/octal/hexadecimal integers, and character-code constants are accepted.
5599
+ Normal mode additionally accepts the Trealla-compatible `"text"||Tail`
5600
+ right-splice for double-quoted `chars`/`codes` lists; strict ISO mode does not.
5591
5601
 
5592
5602
  The processor character set is shared by normal and `--iso-strict` modes because
5593
5603
  Part 1 makes it implementation defined rather than an extension boundary.
@@ -8017,7 +8027,9 @@ Notable implementation boundaries are:
8017
8027
  - module and ISO/IEC TS 13211-3-oriented DCG compatibility profiles are supported in normal mode, without a complete Part 2/Part 3 certification claim;
8018
8028
  - variables cannot occupy functor or predicate position;
8019
8029
  - double-quoted text follows `double_quotes` exactly; the default `chars` value
8020
- matches Trealla and Scryer and may be changed to `codes` or `atom`;
8030
+ matches Trealla and Scryer and may be changed to `codes` or `atom`; normal
8031
+ mode additionally accepts Trealla-compatible `"text"||Tail` right-splicing
8032
+ for the `chars` and `codes` values, while strict mode rejects that syntax;
8021
8033
  - `write_term/2-3` implements the Part 1 plus Corrigendum 3 `quoted/1`,
8022
8034
  `ignore_ops/1`, `numbervars/1`, and `variable_names/1` option surface,
8023
8035
  including option validation and traversal rules; normal mode also offers