From 3e2121c6ccc44dbfe9c72b7ee362754126f45bde Mon Sep 17 00:00:00 2001 From: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:47:18 +0200 Subject: [PATCH] `{ for ... }` bounded lists (#100) --- crates/kobold/src/internal.rs | 23 ++- crates/kobold/src/keywords.rs | 31 +++- crates/kobold/src/list.rs | 144 +++++----------- crates/kobold/src/list/bounded.rs | 181 +++++++++++++++++++++ crates/kobold/src/list/unbounded.rs | 107 ++++++++++++ crates/kobold_macros/src/dom.rs | 14 +- crates/kobold_macros/src/dom/expression.rs | 56 ++++--- 7 files changed, 425 insertions(+), 131 deletions(-) create mode 100644 crates/kobold/src/list/bounded.rs create mode 100644 crates/kobold/src/list/unbounded.rs diff --git a/crates/kobold/src/internal.rs b/crates/kobold/src/internal.rs index 9cd1113..9aee547 100644 --- a/crates/kobold/src/internal.rs +++ b/crates/kobold/src/internal.rs @@ -18,7 +18,7 @@ use crate::View; /// Used for the initialize-in-place strategy employed by the [`View::build`] method. #[must_use] #[repr(transparent)] -pub struct In<'a, T>(&'a mut MaybeUninit); +pub struct In<'a, T>(pub(crate) &'a mut MaybeUninit); /// Initialized stable pointer to `T`. /// @@ -67,6 +67,20 @@ impl DerefMut for Out<'_, T> { } impl<'a, T> In<'a, T> { + /// Create a box from an `In` -> `Out` constructor. + pub fn boxed(f: F) -> Box + where + F: FnOnce(In) -> Out, + { + unsafe { + let ptr = std::alloc::alloc(std::alloc::Layout::new::()) as *mut T; + + In::raw(ptr, f); + + Box::from_raw(ptr) + } + } + /// Cast this pointer from `In` to `In`. /// /// # Safety @@ -304,6 +318,13 @@ mod test { use std::pin::pin; + #[test] + fn boxed() { + let data = In::boxed(|p| p.put(42)); + + assert_eq!(*data, 42); + } + #[test] fn pinned() { let data = pin!(MaybeUninit::uninit()); diff --git a/crates/kobold/src/keywords.rs b/crates/kobold/src/keywords.rs index c68964f..4a84465 100644 --- a/crates/kobold/src/keywords.rs +++ b/crates/kobold/src/keywords.rs @@ -5,7 +5,7 @@ //! Keyword handles for `{ ... }` expressions in the [`view!`](crate::view) macro. use crate::diff::{Eager, Ref, Static}; -use crate::list::List; +use crate::list::{Bounded, List}; use crate::View; /// `{ for ... }`: turn an [`IntoIterator`] type into a [`View`]. @@ -15,7 +15,7 @@ use crate::View; /// view! { ///

"Integers 1 to 10:"

///
    -/// { for (1..=10).map(|n| view! {
  • { n }
  • }) } +/// { for (1..=10).map(|n| view! {
  • { n } }) } ///
/// } /// # ; @@ -25,7 +25,32 @@ where T: IntoIterator, T::Item: View, { - List(iterator) + List::new(iterator) +} + +/// `{ for ... }`: turn an [`IntoIterator`] type into a [`View`], +/// bounded to max length of `N`. +/// +/// This should be used only for small values of `N`. +/// +/// # Performance +/// +/// The main advantage in using `for` over regular `for` is that the +/// bounded variant of a [`List`] doesn't need to allocate as the max size is fixed +/// and known at compile time. +/// +/// ``` +/// # use kobold::prelude::*; +/// view! { +///

"Integers 1 to 10:"

+///
    +/// { for<10> (1..=10).map(|n| view! {
  • { n } }) } +///
