Switch to a pool of dynamic values for hot reloading (#2705)

* create the dynamic value pool

* assign ids to dynamic formatted segments

* separate the rendering and literal pools

* rsx output compiles again

* more examples compiling with new rsx expansion

* update template body explanation

* all workspace examples compile

* fix formatted segments in keys

* start hot reload diffing

* fix component literal hot reloading

* start integrate new hot reloading with the CLI

* simple hot reloads working

* Fix hot reloading blocks with components

* implement hot reloading for if chains

* Fix hot reloading after a template requires a full rebuild

* Fix hot reloading any attribute values

* remove unsafe from hot reload utils

* Fix hot reloading empty rsx

* add more hot reloading tests

* reorganize hot reload module

* fix web hydration

* fix empty rsx nodes in autoformatting

* fix tests

* remove path sorting logic from core

* make template names more consistent in debug mode

* fix quote_as_hot_reload_literal for explicitly typed literals

* fix can_be_shorthand for string literals

* fix formatted single dynamic expression

* Fix usize component properties and playwright tests

* remove default implementation for TemplateBody

* add a bunch more comments for diffing, scoring and why this scoring system is optimal
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-07-30 17:16:27 +00:00
1 parent f3ca1484a1
commit 34bdcd15cf
40 files changed
+2288 -1897

No files matched your search

+112 -144
View File
@@ -1,13 +1,15 @@
use proc_macro2::Span;
use quote::quote;
use quote::ToTokens;
use quote::{quote, TokenStreamExt};
use std::fmt::Display;
use std::ops::Deref;
use syn::{
parse::{Parse, ParseStream},
Lit, LitBool, LitFloat, LitInt, LitStr,
};
use crate::{location::DynIdx, IfmtInput, Segment};
use proc_macro2::TokenStream as TokenStream2;
/// A literal value in the rsx! macro
///
@@ -17,20 +19,14 @@ use crate::{location::DynIdx, IfmtInput, Segment};
/// Eventually we want to remove this notion of hot literals since we're generating different code
/// in debug than in release, which is harder to maintain and can lead to bugs.
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct HotLiteral {
pub value: HotLiteralType,
pub hr_idx: DynIdx,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum HotLiteralType {
pub enum HotLiteral {
/// A *formatted* string literal
/// We know this will generate a String, not an &'static str
///
/// The raw str type will generate a &'static str, but we need to distinguish the two for component props
///
/// "hello {world}"
Fmted(IfmtInput),
Fmted(HotReloadFormattedSegment),
/// A float literal
///
@@ -48,15 +44,28 @@ pub enum HotLiteralType {
Bool(LitBool),
}
impl HotLiteral {
pub fn quote_as_hot_reload_literal(&self) -> TokenStream2 {
match &self {
HotLiteral::Fmted(f) => quote! { dioxus_core::internal::HotReloadLiteral::Fmted(#f) },
HotLiteral::Float(f) => {
quote! { dioxus_core::internal::HotReloadLiteral::Float(#f as _) }
}
HotLiteral::Int(f) => quote! { dioxus_core::internal::HotReloadLiteral::Int(#f as _) },
HotLiteral::Bool(f) => quote! { dioxus_core::internal::HotReloadLiteral::Bool(#f) },
}
}
}
impl Parse for HotLiteral {
fn parse(input: ParseStream) -> syn::Result<Self> {
let raw = input.parse::<Lit>()?;
let value = match raw.clone() {
Lit::Int(a) => HotLiteralType::Int(a),
Lit::Bool(a) => HotLiteralType::Bool(a),
Lit::Float(a) => HotLiteralType::Float(a),
Lit::Str(a) => HotLiteralType::Fmted(IfmtInput::new_litstr(a)),
Lit::Int(a) => HotLiteral::Int(a),
Lit::Bool(a) => HotLiteral::Bool(a),
Lit::Float(a) => HotLiteral::Float(a),
Lit::Str(a) => HotLiteral::Fmted(IfmtInput::new_litstr(a).into()),
_ => {
return Err(syn::Error::new(
raw.span(),
@@ -65,125 +74,30 @@ impl Parse for HotLiteral {
}
};
Ok(HotLiteral {
value,
hr_idx: DynIdx::default(),
})
Ok(value)
}
}
impl ToTokens for HotLiteral {
fn to_tokens(&self, out: &mut proc_macro2::TokenStream) {
let val = match &self.value {
HotLiteralType::Fmted(fmt) if fmt.is_static() => {
let o = fmt.to_static().unwrap().to_token_stream();
quote! { #o }
match &self {
HotLiteral::Fmted(f) => {
f.formatted_input.to_tokens(out);
}
HotLiteralType::Fmted(fmt) => {
let mut idx = 0_usize;
let segments = fmt.segments.iter().map(|s| match s {
Segment::Literal(lit) => quote! {
dioxus_core::internal::FmtSegment::Literal { value: #lit }
},
Segment::Formatted(_fmt) => {
// increment idx for the dynamic segment so we maintain the mapping
let _idx = idx;
idx += 1;
quote! {
dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
}
}
});
// The static segments with idxs for locations
quote! {
dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
}
}
HotLiteralType::Float(a) => quote! { #a },
HotLiteralType::Int(a) => quote! { #a },
HotLiteralType::Bool(a) => quote! { #a },
};
let mapped = match &self.value {
HotLiteralType::Fmted(f) if f.is_static() => quote! { .clone() as &'static str},
HotLiteralType::Fmted(segments) => {
let rendered_segments = segments.segments.iter().filter_map(|s| match s {
Segment::Literal(_lit) => None,
Segment::Formatted(fmt) => {
// just render as a format_args! call
Some(quote! { #fmt.to_string() })
}
});
quote! {
.render_with(vec![ #(#rendered_segments),* ])
}
}
HotLiteralType::Float(_) => quote! { .clone() },
HotLiteralType::Int(_) => quote! { .clone() },
HotLiteralType::Bool(_) => quote! { .clone() },
};
let as_lit = match &self.value {
HotLiteralType::Fmted(f) if f.is_static() => {
let r = f.to_static().unwrap();
quote! { #r }
}
HotLiteralType::Fmted(f) => f.to_token_stream(),
HotLiteralType::Float(f) => f.to_token_stream(),
HotLiteralType::Int(f) => f.to_token_stream(),
HotLiteralType::Bool(f) => f.to_token_stream(),
};
let map_lit = match &self.value {
HotLiteralType::Fmted(f) if f.is_static() => quote! { .clone() },
HotLiteralType::Fmted(_) => quote! { .to_string() },
HotLiteralType::Float(_) => quote! { .clone() },
HotLiteralType::Int(_) => quote! { .clone() },
HotLiteralType::Bool(_) => quote! { .clone() },
};
let hr_idx = self.hr_idx.get().to_string();
out.append_all(quote! {
{
#[cfg(debug_assertions)]
{
// in debug we still want these tokens to turn into fmt args such that RA can line
// them up, giving us rename powersa
_ = #as_lit;
// The key is important here - we're creating a new GlobalSignal each call to this/
// But the key is what's keeping it stable
GlobalSignal::with_key(
|| #val, {
{
const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/");
const NORMAL: &str = dioxus_core::const_format::str_replace!(PATH, '\\', "/");
dioxus_core::const_format::concatcp!(NORMAL, ':', line!(), ':', column!(), ':', #hr_idx)
}
})
.maybe_with_rt(|s| s #mapped)
}
// just render the literal directly
#[cfg(not(debug_assertions))]
{ #as_lit #map_lit }
}
})
HotLiteral::Float(f) => f.to_tokens(out),
HotLiteral::Int(f) => f.to_tokens(out),
HotLiteral::Bool(f) => f.to_tokens(out),
}
}
}
impl HotLiteralType {
fn span(&self) -> Span {
impl HotLiteral {
pub fn span(&self) -> Span {
match self {
HotLiteralType::Fmted(f) => f.span(),
HotLiteralType::Float(f) => f.span(),
HotLiteralType::Int(f) => f.span(),
HotLiteralType::Bool(f) => f.span(),
HotLiteral::Fmted(f) => f.span(),
HotLiteral::Float(f) => f.span(),
HotLiteral::Int(f) => f.span(),
HotLiteral::Bool(f) => f.span(),
}
}
}
@@ -205,38 +119,92 @@ impl HotLiteral {
}
pub fn is_static(&self) -> bool {
match &self.value {
HotLiteralType::Fmted(fmt) => fmt.is_static(),
match &self {
HotLiteral::Fmted(fmt) => fmt.is_static(),
_ => false,
}
}
pub fn span(&self) -> Span {
self.value.span()
}
pub fn from_raw_text(text: &str) -> Self {
HotLiteral {
value: crate::HotLiteralType::Fmted(IfmtInput {
source: LitStr::new(text, Span::call_site()),
segments: vec![],
}),
hr_idx: Default::default(),
}
HotLiteral::Fmted(HotReloadFormattedSegment::from(IfmtInput {
source: LitStr::new(text, Span::call_site()),
segments: vec![],
}))
}
}
impl Display for HotLiteral {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.value {
HotLiteralType::Fmted(l) => l.to_string_with_quotes().fmt(f),
HotLiteralType::Float(l) => l.fmt(f),
HotLiteralType::Int(l) => l.fmt(f),
HotLiteralType::Bool(l) => l.value().fmt(f),
match &self {
HotLiteral::Fmted(l) => l.to_string_with_quotes().fmt(f),
HotLiteral::Float(l) => l.fmt(f),
HotLiteral::Int(l) => l.fmt(f),
HotLiteral::Bool(l) => l.value().fmt(f),
}
}
}
/// A formatted segment that can be hot reloaded
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct HotReloadFormattedSegment {
pub formatted_input: IfmtInput,
pub dynamic_node_indexes: Vec<DynIdx>,
}
impl Deref for HotReloadFormattedSegment {
type Target = IfmtInput;
fn deref(&self) -> &Self::Target {
&self.formatted_input
}
}
impl From<IfmtInput> for HotReloadFormattedSegment {
fn from(input: IfmtInput) -> Self {
let mut dynamic_node_indexes = Vec::new();
for segment in &input.segments {
if let Segment::Formatted { .. } = segment {
dynamic_node_indexes.push(DynIdx::default());
}
}
Self {
formatted_input: input,
dynamic_node_indexes,
}
}
}
impl Parse for HotReloadFormattedSegment {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ifmt: IfmtInput = input.parse()?;
Ok(Self::from(ifmt))
}
}
impl ToTokens for HotReloadFormattedSegment {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let mut idx = 0_usize;
let segments = self.segments.iter().map(|s| match s {
Segment::Literal(lit) => quote! {
dioxus_core::internal::FmtSegment::Literal { value: #lit }
},
Segment::Formatted(_fmt) => {
// increment idx for the dynamic segment so we maintain the mapping
let _idx = self.dynamic_node_indexes[idx].get();
idx += 1;
quote! {
dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
}
}
});
// The static segments with idxs for locations
tokens.extend(quote! {
dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
});
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -256,10 +224,10 @@ mod tests {
assert!(syn::parse2::<HotLiteral>(quote! { 'a' }).is_err());
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
assert!(matches!(lit, HotLiteral::Fmted(_)));
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
assert!(matches!(lit, HotLiteral::Fmted(_)));
}
#[test]
@@ -279,7 +247,7 @@ mod tests {
#[test]
fn static_str_becomes_str() {
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
let HotLiteralType::Fmted(segments) = &lit.value else {
let HotLiteral::Fmted(segments) = &lit else {
panic!("expected a formatted string");
};
assert!(segments.is_static());
@@ -290,7 +258,7 @@ mod tests {
#[test]
fn formatted_prints_as_formatted() {
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
let HotLiteralType::Fmted(segments) = &lit.value else {
let HotLiteral::Fmted(segments) = &lit else {
panic!("expected a formatted string");
};
assert!(!segments.is_static());