Skip to content
Open
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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ jobs:
# feature) in automated runs so it keeps guarding against a pass that
# produces an ill-typed tree — it is off by default locally because it is
# superlinear on nested comprehensions (see `ci_test` / `debug_typecheck`).
run: DEEP_TYPECHECK=1 ./ci.sh test
# `CAMBRA_LINEAGE_GATE=1` makes every compile fold its pane boundaries and
# gate the leak classes, so the gate's corpus is the whole test suite
# rather than the handful of programs `context.rs`'s `corpus()` lists. Two
# recording gaps lived outside that sample; measured at ~+4% on the test
# step, which is worth paying to keep the next one from doing the same.
run: DEEP_TYPECHECK=1 CAMBRA_LINEAGE_GATE=1 ./ci.sh test

# 4. Success Signal for noops
- name: No-op for docs
Expand Down
Empty file added NodeId
Empty file.
100 changes: 82 additions & 18 deletions src/ccl/ccl_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ pub fn apply_function(expr: Expr, function: Expr, output_ty: Type) -> Expr {
/// where its own guard holds and no earlier arm's did. `prior` holds `g₀ … gᵢ₋₁`
/// in order; `guard` is `gᵢ`. Every guard is a `Bool`-typed expression over the
/// same element, so the synthesized conjunction is `Bool` too — typed here
/// because the callers (post-inference desugar, lambda elimination, the
/// because the callers (post-inference channelize, lambda elimination, the
/// transaction path walk) must be type-preserving.
///
/// Shared by [`crate::ccl::channelize`] (feed fan-out), [`crate::ccl::lambda_elim`]
Expand Down Expand Up @@ -1255,10 +1255,41 @@ impl<C> Default for PredMemo<C> {
PredMemo(Rc::new(RefCell::new(MemoStore {
entries: HashMap::new(),
revision: 0,
replacing: false,
})))
}
}

impl<C> PredMemo<C> {
/// A memo whose rebuilds **replace** the terms they are handed, so the
/// rebuilt term keeps the original's `NodeId`s.
///
/// Only sound when the owning walk reaches **every** occurrence of every
/// predicate it rebuilds. A predicate is an `Rc` shared across many type
/// slots and a rebuild cannot mutate through it, so it builds a new `Rc` and
/// repoints the refinement it was given. If some other type still holds the
/// original, the two coexist — and preserving ids then puts one id-set on two
/// live terms, which nothing catches because predicate uniqueness is not
/// asserted.
///
/// [`uniquify`](crate::ccl::uniquify) is the one caller entitled to this: it
/// walks the whole tree, and asserts the resulting 1:1 correspondence
/// (N distinct terms in, N out, same ids) on every compile.
///
/// Every other caller rebuilds within a single type while the original may
/// survive elsewhere. Those are *derivations*, and [`Default`] gives them the
/// recording behaviour they need.
pub fn replacing() -> Self {
let m = Self::default();
m.0.borrow_mut().replacing = true;
m
}

fn is_replacing(&self) -> bool {
self.0.borrow().replacing
}
}

/// Shares predicate *terms* without sharing rebuild *results*: the transform runs
/// at every occurrence, and occurrences that entered sharing one `Rc` leave
/// sharing one `Rc`.
Expand Down Expand Up @@ -1289,6 +1320,9 @@ struct MemoStore<C> {
/// own recursion, which the callback cannot report: a nested reuse mutates the
/// callback's copy without the callback doing anything.
revision: u64,
/// See [`PredMemo::replacing`]: the owning walk reaches every occurrence, so
/// a rebuild is a replacement and keeps the original's ids.
replacing: bool,
}

struct Entry<C> {
Expand Down Expand Up @@ -1342,7 +1376,7 @@ impl<C: PartialEq + Clone> PredMemo<C> {
context: &C,
f: impl FnOnce(&mut Expr) -> bool,
) -> bool {
let (mut pred, keepalive, before) = {
let (keepalive, before) = {
let mut store = self.0.borrow_mut();
let hit = store
.entries
Expand All @@ -1356,22 +1390,39 @@ impl<C: PartialEq + Clone> PredMemo<C> {
}
None => {
let keepalive = Rc::clone(&refinement.predicate);
// Copy-on-write, not duplication: the rebuilt term is
// installed *in place of* the original, so it is the same
// logical node at a new allocation and keeps its ids.
let copy = refinement.predicate.clone_preserving_ids();
let rev = store.revision;
(copy, keepalive, rev)
(keepalive, rev)
}
}
};
// The rebuild runs id-preserving, covering the rewrite as well as the
// copy-on-write above (a substitution firing inside a predicate
// materializes its template here). Nothing records a predicate rewrite,
// so an id minted here is one no record explains; preserving is honest
// because the rebuilt term *replaces* the original everywhere this walk
// reaches.
let reported = crate::ccl::lineage::preserving_ids(|| f(&mut pred));
// The copy and the rewrite run together under the memo's declared intent
// — `f` mints and copies *into* the term, so its products belong to
// whichever the rebuild is.
//
// **Replacing** (see [`PredMemo::replacing`]): the rebuilt term stands in
// for the original everywhere, so it is the same logical predicate at a
// new allocation and keeps its ids.
//
// **Deriving** (the default): the original may survive on a type this
// walk never reaches, so the rebuilt term is a genuinely new one. It
// freshens, recorded against the source predicate's own root, which records
// it as derived from the term it was rebuilt from.
let (pred, reported) = if self.is_replacing() {
crate::ccl::lineage::preserving_ids(|| {
let mut pred = (*keepalive).clone();
let reported = f(&mut pred);
(pred, reported)
})
} else {
let _g = crate::ccl::lineage::enter(
keepalive.node_id(),
"predicate.rebuild",
crate::ccl::lineage::Nature::Machinery,
);
let mut pred = (*keepalive).clone();
let reported = f(&mut pred);
(pred, reported)
};
let mut store = self.0.borrow_mut();
let changed = reported || store.revision != before;
let installed = if changed {
Expand Down Expand Up @@ -1407,10 +1458,23 @@ impl TermMemo {
/// caller) still leaves the occurrence rebuilt and recorded.
pub fn rebuild_always(&self, refinement: &mut Refinement, f: impl FnOnce(&mut Expr)) {
let keepalive = Rc::clone(&refinement.predicate);
// Copy-on-write; see `rebuild`.
let mut pred = refinement.predicate.clone_preserving_ids();
// Id-preserving; see `rebuild`.
crate::ccl::lineage::preserving_ids(|| f(&mut pred));
// Copy and rewrite under the memo's declared intent; see `rebuild`.
let pred = if self.0.is_replacing() {
crate::ccl::lineage::preserving_ids(|| {
let mut pred = (*keepalive).clone();
f(&mut pred);
pred
})
} else {
let _g = crate::ccl::lineage::enter(
keepalive.node_id(),
"predicate.rebuild",
crate::ccl::lineage::Nature::Machinery,
);
let mut pred = (*keepalive).clone();
f(&mut pred);
pred
};
let mut store = self.0.0.borrow_mut();
let shared = store
.entries
Expand Down
Loading
Loading