Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .claude/plans/d-dt64-6-routing-bindstore-behavior-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,19 @@ Disable runs on the `dr` commit, each turning a test red:
| `LocalMap` width read as 64 | 1 |
| address width read as 64 | 1 |
| a `use crate::` in `dr.rs` (the boundary test) | 1 |

## A.6 (2026-11-07) L5c: method references are issued, stamped and tied to their store

Ruling: Linear EXO-199. D1: the classid stays the class; no class versioning. D2: a cloned `BindStore` is another store.

Contract (`lance-graph-dt64`):
- `BindStore` has a private identity (`Arc<()>`). `Clone` is written by hand and mints a fresh identity, so a clone refuses the original's references and the original refuses the clone's. `Default` is still derived (`Arc::default` is fresh).
- At registration the store keeps one binding digest per method: FNV-1a 128 with the basis-witness constants under the tag `dt64.binding.v1`, over the classid, the basis witness, the method id, the signature revision, ON and reads positions, the ordered effects (target, lens, word, the lens's schema revision, the word's arity, tagged arguments), and population, truth and value demand. The source `name` and the guard are not in it; `admit` compares the guard witness structurally.
- `MethodRef` gains one public field, `stamp: Stamp`. `Stamp` has private fields (`issuer: Weak<()>`, `binding: u128`), no public constructor, a read-only `binding()`, and `PartialEq`/`Eq`/`Hash` by issuer pointer and digest. Struct-update forgeries still compile; they can only copy a stamp the store issued, and keep their refusals.
- `MethodRef::of` is removed. `BindStore::method_ref(class, id)` is the only issuer. `BindStore::check_issuer(r)` compares the issuer alone.
- `admit` runs its existing checks first and unchanged, then compares the stamp's digest with the STORED one (`Refusal::BindingMismatch`), then the issuer (`Refusal::ForeignStore`). Only this order lets both fire: the store never rebinds (D5), so with the issuer first a digest mismatch would never be reported.

Contract (`lance-graph-r2il`):
- `run`, `prepare_native` and `run_native` check that `store` issued the plan's method reference before anything else, refusing with `ExecRefusal::Binding(Refusal::ForeignStore)`. `prepare` gets the same check through `admit`.
- `Prepared`'s fields are `pub(crate)`; outside the crate it is read through `method()`, `lens()`, `route()`, `guards()`, `steps()` and `plan_id()`. A `compile_fail` doctest pins that `steps` cannot be edited outside the crate.
- `plan_id` also hashes the stamp's binding digest. It does not hash the lane kind: lanes are bound per run (`FieldLanes`), never on `Prepared` or a Program (C5), so one plan may still read an I32 lane on one run and a U32 lane on another. That gap stays open.
20 changes: 11 additions & 9 deletions crates/dt64-admission-probe/src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,18 @@ impl ClassView for FixtureView {
}

/// The faithful reference to a bound method.
/// The store issues it; the fixture checks that it is the reference it
/// expects (signature revision 1, guarded only for `COUNT_BELOW`) and that
/// the store admits it.
pub fn method_ref(store: &BindStore, id: MethodId) -> MethodRef {
let b = store
.admit(&MethodRef {
class: CLASS,
method: id,
sig_rev: SigRev(1),
guard: (id == COUNT_BELOW).then(|| binding(id, "").when.unwrap().witness()),
})
.expect("bound");
MethodRef::of(CLASS, b)
let r = store.method_ref(CLASS, id).expect("bound");
assert_eq!(r.sig_rev, SigRev(1));
assert_eq!(
r.guard,
(id == COUNT_BELOW).then(|| binding(id, "").when.unwrap().witness())
);
store.admit(&r).expect("admitted");
r
}

