eyeprolog 1.5.73 → 1.5.76
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 +16 -8
- package/src/repl.js +16 -4
- package/test/regression/cases-regression.mjs +30 -0
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1153,24 +1153,32 @@ function streamHandle(id) {
|
|
|
1153
1153
|
return compound('$stream', [numberTerm(id)]);
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
|
+
function streamHandleId(value, env) {
|
|
1157
|
+
// A `$stream(Id)` term is an ordinary compound, not an opaque blob, so
|
|
1158
|
+
// general term-construction predicates such as functor/3 and arg/3 can
|
|
1159
|
+
// rebuild one whose argument is a variable that only later gets unified
|
|
1160
|
+
// with the real stream number (issue #109). Deref that argument, the same
|
|
1161
|
+
// as any other compound argument, instead of inspecting the raw arg term
|
|
1162
|
+
// and rejecting an equally valid stream reference as malformed.
|
|
1163
|
+
if (value.type !== COMPOUND || value.name !== '$stream' || value.arity !== 1) return null;
|
|
1164
|
+
const id = deref(value.args[0], env);
|
|
1165
|
+
return id.type === NUMBER && isDecimalInteger(id.name) ? Number(id.name) : null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1156
1168
|
function streamReference(term, env) {
|
|
1157
1169
|
const value = deref(term, env);
|
|
1158
1170
|
if (value.type === VAR) throw new PrologError('instantiation_error');
|
|
1159
1171
|
if (value.type === ATOM) return value.name;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
return Number(value.args[0].name);
|
|
1163
|
-
}
|
|
1172
|
+
const id = streamHandleId(value, env);
|
|
1173
|
+
if (id != null) return id;
|
|
1164
1174
|
throw new PrologError('domain_error(stream_or_alias)', value);
|
|
1165
1175
|
}
|
|
1166
1176
|
|
|
1167
1177
|
function streamTermReference(term, env) {
|
|
1168
1178
|
const value = deref(term, env);
|
|
1169
1179
|
if (value.type === VAR) return null;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
return Number(value.args[0].name);
|
|
1173
|
-
}
|
|
1180
|
+
const id = streamHandleId(value, env);
|
|
1181
|
+
if (id != null) return id;
|
|
1174
1182
|
throw new PrologError('domain_error(stream)', value);
|
|
1175
1183
|
}
|
|
1176
1184
|
|
package/src/repl.js
CHANGED
|
@@ -773,6 +773,18 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
773
773
|
doubleBar: !state.strictIso,
|
|
774
774
|
};
|
|
775
775
|
let generated = 0;
|
|
776
|
+
// Names generated for otherwise-anonymous variables (`_A`, `_B`, ...) must
|
|
777
|
+
// never collide with a name the query itself is already using for a
|
|
778
|
+
// *different* variable (e.g. a query literally naming `_A`) — printing an
|
|
779
|
+
// internal fresh variable as `_A` right next to the user's own `_A` binding
|
|
780
|
+
// falsely suggests they are the same variable (issue #108).
|
|
781
|
+
const usedDisplayNames = new Set(names.values());
|
|
782
|
+
const nextGeneratedName = () => {
|
|
783
|
+
let candidate;
|
|
784
|
+
do { candidate = `_${letterName(generated++)}`; } while (usedDisplayNames.has(candidate));
|
|
785
|
+
usedDisplayNames.add(candidate);
|
|
786
|
+
return candidate;
|
|
787
|
+
};
|
|
776
788
|
|
|
777
789
|
// Meta-predicate wrappers can make a query variable alias an internal fresh
|
|
778
790
|
// variable. Keep residual constraints tied to the query's visible name.
|
|
@@ -780,7 +792,7 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
780
792
|
const root = engine.deref(variable, env);
|
|
781
793
|
if (root.type === 'var' && !names.has(root.name)) names.set(root.name, variable.name);
|
|
782
794
|
}
|
|
783
|
-
for (const variable of variables) collectUnboundVariables(engine, variable, env, names,
|
|
795
|
+
for (const variable of variables) collectUnboundVariables(engine, variable, env, names, nextGeneratedName);
|
|
784
796
|
for (const variable of variables) {
|
|
785
797
|
const value = engine.deref(variable, env);
|
|
786
798
|
if (value.type === 'var' &&
|
|
@@ -796,7 +808,7 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
796
808
|
for (const constraint of env.variableConstraints?.() ?? []) {
|
|
797
809
|
const residual = constraint.residualGoal?.(env);
|
|
798
810
|
if (residual == null) continue;
|
|
799
|
-
collectUnboundVariables(engine, residual, env, names,
|
|
811
|
+
collectUnboundVariables(engine, residual, env, names, nextGeneratedName);
|
|
800
812
|
bindings.push(engine.formatTermForWrite(residual, env, answerWriteOptions));
|
|
801
813
|
}
|
|
802
814
|
// Residual attributes are part of the answer even when the attributed
|
|
@@ -821,9 +833,9 @@ function formatAnswer(engine, state, variables, env) {
|
|
|
821
833
|
for (const root of attributeRoots) {
|
|
822
834
|
if (projectedAttributeRoots.has(root.name) || !env.hasPrologAttributes?.(root.name)) continue;
|
|
823
835
|
projectedAttributeRoots.add(root.name);
|
|
824
|
-
if (!names.has(root.name)) names.set(root.name,
|
|
836
|
+
if (!names.has(root.name)) names.set(root.name, nextGeneratedName());
|
|
825
837
|
for (const residual of state.solver.attributeResidualGoals(root, env)) {
|
|
826
|
-
collectUnboundVariables(engine, residual, env, names,
|
|
838
|
+
collectUnboundVariables(engine, residual, env, names, nextGeneratedName);
|
|
827
839
|
bindings.push(engine.formatTermForWrite(residual, env, answerWriteOptions));
|
|
828
840
|
}
|
|
829
841
|
}
|
|
@@ -28,6 +28,36 @@ import {
|
|
|
28
28
|
|
|
29
29
|
export function regressionCases() {
|
|
30
30
|
return [
|
|
31
|
+
{
|
|
32
|
+
name: 'close/1 accepts a $stream/1 term rebuilt via functor/3 and arg/3 that is structurally == to the real handle (issue #109)',
|
|
33
|
+
run: () => {
|
|
34
|
+
const file = sourceAtom(path.join(temp.dir, `stream-109-${++temp.counter}.txt`));
|
|
35
|
+
const result = runCli([], { input:
|
|
36
|
+
`open(${file},write,S),functor(S,Fn,A),functor(S2,Fn,A),arg(1,S,A1),arg(1,S2,A1),S==S2,close(S2).\n` +
|
|
37
|
+
'halt.\n',
|
|
38
|
+
});
|
|
39
|
+
assertEqual(result.status, 0, result.stderr);
|
|
40
|
+
// S2's argument only becomes the real stream number through
|
|
41
|
+
// unification rather than being built in place by open/3, so
|
|
42
|
+
// close/1 must deref it like any other compound argument instead of
|
|
43
|
+
// rejecting an equally valid, structurally-== stream reference as
|
|
44
|
+
// malformed.
|
|
45
|
+
assertNotIncludes(result.stdout + result.stderr, 'error(', 'no domain_error(stream_or_alias, ...) from close/1');
|
|
46
|
+
assertIncludes(result.stdout, 'A1 = ', 'query succeeds and reports the shared stream number');
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'top level never mints a generated variable name that collides with a query variable\'s own name (issue #108)',
|
|
51
|
+
run: () => {
|
|
52
|
+
const result = runCli([], { input: 'length(L,1),_A=99.\nhalt.\n' });
|
|
53
|
+
assertEqual(result.status, 0, result.stderr);
|
|
54
|
+
// `_A` is a real query variable here, not the top level's own first
|
|
55
|
+
// generated name. Printing the anonymous variable inside L as `_A` as
|
|
56
|
+
// well would misleadingly suggest it is the same variable as the
|
|
57
|
+
// query's own `_A`, which is bound to 99 and unrelated to L.
|
|
58
|
+
assertIncludes(result.stdout, '?- L = [_B], _A = 99.\n', 'generated name skips the query\'s own _A');
|
|
59
|
+
},
|
|
60
|
+
},
|
|
31
61
|
{
|
|
32
62
|
name: 'REPL current_input/1 and current_output/1 fail on closed stream handles (issue #107)',
|
|
33
63
|
run: () => {
|