eyeprolog 1.6.0 → 1.6.1

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.6.0",
6
+ "version": "1.6.1",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -1036,8 +1036,17 @@ class Parser {
1036
1036
  // mode, a double-quoted chars/codes prefix may be followed by `||Tail`.
1037
1037
  // It is equivalent to the corresponding list prefix with Tail as its
1038
1038
  // final list tail, e.g. "ab"||T is [a,b|T] with double_quotes(chars).
1039
- // The extension binds at priority 1 (tighter than ordinary operators)
1040
- // and is deliberately absent from the strict ISO profile.
1039
+ // Per the WG17-accepted design (issue #116; see
1040
+ // https://www.complang.tuwien.ac.at/ulrich/iso-prolog/double_bar),
1041
+ // the whole `dql||term` production, and the tail `term` itself, both
1042
+ // have priority 0: the tail is a *primary* term, not an
1043
+ // operator-priority-1 term. An operator argument therefore requires
1044
+ // explicit parentheses, e.g. "abc"||(1 op 2) with `op` any infix
1045
+ // operator, however low its own declared priority. This also stops
1046
+ // an infix operator immediately following the tail from being
1047
+ // absorbed into it: with `op(1, xfy, op)` declared, "abc"||1 op 2
1048
+ // must parse as ("abc"||1) op 2, not "abc"||(1 op 2). The extension
1049
+ // is deliberately absent from the strict ISO profile.
1041
1050
  if (!this.strictIso && this.token.type === TOK.BAR) {
1042
1051
  const state = {
1043
1052
  pos: this.pos,
@@ -1048,7 +1057,7 @@ class Parser {
1048
1057
  this.advance();
1049
1058
  if (this.token.type === TOK.BAR) {
1050
1059
  this.advance();
1051
- tail = this.parseTerm(operatorStrength(1), false, allowBar, false);
1060
+ tail = this.parseTerm(operatorStrength(0), false, allowBar, false);
1052
1061
  } else {
1053
1062
  // A single bar remains ordinary bar syntax. We had to advance once
1054
1063
  // to distinguish it from `||`, so restore the tokenizer state.
package/src/write.js CHANGED
@@ -356,10 +356,14 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
356
356
  if (quotedSplice != null && (quotedSplice.tail == null || options.doubleBar)) {
357
357
  const prefix = writeString(quotedSplice.text);
358
358
  if (quotedSplice.tail == null) return prefix;
359
- // `||` is a normal-profile syntax extension with priority 1, so a
360
- // looser operator in the right operand must be parenthesized to keep the
361
- // emitted text readable as the same term.
362
- return `${prefix}||${format(quotedSplice.tail, env, options, table, 1, 'term')}`;
359
+ // `||` is a normal-profile syntax extension whose tail has priority 0
360
+ // (issue #116; see
361
+ // https://www.complang.tuwien.ac.at/ulrich/iso-prolog/double_bar), so
362
+ // any operator term in the right operand must be parenthesized to
363
+ // keep the emitted text readable back as the same term — including
364
+ // one whose own declared priority is as low as 1, since the parser
365
+ // accepts nothing but a bare primary term after `||` unparenthesized.
366
+ return `${prefix}||${format(quotedSplice.tail, env, options, table, 0, 'term')}`;
363
367
  }
364
368
 
365
369
  if (!options.ignoreOps) {
@@ -201,6 +201,60 @@ export function whiteBoxCases() {
201
201
  }
202
202
  },
203
203
  },
204
+ {
205
+ // https://www.complang.tuwien.ac.at/ulrich/iso-prolog/double_bar: the
206
+ // whole `dql||term` production, and the tail `term` itself, both have
207
+ // priority 0 (the tail is a primary term, not a priority-1 term as an
208
+ // earlier revision of this extension had it). With a custom operator
209
+ // declared as low as priority 1, an unparenthesized tail must stop
210
+ // before that operator rather than absorb it, and a parenthesized tail
211
+ // must print with parentheses to remain distinguishable on readback.
212
+ name: 'double-bar tail has priority 0, so it is not absorbed into a following low-priority operator (issue #116)',
213
+ run: () => {
214
+ const operatorDefinitions = [...ISO_OPERATOR_DEFINITIONS, [1, 'xfy', 'op']];
215
+
216
+ // "abc"||1 op 2 must parse as ("abc"||1) op 2, not "abc"||(1 op 2):
217
+ // op's priority (1) is not <= the tail's max priority (0), so an
218
+ // unparenthesized tail parse must stop at `1` and let `op 2` apply
219
+ // to the whole splice term instead.
220
+ const unparenthesized = parseGoalText('p("abc"||1 op 2)', { operatorDefinitions }).args[0];
221
+ assertEqual(unparenthesized.name, 'op', 'unabsorbed tail: outer functor');
222
+ assertEqual(unparenthesized.args[1].name, '2', 'unabsorbed tail: outer right operand');
223
+ const splice = unparenthesized.args[0];
224
+ assertEqual(splice.args[1].args[1].args[1].name, '1', 'unabsorbed tail: splice tail value');
225
+
226
+ // "abc"||(1 op 2) is a different term: op(1, 2) spliced in as the
227
+ // tail.
228
+ const parenthesizedTail = parseGoalText('p("abc"||(1 op 2))', { operatorDefinitions }).args[0];
229
+ assertEqual(parenthesizedTail.name, '.', 'parenthesized tail: still a cons cell');
230
+ assertEqual(parenthesizedTail.args[1].args[1].args[1].name, 'op', 'parenthesized tail: splice tail is op(1, 2)');
231
+
232
+ // Printing must disambiguate the two terms above, and must agree
233
+ // that ("abc"||1) op 2 (explicit outer parentheses) is the same
234
+ // term as the unparenthesized case, exercising the real top-level
235
+ // answer-printing path (not just the parser).
236
+ const source = ':- op(1, xfy, op).\n';
237
+ const stdout = run(source, {
238
+ goals: [
239
+ '("abc"||1 op 2) == (("abc"||1) op 2)',
240
+ '("abc"||1 op 2) \\== ("abc"||(1 op 2))',
241
+ 'L1 = "abc"||1 op 2',
242
+ 'L3 = "abc"||(1 op 2)',
243
+ ],
244
+ }).stdout;
245
+ assertEqual(
246
+ stdout,
247
+ [
248
+ '"abc"||1 op 2 == "abc"||1 op 2.',
249
+ '"abc"||1 op 2 \\== "abc"||(1 op 2).',
250
+ '"abc"||1 op 2 = "abc"||1 op 2.',
251
+ '"abc"||(1 op 2) = "abc"||(1 op 2).',
252
+ '',
253
+ ].join('\n'),
254
+ 'unabsorbed and parenthesized-tail terms are distinct and print disambiguated from each other',
255
+ );
256
+ },
257
+ },
204
258
  {
205
259
  name: 'normal integer syntax accepts WG17 digit separators (issue #89)',
206
260
  run: () => {