eyeprolog 1.3.43 → 1.3.45

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
@@ -96,7 +96,7 @@ export const isoBuiltins = {
96
96
  registry.add('current_output', 1, currentOutputBuiltin, { deterministic: true });
97
97
  registry.add('set_input', 1, setCurrentStreamBuiltin('read'), { deterministic: true });
98
98
  registry.add('set_output', 1, setCurrentStreamBuiltin('write'), { deterministic: true });
99
- registry.add('flush_output', 0, succeed, { deterministic: true });
99
+ registry.add('flush_output', 0, flushOutputBuiltin, { deterministic: true });
100
100
  registry.add('flush_output', 1, flushOutputBuiltin, { deterministic: true });
101
101
  registry.add('stream_property', 2, streamPropertyBuiltin);
102
102
  registry.add('set_stream_position', 2, setStreamPositionBuiltin, { deterministic: true });
@@ -346,10 +346,18 @@ function* keysortBuiltin({ goal, env }) {
346
346
  validateListOutput(goal.args[1], env);
347
347
  for (const item of items) {
348
348
  const resolved = deref(item, env);
349
+ if (resolved.type === VAR) throw new PrologError('instantiation_error');
349
350
  if (resolved.type !== COMPOUND || resolved.name !== '-' || resolved.arity !== 2) {
350
351
  throw new PrologError('type_error(pair)', resolved);
351
352
  }
352
353
  }
354
+ const { items: outputPrefix } = listElements(goal.args[1], env);
355
+ for (const item of outputPrefix) {
356
+ if (item.type === VAR) continue;
357
+ if (item.type !== COMPOUND || item.name !== '-' || item.arity !== 2) {
358
+ throw new PrologError('type_error(pair)', item);
359
+ }
360
+ }
353
361
  // Modern ECMAScript specifies a stable Array#sort, as required by keysort/2.
354
362
  // Keep one implementation-dependent variable order for this whole sorting
355
363
  // operation, as required by ISO 7.2.1.
@@ -690,44 +698,49 @@ function* setPrologFlagBuiltin({ solver, goal, env }) {
690
698
 
691
699
  const operatorSpecifiers = new Set(['fx', 'fy', 'xf', 'yf', 'xfx', 'xfy', 'yfx']);
692
700
 
693
- function operatorPriority(term, env) {
701
+ function opListPreflight(term, env) {
694
702
  const value = deref(term, env);
695
703
  if (value.type === VAR) throw new PrologError('instantiation_error');
696
- if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
697
- const priority = BigInt(value.name);
698
- if (priority < 0n || priority > 1200n) throw new PrologError('domain_error(operator_priority)', value);
699
- return Number(priority);
704
+ if (value.type === ATOM) return { value, items: [value], proper: true, atom: true };
705
+ const { items, tail } = listElements(value, env);
706
+ if (tail.type === VAR || items.some((item) => item.type === VAR)) {
707
+ throw new PrologError('instantiation_error');
708
+ }
709
+ const proper = tail.type === ATOM && tail.name === '[]';
710
+ return { value, items, proper, atom: false };
700
711
  }
701
712
 
702
- function operatorSpecifier(term, env) {
703
- const value = deref(term, env);
704
- if (value.type === VAR) throw new PrologError('instantiation_error');
705
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
706
- if (!operatorSpecifiers.has(value.name)) throw new PrologError('domain_error(operator_specifier)', value);
707
- return value.name;
708
- }
713
+ function* opBuiltin({ solver, goal, env }) {
714
+ const priorityTerm = deref(goal.args[0], env);
715
+ const specifierTerm = deref(goal.args[1], env);
709
716
 
710
- function operatorNames(term, env) {
711
- const value = deref(term, env);
712
- if (value.type === VAR) throw new PrologError('instantiation_error');
713
- if (value.type === ATOM) return [value];
714
- const items = properListItems(value, env);
715
- if (items == null) {
716
- if (isPartialList(value, env)) throw new PrologError('instantiation_error');
717
- throw new PrologError('type_error(list)', value);
717
+ // ISO 8.14.3.3 prescribes the ordering below. In particular, all three
718
+ // instantiation cases precede type/domain errors, and Operator list-shape
719
+ // errors precede priority/specifier domain errors.
720
+ if (priorityTerm.type === VAR) throw new PrologError('instantiation_error');
721
+ if (specifierTerm.type === VAR) throw new PrologError('instantiation_error');
722
+ const operator = opListPreflight(goal.args[2], env);
723
+
724
+ if (priorityTerm.type !== NUMBER || !isDecimalInteger(priorityTerm.name)) {
725
+ throw new PrologError('type_error(integer)', priorityTerm);
718
726
  }
719
- for (const item of items) {
720
- const resolved = deref(item, env);
721
- if (resolved.type === VAR) throw new PrologError('instantiation_error');
722
- if (resolved.type !== ATOM) throw new PrologError('type_error(atom)', resolved);
727
+ if (specifierTerm.type !== ATOM) throw new PrologError('type_error(atom)', specifierTerm);
728
+ if (!operator.atom && !operator.proper) throw new PrologError('type_error(list)', operator.value);
729
+ for (const item of operator.items) {
730
+ if (item.type !== ATOM) throw new PrologError('type_error(atom)', item);
723
731
  }
724
- return items.map((item) => deref(item, env));
725
- }
726
732
 
727
- function* opBuiltin({ solver, goal, env }) {
728
- const priority = operatorPriority(goal.args[0], env);
729
- const specifier = operatorSpecifier(goal.args[1], env);
730
- for (const name of operatorNames(goal.args[2], env)) {
733
+ const priorityBig = BigInt(priorityTerm.name);
734
+ if (priorityBig < 0n || priorityBig > 1200n) {
735
+ throw new PrologError('domain_error(operator_priority)', priorityTerm);
736
+ }
737
+ if (!operatorSpecifiers.has(specifierTerm.name)) {
738
+ throw new PrologError('domain_error(operator_specifier)', specifierTerm);
739
+ }
740
+
741
+ const priority = Number(priorityBig);
742
+ const specifier = specifierTerm.name;
743
+ for (const name of operator.items) {
731
744
  if (name.name === ',') {
732
745
  throw new PrologError('permission_error(modify, operator)', name);
733
746
  }
@@ -755,19 +768,16 @@ function* currentOpBuiltin({ solver, goal, env }) {
755
768
  const priority = deref(goal.args[0], env);
756
769
  const specifier = deref(goal.args[1], env);
757
770
  const name = deref(goal.args[2], env);
758
- if (priority.type !== VAR) {
759
- if (priority.type !== NUMBER || !isDecimalInteger(priority.name)) {
760
- throw new PrologError('type_error(integer)', priority);
761
- }
762
- if (BigInt(priority.name) < 0n || BigInt(priority.name) > 1200n) {
763
- throw new PrologError('domain_error(operator_priority)', priority);
764
- }
771
+ // ISO 8.14.4.3 defines the first two argument errors by domain, not by
772
+ // their underlying atom/integer representation type.
773
+ if (priority.type !== VAR &&
774
+ (priority.type !== NUMBER || !isDecimalInteger(priority.name) ||
775
+ BigInt(priority.name) < 0n || BigInt(priority.name) > 1200n)) {
776
+ throw new PrologError('domain_error(operator_priority)', priority);
765
777
  }
766
- if (specifier.type !== VAR) {
767
- if (specifier.type !== ATOM) throw new PrologError('type_error(atom)', specifier);
768
- if (!operatorSpecifiers.has(specifier.name)) {
769
- throw new PrologError('domain_error(operator_specifier)', specifier);
770
- }
778
+ if (specifier.type !== VAR &&
779
+ (specifier.type !== ATOM || !operatorSpecifiers.has(specifier.name))) {
780
+ throw new PrologError('domain_error(operator_specifier)', specifier);
771
781
  }
772
782
  if (name.type !== VAR && name.type !== ATOM) throw new PrologError('type_error(atom)', name);
773
783
  for (const definition of solver.program.operators.values()) {
@@ -828,6 +838,16 @@ function streamReference(term, env) {
828
838
  throw new PrologError('domain_error(stream_or_alias)', value);
829
839
  }
830
840
 
841
+ function streamTermReference(term, env) {
842
+ const value = deref(term, env);
843
+ if (value.type === VAR) return null;
844
+ if (value.type === COMPOUND && value.name === '$stream' && value.arity === 1 &&
845
+ value.args[0].type === NUMBER && isDecimalInteger(value.args[0].name)) {
846
+ return Number(value.args[0].name);
847
+ }
848
+ throw new PrologError('domain_error(stream)', value);
849
+ }
850
+
831
851
  function requireStream(solver, term, env, mode = null) {
832
852
  const culprit = deref(term, env);
833
853
  const stream = solver.io.resolve(streamReference(term, env));
@@ -849,6 +869,28 @@ function optionList(term, env) {
849
869
  return items.map((item) => deref(item, env));
850
870
  }
851
871
 
872
+ // read_term/3 and write_term/3 have prescribed error ordering in ISO 8.14.
873
+ // In particular, a partial option list (or a variable option element) is
874
+ // diagnosed before several stream errors, while the relative order of a
875
+ // non-list Options term and an invalid stream-or-alias differs between the
876
+ // read and write predicates. Split list-shape inspection from full option
877
+ // validation so those predicates can follow the standard order exactly.
878
+ function preflightOptionList(term, env) {
879
+ const value = deref(term, env);
880
+ if (value.type === VAR) throw new PrologError('instantiation_error');
881
+ const { items, tail } = listElements(value, env);
882
+ if (tail.type === VAR || items.some((item) => item.type === VAR)) {
883
+ throw new PrologError('instantiation_error');
884
+ }
885
+ const proper = tail.type === ATOM && tail.name === '[]';
886
+ return { value, items, proper };
887
+ }
888
+
889
+ function requireProperOptionList(preflight) {
890
+ if (!preflight.proper) throw new PrologError('type_error(list)', preflight.value);
891
+ return preflight.items;
892
+ }
893
+
852
894
  function optionAtom(option, name) {
853
895
  if (option.type !== COMPOUND || option.name !== name || option.arity !== 1) return null;
854
896
  return option.args[0];
@@ -862,13 +904,14 @@ function openOptions(term, env) {
862
904
  if ((value = optionAtom(option, 'type'))) {
863
905
  value = deref(value, env);
864
906
  if (value.type === VAR) throw new PrologError('instantiation_error');
865
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
866
- if (!['text', 'binary'].includes(value.name)) throw new PrologError('domain_error(stream_option)', option);
907
+ if (value.type !== ATOM || !['text', 'binary'].includes(value.name)) {
908
+ throw new PrologError('domain_error(stream_option)', option);
909
+ }
867
910
  result.type = value.name;
868
911
  } else if ((value = optionAtom(option, 'alias'))) {
869
912
  value = deref(value, env);
870
913
  if (value.type === VAR) throw new PrologError('instantiation_error');
871
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
914
+ if (value.type !== ATOM) throw new PrologError('domain_error(stream_option)', option);
872
915
  result.alias = value.name;
873
916
  } else if ((value = optionAtom(option, 'reposition'))) {
874
917
  value = deref(value, env);
@@ -887,19 +930,45 @@ function openOptions(term, env) {
887
930
  return result;
888
931
  }
889
932
 
933
+ function streamOptionHasRequiredVariable(option, env) {
934
+ option = deref(option, env);
935
+ if (option.type !== COMPOUND || option.arity !== 1 ||
936
+ !['type', 'alias', 'reposition', 'eof_action'].includes(option.name)) return false;
937
+ return deref(option.args[0], env).type === VAR;
938
+ }
939
+
890
940
  function* openBuiltin({ solver, goal, env }) {
891
- const path = requireAtom(goal.args[0], env);
892
- const mode = requireAtom(goal.args[1], env);
893
- if (!['read', 'write', 'append'].includes(mode.name)) throw new PrologError('domain_error(io_mode)', mode);
941
+ const source = deref(goal.args[0], env);
942
+ const mode = deref(goal.args[1], env);
894
943
  const streamTarget = deref(goal.args[2], env);
944
+ const optionTerm = goal.arity === 3 ? emptyList() : goal.args[3];
945
+
946
+ // ISO 8.11.5.3 + Corrigenda 2/3. Keep each prescribed condition
947
+ // observable on its own and apply the published ordering for deterministic
948
+ // diagnostics when several arguments are erroneous.
949
+ if (source.type === VAR) throw new PrologError('instantiation_error');
950
+ if (mode.type === VAR) throw new PrologError('instantiation_error');
951
+ const preflight = preflightOptionList(optionTerm, env);
952
+ if (preflight.items.some((option) => streamOptionHasRequiredVariable(option, env))) {
953
+ throw new PrologError('instantiation_error');
954
+ }
955
+ if (mode.type !== ATOM) throw new PrologError('type_error(atom)', mode);
956
+ const optionItems = requireProperOptionList(preflight);
895
957
  if (streamTarget.type !== VAR) throw new PrologError('uninstantiation_error', streamTarget);
896
- const options = goal.arity === 3 ? {} : openOptions(goal.args[3], env);
897
- if (options.alias && solver.io.resolve(options.alias)) throw new PrologError('permission_error(open, source_sink)', atom(options.alias));
958
+ if (source.type !== ATOM) throw new PrologError('domain_error(source_sink)', source);
959
+ if (!['read', 'write', 'append'].includes(mode.name)) throw new PrologError('domain_error(io_mode)', mode);
960
+ const options = goal.arity === 3 ? {} : openOptions(optionTerm, env);
961
+ if (options.alias && solver.io.resolve(options.alias)) {
962
+ throw new PrologError('permission_error(open, source_sink)', atom(options.alias));
963
+ }
898
964
  let stream;
899
965
  try {
900
- stream = solver.io.open(path.name, mode.name, options);
901
- } catch (_) {
902
- throw new PrologError('existence_error(source_sink)', path);
966
+ stream = solver.io.open(source.name, mode.name, options);
967
+ } catch (error) {
968
+ if (error?.code === 'ENOENT' && mode.name === 'read') {
969
+ throw new PrologError('existence_error(source_sink)', source);
970
+ }
971
+ throw new PrologError('permission_error(open, source_sink)', source);
903
972
  }
904
973
  const next = env.clone();
905
974
  if (unify(goal.args[2], streamHandle(stream.id), next)) yield next;
@@ -907,22 +976,35 @@ function* openBuiltin({ solver, goal, env }) {
907
976
  }
908
977
 
909
978
  function* closeBuiltin({ solver, goal, env }) {
910
- if (goal.arity === 2) {
911
- for (const option of optionList(goal.args[1], env)) {
912
- if (option.type === VAR) throw new PrologError('instantiation_error');
913
- const force = optionAtom(option, 'force');
914
- const value = force && deref(force, env);
915
- if (value?.type === VAR) throw new PrologError('instantiation_error');
916
- if (!value || value.type !== ATOM || !['true', 'false'].includes(value.name)) {
917
- throw new PrologError('domain_error(close_option)', option);
918
- }
979
+ const reference = deref(goal.args[0], env);
980
+ if (reference.type === VAR) throw new PrologError('instantiation_error');
981
+ const optionTerm = goal.arity === 2 ? goal.args[1] : emptyList();
982
+ const preflight = preflightOptionList(optionTerm, env);
983
+ if (preflight.items.some((option) => {
984
+ const force = optionAtom(option, 'force');
985
+ return force != null && deref(force, env).type === VAR;
986
+ })) throw new PrologError('instantiation_error');
987
+ const options = requireProperOptionList(preflight);
988
+ streamReference(goal.args[0], env);
989
+ let forceClose = false;
990
+ for (const option of options) {
991
+ const force = optionAtom(option, 'force');
992
+ const value = force && deref(force, env);
993
+ if (!value || value.type !== ATOM || !['true', 'false'].includes(value.name)) {
994
+ throw new PrologError('domain_error(close_option)', option);
919
995
  }
996
+ forceClose = value.name === 'true'; // rightmost contradictory option applies
920
997
  }
921
998
  const stream = requireStream(solver, goal.args[0], env);
922
999
  if (!stream.standard) {
1000
+ try {
1001
+ solver.io.close(stream);
1002
+ } catch (_) {
1003
+ if (!forceClose) throw new PrologError('system_error');
1004
+ solver.io.discard(stream);
1005
+ }
923
1006
  if (solver.io.currentInput === stream.id) solver.io.currentInput = 0;
924
1007
  if (solver.io.currentOutput === stream.id) solver.io.currentOutput = 1;
925
- solver.io.close(stream);
926
1008
  }
927
1009
  yield env;
928
1010
  }
@@ -930,7 +1012,8 @@ function* closeBuiltin({ solver, goal, env }) {
930
1012
  function* currentInputBuiltin({ solver, goal, env }) {
931
1013
  const value = deref(goal.args[0], env);
932
1014
  if (value.type !== VAR) {
933
- const stream = solver.io.resolve(streamReference(goal.args[0], env));
1015
+ const id = streamTermReference(goal.args[0], env);
1016
+ const stream = solver.io.resolve(id);
934
1017
  if (!stream) throw new PrologError('domain_error(stream)', value);
935
1018
  if (stream.id === solver.io.currentInput) yield env;
936
1019
  return;
@@ -941,7 +1024,8 @@ function* currentInputBuiltin({ solver, goal, env }) {
941
1024
  function* currentOutputBuiltin({ solver, goal, env }) {
942
1025
  const value = deref(goal.args[0], env);
943
1026
  if (value.type !== VAR) {
944
- const stream = solver.io.resolve(streamReference(goal.args[0], env));
1027
+ const id = streamTermReference(goal.args[0], env);
1028
+ const stream = solver.io.resolve(id);
945
1029
  if (!stream) throw new PrologError('domain_error(stream)', value);
946
1030
  if (stream.id === solver.io.currentOutput) yield env;
947
1031
  return;
@@ -949,6 +1033,7 @@ function* currentOutputBuiltin({ solver, goal, env }) {
949
1033
  const next = env.clone();
950
1034
  if (unify(goal.args[0], streamHandle(solver.io.currentOutput), next)) yield next;
951
1035
  }
1036
+
952
1037
  function setCurrentStreamBuiltin(mode) {
953
1038
  return function* ({ solver, goal, env }) {
954
1039
  const stream = requireStream(solver, goal.args[0], env, mode);
@@ -958,7 +1043,14 @@ function setCurrentStreamBuiltin(mode) {
958
1043
  };
959
1044
  }
960
1045
  function* flushOutputBuiltin({ solver, goal, env }) {
961
- requireStream(solver, goal.args[0], env, 'write');
1046
+ const stream = goal.arity === 0
1047
+ ? solver.io.resolve(solver.io.currentOutput)
1048
+ : requireStream(solver, goal.args[0], env, 'write');
1049
+ try {
1050
+ solver.io.flush(stream);
1051
+ } catch (_) {
1052
+ throw new PrologError('system_error');
1053
+ }
962
1054
  yield env;
963
1055
  }
964
1056
 
@@ -979,23 +1071,51 @@ function streamProperties(stream) {
979
1071
  return properties;
980
1072
  }
981
1073
  function* setStreamPositionBuiltin({ solver, goal, env }) {
982
- const stream = requireStream(solver, goal.args[0], env);
983
- if (!stream.reposition) throw new PrologError('permission_error(reposition, stream)', deref(goal.args[0], env));
1074
+ const reference = deref(goal.args[0], env);
1075
+ if (reference.type === VAR) throw new PrologError('instantiation_error');
984
1076
  let position = deref(goal.args[1], env);
985
- if (position.type === COMPOUND && position.name === 'position' && position.arity === 1) {
1077
+ if (position.type === VAR) throw new PrologError('instantiation_error');
1078
+ streamReference(goal.args[0], env);
1079
+ if (!solver.isoStrict && position.type === COMPOUND && position.name === 'position' && position.arity === 1) {
1080
+ // Historical EyeProlog convenience form. Strict Part 1 mode uses only
1081
+ // the implementation-defined integer stream-position terms returned by
1082
+ // stream_property/2.
986
1083
  position = deref(position.args[0], env);
987
1084
  }
988
- if (position.type === VAR) throw new PrologError('instantiation_error');
989
- if (position.type !== NUMBER || !isDecimalInteger(position.name)) throw new PrologError('domain_error(stream_position)', position);
1085
+ if (position.type === VAR || position.type !== NUMBER || !isDecimalInteger(position.name)) {
1086
+ throw new PrologError('domain_error(stream_position)', position);
1087
+ }
990
1088
  const offset = BigInt(position.name);
991
- if (offset < 0n || offset > BigInt(stream.content.length)) throw new PrologError('domain_error(stream_position)', position);
1089
+ if (offset < 0n) throw new PrologError('domain_error(stream_position)', position);
1090
+ const stream = requireStream(solver, goal.args[0], env);
1091
+ if (!stream.reposition) throw new PrologError('permission_error(reposition, stream)', reference);
1092
+ if (offset > BigInt(stream.content.length)) throw new PrologError('domain_error(stream_position)', position);
992
1093
  stream.position = Number(offset);
993
1094
  stream.pastEnd = false;
994
1095
  yield env;
995
1096
  }
1097
+
1098
+ function isStreamPropertyPattern(value) {
1099
+ if (value.type === VAR) return true;
1100
+ if (value.type === ATOM) return value.name === 'input' || value.name === 'output';
1101
+ return value.type === COMPOUND && value.arity === 1 && [
1102
+ 'file_name', 'mode', 'alias', 'position', 'end_of_stream', 'eof_action', 'reposition', 'type',
1103
+ ].includes(value.name);
1104
+ }
1105
+
996
1106
  function* streamPropertyBuiltin({ solver, goal, env }) {
997
1107
  const reference = deref(goal.args[0], env);
998
- const streams = reference.type === VAR ? [...solver.io.streams.values()] : [requireStream(solver, goal.args[0], env)];
1108
+ const propertyPattern = deref(goal.args[1], env);
1109
+ if (reference.type !== VAR) {
1110
+ const id = streamTermReference(goal.args[0], env);
1111
+ if (!solver.io.resolve(id)) throw new PrologError('domain_error(stream)', reference);
1112
+ }
1113
+ if (!isStreamPropertyPattern(propertyPattern)) {
1114
+ throw new PrologError('domain_error(stream_property)', propertyPattern);
1115
+ }
1116
+ const streams = reference.type === VAR
1117
+ ? [...solver.io.streams.values()]
1118
+ : [solver.io.resolve(streamTermReference(goal.args[0], env))];
999
1119
  for (const stream of streams) {
1000
1120
  for (const property of streamProperties(stream)) {
1001
1121
  const next = env.clone();
@@ -1011,27 +1131,69 @@ function outputStreamFor(solver, goal, env) {
1011
1131
  return goal.arity === 1 ? solver.io.resolve(solver.io.currentOutput) : requireStream(solver, goal.args[0], env, 'write');
1012
1132
  }
1013
1133
  function* atEndBuiltin({ solver, goal, env }) {
1014
- const stream = goal.arity === 0 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
1015
- if (stream.position >= stream.content.length) yield env;
1134
+ // at_end_of_stream/1 is bootstrapped in Part 1 from the end_of_stream/1
1135
+ // stream property and therefore accepts any open stream-or-alias; it is not
1136
+ // an input-only operation.
1137
+ const stream = goal.arity === 0 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env);
1138
+ if (stream.pastEnd || stream.position >= stream.content.length) yield env;
1139
+ }
1140
+ function validStrictInputCharacterCode(value, solver) {
1141
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) return false;
1142
+ const code = BigInt(value.name);
1143
+ if (code === -1n) return true;
1144
+ if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return false;
1145
+ return !solver.isoStrict || isStrictIsoPcsCodePoint(Number(code));
1016
1146
  }
1147
+
1148
+ function preflightInputUnitTarget(name, target, env, solver) {
1149
+ const value = deref(target, env);
1150
+ if (name.endsWith('char')) {
1151
+ if (value.type !== VAR && !(oneChar(value) || (value.type === ATOM && value.name === 'end_of_file'))) {
1152
+ throw new PrologError('type_error(in_character)', value);
1153
+ }
1154
+ if (value.type === ATOM && oneChar(value) && solver.isoStrict && !isStrictIsoPcsCharacter(value.name)) {
1155
+ throw new PrologError('type_error(in_character)', value);
1156
+ }
1157
+ } else if (name.endsWith('code')) {
1158
+ if (value.type !== VAR && (value.type !== NUMBER || !isDecimalInteger(value.name))) {
1159
+ throw new PrologError('type_error(integer)', value);
1160
+ }
1161
+ if (value.type !== VAR && !validStrictInputCharacterCode(value, solver)) {
1162
+ throw new PrologError('representation_error(in_character_code)');
1163
+ }
1164
+ } else {
1165
+ if (value.type !== VAR) {
1166
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(in_byte)', value);
1167
+ const byte = BigInt(value.name);
1168
+ if (byte !== -1n && (byte < 0n || byte > 255n)) throw new PrologError('type_error(in_byte)', value);
1169
+ }
1170
+ }
1171
+ return value;
1172
+ }
1173
+
1017
1174
  function inputUnitBuiltin(name) {
1018
1175
  return function* ({ solver, goal, env }) {
1176
+ const explicit = goal.arity === 2;
1177
+ if (explicit && deref(goal.args[0], env).type === VAR) throw new PrologError('instantiation_error');
1178
+ const target = goal.args[goal.arity - 1];
1179
+ preflightInputUnitTarget(name, target, env, solver);
1019
1180
  const stream = inputStreamFor(solver, goal, env);
1020
1181
  const binary = name.endsWith('byte');
1021
- if (binary !== (stream.type === 'binary')) throw new PrologError('permission_error(input, stream)', streamHandle(stream.id));
1182
+ if (binary && stream.type !== 'binary') {
1183
+ throw new PrologError('permission_error(input, text_stream)', streamHandle(stream.id));
1184
+ }
1185
+ if (!binary && stream.type !== 'text') {
1186
+ throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1187
+ }
1022
1188
  if (stream.pastEnd && stream.eofAction === 'error') {
1023
1189
  throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
1024
1190
  }
1025
1191
  if (stream.pastEnd && stream.eofAction === 'reset') {
1026
- // A terminal EOF (Ctrl-D) is local to one input operation. The next
1027
- // operation should wait for fresh terminal input rather than rewinding
1028
- // and replaying the already-consumed interactive buffer.
1029
1192
  if (typeof stream.interactiveReadUnit !== 'function') stream.position = 0;
1030
1193
  stream.pastEnd = false;
1031
1194
  }
1032
1195
  const peek = name.startsWith('peek');
1033
- if (stream.position >= stream.content.length &&
1034
- typeof stream.interactiveReadUnit === 'function') {
1196
+ if (stream.position >= stream.content.length && typeof stream.interactiveReadUnit === 'function') {
1035
1197
  const text = stream.interactiveReadUnit();
1036
1198
  if (text != null) {
1037
1199
  stream.content += String(text);
@@ -1042,50 +1204,68 @@ function inputUnitBuiltin(name) {
1042
1204
  try {
1043
1205
  unit = solver.io.readUnit(stream, peek);
1044
1206
  } catch (error) {
1045
- if (error?.name === 'InvalidCharacterEncodingError') {
1046
- throw new PrologError('representation_error(character)');
1047
- }
1207
+ if (error?.name === 'InvalidCharacterEncodingError') throw new PrologError('representation_error(character)');
1048
1208
  throw error;
1049
1209
  }
1050
1210
  if (unit == null && !peek) stream.pastEnd = true;
1051
1211
  if (!binary && unit != null && solver.isoStrict && !isStrictIsoPcsCharacter(unit)) {
1052
1212
  throw new PrologError('representation_error(character)');
1053
1213
  }
1054
- const result = unit == null ? (binary ? numberTerm(-1) : name.endsWith('code') ? numberTerm(-1) : atom('end_of_file'))
1214
+ const result = unit == null
1215
+ ? (binary ? numberTerm(-1) : name.endsWith('code') ? numberTerm(-1) : atom('end_of_file'))
1055
1216
  : binary ? numberTerm(unit) : name.endsWith('code') ? numberTerm(unit.codePointAt(0)) : atom(unit);
1056
- const target = goal.args[goal.arity - 1];
1057
1217
  const next = env.clone();
1058
1218
  if (unify(target, result, next)) yield next;
1059
1219
  };
1060
1220
  }
1221
+
1222
+ function preflightOutputUnitValue(name, target, env) {
1223
+ const value = deref(target, env);
1224
+ if (value.type === VAR) throw new PrologError('instantiation_error');
1225
+ if (name === 'put_char') {
1226
+ if (!oneChar(value)) throw new PrologError('type_error(character)', value);
1227
+ return value;
1228
+ }
1229
+ if (name === 'put_byte') {
1230
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(byte)', value);
1231
+ const byte = BigInt(value.name);
1232
+ if (byte < 0n || byte > 255n) throw new PrologError('type_error(byte)', value);
1233
+ return value;
1234
+ }
1235
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
1236
+ return value;
1237
+ }
1238
+
1061
1239
  function outputUnitBuiltin(name) {
1062
1240
  return function* ({ solver, goal, env }) {
1241
+ const explicit = goal.arity === 2;
1242
+ if (explicit && deref(goal.args[0], env).type === VAR) throw new PrologError('instantiation_error');
1243
+ const value = preflightOutputUnitValue(name, goal.args[goal.arity - 1], env);
1063
1244
  const stream = outputStreamFor(solver, goal, env);
1064
- const value = deref(goal.args[goal.arity - 1], env);
1065
- if (value.type === VAR) throw new PrologError('instantiation_error');
1066
1245
  if (name === 'put_char') {
1067
1246
  if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1068
- if (!oneChar(value)) throw new PrologError('type_error(character)', value);
1069
1247
  if (solver.isoStrict && !isStrictIsoPcsCharacter(value.name)) {
1070
1248
  throw new PrologError('representation_error(character)', value);
1071
1249
  }
1072
1250
  solver.io.writeUnit(stream, value.name);
1251
+ } else if (name === 'put_byte') {
1252
+ if (stream.type !== 'binary') throw new PrologError('permission_error(output, text_stream)', streamHandle(stream.id));
1253
+ solver.io.writeUnit(stream, Number(value.name));
1073
1254
  } else {
1074
- if ((name === 'put_byte') !== (stream.type === 'binary')) {
1075
- throw new PrologError('permission_error(output, stream)', streamHandle(stream.id));
1076
- }
1077
- if (value.type !== NUMBER || !isDecimalInteger(value.name)) throw new PrologError('type_error(integer)', value);
1255
+ if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1078
1256
  const code = BigInt(value.name);
1079
- const max = name === 'put_byte' ? 255n : 0x10ffffn;
1080
- if (code < 0n || code > max) throw new PrologError(name === 'put_byte' ? 'type_error(byte)' : 'representation_error(character_code)');
1081
- if (name === 'put_code' && solver.isoStrict && !isStrictIsoPcsCodePoint(Number(code))) {
1257
+ if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) {
1258
+ throw new PrologError('representation_error(character_code)');
1259
+ }
1260
+ if (solver.isoStrict && !isStrictIsoPcsCodePoint(Number(code))) {
1082
1261
  throw new PrologError('representation_error(character_code)');
1083
1262
  }
1084
- solver.io.writeUnit(stream, name === 'put_byte' ? Number(code) : String.fromCodePoint(Number(code)));
1263
+ solver.io.writeUnit(stream, String.fromCodePoint(Number(code)));
1085
1264
  }
1086
1265
  yield env;
1087
1266
  };
1088
1267
  }
1268
+
1089
1269
  function* nlBuiltin({ solver, goal, env }) {
1090
1270
  const stream = goal.arity === 0
1091
1271
  ? solver.io.resolve(solver.io.currentOutput)
@@ -1250,6 +1430,13 @@ export function isCompleteReadTermText(text, solver) {
1250
1430
  }
1251
1431
 
1252
1432
  function readTermFromStream(stream, solver) {
1433
+ if (stream.pastEnd && stream.eofAction === 'error') {
1434
+ throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
1435
+ }
1436
+ if (stream.pastEnd && stream.eofAction === 'reset') {
1437
+ if (typeof stream.interactiveReadTerm !== 'function') stream.position = 0;
1438
+ stream.pastEnd = false;
1439
+ }
1253
1440
  let requestedInteractiveTerm = false;
1254
1441
  while (true) {
1255
1442
  let sawCandidate = false;
@@ -1293,6 +1480,7 @@ function readTermFromStream(stream, solver) {
1293
1480
  stream.position = source.length;
1294
1481
  if (!sawCandidate) {
1295
1482
  if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1483
+ stream.pastEnd = true;
1296
1484
  return { term: atom('end_of_file'), variables: [] };
1297
1485
  }
1298
1486
  throw new PrologError('syntax_error(read_term)');
@@ -1306,17 +1494,38 @@ function* readBuiltin({ solver, goal, env }) {
1306
1494
  const { term } = readTermFromStream(stream, solver);
1307
1495
  if (unify(goal.args[goal.arity - 1], term, next)) yield next;
1308
1496
  }
1497
+ function validateReadOptions(options) {
1498
+ for (const option of options) {
1499
+ if (option.type !== COMPOUND || option.arity !== 1 ||
1500
+ !['variables', 'variable_names', 'singletons'].includes(option.name)) {
1501
+ throw new PrologError('domain_error(read_option)', option);
1502
+ }
1503
+ }
1504
+ }
1505
+
1309
1506
  function* readTermBuiltin({ solver, goal, env }) {
1310
- const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
1507
+ const optionTerm = goal.args[goal.arity - 1];
1508
+ const streamTerm = goal.arity === 3 ? deref(goal.args[0], env) : null;
1509
+
1510
+ // ISO 8.14.1.3 error order:
1511
+ // a) variable stream; b) partial/options-variable; c) bad stream-or-alias;
1512
+ // d) non-list options; e) invalid read option; then stream existence/mode.
1513
+ if (streamTerm?.type === VAR) throw new PrologError('instantiation_error');
1514
+ const preflight = preflightOptionList(optionTerm, env);
1515
+ if (goal.arity === 3) streamReference(goal.args[0], env);
1516
+ const options = requireProperOptionList(preflight);
1517
+ validateReadOptions(options);
1518
+
1519
+ const stream = goal.arity === 2
1520
+ ? solver.io.resolve(solver.io.currentInput)
1521
+ : requireStream(solver, goal.args[0], env, 'read');
1311
1522
  if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1312
- const options = optionList(goal.args[goal.arity - 1], env);
1523
+
1313
1524
  const target = goal.args[goal.arity - 2];
1314
1525
  const { term, variables } = readTermFromStream(stream, solver);
1315
1526
  const next = env.clone();
1316
1527
  if (!unify(target, term, next)) return;
1317
1528
  for (const option of options) {
1318
- if (option.type === VAR) throw new PrologError('instantiation_error');
1319
- if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(read_option)', option);
1320
1529
  let value;
1321
1530
  if (option.name === 'variables') {
1322
1531
  value = listFromItems(variables.map((item) => item.term));
@@ -1324,12 +1533,10 @@ function* readTermBuiltin({ solver, goal, env }) {
1324
1533
  value = listFromItems(variables
1325
1534
  .filter((item) => !item.anonymous)
1326
1535
  .map((item) => compound('=', [atom(item.sourceName), item.term])));
1327
- } else if (option.name === 'singletons') {
1536
+ } else {
1328
1537
  value = listFromItems(variables
1329
1538
  .filter((item) => !item.anonymous && item.count === 1)
1330
1539
  .map((item) => compound('=', [atom(item.sourceName), item.term])));
1331
- } else {
1332
- throw new PrologError('domain_error(read_option)', option);
1333
1540
  }
1334
1541
  if (!unify(option.args[0], value, next)) return;
1335
1542
  }
@@ -1378,23 +1585,26 @@ function writeVariableNames(value, env, option) {
1378
1585
  return names;
1379
1586
  }
1380
1587
 
1381
- function termWriteOptions(term, env, mode = 'write_term') {
1588
+ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = null) {
1382
1589
  const result = defaultTermWriteOptions(mode);
1383
- for (const option of optionList(term, env)) {
1384
- if (option.type === VAR) throw new PrologError('instantiation_error');
1590
+ for (const option of options) {
1385
1591
  if (option.type !== COMPOUND || option.arity !== 1) {
1386
1592
  throw new PrologError('domain_error(write_option)', option);
1387
1593
  }
1388
1594
  if (option.name === 'quoted') result.quoted = writeOptionBoolean(option.args[0], env, option);
1389
1595
  else if (option.name === 'ignore_ops') result.ignoreOps = writeOptionBoolean(option.args[0], env, option);
1390
1596
  else if (option.name === 'numbervars') result.numbervars = writeOptionBoolean(option.args[0], env, option);
1391
- else if (option.name === 'double_quotes') result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
1392
1597
  else if (option.name === 'variable_names') result.variableNames = writeVariableNames(option.args[0], env, option);
1393
- else throw new PrologError('domain_error(write_option)', option);
1598
+ else if (option.name === 'double_quotes' && !solver?.isoStrict) {
1599
+ result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
1600
+ } else {
1601
+ throw new PrologError('domain_error(write_option)', option);
1602
+ }
1394
1603
  }
1395
1604
  return result;
1396
1605
  }
1397
1606
 
1607
+
1398
1608
  function writeBuiltin(mode) {
1399
1609
  return function* ({ solver, goal, env }) {
1400
1610
  const stream = outputStreamFor(solver, goal, env);
@@ -1410,9 +1620,22 @@ function writeBuiltin(mode) {
1410
1620
  };
1411
1621
  }
1412
1622
  function* writeTermBuiltin({ solver, goal, env }) {
1413
- const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentOutput) : requireStream(solver, goal.args[0], env, 'write');
1623
+ const optionTerm = goal.args[goal.arity - 1];
1624
+ const streamTerm = goal.arity === 3 ? deref(goal.args[0], env) : null;
1625
+
1626
+ // ISO 8.14.2.3 error order:
1627
+ // a) variable stream; b) partial/options-variable; c) non-list options;
1628
+ // d) bad stream-or-alias; e) invalid write option; then stream errors.
1629
+ if (streamTerm?.type === VAR) throw new PrologError('instantiation_error');
1630
+ const preflight = preflightOptionList(optionTerm, env);
1631
+ const optionItems = requireProperOptionList(preflight);
1632
+ if (goal.arity === 3) streamReference(goal.args[0], env);
1633
+ const options = termWriteOptionsFromItems(optionItems, env, 'write_term', solver);
1634
+
1635
+ const stream = goal.arity === 2
1636
+ ? solver.io.resolve(solver.io.currentOutput)
1637
+ : requireStream(solver, goal.args[0], env, 'write');
1414
1638
  if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1415
- const options = termWriteOptions(goal.args[goal.arity - 1], env);
1416
1639
  if (options.doubleQuotes === true) {
1417
1640
  options.doubleQuotes = solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars';
1418
1641
  } else {
@@ -1730,14 +1953,14 @@ function canonicalNumberText(value) {
1730
1953
  return text;
1731
1954
  }
1732
1955
 
1733
- function numberListText(list, env, kind, valueIsBound) {
1956
+ function numberListText(list, env, kind, valueIsBound, solver = null) {
1734
1957
  const whole = deref(list, env);
1735
1958
  const { items, tail } = listElements(list, env);
1736
1959
  const proper = tail.type === ATOM && tail.name === '[]';
1737
1960
  if (tail.type !== VAR && !proper) throw new PrologError('type_error(list)', whole);
1738
1961
 
1739
1962
  const invalid = items.find((item) => item.type !== VAR &&
1740
- (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item)));
1963
+ (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item, solver)));
1741
1964
  if (invalid) {
1742
1965
  if (kind === 'chars') throw new PrologError('type_error(character)', invalid);
1743
1966
  if (invalid.type !== NUMBER || !isDecimalInteger(invalid.name)) {
@@ -1757,12 +1980,12 @@ function numberListText(list, env, kind, valueIsBound) {
1757
1980
  const numberSyntaxError = new PrologError('syntax_error(number)');
1758
1981
 
1759
1982
  function numberListBuiltin(kind) {
1760
- return function* ({ goal, env }) {
1983
+ return function* ({ solver, goal, env }) {
1761
1984
  const value = deref(goal.args[0], env);
1762
1985
  if (value.type !== VAR && value.type !== NUMBER) throw new PrologError('type_error(number)', value);
1763
1986
  const list = deref(goal.args[1], env);
1764
1987
  if (value.type === VAR && list.type === VAR) throw new PrologError('instantiation_error');
1765
- const text = numberListText(list, env, kind, value.type === NUMBER);
1988
+ const text = numberListText(list, env, kind, value.type === NUMBER, solver);
1766
1989
  if (value.type === NUMBER) {
1767
1990
  if (text != null) {
1768
1991
  const parsed = parseIsoNumber(text);