mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 07:08:11 +08:00
Let an async source write an owned attribute through its claim
This commit is contained in:
@@ -1859,14 +1859,28 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
// carried frame when the node has a carrier.
|
// carried frame when the node has a carrier.
|
||||||
// A writing source stores the kernel's whole tuple as that plain value:
|
// A writing source stores the kernel's whole tuple as that plain value:
|
||||||
// the lift writes the attributes through the claim, then lifts the
|
// the lift writes the attributes through the claim, then lifts the
|
||||||
// element, the shape the sync record tail closes with.
|
// element, the shape the sync record tail closes with. An owned crossing
|
||||||
|
// parks into the serving arena first, so its exhaustion is a poll.
|
||||||
let source_writes = (record_io && async_source && !write_markers.is_empty()).then(|| {
|
let source_writes = (record_io && async_source && !write_markers.is_empty()).then(|| {
|
||||||
let binders: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
|
let binders: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
|
||||||
let slots: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).collect();
|
let stores = node.output.shape.attrs.iter().enumerate().map(|(index, attr)| {
|
||||||
|
let binder = &binders[index];
|
||||||
|
let slot = format_ident!("__write_{index}");
|
||||||
|
match attr.owned {
|
||||||
|
false => quote!(unsafe { __frame.attr_at(self.#slot, #binder.0) };),
|
||||||
|
true => quote! {
|
||||||
|
let #binder = match #binder.park(#core_types::context::ExtractArena::arena(__input)) {
|
||||||
|
::core::option::Option::Some(value) => value,
|
||||||
|
::core::option::Option::None => return #core_types::gpoll::GPoll::arena_exhausted(),
|
||||||
|
};
|
||||||
|
unsafe { __frame.attr_at(self.#slot, #binder) };
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
quote! {
|
quote! {
|
||||||
.map(|(__element #(, #core_types::attribute::Attr(#binders))*)| {
|
.and_then(|(__element #(, #binders)*)| {
|
||||||
#(unsafe { __frame.attr_at(self.#slots, #binders) };)*
|
#(#stores)*
|
||||||
__element
|
#core_types::gpoll::GPoll::Final(__element)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,9 +96,23 @@ fn output(parsed: &ParsedNodeFn, generics: &[Ident]) -> Output {
|
|||||||
shape: ItemShape {
|
shape: ItemShape {
|
||||||
element: element_of(&element, generics),
|
element: element_of(&element, generics),
|
||||||
depth,
|
depth,
|
||||||
attrs: writes.into_iter().map(|marker| LevelAttr { marker, level: 0 }).collect(),
|
attrs: writes
|
||||||
|
.into_iter()
|
||||||
|
.map(|write| LevelAttr {
|
||||||
|
marker: write.marker,
|
||||||
|
level: 0,
|
||||||
|
owned: write.owned,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
},
|
},
|
||||||
removes: removes.into_iter().map(|marker| LevelAttr { marker, level: 0 }).collect(),
|
removes: removes
|
||||||
|
.into_iter()
|
||||||
|
.map(|marker| LevelAttr {
|
||||||
|
marker,
|
||||||
|
level: 0,
|
||||||
|
owned: false,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
gathers,
|
gathers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +172,7 @@ fn item_shape(element: &Type, depth: u8, reads: &[AttributeRead], generics: &[Id
|
|||||||
.map(|read| LevelAttr {
|
.map(|read| LevelAttr {
|
||||||
marker: read.marker.clone(),
|
marker: read.marker.clone(),
|
||||||
level: 0,
|
level: 0,
|
||||||
|
owned: false,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
@@ -557,6 +572,8 @@ pub(crate) enum Element {
|
|||||||
pub(crate) struct LevelAttr {
|
pub(crate) struct LevelAttr {
|
||||||
pub(crate) marker: Type,
|
pub(crate) marker: Type,
|
||||||
pub(crate) level: u8,
|
pub(crate) level: u8,
|
||||||
|
/// Writes only: the value crosses as an owned copy that parks at the lift.
|
||||||
|
pub(crate) owned: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum Effect {
|
pub(crate) enum Effect {
|
||||||
@@ -680,7 +697,7 @@ mod tests {
|
|||||||
Facts {
|
Facts {
|
||||||
sources: if skips_carrier(parsed) { vec![] } else { vec![0] },
|
sources: if skips_carrier(parsed) { vec![] } else { vec![0] },
|
||||||
carried: token_carrier(parsed),
|
carried: token_carrier(parsed),
|
||||||
writes: markers(write_markers.iter()),
|
writes: markers(write_markers.iter().map(|write| &write.marker)),
|
||||||
removes: markers(removes.iter()),
|
removes: markers(removes.iter()),
|
||||||
delta: 0,
|
delta: 0,
|
||||||
}
|
}
|
||||||
@@ -780,6 +797,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bridge_record_owned_write_async_source() {
|
||||||
|
let node = assert_bridge(
|
||||||
|
quote!(category("")),
|
||||||
|
quote!(
|
||||||
|
async fn tag_async(_: impl Ctx, _: (), val: f64) -> (f64, OwnedAttr<Label>) {
|
||||||
|
(val, OwnedAttr::new(""))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert!(node.output.shape.attrs.iter().all(|attr| attr.owned), "an `OwnedAttr` slot crosses the boundary owned");
|
||||||
|
}
|
||||||
|
|
||||||
/// An async source's element is the value its slot stores, so a byte-carried
|
/// An async source's element is the value its slot stores, so a byte-carried
|
||||||
/// generic token has no form here.
|
/// generic token has no form here.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -51,13 +51,20 @@ pub(crate) struct AttributeRead {
|
|||||||
pub(crate) marker: Type,
|
pub(crate) marker: Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One attribute write slot: the marker, and whether it crosses as an owned
|
||||||
|
/// copy (`OwnedAttr<M>`) instead of an evaluation-lifetime value (`Attr<M>`).
|
||||||
|
pub(crate) struct AttrWrite {
|
||||||
|
pub(crate) marker: Type,
|
||||||
|
pub(crate) owned: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// The write half of a record kernel's return: the element type in the first
|
/// The write half of a record kernel's return: the element type in the first
|
||||||
/// tuple slot, then the attribute markers written and the ones removed. `None`
|
/// tuple slot, then the attribute markers written and the ones removed. `None`
|
||||||
/// unless the value is a well-formed write tuple (a non-marker element first,
|
/// unless the value is a well-formed write tuple (a non-marker element first,
|
||||||
/// then only `Attr` and `RemoveAttr` slots, at least one).
|
/// then only `Attr`, `OwnedAttr` and `RemoveAttr` slots, at least one).
|
||||||
pub(crate) struct RecordWrites {
|
pub(crate) struct RecordWrites {
|
||||||
pub(crate) element: Type,
|
pub(crate) element: Type,
|
||||||
pub(crate) markers: Vec<Type>,
|
pub(crate) markers: Vec<AttrWrite>,
|
||||||
pub(crate) removes: Vec<Type>,
|
pub(crate) removes: Vec<Type>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,14 +72,16 @@ pub(crate) fn record_writes(value: &Type) -> Option<RecordWrites> {
|
|||||||
let Type::Tuple(tuple) = value else { return None };
|
let Type::Tuple(tuple) = value else { return None };
|
||||||
let mut slots = tuple.elems.iter();
|
let mut slots = tuple.elems.iter();
|
||||||
let element = slots.next()?;
|
let element = slots.next()?;
|
||||||
if attr_marker(element).is_some() || remove_attr_marker(element).is_some() {
|
if attr_marker(element).is_some() || owned_attr_marker(element).is_some() || remove_attr_marker(element).is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut markers = Vec::new();
|
let mut markers = Vec::new();
|
||||||
let mut removes = Vec::new();
|
let mut removes = Vec::new();
|
||||||
for slot in slots {
|
for slot in slots {
|
||||||
if let Some(marker) = attr_marker(slot) {
|
if let Some(marker) = attr_marker(slot) {
|
||||||
markers.push(marker);
|
markers.push(AttrWrite { marker, owned: false });
|
||||||
|
} else if let Some(marker) = owned_attr_marker(slot) {
|
||||||
|
markers.push(AttrWrite { marker, owned: true });
|
||||||
} else if let Some(marker) = remove_attr_marker(slot) {
|
} else if let Some(marker) = remove_attr_marker(slot) {
|
||||||
removes.push(marker);
|
removes.push(marker);
|
||||||
} else {
|
} else {
|
||||||
@@ -91,6 +100,11 @@ pub(crate) fn attr_marker(ty: &Type) -> Option<Type> {
|
|||||||
marker_of(ty, "Attr")
|
marker_of(ty, "Attr")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the marker type of an `OwnedAttr<Marker>` type, if `ty` is one.
|
||||||
|
pub(crate) fn owned_attr_marker(ty: &Type) -> Option<Type> {
|
||||||
|
marker_of(ty, "OwnedAttr")
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the marker type of a `RemoveAttr<Marker>` type, if `ty` is one.
|
/// Returns the marker type of a `RemoveAttr<Marker>` type, if `ty` is one.
|
||||||
pub(crate) fn remove_attr_marker(ty: &Type) -> Option<Type> {
|
pub(crate) fn remove_attr_marker(ty: &Type) -> Option<Type> {
|
||||||
marker_of(ty, "RemoveAttr")
|
marker_of(ty, "RemoveAttr")
|
||||||
|
|||||||
@@ -162,8 +162,11 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
|||||||
}
|
}
|
||||||
if let Some(writes) = &writes {
|
if let Some(writes) = &writes {
|
||||||
let mut seen_writes: Vec<String> = Vec::new();
|
let mut seen_writes: Vec<String> = Vec::new();
|
||||||
for marker in &writes.markers {
|
for write in &writes.markers {
|
||||||
let written = marker.to_token_stream().to_string();
|
if write.owned && !async_source {
|
||||||
|
emit_error!(parsed.output_type.span(), "an owned attribute crossing belongs to an async source; a synchronous write parks its value in the kernel");
|
||||||
|
}
|
||||||
|
let written = write.marker.to_token_stream().to_string();
|
||||||
if seen_writes.contains(&written) {
|
if seen_writes.contains(&written) {
|
||||||
emit_error!(parsed.output_type.span(), "attribute `{}` is written twice", written);
|
emit_error!(parsed.output_type.span(), "attribute `{}` is written twice", written);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! wiring is by hand until the compiler pass constructs layouts.
|
//! wiring is by hand until the compiler pass constructs layouts.
|
||||||
|
|
||||||
use core_types::Ctx;
|
use core_types::Ctx;
|
||||||
use core_types::attribute::{Attr, EditorLayerPath, Opacity, RemoveAttr, Transform};
|
use core_types::attribute::{Attr, EditorLayerPath, Opacity, OwnedAttr, RemoveAttr, Transform};
|
||||||
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex, ModifyIndex};
|
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex, ModifyIndex};
|
||||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
||||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
||||||
@@ -379,6 +379,13 @@ async fn measure_async(_: impl Ctx, _: (), element: f64) -> (f64, Attr<Length>)
|
|||||||
(element, Attr(element.abs()))
|
(element, Attr(element.abs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test-only async source whose reference value crosses the future boundary
|
||||||
|
/// owned: the slot holds the deep copy and every lift parks it afresh.
|
||||||
|
#[node_macro::node(category("Test"))]
|
||||||
|
async fn tag_async(_: impl Ctx, _: (), element: f64, label: String) -> (f64, OwnedAttr<Label>) {
|
||||||
|
(element, OwnedAttr::new(label.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Test"))]
|
#[node_macro::node(category("Test"))]
|
||||||
fn fallback(ctx: impl Ctx, _: (), #[expose] content: impl Node<Context<'_>, Output = (f64, Attr<Opacity>)>, #[expose] alternate: impl Node<Context<'_>, Output = f64>) -> Result<f64, Interrupt> {
|
fn fallback(ctx: impl Ctx, _: (), #[expose] content: impl Node<Context<'_>, Output = (f64, Attr<Opacity>)>, #[expose] alternate: impl Node<Context<'_>, Output = f64>) -> Result<f64, Interrupt> {
|
||||||
let (element, opacity) = content.eval(ctx)?;
|
let (element, opacity) = content.eval(ctx)?;
|
||||||
@@ -2452,6 +2459,37 @@ mod tests {
|
|||||||
assert_eq!(served.attr::<Length>(), 4.);
|
assert_eq!(served.attr::<Length>(), 4.);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_owned_crossing_parks_its_reference_on_every_lift() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let (runtime, _) = lifted_value(core_types::runtime::RuntimeHandle(std::sync::Arc::new(InlineRuntime)));
|
||||||
|
let (source_id, _) = lifted_value(7 as SourceId);
|
||||||
|
let layout = tag_async_layout();
|
||||||
|
let frames = frames_for(&[&layout]);
|
||||||
|
|
||||||
|
let node = install(
|
||||||
|
TagAsyncNode::new(ValueSource::new(()), ValueSource::new(3.), ValueSource::new("tagged".to_string()), runtime, source_id),
|
||||||
|
tag_async_layout_meta(),
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
assert_eq!(Node::<ContextImpl>::layout(&node), &layout);
|
||||||
|
let offset = layout.offset_of("label", 0).expect("the source writes the label");
|
||||||
|
|
||||||
|
for pass in ["the spawning eval", "the slot hit"] {
|
||||||
|
let scoped = frames.scope();
|
||||||
|
let GPoll::Final(value) = core_types::record::serve_edge(&node, &ctx, &scoped) else {
|
||||||
|
panic!("an inline completion is final on {pass}");
|
||||||
|
};
|
||||||
|
let rec = layout.rec(&value);
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 3., "{pass}");
|
||||||
|
assert_eq!(unsafe { rec.read::<&str>(offset) }, "tagged", "{pass}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lazy_reads_bind_to_their_edge_and_leave_the_untaken_branch_unevaluated() {
|
fn lazy_reads_bind_to_their_edge_and_leave_the_untaken_branch_unevaluated() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user