eyeprolog 1.2.24 → 1.2.26

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/repl.js CHANGED
@@ -4,6 +4,7 @@ import { readSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { formalErrorTerm } from './iso.js';
7
+ import { characterCodeConstantEnd, quotedEscapeEnd } from './syntax-scan.js';
7
8
 
8
9
  const ANSWER_HELP = `
9
10
  SPACE, "n" or ";": next solution, if any
@@ -344,38 +345,6 @@ async function readInteractiveTerm(reader) {
344
345
  }
345
346
  }
346
347
 
347
- function quotedEscapeEnd(source, index) {
348
- const escaped = source[index + 1] ?? '';
349
- if (!escaped) return index;
350
-
351
- // A backslash-newline pair is a continuation escape (6.4.2), so the
352
- // newline is not a literal quoted character. Accept CRLF as the host text
353
- // representation of the same continuation boundary.
354
- if (escaped === '\n') return index + 1;
355
- if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
356
-
357
- // ISO 6.4.2.1 octal and hexadecimal escapes are terminated by a
358
- // backslash. Consume that terminator as part of the escape so the REPL
359
- // scanner does not mistake it for an escape of the following quote.
360
- if (escaped === 'x') {
361
- let cursor = index + 2;
362
- while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
363
- return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
364
- }
365
- if (/^[0-9]$/.test(escaped)) {
366
- let cursor = index + 1;
367
- // Scan all decimal digits here, including 8 and 9. This scanner only
368
- // locates the end of a candidate quoted escape; the parser remains
369
- // authoritative and rejects non-octal digits.
370
- while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
371
- return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
372
- }
373
-
374
- // Meta escapes, symbolic control escapes, and continuation escapes consume
375
- // one character after the backslash. The parser performs validity checks.
376
- return index + 1;
377
- }
378
-
379
348
  function terminalFullStop(source) {
380
349
  let quote = null;
381
350
  let lineComment = false;
@@ -411,6 +380,11 @@ function terminalFullStop(source) {
411
380
  }
412
381
  continue;
413
382
  }
383
+ const characterCodeEnd = characterCodeConstantEnd(source, i);
384
+ if (characterCodeEnd != null) {
385
+ i = characterCodeEnd;
386
+ continue;
387
+ }
414
388
  if (ch === '%') {
415
389
  lineComment = true;
416
390
  continue;
@@ -636,6 +610,10 @@ function formatAnswer(engine, state, variables, env) {
636
610
  operators,
637
611
  variableNames: names,
638
612
  maxPriority: valueMaxPriority,
613
+ // A binding value is the right-hand `arg` of =/2. ISO 6.3.3.1 permits
614
+ // current operator atoms in argument and list-element positions without
615
+ // quotes, just as writeq/1 already prints them.
616
+ operatorAtomsAsArgs: true,
639
617
  })}`);
640
618
  }
641
619
  return bindings.length === 0 ? 'true' : bindings.join(', ');
