eyeprolog 1.2.26 → 1.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/iso.js +47 -18
- package/src/term.js +19 -2
- package/src/write.js +36 -2
- package/test/run-regression.mjs +29 -0
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1144,6 +1144,43 @@ function convertedTermText(text, solver) {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
return result;
|
|
1146
1146
|
}
|
|
1147
|
+
|
|
1148
|
+
function scopeReadTerm(term) {
|
|
1149
|
+
// A term read from a stream has its own variable set (ISO 7.10.3). Parser
|
|
1150
|
+
// variable names cannot be used as environment identities here: a caller
|
|
1151
|
+
// such as read(X) and an input term X=a would otherwise share the same `X`
|
|
1152
|
+
// and incorrectly attempt the cyclic unification X=(X=a). Use an internal
|
|
1153
|
+
// name containing NUL, which cannot occur in Prolog source, while retaining
|
|
1154
|
+
// the spelling and occurrence count required by read_term/3 metadata.
|
|
1155
|
+
const scope = ++isoFresh;
|
|
1156
|
+
const bySourceName = new Map();
|
|
1157
|
+
const variables = [];
|
|
1158
|
+
|
|
1159
|
+
const copy = (item) => {
|
|
1160
|
+
if (item.type === VAR) {
|
|
1161
|
+
let record = bySourceName.get(item.name);
|
|
1162
|
+
if (record == null) {
|
|
1163
|
+
const scoped = variable(`\u0000read:${scope}:${variables.length}`);
|
|
1164
|
+
scoped.displayName = item.name;
|
|
1165
|
+
record = {
|
|
1166
|
+
sourceName: item.name,
|
|
1167
|
+
term: scoped,
|
|
1168
|
+
count: 0,
|
|
1169
|
+
anonymous: item.name.startsWith('__anon'),
|
|
1170
|
+
};
|
|
1171
|
+
bySourceName.set(item.name, record);
|
|
1172
|
+
variables.push(record);
|
|
1173
|
+
}
|
|
1174
|
+
record.count++;
|
|
1175
|
+
return record.term;
|
|
1176
|
+
}
|
|
1177
|
+
if (item.type !== COMPOUND) return item;
|
|
1178
|
+
return compound(item.name, item.args.map(copy));
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
return { term: copy(term), variables };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1147
1184
|
function readTermFromStream(stream, solver) {
|
|
1148
1185
|
let requestedInteractiveTerm = false;
|
|
1149
1186
|
while (true) {
|
|
@@ -1164,7 +1201,7 @@ function readTermFromStream(stream, solver) {
|
|
|
1164
1201
|
});
|
|
1165
1202
|
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1166
1203
|
stream.position = candidate.end;
|
|
1167
|
-
return clauses[0].head;
|
|
1204
|
+
return scopeReadTerm(clauses[0].head);
|
|
1168
1205
|
} catch (_) {
|
|
1169
1206
|
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1170
1207
|
// terminator. Keep scanning until a complete term parses.
|
|
@@ -1192,7 +1229,7 @@ function readTermFromStream(stream, solver) {
|
|
|
1192
1229
|
stream.position = source.length;
|
|
1193
1230
|
if (!sawCandidate) {
|
|
1194
1231
|
if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
|
|
1195
|
-
return atom('end_of_file');
|
|
1232
|
+
return { term: atom('end_of_file'), variables: [] };
|
|
1196
1233
|
}
|
|
1197
1234
|
throw new PrologError('syntax_error(read_term)');
|
|
1198
1235
|
}
|
|
@@ -1202,39 +1239,31 @@ function* readBuiltin({ solver, goal, env }) {
|
|
|
1202
1239
|
const stream = inputStreamFor(solver, goal, env);
|
|
1203
1240
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
|
1204
1241
|
const next = env.clone();
|
|
1205
|
-
|
|
1242
|
+
const { term } = readTermFromStream(stream, solver);
|
|
1243
|
+
if (unify(goal.args[goal.arity - 1], term, next)) yield next;
|
|
1206
1244
|
}
|
|
1207
1245
|
function* readTermBuiltin({ solver, goal, env }) {
|
|
1208
1246
|
const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
|
|
1209
1247
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
|
1210
1248
|
const options = optionList(goal.args[goal.arity - 1], env);
|
|
1211
1249
|
const target = goal.args[goal.arity - 2];
|
|
1212
|
-
const term = readTermFromStream(stream, solver);
|
|
1250
|
+
const { term, variables } = readTermFromStream(stream, solver);
|
|
1213
1251
|
const next = env.clone();
|
|
1214
1252
|
if (!unify(target, term, next)) return;
|
|
1215
|
-
const variables = [];
|
|
1216
|
-
const counts = new Map();
|
|
1217
|
-
const visit = (item) => {
|
|
1218
|
-
if (item.type === VAR) {
|
|
1219
|
-
counts.set(item.name, (counts.get(item.name) ?? 0) + 1);
|
|
1220
|
-
if (!variables.some((entry) => entry.name === item.name)) variables.push(item);
|
|
1221
|
-
} else for (const arg of item.args) visit(arg);
|
|
1222
|
-
};
|
|
1223
|
-
visit(term);
|
|
1224
1253
|
for (const option of options) {
|
|
1225
1254
|
if (option.type === VAR) throw new PrologError('instantiation_error');
|
|
1226
1255
|
if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(read_option)', option);
|
|
1227
1256
|
let value;
|
|
1228
1257
|
if (option.name === 'variables') {
|
|
1229
|
-
value = listFromItems(variables);
|
|
1258
|
+
value = listFromItems(variables.map((item) => item.term));
|
|
1230
1259
|
} else if (option.name === 'variable_names') {
|
|
1231
1260
|
value = listFromItems(variables
|
|
1232
|
-
.filter((item) => !item.
|
|
1233
|
-
.map((item) => compound('=', [atom(item.
|
|
1261
|
+
.filter((item) => !item.anonymous)
|
|
1262
|
+
.map((item) => compound('=', [atom(item.sourceName), item.term])));
|
|
1234
1263
|
} else if (option.name === 'singletons') {
|
|
1235
1264
|
value = listFromItems(variables
|
|
1236
|
-
.filter((item) => !item.
|
|
1237
|
-
.map((item) => compound('=', [atom(item.
|
|
1265
|
+
.filter((item) => !item.anonymous && item.count === 1)
|
|
1266
|
+
.map((item) => compound('=', [atom(item.sourceName), item.term])));
|
|
1238
1267
|
} else {
|
|
1239
1268
|
throw new PrologError('domain_error(read_option)', option);
|
|
1240
1269
|
}
|
package/src/term.js
CHANGED
|
@@ -413,9 +413,26 @@ function writeList(term, env, options) {
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
export function termToString(term, env = new Env(), quoteStrings = true, options = {}) {
|
|
416
|
-
options = {
|
|
416
|
+
options = {
|
|
417
|
+
...options,
|
|
418
|
+
doubleQuotes: options.doubleQuotes ?? 'chars',
|
|
419
|
+
readVariableNames: options.readVariableNames instanceof Map ? options.readVariableNames : new Map(),
|
|
420
|
+
usedReadVariableNames: options.usedReadVariableNames instanceof Set ? options.usedReadVariableNames : new Set(),
|
|
421
|
+
};
|
|
417
422
|
const resolved = deref(term, env);
|
|
418
|
-
if (resolved.type === VAR)
|
|
423
|
+
if (resolved.type === VAR) {
|
|
424
|
+
if (resolved.displayName == null) return writeVariable(resolved.name);
|
|
425
|
+
let printed = options.readVariableNames.get(resolved.name);
|
|
426
|
+
if (printed == null) {
|
|
427
|
+
const base = writeVariable(resolved.displayName);
|
|
428
|
+
printed = base;
|
|
429
|
+
let suffix = 1;
|
|
430
|
+
while (options.usedReadVariableNames.has(printed)) printed = `${base}_${suffix++}`;
|
|
431
|
+
options.readVariableNames.set(resolved.name, printed);
|
|
432
|
+
options.usedReadVariableNames.add(printed);
|
|
433
|
+
}
|
|
434
|
+
return printed;
|
|
435
|
+
}
|
|
419
436
|
if (isCons(resolved)) return writeList(resolved, env, options);
|
|
420
437
|
if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
|
|
421
438
|
if (resolved.type === ATOM) return writeAtom(resolved.name);
|
package/src/write.js
CHANGED
|
@@ -134,9 +134,42 @@ function chooseOperator(term, table) {
|
|
|
134
134
|
return null;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function printableReadVariableNames(term, env, explicit) {
|
|
138
|
+
const names = new Map(explicit);
|
|
139
|
+
const used = new Set(names.values());
|
|
140
|
+
const suffixes = new Map();
|
|
141
|
+
const seenVariables = new Set();
|
|
142
|
+
const seenTerms = new Set();
|
|
143
|
+
const stack = [term];
|
|
144
|
+
|
|
145
|
+
while (stack.length) {
|
|
146
|
+
const current = deref(stack.pop(), env);
|
|
147
|
+
if (current.type === VAR) {
|
|
148
|
+
if (seenVariables.has(current.name)) continue;
|
|
149
|
+
seenVariables.add(current.name);
|
|
150
|
+
if (names.has(current.name) || current.displayName == null) continue;
|
|
151
|
+
const base = writeVariable(current.displayName);
|
|
152
|
+
let candidate = base;
|
|
153
|
+
let suffix = suffixes.get(base) ?? 1;
|
|
154
|
+
while (used.has(candidate)) candidate = `${base}_${suffix++}`;
|
|
155
|
+
suffixes.set(base, suffix);
|
|
156
|
+
names.set(current.name, candidate);
|
|
157
|
+
used.add(candidate);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (current.type !== COMPOUND || seenTerms.has(current)) continue;
|
|
161
|
+
seenTerms.add(current);
|
|
162
|
+
for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return names;
|
|
166
|
+
}
|
|
167
|
+
|
|
137
168
|
function format(term, env, options, table, maxPriority = 1200, context = 'term') {
|
|
138
169
|
const resolved = deref(term, env);
|
|
139
|
-
if (resolved.type === VAR)
|
|
170
|
+
if (resolved.type === VAR) {
|
|
171
|
+
return options.variableNames.get(resolved.name) ?? writeVariable(resolved.displayName ?? resolved.name);
|
|
172
|
+
}
|
|
140
173
|
if (resolved.type === STRING) return writeString(resolved.name);
|
|
141
174
|
if (resolved.type === ATOM) {
|
|
142
175
|
if (!options.quoted) return resolved.name;
|
|
@@ -224,12 +257,13 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
|
|
|
224
257
|
}
|
|
225
258
|
|
|
226
259
|
export function formatTermForWrite(term, env = new Env(), options = {}) {
|
|
260
|
+
const explicitVariableNames = options.variableNames instanceof Map ? options.variableNames : new Map();
|
|
227
261
|
const normalized = {
|
|
228
262
|
quoted: options.quoted === true,
|
|
229
263
|
ignoreOps: options.ignoreOps === true,
|
|
230
264
|
numbervars: options.numbervars !== false,
|
|
231
265
|
doubleQuotes: options.doubleQuotes,
|
|
232
|
-
variableNames:
|
|
266
|
+
variableNames: printableReadVariableNames(term, env, explicitVariableNames),
|
|
233
267
|
compact: options.compact === true,
|
|
234
268
|
operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
|
|
235
269
|
};
|
package/test/run-regression.mjs
CHANGED
|
@@ -1284,6 +1284,35 @@ c4 ?- call((!;1)).
|
|
|
1284
1284
|
assertEqual(result.stderr, '', 'stderr');
|
|
1285
1285
|
},
|
|
1286
1286
|
},
|
|
1287
|
+
{
|
|
1288
|
+
name: 'read terms have a variable scope distinct from the calling query',
|
|
1289
|
+
run: () => {
|
|
1290
|
+
const result = runCli([], {
|
|
1291
|
+
input:
|
|
1292
|
+
'read(X).\n' +
|
|
1293
|
+
'X=a.\n' +
|
|
1294
|
+
'read(X).\n' +
|
|
1295
|
+
'Y=a.\n' +
|
|
1296
|
+
'read_term(X, [variables(Vs), variable_names(Names), singletons(Singletons)]).\n' +
|
|
1297
|
+
'X = pair(X, Y).\n' +
|
|
1298
|
+
'halt.\n',
|
|
1299
|
+
});
|
|
1300
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1301
|
+
assertEqual(result.stdout,
|
|
1302
|
+
'?- |: X = (_A = a).\n' +
|
|
1303
|
+
'?- |: X = (_A = a).\n' +
|
|
1304
|
+
"?- |: X = (_A = pair(_A, _B)), Vs = [_A, _B], Names = ['X' = _A, 'Y' = _B], Singletons = ['Y' = _B].\n" +
|
|
1305
|
+
'?- ',
|
|
1306
|
+
'stdout');
|
|
1307
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1308
|
+
|
|
1309
|
+
const distinctReads = run(
|
|
1310
|
+
'check :- read(A), read(B), A \\== B, write_canonical(pair(A, B)), nl.\n',
|
|
1311
|
+
{ goal: 'check', ioOptions: { input: 'V.\nV.\n' } },
|
|
1312
|
+
);
|
|
1313
|
+
assertEqual(distinctReads.stdout, 'pair(V,V_1)\ncheck.\n', 'separate read variable sets');
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1287
1316
|
{
|
|
1288
1317
|
name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
|
|
1289
1318
|
run: () => {
|