⏺ All Stage C column kinds work correctly — compound (enumerate + partial #22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fact-table-stage-C"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
bind), list, float (both directions), big-int, mixed immediate-col0 +
compound-col1, and compound col-0 with binary-search disabled (full scan +
restore + unify, including partial binding rel(point(N,4),Y) → N=3). Now
the detection widened to ground (not just immediate), so compound-column
facts that were per-clause now route through the table — let me run the
full suite to see exactly what that shifts:
Review — Fact-table Stage C (ground compound/list/float/big-int columns)
The architecture is right and reuses the existing infrastructure well. Three load-bearing calls:
tag_of(col) in {TAG_ATOM, TAG_INT}→ unify inline; else → it's a blob-ref thatrestore_cellsmaterializes onto the heap, then unify. The cell tag answers "immediate or blob?" with no extra word per column.restore_cellsextracted fromrestore_from_buf, so the same materialization code that handlesTermBuf(dynamic, used by error preservation through backtracking) handles the read-only.rodatablob. Two consumers, one walker. The doc note that "those blobs are ground, so theTAG_REFarm never fires there" pins the invariant precisely.Other strong calls:
serialize_argwith a work queue (rather than recursion) means deep ground terms — long lists, nested compounds — can't overflow the compiler stack.!ir.contains("_blob =")). Pay-for-what-you-use carries through.m(1, point(2, 3))indexes on the immediate col 0 and restores the compound col 1 — the index gate is(0..nrows).all(|r| ATOM|INT in col 0)per predicate, not all-or-nothing on the whole table.rel(point(N, 4), Y) → N=3, Y=bbtest pins that the fallback path correctly handles partial binding through the restored compound.f64::to_bits()↔ blob ↔ TAG_FLT heap cell. Thepii(p, 3.14)test asserts a ground float query unifies (which is bit-equality, not numeric equality — the only sane choice).i61serializes to a BIG blob cell, not a runtime-built box. The golden test pins the absence ofcall i64 @plg_rt_put_bigin the fact-table path — confirms that the big-int genuinely lives in.rodatarather than being boxed per-row at startup.Issues below — none are blockers.
Real concerns
1. Tag constants are now duplicated across compiler and runtime without a compile-time agreement check.
crates/compiler/src/codegen/facts.rshardcodes:These mirror
plg-runtime/src/cell.rs. If the runtime's tag values, tag width (the& 7mask assumes 3 bits), shift count (<< 3), or functor-packing layout ever changes one-sidedly, the codegen emits garbage into the.rodatablob and the runtime decodes it as something else — silently. The PR description calls this "the ABI contract for the serialized blob," but there's no enforcement.Three options, increasingly principled:
TAG_ATOM_compiler == TAG_ATOM_runtime(and friends). One-shot drift detector.mk/pack_functorhelpers intoplg-shared(or a tiny newplg-cellcrate) andusethem in both places. One source of truth.static_assertions::const_assert_eq!at the compiler side that references the runtime's pub constant directly. Compile-time check, no runtime cost.This is the same class of ABI-drift exposure as PR #15's
NO_SITEconstants (two crates, same value, hopefully). That one was small enough for a doc note; this one is six tag values plus two packing helpers, with the surface growing if Stage D/E adds more cell kinds. Lifting toplg-sharedwould close the class.2. Compound col 0 missed-indexing opportunity, not a bug. Today: a compound col 0 cell stores a blob-ref
STRword whose payload is the blob offset for that serialization. Two facts with identical compound col 0 (e.g. two clauses withpoint(1, 2)as col 0) serialize to different blob offsets, so their u64 values differ — even though they're semantically equal. The PR correctly avoids indexing on this. A future optimization (interning ground compounds in the blob — already-seen subterms reuse offsets) would make col-0 sort by blob-offset semantically meaningful and re-enable the index. Worth a one-line "interned blob would allow Stage D to re-index compound col 0" note in FACT_TABLE.md. Not in scope for Stage C.Small observations
3. NaN in float columns is the same gotcha I flagged for Stage B's future float-indexing case. Today's design tests bit-equality (the
to_bits()/restore path), sonan(value)with the same NaN bit pattern would round-trip; two NaNs with different bit patterns wouldn't unify even though both are NaN. This matches ISO Prolog semantics for==(term-identity), so the behavior is correct. Worth a one-line "ground-fact float columns compare by IEEE-754 bit pattern; different NaN encodings do not unify" in FACT_TABLE.md to head off "wait, why didnannot match my fact?" confusion.4. No test for a deep ground term. The iterative
serialize_argwas explicitly designed to avoid compiler-stack overflow on deep structures (the comment says so). Adata(deep_nested_list_of_1000_atoms)test would confirm the iteration actually works at depth where recursion would have blown. Cheap to add; pins the pattern's value.5. The frame slot count keeps growing. Stage A: 8 prefix slots. Stage B: 10 (added IDX, END). Stage C: 12 (added BLOB, BLOBLEN). Each non-immediate fact-table predicate's frame is 12+arity cells per invocation, and each
path/2-style recursion lives one frame deep. Modest cost (cells live on the machine heap, not the C stack), but worth noting that the frame is now ~1.5× Stage A. If Stage D/E adds more, a struct-shaped frame layout (rather than positional constants) would scale better. Stylistic only.6.
has_blobis computed by!blob.is_empty(). Correct, but a predicate whose only non-immediate column happens to be the empty list[]actually doesn't need a blob (the empty list is an atom[], an immediate). So today the check matches "blob has content," which is the right semantics. Just confirming the flag's name reads as intended — "did serialization actually produce something" rather than "predicate has non-immediate columns."7. Iterative work queue with LIFO ordering.
work.push((a, dst))followed bywork.pop()processes in reverse argument order. Dst slots are captured at enqueue, so write order doesn't affect correctness. Worth one line of comment ("processing order is LIFO but each dst index is captured at enqueue, so write order doesn't matter") because a future contributor optimizing this might be tempted to switch to FIFO and wonder why.8. Golden IR test renames are good housekeeping.
fact_compiles_to_unify_and_continuation_jump→per_clause_fact_compiles_to_unify_and_continuation_jumpmakes the path it covers explicit. Same formulti_clause_predicate_pushes_choice_points. Future readers know without re-reading which path each test pins.What's good
copyterm's walker is the load-bearing move. The fact-table doesn't reinvent term restoration; it borrows the existing one. One walker means one place to fix bugs and one place that needs to evolve when the cell format does.is_groundwalks both compounds and lists recursively. Nested vars (e.g.f(g(X))) correctly disqualify. TheTerm::List { head, tail }branch recurses into both —[1, X, 3]is not ground.restore_cellsdoc note ("ground blobs, so TAG_REF never fires") makes the invariant the codegen depends on explicit.unsafeseam), and why. Future contributors don't have to reconstruct the scope from the diff.Suggested order
#1 (ABI agreement check between the two crates' tag constants) is the only thing worth landing before merge — it's the bug class that bites silently and grows with Stage D/E. A unit test importing both is sufficient; lifting to
plg-sharedis the principled fix. #2 (compound-col-0 interning note in FACT_TABLE.md) is one line. #3+ are observations.