+/// } +/// # ; +/// ``` +pub const fn for_bounded(iterator: T) -> List> { + List::new_bounded(iterator) } /// `{ ref ... }`: diff this value by its reference address. diff --git a/crates/kobold/src/list.rs b/crates/kobold/src/list.rs index 3663ac4..c38d106 100644 --- a/crates/kobold/src/list.rs +++ b/crates/kobold/src/list.rs @@ -4,48 +4,38 @@ //! Utilities for rendering lists -use std::mem::MaybeUninit; -use std::pin::Pin; +use std::marker::PhantomData; -use web_sys::Node; - -use crate::dom::{Anchor, Fragment, FragmentBuilder}; use crate::internal::{In, Out}; -use crate::{Mountable, View}; +use crate::View; -/// Wrapper type that implements `View` for iterators, created by the -/// [`for`](crate::keywords::for) keyword. -#[repr(transparent)] -pub struct List(pub(crate) T); +pub mod bounded; +pub mod unbounded; -pub struct ListProduct { - list: Vec>, - mounted: usize, - fragment: FragmentBuilder, -} +use bounded::BoundedProduct; +use unbounded::ListProduct; -impl

Anchor for ListProduct

-where - P: Mountable, -{ - type Js = Node; - type Target = Fragment; +/// Zero-sized marker making the [`List`] unbounded: it can grow to arbitrary +/// size but will require memory allocation. +pub struct Unbounded; - fn anchor(&self) -> &Fragment { - &self.fragment - } -} +/// Zero-sized marker making the [`List`] bounded to a max length of `N`: +/// elements over the limit are ignored and no allocations are made. +pub struct Bounded; -fn uninit() -> Pin>> { - unsafe { - let ptr = std::alloc::alloc(std::alloc::Layout::new::()); +/// Wrapper type that implements `View` for iterators, created by the +/// [`for`](crate::keywords::for) keyword. +#[repr(transparent)] +pub struct List(T, PhantomData); - Pin::new_unchecked(Box::from_raw(ptr as *mut MaybeUninit)) +impl List { + pub const fn new(item: T) -> Self { + List(item, PhantomData) } -} -unsafe fn unpin_assume_init(pin: Pin>>) -> Box { - std::mem::transmute(pin) + pub const fn new_bounded(item: T) -> List> { + List(item, PhantomData) + } } impl View for List @@ -56,73 +46,27 @@ where type Product = ListProduct<::Product>; fn build(self, p: In) -> Out { - let iter = self.0.into_iter(); - let fragment = FragmentBuilder::new(); - - let list: Vec<_> = iter - .map(|view| { - let mut pin = uninit(); - - let built = In::pinned(pin.as_mut(), |b| view.build(b)); - - fragment.append(built.js()); - - unsafe { unpin_assume_init(pin) } - }) - .collect(); - - let mounted = list.len(); - - p.put(ListProduct { - list, - mounted, - fragment, - }) + ListProduct::build(self.0.into_iter(), p) } fn update(self, p: &mut Self::Product) { - // `mounted` is always within the bounds of `len`, this - // convinces the compiler that this is indeed the fact, - // so it can optimize bounds checks here. - if p.mounted > p.list.len() { - unsafe { std::hint::unreachable_unchecked() } - } - - let mut new = self.0.into_iter(); - let mut consumed = 0; - - while let Some(old) = p.list.get_mut(consumed) { - let Some(new) = new.next() else { - break; - }; - - new.update(old); - consumed += 1; - } - - if consumed < p.mounted { - for tail in p.list[consumed..p.mounted].iter() { - tail.unmount(); - } - p.mounted = consumed; - return; - } - - p.list.extend(new.map(|view| { - let mut pin = uninit(); - - In::pinned(pin.as_mut(), |b| view.build(b)); - - consumed += 1; + p.update(self.0.into_iter()); + } +} - unsafe { unpin_assume_init(pin) } - })); +impl View for List> +where + T: IntoIterator, + ::Item: View, +{ + type Product = BoundedProduct<::Product, N>; - for built in p.list[p.mounted..consumed].iter() { - p.fragment.append(built.js()); - } + fn build(self, p: In) -> Out { + BoundedProduct::build(self.0.into_iter(), p) + } - p.mounted = consumed; + fn update(self, p: &mut Self::Product) { + p.update(self.0.into_iter()); } } @@ -130,11 +74,11 @@ impl View for Vec { type Product = ListProduct; fn build(self, p: In) -> Out { - List(self).build(p) + List::new(self).build(p) } fn update(self, p: &mut Self::Product) { - List(self).update(p); + List::new(self).update(p); } } @@ -145,22 +89,22 @@ where type Product = ListProduct<<&'a V as View>::Product>; fn build(self, p: In) -> Out { - List(self).build(p) + List::new(self).build(p) } fn update(self, p: &mut Self::Product) { - List(self).update(p) + List::new(self).update(p) } } impl View for [V; N] { - type Product = ListProduct; + type Product = BoundedProduct; fn build(self, p: In) -> Out { - List(self).build(p) + List::new_bounded(self).build(p) } fn update(self, p: &mut Self::Product) { - List(self).update(p) + List::new_bounded(self).update(p) } } diff --git a/crates/kobold/src/list/bounded.rs b/crates/kobold/src/list/bounded.rs new file mode 100644 index 0000000..228ce1c --- /dev/null +++ b/crates/kobold/src/list/bounded.rs @@ -0,0 +1,181 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::mem::MaybeUninit; +use std::ops::{Deref, DerefMut}; + +use web_sys::Node; + +use crate::dom::{Anchor, Fragment, FragmentBuilder}; +use crate::init; +use crate::internal::{In, Out}; +use crate::{Mountable, View}; + +pub struct BoundedProduct { + list: BoundedVec, + mounted: usize, + fragment: FragmentBuilder, +} + +impl BoundedProduct { + pub fn build(iter: I, p: In) -> Out + where + I: Iterator, + I::Item: View, + { + let mut list = p.in_place(|p| unsafe { + init!(p.list @ BoundedVec::new(p)); + init!(p.mounted = 0); + init!(p.fragment = FragmentBuilder::new()); + + Out::from_raw(p) + }); + + list.extend(iter); + list + } + + pub fn update(&mut self, mut iter: I) + where + I: Iterator, + I::Item: View, + { + let mut updated = 0; + + while let Some(old) = self.list.get_mut(updated) { + let Some(new) = iter.next() else { + break; + }; + + new.update(old); + updated += 1; + } + + if updated < self.mounted { + self.unmount(updated); + } else { + self.mount(updated); + + if updated == self.list.len() { + self.extend(iter); + } + } + } + + fn extend(&mut self, iter: I) + where + I: Iterator, + I::Item: View, + { + self.list.extend(iter, |view, p| { + let built = view.build(p); + + self.fragment.append(built.js()); + + built + }); + + self.mounted = self.list.len(); + } + + fn unmount(&mut self, from: usize) { + debug_assert!(self.list.get(from..self.mounted).is_some()); + + for p in unsafe { self.list.get_unchecked(from..self.mounted).iter() } { + p.unmount(); + } + self.mounted = from; + } + + fn mount(&mut self, to: usize) { + debug_assert!(self.list.get(self.mounted..to).is_some()); + + for p in unsafe { self.list.get_unchecked(self.mounted..to).iter() } { + self.fragment.append(p.js()); + } + self.mounted = to; + } +} + +impl Anchor for BoundedProduct +where + P: Mountable, +{ + type Js = Node; + type Target = Fragment; + + fn anchor(&self) -> &Fragment { + &self.fragment + } +} + +pub struct BoundedVec { + data: [MaybeUninit; N], + len: usize, +} + +impl BoundedVec { + pub fn push_in(&mut self, f: F) + where + F: FnOnce(In) -> Out, + { + if self.len >= N { + return; + } + + let _ = f(In(&mut self.data[self.len])); + + self.len += 1; + } +} + +impl BoundedVec { + fn new(mem: In) -> Out { + mem.in_place(|ptr| unsafe { + init!(ptr.len = 0); + + Out::from_raw(ptr) + }) + } + + fn len(&self) -> usize { + self.len + } + + fn extend(&mut self, iter: I, mut f: F) + where + I: Iterator, + F: FnMut(I::Item, In) -> Out, + { + for item in iter { + self.push_in(|p| f(item, p)); + } + } +} + +impl Deref for BoundedVec { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + if self.len > N { + unsafe { std::hint::unreachable_unchecked() } + } + + let ptr = &self.data[..self.len] as *const [MaybeUninit] as *const [T]; + + unsafe { &*ptr } + } +} + +impl DerefMut for BoundedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + if self.len > N { + unsafe { std::hint::unreachable_unchecked() } + } + + let ptr = &mut self.data[..self.len] as *mut [MaybeUninit] as *mut [T]; + + unsafe { &mut *ptr } + } +} diff --git a/crates/kobold/src/list/unbounded.rs b/crates/kobold/src/list/unbounded.rs new file mode 100644 index 0000000..e63339a --- /dev/null +++ b/crates/kobold/src/list/unbounded.rs @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Utilities for rendering lists + +use web_sys::Node; + +use crate::dom::{Anchor, Fragment, FragmentBuilder}; +use crate::internal::{In, Out}; +use crate::{Mountable, View}; + +pub struct ListProduct { + list: Vec>, + mounted: usize, + fragment: FragmentBuilder, +} + +impl ListProduct

{ + pub fn build(iter: I, p: In) -> Out + where + I: Iterator, + I::Item: View, + { + let mut list = p.put(ListProduct { + list: Vec::new(), + mounted: 0, + fragment: FragmentBuilder::new(), + }); + + list.extend(iter); + list + } + + pub fn update(&mut self, mut iter: I) + where + I: Iterator, + I::Item: View, + { + let mut updated = 0; + + while let Some(old) = self.list.get_mut(updated) { + let Some(new) = iter.next() else { + break; + }; + + new.update(old); + updated += 1; + } + + if updated < self.mounted { + self.unmount(updated); + } else { + self.mount(updated); + + if updated == self.list.len() { + self.extend(iter); + } + } + } + + fn extend(&mut self, iter: I) + where + I: Iterator, + I::Item: View, + { + self.list.extend(iter.map(|view| { + let built = In::boxed(|p| view.build(p)); + + self.fragment.append(built.js()); + + built + })); + + self.mounted = self.list.len(); + } + + fn unmount(&mut self, from: usize) { + debug_assert!(self.list.get(from..self.mounted).is_some()); + + for p in unsafe { self.list.get_unchecked(from..self.mounted).iter() } { + p.unmount(); + } + self.mounted = from; + } + + fn mount(&mut self, to: usize) { + debug_assert!(self.list.get(self.mounted..to).is_some()); + + for p in unsafe { self.list.get_unchecked(self.mounted..to).iter() } { + self.fragment.append(p.js()); + } + self.mounted = to; + } +} + +impl

Anchor for ListProduct

+where + P: Mountable, +{ + type Js = Node; + type Target = Fragment; + + fn anchor(&self) -> &Fragment { + &self.fragment + } +} diff --git a/crates/kobold_macros/src/dom.rs b/crates/kobold_macros/src/dom.rs index 49556f9..32caf65 100644 --- a/crates/kobold_macros/src/dom.rs +++ b/crates/kobold_macros/src/dom.rs @@ -97,7 +97,7 @@ impl Node { return Ok(1); } Some(Ok(ShallowNode::Expression(expr))) => { - parent.push(Expression::from(expr).into()); + parent.push(Expression::try_from(expr)?.into()); return Ok(1); } Some(Err(error)) => return Err(error), @@ -259,7 +259,7 @@ impl Parse for Property { return Ok(Property { name, - expr: Expression::from(expr), + expr: Expression::try_from(expr)?, }); } @@ -270,11 +270,11 @@ impl Parse for Property { match stream.next() { Some(tt) if tt.is('{') || tt.is(Lit) => Ok(Property { name, - expr: Expression::from(tt), + expr: Expression::try_from(tt)?, }), Some(TokenTree::Ident(b)) if b.one_of(["true", "false"]) => Ok(Property { name, - expr: Expression::from(TokenTree::from(b)), + expr: Expression::try_from(TokenTree::from(b))?, }), _ => Err(ParseError::new( "Component properties must contain {expressions} or literals", @@ -307,7 +307,7 @@ impl CssValue { impl Parse for CssValue { fn parse(stream: &mut ParseStream) -> Result { if let Some(expr) = stream.allow_consume('{') { - return Ok(CssValue::Expression(Expression::from(expr))); + return Ok(CssValue::Expression(Expression::try_from(expr)?)); } let css_label: CssLabel = stream @@ -358,7 +358,7 @@ impl Parse for Attribute { return Ok(Attribute { name, - value: Expression::from(expr).into(), + value: Expression::try_from(expr)?.into(), }); } @@ -384,7 +384,7 @@ impl Parse for Attribute { }), Some(tt) if tt.is('{') => Ok(Attribute { name, - value: Expression::from(tt).into(), + value: Expression::try_from(tt)?.into(), }), _ => Err(ParseError::new( "Element attributes must contain {expressions} or literals", diff --git a/crates/kobold_macros/src/dom/expression.rs b/crates/kobold_macros/src/dom/expression.rs index 73fa52d..2a23916 100644 --- a/crates/kobold_macros/src/dom/expression.rs +++ b/crates/kobold_macros/src/dom/expression.rs @@ -6,7 +6,7 @@ use std::fmt::{self, Debug}; use tokens::{Group, Ident, Span, TokenStream, TokenTree}; -use crate::dom::Node; +use crate::dom::{IteratorExt, Lit, Node, ParseError}; use crate::parse::IdentExt; use crate::tokenize::prelude::*; @@ -28,72 +28,88 @@ impl From for Node { } } -impl From for Expression { - fn from(tt: TokenTree) -> Self { +impl TryFrom for Expression { + type Error = ParseError; + + fn try_from(tt: TokenTree) -> Result { if let TokenTree::Group(group) = tt { - return Expression::from(group); + return Ok(Expression::try_from(group)?); } let span = tt.span(); let stream = tt.tokenize(); - Expression { + Ok(Expression { stream, span, is_static: false, - } + }) } } -impl From for Expression { - fn from(group: Group) -> Self { +impl TryFrom for Expression { + type Error = ParseError; + + fn try_from(group: Group) -> Result { let mut stream = group.stream().parse_stream(); if let Some(TokenTree::Ident(ident)) = stream.peek() { let span = ident.span(); let mut is_static = false; let mut deref = false; - let mut invoke = false; + let mut invoke = None; let keyword = ident.with_str(|ident| match ident { - "for" | "use" => Some(Ident::new_raw(ident, span)), + "for" => Some("for"), + "use" => Some("use"), "ref" => { deref = true; - Some(Ident::new_raw(ident, span)) + Some("ref") } "static" => { is_static = true; - Some(Ident::new_raw(ident, span)) + Some("static") } "do" => { - invoke = true; + invoke = Some('!'.tokenize()); - Some(Ident::new_raw(ident, span)) + Some("do") } _ => None, }); - if let Some(keyword) = keyword { + if let Some(mut keyword) = keyword { stream.next(); - return Expression { + if keyword == "for" { + if let Some(_) = stream.allow_consume('<') { + let n = stream.expect(Lit)?; + let close = stream.expect('>')?; + + keyword = "for_bounded"; + invoke = Some(("::<_, ", n, close).tokenize()) + } + } + let keyword = Ident::new_raw(keyword, span); + + return Ok(Expression { stream: call( - ("::kobold::keywords::", keyword, invoke.then_some('!')), + ("::kobold::keywords::", keyword, invoke), (deref.then_some('&'), stream), ), span: group.span(), is_static, - }; + }); } } - Expression { + Ok(Expression { stream: stream.collect(), span: group.span(), is_static: false, - } + }) } }