The effect graph fills ck_effects (ruling #30) #160

Manually merged
mdoorn merged 1 commit from feat/160-the-effect-graph-fills-ck-effects-ruling-30 into master 2026-07-10 16:49:10 +02:00
Owner

The effect graph fills ck_effects (ruling #30)

Along the way:

  • effects(graph): transitive-incursion reachability — the ck_effects stub is filled

Touches 2 files: src/rust/crates/ck_effects/src/lib.rs, src/rust/crates/ck_effects/src/tests.rs.

The effect graph fills ck_effects (ruling #30) Along the way: - effects(graph): transitive-incursion reachability — the ck_effects stub is filled Touches 2 files: `src/rust/crates/ck_effects/src/lib.rs`, `src/rust/crates/ck_effects/src/tests.rs`.
mdoorn approved these changes 2026-07-10 13:11:35 +02:00
mdoorn left a comment

Good. Small enough to read in one sitting, and src/rust/crates/ck_effects/src/tests.rs is easier to follow than it was.

Good. Small enough to read in one sitting, and `src/rust/crates/ck_effects/src/tests.rs` is easier to follow than it was.
@ -5,0 +225,4 @@
/// forbids to declare `e` while 3b must still thread a handle through
/// them, which is the whole reason this crate exists.
#[must_use]
pub fn carries_only(&self, f: FnId, e: EffectId) -> bool {
Owner

carries_only is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.

`carries_only` is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.
@ -0,0 +100,4 @@
}
#[test]
fn mutual_recursion_settles_instead_of_spinning() {
Owner

mutual_recursion_settles_instead_of_spinning reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?

`mutual_recursion_settles_instead_of_spinning` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
Author
Owner

Split. The decision in mutual_recursion_settles_instead_of_spinning is a pure function now, with its own tests in src/rust/crates/ck_effects/src/tests.rs.

Split. The decision in `mutual_recursion_settles_instead_of_spinning` is a pure function now, with its own tests in `src/rust/crates/ck_effects/src/tests.rs`.
Author
Owner

Nothing outside calls carries_only. It is pub(crate) in src/rust/crates/ck_effects/src/lib.rs now.

Nothing outside calls `carries_only`. It is pub(crate) in `src/rust/crates/ck_effects/src/lib.rs` now.
hkowalski approved these changes 2026-07-10 15:42:13 +02:00
hkowalski left a comment

Second read on src/rust/crates/ck_effects/src/tests.rs. Nothing to add.

Second read on `src/rust/crates/ck_effects/src/tests.rs`. Nothing to add.
Ruling #30's slice: the effect graph lands on its own, before its
consumers, so it is not shaped by whichever one builds first.

THE QUESTION IT ANSWERS. For any frame and any effect: does this frame
lie on a path to an incursion? The row law makes that unanswerable from
written rows — a row equals the effects a fn incurs DIRECTLY, transitive
effects are inferred and never restated (effect-rows.md, LOCKED
2026-05-27) — so in `main → mid → deep` the frames that must CARRY a
provider handle are exactly the frames forbidden to DECLARE the row.

TWO CONSUMERS, BIASED TO NEITHER. `direct_effects` / `incurs_directly`
are E0413's input (a written row is compared against direct incurrence);
`reaches` / `carries_only` / `frames_reaching` are 3b's (which frames
must thread a handle). `direct` is KEPT after the fixpoint rather than
recomputed or discarded, so neither consumer reconstructs the other's
half. `carries_only` names the exact set the row law forbids to declare
while 3b must still thread through — the crate's reason to exist, as one
call.

THE INPUT IS ABSTRACT — the DI-at-check precedent. The graph never walks
an IR: it takes `(FnId, FrameFacts)` pairs exactly as
`dispatch::resolve_ops` takes `(OpId, Span)` pairs, so the emit path can
feed it from Floor IR and the check path from resolved HIR — two callers
of one law, never two laws. That is also what keeps the dependency
direction legal: ck_effects still depends on ck_diag/ck_hir/ck_span and
nothing more, so R1's manifest graph is unchanged. A graph that reached
for the driver's `user_callees` would have inverted it.

THREE CONSTRUCTS THAT LOOK LIKE THEY NEED A RULING, AND DO NOT — same
reason each time: this graph answers reachability over call edges, and
row SEMANTICS belong to the consumer.
- Traps ([PROPOSED — trap does not discharge the row]): a trapped effect
  is still incurred, so the frame lies on the path either way and
  `reaches` is unaffected by how that ruling lands. Discharge, if ever
  ruled, is a subtraction the row checker applies to its own answer.
- Actors' `receive`: send is enqueue, dispatch is the tick, decoupled —
  the sender calls the generated `ck_send_*`, which enqueues and
  returns, so a handler is a fixpoint ROOT and its effects do not flow
  back to senders. Nothing here encodes that; it falls out of the edges
  the feeder supplies, which is the point.
- Generated fns ([PROPOSED — generated fns carry computed rows]):
  uniform treatment IS that answer, and the only non-special-case option
  (CLAUDE.md §2). A generated frame is a frame.

TERMINATION carries no iteration cap and must not: a magic capacity
would prove nothing and move the failure (§1). Each frame's set only
grows, the lattice is the finite powerset of declared effects over a
finite frame set, so the worklist drains. Cycles need no special case —
a frame is re-queued only when a callee's set actually grew.

NO ENFORCEMENT LANDS HERE: no diagnostic, no row checking, no
threading. Nothing refuses because of this.

OBSERVABILITY — the honest interim, stated. 15 unit tests in
`ck_effects::tests` are this slice's observable, and no capability cell
is possible: the matrix declares what the front end says, where lowering
or codegen stopped, or what a run did, and this slice by ruling emits no
diagnostic and changes no emitted code. Probed rather than assumed —
there is no `ckc query` verb (`sizes` is the only report command), the
meta stream carries runtime events not compile-time analyses, and the
diff is confined to a crate nothing depends on, so no `ckc` output can
differ by construction. Adding a report command would be surface with no
consumer. The graph gets `typecheck:` cells the moment E0413 gives it a
reachable surface — that is the same slice that moves
`effect_row_is_not_checked_yet`, which does NOT move here.

The tests pin: multi-hop propagation, direct-vs-transitive as separate
answers, `carries_only`, diamonds without double-counting, mutual and
self recursion settling, an unknown callee contributing nothing (the
intrinsic case), the actor send barrier, a generated frame as ordinary,
duplicate facts replacing rather than half-merging, the empty graph, and
stable iteration order (BTree throughout — EffectId's Ord is its dense
allocation index, so answers never come back in hash order).

Capability counts unchanged and already at tree truth: 348 / 42 / 149.

Gates: `cargo fmt --check` clean · `cargo clippy --all-targets -D
warnings` clean, 0 warnings · `cargo test --workspace` exit code 0
(captured directly to a file, not through a pipe): 1494 passed, 0
failed, 1 ignored across 93 targets · architecture 32 passed / 0 failed
(R1 manifest unchanged, R3/R4 caps clear). Every invocation via
scripts/rotate-exec.sh.
mdoorn manually merged commit a25b3060f5 into master 2026-07-10 16:49:10 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/cklang-bootstrap!160
No description provided.