@@ -0,0 +1,54 @@
1
+ // Lightweight lexical helpers shared by the interactive and ISO stream
2
+ // readers. These locate token boundaries only; parser.js remains responsible
3
+ // for accepting or rejecting the token itself.
4
+
5
+ export function quotedEscapeEnd(source, index) {
6
+ const escaped = source[index + 1] ?? '';
7
+ if (!escaped) return index;
8
+
9
+ if (escaped === '\n') return index + 1;
10
+ if (escaped === '\r' && source[index + 2] === '\n') return index + 2;
11
+
12
+ if (escaped === 'x') {
13
+ let cursor = index + 2;
14
+ while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
15
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
16
+ }
17
+ if (/^[0-9]$/.test(escaped)) {
18
+ let cursor = index + 1;
19
+ // Include 8 and 9 while finding the boundary. The parser rejects them as
20
+ // non-octal rather than letting the reader split the malformed token.
21
+ while (/^[0-9]$/.test(source[cursor] ?? '')) cursor++;
22
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
23
+ }
24
+
25
+ return index + 1;
26
+ }
27
+
28
+ export function characterCodeConstantEnd(source, apostropheIndex) {
29
+ if (source[apostropheIndex] !== "'" || source[apostropheIndex - 1] !== '0') return null;
30
+ // The 0 must begin a numeric token. In particular, do not reinterpret the
31
+ // apostrophe in an identifier such as a0'x as character-code notation.
32
+ if (/[A-Za-z0-9_]/.test(source[apostropheIndex - 2] ?? '')) return null;
33
+
34
+ const characterIndex = apostropheIndex + 1;
35
+ const character = source[characterIndex] ?? '';
36
+ if (!character) return apostropheIndex;
37
+ // `0''` is two tokens (0 and the empty atom), and `0'\\\n...'
38
+ // likewise starts a quoted atom containing a continuation. Only `0'''`
39
+ // denotes the character-code constant for an apostrophe.
40
+ if (character === "'" && source[characterIndex + 1] !== "'") return null;
41
+ if (character === '\\' && ['\n', '\r'].includes(source[characterIndex + 1])) return null;
42
+ if (character === '\\') return quotedEscapeEnd(source, characterIndex);
43
+
44
+ // An apostrophe character is doubled in 0''' exactly as it is in a quoted
45
+ // atom. Consume both so neither one opens a quoted-token scan state.
46
+ if (character === "'" && source[characterIndex + 1] === "'") return characterIndex + 1;
47
+
48
+ const firstCode = character.charCodeAt(0);
49
+ if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
50
+ const secondCode = source.charCodeAt(characterIndex + 1);
51
+ if (secondCode >= 0xdc00 && secondCode <= 0xdfff) return characterIndex + 1;
52
+ }
53
+ return characterIndex;
54
+ }
package/src/write.js CHANGED
@@ -28,6 +28,7 @@ function atomNeedsQuotes(name) {
28
28
  if (!name) return true;
29
29
  if (name === '[]' || name === '{}') return false;
30
30
  if (name === '...') return false;
31
+ if (name.startsWith('/*')) return true;
31
32
  if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
32
33
  if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
33
34
  for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
@@ -104,6 +105,7 @@ function writeNumberedVariable(index) {
104
105
  }
105
106
 
106
107
  function operatorName(name) {
108
+ if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
107
109
  if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return name;
108
110
  if (/^[!#$&*+\-./<=>@^~\\;:]+$/.test(name)) return name;
109
111
  return quoteAtom(name);
@@ -142,6 +144,12 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
142
144
  // `arg` production: an atom that is a current operator is valid there
143
145
  // without quoting. Keep lexical exceptions such as `|` quoted.
144
146
  if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
147
+ if (!options.ignoreOps && context !== 'argument' && table.has(resolved.name)) {
148
+ const definitions = table.get(resolved.name);
149
+ const requiresParentheses = definitions.some(({ specifier }) =>
150
+ ['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(specifier));
151
+ if (requiresParentheses) return `(${operatorName(resolved.name)})`;
152
+ }
145
153
  return writeAtom(resolved.name);
146
154
  }
147
155
  if (resolved.type === NUMBER) return resolved.name;
@@ -186,11 +194,20 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
186
194
  const argumentPriority = specifier === 'fx' ? priority - 1 : priority;
187
195
  text = `${token} ${format(resolved.args[0], env, options, table, argumentPriority)}`;
188
196
  } else if (specifier === 'xf' || specifier === 'yf') {
189
- const argumentPriority = specifier === 'xf' ? priority - 1 : priority;
197
+ let argumentPriority = specifier === 'xf' ? priority - 1 : priority;
198
+ const childDefinition = chooseOperator(deref(resolved.args[0], env), table);
199
+ if (childDefinition?.priority === priority &&
200
+ ['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(childDefinition.specifier)) {
201
+ argumentPriority = priority - 1;
202
+ }
190
203
  text = `${format(resolved.args[0], env, options, table, argumentPriority)} ${token}`;
191
204
  } else {
192
- const leftPriority = specifier === 'yfx' ? priority : priority - 1;
205
+ let leftPriority = specifier === 'yfx' ? priority : priority - 1;
193
206
  const rightPriority = specifier === 'xfy' ? priority : priority - 1;
207
+ const leftDefinition = chooseOperator(deref(resolved.args[0], env), table);
208
+ if (leftDefinition?.priority === priority && ['fx', 'fy'].includes(leftDefinition.specifier)) {
209
+ leftPriority = priority - 1;
210
+ }
194
211
  const left = format(resolved.args[0], env, options, table, leftPriority);
195
212
  const right = format(resolved.args[1], env, options, table, rightPriority);
196
213
  text = resolved.name === ',' ? `${left}, ${right}`
@@ -18,7 +18,7 @@ error-ordering alternative to an individual executable assertion.
18
18
 
19
19
  | Requirement | Status | EyeProlog evidence / remaining work |
20
20
  | --- | --- | --- |
21
- | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the selected WG17 high-risk syntax case. Complete character-by-character external syntax sweep remains open. |
21
+ | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete 366-case WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
22
22
  | 5.1(b) execute conforming Prolog goals | audit | Clause 7-9 conformance corpus plus regression/API/example gates. A normative goal-semantics ledger is still being expanded. |
23
23
  | 5.1(c) reject nonconforming text/read-terms | audit | Dedicated syntax-error cases and strict-core extension rejection. Exhaustive lexical rejection coverage remains open. |
24
24
  | 5.1(d) document permitted variations | audit | Major implementation-defined choices are documented in *The Art of EyeProlog*. Every occurrence of “implementation defined/dependent/specific” in Part 1 still needs a final documentation cross-check. |
@@ -30,7 +30,7 @@ error-ordering alternative to an individual executable assertion.
30
30
 
31
31
  | Standard area | Status | Current evidence |
32
32
  | --- | --- | --- |
33
- | Clause 6 — tokens, terms, lists, operators, quoted text | audit | `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, `wg17_syntax_high_risk`, quoted-layout/escape error cases, writer/read-back regressions. |
33
+ | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete 366-case WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
35
35
  | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
36
36
  | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
@@ -75,6 +75,7 @@ A release intended to advance ISO conformance must pass all of:
75
75
  npm test
76
76
  npm run test:iso-strict
77
77
  npm run test:conformance
78
+ npm run test:wg17-syntax
78
79
  ```
79
80
 
80
81
  The unified `npm test` gate includes the strict-core suite. Expected conformance
@@ -88,9 +89,9 @@ ISO/IEC 13211-1 processor” until all of the following are true:
88
89
  1. every normative Part 1 processor requirement is represented in this ledger;
89
90
  2. every `audit` row above has been reduced to explicit pass/not-applicable or
90
91
  documented implementation-defined choices;
91
- 3. the complete external WG17 syntax/conversion/variable-name conformity
92
- corpus has been run against the strict core mode, with every difference
93
- explained or fixed;
92
+ 3. the external WG17 conversion and variable-name conformity corpora have
93
+ joined the now-complete syntax corpus in strict core mode, with every
94
+ difference explained or fixed;
94
95
  4. prescribed modes, errors, side effects, and relevant error precedence for
95
96
  every Part 1 built-in have executable coverage;
96
97
  5. every implementation-defined/dependent/specific choice required to be
@@ -0,0 +1,42 @@
1
+ # WG17 syntax traceability status
2
+
3
+ Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
4
+ Upstream inventory checked: 2026-08-15
5
+
6
+ This ledger counts an upstream case only when its WG17 identifier, query,
7
+ expected ISO disposition, and observed EyeProlog outcome are stored in the
8
+ offline executable matrix. Semantically similar parser tests are not inferred
9
+ as coverage.
10
+
11
+ ## Current standing
12
+
13
+ | Measure | Count |
14
+ | --- | ---: |
15
+ | Active upstream cases | 366 |
16
+ | Executable EyeProlog dispositions | 366 (100.0%) |
17
+ | Not yet traced one-by-one | 0 |
18
+ | Deleted upstream identifiers | #20, #273 |
19
+
20
+ The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
21
+ three upstream `waits` cases are checked through EyeProlog's interactive input
22
+ hook; the other 363 cases are checked for their exact stored
23
+ success output, bindings, failure, or ISO error category.
24
+
25
+ ## Traceable evidence
26
+
27
+ | Executable evidence | Referenced IDs | WG17 cases |
28
+ | --- | ---: | --- |
29
+ | [complete offline executable matrix](../run-wg17-syntax.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
30
+
31
+ The evidence groups overlap. Their union is **366** active cases:
32
+ #1–#19, #21–#272, #274–#368.
33
+
34
+ ## Untraced upstream identifiers
35
+
36
+ None.
37
+
38
+ ## Maintenance
39
+
40
+ 1. Refresh the dated fixture when the upstream table changes.
41
+ 2. Review any changed ISO expectation before updating an observed snapshot.
42
+ 3. Keep this generated status page synchronized in the release gate.
@@ -1,6 +1,6 @@
1
1
  % High-risk ISO syntax/write regressions, independently derived from
2
2
  % ISO/IEC 13211-1 clauses 6.3, 6.4 and 7.10 and cross-checked against the
3
- % public WG17 conformity-testing syntax cases (#1, #7-10, #14-15, #18, #28-31, #33-34, #301, #315-316).
3
+ % public WG17 conformity-testing syntax cases (#1, #7-10, #14-15, #18, #28-31, #33-34, #299, #301, #315-316).
4
4
 
5
5
  %% goal: wg17_numeric_escape
6
6
  wg17_numeric_escape :-
@@ -21,6 +21,10 @@ wg17_operator_precedence :-
21
21
  writeq(a*(b+c)), nl,
22
22
  writeq((a :- b,c)), nl.
23
23
 
24
+ %% goal: wg17_spaced_prefix_operator(T)
25
+ wg17_spaced_prefix_operator(T) :-
26
+ (\+ (a,b)) = \+(T).
27
+
24
28
  %% goal: wg17_canonical_list
25
29
  wg17_canonical_list :-
26
30
  write_canonical([a]), nl.
@@ -9,6 +9,7 @@ wg17_operator_arguments.
9
9
  a * (b + c)
10
10
  a :- b, c
11
11
  wg17_operator_precedence.
12
+ wg17_spaced_prefix_operator((a, b)).
12
13
  '.'(a,[])
13
14
  wg17_canonical_list.
14
15
  '\0\'