Found while preparing experiments for the ASPOCP'26 paper (Skvortsov, Xia, Garg, Bowers, Ludäscher). Logica @ GitHub main (2026-08-06), clingo 5.8.1, DuckDB 1.5.5, macOS; installed via pip install git+https://github.com/EvgSkv/logica in a fresh venv.
Summary
A recursive cycle that passes through a Clingo() call is evaluated for exactly one round — the feedback edge is effectively cut — at every @Recursive budget. Inspecting the generated SQL shows the unroller does attempt to unfold the cycle, but emits a scrambled frame graph: several iteration-frame tables are CREATE TABLE'd twice from different inputs and overwrite each other mid-execution. A no-Clingo control with the same shape (functional predicate inside the SCC) fails outright with a DuckDB binder error, suggesting the root cause is general: recursion through functional (=) predicates in an SCC.
Repro 1 — one round instead of iteration
@Engine("duckdb", clingo: {time_limit: ∞, models_limit: ∞});
# Candidates: "i", and the successor (one more "i") of anything picked.
In(x) distinct :- x == "i" | Out(y), x == y ++ "i";
# ASP fragment: free choice; union over all models = all candidates.
Pick(x) couldbe :- Cand(x);
M() = Clingo(["Pick", "Cand"],
List{ClingoFact("Cand", [x]) :- In(x)});
Out(x) distinct :-
ExtractClingoCall(x, predicate: "Pick", model_id:) = M();
logica grow.l run Out → {"i"} (one round).
Control without the solver (same growth recursion):
@Engine("duckdb");
In(x) distinct :- x == "i" | In(y), x == y ++ "i", Length(y) < 20;
→ "i" … "iiiii" (normal iterative evaluation). Adding @Recursive(In, 12) or (24) to the Clingo version adds exactly one _ifr frame to the plan and never changes the result.
Evidence: the generated frame graph is incoherent
logica grow.l print Out emits frames In_ifr0..4, M_ifr0..4, Out_ifr1..4, and the feedback edge is present (In_ifr4 reads Out_ifr3.col0 || 'i') — so this is not a deliberate cut. But mapping each CREATE TABLE to the tables it reads gives three interleaved chains:
In_ifr1 (base) -> M_ifr2 -> Out_ifr3 -> In_ifr4 -> M_ifr3
M_ifr0 (base) -> Out_ifr1 -> In_ifr2 -> M_ifr3 (M_ifr3 created 2nd time)
In_ifr0 (base) -> M_ifr1 -> Out_ifr2 -> In_ifr3 -> M_ifr4
Out_ifr3 <- M_ifr3, M_ifr4 (Out_ifr3 created 2nd time)
final SELECT <- Out_ifr3, M_ifr3, M_ifr4
M_ifr3, In_ifr3, and Out_ifr3 are each created twice with different inputs; sequential execution silently overwrites, and the final answer reads from mixed chains.
Repro 2 — same shape without Clingo doesn't even bind
@Engine("duckdb");
In(x) distinct :- x == "i" | Out(y), x == y ++ "i";
F() = List{x :- In(x)};
Out(x) distinct :- x in F(), Length(x) < 6;
→ _duckdb.BinderException: Binder Error: UNNEST not supported here
So the pure functional-predicate-in-SCC case errors, while the Clingo case produces running-but-wrong SQL.
Suggested fix
Until the unroller supports SCCs containing functional predicates (and Clingo() calls in particular): detect and reject them at compile time with a clear message. Silent one-round evaluation is the worst outcome — the ASPOCP camera-ready now documents the current behavior as "mutual recursion across the boundary is currently not iterated."
Smaller issues found in the same session (can file separately if useful)
- Empty
List{ClingoFact(...) :- ...} (zero rows) makes the facts argument NULL and Clingo() silently returns NULL — no error, no models.
~P(x) over fact-fed predicates in a cantbe rule makes the dependency check demand a synthesized internal predicate: "Predicate MustPick depends on IsNull but IsNull was not requested."
Count{...} shouldbe passes the compiler but generates ASP that clingo rejects with "parsing failed" (only Max/Min are documented for optimization).
- Docs/paper use
@Engine("duckdb", clingo: {time_limit: inf, ...}) but the parser only accepts ∞ — inf fails with "Could not understand arguments of annotation."
🤖 Generated with Claude Code
https://claude.ai/code/session_01NA7AvfwRmaHjWwcsFgbxo4
Found while preparing experiments for the ASPOCP'26 paper (Skvortsov, Xia, Garg, Bowers, Ludäscher). Logica @ GitHub main (2026-08-06), clingo 5.8.1, DuckDB 1.5.5, macOS; installed via
pip install git+https://github.com/EvgSkv/logicain a fresh venv.Summary
A recursive cycle that passes through a
Clingo()call is evaluated for exactly one round — the feedback edge is effectively cut — at every@Recursivebudget. Inspecting the generated SQL shows the unroller does attempt to unfold the cycle, but emits a scrambled frame graph: several iteration-frame tables areCREATE TABLE'd twice from different inputs and overwrite each other mid-execution. A no-Clingo control with the same shape (functional predicate inside the SCC) fails outright with a DuckDB binder error, suggesting the root cause is general: recursion through functional (=) predicates in an SCC.Repro 1 — one round instead of iteration
logica grow.l run Out→{"i"}(one round).Control without the solver (same growth recursion):
→
"i" … "iiiii"(normal iterative evaluation). Adding@Recursive(In, 12)or(24)to the Clingo version adds exactly one_ifrframe to the plan and never changes the result.Evidence: the generated frame graph is incoherent
logica grow.l print Outemits framesIn_ifr0..4,M_ifr0..4,Out_ifr1..4, and the feedback edge is present (In_ifr4readsOut_ifr3.col0 || 'i') — so this is not a deliberate cut. But mapping eachCREATE TABLEto the tables it reads gives three interleaved chains:M_ifr3,In_ifr3, andOut_ifr3are each created twice with different inputs; sequential execution silently overwrites, and the final answer reads from mixed chains.Repro 2 — same shape without Clingo doesn't even bind
→
_duckdb.BinderException: Binder Error: UNNEST not supported hereSo the pure functional-predicate-in-SCC case errors, while the Clingo case produces running-but-wrong SQL.
Suggested fix
Until the unroller supports SCCs containing functional predicates (and Clingo() calls in particular): detect and reject them at compile time with a clear message. Silent one-round evaluation is the worst outcome — the ASPOCP camera-ready now documents the current behavior as "mutual recursion across the boundary is currently not iterated."
Smaller issues found in the same session (can file separately if useful)
List{ClingoFact(...) :- ...}(zero rows) makes the facts argument NULL andClingo()silently returns NULL — no error, no models.~P(x)over fact-fed predicates in acantberule makes the dependency check demand a synthesized internal predicate: "Predicate MustPick depends on IsNull but IsNull was not requested."Count{...} shouldbepasses the compiler but generates ASP that clingo rejects with "parsing failed" (onlyMax/Minare documented for optimization).@Engine("duckdb", clingo: {time_limit: inf, ...})but the parser only accepts∞—inffails with "Could not understand arguments of annotation."🤖 Generated with Claude Code
https://claude.ai/code/session_01NA7AvfwRmaHjWwcsFgbxo4