fd-in x dom-list: narrows x's domain. If x is a ground number, checks
membership; if x is a logic var, intersects existing domain (or sets
fresh) and stores via fd-set-domain. Fails if domain becomes empty.
fd-label vars: drives search by enumerating each var's domain. Each
var is unified with each value in its domain, in order, via mk-mplus
of singleton streams.
Forward: (fd-in x dom) (fd-label (list x)) iterates x over dom.
Intersection: two fd-in goals on the same var compose via dom-intersect.
Disjoint domains -> empty answer set. Ground value membership check
gates pass/fail. Composes with the rest of the miniKanren machinery —
fresh / conde / membero etc. all work alongside.
9 new tests, 586/586 cumulative.
Foundation for native CLP(FD). The substitution dict carries a reserved
"_fd" key holding a constraint store:
{:domains {var-name -> sorted-int-list}
:constraints (... pending constraints ...)}
This commit ships only the domain machinery + accessors:
fd-dom-from-list / fd-dom-range / fd-dom-empty? / fd-dom-singleton?
fd-dom-min / fd-dom-max / fd-dom-member? / fd-dom-intersect /
fd-dom-without
fd-store-of / fd-domain-of / fd-set-domain / fd-with-store
fd-set-domain returns nil when the domain becomes empty (failure),
which is the wire signal subsequent constraint goals will consume.
The constraints field is reserved for the next iteration.
26 new tests, 577/577 cumulative.