eyeprolog 1.1.24 → 1.1.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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.24",
6
+ "version": "1.1.26",
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
@@ -1164,9 +1164,9 @@ function* readTermBuiltin({ solver, goal, env }) {
1164
1164
  yield next;
1165
1165
  }
1166
1166
  function defaultTermWriteOptions(mode) {
1167
- if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map() };
1168
- if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map() };
1169
- return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map() };
1167
+ if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1168
+ if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1169
+ return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1170
1170
  }
1171
1171
 
1172
1172
  function writeOptionBoolean(value, env, option) {
package/src/quads.js CHANGED
@@ -106,6 +106,11 @@ function checkAlternative(program, quad, alternative, options) {
106
106
  if (leaf.more && !leaf.hasExpectation) return { ok: true };
107
107
  const matches = matchLeaf(quad.query, leaf, actual, position);
108
108
  if (leaf.unexpected ? matches : !matches) return { ok: false };
109
+ // An unexpected error description is a negative assertion about that
110
+ // particular error pattern. A different exception may be present and is
111
+ // checked by other descriptions; do not make the final 'no error' test
112
+ // turn a successful negative match back into a failure.
113
+ if (leaf.unexpected && leaf.error != null) return { ok: true };
109
114
  if (leaf.more) return { ok: true };
110
115
  if (!leaf.unexpected && (leaf.false || leaf.error != null)) {
111
116
  return { ok: position === leaves.length - 1 };
@@ -252,7 +257,7 @@ function matchLeaf(query, leaf, actual, position) {
252
257
  }
253
258
  if (leaf.error != null) {
254
259
  return position === actual.solutions.length && actual.error != null &&
255
- errorMatches(leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
260
+ errorMatches(query, leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
256
261
  }
257
262
  const solution = actual.solutions[position];
258
263
  if (!solution || !outputMatches(leaf.output, solution.output)) return false;
@@ -297,12 +302,15 @@ function namedVariables(term) {
297
302
  return found;
298
303
  }
299
304
 
300
- function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map()) {
305
+ function patternVariant(
306
+ pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map(), fixedPatternNames = null,
307
+ ) {
301
308
  pattern = deref(pattern, patternEnv);
302
309
  actual = deref(actual, actualEnv);
303
310
  if (pattern.type === ATOM && pattern.name === '...') return true;
304
311
  if (pattern.type === VAR || actual.type === VAR) {
305
312
  if (pattern.type !== VAR || actual.type !== VAR) return false;
313
+ if (fixedPatternNames?.has(pattern.name)) return pattern.name === actual.name;
306
314
  const paired = pairs.get(pattern.name);
307
315
  const reversed = reverse.get(actual.name);
308
316
  if (paired != null || reversed != null) return paired === actual.name && reversed === pattern.name;
@@ -312,7 +320,9 @@ function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(
312
320
  }
313
321
  if (pattern.type !== actual.type || pattern.name !== actual.name || pattern.arity !== actual.arity) return false;
314
322
  for (let index = 0; index < pattern.arity; index++) {
315
- if (!patternVariant(pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse)) return false;
323
+ if (!patternVariant(
324
+ pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse, fixedPatternNames,
325
+ )) return false;
316
326
  }
317
327
  return true;
318
328
  }
@@ -334,16 +344,26 @@ function errorTerm(error) {
334
344
  return compound('error', [atom('system_error'), variable('$quad_context')]);
335
345
  }
336
346
 
337
- function errorMatches(expected, actual) {
347
+ function errorMatches(query, expected, actual) {
348
+ // Variables named in the query keep their identity in answer descriptions.
349
+ // Seed both directions so a query variable can match only that same query
350
+ // variable, while variables introduced by the description (for example _X)
351
+ // remain fresh pattern variables. This is especially important for throw/1:
352
+ // ISO requires the thrown ball to be a renamed copy, so throw(g(_X)) may
353
+ // describe throw(g(X)), but throw(g(X)) must not.
354
+ const queryNames = new Set(namedVariables(query).map((item) => item.name));
355
+ const matches = (pattern, term) => patternVariant(
356
+ pattern, new Env(), term, new Env(), new Map(), new Map(), queryNames);
357
+
338
358
  if (expected.type === COMPOUND && expected.name === 'throw' && expected.arity === 1 &&
339
359
  actual.type === COMPOUND && actual.name === '$quad_thrown' && actual.arity === 1) {
340
- return patternVariant(expected.args[0], new Env(), actual.args[0], new Env());
360
+ return matches(expected.args[0], actual.args[0]);
341
361
  }
342
362
  if (actual.type !== COMPOUND || actual.name !== 'error' || actual.arity !== 2) return false;
343
363
  if (expected.type === COMPOUND && expected.name === 'error' && expected.arity === 2) {
344
- return patternVariant(expected, new Env(), actual, new Env());
364
+ return matches(expected, actual);
345
365
  }
346
- return patternVariant(expected, new Env(), actual.args[0], new Env());
366
+ return matches(expected, actual.args[0]);
347
367
  }
348
368
 
349
369
  function isErrorDescription(term) {
package/src/write.js CHANGED
@@ -124,11 +124,18 @@ function chooseOperator(term, table) {
124
124
  return null;
125
125
  }
126
126
 
127
- function format(term, env, options, table, maxPriority = 1200) {
127
+ function format(term, env, options, table, maxPriority = 1200, context = 'term') {
128
128
  const resolved = deref(term, env);
129
129
  if (resolved.type === VAR) return options.variableNames.get(resolved.name) ?? writeVariable(resolved.name);
130
130
  if (resolved.type === STRING) return writeString(resolved.name);
131
- if (resolved.type === ATOM) return options.quoted ? writeAtom(resolved.name) : resolved.name;
131
+ if (resolved.type === ATOM) {
132
+ if (!options.quoted) return resolved.name;
133
+ // ISO 6.3.3.1 gives functional arguments and list elements a special
134
+ // `arg` production: an atom that is a current operator is valid there
135
+ // without quoting. Keep lexical exceptions such as `|` quoted.
136
+ if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
137
+ return writeAtom(resolved.name);
138
+ }
132
139
  if (resolved.type === NUMBER) return resolved.name;
133
140
 
134
141
  if (options.numbervars && resolved.type === COMPOUND && resolved.name === '$VAR' && resolved.arity === 1) {
@@ -146,9 +153,13 @@ function format(term, env, options, table, maxPriority = 1200) {
146
153
  let cursor = resolved;
147
154
  while (true) {
148
155
  cursor = deref(cursor, env);
149
- if (isEmptyList(cursor)) return `[${parts.join(', ')}]`;
150
- if (!isCons(cursor)) return `[${parts.join(', ')} | ${format(cursor, env, options, table, 999)}]`;
151
- parts.push(format(cursor.args[0], env, options, table, 999));
156
+ const separator = options.compact ? ',' : ', ';
157
+ if (isEmptyList(cursor)) return `[${parts.join(separator)}]`;
158
+ if (!isCons(cursor)) {
159
+ const tailSeparator = options.compact ? '|' : ' | ';
160
+ return `[${parts.join(separator)}${tailSeparator}${format(cursor, env, options, table, 999, 'argument')}]`;
161
+ }
162
+ parts.push(format(cursor.args[0], env, options, table, 999, 'argument'));
152
163
  cursor = cursor.args[1];
153
164
  }
154
165
  }
@@ -183,8 +194,8 @@ function format(term, env, options, table, maxPriority = 1200) {
183
194
  }
184
195
 
185
196
  const name = options.quoted ? writeAtom(resolved.name) : resolved.name;
186
- const args = resolved.args.map((arg) => format(arg, env, options, table, 999));
187
- return `${name}(${args.join(', ')})`;
197
+ const args = resolved.args.map((arg) => format(arg, env, options, table, 999, 'argument'));
198
+ return `${name}(${args.join(options.compact ? ',' : ', ')})`;
188
199
  }
189
200
 
190
201
  export function formatTermForWrite(term, env = new Env(), options = {}) {
@@ -194,6 +205,8 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
194
205
  numbervars: options.numbervars !== false,
195
206
  doubleQuotes: options.doubleQuotes,
196
207
  variableNames: options.variableNames instanceof Map ? options.variableNames : new Map(),
208
+ compact: options.compact === true,
209
+ operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
197
210
  };
198
211
  return format(term, env, normalized, operatorTable(options.operators), 1200);
199
212
  }
@@ -1,2 +1,2 @@
1
- pair(First, Second)
1
+ pair(First,Second)
2
2
  corrigendum3_write_variable_names.
@@ -243,6 +243,24 @@ why(
243
243
  assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
244
244
  },
245
245
  },
246
+ {
247
+ name: 'runQuads distinguishes query variables from renamed throw variables',
248
+ run: () => {
249
+ const source = `?- throw(g(X)).\n` +
250
+ ` throw(g(_X)).\n` +
251
+ ` throw(g(X)), unexpected.\n`;
252
+ const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'throw-copy-quad.pl' }]));
253
+ assertEqual(result.total, 1, 'quad total');
254
+ assertEqual(result.passed, 1, 'quad passed');
255
+ assertEqual(result.failed, 0, 'quad failed');
256
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
257
+
258
+ const forbiddenFresh = publicApi.runQuads(
259
+ `?- throw(g(X)).\n throw(g(_X)), unexpected.\n`,
260
+ );
261
+ assertEqual(forbiddenFresh.failed, 1, 'fresh thrown variable is detected');
262
+ },
263
+ },
246
264
  {
247
265
  name: 'runQuads matches the corrected ISO phrase quad boundaries',
248
266
  run: () => {
@@ -950,6 +968,23 @@ c4 ?- call((!;1)).
950
968
  assertEqual(run(source, { goal: 'operator_argument(ok)' }).stdout, 'operator_argument(ok).\n', 'operator argument syntax');
951
969
  },
952
970
  },
971
+ {
972
+ name: 'ISO writeq preserves operator atoms in argument syntax',
973
+ run: () => {
974
+ const source = [
975
+ 'emit_operator_arguments :-',
976
+ " writeq([:-,-]), put_char('|'),",
977
+ " writeq(f(*)), put_char('|'),",
978
+ " writeq(f(;,'|',';;')).",
979
+ '',
980
+ ].join('\n');
981
+ assertEqual(
982
+ run(source, { goal: 'emit_operator_arguments' }).stdout,
983
+ "[:-,-]|f(*)|f(;,'|',';;')emit_operator_arguments.\n",
984
+ 'operator argument output',
985
+ );
986
+ },
987
+ },
953
988
  {
954
989
  name: 'ISO query operator and quad infix extension are visible through current_op/3',
955
990
  run: () => {
@@ -1020,7 +1055,7 @@ c4 ?- call((!;1)).
1020
1055
  ].join('\n');
1021
1056
  assertEqual(
1022
1057
  run(source, { goal: 'emit' }).stdout,
1023
- "hello world|'hello world'|a + b * c|'+'(a, *(b, c))|hello world|'hello world'|+(a, b)|a + b|A|$VAR(0)|pair(Left, Right)emit.\n",
1058
+ "hello world|'hello world'|a + b * c|'+'(a,*(b,c))|hello world|'hello world'|+(a,b)|a + b|A|$VAR(0)|pair(Left,Right)emit.\n",
1024
1059
  'stdout',
1025
1060
  );
1026
1061
  },
@@ -5131,7 +5131,12 @@ parsing of subsequent text, place them before their first use. ISO argument
5131
5131
  syntax also permits an atom that is currently an operator to appear directly
5132
5132
  as a functional argument or list element, so forms such as
5133
5133
  `current_op(Priority, Specifier, :-)` and `[:-,-]` are valid without quoting
5134
- or parenthesizing those operator atoms. The ISO initial operator table also
5134
+ or parenthesizing those operator atoms. Term output observes the same `arg`
5135
+ rule: with `quoted(true)`, an operator atom is not quoted merely because it is
5136
+ an operator when it occurs as a functional argument or list element. Thus
5137
+ `writeq([:-,-])` emits `[:-,-]`, and `writeq(f(;,'|',';;'))` emits
5138
+ `f(;,'|',';;')`; the bar stays quoted because ISO treats the unquoted `|`
5139
+ token as a list separator rather than an atom. The ISO initial operator table also
5135
5140
  contains `?-` at priority 1200 with specifier `fx`, so
5136
5141
  `current_op(1200, fx, ?-)` succeeds. EyeProlog's embedded quad syntax permits
5137
5142
  an optional label before the query marker (`Label ?- Query.`), so while quad
@@ -5171,7 +5176,10 @@ write_event(Path, Event) :-
5171
5176
 
5172
5177
  The period is essential when another Prolog processor will read the result as
5173
5178
  a term. `write/1-2` uses readable conventional syntax, `writeq/1-2` quotes
5174
- where needed, and `write_canonical/1-2` exposes canonical structure.
5179
+ where needed, and `write_canonical/1-2` exposes canonical structure. ISO term
5180
+ output uses only the separator characters needed by the syntax, so functional
5181
+ arguments and list elements are emitted compactly; for example `writeq([a,b])`
5182
+ outputs `[a,b]`.
5175
5183
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5176
5184
  `variable_names/1`.
5177
5185
 
@@ -6292,7 +6300,11 @@ accepted as a negative answer.
6292
6300
  Answer descriptions support ordered answers separated by `;`, acceptable
6293
6301
  alternatives separated by `|`, `true`, `false`, standard error descriptions,
6294
6302
  and the `unexpected` annotation for an answer that must not occur (`inattendue`
6295
- is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
6303
+ is its synonym). Variables named in the query keep their identity inside answer
6304
+ descriptions; variables introduced only by a description are fresh. For example,
6305
+ a query `throw(g(X))` is described by `throw(g(_X))`, while
6306
+ `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6307
+ variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
6296
6308
  indented descriptions after one query must all hold. `inputs/1` supplies and
6297
6309
  checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
6298
6310
  an answer description that this finite-tree implementation skips. `loops` is