eyeprolog 1.3.44 → 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/README.md CHANGED
@@ -223,7 +223,12 @@ and `write_term/2-3` accept the Part 1 plus Corrigendum 3 options, while the
223
223
  normal-profile `write_term(...,[double_quotes(...)])` option remains an
224
224
  EyeProlog extension. The strict gate also checks the prescribed ISO error
225
225
  precedence for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3`, and
226
- the complete Part 1 flag defaults/value domains/changeability rules.
226
+ the complete Part 1 flag defaults/value domains/changeability rules. The
227
+ ongoing issue #65 audit additionally tightens Prolog-text declaration ordering,
228
+ initialization lifetime, stream/open/close behavior, character/byte stream
229
+ errors, `keysort/2` error cases, and strict arithmetic-extension boundaries.
230
+ Those additions improve the Part 1 surface without turning the remaining audit
231
+ rows into a blanket conformance claim.
227
232
 
228
233
  The auditable processor-requirement checklist lives in
229
234
  [`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.44",
6
+ "version": "1.3.45",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/io.js CHANGED
@@ -88,8 +88,13 @@ export class StreamManager {
88
88
  const raw = fs.readFileSync(path);
89
89
  if (type === 'binary') content = raw;
90
90
  else { content = decodeUtf8ForTextStream(raw); strictUtf8 = true; }
91
- } else if (mode === 'append' && fs.existsSync(path)) {
92
- content = fs.readFileSync(path, type === 'binary' ? null : 'utf8');
91
+ } else if (mode === 'write') {
92
+ // ISO 7.10.1.1: opening an existing sink in write mode empties it,
93
+ // while a missing sink is created at open time (not deferred to close).
94
+ fs.writeFileSync(path, type === 'binary' ? BufferCtor.from([]) : '');
95
+ } else if (mode === 'append') {
96
+ if (fs.existsSync(path)) content = fs.readFileSync(path, type === 'binary' ? null : 'utf8');
97
+ else fs.writeFileSync(path, type === 'binary' ? BufferCtor.from([]) : '');
93
98
  }
94
99
  if (BufferCtor?.isBuffer(content)) content = [...content];
95
100
  return this.add({
@@ -100,13 +105,18 @@ export class StreamManager {
100
105
  pastEnd: false,
101
106
  });
102
107
  }