fn attachments() -> Vec<Attachment> {
Expand Down
6 changes: 4 additions & 2 deletions crates/grammar-constraints-probe/examples/when_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::time::Instant;
use grammar_constraints_probe::fixtures;
use grammar_constraints_probe::when::*;
use grammar_constraints_probe::{tile, Ids, Vocab};
use lance_graph_dt64::{BindStore, MethodRef};
use lance_graph_dt64::BindStore;

fn ns<T>(f: impl FnOnce() -> T) -> (T, u128) {
let t = Instant::now();
Expand Down Expand Up @@ -54,7 +54,9 @@ fn main() {
let mut composed = Vec::new();
let mut t_comp = 0;
for d in &rules {
let r = MethodRef::of(class, &d.binding(phys, &v, &lex));
let r = store
.method_ref(class, d.binding(phys, &v, &lex).id)
.unwrap();
let (c, t) = ns(|| compose(&store, &r, d, phys, &v, &lex).unwrap());
t_comp += t;
composed.push(c);
Expand Down
41 changes: 30 additions & 11 deletions crates/grammar-constraints-probe/tests/when.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use grammar_constraints_probe::{
use lance_graph_contract::set_algebra::{Logic, SetExpr};
use lance_graph_dt64::dr::{Frame, MapRead, MapRefusal, MapTable};
use lance_graph_dt64::{
AlgebraRef, Arg, BindStore, Effect, Extent, Guard, MethodRef, Refusal, SchemaRev,
AlgebraRef, Arg, BindStore, Effect, Extent, Guard, MethodBinding, MethodRef, Refusal, SchemaRev,
};

thread_local! { static BYTES: Cell<usize> = const { Cell::new(0) }; }
Expand Down Expand Up @@ -79,7 +79,19 @@ fn class_of(p: Physical) -> u32 {
}

fn mref(w: &W, class: u32, desc: &RuleDesc, p: Physical) -> MethodRef {
MethodRef::of(class, &desc.binding(p, &w.v, &w.lex))
stamped(&w.store, class, &desc.binding(p, &w.v, &w.lex))
}

/// The reference `binding` describes (its id, signature revision and guard
/// witness), carrying the stamp `store` issues for that method id. The
/// description may differ from what the store bound; `admit` and `compose`
/// then refuse exactly as they did before references were stamped.
fn stamped(store: &BindStore, class: u32, binding: &MethodBinding) -> MethodRef {
MethodRef {
sig_rev: binding.sig_rev,
guard: binding.when.as_ref().map(Guard::witness),
..store.method_ref(class, binding.id).unwrap()
}
}

fn all_plane(w: &W) -> Vec<u64> {
Expand Down Expand Up @@ -235,7 +247,7 @@ fn candidates_are_read_only() {
store
.register(&LabView::new(), CLASS_COADDR, schema(), vec![b.clone()])
.unwrap();
let r = MethodRef::of(CLASS_COADDR, &b);
let r = stamped(&store, CLASS_COADDR, &b);
assert_eq!(
compose(
&store,
Expand Down Expand Up @@ -284,7 +296,8 @@ fn n01_remove_head_frame() {
&w.lex,
)
.unwrap();
let r = MethodRef::of(
let r = stamped(
&store,
CLASS_COADDR,
&d.binding(Physical::CoAddressed, &w.v, &w.lex),
);
Expand Down Expand Up @@ -477,12 +490,17 @@ fn n06b_resealed_same_size_layout() {
#[test]
fn n07_foreign_class() {
let w = world(&A);
let r = mref(
&w,
CLASS_FOREIGN,
&RuleDesc::recipient(),
Physical::CoAddressed,
);
// The store issues no reference for a class it does not bind, so the
// foreign classid is put on a reference it issued for another class.
let r = MethodRef {
class: CLASS_FOREIGN,
..mref(
&w,
CLASS_COADDR,
&RuleDesc::recipient(),
Physical::CoAddressed,
)
};
assert_eq!(
compose(
&w.store,
Expand Down Expand Up @@ -528,7 +546,8 @@ fn n08_remove_operand() {
&w.lex,
)
.unwrap();
let r = MethodRef::of(
let r = stamped(
&store,
CLASS_COADDR,
&d.binding(Physical::CoAddressed, &w.v, &w.lex),
);
Expand Down
12 changes: 9 additions & 3 deletions crates/lance-graph-dir-sim/tests/ordinal_chain_lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ fn dt64_method_pointer_is_not_a_coordinate_or_an_excel_value() {
let mut dt = BindStore::new();
dt.register(&view, class, schema, vec![method.clone()])
.unwrap();
let pointer = MethodRef::of(class, &method);
let pointer = dt.method_ref(class, method.id).unwrap();
assert_eq!(dt.admit(&pointer).unwrap(), &method);
let before = dt.route(0, class).unwrap();
assert_eq!(dt.routed_at(before, 0).unwrap(), Some(class));
Expand Down Expand Up @@ -1231,7 +1231,10 @@ fn bound_property_lanes_fold_under_the_ruler_selection() {
assert!(binding.fields().has(DEPARTMENT) && binding.fields().has(COST_CENTER));

// Operation 1: the admitted method's `reads` names the lane it folds.
let m1 = MethodRef::of(HYBRID_USER, &method(1, DEPARTMENT));
let m1 = b
.store
.method_ref(HYBRID_USER, method(1, DEPARTMENT).id)
.unwrap();
let admitted = b.store.admit(&m1).unwrap();
assert!(admitted.reads.has(DEPARTMENT) && binding.fields().has(DEPARTMENT));
let sum = lowered(confirmed_hybrid(), Agg::SumI32(Col(0)));
Expand All @@ -1249,7 +1252,10 @@ fn bound_property_lanes_fold_under_the_ruler_selection() {
let got = execute_into(&sum, &inputs, &Foreign::NONE, &mut scratch, Out::None).unwrap();

// Operation 2, same binding: confirmed ∧ cost_center > 150, counted.
let m2 = MethodRef::of(HYBRID_USER, &method(2, COST_CENTER));
let m2 = b
.store
.method_ref(HYBRID_USER, method(2, COST_CENTER).id)
.unwrap();
assert!(b.store.admit(&m2).unwrap().reads.has(COST_CENTER));
let count = lowered(
Filter::and([confirmed_hybrid(), Filter::cmp(Col(0), Cmp::GtI32(150))]),
Expand Down
Loading
Loading