The advanced/ module extends UnifyWeaver’s basic recursion support with sophisticated pattern detection and compilation strategies. It handles tail recursion, linear recursion, and mutual recursion through SCC (Strongly Connected Components) analysis.
Priority-Based Compilation: Try simpler patterns before complex ones
Separation of Concerns: Advanced patterns isolated from basic compiler
recursive_compiler.plsrc/unifyweaver/core/advanced/src/unifyweaver/core/advanced/
├── advanced_recursive_compiler.pl # Orchestrator (main entry point)
├── call_graph.pl # Build predicate dependency graphs
├── scc_detection.pl # Tarjan's SCC algorithm
├── pattern_matchers.pl # Pattern detection utilities
├── tail_recursion.pl # Tail recursion → loop compiler
├── linear_recursion.pl # Linear recursion → memoized compiler
├── tree_recursion.pl # Tree recursion → structural compiler
├── mutual_recursion.pl # Mutual recursion → joint memo compiler
└── test_advanced.pl # Comprehensive test suite
Pattern: Recursive call is the last operation in a clause
% Accumulator pattern (arity 3)
count([], Acc, Acc).
count([_|T], Acc, N) :-
Acc1 is Acc + 1,
count(T, Acc1, N).
Compilation Strategy: Convert to bash for loop with accumulator variable
Detected by: pattern_matchers:is_tail_recursive_accumulator/2
Pattern: One OR MORE recursive calls per clause, with independent calls
Key Requirements:
[V,L,R] patterns)Examples:
Single recursive call (classic linear):
length([], 0).
length([_|T], N) :-
length(T, N1),
N is N1 + 1.
Multiple recursive calls (fibonacci-style):
fib(0, 0).
fib(1, 1).
fib(N, F) :-
N > 1,
N1 is N - 1, % Pre-computed
N2 is N - 2, % Pre-computed
fib(N1, F1), % Independent call
fib(N2, F2), % Independent call
F is F1 + F2. % Aggregation
Compilation Strategy: Memoization with associative arrays
Detected by: pattern_matchers:is_linear_recursive_streamable/1
See also: docs/RECURSION_PATTERN_THEORY.md for detailed theory on variable independence
Excluding Predicates from Linear Recursion:
Sometimes you want to prevent a predicate from being compiled as linear recursion:
% Mark predicate as forbidden for linear recursion
forbid_linear_recursion(my_pred/2).
% Check if forbidden
is_forbidden_linear_recursion(my_pred/2). % true
% Remove forbid
clear_linear_recursion_forbid(my_pred/2).
Automatic exclusion via constraints:
ordered constraint are automatically forbiddenUse cases:
See docs/RECURSION_PATTERN_THEORY.md for detailed examples.
Pattern: Structural decomposition with recursive calls on structure parts
Key Requirements:
[V,L,R] for trees)Example:
tree_sum([], 0).
tree_sum([V, L, R], Sum) :-
tree_sum(L, LS), % Recurse on left subtree
tree_sum(R, RS), % Recurse on right subtree
Sum is V + LS + RS. % Combine
Compilation Strategy: Generate tree parser and recursive structure handling
Detected by: tree_recursion:is_tree_recursive/1
Note: Tree recursion is for STRUCTURAL patterns. Fibonacci-style patterns with multiple calls on SCALARS compile as linear recursion with aggregation (see above).
Pattern: Multiple predicates calling each other in a cycle
is_even(0).
is_even(N) :- N > 0, N1 is N - 1, is_odd(N1).
is_odd(1).
is_odd(N) :- N > 1, N1 is N - 1, is_even(N1).
Compilation Strategy:
Detected by: scc_detection:find_sccs/2
% Compile single predicate with advanced patterns
advanced_recursive_compiler:compile_advanced_recursive(+Pred/Arity, +Options, -BashCode)
% Compile group of predicates (for explicit mutual recursion)
advanced_recursive_compiler:compile_predicate_group(+[Pred/Arity, ...], +Options, -BashCode)
All advanced compilers accept an Options parameter for configuration:
% All compilers support Options
compile_tail_recursion(Pred/Arity, Options, BashCode).
compile_linear_recursion(Pred/Arity, Options, BashCode).
compile_mutual_recursion(Predicates, Options, BashCode).
Options Format: List of Key=Value pairs
[unique(true), ordered=false, output_lang=bash]Constraint Integration:
constraint_analyzer:get_constraints/2 for predicate constraints.tail_recursion compiler now uses the unique(true) constraint to generate more efficient code. When this constraint is present, an exit 0 is added to the generated bash script, causing it to terminate immediately after producing its single result.recursive_compiler.pl automatically tries advanced patterns before falling back to basic memoization:
compile_recursive(Pred/Arity, Options, BashCode) :-
% ... try basic patterns ...
;
catch(
advanced_recursive_compiler:compile_advanced_recursive(
Pred/Arity, Options, BashCode
),
error(existence_error(procedure, _), _),
fail
) ->
true
;
% Fall back to basic memoization
compile_memoized_recursion(Pred, Arity, Options, BashCode)
).
Uses Tarjan’s algorithm for finding strongly connected components:
index = lowlink, pop SCC from stackTime Complexity: O(V + E) where V = predicates, E = calls
Implementation: scc_detection.pl
Uses list-of-strings for bash templates (better readability):
TemplateLines = [
"#!/bin/bash",
"# - tail recursive",
"() {",
" echo 'Hello'",
"}"
],
atomic_list_concat(TemplateLines, '\n', Template),
render_template(Template, [pred=PredStr], BashCode).
Benefits:
Tail Recursion Example:
#!/bin/bash
# count_items - tail recursive accumulator pattern
count_items() {
local input="$1"
local acc="$2"
# Convert to array, iterate with accumulator
for item in "${items[@]}"; do
acc=$((acc + 1))
done
echo "$acc"
}
Mutual Recursion Example:
#!/bin/bash
# Mutually recursive group: is_even_is_odd
declare -gA is_even_is_odd_memo
is_even() {
local key="is_even:$*"
[[ -n "${is_even_is_odd_memo[$key]}" ]] && echo "${is_even_is_odd_memo[$key]}" && return
# ... base cases, recursive logic ...
is_even_is_odd_memo["$key"]="$result"
}
is_odd() {
local key="is_odd:$*"
[[ -n "${is_even_is_odd_memo[$key]}" ]] && echo "${is_even_is_odd_memo[$key]}" && return
# ... base cases, recursive logic ...
is_even_is_odd_memo["$key"]="$result"
}
Prolog Source:
sum_list([], Acc, Acc).
sum_list([H|T], Acc, Sum) :-
Acc1 is Acc + H,
sum_list(T, Acc1, Sum).
Detection:
?- is_tail_recursive_accumulator(sum_list/3, Info).
Info = acc_pattern([clause(sum_list([], Acc, Acc), true)],
[clause(sum_list([H|T], Acc, Sum), (...))],
2). % Accumulator at position 2
Generated Bash Code:
#!/bin/bash
# sum_list - tail recursive accumulator pattern
# Compiled to iterative while loop
sum_list() {
local input="$1"
local acc="$2"
local result_var="$3"
# Convert input to array if it's a list notation
if [[ "$input" =~ ^\[.*\]$ ]]; then
input="${input#[}"
input="${input%]}"
IFS=',' read -ra items <<< "$input"
else
items=()
fi
local current_acc="$acc"
# Iterative loop (tail recursion optimization)
for item in "${items[@]}"; do
# Step operation: Acc1 is Acc + H
current_acc=$((current_acc + item))
done
# Return result
if [[ -n "$result_var" ]]; then
eval "$result_var=$current_acc"
else
echo "$current_acc"
fi
}
# Helper function for common use case
sum_list_eval() {
sum_list "$1" 0 result
echo "$result"
}
Usage:
$ source sum_list.sh
$ sum_list "[1,2,3]" 0 result
$ echo $result
6
$ sum_list_eval "[5,10,15]"
30
Prolog Source:
factorial(0, 1).
factorial(N, F) :-
N > 0,
N1 is N - 1,
factorial(N1, F1),
F is F1 * N.
Detection:
?- is_linear_recursive_streamable(factorial/2).
true. % Single recursive call (classic linear recursion)
Generated Bash Code:
#!/bin/bash
# factorial - linear recursive with memoization
declare -gA factorial_memo
factorial() {
local n="$1"
local expected="$2"
# Memoization check
local key="${n}"
if [[ -n "${factorial_memo[$key]}" ]]; then
local cached="${factorial_memo[$key]}"
if [[ -n "$expected" ]]; then
[[ "$cached" == "$expected" ]] && echo "true" || echo "false"
else
echo "$cached"
fi
return
fi
# Base case: factorial(0, 1)
if [[ "$n" == "0" ]]; then
factorial_memo["$key"]="1"
if [[ -n "$expected" ]]; then
[[ "1" == "$expected" ]] && echo "true" || echo "false"
else
echo "1"
fi
return
fi
# Recursive case
if (( n > 0 )); then
local n1=$((n - 1))
local f1=$(factorial "$n1" "")
local f=$((f1 * n))
factorial_memo["$key"]="$f"
if [[ -n "$expected" ]]; then
[[ "$f" == "$expected" ]] && echo "true" || echo "false"
else
echo "$f"
fi
fi
}
Usage:
$ source factorial.sh
$ factorial 5
120
$ factorial 10
3628800
# Second call uses memoization - instant!
$ factorial 5
120
Prolog Source:
is_even(0).
is_even(N) :- N > 0, N1 is N - 1, is_odd(N1).
is_odd(1).
is_odd(N) :- N > 1, N1 is N - 1, is_even(N1).
Detection:
?- build_call_graph([is_even/1, is_odd/1], Graph).
Graph = [(is_even/1 -> is_odd/1), (is_odd/1 -> is_even/1)].
?- find_sccs(Graph, SCCs).
SCCs = [[is_even/1, is_odd/1]]. % Single SCC = mutual recursion
Generated Bash Code:
#!/bin/bash
# Mutually recursive group: is_even, is_odd
# Shared memoization table for the SCC
declare -gA even_odd_memo
is_even() {
local n="$1"
local key="is_even:${n}"
# Check memo
if [[ -n "${even_odd_memo[$key]}" ]]; then
echo "${even_odd_memo[$key]}"
return
fi
# Base case
if [[ "$n" == "0" ]]; then
even_odd_memo["$key"]="true"
echo "true"
return
fi
# Recursive case: call is_odd
if (( n > 0 )); then
local n1=$((n - 1))
local result=$(is_odd "$n1")
even_odd_memo["$key"]="$result"
echo "$result"
else
even_odd_memo["$key"]="false"
echo "false"
fi
}
is_odd() {
local n="$1"
local key="is_odd:${n}"
# Check memo
if [[ -n "${even_odd_memo[$key]}" ]]; then
echo "${even_odd_memo[$key]}"
return
fi
# Base case
if [[ "$n" == "1" ]]; then
even_odd_memo["$key"]="true"
echo "true"
return
fi
# Recursive case: call is_even
if (( n > 1 )); then
local n1=$((n - 1))
local result=$(is_even "$n1")
even_odd_memo["$key"]="$result"
echo "$result"
else
even_odd_memo["$key"]="false"
echo "false"
fi
}
Usage:
$ source even_odd.sh
$ is_even 4
true
$ is_even 7
false
$ is_odd 3
true
$ is_odd 8
false
user:clause TechniqueProblem: Pattern matchers in modules can’t see predicates defined in other modules or user context.
Solution: Use user:clause/2 instead of clause/2:
% ❌ WRONG - Won't see predicates from other modules
my_pattern_matcher(Pred/Arity) :-
functor(Head, Pred, Arity),
clause(Head, Body), % Only sees current module!
analyze(Body).
% ✅ CORRECT - Sees predicates in user namespace
my_pattern_matcher(Pred/Arity) :-
functor(Head, Pred, Arity),
user:clause(Head, Body), % Sees user predicates!
analyze(Body).
When to use:
Example from pattern_matchers.pl:
is_tail_recursive_accumulator(Pred/Arity, AccInfo) :-
functor(BaseHead, Pred, Arity),
user:clause(BaseHead, true), % ← user: prefix
functor(RecHead, Pred, Arity),
user:clause(RecHead, RecBody), % ← user: prefix
contains_call_to(RecBody, Pred/Arity).
Test predicates must also use user namespace:
% In test code
test_my_predicate :-
% ❌ WRONG
assertz(foo(X) :- bar(X)), % Goes to current module
% ✅ CORRECT
assertz(user:(foo(X) :- bar(X))), % Goes to user namespace
my_pattern_matcher(foo/1). % Now can see foo/1!
?- [test_advanced].
?- test_all_advanced. % Run all module tests
?- test_all. % Include integration, performance, regression tests
?- test_call_graph.
?- test_scc_detection.
?- test_pattern_matchers.
?- test_tail_recursion.
?- test_linear_recursion.
?- test_mutual_recursion.
?- test_advanced_compiler.
Tests generate bash scripts in output/advanced/:
count_items.sh - Tail recursion examplelist_length.sh - Linear recursion exampleeven_odd.sh - Mutual recursion exampleProblem: How to identify mutual recursion groups?
Solution: SCCs group predicates that call each other cyclically
Benefit: Handles arbitrary-sized mutual recursion (not just pairs)
Problem: Multiple patterns might match same predicate
Solution: Try simplest (most optimized) pattern first
Benefit: Ensures best performance for each pattern type
Problem: Don’t want to modify existing working code
Solution: Optional hook with graceful fallback
Benefit:
Symptom: Your tail recursive predicate compiles to memoization instead of iterative loop
Causes:
Solutions:
pred(..., Acc, Result) with Acc = Result in base caseis_tail_recursive_accumulator/2 to debugExample:
% ❌ NOT tail recursive - multiplication happens after
length([], 0).
length([_|T], N) :-
length(T, N1),
N is N1 + 1. % ← Not tail recursive!
% ✅ Tail recursive - uses accumulator
length_acc(List, Len) :- length_acc(List, 0, Len).
length_acc([], Acc, Acc).
length_acc([_|T], Acc, Len) :-
Acc1 is Acc + 1,
length_acc(T, Acc1, Len). % ← Last operation!
Symptom: Error when loading pattern_matchers.pl
Solution: Ensure you’re using SWI-Prolog (other Prolog systems may use different module syntax)
Symptom: Syntax errors or incorrect output from generated scripts
Debugging:
bash -n output/advanced/yourscript.shbash -x output/advanced/yourscript.shSymptom: Warning: Singleton variables: [X]
Cause: Variable appears only once in a clause
Fix: Prefix unused variables with underscore:
% ❌ Warning
expr_to_bash(Acc + Const, Expr) :-
% ✅ No warning
expr_to_bash(_Acc + Const, Expr) :-
Symptom: Even/odd style predicates compile separately instead of as a group
Debugging:
?- build_call_graph([is_even/1, is_odd/1], Graph).
% Should show: [(is_even/1->is_odd/1), (is_odd/1->is_even/1)]
?- find_sccs(Graph, SCCs).
% Should show: [[is_even/1, is_odd/1]]
Common cause: Predicates not asserted to user namespace:
% ✅ Correct
assertz(user:(is_even(0))).
assertz(user:(is_even(N) :- ...)).
Tail recursion (iterative loop):
Linear recursion (memoization):
When to use which:
Benefits:
Costs:
Best for:
Not worth it for:
Last updated: 2025-10-11