103
- close(stream) {
104
- if (!stream.standard && stream.mode !== 'read') {
105
- fs.writeFileSync(stream.path, stream.type === 'binary' ? BufferCtor.from(stream.content) : stream.content);
106
- }
108
+ flush(stream) {
109
+ if (!stream || stream.standard || stream.mode === 'read') return;
110
+ fs.writeFileSync(stream.path, stream.type === 'binary' ? BufferCtor.from(stream.content) : stream.content);
111
+ }
112
+ discard(stream) {
107
113
  if (stream.alias) this.aliases.delete(stream.alias);
108
114
  this.streams.delete(stream.id);
109
115
  }
116
+ close(stream) {
117
+ this.flush(stream);
118
+ this.discard(stream);
119
+ }
110
120
  readUnit(stream, peek = false) {
111
121
  if (stream.position >= stream.content.length) return null;
112
122
  if (stream.type === 'binary') {
@@ -126,9 +136,22 @@ export class StreamManager {
126
136
  return value;
127
137
  }
128
138
  writeUnit(stream, value) {
129
- if (stream.standard && stream.write) stream.write(String(value));
130
- else if (stream.type === 'binary') stream.content.push(value);
131
- else stream.content += value;
132
- stream.position = stream.content.length;
139
+ if (stream.standard && stream.write) {
140
+ stream.write(String(value));
141
+ return;
142
+ }
143
+ // 7.10.2.8: output after repositioning overwrites the existing sink
144
+ // contents at the selected stream position rather than always appending.
145
+ if (stream.type === 'binary') {
146
+ if (stream.position < stream.content.length) stream.content[stream.position] = value;
147
+ else stream.content.push(value);
148
+ stream.position++;
149
+ return;
150
+ }
151
+ const text = String(value);
152
+ const content = String(stream.content);
153
+ const end = Math.min(content.length, stream.position + text.length);
154
+ stream.content = `${content.slice(0, stream.position)}${text}${content.slice(end)}`;
155
+ stream.position += text.length;
133
156
  }
134
157
  }
@@ -5,7 +5,7 @@ import {
5
5
  } from './term.js';
6
6
  import { PrologError } from './errors.js';
7
7
 
8
- function evaluate(term, env) {
8
+ function evaluate(term, env, options = {}) {
9
9
  term = deref(term, env);
10
10
  if (term.type === VAR) throw new PrologError('instantiation_error');
11
11
  if (term.type === NUMBER) {
@@ -16,10 +16,11 @@ function evaluate(term, env) {
16
16
  }
17
17
  if (term.type === ATOM) {
18
18
  if (term.name === 'pi') return { integer: false, value: Math.PI };
19
- if (term.name === 'e') return { integer: false, value: Math.E };
19
+ if (term.name === 'e' && options.isoStrict !== true) return { integer: false, value: Math.E };
20
+ throw new PrologError('type_error(evaluable)', compound('/', [atom(term.name), numberTerm(0)]));
20
21
  }
21
22
  if (term.type !== COMPOUND) throw new PrologError('type_error(evaluable)', term);
22
- const args = term.args.map((arg) => evaluate(arg, env));
23
+ const args = term.args.map((arg) => evaluate(arg, env, options));
23
24
  return evaluateOperation(term, args);
24
25
  }
25
26
  function evaluateOperation(term, args) {
@@ -120,8 +121,8 @@ export function arithmeticValueTerm(value) {
120
121
  function numericTerm(value) {
121
122
  return arithmeticValueTerm(value);
122
123
  }
123
- export function evaluateArithmetic(term, env) {
124
- return evaluate(term, env);
124
+ export function evaluateArithmetic(term, env, options = {}) {
125
+ return evaluate(term, env, options);
125
126
  }
126
127
  function compareIntegerToFloat(integerValue, floatValue) {
127
128
  if (!Number.isFinite(floatValue)) throw new PrologError('evaluation_error(float_overflow)');
@@ -151,15 +152,16 @@ export function compareArithmeticValues(left, right) {
151
152
  if (right.integer) return -compareIntegerToFloat(b, a);
152
153
  return a < b ? -1 : a > b ? 1 : 0;
153
154
  }
154
- export function* isBuiltin({ goal, env }) {
155
- const result = arithmeticValueTerm(evaluateArithmetic(goal.args[1], env));
155
+ export function* isBuiltin({ solver, goal, env }) {
156
+ const result = arithmeticValueTerm(evaluateArithmetic(goal.args[1], env, { isoStrict: solver?.isoStrict === true }));
156
157
  const next = env.clone();
157
158
  if (unify(goal.args[0], result, next)) yield next;
158
159
  }
159
160
  export function arithmeticComparison(test) {
160
- return function* ({ goal, env }) {
161
- const left = evaluateArithmetic(goal.args[0], env);
162
- const right = evaluateArithmetic(goal.args[1], env);
161
+ return function* ({ solver, goal, env }) {
162
+ const options = { isoStrict: solver?.isoStrict === true };
163
+ const left = evaluateArithmetic(goal.args[0], env, options);
164
+ const right = evaluateArithmetic(goal.args[1], env, options);
163
165
  const cmp = compareArithmeticValues(left, right);
164
166
  if (test(cmp)) yield env;
165
167
  };
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.
@@ -830,6 +838,16 @@ function streamReference(term, env) {
830
838
  throw new PrologError('domain_error(stream_or_alias)', value);
831
839
  }
832
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
+
833
851
  function requireStream(solver, term, env, mode = null) {
834
852
  const culprit = deref(term, env);
835
853
  const stream = solver.io.resolve(streamReference(term, env));
@@ -886,13 +904,14 @@ function openOptions(term, env) {
886
904
  if ((value = optionAtom(option, 'type'))) {
887
905
  value = deref(value, env);
888
906
  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);
907
+ if (value.type !== ATOM || !['text', 'binary'].includes(value.name)) {
908
+ throw new PrologError('domain_error(stream_option)', option);
909
+ }
891
910
  result.type = value.name;
892
911
  } else if ((value = optionAtom(option, 'alias'))) {
893
912
  value = deref(value, env);
894
913
  if (value.type === VAR) throw new PrologError('instantiation_error');
895
- if (value.type !== ATOM) throw new PrologError('type_error(atom)', value);
914
+ if (value.type !== ATOM) throw new PrologError('domain_error(stream_option)', option);
896
915
  result.alias = value.name;
897
916
  } else if ((value = optionAtom(option, 'reposition'))) {
898
917
  value = deref(value, env);
@@ -911,19 +930,45 @@ function openOptions(term, env) {
911
930
  return result;
912
931
  }
913
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
+
914
940
  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);
941
+ const source = deref(goal.args[0], env);
942
+ const mode = deref(goal.args[1], env);
918
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);
919
957
  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));
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
+ }
922
964
  let stream;
923
965
  try {
924
- stream = solver.io.open(path.name, mode.name, options);
925
- } catch (_) {
926
- 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);
927
972
  }
928
973
  const next = env.clone();
929
974
  if (unify(goal.args[2], streamHandle(stream.id), next)) yield next;
@@ -931,22 +976,35 @@ function* openBuiltin({ solver, goal, env }) {
931
976
  }
932
977
 
933
978
  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
- }
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);
943
995
  }
996
+ forceClose = value.name === 'true'; // rightmost contradictory option applies
944
997
  }
945
998
  const stream = requireStream(solver, goal.args[0], env);
946
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
+ }
947
1006
  if (solver.io.currentInput === stream.id) solver.io.currentInput = 0;
948
1007
  if (solver.io.currentOutput === stream.id) solver.io.currentOutput = 1;
949
- solver.io.close(stream);
950
1008
  }
951
1009
  yield env;
952
1010
  }
@@ -954,7 +1012,8 @@ function* closeBuiltin({ solver, goal, env }) {
954
1012
  function* currentInputBuiltin({ solver, goal, env }) {
955
1013
  const value = deref(goal.args[0], env);
956
1014
  if (value.type !== VAR) {
957
- 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);
958
1017
  if (!stream) throw new PrologError('domain_error(stream)', value);
959
1018
  if (stream.id === solver.io.currentInput) yield env;
960
1019
  return;
@@ -965,7 +1024,8 @@ function* currentInputBuiltin({ solver, goal, env }) {
965
1024
  function* currentOutputBuiltin({ solver, goal, env }) {
966
1025
  const value = deref(goal.args[0], env);
967
1026
  if (value.type !== VAR) {
968
- 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);
969
1029
  if (!stream) throw new PrologError('domain_error(stream)', value);
970
1030
  if (stream.id === solver.io.currentOutput) yield env;
971
1031
  return;
@@ -973,6 +1033,7 @@ function* currentOutputBuiltin({ solver, goal, env }) {
973
1033
  const next = env.clone();
974
1034
  if (unify(goal.args[0], streamHandle(solver.io.currentOutput), next)) yield next;
975
1035
  }
1036
+
976
1037
  function setCurrentStreamBuiltin(mode) {
977
1038
  return function* ({ solver, goal, env }) {
978
1039
  const stream = requireStream(solver, goal.args[0], env, mode);
@@ -982,7 +1043,14 @@ function setCurrentStreamBuiltin(mode) {
982
1043
  };
983
1044
  }
984
1045
  function* flushOutputBuiltin({ solver, goal, env }) {
985
- 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
+ }
986
1054
  yield env;
987
1055
  }
988
1056
 
@@ -1003,23 +1071,51 @@ function streamProperties(stream) {
1003
1071
  return properties;
1004
1072
  }
1005
1073
  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));
1074
+ const reference = deref(goal.args[0], env);
1075
+ if (reference.type === VAR) throw new PrologError('instantiation_error');
1008
1076
  let position = deref(goal.args[1], env);
1009
- 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.
1010
1083
  position = deref(position.args[0], env);
1011
1084
  }
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);
1085
+ if (position.type === VAR || position.type !== NUMBER || !isDecimalInteger(position.name)) {
1086
+ throw new PrologError('domain_error(stream_position)', position);
1087
+ }
1014
1088
  const offset = BigInt(position.name);
