Got it. Hmm.
The problem is that the partial derivations do need to be tabled completely: otherwise the issue of cycle detection comes back.
Here would be a very beautiful world that probably has problems. I’m not going to implement it but I think the fact it should work is really cool.
Partial derivations would be stored like you’d expect. So implies(trait(X, p, v), trait(X, p2, v2), D) would have some value for D in the table: say
[theorem(trait(Y, p, v), trait(Y, intermediate, v)), theorem(trait(Y, intermediate, v), trait(Y, p2, v2)]
Here I’m using Y to represent a variable which is internal to the table.
Upon querying, we make a copy of this list with a fresh variable, and never unify the table variable Y with our input. Makes sense.
But in the table, if I look for implies(trait(X, intermediate, v), trait(X, p2, v2), D), then the table would have stored
[theorem(trait(Y, intermediate, v), trait(Y, p2, v2)]
with the same internal Y. That would allow for linear space complexity.
As far as I’m aware, this is sound, because every time we get a value from the table we deep-copy it anyway. And if we finished doing subgoal/n and goal/n backtracks, oh well, no goal/n for the table, but subgoal/n is doing just fine. After each subgoal finishes, we copy its value out for our use, then use it, and then, if our main goal succeeds, (somehow) copy the main goal’s value, keeping track of the subgoal’s value within it, and re-unify that subgoal’s value with the value from the table. Then put the main goal value (which has now been copied, and so the only thing it shares with is the subgoal value from the table), into the table. And we’re still disjoint from the stacks.
I don’t know the most general terms in which this optimization can be phrased, but it’s interesting. Of course it costs an extra unification.
Of course, that isn’t the real world. So I’ll think about other options later.
Edit: Well when I put it like that, it seems pretty clear that nodes totally would work. Oops!
In fact, all a node would be is just the clause that was used (and perhaps variable instantiations). So if I queried some trait—if it was successful, it would provide me the clause that it used to derive the trait. From there, I call each node in the body, which calls each node in its body, etc. to reconstruct a tree. If the proof fails, no node is returned.