eyeprolog 1.3.44 → 1.3.46

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/iso.js CHANGED
@@ -14,6 +14,7 @@ import { formatTermForWrite } from './write.js';
14
14
  import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
15
15
  import { INVALID_UTF8_SENTINEL } from './io.js';
16
16
  import { CharacterRepresentationError, isStrictIsoPcsCharacter, isStrictIsoPcsCodePoint } from './iso-character.js';
17
+ import { ISO_MAX_ARITY } from './iso-limits.js';
17
18
  import {
18
19
  characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
19
20
  } from './syntax-scan.js';
@@ -96,7 +97,7 @@ export const isoBuiltins = {
96
97
  registry.add('current_output', 1, currentOutputBuiltin, { deterministic: true });
97
98
  registry.add('set_input', 1, setCurrentStreamBuiltin('read'), { deterministic: true });
98
99
  registry.add('set_output', 1, setCurrentStreamBuiltin('write'), { deterministic: true });
99
- registry.add('flush_output', 0, succeed, { deterministic: true });
100
+ registry.add('flush_output', 0, flushOutputBuiltin, { deterministic: true });
100
101
  registry.add('flush_output', 1, flushOutputBuiltin, { deterministic: true });
101
102
  registry.add('stream_property', 2, streamPropertyBuiltin);
102
103
  registry.add('set_stream_position', 2, setStreamPositionBuiltin, { deterministic: true });
@@ -346,10 +347,18 @@ function* keysortBuiltin({ goal, env }) {
346
347
  validateListOutput(goal.args[1], env);
347
348
  for (const item of items) {
348
349
  const resolved = deref(item, env);
350
+ if (resolved.type === VAR) throw new PrologError('instantiation_error');
349
351
  if (resolved.type !== COMPOUND || resolved.name !== '-' || resolved.arity !== 2) {
350
352
  throw new PrologError('type_error(pair)', resolved);
351
353
  }
352
354
  }
355
+ const { items: outputPrefix } = listElements(goal.args[1], env);
356
+ for (const item of outputPrefix) {
357
+ if (item.type === VAR) continue;
358
+ if (item.type !== COMPOUND || item.name !== '-' || item.arity !== 2) {
359
+ throw new PrologError('type_error(pair)', item);
360
+ }
361
+ }
353
362
  // Modern ECMAScript specifies a stable Array#sort, as required by keysort/2.
354
363
  // Keep one implementation-dependent variable order for this whole sorting
355
364
  // operation, as required by ISO 7.2.1.
@@ -381,18 +390,26 @@ function* functorBuiltin({ goal, env }) {
381
390
  if (unify(goal.args[1], name, next) && unify(goal.args[2], numberTerm(term.arity), next)) yield next;
382
391
  return;
383
392
  }
393
+
384
394
  const name = deref(goal.args[1], env);
385
- const arity = requireInteger(goal.args[2], env);
386
- if (arity < 0n) throw new PrologError('domain_error(not_less_than_zero)', deref(goal.args[2], env));
387
- if (arity > BigInt(Number.MAX_SAFE_INTEGER)) throw new PrologError('representation_error(max_arity)');
395
+ const arityTerm = deref(goal.args[2], env);
396
+ // ISO 8.5.1.3 orders construction-mode diagnostics. A compound Name is an
397
+ // atomic type error (before an Arity type error); only an atomic non-atom
398
+ // used for positive arity receives type_error(atom,...).
399
+ if (name.type === VAR) throw new PrologError('instantiation_error');
400
+ if (arityTerm.type === VAR) throw new PrologError('instantiation_error');
401
+ if (name.type === COMPOUND) throw new PrologError('type_error(atomic)', name);
402
+ if (arityTerm.type !== NUMBER || !isDecimalInteger(arityTerm.name)) {
403
+ throw new PrologError('type_error(integer)', arityTerm);
404
+ }
405
+ const arity = BigInt(arityTerm.name);
406
+ if (arity > 0n && name.type !== ATOM) throw new PrologError('type_error(atom)', name);
407
+ if (arity > BigInt(ISO_MAX_ARITY)) throw new PrologError('representation_error(max_arity)');
408
+ if (arity < 0n) throw new PrologError('domain_error(not_less_than_zero)', arityTerm);
388
409
  if (arity === 0n) {
389
- if (name.type === VAR) throw new PrologError('instantiation_error');
390
- if (name.type === COMPOUND) throw new PrologError('type_error(atomic)', name);
391
410
  if (unify(goal.args[0], name, next)) yield next;
392
411
  return;
393
412
  }
394
- if (name.type === VAR) throw new PrologError('instantiation_error');
395
- if (name.type !== ATOM) throw new PrologError('type_error(atom)', name);
396
413
  const id = ++isoFresh;
397
414
  if (unify(goal.args[0], compound(name.name, Array.from({ length: Number(arity) }, (_, i) => variable(`__functor${id}_${i}`))), next)) yield next;
398
415
  }
@@ -412,6 +429,11 @@ function* univBuiltin({ goal, env }) {
412
429
  const term = deref(goal.args[0], env);
413
430
  const next = env.clone();
414
431
  if (term.type !== VAR) {
432
+ // 8.5.3.3(b) also applies in decomposition mode: a fixed second argument
433
+ // that is neither a list nor a partial list is an error, not failure.
434
+ if (listKind(goal.args[1], env) === 'nonlist') {
435
+ throw new PrologError('type_error(list)', deref(goal.args[1], env));
436
+ }
415
437
  const items = term.type === COMPOUND ? [atom(term.name), ...term.args] : [term];
416
438
  if (unify(goal.args[1], listFromItems(items), next)) yield next;
417
439
  return;
@@ -430,6 +452,7 @@ function* univBuiltin({ goal, env }) {
430
452
  return;
431
453
  }
432
454
  const name = requireAtom(items[0], env);
455
+ if (items.length - 1 > ISO_MAX_ARITY) throw new PrologError('representation_error(max_arity)');
433
456
  if (unify(goal.args[0], compound(name.name, items.slice(1)), next)) yield next;
434
457
  }
435
458
 
@@ -511,20 +534,15 @@ function* clauseBuiltin({ solver, goal, env }) {
511
534
  const head = deref(goal.args[0], env);
512
535
  if (head.type === VAR) throw new PrologError('instantiation_error');
513
536
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
514
- callableOrVariable(goal.args[1], env);
515
537
  const indicator = compound('/', [atom(head.name), numberTerm(head.arity)]);
516
- if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(solver, head)) {
517
- throw new PrologError('permission_error(access, private_procedure)', indicator);
518
- }
519
538
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
520
- if (!group) return;
521
- // ISO 7.5.3 makes dynamic procedures public and static user-defined
522
- // procedures private by default. EyeProlog's normal profile keeps static
523
- // clauses inspectable for proof tooling; strict core mode restores the ISO
524
- // access rule used by clause/2.
525
- if (solver.isoStrict && !group.dynamic) {
539
+ // ISO 8.8.1.3 places private-procedure access before Body callability.
540
+ if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(solver, head) ||
541
+ (solver.isoStrict && group && !group.dynamic)) {
526
542
  throw new PrologError('permission_error(access, private_procedure)', indicator);
527
543
  }
544
+ callableOrVariable(goal.args[1], env);
545
+ if (!group) return;
528
546
  for (const clause of group.clauses) {
529
547
  const pair = compound('$clause', [clause.head, clauseBodyTerm(clause.body)]);
530
548
  const copied = freshCopy(pair, new Env());
@@ -641,7 +659,7 @@ function predicateIndicatorParts(term, env) {
641
659
  if (name.type !== ATOM) throw new PrologError('type_error(atom)', name);
642
660
  const integer = BigInt(arity.name);
643
661
  if (integer < 0n) throw new PrologError('domain_error(not_less_than_zero)', arity);
644
- if (integer > BigInt(Number.MAX_SAFE_INTEGER)) throw new PrologError('representation_error(max_arity)');
662
+ if (integer > BigInt(ISO_MAX_ARITY)) throw new PrologError('representation_error(max_arity)');
645
663
  return { name: name.name, arity: Number(integer), indicator };
646
664
  }
647
665
 
@@ -680,7 +698,8 @@ function* setPrologFlagBuiltin({ solver, goal, env }) {
680
698
  if (flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
681
699
  const definition = solver.prologFlags.get(flag.name);
682
700
  if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
683
- if (value.type !== ATOM || !definition.allowed.includes(value.name)) {
701
+ const expectedType = definition.valueType ?? ATOM;
702
+ if (value.type !== expectedType || !definition.allowed.includes(value.name)) {
684
703
  throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
685
704
  }
686
705
  if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
@@ -830,6 +849,16 @@ function streamReference(term, env) {
830
849
  throw new PrologError('domain_error(stream_or_alias)', value);
831
850
  }
832
851
 
852
+ function streamTermReference(term, env) {
853
+ const value = deref(term, env);
854
+ if (value.type === VAR) return null;
855
+ if (value.type === COMPOUND && value.name === '$stream' && value.arity === 1 &&
856
+ value.args[0].type === NUMBER && isDecimalInteger(value.args[0].name)) {
857
+ return Number(value.args[0].name);
858
+ }
859
+ throw new PrologError('domain_error(stream)', value);
860
+ }
861
+
833
862
  function requireStream(solver, term, env, mode = null) {
834
863
  const culprit = deref(term, env);
835
864
  const stream = solver.io.resolve(streamReference(term, env));
@@ -886,13 +915,14 @@ function openOptions(term, env) {
886
915
  if ((value = optionAtom(option, 'type'))) {
887
916
  value = deref(value, env);
888
917
  if (value.type === VAR) throw new PrologError('instantiation_error');
889
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
890
- if (!['text', 'binary'].includes(value.name)) throw new PrologError('domain_error(stream_option)', option);
918
+ if (value.type !== ATOM || !['text', 'binary'].includes(value.name)) {
919
+ throw new PrologError('domain_error(stream_option)', option);
920
+ }
891
921
  result.type = value.name;
892
922
  } else if ((value = optionAtom(option, 'alias'))) {
893
923
  value = deref(value, env);
894
924
  if (value.type === VAR) throw new PrologError('instantiation_error');
895
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
925
+ if (value.type !== ATOM) throw new PrologError('domain_error(stream_option)', option);
896
926
  result.alias = value.name;
897
927
  } else if ((value = optionAtom(option, 'reposition'))) {
898
928
  value = deref(value, env);
@@ -911,19 +941,45 @@ function openOptions(term, env) {
911
941
  return result;
912
942
  }
913
943
 
944
+ function streamOptionHasRequiredVariable(option, env) {
945
+ option = deref(option, env);
946
+ if (option.type !== COMPOUND || option.arity !== 1 ||
947
+ !['type', 'alias', 'reposition', 'eof_action'].includes(option.name)) return false;
948
+ return deref(option.args[0], env).type === VAR;
949
+ }
950
+
914
951
  function* openBuiltin({ solver, goal, env }) {
915
- const path = requireAtom(goal.args[0], env);
916
- const mode = requireAtom(goal.args[1], env);
917
- if (!['read', 'write', 'append'].includes(mode.name)) throw new PrologError('domain_error(io_mode)', mode);
952
+ const source = deref(goal.args[0], env);
953
+ const mode = deref(goal.args[1], env);
918
954
  const streamTarget = deref(goal.args[2], env);
955
+ const optionTerm = goal.arity === 3 ? emptyList() : goal.args[3];
956
+
957
+ // ISO 8.11.5.3 + Corrigenda 2/3. Keep each prescribed condition
958
+ // observable on its own and apply the published ordering for deterministic
959
+ // diagnostics when several arguments are erroneous.
960
+ if (source.type === VAR) throw new PrologError('instantiation_error');
961
+ if (mode.type === VAR) throw new PrologError('instantiation_error');
962
+ const preflight = preflightOptionList(optionTerm, env);
963
+ if (preflight.items.some((option) => streamOptionHasRequiredVariable(option, env))) {
964
+ throw new PrologError('instantiation_error');
965
+ }
966
+ if (mode.type !== ATOM) throw new PrologError('type_error(atom)', mode);
967
+ const optionItems = requireProperOptionList(preflight);
919
968
  if (streamTarget.type !== VAR) throw new PrologError('uninstantiation_error', streamTarget);
920
- const options = goal.arity === 3 ? {} : openOptions(goal.args[3], env);
921
- if (options.alias && solver.io.resolve(options.alias)) throw new PrologError('permission_error(open, source_sink)', atom(options.alias));
969
+ if (source.type !== ATOM) throw new PrologError('domain_error(source_sink)', source);
970
+ if (!['read', 'write', 'append'].includes(mode.name)) throw new PrologError('domain_error(io_mode)', mode);
971
+ const options = goal.arity === 3 ? {} : openOptions(optionTerm, env);
972
+ if (options.alias && solver.io.resolve(options.alias)) {
973
+ throw new PrologError('permission_error(open, source_sink)', atom(options.alias));
974
+ }
922
975
  let stream;
923
976
  try {
924
- stream = solver.io.open(path.name, mode.name, options);
925
- } catch (_) {
926
- throw new PrologError('existence_error(source_sink)', path);
977
+ stream = solver.io.open(source.name, mode.name, options);
978
+ } catch (error) {
979
+ if (error?.code === 'ENOENT' && mode.name === 'read') {
980
+ throw new PrologError('existence_error(source_sink)', source);
981
+ }
982
+ throw new PrologError('permission_error(open, source_sink)', source);
927
983
  }
928
984
  const next = env.clone();
929
985
  if (unify(goal.args[2], streamHandle(stream.id), next)) yield next;
@@ -931,22 +987,35 @@ function* openBuiltin({ solver, goal, env }) {
931
987
  }
932
988
 
933
989
  function* closeBuiltin({ solver, goal, env }) {
934
- if (goal.arity === 2) {
935
- for (const option of optionList(goal.args[1], env)) {
936
- if (option.type === VAR) throw new PrologError('instantiation_error');
937
- const force = optionAtom(option, 'force');
938
- const value = force && deref(force, env);
939
- if (value?.type === VAR) throw new PrologError('instantiation_error');
940
- if (!value || value.type !== ATOM || !['true', 'false'].includes(value.name)) {
941
- throw new PrologError('domain_error(close_option)', option);
942
- }
990
+ const reference = deref(goal.args[0], env);
991
+ if (reference.type === VAR) throw new PrologError('instantiation_error');
992
+ const optionTerm = goal.arity === 2 ? goal.args[1] : emptyList();
993
+ const preflight = preflightOptionList(optionTerm, env);
994
+ if (preflight.items.some((option) => {
995
+ const force = optionAtom(option, 'force');
996
+ return force != null && deref(force, env).type === VAR;
997
+ })) throw new PrologError('instantiation_error');
998
+ const options = requireProperOptionList(preflight);
999
+ streamReference(goal.args[0], env);
1000
+ let forceClose = false;
1001
+ for (const option of options) {
1002
+ const force = optionAtom(option, 'force');
1003
+ const value = force && deref(force, env);
1004
+ if (!value || value.type !== ATOM || !['true', 'false'].includes(value.name)) {
1005
+ throw new PrologError('domain_error(close_option)', option);
943
1006
  }
1007
+ forceClose = value.name === 'true'; // rightmost contradictory option applies
944
1008
  }
945
1009
  const stream = requireStream(solver, goal.args[0], env);
946
1010
  if (!stream.standard) {
1011
+ try {
1012
+ solver.io.close(stream);
1013
+ } catch (_) {
1014
+ if (!forceClose) throw new PrologError('system_error');
1015
+ solver.io.discard(stream);
1016
+ }
947
1017
  if (solver.io.currentInput === stream.id) solver.io.currentInput = 0;
948
1018
  if (solver.io.currentOutput === stream.id) solver.io.currentOutput = 1;
949
- solver.io.close(stream);
950
1019
  }
951
1020
  yield env;
952
1021
  }
@@ -954,7 +1023,8 @@ function* closeBuiltin({ solver, goal, env }) {
954
1023
  function* currentInputBuiltin({ solver, goal, env }) {
955
1024
  const value = deref(goal.args[0], env);
956
1025
  if (value.type !== VAR) {
957
- const stream = solver.io.resolve(streamReference(goal.args[0], env));
1026
+ const id = streamTermReference(goal.args[0], env);
1027
+ const stream = solver.io.resolve(id);
958
1028
  if (!stream) throw new PrologError('domain_error(stream)', value);
959
1029
  if (stream.id === solver.io.currentInput) yield env;
960
1030
  return;
@@ -965,7 +1035,8 @@ function* currentInputBuiltin({ solver, goal, env }) {
965
1035
  function* currentOutputBuiltin({ solver, goal, env }) {
966
1036
  const value = deref(goal.args[0], env);
967
1037
  if (value.type !== VAR) {
968
- const stream = solver.io.resolve(streamReference(goal.args[0], env));
1038
+ const id = streamTermReference(goal.args[0], env);
1039
+ const stream = solver.io.resolve(id);
969
1040
  if (!stream) throw new PrologError('domain_error(stream)', value);
970
1041
  if (stream.id === solver.io.currentOutput) yield env;
971
1042
  return;
@@ -973,6 +1044,7 @@ function* currentOutputBuiltin({ solver, goal, env }) {
973
1044
  const next = env.clone();
974
1045
  if (unify(goal.args[0], streamHandle(solver.io.currentOutput), next)) yield next;
975
1046
  }
1047
+
976
1048
  function setCurrentStreamBuiltin(mode) {
977
1049
  return function* ({ solver, goal, env }) {
978
1050
  const stream = requireStream(solver, goal.args[0], env, mode);
@@ -982,7 +1054,14 @@ function setCurrentStreamBuiltin(mode) {
982
1054
  };
983
1055
  }
984
1056
  function* flushOutputBuiltin({ solver, goal, env }) {
985
- requireStream(solver, goal.args[0], env, 'write');
1057
+ const stream = goal.arity === 0
1058
+ ? solver.io.resolve(solver.io.currentOutput)
1059
+ : requireStream(solver, goal.args[0], env, 'write');
1060
+ try {
1061
+ solver.io.flush(stream);
1062
+ } catch (_) {
1063
+ throw new PrologError('system_error');
1064
+ }
986
1065
  yield env;
987
1066
  }
988
1067
 
@@ -1003,23 +1082,51 @@ function streamProperties(stream) {
1003
1082
  return properties;
1004
1083
  }
1005
1084
  function* setStreamPositionBuiltin({ solver, goal, env }) {
1006
- const stream = requireStream(solver, goal.args[0], env);
1007
- if (!stream.reposition) throw new PrologError('permission_error(reposition, stream)', deref(goal.args[0], env));
1085
+ const reference = deref(goal.args[0], env);
1086
+ if (reference.type === VAR) throw new PrologError('instantiation_error');
1008
1087
  let position = deref(goal.args[1], env);
1009
- if (position.type === COMPOUND && position.name === 'position' && position.arity === 1) {
1088
+ if (position.type === VAR) throw new PrologError('instantiation_error');
1089
+ streamReference(goal.args[0], env);
1090
+ if (!solver.isoStrict && position.type === COMPOUND && position.name === 'position' && position.arity === 1) {
1091
+ // Historical EyeProlog convenience form. Strict Part 1 mode uses only
1092
+ // the implementation-defined integer stream-position terms returned by
1093
+ // stream_property/2.
1010
1094
  position = deref(position.args[0], env);
1011
1095
  }
1012
- if (position.type === VAR) throw new PrologError('instantiation_error');
1013
- if (position.type !== NUMBER || !isDecimalInteger(position.name)) throw new PrologError('domain_error(stream_position)', position);
1096
+ if (position.type === VAR || position.type !== NUMBER || !isDecimalInteger(position.name)) {
1097
+ throw new PrologError('domain_error(stream_position)', position);
1098
+ }
1014
1099
  const offset = BigInt(position.name);
1015
- if (offset < 0n || offset > BigInt(stream.content.length)) throw new PrologError('domain_error(stream_position)', position);
1100
+ if (offset < 0n) throw new PrologError('domain_error(stream_position)', position);
1101
+ const stream = requireStream(solver, goal.args[0], env);
1102
+ if (!stream.reposition) throw new PrologError('permission_error(reposition, stream)', reference);
1103
+ if (offset > BigInt(stream.content.length)) throw new PrologError('domain_error(stream_position)', position);
1016
1104
  stream.position = Number(offset);
1017
1105
  stream.pastEnd = false;
1018
1106
  yield env;
1019
1107
  }
1108
+
1109
+ function isStreamPropertyPattern(value) {
1110
+ if (value.type === VAR) return true;
1111
+ if (value.type === ATOM) return value.name === 'input' || value.name === 'output';
1112
+ return value.type === COMPOUND && value.arity === 1 && [
1113
+ 'file_name', 'mode', 'alias', 'position', 'end_of_stream', 'eof_action', 'reposition', 'type',
1114
+ ].includes(value.name);
1115
+ }
1116
+
1020
1117
  function* streamPropertyBuiltin({ solver, goal, env }) {
1021
1118
  const reference = deref(goal.args[0], env);
1022
- const streams = reference.type === VAR ? [...solver.io.streams.values()] : [requireStream(solver, goal.args[0], env)];
1119
+ const propertyPattern = deref(goal.args[1], env);
1120
+ if (reference.type !== VAR) {
1121
+ const id = streamTermReference(goal.args[0], env);
1122
+ if (!solver.io.resolve(id)) throw new PrologError('domain_error(stream)', reference);
1123
+ }
1124
+ if (!isStreamPropertyPattern(propertyPattern)) {
1125
+ throw new PrologError('domain_error(stream_property)', propertyPattern);
1126
+ }
1127
+ const streams = reference.type === VAR
1128
+ ? [...solver.io.streams.values()]
1129
+ : [solver.io.resolve(streamTermReference(goal.args[0], env))];
1023
1130
  for (const stream of streams) {
1024
1131
  for (const property of streamProperties(stream)) {
1025
1132
  const next = env.clone();
@@ -1035,27 +1142,69 @@ function outputStreamFor(solver, goal, env) {
1035
1142
  return goal.arity === 1 ? solver.io.resolve(solver.io.currentOutput) : requireStream(solver, goal.args[0], env, 'write');
1036
1143
  }
1037
1144
  function* atEndBuiltin({ solver, goal, env }) {
1038
- const stream = goal.arity === 0 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
1039
- if (stream.position >= stream.content.length) yield env;
1145
+ // at_end_of_stream/1 is bootstrapped in Part 1 from the end_of_stream/1
1146
+ // stream property and therefore accepts any open stream-or-alias; it is not
1147
+ // an input-only operation.
1148
+ const stream = goal.arity === 0 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env);
1149
+ if (stream.pastEnd || stream.position >= stream.content.length) yield env;
1150
+ }
1151
+ function validStrictInputCharacterCode(value, solver) {
1152
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) return false;
1153
+ const code = BigInt(value.name);
1154
+ if (code === -1n) return true;
1155
+ if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return false;
1156
+ return !solver.isoStrict || isStrictIsoPcsCodePoint(Number(code));
1157
+ }
1158
+
1159
+ function preflightInputUnitTarget(name, target, env, solver) {
1160
+ const value = deref(target, env);
1161
+ if (name.endsWith('char')) {
1162
+ if (value.type !== VAR && !(oneChar(value) || (value.type === ATOM && value.name === 'end_of_file'))) {
1163
+ throw new PrologError('type_error(in_character)', value);
1164
+ }
1165
+ if (value.type === ATOM && oneChar(value) && solver.isoStrict && !isStrictIsoPcsCharacter(value.name)) {
1166
+ throw new PrologError('type_error(in_character)', value);
1167
+ }
1168
+ } else if (name.endsWith('code')) {
1169
+ if (value.type !== VAR && (value.type !== NUMBER || !isDecimalInteger(value.name))) {
1170
+ throw new PrologError('type_error(integer)', value);
1171
+ }
1172
+ if (value.type !== VAR && !validStrictInputCharacterCode(value, solver)) {
1173
+ throw new PrologError('representation_error(in_character_code)');
1174
+ }
1175
+ } else {
1176
+ if (value.type !== VAR) {
1177
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(in_byte)', value);
1178
+ const byte = BigInt(value.name);
1179
+ if (byte !== -1n && (byte < 0n || byte > 255n)) throw new PrologError('type_error(in_byte)', value);
1180
+ }
1181
+ }
1182
+ return value;
1040
1183
  }
1184
+
1041
1185
  function inputUnitBuiltin(name) {
1042
1186
  return function* ({ solver, goal, env }) {
1187
+ const explicit = goal.arity === 2;
1188
+ if (explicit && deref(goal.args[0], env).type === VAR) throw new PrologError('instantiation_error');
1189
+ const target = goal.args[goal.arity - 1];
1190
+ preflightInputUnitTarget(name, target, env, solver);
1043
1191
  const stream = inputStreamFor(solver, goal, env);
1044
1192
  const binary = name.endsWith('byte');
1045
- if (binary !== (stream.type === 'binary')) throw new PrologError('permission_error(input, stream)', streamHandle(stream.id));
1193
+ if (binary && stream.type !== 'binary') {
1194
+ throw new PrologError('permission_error(input, text_stream)', streamHandle(stream.id));
1195
+ }
1196
+ if (!binary && stream.type !== 'text') {
1197
+ throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1198
+ }
1046
1199
  if (stream.pastEnd && stream.eofAction === 'error') {
1047
1200
  throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
1048
1201
  }
1049
1202
  if (stream.pastEnd && stream.eofAction === 'reset') {
1050
- // A terminal EOF (Ctrl-D) is local to one input operation. The next
1051
- // operation should wait for fresh terminal input rather than rewinding
1052
- // and replaying the already-consumed interactive buffer.
1053
1203
  if (typeof stream.interactiveReadUnit !== 'function') stream.position = 0;
1054
1204
  stream.pastEnd = false;
1055
1205
  }
1056
1206
  const peek = name.startsWith('peek');
1057
- if (stream.position >= stream.content.length &&
1058
- typeof stream.interactiveReadUnit === 'function') {
1207
+ if (stream.position >= stream.content.length && typeof stream.interactiveReadUnit === 'function') {
1059
1208
  const text = stream.interactiveReadUnit();
1060
1209
  if (text != null) {
1061
1210
  stream.content += String(text);
@@ -1066,50 +1215,68 @@ function inputUnitBuiltin(name) {
1066
1215
  try {
1067
1216
  unit = solver.io.readUnit(stream, peek);
1068
1217
  } catch (error) {
1069
- if (error?.name === 'InvalidCharacterEncodingError') {
1070
- throw new PrologError('representation_error(character)');
1071
- }
1218
+ if (error?.name === 'InvalidCharacterEncodingError') throw new PrologError('representation_error(character)');
1072
1219
  throw error;
1073
1220
  }
1074
1221
  if (unit == null && !peek) stream.pastEnd = true;
1075
1222
  if (!binary && unit != null && solver.isoStrict && !isStrictIsoPcsCharacter(unit)) {
1076
1223
  throw new PrologError('representation_error(character)');
1077
1224
  }
1078
- const result = unit == null ? (binary ? numberTerm(-1) : name.endsWith('code') ? numberTerm(-1) : atom('end_of_file'))
1225
+ const result = unit == null
1226
+ ? (binary ? numberTerm(-1) : name.endsWith('code') ? numberTerm(-1) : atom('end_of_file'))
1079
1227
  : binary ? numberTerm(unit) : name.endsWith('code') ? numberTerm(unit.codePointAt(0)) : atom(unit);
1080
- const target = goal.args[goal.arity - 1];
1081
1228
  const next = env.clone();
1082
1229
  if (unify(target, result, next)) yield next;
1083
1230
  };
1084
1231
  }
1232
+
1233
+ function preflightOutputUnitValue(name, target, env) {
1234
+ const value = deref(target, env);
1235
+ if (value.type === VAR) throw new PrologError('instantiation_error');
1236
+ if (name === 'put_char') {
1237
+ if (!oneChar(value)) throw new PrologError('type_error(character)', value);
1238
+ return value;
1239
+ }
1240
+ if (name === 'put_byte') {
1241
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(byte)', value);
1242
+ const byte = BigInt(value.name);
1243
+ if (byte < 0n || byte > 255n) throw new PrologError('type_error(byte)', value);
1244
+ return value;
1245
+ }
1246
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
1247
+ return value;
1248
+ }
1249
+
1085
1250
  function outputUnitBuiltin(name) {
1086
1251
  return function* ({ solver, goal, env }) {
1252
+ const explicit = goal.arity === 2;
1253
+ if (explicit && deref(goal.args[0], env).type === VAR) throw new PrologError('instantiation_error');
1254
+ const value = preflightOutputUnitValue(name, goal.args[goal.arity - 1], env);
1087
1255
  const stream = outputStreamFor(solver, goal, env);
1088
- const value = deref(goal.args[goal.arity - 1], env);
1089
- if (value.type === VAR) throw new PrologError('instantiation_error');
1090
1256
  if (name === 'put_char') {
1091
1257
  if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1092
- if (!oneChar(value)) throw new PrologError('type_error(character)', value);
1093
1258
  if (solver.isoStrict && !isStrictIsoPcsCharacter(value.name)) {
1094
1259
  throw new PrologError('representation_error(character)', value);
1095
1260
  }
1096
1261
  solver.io.writeUnit(stream, value.name);
1262
+ } else if (name === 'put_byte') {
1263
+ if (stream.type !== 'binary') throw new PrologError('permission_error(output, text_stream)', streamHandle(stream.id));
1264
+ solver.io.writeUnit(stream, Number(value.name));
1097
1265
  } else {
1098
- if ((name === 'put_byte') !== (stream.type === 'binary')) {
1099
- throw new PrologError('permission_error(output, stream)', streamHandle(stream.id));
1100
- }
1101
- if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
1266
+ if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1102
1267
  const code = BigInt(value.name);
1103
- const max = name === 'put_byte' ? 255n : 0x10ffffn;
1104
- if (code < 0n || code > max) throw new PrologError(name === 'put_byte' ? 'type_error(byte)' : 'representation_error(character_code)');
1105
- if (name === 'put_code' && solver.isoStrict && !isStrictIsoPcsCodePoint(Number(code))) {
1268
+ if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) {
1106
1269
  throw new PrologError('representation_error(character_code)');
1107
1270
  }
1108
- solver.io.writeUnit(stream, name === 'put_byte' ? Number(code) : String.fromCodePoint(Number(code)));
1271
+ if (solver.isoStrict && !isStrictIsoPcsCodePoint(Number(code))) {
1272
+ throw new PrologError('representation_error(character_code)');
1273
+ }
1274
+ solver.io.writeUnit(stream, String.fromCodePoint(Number(code)));
1109
1275
  }
1110
1276
  yield env;
1111
1277
  };
1112
1278
  }
1279
+
1113
1280
  function* nlBuiltin({ solver, goal, env }) {
1114
1281
  const stream = goal.arity === 0
1115
1282
  ? solver.io.resolve(solver.io.currentOutput)
@@ -1274,6 +1441,13 @@ export function isCompleteReadTermText(text, solver) {
1274
1441
  }
1275
1442
 
1276
1443
  function readTermFromStream(stream, solver) {
1444
+ if (stream.pastEnd && stream.eofAction === 'error') {
1445
+ throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
1446
+ }
1447
+ if (stream.pastEnd && stream.eofAction === 'reset') {
1448
+ if (typeof stream.interactiveReadTerm !== 'function') stream.position = 0;
1449
+ stream.pastEnd = false;
1450
+ }
1277
1451
  let requestedInteractiveTerm = false;
1278
1452
  while (true) {
1279
1453
  let sawCandidate = false;
@@ -1317,6 +1491,7 @@ function readTermFromStream(stream, solver) {
1317
1491
  stream.position = source.length;
1318
1492
  if (!sawCandidate) {
1319
1493
  if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1494
+ stream.pastEnd = true;
1320
1495
  return { term: atom('end_of_file'), variables: [] };
1321
1496
  }
1322
1497
  throw new PrologError('syntax_error(read_term)');
@@ -1789,14 +1964,14 @@ function canonicalNumberText(value) {
1789
1964
  return text;
1790
1965
  }
1791
1966
 
1792
- function numberListText(list, env, kind, valueIsBound) {
1967
+ function numberListText(list, env, kind, valueIsBound, solver = null) {
1793
1968
  const whole = deref(list, env);
1794
1969
  const { items, tail } = listElements(list, env);
1795
1970
  const proper = tail.type === ATOM && tail.name === '[]';
1796
1971
  if (tail.type !== VAR && !proper) throw new PrologError('type_error(list)', whole);
1797
1972
 
1798
1973
  const invalid = items.find((item) => item.type !== VAR &&
1799
- (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item)));
1974
+ (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item, solver)));
1800
1975
  if (invalid) {
1801
1976
  if (kind === 'chars') throw new PrologError('type_error(character)', invalid);
1802
1977
  if (invalid.type !== NUMBER || !isDecimalInteger(invalid.name)) {
@@ -1816,12 +1991,12 @@ function numberListText(list, env, kind, valueIsBound) {
1816
1991
  const numberSyntaxError = new PrologError('syntax_error(number)');
1817
1992
 
1818
1993
  function numberListBuiltin(kind) {
1819
- return function* ({ goal, env }) {
1994
+ return function* ({ solver, goal, env }) {
1820
1995
  const value = deref(goal.args[0], env);
1821
1996
  if (value.type !== VAR && value.type !== NUMBER) throw new PrologError('type_error(number)', value);
1822
1997
  const list = deref(goal.args[1], env);
1823
1998
  if (value.type === VAR && list.type === VAR) throw new PrologError('instantiation_error');
1824
- const text = numberListText(list, env, kind, value.type === NUMBER);
1999
+ const text = numberListText(list, env, kind, value.type === NUMBER, solver);
1825
2000
  if (value.type === NUMBER) {
1826
2001
  if (text != null) {
1827
2002
  const parsed = parseIsoNumber(text);
@@ -1908,11 +2083,13 @@ function flatFindallTemplate(template) {
1908
2083
 
1909
2084
  function* findallBuiltin({ solver, goal, env }) {
1910
2085
  const [template, innerGoal, bag] = goal.args;
2086
+ // ISO 8.10.1.3 diagnoses Goal before Instances when both are erroneous.
2087
+ const invoked = callable(innerGoal, env);
1911
2088
  assertListOrPartial(bag, env);
1912
2089
  const collector = solver.cloneForInnerGoal(10000000);
1913
2090
  let compact = flatFindallTemplate(template);
1914
2091
  let collected = compact == null ? [] : null;
1915
- for (const answerEnv of collector.solve([callable(innerGoal, env)], env.clone(), 0)) {
2092
+ for (const answerEnv of collector.solve([invoked], env.clone(), 0)) {
1916
2093
  if (compact != null && compact.push(template, answerEnv)) continue;
1917
2094
  if (compact != null) {
1918
2095
  collected = compact.materialize();
@@ -1971,8 +2148,9 @@ function sortedUnique(items) {
1971
2148
 
1972
2149
  function allSolutionsBuiltin(asSet) {
1973
2150
  return function* ({ solver, goal, env }) {
1974
- assertListOrPartial(goal.args[2], env);
2151
+ // ISO 8.10.2.3/8.10.3.3 diagnose the iterated goal before Instances.
1975
2152
  const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2153
+ assertListOrPartial(goal.args[2], env);
1976
2154
  const free = freeVariables(iterated, goal.args[0], quantified, env);
1977
2155
  const collector = solver.cloneForInnerGoal(10000000);
1978
2156
  const groups = [];
package/src/parser.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { ATOM, COMPOUND, atom, compound, cons, emptyList, numberTerm, variable } from './term.js';
4
4
  import { continuesGraphicToken, isTerminatingFullStop } from './syntax-scan.js';
5
5
  import { CharacterRepresentationError, isStrictIsoPcsCodePoint } from './iso-character.js';
6
+ import { ISO_MAX_ARITY } from './iso-limits.js';
6
7
 
7
8
 
8
9
  export class NumberRepresentationError extends Error {
@@ -723,6 +724,7 @@ class Parser {
723
724
  }
724
725
  while (true) {
725
726
  args.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
727
+ if (args.length > ISO_MAX_ARITY) throw new NumberRepresentationError('representation_error(max_arity)');
726
728
  if (this.token.type !== TOK.COMMA) break;
727
729
  this.advance();
728
730
  }