1015
- 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);
1016
1093
  stream.position = Number(offset);
1017
1094
  stream.pastEnd = false;
1018
1095
  yield env;
1019
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
+
1020
1106
  function* streamPropertyBuiltin({ solver, goal, env }) {
1021
1107
  const reference = deref(goal.args[0], env);
1022
- 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))];
1023
1119
  for (const stream of streams) {
1024
1120
  for (const property of streamProperties(stream)) {
1025
1121
  const next = env.clone();
@@ -1035,27 +1131,69 @@ function outputStreamFor(solver, goal, env) {
1035
1131
  return goal.arity === 1 ? solver.io.resolve(solver.io.currentOutput) : requireStream(solver, goal.args[0], env, 'write');
1036
1132
  }
1037
1133
  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;
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));
1040
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
+
1041
1174
  function inputUnitBuiltin(name) {
1042
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);
1043
1180
  const stream = inputStreamFor(solver, goal, env);
1044
1181
  const binary = name.endsWith('byte');
1045
- 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
+ }
1046
1188
  if (stream.pastEnd && stream.eofAction === 'error') {
1047
1189
  throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
1048
1190
  }
1049
1191
  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
1192
  if (typeof stream.interactiveReadUnit !== 'function') stream.position = 0;
1054
1193
  stream.pastEnd = false;
1055
1194
  }
1056
1195
  const peek = name.startsWith('peek');
1057
- if (stream.position >= stream.content.length &&
1058
- typeof stream.interactiveReadUnit === 'function') {
1196
+ if (stream.position >= stream.content.length && typeof stream.interactiveReadUnit === 'function') {
1059
1197
  const text = stream.interactiveReadUnit();
1060
1198
  if (text != null) {
1061
1199
  stream.content += String(text);
@@ -1066,50 +1204,68 @@ function inputUnitBuiltin(name) {
1066
1204
  try {
1067
1205
  unit = solver.io.readUnit(stream, peek);
1068
1206
  } catch (error) {
1069
- if (error?.name === 'InvalidCharacterEncodingError') {
1070
- throw new PrologError('representation_error(character)');
1071
- }
1207
+ if (error?.name === 'InvalidCharacterEncodingError') throw new PrologError('representation_error(character)');
1072
1208
  throw error;
1073
1209
  }
1074
1210
  if (unit == null && !peek) stream.pastEnd = true;
1075
1211
  if (!binary && unit != null && solver.isoStrict && !isStrictIsoPcsCharacter(unit)) {
1076
1212
  throw new PrologError('representation_error(character)');
1077
1213
  }
1078
- 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'))
1079
1216
  : binary ? numberTerm(unit) : name.endsWith('code') ? numberTerm(unit.codePointAt(0)) : atom(unit);
1080
- const target = goal.args[goal.arity - 1];
1081
1217
  const next = env.clone();
1082
1218
  if (unify(target, result, next)) yield next;
1083
1219
  };
1084
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
+
1085
1239
  function outputUnitBuiltin(name) {
1086
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);
1087
1244
  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
1245
  if (name === 'put_char') {
1091
1246
  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
1247
  if (solver.isoStrict && !isStrictIsoPcsCharacter(value.name)) {
1094
1248
  throw new PrologError('representation_error(character)', value);
1095
1249
  }
1096
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));
1097
1254
  } 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);
1255
+ if (stream.type !== 'text') throw new PrologError('permission_error(output, binary_stream)', streamHandle(stream.id));
1102
1256
  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))) {
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))) {
1106
1261
  throw new PrologError('representation_error(character_code)');
1107
1262
  }
1108
- solver.io.writeUnit(stream, name === 'put_byte' ? Number(code) : String.fromCodePoint(Number(code)));
1263
+ solver.io.writeUnit(stream, String.fromCodePoint(Number(code)));
1109
1264
  }
1110
1265
  yield env;
1111
1266
  };
1112
1267
  }
1268
+
1113
1269
  function* nlBuiltin({ solver, goal, env }) {
1114
1270
  const stream = goal.arity === 0
1115
1271
  ? solver.io.resolve(solver.io.currentOutput)
@@ -1274,6 +1430,13 @@ export function isCompleteReadTermText(text, solver) {
1274
1430
  }
1275
1431
 
1276
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
+ }
1277
1440
  let requestedInteractiveTerm = false;
