Skip to content

Commit

Permalink
{ for<N> ... } bounded lists (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciejhirsz authored Apr 11, 2024
1 parent 3eb6dd8 commit 3e2121c
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 131 deletions.
23 changes: 22 additions & 1 deletion crates/kobold/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>);
pub struct In<'a, T>(pub(crate) &'a mut MaybeUninit<T>);

/// Initialized stable pointer to `T`.
///
Expand Down Expand Up @@ -67,6 +67,20 @@ impl<T> DerefMut for Out<'_, T> {
}

impl<'a, T> In<'a, T> {
/// Create a box from an `In` -> `Out` constructor.
pub fn boxed<F>(f: F) -> Box<T>
where
F: FnOnce(In<T>) -> Out<T>,
{
unsafe {
let ptr = std::alloc::alloc(std::alloc::Layout::new::<T>()) as *mut T;

In::raw(ptr, f);

Box::from_raw(ptr)
}
}

/// Cast this pointer from `In<T>` to `In<U>`.
///
/// # Safety
Expand Down Expand Up @@ -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());
Expand Down
31 changes: 28 additions & 3 deletions crates/kobold/src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand All @@ -15,7 +15,7 @@ use crate::View;
/// view! {
/// <h1>"Integers 1 to 10:"</h1>
/// <ul>
/// { for (1..=10).map(|n| view! { <li>{ n }</li> }) }
/// { for (1..=10).map(|n| view! { <li>{ n } }) }
/// </ul>
/// }
/// # ;
Expand All @@ -25,7 +25,32 @@ where
T: IntoIterator,
T::Item: View,
{
List(iterator)
List::new(iterator)
}

/// `{ for<N> ... }`: 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<N>` 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! {
/// <h1>"Integers 1 to 10:"</h1>
/// <ul>
/// { for<10> (1..=10).map(|n| view! { <li>{ n } }) }
/// </ul>
/// }
/// # ;
/// ```
pub const fn for_bounded<T, const N: usize>(iterator: T) -> List<T, Bounded<N>> {
List::new_bounded(iterator)
}

/// `{ ref ... }`: diff this value by its reference address.
Expand Down
144 changes: 44 additions & 100 deletions crates/kobold/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(pub(crate) T);
pub mod bounded;
pub mod unbounded;

pub struct ListProduct<P: Mountable> {
list: Vec<Box<P>>,
mounted: usize,
fragment: FragmentBuilder,
}
use bounded::BoundedProduct;
use unbounded::ListProduct;

impl<P> Anchor for ListProduct<P>
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<const N: usize>;

fn uninit<T>() -> Pin<Box<MaybeUninit<T>>> {
unsafe {
let ptr = std::alloc::alloc(std::alloc::Layout::new::<T>());
/// Wrapper type that implements `View` for iterators, created by the
/// [`for`](crate::keywords::for) keyword.
#[repr(transparent)]
pub struct List<T, B = Unbounded>(T, PhantomData<B>);

Pin::new_unchecked(Box::from_raw(ptr as *mut MaybeUninit<T>))
impl<T> List<T> {
pub const fn new(item: T) -> Self {
List(item, PhantomData)
}
}

unsafe fn unpin_assume_init<T>(pin: Pin<Box<MaybeUninit<T>>>) -> Box<T> {
std::mem::transmute(pin)
pub const fn new_bounded<const N: usize>(item: T) -> List<T, Bounded<N>> {
List(item, PhantomData)
}
}

impl<T> View for List<T>
Expand All @@ -56,85 +46,39 @@ where
type Product = ListProduct<<T::Item as View>::Product>;

fn build(self, p: In<Self::Product>) -> Out<Self::Product> {
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<T, const N: usize> View for List<T, Bounded<N>>
where
T: IntoIterator,
<T as IntoIterator>::Item: View,
{
type Product = BoundedProduct<<T::Item as View>::Product, N>;

for built in p.list[p.mounted..consumed].iter() {
p.fragment.append(built.js());
}
fn build(self, p: In<Self::Product>) -> Out<Self::Product> {
BoundedProduct::build(self.0.into_iter(), p)
}

p.mounted = consumed;
fn update(self, p: &mut Self::Product) {
p.update(self.0.into_iter());
}
}

impl<V: View> View for Vec<V> {
type Product = ListProduct<V::Product>;

fn build(self, p: In<Self::Product>) -> Out<Self::Product> {
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);
}
}

Expand All @@ -145,22 +89,22 @@ where
type Product = ListProduct<<&'a V as View>::Product>;

fn build(self, p: In<Self::Product>) -> Out<Self::Product> {
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<V: View, const N: usize> View for [V; N] {
type Product = ListProduct<V::Product>;
type Product = BoundedProduct<V::Product, N>;

fn build(self, p: In<Self::Product>) -> Out<Self::Product> {
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)
}
}
Loading

0 comments on commit 3e2121c

Please sign in to comment.