Claim the carrier's frame before the lazy cursor snapshot

This commit is contained in:
Dennis Kobert
2026-09-07 15:46:50 +00:00
parent df99ab3a72
commit f237a5ee50
2 changed files with 86 additions and 24 deletions

View File

@@ -1265,6 +1265,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
};
// A lazy input claims beyond every input frame this node holds, and its
// cursor is shared, so the inputs a kernel drives claim past each other.
// The snapshot copies the cursor where it stands, so every frame the node
// still reads after the kernel runs (the carrier's included) must have been
// claimed before it.
let lazy_frames_entry = quote! {
let __lazy_frames = __frame.frames().reborrow();
};
@@ -1807,6 +1810,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.filter(|field| matches!(field.ty, ParsedFieldType::Regular(_)))
.map(|field| &field.pat_ident.ident)
.collect();
let tail_form = if async_fn {
Tail::SpawnAsyncFn
} else if future_kernel {
Tail::SpawnFuture
} else {
match ir::node_kind(&node) {
ir::NodeKind::RecordIo => Tail::Record,
ir::NodeKind::Flip => Tail::Flip,
ir::NodeKind::Routing | ir::NodeKind::Opaque => Tail::Forward,
}
};
// A carried tail claims the node's frame first, evaluates the carrier
// beyond it, and carries its fields; every exit closes the frame through
// `lift_poll_into`.
@@ -1850,6 +1864,37 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
})
});
// A record tail's carrier claim, the one the plan and the kernel read
// `__src_rec` out of.
let carrier_eval = (record_io && !skips_carrier && !lazy_carrier).then(|| {
let name = &regular_fields[0].pat_ident.ident;
quote! {
let __src = match __cell.eval_input(0, &self.#name, __input, __frame.frames()) {
Ok(value) => value,
Err(interrupt) => return interrupt.into()
};
let __src_rec = self.__carrier.rec(&__src);
}
});
// The carrier's record stays claimed and readable through the kernel call, so
// its claim runs before the lazy snapshot and the lazy handles claim past it
// rather than over it. With no lazy input there is no snapshot and the claim
// stays in the tail.
let has_lazy = regular_fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_)));
let hoisted_carrier = match (has_lazy, tail_form) {
(false, _) => TokenStream2::new(),
(true, Tail::Record) => carrier_eval.clone().unwrap_or_default(),
(true, _) => carried_prelude.clone().unwrap_or_default(),
};
let carried_prelude = match has_lazy {
true => None,
false => carried_prelude,
};
let carrier_eval = match has_lazy {
true => None,
false => carrier_eval,
};
let lazy_entry = quote!(#hoisted_carrier #lazy_frames_entry);
// Async slots persist plain values across evaluations; the source lifts
// the slot value onto its record input at every merge point, into the
// carried frame when the node has a carrier.
@@ -1966,16 +2011,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
}
});
let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)*));
let carrier_eval = (!skips_carrier && !lazy_carrier).then(|| {
let name = &regular_fields[0].pat_ident.ident;
quote! {
let __src = match __cell.eval_input(0, &self.#name, __input, __frame.frames()) {
Ok(value) => value,
Err(interrupt) => return interrupt.into()
};
let __src_rec = self.__carrier.rec(&__src);
}
});
let carry = (!skips_carrier && !lazy_carrier).then(|| quote!(unsafe { __frame.carry(__src_rec, &self.__plan) };));
// A lazy carrier's source record is the token the kernel returned; its
// content frames sit above the claim and stay readable until its drop.
@@ -2097,17 +2132,6 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
__cell.merge(__frame.lift_served(#core_types::gpoll::GPoll::Final(__kernel_value), #core_types::context::ExtractArena::arena(__input)))
}
});
let tail_form = if async_fn {
Tail::SpawnAsyncFn
} else if future_kernel {
Tail::SpawnFuture
} else {
match ir::node_kind(&node) {
ir::NodeKind::RecordIo => Tail::Record,
ir::NodeKind::Flip => Tail::Flip,
ir::NodeKind::Routing | ir::NodeKind::Opaque => Tail::Forward,
}
};
let lower_tail = |form: Tail| match form {
Tail::Forward => lift.clone(),
Tail::Record => record_tail.clone().expect("a record-io node has a record tail"),
@@ -2225,7 +2249,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let clamp = clamp_tokens(field);
(field, quote!(#body #clamp))
}),
&lazy_frames_entry,
&lazy_entry,
);
// The rebind path with nothing hoisted: every non-carrier input binds
// fresh per lane, so an index-dependent input reaches its own lane.
@@ -2242,7 +2266,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let clamp = clamp_tokens(field);
(field, quote!(#body #clamp))
}),
&lazy_frames_entry,
&lazy_entry,
);
// A hoisted value is moved into every lane's kernel call, so each
// lane consumes a clone; view and borrow binds copy freely.
@@ -2694,7 +2718,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
EvalStep::Bind(index, field) => {
let body = bind_body(*index, field, false, &quote!(__frame.frames()));
match matches!(field.ty, ParsedFieldType::Node(_)) && !std::mem::replace(&mut lazy_declared, true) {
true => quote!(#lazy_frames_entry #body),
true => quote!(#lazy_entry #body),
false => body,
}
}

View File

@@ -360,6 +360,15 @@ fn offset(_: impl Ctx, element: f64, by: &f64) -> f64 {
element + *by
}
/// Test-only flip node holding a borrow of its lent carrier across a lazy
/// input's evaluation: the borrow points into the carrier's frame, so the lazy
/// input's claim has to start beyond it.
#[node_macro::node(category("Test"))]
fn lend_across_lazy(ctx: impl Ctx, element: &f64, addend: impl Node<Context<'_>, Output = f64>) -> Result<f64, Interrupt> {
let added = addend.eval(ctx)?;
Ok(*element + added)
}
#[node_macro::node(category("Test"))]
async fn double_async(_: impl Ctx, element: f64) -> f64 {
element * 2.
@@ -2342,6 +2351,35 @@ mod tests {
assert_eq!(served.attr::<Opacity>(), 0.25);
}
#[test]
fn a_lent_carrier_borrow_survives_a_lazy_inputs_claim() {
let arena = Arena::new(1024).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let carrier_layout = f64_layout(&["opacity"]);
let addend_layout = f64_layout(&["opacity"]);
assert!(carrier_layout.frame_bytes() != 0, "the lend must borrow into a spilled frame for the claims to overlap");
let frames = frames_for(&[&carrier_layout, &addend_layout]);
let node = install(
LendAcrossLazyNode::new(
f64_record_source(&carrier_layout, 2., vec![("opacity", 0.25)]),
f64_record_source(&addend_layout, 40., vec![("opacity", 0.5)]),
&carrier_layout,
&addend_layout,
),
lend_across_lazy_layout_meta(),
&[Some(&carrier_layout), None],
);
let GPoll::Final(served) = core_types::record::capture(&node, &ctx, &frames) else {
panic!("expected a final record");
};
assert_eq!(served.element::<f64>(), 42., "the lazy input claims beyond the carrier's frame, so the borrow keeps its element");
assert_eq!(served.attr::<Opacity>(), 0.25);
}
struct InlineRuntime;
impl core_types::runtime::Runtime for InlineRuntime {