1278
1441
  while (true) {
1279
1442
  let sawCandidate = false;
@@ -1317,6 +1480,7 @@ function readTermFromStream(stream, solver) {
1317
1480
  stream.position = source.length;
1318
1481
  if (!sawCandidate) {
1319
1482
  if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1483
+ stream.pastEnd = true;
1320
1484
  return { term: atom('end_of_file'), variables: [] };
1321
1485
  }
1322
1486
  throw new PrologError('syntax_error(read_term)');
@@ -1789,14 +1953,14 @@ function canonicalNumberText(value) {
1789
1953
  return text;
1790
1954
  }
1791
1955
 
1792
- function numberListText(list, env, kind, valueIsBound) {
1956
+ function numberListText(list, env, kind, valueIsBound, solver = null) {
1793
1957
  const whole = deref(list, env);
1794
1958
  const { items, tail } = listElements(list, env);
1795
1959
  const proper = tail.type === ATOM && tail.name === '[]';
1796
1960
  if (tail.type !== VAR && !proper) throw new PrologError('type_error(list)', whole);
1797
1961
 
1798
1962
  const invalid = items.find((item) => item.type !== VAR &&
1799
- (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item)));
1963
+ (kind === 'chars' ? !oneChar(item) : !validCharacterCode(item, solver)));
1800
1964
  if (invalid) {
1801
1965
  if (kind === 'chars') throw new PrologError('type_error(character)', invalid);
1802
1966
  if (invalid.type !== NUMBER || !isDecimalInteger(invalid.name)) {
@@ -1816,12 +1980,12 @@ function numberListText(list, env, kind, valueIsBound) {
1816
1980
  const numberSyntaxError = new PrologError('syntax_error(number)');
1817
1981
 
1818
1982
  function numberListBuiltin(kind) {
1819
- return function* ({ goal, env }) {
1983
+ return function* ({ solver, goal, env }) {
1820
1984
  const value = deref(goal.args[0], env);
1821
1985
  if (value.type !== VAR && value.type !== NUMBER) throw new PrologError('type_error(number)', value);
1822
1986
  const list = deref(goal.args[1], env);
1823
1987
  if (value.type === VAR && list.type === VAR) throw new PrologError('instantiation_error');
1824
- const text = numberListText(list, env, kind, value.type === NUMBER);
1988
+ const text = numberListText(list, env, kind, value.type === NUMBER, solver);
1825
1989
  if (value.type === NUMBER) {
1826
1990
  if (text != null) {
1827
1991
  const parsed = parseIsoNumber(text);
package/src/program.js CHANGED
@@ -44,6 +44,8 @@ export class Program {
44
44
  this.libraryImports = [];
45
45
  this.interopPortabilityWarnings = [];
46
46
  this.dynamicPredicates = new Set();
47
+ this.multifilePredicates = new Set();
48
+ this.discontiguousPredicates = new Set();
47
49
  this.strictIso = options.isoStrict === true;
48
50
  this.operators = new Map();
49
51
  const predefinedOperatorSets = this.strictIso
@@ -55,6 +57,7 @@ export class Program {
55
57
  }
56
58
  }
57
59
  this.initializations = [];
60
+ this._initializationsExecuted = false;
58
61
  this.quads = [];
59
62
  this.prologFlagDirectives = [];
60
63
  this.charConversionDirectives = [];
@@ -476,11 +479,94 @@ class ProgramBuilder {
476
479
  this.options = options;
477
480
  this.program = program ?? new Program([], { ...options, [DEFER_PROGRAM_BUILD]: true });
478
481
  this.declaredDynamicIndicators = new Map();
482
+ this.declaredMultifileIndicators = new Map();
483
+ this.declaredDiscontiguousIndicators = new Map();
484
+ this.directiveDeclarationsByText = new Map();
485
+ this.clauseTextUnitsByKey = new Map();
486
+ this.lastPredicateByText = new Map();
479
487
  this.lastGroupKey = null;
480
488
  this.lastGroup = null;
481
489
  this.finished = false;
482
490
  }
483
491
 
492
+ declarationSet(textUnit, kind) {
493
+ const unit = textUnit ?? '<input>';
494
+ let declarations = this.directiveDeclarationsByText.get(unit);
495
+ if (!declarations) {
496
+ declarations = { dynamic: new Set(), multifile: new Set(), discontiguous: new Set() };
497
+ this.directiveDeclarationsByText.set(unit, declarations);
498
+ }
499
+ return declarations[kind];
500
+ }
501
+
502
+ noteProcedureClause(key, textUnit) {
503
+ if (!this.program.strictIso) return;
504
+ const unit = textUnit ?? '<input>';
505
+ const declarations = this.directiveDeclarationsByText.get(unit);
506
+ const units = this.clauseTextUnitsByKey.get(key) ?? new Set();
507
+
508
+ if (this.program.dynamicPredicates.has(key) && !declarations?.dynamic.has(key)) {
509
+ throw new Error(`ISO preparation error: dynamic(${key}) must precede clauses for ${key} in every Prolog text`);
510
+ }
511
+
512
+ if (units.size > 0 && !units.has(unit)) {
513
+ if (!declarations?.multifile.has(key)) {
514
+ throw new Error(`ISO preparation error: multifile(${key}) is required before clauses for ${key} in this Prolog text`);
515
+ }
516
+ for (const prior of units) {
517
+ if (!this.directiveDeclarationsByText.get(prior)?.multifile.has(key)) {
518
+ throw new Error(`ISO preparation error: multifile(${key}) is required in every Prolog text containing clauses for ${key}`);
519
+ }
520
+ }
521
+ }
522
+
523
+ const last = this.lastPredicateByText.get(unit);
524
+ if (last != null && last !== key && units.has(unit) && !declarations?.discontiguous.has(key)) {
525
+ throw new Error(`ISO preparation error: discontiguous(${key}) must precede non-consecutive clauses for ${key}`);
526
+ }
527
+ units.add(unit);
528
+ this.clauseTextUnitsByKey.set(key, units);
529
+ this.lastPredicateByText.set(unit, key);
530
+ }
531
+
532
+ addProcedureDirective(clause, kind, targetSet, declaredMap) {
533
+ const program = this.program;
534
+ const indicators = procedureDirectiveIndicators(clause, kind, program.strictIso);
535
+ if (indicators == null) return false;
536
+ const textUnit = clause.textUnit ?? '<input>';
537
+ const local = this.declarationSet(textUnit, kind);
538
+ for (const indicator of indicators) {
539
+ assertDynamicIndicatorIsDefinable(indicator, program.strictIso);
540
+ const module = clause.module ?? 'user';
541
+ const key = modulePredicateKey(module, indicator.name, indicator.arity);
542
+ if (program.strictIso && this.clauseTextUnitsByKey.get(key)?.has(textUnit)) {
543
+ throw new Error(`ISO preparation error: ${kind}(${indicator.key}) must precede all clauses for ${indicator.key}`);
544
+ }
545
+ if (program.strictIso && kind === 'dynamic') {
546
+ for (const prior of this.clauseTextUnitsByKey.get(key) ?? []) {
547
+ if (prior !== textUnit && !this.directiveDeclarationsByText.get(prior)?.dynamic.has(key)) {
548
+ throw new Error(`ISO preparation error: dynamic(${indicator.key}) is required in every Prolog text containing clauses for ${indicator.key}`);
549
+ }
550
+ }
551
+ }
552
+ if (program.strictIso && kind === 'multifile') {
553
+ for (const prior of this.clauseTextUnitsByKey.get(key) ?? []) {
554
+ if (prior !== textUnit && !this.directiveDeclarationsByText.get(prior)?.multifile.has(key)) {
555
+ throw new Error(`ISO preparation error: multifile(${indicator.key}) is required in every Prolog text containing clauses for ${indicator.key}`);
556
+ }
557
+ }
558
+ }
559
+ local.add(key);
560
+ targetSet.add(key);
561
+ declaredMap.set(`${textUnit}\u0000${key}`, { ...indicator, key, module, textUnit });
562
+ if (kind === 'dynamic') {
563
+ const existing = program.groups.get(key);
564
+ if (existing) existing.dynamic = true;
565
+ }
566
+ }
567
+ return true;
568
+ }
569
+
484
570
  addClauses(clauses) {
485
571
  if (this.finished) throw new Error('program builder is already finalized');
486
572
  const program = this.program;
@@ -501,6 +587,7 @@ class ProgramBuilder {
501
587
  assertPredicateIsDefinable(clause.headName, 2, program.strictIso);
502
588
  const module = clause.module ?? 'user';
503
589
  const key = modulePredicateKey(module, clause.headName, 2);
590
+ this.noteProcedureClause(key, clause.textUnit);
504
591
  let group = key === lastGroupKey ? lastGroup : program.groups.get(key);
505
592
  if (!group) {
506
593
  group = program.makeGroup(clause.headName, 2, module);
@@ -524,6 +611,7 @@ class ProgramBuilder {
524
611
  if (head.type !== ATOM && head.type !== COMPOUND) continue;
525
612
  const module = clause.module ?? 'user';
526
613
  const key = modulePredicateKey(module, head.name, head.arity);
614
+ this.noteProcedureClause(key, clause.textUnit);
527
615
  let group = key === lastGroupKey ? lastGroup : program.groups.get(key);
528
616
  if (!group) {
529
617
  group = program.makeGroup(head.name, head.arity, module);
@@ -553,14 +641,10 @@ class ProgramBuilder {
553
641
  addDirectiveClause(clause) {
554
642
  const program = this.program;
555
643
  const module = clause.module ?? 'user';
556
- for (const indicator of dynamicDirectiveIndicators(clause)) {
557
- assertDynamicIndicatorIsDefinable(indicator, program.strictIso);
558
- const key = modulePredicateKey(module, indicator.name, indicator.arity);
559
- program.dynamicPredicates.add(key);
560
- this.declaredDynamicIndicators.set(key, { ...indicator, key, module });
561
- const existing = program.groups.get(key);
562
- if (existing) existing.dynamic = true;
563
- }
644
+ const textUnit = clause.textUnit ?? '<input>';
645
+ this.addProcedureDirective(clause, 'dynamic', program.dynamicPredicates, this.declaredDynamicIndicators);
646
+ this.addProcedureDirective(clause, 'multifile', program.multifilePredicates, this.declaredMultifileIndicators);
647
+ this.addProcedureDirective(clause, 'discontiguous', program.discontiguousPredicates, this.declaredDiscontiguousIndicators);
564
648
 
565
649
  const operator = operatorDirective(clause);
566
650
  if (operator) {
@@ -586,6 +670,7 @@ class ProgramBuilder {
586
670
  } else if (directive?.type === COMPOUND && directive.name === 'char_conversion' && directive.arity === 2) {
587
671
  program.charConversionDirectives.push(directive.args);
588
672
  }
673
+ if (program.strictIso) this.lastPredicateByText.set(textUnit, '@directive');
589
674
  }
590
675
 
591
676
  finish() {
@@ -596,9 +681,13 @@ class ProgramBuilder {
596
681
  // A dynamic declaration creates a procedure even when it has no clauses.
597
682
  // Calls to that procedure fail normally instead of being treated as calls
598
683
  // to an unknown predicate.
599
- for (const indicator of this.declaredDynamicIndicators.values()) {
600
- if (!program.groups.has(indicator.key)) {
601
- program.groups.set(indicator.key, program.makeGroup(indicator.name, indicator.arity, indicator.module));
684
+ for (const declarations of [
685
+ this.declaredDynamicIndicators, this.declaredMultifileIndicators, this.declaredDiscontiguousIndicators,
686
+ ]) {
687
+ for (const indicator of declarations.values()) {
688
+ if (!program.groups.has(indicator.key)) {
689
+ program.groups.set(indicator.key, program.makeGroup(indicator.name, indicator.arity, indicator.module));
690
+ }
602
691
  }
603
692
  }
604
693
  program.mutable = program.dynamicPredicates.size > 0;
@@ -694,11 +783,14 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
694
783
  if (filename) ensured.add(filename);
695
784
  }
696
785
  try {
697
- for (const item of prepared) {
786
+ for (const [sourceIndex, item] of prepared.entries()) {
698
787
  const text = typeof item.source === 'string'
699
788
  ? item.source
700
789
  : item.source?.text ?? item.source?.source ?? '';
701
- const context = { module: 'user' };
790
+ const context = {
791
+ module: 'user',
792
+ textUnit: sourcePath(item.options) ?? `<input:${sourceIndex}>`,
793
+ };
702
794
  if (!loadSourceIntoBuilder(builder, text, item.options, ensured, loadedModules, fast, context)) return false;
703
795
  }
704
796
  const autoloadGoals = parseInteropGoalInputs(options.autoloadGoals, {
@@ -964,6 +1056,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
964
1056
  if (moduleDeclaration) {
965
1057
  flush();
966
1058
  clause.module = moduleDeclaration.name;
1059
+ clause.textUnit = context.textUnit;
967
1060
  clause.moduleFilename = options.filename ?? '<input>';
968
1061
  builder.addClauses([clause]);
969
1062
  context.module = moduleDeclaration.name;
@@ -974,6 +1067,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
974
1067
  if (use) {
975
1068
  flush();
976
1069
  clause.module = context.module;
1070
+ clause.textUnit = context.textUnit;
977
1071
  builder.addClauses([clause]);
978
1072
  const libraryName = libraryDesignationName(use.designation);
979
1073
  if (libraryName != null) {
@@ -996,6 +1090,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
996
1090
  const include = includeDirective(clause);
997
1091
  if (!include) {
998
1092
  clause.module ??= context.module;
1093
+ clause.textUnit ??= context.textUnit;
999
1094
  if (!isDirectiveClause(clause)) {
1000
1095
  for (const goal of clause.body) annotateGoalModule(goal, clause.module);
1001
1096
  }
@@ -1004,9 +1099,15 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1004
1099
  return;
1005
1100
  }
1006
1101
  flush();
1102
+ if (builder.program.strictIso && include.name === 'ensure_loaded') {
1103
+ builder.lastPredicateByText.set(context.textUnit ?? '<input>', '@directive');
1104
+ }
1007
1105
  const child = readIncludedSource(include, options, ensured);
1008
1106
  if (!child) return;
1009
- if (!loadSourceIntoBuilder(builder, child.text, child.options, ensured, loadedModules, fast, context)) {
1107
+ const childContext = include.name === 'include'
1108
+ ? context
1109
+ : { module: context.module, textUnit: sourcePath(child.options) ?? context.textUnit };
1110
+ if (!loadSourceIntoBuilder(builder, child.text, child.options, ensured, loadedModules, fast, childContext)) {
1010
1111
  throw FAST_PARSE_ABORT;
1011
1112
  }
1012
1113
  };
@@ -1019,6 +1120,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
1019
1120
  bodyName, body0Type, body0Name, body1Type, body1Name,
1020
1121
  ));
1021
1122
  batch[batch.length - 1].module = context.module;
1123
+ batch[batch.length - 1].textUnit = context.textUnit;
1022
1124
  if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
1023
1125
  };
1024
1126
  const parsed = tryParseClausesFastInto(source, accept, acceptBinary, options);
@@ -1148,16 +1250,25 @@ function isDirectiveClause(clause) {
1148
1250
  clause.head.name === ':-' && clause.head.arity === 1;
1149
1251
  }
1150
1252
 
1151
- function dynamicDirectiveIndicators(clause) {
1152
- if (!isDirectiveClause(clause)) return [];
1253
+ function procedureDirectiveIndicators(clause, name, strictIso = false) {
1254
+ if (!isDirectiveClause(clause)) return null;
1153
1255
  const directive = clause.head.args[0];
1154
- if (directive.type !== COMPOUND || directive.name !== 'dynamic' || directive.arity !== 1) return [];
1256
+ if (directive.type !== COMPOUND || directive.name !== name || directive.arity !== 1) return null;
1155
1257
  const terms = properListItems(directive.args[0], new Env()) ?? flattenDirectiveSequence(directive.args[0]);
1156
- return terms.map((indicator) =>
1258
+ const indicators = terms.map((indicator) =>
1157
1259
  indicator.type === COMPOUND && ['/', '//'].includes(indicator.name) && indicator.arity === 2
1158
1260
  ? nonterminalOrPredicateIndicator(indicator)
1159
1261
  : null
1160
- ).filter(Boolean);
1262
+ );
1263
+ if (indicators.some((indicator) => indicator == null)) {
1264
+ if (strictIso) throw new Error(`ISO preparation error: invalid ${name}/1 predicate indicator`);
1265
+ return [];
1266
+ }
1267
+ return indicators;
1268
+ }
1269
+
1270
+ function dynamicDirectiveIndicators(clause) {
1271
+ return procedureDirectiveIndicators(clause, 'dynamic', false) ?? [];
1161
1272
  }
1162
1273
 
1163
1274
  function nonterminalOrPredicateIndicator(term) {
package/src/solver.js CHANGED
@@ -283,6 +283,7 @@ export class Solver {
283
283
  }
284
284
 
285
285
  runInitializations() {
286
+ if (this.program._initializationsExecuted) return;
286
287
  for (const goal of this.program.initializations ?? []) {
287
288
  let succeeded = false;
288
289
  for (const _ of this.solve([goal], new Env(), 0)) {
@@ -291,6 +292,9 @@ export class Solver {
291
292
  }
292
293
  if (!succeeded) throw new PrologError('initialization_error');
293
294
  }
295
+ // 7.4.2.6 attaches initialization to preparation of the Prolog text, not
296
+ // to each later Solver instance created for the same prepared Program.
297
+ this.program._initializationsExecuted = true;
294
298
  }
295
299
 
296
300
  *solve(goals, env = new Env(), depth = 0) {
@@ -32,16 +32,16 @@ error-ordering alternative to an individual executable assertion.
32
32
  | --- | --- | --- |
33
33
  | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete vendored WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, writer/read-back regressions, and strict ASCII PCS/collation boundary tests. The implementation-defined 6.5/6.6 character-model decisions are now closed; wider shall-by-shall lexical mapping remains open. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates, plus strict checks for the required `variable < float < integer < atom < compound` type order and PCS-based atom collation. |
35
- | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Preparation-time `char_conversion/2` now affects later unquoted source text and respects `char_conversion=off`. Cross-text `multifile/1` and ordering constraints still require explicit shall-by-shall audit. |
35
+ | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Preparation-time `char_conversion/2` affects later unquoted source text and respects `char_conversion=off`. Strict preparation now enforces declaration-before-clause ordering for `dynamic/1`, `multifile/1`, and `discontiguous/1`, cross-text `multifile/1`, discontiguous clause grouping, empty declared procedures, include textual-replacement behavior, and one-time initialization per prepared program. The remaining shall-by-shall mapping is still open. |
36
36
  | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
37
37
  | 7.7 — execution and backtracking | audit | Control/search suites. Strict mode disables EyeProlog automatic tabling, cycle guards, and recursive numeric shortcuts so core execution uses ordinary clause selection/backtracking. |
38
38
  | 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction (including failed branches after callee-local cuts), if-then, catch/throw, renamed-copy tests. |
39
39
  | 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Exceptional-value/error-precedence matrix remains to be exhaustively enumerated. |
40
- | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, and Corrigendum 3 writer cases. The Part 1/Corrigendum 3 `read_term/2-3` and `write_term/2-3` option surfaces and ISO 8.14.1-8.14.2 prescribed error ordering now have dedicated strict tests; the wider stream/open/close option cross-product remains open. |
40
+ | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, and Corrigendum 3 writer cases. The audit now also covers write-mode creation/truncation, append creation, repositioned overwrite, flushing, EOF actions, close/current-stream handling, strict stream-position terms, stream-property validation, stream-term requirements, and text-vs-binary permission errors. The full option/mode cross-product is still being mapped. |
41
41
  | 7.11 — flags | covered | The complete Part 1 flag set, selected defaults, standard value domains, changeability, `current_prolog_flag/2`, and `set_prolog_flag/2` error behavior have dedicated strict tests. EyeProlog selects `bounded=false` and `integer_rounding_function=toward_zero`; valid alternative values of those fixed flags reach `permission_error(modify,flag,...)`, while `max_integer` and `min_integer` have no current value. Strict mode excludes the EyeProlog `occurs_check` extension. |
42
- | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Exact ISO 8.14.1-8.14.4 error precedence is now covered for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3`; prescribed ordering for the remaining built-in families is still being mapped. |
43
- | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. The 8.14 term-I/O/operator subfamily now has dedicated strict option-surface and prescribed-error-order assertions, and 8.17 flag behavior is closed; the remaining mode/error matrix is not yet one-row-per-standard-row. |
44
- | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Host floating-point representation choices remain documented implementation-defined behavior. |
42
+ | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Exact ISO 8.14.1-8.14.4 error precedence is covered for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3`; the continuing audit has corrected additional 8.11-8.13 stream/character/byte errors, Corrigendum 2 `keysort/2` errors, and arithmetic culprit reporting. A complete one-row-per-prescribed-error ordering map remains open. |
43
+ | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. The audit now includes corrected `keysort/2` variable/non-pair errors, stricter 8.11-8.13 stream and character/byte modes/errors, the 8.14 option/error-order subfamily, and closed 8.17 flags. The remaining mode/error matrix is not yet one-row-per-standard-row. |
44
+ | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Strict mode excludes the EyeProlog-only evaluable atom `e`, retains Corrigendum 2 arithmetic additions, and reports unknown zero-arity evaluables with the required `F/0` culprit shape. Host floating-point representation choices remain documented implementation-defined behavior; exhaustive exceptional-value precedence remains open. |
45
45
  | Corrigendum 1 | covered | Double-quoted atom/operator-priority corrections have dedicated cases. |
46
46
  | Corrigendum 2 | covered | Added predicates/functors, catch corrections, bar/operator and uninstantiation corrections have dedicated cases. |
47
47
  | Corrigendum 3 | covered | Writer options, `variable_names/1`, canonical list output and negative-power corrections have dedicated cases. |
@@ -81,6 +81,20 @@ the Part 1 plus Corrigendum 3 option surface; the normal-profile
81
81
  `double_quotes/1` write option remains an explicitly documented EyeProlog
82
82
  extension.
83
83
 
84
+
85
+ The current continuation of issue #65 expands that audit into Prolog-text,
86
+ stream, atomic-conversion, sorting, and arithmetic edge cases. Strict
87
+ preparation now enforces the Part 1 declaration constraints for `dynamic/1`,
88
+ `multifile/1`, and `discontiguous/1`, including cross-text multifile use and
89
+ one-time initialization per prepared program. Stream handling now distinguishes
90
+ text and binary permission errors, validates stream properties and stream-term
91
+ requirements, and tightens creation, truncation/append, repositioning, flush,
92
+ EOF, close, and current-stream behavior. Corrigendum 2 `keysort/2` variable and
93
+ non-pair errors are corrected. Strict arithmetic no longer exposes the
94
+ EyeProlog-only evaluable atom `e`, while the Corrigendum arithmetic additions
95
+ remain available. These corrections narrow the remaining `audit` rows but do
96
+ not by themselves close the full shall-by-shall/error-order exit criteria.
97
+
84
98
  ## Strict-core boundary
85
99
 
86
100
  `isoStrict: true` is intentionally a **Part 1 + Corrigenda 1-3** mode. It does
@@ -36,7 +36,7 @@ Status values are:
36
36
  | 7.1.4.1 | Set `C` of characters represented by one-char atoms | In strict mode `C` is exactly the ASCII PCS U+0000..U+007F. Normal mode extends character predicates to Unicode scalar values U+0000..U+10FFFF excluding surrogates. | **defined** — `src/iso-character.js`, `src/iso.js` character-code validation. |
37
37
  | 7.4.2.4 | Whether `op/3` directives affect other Prolog texts or execution | An `op/3` directive changes parsing of subsequent text loaded into the same `Program`; the resulting operator table is also used by execution-time term I/O. Separately created `Program` objects are independent. | **defined** — `src/parser.js`, `src/program.js`, `src/iso.js`. |
38
38
  | 7.4.2.5 | Whether directive-created `Convc` affects other text/execution | Yes. A `char_conversion/2` directive updates preparation-time conversion for later unquoted source characters and the recorded mapping initializes execution-time term input. Quoted characters are not converted; `char_conversion=off` disables following preparation-time conversion. | **defined** — `src/parser.js`, `src/program.js`, `src/solver.js`; strict-core regression coverage. |
39
- | 7.4.2.6 | Order of `initialization/1` goals | Initialization goals run once, in source/inclusion order, before requested goals; each must obtain a first solution. | **defined** — `Program.initializations`, `Solver.runInitializations()`. |
39
+ | 7.4.2.6 | Order of `initialization/1` goals | Initialization goals run once per prepared `Program`, in source/inclusion order, before requested goals; each must obtain a first solution. Reusing the same prepared program in another solver does not execute its initialization goals again. | **defined** — `Program.initializations`, preparation state, `Solver.runInitializations()`. |
40
40
  | 7.4.2.7 | Ground term designating a Prolog text for `include/1` | A source designation is an atom interpreted as a file path; relative paths resolve from the including file's directory (or the current working directory for unanchored input). | **defined** — `src/program.js` (`readIncludedSource`). |
41
41
  | 7.4.2.8 | Ground term designating a Prolog text for `ensure_loaded/1` | Same atom/file-path designation as `include/1`. | **defined** — `src/program.js`. |
42
42
  | 7.4.2.8 | Position at which an ensured text is included | The first `ensure_loaded/1` expands the text in place at the directive; later requests for the same resolved path are ignored. | **defined** — `src/program.js` (`ensured` set and in-place builder loading). |
@@ -11,14 +11,14 @@ compliance audit and the remaining work before a full conformance claim.
11
11
  | Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments, strict ASCII PCS/collation | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, `wg17_syntax_high_risk`, `wg17_invalid_octal_escape`, `wg17_unterminated_quoted_token`, `wg17_literal_newline_in_quote`, `wg17_non_iso_escape`, strict PCS/collation tests in `run-iso-strict.mjs`, syntax error cases |
12
12
  | Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
13
13
  | Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
14
- | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases |
14
+ | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases, including Corrigendum 2 `keysort/2` variable/non-pair errors |
15
15
  | 8.6-8.7 arithmetic predicates | `is/2` and all six arithmetic comparisons | `arithmetic`, comparison cases, isolated evaluation errors |
16
16
  | 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses` |
17
- | 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases, plus strict 8.14.1-8.14.4 option-surface/error-precedence tests for `read_term/3`, `write_term/3`, `op/3`, and `current_op/3` |
17
+ | 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases, strict 8.14.1-8.14.4 option/error-order tests, and issue #65 checks for stream-term/property validation, text-vs-binary permissions, EOF/flush/position, and open/close semantics |
18
18
  | 8.15 logic and control | negation, once, repeat, `call/2` through `call/8`, `false/0` | `logtalk_once`, `corrigenda_call_closure`, `false_builtin` |
19
19
  | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, parenthesized-number rejection, Logtalk-derived error cases |
20
20
  | 8.17 flags and hooks | complete required Part 1 flag set, selected defaults, standard value domains/changeability, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases, and the strict complete-flag audit in `run-iso-strict.mjs` |
21
- | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power` |
21
+ | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power`, strict exclusion of the normal-mode `e` evaluable extension and zero-arity unknown-evaluable error checks |
22
22
  | Module compatibility profile (related to ISO/IEC 13211-2 and later WG17 amendment work) | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
23
23
  | Part 3-oriented DCG compatibility profile | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
24
24
 
@@ -1 +1 @@
1
- error(permission_error(input, stream), '$stream'(0))
1
+ error(permission_error(input, text_stream), '$stream'(0))
@@ -7653,7 +7653,13 @@ integrity relations, automatic tabling, and the EyeProlog library. The strict au
7653
7653
  set and the prescribed 8.14 error precedence for `read_term/3`, `write_term/3`,
7654
7654
  `op/3`, and `current_op/3`; these checks distinguish invalid values from
7655
7655
  valid-but-fixed flag values and reject normal-profile write options at the
7656
- strict boundary. The remaining qualifications are:
7656
+ strict boundary. The continuing issue #65 audit also enforces Part 1
7657
+ preparation constraints for `dynamic/1`, `multifile/1`, and `discontiguous/1`,
7658
+ keeps initialization tied to preparation of a program, tightens stream
7659
+ creation/position/EOF behavior and text-vs-binary permission errors, corrects
7660
+ Corrigendum 2 `keysort/2` error cases, and keeps the non-standard evaluable atom
7661
+ `e` outside strict mode. These are conformance improvements, while the
7662
+ remaining qualifications are:
7657
7663
 
7658
7664
  - zero-arity compound syntax such as `ready()` is represented by the atom
7659
7665
  `ready`;