eyeprolog 1.5.80 → 1.5.82
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/conformance-report.md +1 -1
- package/examples/defeasible-reasoning.pl +120 -0
- package/examples/output/defeasible-reasoning.pl +6 -0
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/explain.js +14 -21
- package/src/index.js +1 -1
- package/src/iso.js +47 -134
- package/src/parser.js +63 -46
- package/src/platform.js +34 -0
- package/src/program.js +54 -101
- package/src/quads.js +64 -28
- package/src/solver.js +64 -305
- package/src/term.js +24 -30
- package/test/conformance/ISO-COMPLIANCE.md +1 -1
- package/test/conformance/NEUMERKEL-LATEST.md +5 -3
- package/test/conformance/NEUMERKEL-LIVE.md +11 -2
- package/test/conformance/README.md +1 -1
- package/test/fixtures/length_quad.pl +2 -1
- package/test/neumerkel.mjs +90 -24
- package/test/regression/cases-regression.mjs +26 -22
- package/test/run-conformance-report.mjs +1 -1
- package/test/run-neumerkel-tests.mjs +6 -4
- package/test/run-neumerkel.mjs +3 -42
- package/test/test-style.mjs +18 -2
- package/the-art-of-eyeprolog.md +4 -3
package/conformance-report.md
CHANGED
|
@@ -7,7 +7,7 @@ when this report is generated; it is not inferred from fixture counts.
|
|
|
7
7
|
## Latest Neumerkel evidence
|
|
8
8
|
|
|
9
9
|
See the tracked [latest Neumerkel conformity report](test/conformance/NEUMERKEL-LATEST.md).
|
|
10
|
-
`npm test` fetches all
|
|
10
|
+
`npm test` fetches all eight TU Wien sources once and executes the discovered inventory.
|
|
11
11
|
The release workflow then synchronizes this tracked report from those exact successful
|
|
12
12
|
cached source bytes, avoiding a second live fetch and its race window.
|
|
13
13
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
% Defeasible reasoning: an employee travel-expense reimbursement policy.
|
|
2
|
+
%
|
|
3
|
+
% Defeasible rules state a default and let a more specific rule override it.
|
|
4
|
+
% Most of that is ordinary, stratified negation as failure (7.3, \+/1): compute
|
|
5
|
+
% the exception from facts, then let the default rule negate the exception.
|
|
6
|
+
% Nothing here needs the well-founded semantics (WFS) yet.
|
|
7
|
+
%
|
|
8
|
+
% The last section is different on purpose. It models two independent
|
|
9
|
+
% reimbursement policies that can both claim the same expense with no company
|
|
10
|
+
% rule saying which one wins — team_offsite_hotel qualifies for both the flat
|
|
11
|
+
% per-diem blanket allowance and itemized reimbursement, and neither policy
|
|
12
|
+
% was written anticipating the other. Three ways to handle that:
|
|
13
|
+
%
|
|
14
|
+
% 1) Plain \+/1, each side negating the other. That is an unstratified
|
|
15
|
+
% negation cycle: proving either side requires failing the other, which
|
|
16
|
+
% requires proving the first again. Try it (swap tnot/1 for \+/1 below
|
|
17
|
+
% and run with --warnings): the engine loops until it runs out of stack.
|
|
18
|
+
% 2) tnot/1 and wfs_truth/2. EyeProlog evaluates the same cycle under WFS
|
|
19
|
+
% and reports both sides `undefined` instead of picking one arbitrarily
|
|
20
|
+
% — honest, since the policies themselves do not resolve the conflict,
|
|
21
|
+
% but callers now have to know to ask wfs_truth/2 instead of just
|
|
22
|
+
% calling covered_by_blanket/1 or itemized/1 directly.
|
|
23
|
+
% 3) An explicit conflict predicate, the way examples/nixon-diamond.pl
|
|
24
|
+
% already handles the same shape of problem (two independent defaults,
|
|
25
|
+
% no priority between them): join the two eligibility facts directly and
|
|
26
|
+
% name the outcome. No negation, no cycle, nothing for --warnings to
|
|
27
|
+
% flag, and the result is an ordinary fact any caller can query without
|
|
28
|
+
% having to know WFS exists.
|
|
29
|
+
%
|
|
30
|
+
% Here (3) is the better fit, and is this codebase's actual convention for
|
|
31
|
+
% this situation. Reach for tnot/1 and wfs_truth/2 when the mutual defeat is
|
|
32
|
+
% already how the rules are naturally stated as each other's negation — a
|
|
33
|
+
% permission that holds unless its prohibition holds and vice versa, as
|
|
34
|
+
% examples/odrl-policy-reasoning.pl's profile rules do for a real policy
|
|
35
|
+
% language — not to manufacture a cycle for a conflict a direct join would
|
|
36
|
+
% already detect. And for everyday defeasible overriding, where a more
|
|
37
|
+
% specific rule is meant to win outright, plain stratified \+/1 already says
|
|
38
|
+
% exactly that and is ISO-portable; WFS would not change the answer there,
|
|
39
|
+
% only the machinery needed to get it.
|
|
40
|
+
|
|
41
|
+
%% goal: reimbursementQuestion(X0, X1, X2)
|
|
42
|
+
%% goal: conflictQuestion(X0, X1, X2)
|
|
43
|
+
%% goal: explicitConflictQuestion(X0, X1, X2)
|
|
44
|
+
|
|
45
|
+
% --- Submitted expenses and the facts a claims clerk would see ------------
|
|
46
|
+
|
|
47
|
+
submitted(taxi_receipt).
|
|
48
|
+
submitted(client_dinner_wine).
|
|
49
|
+
submitted(client_dinner_wine_preapproved).
|
|
50
|
+
submitted(team_offsite_hotel).
|
|
51
|
+
|
|
52
|
+
category(client_dinner_wine, alcohol).
|
|
53
|
+
category(client_dinner_wine_preapproved, alcohol).
|
|
54
|
+
|
|
55
|
+
% A manager can preapprove alcohol as part of client entertainment; that is
|
|
56
|
+
% the exception to the exception.
|
|
57
|
+
preapproved_entertainment(client_dinner_wine_preapproved).
|
|
58
|
+
|
|
59
|
+
% --- Ordinary defeasible tier: specificity resolves every case -------------
|
|
60
|
+
%
|
|
61
|
+
% default: an expense is reimbursable
|
|
62
|
+
% exception: ... unless it is alcohol
|
|
63
|
+
% exception to that: ... unless the alcohol was preapproved entertainment
|
|
64
|
+
%
|
|
65
|
+
% Each rule only negates a lower, already-computed layer, so this is
|
|
66
|
+
% stratified and plain \+/1 is all it takes.
|
|
67
|
+
|
|
68
|
+
excluded(Expense) :-
|
|
69
|
+
category(Expense, alcohol),
|
|
70
|
+
\+ preapproved_entertainment(Expense).
|
|
71
|
+
|
|
72
|
+
reimbursable(Expense) :-
|
|
73
|
+
submitted(Expense),
|
|
74
|
+
\+ excluded(Expense).
|
|
75
|
+
|
|
76
|
+
% --- The one case with a genuine, unresolved conflict, two ways ------------
|
|
77
|
+
%
|
|
78
|
+
% team_offsite_hotel independently qualifies for both the flat per-diem
|
|
79
|
+
% blanket allowance and itemized reimbursement.
|
|
80
|
+
blanket_eligible(team_offsite_hotel).
|
|
81
|
+
itemizable(team_offsite_hotel).
|
|
82
|
+
|
|
83
|
+
% 2) WFS via tnot/1: each classification defeats the other, leaving both
|
|
84
|
+
% `undefined` rather than an arbitrary pick.
|
|
85
|
+
covered_by_blanket(Expense) :-
|
|
86
|
+
blanket_eligible(Expense),
|
|
87
|
+
tnot(itemized(Expense)).
|
|
88
|
+
itemized(Expense) :-
|
|
89
|
+
itemizable(Expense),
|
|
90
|
+
tnot(covered_by_blanket(Expense)).
|
|
91
|
+
|
|
92
|
+
% 3) This codebase's usual idiom (examples/nixon-diamond.pl): detect the
|
|
93
|
+
% conflict directly from the two eligibility facts, no negation involved.
|
|
94
|
+
policy_conflict(Expense, blanket_allowance, itemized_reimbursement) :-
|
|
95
|
+
blanket_eligible(Expense),
|
|
96
|
+
itemizable(Expense).
|
|
97
|
+
|
|
98
|
+
% --- Curated questions -----------------------------------------------------
|
|
99
|
+
|
|
100
|
+
% Plain expense, alcohol excluded by default, and the preapproved override —
|
|
101
|
+
% all decided by ordinary negation as failure, no WFS involved.
|
|
102
|
+
reimbursementQuestion(plain, taxi_receipt, Verdict) :-
|
|
103
|
+
wfs_truth(reimbursable(taxi_receipt), Verdict).
|
|
104
|
+
reimbursementQuestion(alcohol_excluded, client_dinner_wine, Verdict) :-
|
|
105
|
+
wfs_truth(reimbursable(client_dinner_wine), Verdict).
|
|
106
|
+
reimbursementQuestion(alcohol_preapproved, client_dinner_wine_preapproved, Verdict) :-
|
|
107
|
+
wfs_truth(reimbursable(client_dinner_wine_preapproved), Verdict).
|
|
108
|
+
|
|
109
|
+
% The unresolved policy conflict under WFS: both classifications come back
|
|
110
|
+
% `undefined`, which only tells a caller anything if it remembers to ask
|
|
111
|
+
% wfs_truth/2 in the first place.
|
|
112
|
+
conflictQuestion(blanket_allowance, team_offsite_hotel, Verdict) :-
|
|
113
|
+
wfs_truth(covered_by_blanket(team_offsite_hotel), Verdict).
|
|
114
|
+
conflictQuestion(itemized_reimbursement, team_offsite_hotel, Verdict) :-
|
|
115
|
+
wfs_truth(itemized(team_offsite_hotel), Verdict).
|
|
116
|
+
|
|
117
|
+
% The same conflict, detected directly: an ordinary fact any caller can query
|
|
118
|
+
% without knowing WFS is involved at all.
|
|
119
|
+
explicitConflictQuestion(team_offsite_hotel, blanket_allowance, itemized_reimbursement) :-
|
|
120
|
+
policy_conflict(team_offsite_hotel, blanket_allowance, itemized_reimbursement).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
reimbursementQuestion(plain, taxi_receipt, true).
|
|
2
|
+
reimbursementQuestion(alcohol_excluded, client_dinner_wine, false).
|
|
3
|
+
reimbursementQuestion(alcohol_preapproved, client_dinner_wine_preapproved, true).
|
|
4
|
+
conflictQuestion(blanket_allowance, team_offsite_hotel, undefined).
|
|
5
|
+
conflictQuestion(itemized_reimbursement, team_offsite_hotel, undefined).
|
|
6
|
+
explicitConflictQuestion(team_offsite_hotel, blanket_allowance, itemized_reimbursement).
|
package/index.d.ts
CHANGED
|
@@ -96,6 +96,12 @@ export interface EyePrologQuadResult {
|
|
|
96
96
|
kind?: 'failed' | 'malformed' | 'bad_identifier' | 'unsupported' | 'undecided';
|
|
97
97
|
expected?: EyePrologTerm;
|
|
98
98
|
reason?: string;
|
|
99
|
+
/** The quad's query term, for a caller building its own per-result label. */
|
|
100
|
+
query?: EyePrologTerm;
|
|
101
|
+
/** The quad's identifier, or null when the quad has none. */
|
|
102
|
+
id?: EyePrologTerm | null;
|
|
103
|
+
/** The source line this answer description starts on, when known. */
|
|
104
|
+
line?: number | null;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
export interface EyePrologQuadRunResult {
|
|
@@ -312,6 +318,8 @@ export function hasForwardRules(program: Program): boolean;
|
|
|
312
318
|
/** Execute EyeProlog `:+/2` rules to closure using an existing solver. */
|
|
313
319
|
export function executeForwardRules(program: Program, solver: Solver, options?: EyePrologForwardRunOptions): EyePrologForwardRunResult;
|
|
314
320
|
export function runQuads(source: string | Program, options?: EyePrologQuadRunOptions): EyePrologQuadRunResult;
|
|
321
|
+
/** Render a quad term (query, identifier, or expected answer) the way quad failure reports do. */
|
|
322
|
+
export function formatQuadTerm(program: Program, term: EyePrologTerm): string;
|
|
315
323
|
export interface EyePrologProofMethod {
|
|
316
324
|
type: 'source' | 'builtin' | 'library' | 'conjunction';
|
|
317
325
|
kind?: 'fact' | 'rule';
|
|
@@ -433,6 +441,7 @@ declare const eyeprolog: {
|
|
|
433
441
|
hasForwardRules: typeof hasForwardRules;
|
|
434
442
|
executeForwardRules: typeof executeForwardRules;
|
|
435
443
|
runQuads: typeof runQuads;
|
|
444
|
+
formatQuadTerm: typeof formatQuadTerm;
|
|
436
445
|
proofCertificate: typeof proofCertificate;
|
|
437
446
|
proofCertificatesFromText: typeof proofCertificatesFromText;
|
|
438
447
|
verifyProof: typeof verifyProof;
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/explain.js
CHANGED
|
@@ -69,7 +69,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
69
69
|
env: proofEnv,
|
|
70
70
|
node: {
|
|
71
71
|
goal: resolveForProof(goal, proofEnv),
|
|
72
|
-
method:
|
|
72
|
+
method: goalMethod('builtin', goal),
|
|
73
73
|
sourceHead: resolveForProof(goal, proofEnv),
|
|
74
74
|
sourceBody: [],
|
|
75
75
|
bindings: [],
|
|
@@ -97,7 +97,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
97
97
|
env: proofEnv,
|
|
98
98
|
node: {
|
|
99
99
|
goal: resolveForProof(goal, proofEnv),
|
|
100
|
-
method:
|
|
100
|
+
method: goalMethod('library', goal),
|
|
101
101
|
sourceHead: resolveForProof(goal, proofEnv),
|
|
102
102
|
sourceBody: [],
|
|
103
103
|
bindings: [],
|
|
@@ -254,17 +254,9 @@ function sourceMethod(clause, kind) {
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
function
|
|
257
|
+
function goalMethod(type, goal) {
|
|
258
258
|
return {
|
|
259
|
-
type
|
|
260
|
-
name: goal.type === COMPOUND ? goal.name : 'goal',
|
|
261
|
-
arity: goal.type === COMPOUND ? goal.arity : 0,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function libraryMethod(goal) {
|
|
266
|
-
return {
|
|
267
|
-
type: 'library',
|
|
259
|
+
type,
|
|
268
260
|
name: goal.type === COMPOUND ? goal.name : 'goal',
|
|
269
261
|
arity: goal.type === COMPOUND ? goal.arity : 0,
|
|
270
262
|
};
|
|
@@ -380,15 +372,15 @@ function certificateText(term) {
|
|
|
380
372
|
throw new Error('expected certificate text');
|
|
381
373
|
}
|
|
382
374
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
return node.children.some(containsLibraryBoundary);
|
|
375
|
+
function proofTreeContains(node, predicate) {
|
|
376
|
+
return predicate(node) || node.children.some((child) => proofTreeContains(child, predicate));
|
|
386
377
|
}
|
|
387
378
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
379
|
+
const containsLibraryBoundary = (node) => proofTreeContains(node, (n) => n.method?.type === 'library');
|
|
380
|
+
|
|
381
|
+
const containsExpandedLibrarySource = (node) => proofTreeContains(
|
|
382
|
+
node, (n) => n.method?.type === 'source' && String(n.method.filename ?? '').startsWith('src/lib/'),
|
|
383
|
+
);
|
|
392
384
|
|
|
393
385
|
export function verifyProof(program, input, options = {}) {
|
|
394
386
|
const certificate = input?.certificate ?? input;
|
|
@@ -557,8 +549,9 @@ function verifyBindings(bindings, variables, env, program) {
|
|
|
557
549
|
|
|
558
550
|
function renderMethodTerm(method) {
|
|
559
551
|
if (method && method.type === 'source') return `${method.kind}(${quoteString(method.filename)}, clause(${method.clause}))`;
|
|
560
|
-
if (method && method.type === 'builtin'
|
|
561
|
-
|
|
552
|
+
if (method && (method.type === 'builtin' || method.type === 'library')) {
|
|
553
|
+
return `${method.type}(${quoteAtomText(method.name)}, ${method.arity})`;
|
|
554
|
+
}
|
|
562
555
|
return String(method);
|
|
563
556
|
}
|
|
564
557
|
|
package/src/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
eyePrologInteropLibraryModules,
|
|
29
29
|
} from './standard-library.js';
|
|
30
30
|
export { StreamManager } from './io.js';
|
|
31
|
-
export { runQuads } from './quads.js';
|
|
31
|
+
export { formatQuadTerm, runQuads } from './quads.js';
|
|
32
32
|
export { executeForwardRules, hasForwardRules } from './execute.js';
|
|
33
33
|
|
|
34
34
|
import { installCleanupLifecycle } from './cleanup.js';
|
package/src/iso.js
CHANGED
|
@@ -680,10 +680,7 @@ function validPredicateIndicator(term) {
|
|
|
680
680
|
(term.args[1].type === NUMBER && isDecimalInteger(term.args[1].name) && BigInt(term.args[1].name) >= 0n));
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
-
|
|
684
|
-
const state = { pending: false };
|
|
685
|
-
return withPendingState(currentPredicateSolutions(context, state), state);
|
|
686
|
-
}
|
|
683
|
+
const currentPredicateBuiltin = pendingBuiltin(currentPredicateSolutions);
|
|
687
684
|
|
|
688
685
|
function* currentPredicateSolutions({ solver, goal, env }, state) {
|
|
689
686
|
const indicator = copyResolved(goal.args[0], env);
|
|
@@ -719,10 +716,7 @@ function clauseBodyTerm(body) {
|
|
|
719
716
|
return result;
|
|
720
717
|
}
|
|
721
718
|
|
|
722
|
-
|
|
723
|
-
const state = { pending: false };
|
|
724
|
-
return withPendingState(clauseSolutions(context, state), state);
|
|
725
|
-
}
|
|
719
|
+
const clauseBuiltin = pendingBuiltin(clauseSolutions);
|
|
726
720
|
|
|
727
721
|
function* clauseSolutions({ solver, goal, env }, state) {
|
|
728
722
|
const head = deref(goal.args[0], env);
|
|
@@ -851,12 +845,7 @@ function assertBuiltin(atStart) {
|
|
|
851
845
|
};
|
|
852
846
|
}
|
|
853
847
|
|
|
854
|
-
|
|
855
|
-
const state = { pending: true };
|
|
856
|
-
const iterator = retractSolutions(context, state);
|
|
857
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
858
|
-
return iterator;
|
|
859
|
-
}
|
|
848
|
+
const retractBuiltin = pendingBuiltin(retractSolutions, true);
|
|
860
849
|
|
|
861
850
|
function* retractSolutions({ solver, goal, env }, state) {
|
|
862
851
|
const parts = clauseParts(goal.args[0], env);
|
|
@@ -941,10 +930,7 @@ function* abolishBuiltin({ solver, goal, env }) {
|
|
|
941
930
|
yield env;
|
|
942
931
|
}
|
|
943
932
|
|
|
944
|
-
|
|
945
|
-
const state = { pending: false };
|
|
946
|
-
return withPendingState(currentPrologFlagSolutions(context, state), state);
|
|
947
|
-
}
|
|
933
|
+
const currentPrologFlagBuiltin = pendingBuiltin(currentPrologFlagSolutions);
|
|
948
934
|
|
|
949
935
|
function* currentPrologFlagSolutions({ solver, goal, env }, state) {
|
|
950
936
|
const flag = deref(goal.args[0], env);
|
|
@@ -1058,10 +1044,7 @@ function* opBuiltin({ solver, goal, env }) {
|
|
|
1058
1044
|
yield env;
|
|
1059
1045
|
}
|
|
1060
1046
|
|
|
1061
|
-
|
|
1062
|
-
const state = { pending: false };
|
|
1063
|
-
return withPendingState(currentOpSolutions(context, state), state);
|
|
1064
|
-
}
|
|
1047
|
+
const currentOpBuiltin = pendingBuiltin(currentOpSolutions);
|
|
1065
1048
|
|
|
1066
1049
|
function* currentOpSolutions({ solver, goal, env }, state) {
|
|
1067
1050
|
const priority = deref(goal.args[0], env);
|
|
@@ -1124,10 +1107,7 @@ function* charConversionBuiltin({ solver, goal, env }) {
|
|
|
1124
1107
|
else solver.charConversions.set(input.name, output.name);
|
|
1125
1108
|
yield env;
|
|
1126
1109
|
}
|
|
1127
|
-
|
|
1128
|
-
const state = { pending: false };
|
|
1129
|
-
return withPendingState(currentCharConversionSolutions(context, state), state);
|
|
1130
|
-
}
|
|
1110
|
+
const currentCharConversionBuiltin = pendingBuiltin(currentCharConversionSolutions);
|
|
1131
1111
|
|
|
1132
1112
|
function* currentCharConversionSolutions({ solver, goal, env }, state) {
|
|
1133
1113
|
const input = conversionCharacter(goal.args[0], env, true, solver);
|
|
@@ -1355,28 +1335,23 @@ function* closeBuiltin({ solver, goal, env }) {
|
|
|
1355
1335
|
yield env;
|
|
1356
1336
|
}
|
|
1357
1337
|
|
|
1358
|
-
function
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if (
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
if (value.type !== VAR) {
|
|
1373
|
-
const id = streamTermReference(goal.args[0], env);
|
|
1374
|
-
if (id === solver.io.currentOutput) yield env;
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
const next = env.clone();
|
|
1378
|
-
if (unify(goal.args[0], streamHandle(solver.io.currentOutput), next)) yield next;
|
|
1338
|
+
function currentStreamBuiltin(which) {
|
|
1339
|
+
// A closed handle is still a stream-term, but cannot be current (#107).
|
|
1340
|
+
// Validate its shape, then test identity rather than requiring an open stream.
|
|
1341
|
+
return function* ({ solver, goal, env }) {
|
|
1342
|
+
const value = deref(goal.args[0], env);
|
|
1343
|
+
const current = solver.io[which];
|
|
1344
|
+
if (value.type !== VAR) {
|
|
1345
|
+
const id = streamTermReference(goal.args[0], env);
|
|
1346
|
+
if (id === current) yield env;
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const next = env.clone();
|
|
1350
|
+
if (unify(goal.args[0], streamHandle(current), next)) yield next;
|
|
1351
|
+
};
|
|
1379
1352
|
}
|
|
1353
|
+
const currentInputBuiltin = currentStreamBuiltin('currentInput');
|
|
1354
|
+
const currentOutputBuiltin = currentStreamBuiltin('currentOutput');
|
|
1380
1355
|
|
|
1381
1356
|
function setCurrentStreamBuiltin(mode) {
|
|
1382
1357
|
return function* ({ solver, goal, env }) {
|
|
@@ -1456,10 +1431,7 @@ function isStreamPropertyPattern(value) {
|
|
|
1456
1431
|
].includes(value.name);
|
|
1457
1432
|
}
|
|
1458
1433
|
|
|
1459
|
-
|
|
1460
|
-
const state = { pending: false };
|
|
1461
|
-
return withPendingState(streamPropertySolutions(context, state), state);
|
|
1462
|
-
}
|
|
1434
|
+
const streamPropertyBuiltin = pendingBuiltin(streamPropertySolutions);
|
|
1463
1435
|
|
|
1464
1436
|
function* streamPropertySolutions({ solver, goal, env }, state) {
|
|
1465
1437
|
const reference = deref(goal.args[0], env);
|
|
@@ -2068,14 +2040,6 @@ function* writeTermBuiltin({ solver, goal, env }) {
|
|
|
2068
2040
|
yield env;
|
|
2069
2041
|
}
|
|
2070
2042
|
|
|
2071
|
-
function resolvedOrVariable(term, env, expected) {
|
|
2072
|
-
const value = deref(term, env);
|
|
2073
|
-
if (value.type !== VAR && value.type !== expected) {
|
|
2074
|
-
throw new PrologError(`type_error(${expected === ATOM ? 'atom' : 'number'})`, value);
|
|
2075
|
-
}
|
|
2076
|
-
return value;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
2043
|
function characters(text) {
|
|
2080
2044
|
return Array.from(text);
|
|
2081
2045
|
}
|
|
@@ -2095,10 +2059,7 @@ function* atomLengthBuiltin({ goal, env }) {
|
|
|
2095
2059
|
if (unify(goal.args[1], numberTerm(characters(value.name).length), next)) yield next;
|
|
2096
2060
|
}
|
|
2097
2061
|
|
|
2098
|
-
|
|
2099
|
-
const state = { pending: false };
|
|
2100
|
-
return withPendingState(atomConcatSolutions(context, state), state);
|
|
2101
|
-
}
|
|
2062
|
+
const atomConcatBuiltin = pendingBuiltin(atomConcatSolutions);
|
|
2102
2063
|
|
|
2103
2064
|
function* atomConcatSolutions({ goal, env }, state) {
|
|
2104
2065
|
const first = deref(goal.args[0], env);
|
|
@@ -2142,10 +2103,7 @@ function optionalInteger(term, env) {
|
|
|
2142
2103
|
return BigInt(value.name);
|
|
2143
2104
|
}
|
|
2144
2105
|
|
|
2145
|
-
|
|
2146
|
-
const state = { pending: false };
|
|
2147
|
-
return withPendingState(subAtomSolutions(context, state), state);
|
|
2148
|
-
}
|
|
2106
|
+
const subAtomBuiltin = pendingBuiltin(subAtomSolutions);
|
|
2149
2107
|
|
|
2150
2108
|
function* subAtomSolutions({ goal, env }, state) {
|
|
2151
2109
|
const source = deref(goal.args[0], env);
|
|
@@ -2625,10 +2583,7 @@ function sortedUnique(items) {
|
|
|
2625
2583
|
}
|
|
2626
2584
|
|
|
2627
2585
|
function allSolutionsBuiltin(asSet) {
|
|
2628
|
-
return
|
|
2629
|
-
const state = { pending: false };
|
|
2630
|
-
return withPendingState(allSolutionsGroups(context, asSet, state), state);
|
|
2631
|
-
};
|
|
2586
|
+
return pendingBuiltin((context, state) => allSolutionsGroups(context, asSet, state));
|
|
2632
2587
|
}
|
|
2633
2588
|
|
|
2634
2589
|
function* allSolutionsGroups({ solver, goal, env }, asSet, state) {
|
|
@@ -2697,6 +2652,15 @@ function withPendingState(iterator, state) {
|
|
|
2697
2652
|
iterator.hasPendingAlternatives = () => state.pending;
|
|
2698
2653
|
return iterator;
|
|
2699
2654
|
}
|
|
2655
|
+
|
|
2656
|
+
// Most builtins share this shape: create a { pending } cell, run a solutions
|
|
2657
|
+
// generator against it, and expose that cell through hasPendingAlternatives.
|
|
2658
|
+
function pendingBuiltin(solutionsFn, initialPending = false) {
|
|
2659
|
+
return (context) => {
|
|
2660
|
+
const state = { pending: initialPending };
|
|
2661
|
+
return withPendingState(solutionsFn(context, state), state);
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2700
2664
|
function validateControlCallable(term, culprit, env) {
|
|
2701
2665
|
// Only control constructs need their nested goals validated at meta-call
|
|
2702
2666
|
// entry. Walk them iteratively and dereference each nested goal lazily so
|
|
@@ -2770,9 +2734,9 @@ function expandCallGoal({ goal, env }) {
|
|
|
2770
2734
|
if (converted.module == null) converted.module = module;
|
|
2771
2735
|
return converted;
|
|
2772
2736
|
}
|
|
2773
|
-
function*
|
|
2737
|
+
function* invokeExpandedGoal(context, expand) {
|
|
2774
2738
|
const { solver, env } = context;
|
|
2775
|
-
const invoked =
|
|
2739
|
+
const invoked = expand(context);
|
|
2776
2740
|
const child = solver.cloneForInnerGoal();
|
|
2777
2741
|
try {
|
|
2778
2742
|
yield* child.solve([invoked], env, 0);
|
|
@@ -2780,6 +2744,9 @@ function* callBuiltin(context) {
|
|
|
2780
2744
|
solver.absorbStatsFrom(child);
|
|
2781
2745
|
}
|
|
2782
2746
|
}
|
|
2747
|
+
function* callBuiltin(context) {
|
|
2748
|
+
yield* invokeExpandedGoal(context, expandCallGoal);
|
|
2749
|
+
}
|
|
2783
2750
|
function expandCallClosureGoal({ goal, env }) {
|
|
2784
2751
|
const closure = callable(goal.args[0], env);
|
|
2785
2752
|
const existing = closure.type === COMPOUND ? closure.args : [];
|
|
@@ -2792,14 +2759,7 @@ function expandCallClosureGoal({ goal, env }) {
|
|
|
2792
2759
|
return invoked;
|
|
2793
2760
|
}
|
|
2794
2761
|
function* callClosureBuiltin(context) {
|
|
2795
|
-
|
|
2796
|
-
const invoked = expandCallClosureGoal(context);
|
|
2797
|
-
const child = solver.cloneForInnerGoal();
|
|
2798
|
-
try {
|
|
2799
|
-
yield* child.solve([invoked], env, 0);
|
|
2800
|
-
} finally {
|
|
2801
|
-
solver.absorbStatsFrom(child);
|
|
2802
|
-
}
|
|
2762
|
+
yield* invokeExpandedGoal(context, expandCallClosureGoal);
|
|
2803
2763
|
}
|
|
2804
2764
|
|
|
2805
2765
|
export function* countAllBuiltin({ solver, goal, env }) {
|
|
@@ -2846,10 +2806,7 @@ function writeElapsedTime(solver, startedAt, inferences) {
|
|
|
2846
2806
|
);
|
|
2847
2807
|
}
|
|
2848
2808
|
|
|
2849
|
-
export
|
|
2850
|
-
const state = { pending: true };
|
|
2851
|
-
return withPendingState(timeSolutions(context, state), state);
|
|
2852
|
-
}
|
|
2809
|
+
export const timeBuiltin = pendingBuiltin(timeSolutions, true);
|
|
2853
2810
|
|
|
2854
2811
|
function* timeSolutions({ solver, goal, env }, state) {
|
|
2855
2812
|
const invoked = callable(goal.args[0], env);
|
|
@@ -2877,10 +2834,7 @@ function* timeSolutions({ solver, goal, env }, state) {
|
|
|
2877
2834
|
}
|
|
2878
2835
|
}
|
|
2879
2836
|
|
|
2880
|
-
export
|
|
2881
|
-
const state = { pending: true };
|
|
2882
|
-
return withPendingState(callNthSolutions(context, state), state);
|
|
2883
|
-
}
|
|
2837
|
+
export const callNthBuiltin = pendingBuiltin(callNthSolutions, true);
|
|
2884
2838
|
|
|
2885
2839
|
function* callNthSolutions({ solver, goal, env }, state) {
|
|
2886
2840
|
const requestedTerm = deref(goal.args[1], env);
|
|
@@ -3005,37 +2959,7 @@ export function callResidueVarsBuiltin({ solver, goal, env }) {
|
|
|
3005
2959
|
return iterator;
|
|
3006
2960
|
}
|
|
3007
2961
|
|
|
3008
|
-
|
|
3009
|
-
const state = { pending: true };
|
|
3010
|
-
return withPendingState(freezeSolutions(context, state), state);
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
function* freezeSolutions({ solver, goal, env }, state) {
|
|
3014
|
-
const watched = deref(goal.args[0], env);
|
|
3015
|
-
if (watched.type !== VAR) {
|
|
3016
|
-
const child = solver.cloneForInnerGoal();
|
|
3017
|
-
try {
|
|
3018
|
-
for (const answerEnv of child.solve([callable(goal.args[1], env)], env, 0)) {
|
|
3019
|
-
state.pending = child.hasPendingAlternatives();
|
|
3020
|
-
yield answerEnv;
|
|
3021
|
-
if (!state.pending) return;
|
|
3022
|
-
}
|
|
3023
|
-
state.pending = false;
|
|
3024
|
-
} finally {
|
|
3025
|
-
solver.absorbStatsFrom(child);
|
|
3026
|
-
}
|
|
3027
|
-
return;
|
|
3028
|
-
}
|
|
3029
|
-
const next = env.clone();
|
|
3030
|
-
next.delay(watched.name, goal.args[1], goal.module ?? 'user');
|
|
3031
|
-
state.pending = false;
|
|
3032
|
-
yield next;
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
function phraseBuiltin(context) {
|
|
3036
|
-
const state = { pending: true };
|
|
3037
|
-
return withPendingState(phraseSolutions(context, state), state);
|
|
3038
|
-
}
|
|
2962
|
+
const phraseBuiltin = pendingBuiltin(phraseSolutions, true);
|
|
3039
2963
|
|
|
3040
2964
|
function* phraseSolutions({ solver, goal, env }, state) {
|
|
3041
2965
|
const grammarBody0 = deref(goal.args[0], env);
|
|
@@ -3161,10 +3085,7 @@ function prologErrorBall(error) {
|
|
|
3161
3085
|
if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
|
|
3162
3086
|
return freshCopy(term, new Env());
|
|
3163
3087
|
}
|
|
3164
|
-
|
|
3165
|
-
const state = { pending: true };
|
|
3166
|
-
return withPendingState(catchSolutions(context, state), state);
|
|
3167
|
-
}
|
|
3088
|
+
const catchBuiltin = pendingBuiltin(catchSolutions, true);
|
|
3168
3089
|
|
|
3169
3090
|
function* catchSolutions({ solver, goal, env }, state) {
|
|
3170
3091
|
let child = null;
|
|
@@ -3293,12 +3214,7 @@ function* solveControlBranch(solver, goal, env, observePending = null) {
|
|
|
3293
3214
|
yield answer;
|
|
3294
3215
|
}
|
|
3295
3216
|
}
|
|
3296
|
-
|
|
3297
|
-
const state = { pending: true };
|
|
3298
|
-
const iterator = disjunctionSolutions(context, state);
|
|
3299
|
-
iterator.hasPendingAlternatives = () => state.pending;
|
|
3300
|
-
return iterator;
|
|
3301
|
-
}
|
|
3217
|
+
const disjunctionBuiltin = pendingBuiltin(disjunctionSolutions, true);
|
|
3302
3218
|
function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
3303
3219
|
const left = deref(goal.args[0], env);
|
|
3304
3220
|
if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
|
|
@@ -3333,10 +3249,7 @@ function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
|
3333
3249
|
(pending) => { state.pending = pending; });
|
|
3334
3250
|
state.pending = false;
|
|
3335
3251
|
}
|
|
3336
|
-
|
|
3337
|
-
const state = { pending: true };
|
|
3338
|
-
return withPendingState(ifThenSolutions(context, state), state);
|
|
3339
|
-
}
|
|
3252
|
+
const ifThenBuiltin = pendingBuiltin(ifThenSolutions, true);
|
|
3340
3253
|
|
|
3341
3254
|
function* ifThenSolutions({ solver, goal, env }, state) {
|
|
3342
3255
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|