Skip to content

Add priv/prelude.pl: the textbook why meta-interpreter - #4

Merged
vbergeron merged 3 commits into
mainfrom
claude/textbook-why-meta-interpreter-m4e4mb
Aug 5, 2026
Merged

vbergeron merged 3 commits into
mainfrom
claude/textbook-why-meta-interpreter-m4e4mb

Conversation

@vbergeron

Copy link
Copy Markdown
Owner

Ships an example for MANIFOLD_PRELUDE: solve/2, the vanilla meta-circular
interpreter from Sterling & Shapiro's The Art of Prolog, extended with a
second argument that keeps the derivation (fact/1, rule/2, builtin/1)
instead of discarding it. why/2 is solve/2 under the name a caller wants
to ask for, e.g. ?- why(mortal(socrates), Proof) in question mode.

It only uses kernel-resident predicates (clause/2, predicate_property/2,
call/1) since it loads ahead of the unknown=fail flag that permanently
breaks autoloading for the engine's lifetime (see
test/smoke/prolog_autoload_test.exs). predicate_property/2 routes true
built-ins (>/2, is/2, +/1, ...) around clause/2 before it's reached,
since clause/2 raises a permission_error on those rather than failing.

test/integration/why_meta_interpreter_test.exs consults the shipped file
itself (not a re-typed copy) and checks the proof shape for a fact, a
rule, a rule whose body calls a built-in, and the two failure paths
(goal doesn't hold; predicate never heard of).

Verified the Prolog-level semantics directly against a real swipl (not
available as an Elixir toolchain in this environment) and cross-checked
the JSON compound-term encoding against the installed
library(term_to_json) source to confirm the MQI wire shapes asserted in
the test.

claude added 3 commits August 5, 2026 16:04
Ships an example for MANIFOLD_PRELUDE: solve/2, the vanilla meta-circular
interpreter from Sterling & Shapiro's The Art of Prolog, extended with a
second argument that keeps the derivation (fact/1, rule/2, builtin/1)
instead of discarding it. why/2 is solve/2 under the name a caller wants
to ask for, e.g. `?- why(mortal(socrates), Proof)` in question mode.

It only uses kernel-resident predicates (clause/2, predicate_property/2,
call/1) since it loads ahead of the unknown=fail flag that permanently
breaks autoloading for the engine's lifetime (see
test/smoke/prolog_autoload_test.exs). predicate_property/2 routes true
built-ins (>/2, is/2, \+/1, ...) around clause/2 before it's reached,
since clause/2 raises a permission_error on those rather than failing.

test/integration/why_meta_interpreter_test.exs consults the shipped file
itself (not a re-typed copy) and checks the proof shape for a fact, a
rule, a rule whose body calls a built-in, and the two failure paths
(goal doesn't hold; predicate never heard of).

Verified the Prolog-level semantics directly against a real swipl (not
available as an Elixir toolchain in this environment) and cross-checked
the JSON compound-term encoding against the installed
library(term_to_json) source to confirm the MQI wire shapes asserted in
the test.
solve/2's conjunction clause used to mirror ,/2's own right-associative
shape: (ProofA, (ProofB, ProofC)) for a three-goal body. That's a proof
whose structure reflects how the source text happened to associate
rather than the derivation itself, and it makes a three-goal body
indistinguishable in shape from "two goals, the second of which is
itself two goals."

conj_list/2 walks the ,/2 chain once and flattens it into
[ProofA, ProofB, ProofC]. A single-goal body is unaffected — it was
never a conjunction, so it stays a bare proof term rather than a
one-element list.

Also pins :- encoding(utf8) as the very first line: consulting the file
under a non-UTF-8 locale (LC_CTYPE=POSIX, reproduced in this sandbox)
otherwise misreads the em dashes in the header comment as "Illegal
multibyte Sequence" — harmless today since it only corrupts comments,
but not something to leave load-bearing on the deployment's locale.

Re-verified both changes directly against swipl (still no Elixir
toolchain in this environment): the three-goal case now prints
rule(triple(widget),[fact(price(widget,150)),builtin(150>100),
builtin(150<1000)]) instead of the nested-tuple shape, and consulting
the file under LC_CTYPE=POSIX no longer warns.
solve/2 gets a dedicated (A ; B) clause: try the left arm, and only if
that fails, the right, exactly like ;/2 itself. The proof keeps only
the arm that actually fired, tagged either(left, ProofA) or
either(right, ProofB), instead of `builtin((A;B))` treating the whole
disjunction as one opaque call. It stays exactly as non-deterministic
as plain ;/2: backtracking (findall/3 over why/2) reaches every left
solution before falling through to the right, one either/2 per
solution.

Placement matters and is now load-bearing: the ;/2 clause has to come
before the generic builtin/1 clause, or predicate_property((A;B),
built_in) would claim the disjunction first and call/1 it as one step
— the exact outcome either/2 exists to avoid. It also has to come
before fact/1 and rule/2, since clause((A;B), true) raises the same
permission_error clause(1>2, _) does.

(Cond -> Then ; Else) still gets partial treatment worth documenting:
the outer ;/2 now reports which side fired, but the if-then arm itself
is a ->/2 term and stays one opaque builtin((Cond->Then)) leaf, since
solve/2 still has no dedicated ->/2 clause.

test/integration/why_meta_interpreter_test.exs: two new cases — one
arm firing (plato / vegetarian) and both arms firing on backtracking
(socrates satisfies both branches, checked by sorting the two
either/2-tagged proofs rather than assuming solution order).

Re-verified against real swipl (still no Elixir toolchain here):
either/left and either/right tagging, a 3-way disjunction chain
nesting either(right, either(right, ...)), disjunction embedded inside
a conjunction's flattened list, the if-then-else partial-opacity case,
and findall/3 backtracking giving both tagged proofs for socrates.
@vbergeron
vbergeron merged commit aa8a95e into main Aug 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants