prolog-trace-viz 1.1.2 → 2.0.0
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 +43 -30
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +268 -96
- package/dist/analyzer.js.map +1 -1
- package/dist/build-info.d.ts +3 -3
- package/dist/build-info.js +3 -3
- package/dist/clauses.d.ts +11 -0
- package/dist/clauses.d.ts.map +1 -1
- package/dist/clauses.js +12 -0
- package/dist/clauses.js.map +1 -1
- package/dist/cli.d.ts +4 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.js +80 -22
- package/dist/index.js.map +1 -1
- package/dist/markdown-generator.d.ts +24 -0
- package/dist/markdown-generator.d.ts.map +1 -0
- package/dist/markdown-generator.js +124 -0
- package/dist/markdown-generator.js.map +1 -0
- package/dist/parser.d.ts +12 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +67 -35
- package/dist/parser.js.map +1 -1
- package/dist/timeline-formatter.d.ts +9 -0
- package/dist/timeline-formatter.d.ts.map +1 -0
- package/dist/timeline-formatter.js +149 -0
- package/dist/timeline-formatter.js.map +1 -0
- package/dist/timeline.d.ts +148 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +601 -0
- package/dist/timeline.js.map +1 -0
- package/dist/tree-formatter.d.ts +13 -0
- package/dist/tree-formatter.d.ts.map +1 -0
- package/dist/tree-formatter.js +136 -0
- package/dist/tree-formatter.js.map +1 -0
- package/dist/tree.d.ts +75 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +267 -0
- package/dist/tree.js.map +1 -0
- package/dist/wrapper.d.ts +8 -1
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +41 -17
- package/dist/wrapper.js.map +1 -1
- package/package.json +1 -1
- package/tracer.pl +127 -16
package/dist/analyzer.js
CHANGED
|
@@ -41,14 +41,35 @@ const EMOJIS = {
|
|
|
41
41
|
function mapRuntimeVariablesToSource(unifications, originalQuery) {
|
|
42
42
|
// Extract variables from the original query
|
|
43
43
|
const queryVars = extractVariablesFromGoal(originalQuery);
|
|
44
|
-
return unifications.map(u => {
|
|
45
|
-
//
|
|
46
|
-
// For now, use a simple heuristic: if there's only one variable in the query, map to it
|
|
44
|
+
return unifications.map((u, index) => {
|
|
45
|
+
// For single variable queries, map the first unification to the query variable
|
|
47
46
|
if (queryVars.length === 1) {
|
|
48
47
|
return `${queryVars[0]} = ${u.value}`;
|
|
49
48
|
}
|
|
50
|
-
// For multiple variables,
|
|
51
|
-
|
|
49
|
+
// For multiple variables, try to map by position
|
|
50
|
+
if (index < queryVars.length) {
|
|
51
|
+
return `${queryVars[index]} = ${u.value}`;
|
|
52
|
+
}
|
|
53
|
+
// Fallback: use the runtime variable name
|
|
54
|
+
return `${u.variable} = ${u.value}`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Maps runtime variable names back to the variables in the specific goal being matched.
|
|
59
|
+
*/
|
|
60
|
+
function mapRuntimeVariablesToGoal(unifications, goal) {
|
|
61
|
+
// Extract variables from the specific goal
|
|
62
|
+
const goalVars = extractVariablesFromGoal(goal);
|
|
63
|
+
return unifications.map((u, index) => {
|
|
64
|
+
// For single variable goals, map the first unification to the goal variable
|
|
65
|
+
if (goalVars.length === 1) {
|
|
66
|
+
return `${goalVars[0]} = ${u.value}`;
|
|
67
|
+
}
|
|
68
|
+
// For multiple variables, try to map by position
|
|
69
|
+
if (index < goalVars.length) {
|
|
70
|
+
return `${goalVars[index]} = ${u.value}`;
|
|
71
|
+
}
|
|
72
|
+
// Fallback: use the runtime variable name
|
|
52
73
|
return `${u.variable} = ${u.value}`;
|
|
53
74
|
});
|
|
54
75
|
}
|
|
@@ -69,6 +90,34 @@ function mapGoalVariablesToSource(goal, originalQuery) {
|
|
|
69
90
|
* This helps match trace events (which have runtime variables and different line numbers)
|
|
70
91
|
* with the original source clauses (which have source variables and correct line numbers).
|
|
71
92
|
*/
|
|
93
|
+
/**
|
|
94
|
+
* Checks if two clause heads match structurally, ignoring variable names.
|
|
95
|
+
* For example: "t(X+1+1, Z)" matches "t(_548+1+1, _538)"
|
|
96
|
+
*/
|
|
97
|
+
function clausesStructurallyMatch(traceHead, clauseHead) {
|
|
98
|
+
// Normalize both by replacing variables with a placeholder
|
|
99
|
+
const normalizeClause = (clause) => {
|
|
100
|
+
return clause
|
|
101
|
+
// Replace all variables (starting with uppercase or _) with 'VAR'
|
|
102
|
+
.replace(/\b[A-Z_][a-zA-Z0-9_]*\b/g, 'VAR')
|
|
103
|
+
// Normalize whitespace and remove spaces around commas and operators
|
|
104
|
+
.replace(/\s*,\s*/g, ',')
|
|
105
|
+
.replace(/\s*\+\s*/g, '+')
|
|
106
|
+
.replace(/\s+/g, ' ')
|
|
107
|
+
.trim();
|
|
108
|
+
};
|
|
109
|
+
const normalizedTrace = normalizeClause(traceHead);
|
|
110
|
+
const normalizedClause = normalizeClause(clauseHead);
|
|
111
|
+
return normalizedTrace === normalizedClause;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Maps trace clause information to parsed clauses by finding the best match.
|
|
115
|
+
* This handles the mismatch between tracer line numbers (from wrapper file)
|
|
116
|
+
* and source file line numbers by using structural matching.
|
|
117
|
+
*
|
|
118
|
+
* The tracer provides exact clause heads and bodies, but with different variable names
|
|
119
|
+
* and line numbers than our parsed clauses (which have source variables and correct line numbers).
|
|
120
|
+
*/
|
|
72
121
|
function findMatchingClause(goal, parsedClauses, traceClause) {
|
|
73
122
|
const goalPredicate = goal.match(/^([a-z_][a-zA-Z0-9_]*)\(/);
|
|
74
123
|
if (!goalPredicate)
|
|
@@ -83,7 +132,28 @@ function findMatchingClause(goal, parsedClauses, traceClause) {
|
|
|
83
132
|
// For multiple clauses, use heuristics to pick the right one
|
|
84
133
|
// If we have trace clause info, try to match by structure
|
|
85
134
|
if (traceClause) {
|
|
86
|
-
//
|
|
135
|
+
// First priority: exact line number match
|
|
136
|
+
if (traceClause.line) {
|
|
137
|
+
const exactLineMatch = predicateClauses.find(c => c.number === traceClause.line);
|
|
138
|
+
if (exactLineMatch) {
|
|
139
|
+
return exactLineMatch;
|
|
140
|
+
}
|
|
141
|
+
// If no exact match, try to find by clause structure matching
|
|
142
|
+
// The trace provides the actual clause head and body
|
|
143
|
+
if (traceClause.head) {
|
|
144
|
+
const structureMatch = predicateClauses.find(c => {
|
|
145
|
+
// Normalize both clauses for comparison
|
|
146
|
+
const traceHead = traceClause.head.replace(/\s+/g, ' ').trim();
|
|
147
|
+
const clauseHead = c.head.replace(/\s+/g, ' ').trim();
|
|
148
|
+
// Check if they match structurally (ignoring variable names)
|
|
149
|
+
return clausesStructurallyMatch(traceHead, clauseHead);
|
|
150
|
+
});
|
|
151
|
+
if (structureMatch) {
|
|
152
|
+
return structureMatch;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Fallback: Check if it's a fact (no body) vs rule (has body)
|
|
87
157
|
const traceIsRule = traceClause.body && traceClause.body !== 'true';
|
|
88
158
|
for (const clause of predicateClauses) {
|
|
89
159
|
const clauseIsRule = clause.body !== undefined;
|
|
@@ -93,9 +163,34 @@ function findMatchingClause(goal, parsedClauses, traceClause) {
|
|
|
93
163
|
}
|
|
94
164
|
}
|
|
95
165
|
// Fallback: use goal structure heuristics
|
|
166
|
+
// More precise pattern matching for the t/2 predicate
|
|
167
|
+
if (predicateName === 't') {
|
|
168
|
+
// Check for exact patterns
|
|
169
|
+
if (goal.match(/t\(0\+1,/)) {
|
|
170
|
+
// t(0+1, ...) matches clause 26: t(0+1, 1+0)
|
|
171
|
+
const exactMatch = predicateClauses.find(c => c.text.includes('t(0+1,'));
|
|
172
|
+
if (exactMatch) {
|
|
173
|
+
return exactMatch;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (goal.match(/t\(\w+\+0\+1,/)) {
|
|
177
|
+
// t(X+0+1, ...) matches clause 27: t(X+0+1, X+1+0)
|
|
178
|
+
const exactMatch = predicateClauses.find(c => c.text.includes('+0+1,'));
|
|
179
|
+
if (exactMatch) {
|
|
180
|
+
return exactMatch;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (goal.match(/t\(\w+\+1\+1,/)) {
|
|
184
|
+
// t(X+1+1, ...) matches clause 28: t(X+1+1, Z) :- ...
|
|
185
|
+
const exactMatch = predicateClauses.find(c => c.text.includes('+1+1,'));
|
|
186
|
+
if (exactMatch) {
|
|
187
|
+
return exactMatch;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Generic fallback for other predicates
|
|
96
192
|
const isBaseCase = goal.includes('(0,') || goal.includes('(0 ,') ||
|
|
97
|
-
goal.includes('([],') || goal.includes('([] ,')
|
|
98
|
-
goal.includes('(0+') || goal.includes('(0 +');
|
|
193
|
+
goal.includes('([],') || goal.includes('([] ,');
|
|
99
194
|
// For base cases, prefer the first clause (usually the base case)
|
|
100
195
|
// For recursive cases, prefer later clauses
|
|
101
196
|
return isBaseCase ? predicateClauses[0] : predicateClauses[predicateClauses.length - 1];
|
|
@@ -142,17 +237,63 @@ function extractUnifications(goal, clause) {
|
|
|
142
237
|
for (let i = 0; i < goalArgs.length; i++) {
|
|
143
238
|
const goalArg = goalArgs[i].trim();
|
|
144
239
|
const clauseArg = clauseArgs[i].trim();
|
|
145
|
-
// If clause arg is a variable (
|
|
146
|
-
if (/^[A-Z_]
|
|
147
|
-
|
|
240
|
+
// If clause arg is a simple variable (not a complex expression)
|
|
241
|
+
if (/^[A-Z_][a-zA-Z0-9_]*$/.test(clauseArg)) {
|
|
242
|
+
// For runtime variables like _918, try to map back to original query variables
|
|
243
|
+
let displayValue = goalArg;
|
|
244
|
+
if (goalArg.match(/^_\d+$/)) {
|
|
245
|
+
// This is a runtime variable, try to find the original query variable
|
|
246
|
+
// For now, keep the runtime variable as it shows what actually happened
|
|
247
|
+
displayValue = goalArg;
|
|
248
|
+
}
|
|
249
|
+
unifications.push(`${clauseArg} = ${displayValue}`);
|
|
148
250
|
}
|
|
149
251
|
else if (goalArg !== clauseArg) {
|
|
150
|
-
//
|
|
151
|
-
|
|
252
|
+
// For complex expressions, try to extract the variable part
|
|
253
|
+
// E.g., from "0+1+1" vs "X+1+1", extract "X = 0"
|
|
254
|
+
const varMatch = extractVariableFromExpression(goalArg, clauseArg);
|
|
255
|
+
if (varMatch) {
|
|
256
|
+
unifications.push(varMatch);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Fallback: if clause has a variable at the start, extract it
|
|
260
|
+
const clauseVarMatch = clauseArg.match(/^([A-Z_][a-zA-Z0-9_]*)/);
|
|
261
|
+
const goalValueMatch = goalArg.match(/^([^+\-*/,\s]+)/);
|
|
262
|
+
if (clauseVarMatch && goalValueMatch && clauseVarMatch[1] !== goalValueMatch[1]) {
|
|
263
|
+
unifications.push(`${clauseVarMatch[1]} = ${goalValueMatch[1]}`);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Last resort: show the full expression
|
|
267
|
+
unifications.push(`${clauseArg} = ${goalArg}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
152
270
|
}
|
|
153
271
|
}
|
|
154
272
|
return unifications;
|
|
155
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Extracts variable unification from two expressions.
|
|
276
|
+
* E.g., from "0+1+1" and "X+1+1", extracts "X = 0"
|
|
277
|
+
*/
|
|
278
|
+
function extractVariableFromExpression(goalExpr, clauseExpr) {
|
|
279
|
+
// Handle the specific case of arithmetic expressions
|
|
280
|
+
// Pattern: "0+1+1" vs "X+1+1" should give "X = 0"
|
|
281
|
+
// Check if both expressions have the same structure with one variable difference
|
|
282
|
+
if (goalExpr.includes('+') && clauseExpr.includes('+')) {
|
|
283
|
+
// For expressions like "0+1+1" vs "X+1+1"
|
|
284
|
+
const goalMatch = goalExpr.match(/^(\w+)\+(.+)$/);
|
|
285
|
+
const clauseMatch = clauseExpr.match(/^([A-Z_]\w*)\+(.+)$/);
|
|
286
|
+
if (goalMatch && clauseMatch) {
|
|
287
|
+
const [, goalFirst, goalRest] = goalMatch;
|
|
288
|
+
const [, clauseVar, clauseRest] = clauseMatch;
|
|
289
|
+
// If the rest matches and clause has a variable, goal has a value
|
|
290
|
+
if (goalRest === clauseRest && /^[A-Z_]/.test(clauseVar)) {
|
|
291
|
+
return `${clauseVar} = ${goalFirst}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
156
297
|
/**
|
|
157
298
|
* Parse arguments from a comma-separated string, respecting nested structures.
|
|
158
299
|
*/
|
|
@@ -254,7 +395,7 @@ export function analyzeTree(root, clauses = [], options = {}, traceEvents = [],
|
|
|
254
395
|
}
|
|
255
396
|
};
|
|
256
397
|
// Extract final answer first so we can show it in the success node
|
|
257
|
-
const finalAnswer = extractFinalAnswer(root);
|
|
398
|
+
const finalAnswer = extractFinalAnswer(root, originalQuery);
|
|
258
399
|
// Process the tree
|
|
259
400
|
processTreeNode(root, null, {
|
|
260
401
|
nodes,
|
|
@@ -506,7 +647,11 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
506
647
|
// Map trace clause number to parsed clause number for display
|
|
507
648
|
let displayClauseNumber = clauseNumber;
|
|
508
649
|
if (clauseNumber && isUserPredicate && ctx.clauses.length > 0) {
|
|
509
|
-
const matchedClause = findMatchingClause(node.goal, ctx.clauses)
|
|
650
|
+
const matchedClause = findMatchingClause(node.goal, ctx.clauses, (node.clauseLine && node.clauseHead) ? {
|
|
651
|
+
head: node.clauseHead,
|
|
652
|
+
body: node.clauseBody || '',
|
|
653
|
+
line: node.clauseLine
|
|
654
|
+
} : undefined);
|
|
510
655
|
if (matchedClause) {
|
|
511
656
|
displayClauseNumber = matchedClause.number;
|
|
512
657
|
}
|
|
@@ -532,7 +677,7 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
532
677
|
// Create match nodes for user-defined predicates at detailed/full levels
|
|
533
678
|
// Check if this is a user-defined predicate (not built-in)
|
|
534
679
|
// For simple facts, we need to check the parent's goal, not the current node's goal
|
|
535
|
-
let goalToCheck = node.goal;
|
|
680
|
+
let goalToCheck = node.goal; // Use the ACTUAL goal being processed, not the original query
|
|
536
681
|
let clauseNumberToUse = clauseNumber;
|
|
537
682
|
let unificationsSource = node;
|
|
538
683
|
// If this is a success node and parent is query, use parent's goal for matching
|
|
@@ -556,26 +701,14 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
556
701
|
if (clause) {
|
|
557
702
|
// Create match node
|
|
558
703
|
const matchNodeId = ctx.nextNodeId();
|
|
559
|
-
//
|
|
704
|
+
// Extract unifications by matching goal against clause head
|
|
560
705
|
let unifications;
|
|
561
|
-
// For
|
|
562
|
-
// we
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
parentExecutionNode.unifications.map(u => `${u.variable} = ${u.value}`);
|
|
568
|
-
}
|
|
569
|
-
else if (node.unifications && node.unifications.length > 0) {
|
|
570
|
-
// Use accurate unifications from tracer and map to source variables
|
|
571
|
-
unifications = ctx.originalQuery ?
|
|
572
|
-
mapRuntimeVariablesToSource(node.unifications, ctx.originalQuery) :
|
|
573
|
-
node.unifications.map(u => `${u.variable} = ${u.value}`);
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
// Fallback to extraction (for backward compatibility)
|
|
577
|
-
unifications = extractUnifications(goalToCheck, clause);
|
|
578
|
-
}
|
|
706
|
+
// For match nodes whose parent is the query, use originalQuery to preserve source variable names
|
|
707
|
+
// This ensures we show "Z = B" instead of "Z = _918"
|
|
708
|
+
const parentNode = ctx.nodes.find(n => n.id === parentId);
|
|
709
|
+
const goalForUnification = (ctx.originalQuery && parentNode?.type === 'query') ? ctx.originalQuery : goalToCheck;
|
|
710
|
+
// Always extract unifications from goal-clause matching for accuracy
|
|
711
|
+
unifications = extractUnifications(goalForUnification, clause);
|
|
579
712
|
const subgoals = extractSubgoals(clause);
|
|
580
713
|
// Build match node label - only show clause HEAD, not body
|
|
581
714
|
const clauseHead = clause.text.includes(':-')
|
|
@@ -623,12 +756,21 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
623
756
|
}
|
|
624
757
|
else {
|
|
625
758
|
// No clause found, create edge without match node
|
|
759
|
+
// Use consistent clause numbering with node labels
|
|
760
|
+
const isUserPredicate = /^[a-z_][a-zA-Z0-9_]*\(/.test(node.goal) && !isCompoundGoal(node.goal);
|
|
761
|
+
let edgeClauseNumber = clauseNumber;
|
|
762
|
+
if (clauseNumber && isUserPredicate && ctx.clauses.length > 0) {
|
|
763
|
+
const matchedClause = findMatchingClause(node.goal, ctx.clauses);
|
|
764
|
+
if (matchedClause) {
|
|
765
|
+
edgeClauseNumber = matchedClause.number;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
626
768
|
const edge = {
|
|
627
769
|
id: `edge_${ctx.edges.length}`,
|
|
628
770
|
from: parentId,
|
|
629
771
|
to: nodeId,
|
|
630
772
|
type: 'active',
|
|
631
|
-
label:
|
|
773
|
+
label: edgeClauseNumber ? `clause ${edgeClauseNumber}` : '',
|
|
632
774
|
stepNumber: ctx.stepCounter(),
|
|
633
775
|
};
|
|
634
776
|
ctx.edges.push(edge);
|
|
@@ -637,18 +779,27 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
637
779
|
else {
|
|
638
780
|
// No match node needed, create direct edge
|
|
639
781
|
let edgeLabel = '';
|
|
782
|
+
// Use consistent clause numbering with node labels
|
|
783
|
+
const isUserPredicate = /^[a-z_][a-zA-Z0-9_]*\(/.test(node.goal) && !isCompoundGoal(node.goal);
|
|
784
|
+
let edgeClauseNumber = clauseNumber;
|
|
785
|
+
if (clauseNumber && isUserPredicate && ctx.clauses.length > 0) {
|
|
786
|
+
const matchedClause = findMatchingClause(node.goal, ctx.clauses);
|
|
787
|
+
if (matchedClause) {
|
|
788
|
+
edgeClauseNumber = matchedClause.number;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
640
791
|
if (parentNode) {
|
|
641
792
|
if (parentNode.type === 'query' && vizNode.type === 'solving') {
|
|
642
|
-
edgeLabel =
|
|
793
|
+
edgeLabel = edgeClauseNumber ? `clause ${edgeClauseNumber}` : '';
|
|
643
794
|
}
|
|
644
795
|
else if (parentNode.type === 'query' && vizNode.type === 'success') {
|
|
645
|
-
edgeLabel =
|
|
796
|
+
edgeLabel = edgeClauseNumber ? `clause ${edgeClauseNumber}` : '';
|
|
646
797
|
}
|
|
647
798
|
else if (parentNode.type === 'solved' && vizNode.type === 'solving') {
|
|
648
|
-
edgeLabel =
|
|
799
|
+
edgeLabel = edgeClauseNumber ? `clause ${edgeClauseNumber}` : 'done';
|
|
649
800
|
}
|
|
650
801
|
else if (parentNode.type === 'solving' && vizNode.type === 'success') {
|
|
651
|
-
edgeLabel =
|
|
802
|
+
edgeLabel = edgeClauseNumber ? `clause ${edgeClauseNumber}` : 'success';
|
|
652
803
|
}
|
|
653
804
|
else if (parentNode.type === 'solving' && vizNode.type === 'solving') {
|
|
654
805
|
const existingActiveEdges = ctx.edges.filter(e => e.from === parentId && e.type === 'active' &&
|
|
@@ -657,7 +808,7 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
657
808
|
edgeLabel = 'backtrack';
|
|
658
809
|
}
|
|
659
810
|
else {
|
|
660
|
-
edgeLabel =
|
|
811
|
+
edgeLabel = edgeClauseNumber ? `clause ${edgeClauseNumber}` : '';
|
|
661
812
|
}
|
|
662
813
|
}
|
|
663
814
|
else if (parentNode.type === 'solved' && vizNode.type === 'success') {
|
|
@@ -714,16 +865,11 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
714
865
|
if (clause) {
|
|
715
866
|
// Create match node
|
|
716
867
|
const matchNodeId = ctx.nextNodeId();
|
|
717
|
-
//
|
|
868
|
+
// Extract unifications by matching goal against clause head
|
|
718
869
|
let unifications = [];
|
|
719
|
-
if
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
node.unifications.map(u => `${u.variable} = ${u.value}`);
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
unifications = extractUnifications(node.goal, clause);
|
|
726
|
-
}
|
|
870
|
+
// Use originalQuery if available to preserve source variable names
|
|
871
|
+
const goalForUnification = ctx.originalQuery || node.goal;
|
|
872
|
+
unifications = extractUnifications(goalForUnification, clause);
|
|
727
873
|
const clauseHead = clause.text.includes(':-')
|
|
728
874
|
? clause.text.split(':-')[0].trim()
|
|
729
875
|
: clause.text.trim();
|
|
@@ -766,31 +912,38 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
766
912
|
}
|
|
767
913
|
}
|
|
768
914
|
}
|
|
769
|
-
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
915
|
+
// Skip creating solved node if we just created a match node (at detailed/full levels)
|
|
916
|
+
// The match node already shows the unifications, so the solved node is redundant
|
|
917
|
+
const justCreatedMatchNode = (ctx.detailLevel === 'detailed' || ctx.detailLevel === 'full') &&
|
|
918
|
+
ctx.nodes.length > 0 &&
|
|
919
|
+
ctx.nodes[ctx.nodes.length - 1].type === 'match';
|
|
920
|
+
if (!justCreatedMatchNode) {
|
|
921
|
+
const solvedId = ctx.nextNodeId();
|
|
922
|
+
// Convert binding from X/value to X = value format and map variables to source
|
|
923
|
+
let formattedBinding = node.binding.replace(/([^/]+)\/(.+)/, '$1 = $2');
|
|
924
|
+
if (ctx.originalQuery) {
|
|
925
|
+
formattedBinding = mapGoalVariablesToSource(formattedBinding, ctx.originalQuery);
|
|
926
|
+
}
|
|
927
|
+
const solvedNode = {
|
|
928
|
+
id: solvedId,
|
|
929
|
+
type: 'solved',
|
|
930
|
+
label: `Solved: ${formattedBinding}`,
|
|
931
|
+
emoji: EMOJIS.solved,
|
|
932
|
+
level: node.level,
|
|
933
|
+
};
|
|
934
|
+
ctx.nodes.push(solvedNode);
|
|
935
|
+
// Edge from solving node to solved node - just show the binding
|
|
936
|
+
const doneEdge = {
|
|
937
|
+
id: `edge_${ctx.edges.length}`,
|
|
938
|
+
from: nodeId,
|
|
939
|
+
to: solvedId,
|
|
940
|
+
type: 'active',
|
|
941
|
+
label: formattedBinding,
|
|
942
|
+
stepNumber: ctx.stepCounter(),
|
|
943
|
+
};
|
|
944
|
+
ctx.edges.push(doneEdge);
|
|
945
|
+
lastNodeId = solvedId;
|
|
774
946
|
}
|
|
775
|
-
const solvedNode = {
|
|
776
|
-
id: solvedId,
|
|
777
|
-
type: 'solved',
|
|
778
|
-
label: `Solved: ${formattedBinding}`,
|
|
779
|
-
emoji: EMOJIS.solved,
|
|
780
|
-
level: node.level,
|
|
781
|
-
};
|
|
782
|
-
ctx.nodes.push(solvedNode);
|
|
783
|
-
// Edge from solving node to solved node - just show the binding
|
|
784
|
-
const doneEdge = {
|
|
785
|
-
id: `edge_${ctx.edges.length}`,
|
|
786
|
-
from: nodeId,
|
|
787
|
-
to: solvedId,
|
|
788
|
-
type: 'active',
|
|
789
|
-
label: formattedBinding,
|
|
790
|
-
stepNumber: ctx.stepCounter(),
|
|
791
|
-
};
|
|
792
|
-
ctx.edges.push(doneEdge);
|
|
793
|
-
lastNodeId = solvedId;
|
|
794
947
|
}
|
|
795
948
|
// Update ancestor goal for recursion detection
|
|
796
949
|
// For children, the ancestor is the current node's goal (if it's a user predicate)
|
|
@@ -854,6 +1007,30 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
854
1007
|
let altType;
|
|
855
1008
|
// Get clause number from alternative child
|
|
856
1009
|
const altClauseNumber = altChild.clauseNumber;
|
|
1010
|
+
// Map trace clause number to parsed clause number for display (needed for both labels and edges)
|
|
1011
|
+
let displayAltClauseNumber = altClauseNumber;
|
|
1012
|
+
if (altClauseNumber) {
|
|
1013
|
+
// Check if it's a user predicate to determine if we should map clause numbers
|
|
1014
|
+
let depth = 0;
|
|
1015
|
+
let hasTopLevelComma = false;
|
|
1016
|
+
for (const char of altChild.goal) {
|
|
1017
|
+
if (char === '(' || char === '[')
|
|
1018
|
+
depth++;
|
|
1019
|
+
else if (char === ')' || char === ']')
|
|
1020
|
+
depth--;
|
|
1021
|
+
else if (char === ',' && depth === 0) {
|
|
1022
|
+
hasTopLevelComma = true;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const isUserPredicate = /^[a-z_][a-zA-Z0-9_]*\(/.test(altChild.goal) && !hasTopLevelComma;
|
|
1027
|
+
if (isUserPredicate && newCtx.clauses.length > 0) {
|
|
1028
|
+
const matchedAltClause = findMatchingClause(altChild.goal, newCtx.clauses);
|
|
1029
|
+
if (matchedAltClause) {
|
|
1030
|
+
displayAltClauseNumber = matchedAltClause.number;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
857
1034
|
if (altChild.type === 'success') {
|
|
858
1035
|
altType = 'success';
|
|
859
1036
|
if (newCtx.finalAnswer) {
|
|
@@ -886,14 +1063,6 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
886
1063
|
const isUserPredicate = /^[a-z_][a-zA-Z0-9_]*\(/.test(altChild.goal) && !hasTopLevelComma;
|
|
887
1064
|
const isRecursive = newCtx.detailLevel !== 'minimal' && isRecursiveCall(altChild.goal, newCtx.ancestorGoal || null);
|
|
888
1065
|
const recursivePrefix = isRecursive ? '🔁 Recurse: ' : 'Solve: ';
|
|
889
|
-
// Map trace clause number to parsed clause number for display
|
|
890
|
-
let displayAltClauseNumber = altClauseNumber;
|
|
891
|
-
if (altClauseNumber && isUserPredicate && newCtx.clauses.length > 0) {
|
|
892
|
-
const matchedAltClause = findMatchingClause(altChild.goal, newCtx.clauses);
|
|
893
|
-
if (matchedAltClause) {
|
|
894
|
-
displayAltClauseNumber = matchedAltClause.number;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
1066
|
const clauseLabel = (displayAltClauseNumber && isUserPredicate) ? ` [clause ${displayAltClauseNumber}]` : '';
|
|
898
1067
|
altLabel = `${recursivePrefix}${formattedGoal}${clauseLabel}`;
|
|
899
1068
|
}
|
|
@@ -907,7 +1076,7 @@ function processTreeNode(node, parentId, ctx, parentExecutionNode) {
|
|
|
907
1076
|
};
|
|
908
1077
|
newCtx.nodes.push(altVizNode);
|
|
909
1078
|
// Create edge from parent (or match node) to alternative node
|
|
910
|
-
const backtrackLabel =
|
|
1079
|
+
const backtrackLabel = displayAltClauseNumber ? `clause ${displayAltClauseNumber}` : '';
|
|
911
1080
|
const backtrackEdge = {
|
|
912
1081
|
id: `edge_${newCtx.edges.length}`,
|
|
913
1082
|
from: actualParentForAlt,
|
|
@@ -1051,18 +1220,20 @@ function renameVariablesWithLevel(text, level) {
|
|
|
1051
1220
|
/**
|
|
1052
1221
|
* Extracts the final answer from a successful execution tree.
|
|
1053
1222
|
*/
|
|
1054
|
-
function extractFinalAnswer(root) {
|
|
1055
|
-
//
|
|
1223
|
+
function extractFinalAnswer(root, originalQuery) {
|
|
1224
|
+
// Use original query if available to get source variable names
|
|
1225
|
+
const goalToAnalyze = originalQuery || root.goal;
|
|
1226
|
+
// Extract the query variable from the goal
|
|
1056
1227
|
// Try multiple patterns: member(X, ...), append(..., X), factorial(..., X), etc.
|
|
1057
1228
|
let queryVar = null;
|
|
1058
1229
|
// Pattern 1: First argument is a variable (e.g., member(X, [a,b,c]))
|
|
1059
|
-
const firstArgMatch =
|
|
1230
|
+
const firstArgMatch = goalToAnalyze.match(/^[a-z_][a-zA-Z0-9_]*\(\s*([A-Z][A-Za-z0-9_]*)[\u2080-\u2089]*/);
|
|
1060
1231
|
if (firstArgMatch) {
|
|
1061
1232
|
queryVar = firstArgMatch[1];
|
|
1062
1233
|
}
|
|
1063
1234
|
// Pattern 2: Last argument is a variable (e.g., factorial(5, X))
|
|
1064
1235
|
if (!queryVar) {
|
|
1065
|
-
const lastArgMatch =
|
|
1236
|
+
const lastArgMatch = goalToAnalyze.match(/,\s*([A-Z][A-Za-z0-9_]*)[\u2080-\u2089]*\s*\)/);
|
|
1066
1237
|
if (lastArgMatch) {
|
|
1067
1238
|
queryVar = lastArgMatch[1];
|
|
1068
1239
|
}
|
|
@@ -1071,10 +1242,15 @@ function extractFinalAnswer(root) {
|
|
|
1071
1242
|
let finalBinding;
|
|
1072
1243
|
function findQueryBinding(node) {
|
|
1073
1244
|
if (node.binding && queryVar) {
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1245
|
+
// Binding format is now "variable = value" or "var1 = val1, var2 = val2"
|
|
1246
|
+
// Split by comma and check each binding
|
|
1247
|
+
const bindings = node.binding.split(',').map(b => b.trim());
|
|
1248
|
+
for (const binding of bindings) {
|
|
1249
|
+
const match = binding.match(/^([A-Z][A-Za-z0-9_]*)[\u2080-\u2089]*\s*=\s*(.+)$/);
|
|
1250
|
+
if (match && match[1] === queryVar) {
|
|
1251
|
+
finalBinding = `${match[1]} = ${match[2]}`;
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1078
1254
|
}
|
|
1079
1255
|
}
|
|
1080
1256
|
for (const child of node.children) {
|
|
@@ -1082,11 +1258,7 @@ function extractFinalAnswer(root) {
|
|
|
1082
1258
|
}
|
|
1083
1259
|
}
|
|
1084
1260
|
findQueryBinding(root);
|
|
1085
|
-
|
|
1086
|
-
if (finalBinding) {
|
|
1087
|
-
return finalBinding.replace(/^([A-Z][A-Za-z0-9_]*)[\u2080-\u2089]*\/(.+)/, '$1 = $2');
|
|
1088
|
-
}
|
|
1089
|
-
return undefined;
|
|
1261
|
+
return finalBinding;
|
|
1090
1262
|
}
|
|
1091
1263
|
/**
|
|
1092
1264
|
* Determines the execution order (left-to-right, depth-first).
|