Declare which index levels a node reads and nullify the rest

This commit is contained in:
Dennis Kobert
2026-08-24 13:38:16 +00:00
parent f1ccb74625
commit 027218f16c
15 changed files with 275 additions and 65 deletions

View File

@@ -1,7 +1,7 @@
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::list::List;
use core_types::{Color, ExtractVarArgs};
use core_types::{Ctx, ExtractIndex, ExtractPosition};
use core_types::{Ctx, ExtractIndex, ExtractIndices, ExtractPosition};
use glam::DVec2;
use graphic_types::vector_types::GradientStops;
use graphic_types::{Graphic, Vector};
@@ -64,7 +64,7 @@ fn vararg_lanes<T: 'static>(ctx: &impl ExtractVarArgs, level: u8) -> GPoll<Exten
fn vararg_element<T: Clone + 'static>(ctx: &(impl ExtractVarArgs + ExtractIndex)) -> Result<T, Interrupt> {
vararg_list::<T>(ctx)
.and_then(|list| list.element(ctx.innermost_index() as usize))
.and_then(|list| list.element(ctx.index() as usize))
.cloned()
.ok_or_else(|| GraphError::new("vararg row addressed past its items").into())
}
@@ -138,7 +138,9 @@ fn read_position(
/// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops.
#[node_macro::node(category("Context"), path(core_types::vector))]
fn read_index(
ctx: impl Ctx + ExtractIndex,
// `loop_level` is a runtime input, so no level is statically known and the
// whole chain has to survive nullification.
ctx: impl Ctx + ExtractIndices,
_primary: (),
/// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels.
///

View File

@@ -1,5 +1,5 @@
use core_types::context::{ContextModification, Ctx, DeriveCtx};
use core_types::gpoll::Interrupt;
use core_types::context::{ContextFeatures, ContextModification, Ctx, DeriveCtx, IndexLink, nullify_index_levels};
use core_types::gpoll::{ErrorKind, GraphError, Interrupt};
/// Filters out what should be unused components of the context based on the specified requirements.
/// This node is inserted by the compiler to "zero out" unused context components.
@@ -12,5 +12,15 @@ fn context_modification<T>(
modification: ContextModification,
) -> Result<T, Interrupt> {
let scope = ctx.scope().nullified(modification.features, Some(modification.sources()));
value.eval(&ctx.nullified(modification.features, &scope))
let exhausted = || {
Interrupt::from(GraphError {
kind: ErrorKind::ArenaExhausted,
trace: Vec::new(),
})
};
let index = match modification.features.contains(ContextFeatures::INDEX) {
true => nullify_index_levels(ctx.index_head(), modification.index_levels, scope.arena()).ok_or_else(exhausted)?,
false => IndexLink { index: 0, outer: None },
};
value.eval(&ctx.nullified(modification.features, index, &scope))
}

View File

@@ -41,7 +41,7 @@ fn memoize<'e>(
// keys with the lane normalized away.
let leveled = content.layout().depth > 0;
let lane = match leveled {
true => ctx.innermost_index() as usize,
true => ctx.index() as usize,
false => 0,
};
let key = match leveled {
@@ -203,7 +203,7 @@ fn monitor<'e>(
// computes lane by lane. Serving THIS lane out of that batch rather than
// evaluating the content separately is what keeps the cost linear: the extra
// eval would double the work under every enclosing monitor.
if content.layout().depth > 0 && ctx.innermost_index() == 0 {
if content.layout().depth > 0 && ctx.index() == 0 {
return match content.materialize_level(ctx, ctx.arena()) {
LevelStatus::Batch(batch, finality) => {
// SAFETY: the batch came from this edge, so it carries the edge's layout.
@@ -235,7 +235,7 @@ fn monitor<'e>(
};
}
let result = content.eval(&ctx);
if ctx.innermost_index() == 0
if ctx.index() == 0
&& let GPoll::Final(value) | GPoll::Partial(value) = &result
{
// SAFETY: the value came from this edge, so it carries the edge's layout.

View File

@@ -7,7 +7,7 @@
use core_types::attribute::{Attr, EditorLayerPath, Opacity, RemoveAttr, Transform};
use glam::DAffine2;
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
use core_types::context::{DeriveCtx, ExtractIndex, ExtractIndices, IndexLink, InjectIndex};
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
use core_types::node::Lane;
@@ -58,8 +58,8 @@ fn fade<T>(_: impl Ctx, (element, opacity): (T, Attr<Opacity>), factor: f64) ->
/// writes a per-copy opacity indexed by the copy's own index.
#[node_macro::node(category("Test"), extent(repeat_opacity_extent))]
fn repeat_opacity(ctx: impl Ctx + ExtractIndex, element: f64, count: u32) -> IList<(f64, Attr<Opacity>)> {
debug_assert!(ctx.innermost_index() < count as u64, "repeat addressed past its copy count");
emit(element, Attr(ctx.innermost_index() as f64))
debug_assert!(ctx.index() < count as u64, "repeat addressed past its copy count");
emit(element, Attr(ctx.index() as f64))
}
#[node_macro::node(category("Test"))]
@@ -155,7 +155,7 @@ fn extend<T>(
GPoll::Pending => return Err(Interrupt::Pending),
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
};
let lane = ctx.innermost_index();
let lane = ctx.index();
match lane < split {
true => base.eval(ctx),
false => {
@@ -210,7 +210,7 @@ fn omit_element<T>(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: i
GPoll::Pending => return Err(Interrupt::Pending),
_ => return Err(GraphError::new("omit over a non-exact extent").into()),
};
let lane = ctx.innermost_index();
let lane = ctx.index();
let source = match resolve_index(index, total) {
Some(omitted) if lane >= omitted => lane + 1,
_ => lane,
@@ -270,7 +270,7 @@ fn extract_element(_: impl Ctx + InjectIndex + Copy, list: IList<f64>, index: f6
#[node_macro::node(category("Test"), extent(mirror_extent))]
fn mirror(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<f64>, keep_original: bool) -> Result<IList<(f64, Attr<Transform>)>, Interrupt> {
let total = content.len() as u64;
let lane = ctx.innermost_index();
let lane = ctx.index();
let (source, mirrored) = match (keep_original, lane < total) {
(true, true) => (lane, false),
(true, false) => (lane - total, true),

View File

@@ -71,7 +71,7 @@ pub fn omit_element<T>(
GPoll::Pending => return Err(Interrupt::Pending),
_ => return Err(GraphError::new("omit over a non-exact extent").into()),
};
let lane = ctx.innermost_index();
let lane = ctx.index();
let source = match resolve_index(index, total) {
Some(omitted) if lane >= omitted => lane + 1,
_ => lane,
@@ -115,7 +115,7 @@ fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
#[implementations(Graphic, Vector, Raster<CPU>, Color, GradientStops, String)] content: IList<Row>,
mapped: impl Node<Context<'_>, Output = IList<T>>,
) -> Result<IList<T>, Interrupt> {
let mut remaining = ctx.innermost_index();
let mut remaining = ctx.index();
for row in 0..content.len() {
let item = crate::record::vararg_row(content, row);
let scoped = ctx.push_vararg(&item);
@@ -276,7 +276,7 @@ fn mirror<'e>(
mirror_lane(
ctx.arena(),
legacy_render_list_of(content),
ctx.innermost_index() as usize,
ctx.index() as usize,
relative_to_bounds,
offset,
angle,
@@ -334,7 +334,7 @@ fn mirror_vector<'e>(
mirror_lane(
ctx.arena(),
legacy_render_list_of(content),
ctx.innermost_index() as usize,
ctx.index() as usize,
relative_to_bounds,
offset,
angle,
@@ -403,7 +403,7 @@ pub fn extend<T>(
GPoll::Pending => return Err(Interrupt::Pending),
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
};
let lane = ctx.innermost_index();
let lane = ctx.index();
match lane < split {
true => base.eval(ctx),
false => {
@@ -586,7 +586,7 @@ pub use _to_graphic_unit_mod::to_graphic_unit_entries;
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
#[node_macro::node(category("General"), extent(flatten_graphic_extent))]
pub fn flatten_graphic(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Graphic>, fully_flatten: bool) -> Result<IList<(Graphic, Attr<TransformAttr>)>, Interrupt> {
let mut remaining = ctx.innermost_index() as usize;
let mut remaining = ctx.index() as usize;
for row in 0..content.len() {
let graphic = content.element_ref(row);
let count = crate::record::leaf_count(graphic, fully_flatten, 0);

View File

@@ -85,7 +85,7 @@ pub(crate) fn locate(graphic: &Graphic, transform: DAffine2, fully_flatten: bool
/// rides as a leaf with its embedded transforms untouched.
#[node_macro::node(category("Test"), extent(flatten_extent))]
fn flatten(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Graphic>, fully_flatten: bool) -> Result<IList<(Graphic, Attr<Transform>)>, Interrupt> {
let mut remaining = ctx.innermost_index() as usize;
let mut remaining = ctx.index() as usize;
for row in 0..content.len() {
let graphic = content.element_ref(row);
let count = leaf_count(graphic, fully_flatten, 0);
@@ -157,7 +157,7 @@ fn map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
#[implementations(Graphic, Vector, Raster<CPU>, Color, GradientStops, String)] content: IList<Row>,
mapped: impl Node<Context<'_>, Output = IList<T>>,
) -> Result<IList<IList<T>>, Interrupt> {
let mut remaining = ctx.innermost_index();
let mut remaining = ctx.index();
for row in 0..content.len() {
let item = vararg_row(content, row);
let scoped = ctx.push_vararg(&item);
@@ -181,7 +181,7 @@ fn flat_map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
#[implementations(Graphic, Vector, Raster<CPU>, Color, GradientStops, String)] content: IList<Row>,
mapped: impl Node<Context<'_>, Output = IList<T>>,
) -> Result<IList<T>, Interrupt> {
let mut remaining = ctx.innermost_index();
let mut remaining = ctx.index();
for row in 0..content.len() {
let item = vararg_row(content, row);
let scoped = ctx.push_vararg(&item);
@@ -201,7 +201,7 @@ fn flat_map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
#[node_macro::node(category("Test"), extent(flatten_levels_extent))]
fn flatten_levels<T>(ctx: impl Ctx + DeriveCtx + ExtractIndex, content: impl Node<Context<'_>, Output = IList<IList<T>>>) -> Result<IList<T>, Interrupt> {
let head = ctx.index_head();
content.eval(&ctx.promoted(&head, ctx.innermost_index()))
content.eval(&ctx.promoted(&head, ctx.index()))
}
/// The collapsed level's extent is the sum of the inner extents across the
@@ -237,7 +237,7 @@ mod tests {
use core_types::SourceId;
use core_types::arena::Arena;
use core_types::attribute::Attribute as AttributeMarker;
use core_types::context::{ContextImpl, EvalScope, ExtractArena};
use core_types::context::{ContextImpl, EvalScope, ExtractArena, ExtractIndices};
use core_types::list::{Item, List};
use core_types::node::Node;
use core_types::record::{self, Layout, Rec, RecordSource, RecordValue, stack};

View File

@@ -811,7 +811,7 @@ fn hsla_to_color(_: impl Ctx, _primary: (), hue: Fraction, #[default(1.)] satura
#[node_macro::node(category("Color"), name("Hex to Color"))]
fn hex_to_color(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, hex_code: String) -> Result<IList<Color>, Interrupt> {
// An invalid input serves an empty level: no color
match (core_types::misc::parse_css_color(&hex_code), ctx.innermost_index()) {
match (core_types::misc::parse_css_color(&hex_code), ctx.index()) {
(Some(color), 0) => Ok(color),
_ => Err(GraphError::past_end().into()),
}
@@ -839,7 +839,7 @@ fn spread_method(_: impl Ctx, gradient: GradientStops, spread_method: vector_typ
#[node_macro::node(category("Color"))]
fn sample_gradient(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, _primary: (), gradient: IList<GradientStops>, position: Fraction) -> Result<IList<Color>, Interrupt> {
// An unwired gradient serves an empty level: no color
if gradient.is_empty() || ctx.innermost_index() != 0 {
if gradient.is_empty() || ctx.index() != 0 {
return Err(GraphError::past_end().into());
}
@@ -1035,7 +1035,7 @@ mod graphene_test {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
GPoll::Final(input.innermost_index() as f64)
GPoll::Final(input.index() as f64)
}
}

View File

@@ -59,7 +59,7 @@ fn image_color_palette(
})
.collect();
palette.get(ctx.innermost_index() as usize).copied().ok_or_else(|| GraphError::past_end().into())
palette.get(ctx.index() as usize).copied().ok_or_else(|| GraphError::past_end().into())
}
#[cfg(test)]

View File

@@ -107,7 +107,7 @@ pub fn combine_channels<'e>(
)>,
Interrupt,
> {
let lane = ctx.innermost_index() as usize;
let lane = ctx.index() as usize;
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
if lane >= max_len {
return Err(GraphError::past_end().into());

View File

@@ -231,7 +231,7 @@ mod test {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
use core_types::context::{ExtractArena, ExtractIndex};
use core_types::context::{ExtractArena, ExtractIndices};
let (vector, transform) = &self.rows[input.innermost_index() as usize % self.rows.len()];
let dst = stack::push(self.layout.frame_bytes());
// SAFETY: dst is the claimed frame of this layout; offsets are the layout's own.

View File

@@ -18,7 +18,7 @@ fn path_modify<'e>(
node_path: Vec<NodeId>,
) -> Result<(Vector, Attr<'e, EditorLayerPath>, RemoveAttr<EditorClickTarget>), Interrupt> {
let mut element = element;
if ctx.innermost_index() == 0 {
if ctx.index() == 0 {
modification.apply(&mut element);
}

View File

@@ -101,7 +101,7 @@ fn assign_colors<'e>(
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
repeat_every: u32,
) -> Result<IList<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>)>, Interrupt> {
let lane = ctx.innermost_index() as usize;
let lane = ctx.index() as usize;
if lane >= content.len() {
return Err(GraphError::past_end().into());
}
@@ -173,7 +173,7 @@ fn assign_colors_graphic<'e>(
seed: SeedValue,
repeat_every: u32,
) -> Result<IList<(Graphic, Attr<'e, TransformAttr>, Attr<'e, EditorLayerPath>)>, Interrupt> {
let lane = ctx.innermost_index() as usize;
let lane = ctx.index() as usize;
if lane >= content.len() {
return Err(GraphError::past_end().into());
}
@@ -1549,7 +1549,7 @@ fn solidify_stroke<'e>(
)>,
Interrupt,
> {
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize)
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.index() as usize)
}
/// A fill-bearing row splits into a fill lane and a solidified stroke lane,
@@ -1588,7 +1588,7 @@ fn solidify_stroke_vector<'e>(
)>,
Interrupt,
> {
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.innermost_index() as usize)
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.index() as usize)
}
fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll<Extent> {
@@ -1655,7 +1655,7 @@ fn separate_subpaths<'e>(
Interrupt,
> {
let output = separate_subpaths_core(legacy_vector_list_of(content));
emit_legacy_lane(ctx.arena(), output, ctx.innermost_index() as usize)
emit_legacy_lane(ctx.arena(), output, ctx.index() as usize)
}
/// A row splits into one lane per subpath, so the count depends on the
@@ -1726,7 +1726,7 @@ fn map_points<'e>(
}
}
emit_legacy_lane(ctx.arena(), content, ctx.innermost_index() as usize)
emit_legacy_lane(ctx.arena(), content, ctx.index() as usize)
}
fn map_points_extent(content: ListIn<'_, Vector>, _mapped: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent> {
@@ -2176,7 +2176,7 @@ fn cut_path<'e>(
Interrupt,
> {
let output = cut_path_core(legacy_vector_list_of(content), progression, reverse, parameterized_distance);
emit_legacy_lane(ctx.arena(), output, ctx.innermost_index() as usize)
emit_legacy_lane(ctx.arena(), output, ctx.index() as usize)
}
fn cut_path_extent(content: ListIn<'_, Vector>, _progression: ValueIn<'_, f64>, _reverse: ValueIn<'_, bool>, _parameterized_distance: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {