Selaa lähdekoodia

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
Evan Almloff 11 kuukautta sitten
vanhempi
commit
34bdcd15cf
40 muutettua tiedostoa jossa 2289 lisäystä ja 1898 poistoa
  1. 40 39
      packages/cli/src/serve/hot_reloading_file_map.rs
  2. 1 1
      packages/core/src/diff/iterator.rs
  3. 3 3
      packages/core/src/diff/mod.rs
  4. 18 52
      packages/core/src/diff/node.rs
  5. 417 19
      packages/core/src/hotreload_utils.rs
  6. 4 1
      packages/core/src/lib.rs
  7. 22 109
      packages/core/src/nodes.rs
  8. 7 61
      packages/core/src/virtual_dom.rs
  9. 1 1
      packages/core/tests/attributes_pass.rs
  10. 0 38
      packages/core/tests/hotreload.rs
  11. 10 33
      packages/hot-reload/src/client.rs
  12. 2 2
      packages/hot-reload/src/lib.rs
  13. 7 7
      packages/html/src/document/head.rs
  14. 1 0
      packages/rsx-rosetta/src/lib.rs
  15. 138 0
      packages/rsx/src/assign_dyn_ids.rs
  16. 50 21
      packages/rsx/src/attribute.rs
  17. 43 32
      packages/rsx/src/component.rs
  18. 6 11
      packages/rsx/src/element.rs
  19. 0 0
      packages/rsx/src/hot_reload/collect.rs
  20. 0 0
      packages/rsx/src/hot_reload/context.rs
  21. 651 0
      packages/rsx/src/hot_reload/diff.rs
  22. 157 0
      packages/rsx/src/hot_reload/last_build_state.rs
  23. 12 4
      packages/rsx/src/hot_reload/mod.rs
  24. 0 429
      packages/rsx/src/hotreload.rs
  25. 0 10
      packages/rsx/src/ifchain.rs
  26. 8 71
      packages/rsx/src/ifmt.rs
  27. 1 8
      packages/rsx/src/lib.rs
  28. 112 144
      packages/rsx/src/literal.rs
  29. 3 5
      packages/rsx/src/node.rs
  30. 0 113
      packages/rsx/src/reload_stack.rs
  31. 5 33
      packages/rsx/src/rsx_call.rs
  32. 0 330
      packages/rsx/src/scoring.rs
  33. 178 179
      packages/rsx/src/template_body.rs
  34. 4 14
      packages/rsx/src/text_node.rs
  35. 1 1
      packages/rsx/src/util.rs
  36. 383 120
      packages/rsx/tests/hotreload_pattern.rs
  37. 1 4
      packages/rsx/tests/parsing.rs
  38. 1 1
      packages/ssr/src/cache.rs
  39. 1 1
      packages/ssr/src/renderer.rs
  40. 1 1
      packages/web/src/hydration/hydrate.rs

+ 40 - 39
packages/cli/src/serve/hot_reloading_file_map.rs

@@ -1,7 +1,7 @@
-use dioxus_core::{internal::HotReloadLiteral, Template};
+use dioxus_core::internal::{HotReloadTemplateWithLocation, HotReloadedTemplate};
 use dioxus_rsx::{
     hot_reload::{diff_rsx, ChangedRsx},
-    CallBody, HotReloadedTemplate, HotReloadingContext,
+    CallBody, HotReloadingContext,
 };
 use krates::cm::MetadataCommand;
 use krates::Cmd;
@@ -18,8 +18,6 @@ pub struct FileMap {
     pub errors: Vec<io::Error>,
 
     pub in_workspace: HashMap<PathBuf, Option<PathBuf>>,
-
-    pub changed_lits: HashMap<String, HotReloadLiteral>,
 }
 
 /// A cached file that has been parsed
@@ -27,7 +25,7 @@ pub struct FileMap {
 /// We store the templates found in this file
 pub struct CachedSynFile {
     pub raw: String,
-    pub templates: HashMap<&'static str, Template>,
+    pub templates: HashMap<String, HotReloadedTemplate>,
 }
 
 impl FileMap {
@@ -56,7 +54,6 @@ impl FileMap {
             map,
             errors,
             in_workspace: HashMap::new(),
-            changed_lits: HashMap::new(),
         };
 
         map.load_assets::<Ctx>(crate_dir.as_path());
@@ -74,12 +71,23 @@ impl FileMap {
         }
     }
 
+    /// Insert a file into the map and force a full rebuild
+    fn full_rebuild(&mut self, file_path: PathBuf, src: String) -> HotreloadError {
+        let cached_file = CachedSynFile {
+            raw: src.clone(),
+            templates: HashMap::new(),
+        };
+
+        self.map.insert(file_path, cached_file);
+        HotreloadError::Notreloadable
+    }
+
     /// Try to update the rsx in a file
     pub fn update_rsx<Ctx: HotReloadingContext>(
         &mut self,
         file_path: &Path,
         crate_dir: &Path,
-    ) -> Result<Vec<HotReloadedTemplate>, HotreloadError> {
+    ) -> Result<Vec<HotReloadTemplateWithLocation>, HotreloadError> {
         let src = std::fs::read_to_string(file_path)?;
 
         // If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error
@@ -116,13 +124,7 @@ impl FileMap {
             // If the changes were some code, we should insert the file into the map and rebuild
             // todo: not sure we even need to put the cached file into the map, but whatever
             None => {
-                let cached_file = CachedSynFile {
-                    raw: src.clone(),
-                    templates: HashMap::new(),
-                };
-
-                self.map.insert(file_path.to_path_buf(), cached_file);
-                return Err(HotreloadError::Notreloadable);
+                return Err(self.full_rebuild(file_path.to_path_buf(), src));
             }
         };
 
@@ -150,47 +152,48 @@ impl FileMap {
                 continue;
             };
 
-            // We leak the template since templates are a compiletime value
-            // This is not ideal, but also not a huge deal for hot reloading
-            // TODO: we could consider arena allocating the templates and dropping them when the connection is closed
-            let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
+            let template_location = template_location(old_start, file);
 
             // Returns a list of templates that are hotreloadable
-            let hotreload_result = dioxus_rsx::hotreload::HotReloadedTemplate::new::<Ctx>(
-                &old_call_body,
-                &new_call_body,
-                leaked_location,
-                self.changed_lits.clone(),
+            let hotreload_result = dioxus_rsx::hot_reload::HotReloadResult::new::<Ctx>(
+                &old_call_body.body,
+                &new_call_body.body,
+                template_location.clone(),
             );
 
             // if the template is not hotreloadable, we need to do a full rebuild
             let Some(mut results) = hotreload_result else {
-                return Err(HotreloadError::Notreloadable);
+                return Err(self.full_rebuild(file_path.to_path_buf(), src));
             };
 
-            // self.changed_lits
-            //     .extend(std::mem::take(&mut results.changed_lits));
-
             // Be careful to not send the bad templates
-            results.templates.retain(|template| {
+            results.templates.retain(|idx, template| {
                 // dioxus cannot handle empty templates...
                 if template.roots.is_empty() {
                     return false;
                 }
+                let template_location = format_template_name(&template_location, *idx);
 
-                // if the template is the same, don't send it
-                if old_cached.templates.get(template.name) == Some(template) {
+                // if the template is the same, don't send its
+                if old_cached.templates.get(&template_location) == Some(&*template) {
                     return false;
                 };
 
                 // Update the most recent idea of the template
                 // This lets us know if the template has changed so we don't need to send it
-                old_cached.templates.insert(template.name, *template);
+                old_cached
+                    .templates
+                    .insert(template_location, template.clone());
 
                 true
             });
 
-            out_templates.push(results);
+            out_templates.extend(results.templates.into_iter().map(|(idx, template)| {
+                HotReloadTemplateWithLocation {
+                    location: format_template_name(&template_location, idx),
+                    template,
+                }
+            }));
         }
 
         Ok(out_templates)
@@ -228,13 +231,11 @@ pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> Str
         .collect::<Vec<_>>()
         .join("/");
 
-    path
-        + ":"
-        + line.to_string().as_str()
-        + ":"
-        + column.to_string().as_str()
-        // the byte index doesn't matter, but dioxus needs it
-        + ":0"
+    path + ":" + line.to_string().as_str() + ":" + column.to_string().as_str()
+}
+
+pub fn format_template_name(name: &str, index: usize) -> String {
+    format!("{}:{}", name, index)
 }
 
 struct FileMapSearchResult {

+ 1 - 1
packages/core/src/diff/iterator.rs

@@ -467,7 +467,7 @@ impl VNode {
         dom: &VirtualDom,
         to: &mut impl WriteMutations,
     ) -> usize {
-        let template = self.template.get();
+        let template = self.template;
 
         let mount = dom.mounts.get(self.mount.get().0).unwrap();
 

+ 3 - 3
packages/core/src/diff/mod.rs

@@ -103,11 +103,11 @@ impl VirtualDom {
         to: &mut impl WriteMutations,
         mut template: Template,
     ) {
-        if self.templates.contains_key(template.name) {
+        if self.templates.contains(&template.name) {
             return;
         }
 
-        _ = self.templates.insert(template.name, template);
+        _ = self.templates.insert(template.name);
 
         // If it's all dynamic nodes, then we don't need to register it
         if !template.is_completely_dynamic() {
@@ -124,7 +124,7 @@ impl VirtualDom {
 ///  - for appending children we can use AppendChildren
 #[allow(dead_code)]
 fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
-    let template = node.template.get();
+    let template = node.template;
     let path = template.node_paths[idx];
 
     // use a loop to index every static node's children until the path has run out

+ 18 - 52
packages/core/src/diff/node.rs

@@ -1,5 +1,5 @@
 use crate::innerlude::MountId;
-use crate::{Attribute, AttributeValue, DynamicNode::*, Template};
+use crate::{Attribute, AttributeValue, DynamicNode::*};
 use crate::{VNode, VirtualDom, WriteMutations};
 use core::iter::Peekable;
 
@@ -21,21 +21,6 @@ impl VNode {
         // The node we are diffing from should always be mounted
         debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none());
 
-        // If hot reloading is enabled, we need to make sure we're using the latest template
-        #[cfg(debug_assertions)]
-        {
-            let name = new.template.get().name;
-            if let Some(template) = dom.templates.get(name).cloned() {
-                new.template.set(template);
-                if template != self.template.get() {
-                    let mount_id = self.mount.get();
-                    let parent = dom.mounts[mount_id.0].parent;
-                    self.replace(std::slice::from_ref(new), parent, dom, to);
-                    return;
-                }
-            }
-        }
-
         // If the templates are different by name, we need to replace the entire template
         if self.templates_are_different(new) {
             return self.light_diff_templates(new, dom, to);
@@ -120,7 +105,7 @@ impl VNode {
         &self,
         root_idx: usize,
     ) -> Option<(usize, &DynamicNode)> {
-        self.template.get().roots[root_idx]
+        self.template.roots[root_idx]
             .dynamic_id()
             .map(|id| (id, &self.dynamic_nodes[id]))
     }
@@ -148,7 +133,7 @@ impl VNode {
 
     pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId {
         let mount = &dom.mounts[self.mount.get().0];
-        let last_root_index = self.template.get().roots.len() - 1;
+        let last_root_index = self.template.roots.len() - 1;
         match self.get_dynamic_root_node_and_id(last_root_index) {
             // This node is static, just get the root id
             None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index],
@@ -272,7 +257,7 @@ impl VNode {
         destroy_component_state: bool,
         replace_with: Option<usize>,
     ) {
-        let roots = self.template.get().roots;
+        let roots = self.template.roots;
         for (idx, node) in roots.iter().enumerate() {
             let last_node = idx == roots.len() - 1;
             if let Some(id) = node.dynamic_id() {
@@ -305,7 +290,7 @@ impl VNode {
         dom: &mut VirtualDom,
         destroy_component_state: bool,
     ) {
-        let template = self.template.get();
+        let template = self.template;
         for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() {
             let path_len = template.node_paths.get(idx).map(|path| path.len());
             // Roots are cleaned up automatically above and nodes with a empty path are placeholders
@@ -361,14 +346,14 @@ impl VNode {
     }
 
     fn templates_are_different(&self, other: &VNode) -> bool {
-        let self_node_name = self.template.get().id();
-        let other_node_name = other.template.get().id();
+        let self_node_name = self.template.id();
+        let other_node_name = other.template.id();
         self_node_name != other_node_name
     }
 
     pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) {
         let mut next_id = None;
-        for (idx, path) in self.template.get().attr_paths.iter().enumerate() {
+        for (idx, path) in self.template.attr_paths.iter().enumerate() {
             // We clean up the roots in the next step, so don't worry about them here
             if path.len() <= 1 {
                 continue;
@@ -399,7 +384,7 @@ impl VNode {
             let mut old_attributes_iter = old_attrs.iter().peekable();
             let mut new_attributes_iter = new_attrs.iter().peekable();
             let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx];
-            let path = self.template.get().attr_paths[idx];
+            let path = self.template.attr_paths[idx];
 
             loop {
                 match (old_attributes_iter.peek(), new_attributes_iter.peek()) {
@@ -566,21 +551,6 @@ impl VNode {
         }
     }
 
-    /// Get the most up to date template for this rsx block
-    #[allow(unused)]
-    pub(crate) fn template(&self, dom: &VirtualDom) -> Template {
-        // check for a overridden template
-        #[cfg(debug_assertions)]
-        {
-            let template = self.template.get();
-            if let Some(new_template) = dom.templates.get(template.name) {
-                self.template.set(*new_template);
-            }
-        };
-
-        self.template.get()
-    }
-
     /// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM.
     pub(crate) fn create(
         &self,
@@ -589,7 +559,7 @@ impl VNode {
         mut to: Option<&mut impl WriteMutations>,
     ) -> usize {
         // Get the most up to date template
-        let template = self.template(dom);
+        let template = self.template;
 
         // Initialize the mount information for this vnode if it isn't already mounted
         if !self.mount.get().mounted() {
@@ -616,13 +586,9 @@ impl VNode {
         }
 
         // Walk the roots, creating nodes and assigning IDs
-        // nodes in an iterator of (dynamic_node_index, path)
-
-        let nodes_sorted = template.breadth_first_node_paths();
-        let attrs_sorted = template.breadth_first_attribute_paths();
-
-        let mut nodes = nodes_sorted.peekable();
-        let mut attrs = attrs_sorted.peekable();
+        // nodes in an iterator of (dynamic_node_index, path) and attrs in an iterator of (attr_index, path)
+        let mut nodes = template.node_paths.iter().copied().enumerate().peekable();
+        let mut attrs = template.attr_paths.iter().copied().enumerate().peekable();
 
         // Get the mounted id of this block
         // At this point, we should have already mounted the block
@@ -690,7 +656,7 @@ impl VNode {
     fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef {
         ElementRef {
             path: ElementPath {
-                path: self.template.get().node_paths[dynamic_node_id],
+                path: self.template.node_paths[dynamic_node_id],
             },
             mount,
         }
@@ -834,7 +800,7 @@ impl VNode {
         let this_id = dom.next_element();
         dom.mounts[mount.0].root_ids[root_idx] = this_id;
 
-        to.load_template(self.template.get().name, root_idx, this_id);
+        to.load_template(self.template.name, root_idx, this_id);
 
         this_id
     }
@@ -874,7 +840,7 @@ impl VNode {
         dom: &mut VirtualDom,
     ) -> (ElementId, &'static [u8]) {
         // Add the mutation to the list
-        let path = self.template.get().node_paths[idx];
+        let path = self.template.node_paths[idx];
 
         // Allocate a dynamic element reference for this text node
         let new_id = mount.mount_node(idx, dom);
@@ -940,8 +906,8 @@ fn matching_components<'a>(
     left: &'a VNode,
     right: &'a VNode,
 ) -> Option<Vec<(&'a VComponent, &'a VComponent)>> {
-    let left_node = left.template.get();
-    let right_node = right.template.get();
+    let left_node = left.template;
+    let right_node = right.template;
     if left_node.roots.len() != right_node.roots.len() {
         return None;
     }

+ 417 - 19
packages/core/src/hotreload_utils.rs

@@ -1,15 +1,26 @@
-#[doc(hidden)]
+use std::{
+    any::{Any, TypeId},
+    hash::{Hash, Hasher},
+};
+
+#[cfg(feature = "serialize")]
+use crate::nodes::deserialize_string_leaky;
+use crate::{
+    Attribute, AttributeValue, DynamicNode, Template, TemplateAttribute, TemplateNode, VNode, VText,
+};
+
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+#[doc(hidden)]
 #[derive(Debug, PartialEq, Clone)]
 pub struct HotreloadedLiteral {
     pub name: String,
     pub value: HotReloadLiteral,
 }
 
-#[doc(hidden)]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+#[doc(hidden)]
 #[derive(Debug, PartialEq, Clone)]
 pub enum HotReloadLiteral {
     Fmted(FmtedSegments),
@@ -18,12 +29,53 @@ pub enum HotReloadLiteral {
     Bool(bool),
 }
 
-#[doc(hidden)]
+impl HotReloadLiteral {
+    pub fn as_fmted(&self) -> Option<&FmtedSegments> {
+        match self {
+            Self::Fmted(segments) => Some(segments),
+            _ => None,
+        }
+    }
+
+    pub fn as_float(&self) -> Option<f64> {
+        match self {
+            Self::Float(f) => Some(*f),
+            _ => None,
+        }
+    }
+
+    pub fn as_int(&self) -> Option<i64> {
+        match self {
+            Self::Int(i) => Some(*i),
+            _ => None,
+        }
+    }
+
+    pub fn as_bool(&self) -> Option<bool> {
+        match self {
+            Self::Bool(b) => Some(*b),
+            _ => None,
+        }
+    }
+}
+
+impl Hash for HotReloadLiteral {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        match self {
+            Self::Fmted(segments) => segments.hash(state),
+            Self::Float(f) => f.to_bits().hash(state),
+            Self::Int(i) => i.hash(state),
+            Self::Bool(b) => b.hash(state),
+        }
+    }
+}
+
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 pub struct FmtedSegments {
-    pub segments: Vec<FmtSegment>,
+    pub(crate) segments: Vec<FmtSegment>,
 }
 
 impl FmtedSegments {
@@ -32,33 +84,23 @@ impl FmtedSegments {
     }
 
     /// Render the formatted string by stitching together the segments
-    pub fn render_with(&self, dynamic_nodes: Vec<String>) -> String {
+    pub(crate) fn render_with(&self, dynamic_text: &[String]) -> String {
         let mut out = String::new();
 
         for segment in &self.segments {
             match segment {
                 FmtSegment::Literal { value } => out.push_str(value),
-                FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]),
+                FmtSegment::Dynamic { id } => out.push_str(&dynamic_text[*id]),
             }
         }
 
         out
     }
-
-    /// Update the segments with new segments
-    ///
-    /// this will change how we render the formatted string
-    pub fn update_segments(&mut self, new_segments: Vec<FmtSegment>) {
-        self.segments = new_segments;
-    }
 }
 
-#[cfg(feature = "serialize")]
-use crate::nodes::deserialize_string_leaky;
-
-#[doc(hidden)]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
 pub enum FmtSegment {
     Literal {
         #[cfg_attr(
@@ -71,3 +113,359 @@ pub enum FmtSegment {
         id: usize,
     },
 }
+
+// let __pool = DynamicValuePool::new(
+//     vec![...],
+//     vec![...],
+//     vec![...],
+// );
+// VNode::new(
+//     None,
+//     Template {
+//         name: "...",
+//         roots: &[...],
+//         node_paths: &[..],
+//         attr_paths: &[...],
+//     },
+//     Box::new([...]),
+//     Box::new([...]),
+// )
+
+// Open questions:
+// - How do we handle type coercion for different sized component property integers?
+// - Should non-string hot literals go through the centralized pool?
+// - Should formatted strings be a runtime concept?
+
+#[doc(hidden)]
+pub struct DynamicLiteralPool {
+    dynamic_text: Box<[String]>,
+}
+
+impl DynamicLiteralPool {
+    pub fn new(dynamic_text: Vec<String>) -> Self {
+        Self {
+            dynamic_text: dynamic_text.into_boxed_slice(),
+        }
+    }
+
+    pub fn get_component_property<'a, T>(
+        &self,
+        id: usize,
+        hot_reload: &'a HotReloadedTemplate,
+        f: impl FnOnce(&'a HotReloadLiteral) -> Option<T>,
+    ) -> Option<T> {
+        f(hot_reload.component_values.get(id)?)
+    }
+
+    /// Get a component property of a specific type at the component property index
+    pub fn component_property<T: 'static>(
+        &mut self,
+        id: usize,
+        hot_reload: &HotReloadedTemplate,
+        // We pass in the original value for better type inference
+        // For example, if the original literal is `0i128`, we know the output must be the type `i128`
+        _coherse_type: T,
+    ) -> T {
+        fn assert_type<T: 'static, T2: 'static>(t: T) -> T2 {
+            *(Box::new(t) as Box<dyn Any>).downcast::<T2>().unwrap()
+        }
+        let grab_float = || {
+            self.get_component_property(id, hot_reload, HotReloadLiteral::as_float).expect("Expected a float component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
+        };
+        let grab_int = || {
+            self.get_component_property(id, hot_reload, HotReloadLiteral::as_int).expect("Expected an int component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
+        };
+        let grab_bool = || {
+            self.get_component_property(id, hot_reload, HotReloadLiteral::as_bool).expect("Expected a bool component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
+        };
+        let grab_fmted = || {
+            self.get_component_property(id, hot_reload, |fmted| HotReloadLiteral::as_fmted(fmted).map(|segments| self.render_formatted(segments))).expect("Expected a string component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
+        };
+        match TypeId::of::<T>() {
+            // Any string types that accept a literal
+            _ if TypeId::of::<String>() == TypeId::of::<T>() => assert_type(grab_fmted()),
+            _ if TypeId::of::<&'static str>() == TypeId::of::<T>() => {
+                assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str)
+            }
+            // Any integer types that accept a literal
+            _ if TypeId::of::<i128>() == TypeId::of::<T>() => assert_type(grab_int() as i128),
+            _ if TypeId::of::<i64>() == TypeId::of::<T>() => assert_type(grab_int()),
+            _ if TypeId::of::<i32>() == TypeId::of::<T>() => assert_type(grab_int() as i32),
+            _ if TypeId::of::<i16>() == TypeId::of::<T>() => assert_type(grab_int() as i16),
+            _ if TypeId::of::<i8>() == TypeId::of::<T>() => assert_type(grab_int() as i8),
+            _ if TypeId::of::<isize>() == TypeId::of::<T>() => assert_type(grab_int() as isize),
+            _ if TypeId::of::<u128>() == TypeId::of::<T>() => assert_type(grab_int() as u128),
+            _ if TypeId::of::<u64>() == TypeId::of::<T>() => assert_type(grab_int() as u64),
+            _ if TypeId::of::<u32>() == TypeId::of::<T>() => assert_type(grab_int() as u32),
+            _ if TypeId::of::<u16>() == TypeId::of::<T>() => assert_type(grab_int() as u16),
+            _ if TypeId::of::<u8>() == TypeId::of::<T>() => assert_type(grab_int() as u8),
+            _ if TypeId::of::<usize>() == TypeId::of::<T>() => assert_type(grab_int() as usize),
+            // Any float types that accept a literal
+            _ if TypeId::of::<f64>() == TypeId::of::<T>() => assert_type(grab_float()),
+            _ if TypeId::of::<f32>() == TypeId::of::<T>() => assert_type(grab_float() as f32),
+            // Any bool types that accept a literal
+            _ if TypeId::of::<bool>() == TypeId::of::<T>() => assert_type(grab_bool()),
+            _ => panic!("Unsupported component property type"),
+        }
+    }
+
+    pub fn render_formatted(&self, segments: &FmtedSegments) -> String {
+        segments.render_with(&self.dynamic_text)
+    }
+}
+#[doc(hidden)]
+pub struct DynamicValuePool {
+    dynamic_attributes: Box<[Box<[Attribute]>]>,
+    dynamic_nodes: Box<[DynamicNode]>,
+    literal_pool: DynamicLiteralPool,
+}
+
+impl DynamicValuePool {
+    pub fn new(
+        dynamic_nodes: Vec<DynamicNode>,
+        dynamic_attributes: Vec<Box<[Attribute]>>,
+        literal_pool: DynamicLiteralPool,
+    ) -> Self {
+        Self {
+            dynamic_attributes: dynamic_attributes.into_boxed_slice(),
+            dynamic_nodes: dynamic_nodes.into_boxed_slice(),
+            literal_pool,
+        }
+    }
+
+    pub fn render_with(&mut self, hot_reload: &HotReloadedTemplate) -> VNode {
+        // Get the node_paths from a depth first traversal of the template
+        let node_paths = hot_reload.node_paths();
+        let attr_paths = hot_reload.attr_paths();
+
+        let template = Template {
+            name: hot_reload.name,
+            roots: hot_reload.roots,
+            node_paths,
+            attr_paths,
+        };
+        let key = hot_reload
+            .key
+            .as_ref()
+            .map(|key| self.literal_pool.render_formatted(key));
+        let dynamic_nodes = hot_reload
+            .dynamic_nodes
+            .iter()
+            .map(|node| self.render_dynamic_node(node))
+            .collect();
+        let dynamic_attrs = hot_reload
+            .dynamic_attributes
+            .iter()
+            .map(|attr| self.render_attribute(attr))
+            .collect();
+
+        VNode::new(key, template, dynamic_nodes, dynamic_attrs)
+    }
+
+    fn render_dynamic_node(&mut self, node: &HotReloadDynamicNode) -> DynamicNode {
+        match node {
+            // If the node is dynamic, take it from the pool and return it
+            HotReloadDynamicNode::Dynamic(id) => self.dynamic_nodes[*id].clone(),
+            // Otherwise, format the text node and return it
+            HotReloadDynamicNode::Formatted(segments) => DynamicNode::Text(VText {
+                value: self.literal_pool.render_formatted(segments),
+            }),
+        }
+    }
+
+    fn render_attribute(&mut self, attr: &HotReloadDynamicAttribute) -> Box<[Attribute]> {
+        match attr {
+            HotReloadDynamicAttribute::Dynamic(id) => self.dynamic_attributes[*id].clone(),
+            HotReloadDynamicAttribute::Named(NamedAttribute {
+                name,
+                namespace,
+                value,
+            }) => Box::new([Attribute {
+                name,
+                namespace: *namespace,
+                value: match value {
+                    HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(segments)) => {
+                        AttributeValue::Text(self.literal_pool.render_formatted(segments))
+                    }
+                    HotReloadAttributeValue::Literal(HotReloadLiteral::Float(f)) => {
+                        AttributeValue::Float(*f)
+                    }
+                    HotReloadAttributeValue::Literal(HotReloadLiteral::Int(i)) => {
+                        AttributeValue::Int(*i)
+                    }
+                    HotReloadAttributeValue::Literal(HotReloadLiteral::Bool(b)) => {
+                        AttributeValue::Bool(*b)
+                    }
+                    HotReloadAttributeValue::Dynamic(id) => {
+                        self.dynamic_attributes[*id][0].value.clone()
+                    }
+                },
+                volatile: false,
+            }]),
+        }
+    }
+}
+
+#[doc(hidden)]
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+pub struct HotReloadTemplateWithLocation {
+    pub location: String,
+    pub template: HotReloadedTemplate,
+}
+
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+pub struct HotReloadedTemplate {
+    pub name: &'static str,
+    pub key: Option<FmtedSegments>,
+    pub dynamic_nodes: Vec<HotReloadDynamicNode>,
+    pub dynamic_attributes: Vec<HotReloadDynamicAttribute>,
+    pub component_values: Vec<HotReloadLiteral>,
+    #[cfg_attr(
+        feature = "serialize",
+        serde(deserialize_with = "crate::nodes::deserialize_leaky")
+    )]
+    pub roots: &'static [TemplateNode],
+}
+
+impl HotReloadedTemplate {
+    pub fn new(
+        name: &'static str,
+        key: Option<FmtedSegments>,
+        dynamic_nodes: Vec<HotReloadDynamicNode>,
+        dynamic_attributes: Vec<HotReloadDynamicAttribute>,
+        component_values: Vec<HotReloadLiteral>,
+        roots: &'static [TemplateNode],
+    ) -> Self {
+        Self {
+            name,
+            key,
+            dynamic_nodes,
+            dynamic_attributes,
+            component_values,
+            roots,
+        }
+    }
+
+    fn node_paths(&self) -> &'static [&'static [u8]] {
+        fn add_node_paths(
+            roots: &[TemplateNode],
+            node_paths: &mut Vec<&'static [u8]>,
+            current_path: Vec<u8>,
+        ) {
+            for (idx, node) in roots.iter().enumerate() {
+                let mut path = current_path.clone();
+                path.push(idx as u8);
+                match node {
+                    TemplateNode::Element { children, .. } => {
+                        add_node_paths(children, node_paths, path);
+                    }
+                    TemplateNode::Text { .. } => {}
+                    TemplateNode::Dynamic { id } => {
+                        debug_assert_eq!(node_paths.len(), *id);
+                        node_paths.push(Box::leak(path.into_boxed_slice()));
+                    }
+                }
+            }
+        }
+
+        let mut node_paths = Vec::new();
+        add_node_paths(self.roots, &mut node_paths, Vec::new());
+        let leaked: &'static [&'static [u8]] = Box::leak(node_paths.into_boxed_slice());
+        leaked
+    }
+
+    fn attr_paths(&self) -> &'static [&'static [u8]] {
+        fn add_attr_paths(
+            roots: &[TemplateNode],
+            attr_paths: &mut Vec<&'static [u8]>,
+            current_path: Vec<u8>,
+        ) {
+            for (idx, node) in roots.iter().enumerate() {
+                let mut path = current_path.clone();
+                path.push(idx as u8);
+                if let TemplateNode::Element {
+                    children, attrs, ..
+                } = node
+                {
+                    for attr in *attrs {
+                        if let TemplateAttribute::Dynamic { id } = attr {
+                            debug_assert_eq!(attr_paths.len(), *id);
+                            attr_paths.push(Box::leak(path.clone().into_boxed_slice()));
+                        }
+                    }
+                    add_attr_paths(children, attr_paths, path);
+                }
+            }
+        }
+
+        let mut attr_paths = Vec::new();
+        add_attr_paths(self.roots, &mut attr_paths, Vec::new());
+        let leaked: &'static [&'static [u8]] = Box::leak(attr_paths.into_boxed_slice());
+        leaked
+    }
+}
+
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Clone, Hash)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+pub enum HotReloadDynamicNode {
+    Dynamic(usize),
+    Formatted(FmtedSegments),
+}
+
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Clone, Hash)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+pub enum HotReloadDynamicAttribute {
+    Dynamic(usize),
+    Named(NamedAttribute),
+}
+
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Clone, Hash)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+pub struct NamedAttribute {
+    /// The name of this attribute.
+    #[cfg_attr(
+        feature = "serialize",
+        serde(deserialize_with = "crate::nodes::deserialize_string_leaky")
+    )]
+    name: &'static str,
+    /// The namespace of this attribute. Does not exist in the HTML spec
+    #[cfg_attr(
+        feature = "serialize",
+        serde(deserialize_with = "crate::nodes::deserialize_option_leaky")
+    )]
+    namespace: Option<&'static str>,
+
+    value: HotReloadAttributeValue,
+}
+
+impl NamedAttribute {
+    pub fn new(
+        name: &'static str,
+        namespace: Option<&'static str>,
+        value: HotReloadAttributeValue,
+    ) -> Self {
+        Self {
+            name,
+            namespace,
+            value,
+        }
+    }
+}
+
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Clone, Hash)]
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
+pub enum HotReloadAttributeValue {
+    Literal(HotReloadLiteral),
+    Dynamic(usize),
+}

+ 4 - 1
packages/core/src/lib.rs

@@ -34,8 +34,11 @@ mod hotreload_utils;
 pub mod internal {
     pub use crate::properties::verify_component_called_as_component;
 
+    #[doc(hidden)]
     pub use crate::hotreload_utils::{
-        FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral,
+        DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue,
+        HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral,
+        HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute,
     };
 }
 

+ 22 - 109
packages/core/src/nodes.rs

@@ -6,7 +6,6 @@ use crate::{
     properties::ComponentFunction,
 };
 use crate::{Properties, ScopeId, VirtualDom};
-use core::panic;
 use std::ops::{Deref, DerefMut};
 use std::rc::Rc;
 use std::vec;
@@ -122,7 +121,7 @@ pub struct VNodeInner {
     pub key: Option<String>,
 
     /// The static nodes and static descriptor of the template
-    pub template: Cell<Template>,
+    pub template: Template,
 
     /// The dynamic nodes in the template
     pub dynamic_nodes: Box<[DynamicNode]>,
@@ -252,12 +251,12 @@ impl VNode {
                     key: None,
                     dynamic_nodes: Box::new([DynamicNode::Placeholder(Default::default())]),
                     dynamic_attrs: Box::new([]),
-                    template: Cell::new(Template {
+                    template: Template {
                         name: "packages/core/nodes.rs:198:0:0",
                         roots: &[TemplateNode::Dynamic { id: 0 }],
                         node_paths: &[&[0]],
                         attr_paths: &[],
-                    }),
+                    },
                 })
             })
             .clone()
@@ -278,7 +277,7 @@ impl VNode {
         Self {
             vnode: Rc::new(VNodeInner {
                 key,
-                template: Cell::new(template),
+                template,
                 dynamic_nodes,
                 dynamic_attrs,
             }),
@@ -290,7 +289,7 @@ impl VNode {
     ///
     /// Returns [`None`] if the root is actually a static node (Element/Text)
     pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
-        self.template.get().roots[idx]
+        self.template.roots[idx]
             .dynamic_id()
             .map(|id| &self.dynamic_nodes[id])
     }
@@ -411,7 +410,7 @@ where
 }
 
 #[cfg(feature = "serialize")]
-fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
+pub(crate) fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
 where
     T: serde::Deserialize<'de>,
     D: serde::Deserializer<'de>,
@@ -423,7 +422,9 @@ where
 }
 
 #[cfg(feature = "serialize")]
-fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
+pub(crate) fn deserialize_option_leaky<'a, 'de, D>(
+    deserializer: D,
+) -> Result<Option<&'static str>, D::Error>
 where
     D: serde::Deserializer<'de>,
 {
@@ -448,36 +449,6 @@ impl Template {
         let ptr: *const str = self.name;
         ptr as *const () as usize
     }
-
-    /// Iterate over the attribute paths in order along with the original indexes for each path
-    pub(crate) fn breadth_first_attribute_paths(
-        &self,
-    ) -> impl Iterator<Item = (usize, &'static [u8])> {
-        // In release mode, hot reloading is disabled and everything is in breadth first order already
-        #[cfg(not(debug_assertions))]
-        {
-            self.attr_paths.iter().copied().enumerate()
-        }
-        // If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
-        #[cfg(debug_assertions)]
-        {
-            sort_bfo(self.attr_paths).into_iter()
-        }
-    }
-
-    /// Iterate over the node paths in order along with the original indexes for each path
-    pub(crate) fn breadth_first_node_paths(&self) -> impl Iterator<Item = (usize, &'static [u8])> {
-        // In release mode, hot reloading is disabled and everything is in breadth first order already
-        #[cfg(not(debug_assertions))]
-        {
-            self.node_paths.iter().copied().enumerate()
-        }
-        // If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
-        #[cfg(debug_assertions)]
-        {
-            sort_bfo(self.node_paths).into_iter()
-        }
-    }
 }
 
 /// A statically known node in a layout.
@@ -523,7 +494,10 @@ pub enum TemplateNode {
     /// This template node is just a piece of static text
     Text {
         /// The actual text
-        #[serde(deserialize_with = "deserialize_string_leaky")]
+        #[cfg_attr(
+            feature = "serialize",
+            serde(deserialize_with = "deserialize_string_leaky")
+        )]
         text: &'static str,
     },
 
@@ -548,7 +522,7 @@ impl TemplateNode {
 /// A node created at runtime
 ///
 /// This node's index in the DynamicNode list on VNode should match its respective `Dynamic` index
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum DynamicNode {
     /// A component node
     ///
@@ -692,8 +666,10 @@ pub struct VText {
 
 impl VText {
     /// Create a new VText
-    pub fn new(value: String) -> Self {
-        Self { value }
+    pub fn new(value: impl ToString) -> Self {
+        Self {
+            value: value.to_string(),
+        }
     }
 }
 
@@ -783,6 +759,7 @@ impl Attribute {
 ///
 /// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`]
 /// variant.
+#[derive(Clone)]
 pub enum AttributeValue {
     /// Text attribute
     Text(String),
@@ -800,7 +777,7 @@ pub enum AttributeValue {
     Listener(ListenerCb),
 
     /// An arbitrary value that implements PartialEq and is static
-    Any(Box<dyn AnyValue>),
+    Any(Rc<dyn AnyValue>),
 
     /// A "none" value, resulting in the removal of an attribute from the dom
     None,
@@ -824,7 +801,7 @@ impl AttributeValue {
 
     /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
     pub fn any_value<T: AnyValue>(value: T) -> AttributeValue {
-        AttributeValue::Any(Box::new(value))
+        AttributeValue::Any(Rc::new(value))
     }
 }
 
@@ -859,19 +836,6 @@ impl PartialEq for AttributeValue {
     }
 }
 
-impl Clone for AttributeValue {
-    fn clone(&self) -> Self {
-        match self {
-            Self::Text(arg0) => Self::Text(arg0.clone()),
-            Self::Float(arg0) => Self::Float(*arg0),
-            Self::Int(arg0) => Self::Int(*arg0),
-            Self::Bool(arg0) => Self::Bool(*arg0),
-            Self::Listener(_) | Self::Any(_) => panic!("Cannot clone listener or any value"),
-            Self::None => Self::None,
-        }
-    }
-}
-
 #[doc(hidden)]
 pub trait AnyValue: 'static {
     fn any_cmp(&self, other: &dyn AnyValue) -> bool;
@@ -1117,7 +1081,7 @@ impl IntoAttributeValue for Arguments<'_> {
     }
 }
 
-impl IntoAttributeValue for Box<dyn AnyValue> {
+impl IntoAttributeValue for Rc<dyn AnyValue> {
     fn into_value(self) -> AttributeValue {
         AttributeValue::Any(self)
     }
@@ -1143,54 +1107,3 @@ pub trait HasAttributes {
         volatile: bool,
     ) -> Self;
 }
-
-#[cfg(debug_assertions)]
-pub(crate) fn sort_bfo(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
-    let mut with_indices = paths.iter().copied().enumerate().collect::<Vec<_>>();
-    with_indices.sort_by(|(_, a), (_, b)| {
-        let mut a = a.iter();
-        let mut b = b.iter();
-        loop {
-            match (a.next(), b.next()) {
-                (Some(a), Some(b)) => {
-                    if a != b {
-                        return a.cmp(b);
-                    }
-                }
-                // The shorter path goes first
-                (None, Some(_)) => return std::cmp::Ordering::Less,
-                (Some(_), None) => return std::cmp::Ordering::Greater,
-                (None, None) => return std::cmp::Ordering::Equal,
-            }
-        }
-    });
-    with_indices
-}
-
-#[test]
-#[cfg(debug_assertions)]
-fn sorting() {
-    let r: [(usize, &[u8]); 5] = [
-        (0, &[0, 1]),
-        (1, &[0, 2]),
-        (2, &[1, 0]),
-        (3, &[1, 0, 1]),
-        (4, &[1, 2]),
-    ];
-    assert_eq!(
-        sort_bfo(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
-        r
-    );
-    let r: [(usize, &[u8]); 6] = [
-        (0, &[0]),
-        (1, &[0, 1]),
-        (2, &[0, 1, 2]),
-        (3, &[1]),
-        (4, &[1, 2]),
-        (5, &[2]),
-    ];
-    assert_eq!(
-        sort_bfo(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
-        r
-    );
-}

+ 7 - 61
packages/core/src/virtual_dom.rs

@@ -18,7 +18,7 @@ use crate::{
 };
 use crate::{Task, VComponent};
 use futures_util::StreamExt;
-use rustc_hash::FxHashMap;
+use rustc_hash::FxHashSet;
 use slab::Slab;
 use std::collections::BTreeSet;
 use std::{any::Any, rc::Rc};
@@ -208,8 +208,8 @@ pub struct VirtualDom {
 
     pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
 
-    // A map of overridden templates?
-    pub(crate) templates: FxHashMap<TemplateId, Template>,
+    // A map of templates we have sent to the renderer
+    pub(crate) templates: FxHashSet<TemplateId>,
 
     // Templates changes that are queued for the next render
     pub(crate) queued_templates: Vec<Template>,
@@ -572,60 +572,6 @@ impl VirtualDom {
         }
     }
 
-    /// Replace a template at runtime. This will re-render all components that use this template.
-    /// This is the primitive that enables hot-reloading.
-    ///
-    /// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
-    ///
-    /// This will only replace the parent template, not any nested templates.
-    #[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
-    pub fn replace_template(&mut self, template: Template) {
-        // we only replace templates if hot reloading is enabled
-        #[cfg(debug_assertions)]
-        {
-            // Save the template ID
-            self.templates.insert(template.name, template);
-
-            // Only queue the template to be written if its not completely dynamic
-            if !template.is_completely_dynamic() {
-                self.queued_templates.push(template);
-            }
-
-            // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
-            let mut dirty = Vec::new();
-            for (id, scope) in self.scopes.iter() {
-                // Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
-                fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
-                    if node.template.get().name == template.name {
-                        return true;
-                    }
-
-                    for dynamic in node.dynamic_nodes.iter() {
-                        if let crate::DynamicNode::Fragment(nodes) = dynamic {
-                            for node in nodes {
-                                if check_node_for_templates(node, template) {
-                                    return true;
-                                }
-                            }
-                        }
-                    }
-
-                    false
-                }
-
-                if let Some(sync) = scope.try_root_node() {
-                    if check_node_for_templates(sync, template) {
-                        dirty.push(ScopeId(id));
-                    }
-                }
-            }
-
-            for dirty in dirty {
-                self.mark_dirty(dirty);
-            }
-        }
-    }
-
     /// Rebuild the virtualdom without handling any of the mutations
     ///
     /// This is useful for testing purposes and in cases where you render the output of the virtualdom without
@@ -885,11 +831,11 @@ impl VirtualDom {
                 return;
             };
             let el_ref = &mount.node;
-            let node_template = el_ref.template.get();
+            let node_template = el_ref.template;
             let target_path = path.path;
 
             // Accumulate listeners into the listener list bottom to top
-            for (idx, this_path) in node_template.breadth_first_attribute_paths() {
+            for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
                 let attrs = &*el_ref.dynamic_attrs[idx];
 
                 for attr in attrs.iter() {
@@ -943,10 +889,10 @@ impl VirtualDom {
             return;
         };
         let el_ref = &mount.node;
-        let node_template = el_ref.template.get();
+        let node_template = el_ref.template;
         let target_path = node.path;
 
-        for (idx, this_path) in node_template.breadth_first_attribute_paths() {
+        for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
             let attrs = &*el_ref.dynamic_attrs[idx];
 
             for attr in attrs.iter() {

+ 1 - 1
packages/core/tests/attributes_pass.rs

@@ -15,7 +15,7 @@ fn attributes_pass_properly() {
 
     let o = h.unwrap();
 
-    let template = &o.template.get();
+    let template = &o.template;
 
     assert_eq!(template.attr_paths.len(), 3);
 

+ 0 - 38
packages/core/tests/hotreload.rs

@@ -1,38 +0,0 @@
-use dioxus::prelude::*;
-use dioxus_core::ElementId;
-use dioxus_core::Mutation::{AppendChildren, LoadTemplate};
-
-/// Swap out the template and get it back via the mutation
-#[test]
-fn hotreloads_template() {
-    let old_rsx = rsx! { "A" };
-    let name = old_rsx.as_ref().unwrap().template.get().name;
-
-    let mut dom = VirtualDom::new_with_props(move |_| old_rsx.clone(), ());
-
-    let new_template = Template {
-        name,
-        roots: &[TemplateNode::Text { text: "B" }],
-        node_paths: &[],
-        attr_paths: &[],
-    };
-
-    dom.replace_template(new_template);
-
-    let muts = dom.rebuild_to_vec();
-
-    // New template comes out
-    assert_eq!(muts.templates.len(), 1);
-
-    assert_eq!(
-        muts.edits,
-        [
-            LoadTemplate {
-                name: "packages/core/tests/hotreload.rs:8:19:0",
-                index: 0,
-                id: ElementId(1,),
-            },
-            AppendChildren { id: ElementId(0,), m: 1 },
-        ]
-    )
-}

+ 10 - 33
packages/hot-reload/src/client.rs

@@ -1,43 +1,20 @@
 use crate::HotReloadMsg;
-use dioxus_core::{internal::HotReloadLiteral, ScopeId, VirtualDom};
+use dioxus_core::{ScopeId, VirtualDom};
 use dioxus_signals::Writable;
 
 /// Applies template and literal changes to the VirtualDom
 ///
 /// Assets need to be handled by the renderer.
 pub fn apply_changes(dom: &mut VirtualDom, msg: &HotReloadMsg) {
-    for templates in &msg.templates {
-        for template in &templates.templates {
-            dom.replace_template(*template);
-        }
-
-        dom.runtime().on_scope(ScopeId::ROOT, || {
-            let ctx = dioxus_signals::get_global_context();
+    dom.runtime().on_scope(ScopeId::ROOT, || {
+        let ctx = dioxus_signals::get_global_context();
 
-            for (id, literal) in templates.changed_lits.iter() {
-                match &literal {
-                    HotReloadLiteral::Fmted(f) => {
-                        if let Some(mut signal) = ctx.get_signal_with_key(id) {
-                            signal.set(f.clone());
-                        }
-                    }
-                    HotReloadLiteral::Float(f) => {
-                        if let Some(mut signal) = ctx.get_signal_with_key::<f64>(id) {
-                            signal.set(*f);
-                        }
-                    }
-                    HotReloadLiteral::Int(f) => {
-                        if let Some(mut signal) = ctx.get_signal_with_key::<i64>(id) {
-                            signal.set(*f);
-                        }
-                    }
-                    HotReloadLiteral::Bool(f) => {
-                        if let Some(mut signal) = ctx.get_signal_with_key::<bool>(id) {
-                            signal.set(*f);
-                        }
-                    }
-                }
+        for template in &msg.templates {
+            let id = &template.location;
+            let value = template.template.clone();
+            if let Some(mut signal) = ctx.get_signal_with_key(id) {
+                signal.set(value);
             }
-        });
-    }
+        }
+    });
 }

+ 2 - 2
packages/hot-reload/src/lib.rs

@@ -1,4 +1,4 @@
-use dioxus_rsx::HotReloadedTemplate;
+use dioxus_core::internal::HotReloadTemplateWithLocation;
 use serde::{Deserialize, Serialize};
 use std::path::PathBuf;
 
@@ -49,7 +49,7 @@ pub enum ClientMsg {
 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
 #[serde(bound(deserialize = "'de: 'static"))]
 pub struct HotReloadMsg {
-    pub templates: Vec<HotReloadedTemplate>,
+    pub templates: Vec<HotReloadTemplateWithLocation>,
     pub assets: Vec<PathBuf>,
 
     /// A file changed that's not an asset or a rust file - best of luck!

+ 7 - 7
packages/html/src/document/head.rs

@@ -30,7 +30,7 @@ fn extract_single_text_node(children: &Element, component: &str) -> Option<Strin
     // The title's children must be in one of two forms:
     // 1. rsx! { "static text" }
     // 2. rsx! { "title: {dynamic_text}" }
-    match vnode.template.get() {
+    match vnode.template {
         // rsx! { "static text" }
         Template {
             roots: &[TemplateNode::Text { text }],
@@ -91,7 +91,7 @@ pub struct TitleProps {
 pub fn Title(props: TitleProps) -> Element {
     let children = props.children;
     let Some(text) = extract_single_text_node(&children, "Title") else {
-        return rsx! {};
+        return VNode::empty();
     };
 
     // Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
@@ -109,7 +109,7 @@ pub fn Title(props: TitleProps) -> Element {
         *last_text = text;
     }
 
-    rsx! {}
+    VNode::empty()
 }
 
 /// Props for the [`Meta`] component
@@ -176,7 +176,7 @@ pub fn Meta(props: MetaProps) -> Element {
         document.create_meta(props);
     });
 
-    rsx! {}
+    VNode::empty()
 }
 
 #[derive(Clone, Props, PartialEq)]
@@ -271,7 +271,7 @@ pub fn Script(props: ScriptProps) -> Element {
         document.create_script(props);
     });
 
-    rsx! {}
+    VNode::empty()
 }
 
 #[derive(Clone, Props, PartialEq)]
@@ -349,7 +349,7 @@ pub fn Style(props: StyleProps) -> Element {
         document.create_style(props);
     });
 
-    rsx! {}
+    VNode::empty()
 }
 
 use super::*;
@@ -462,7 +462,7 @@ pub fn Link(props: LinkProps) -> Element {
         document.create_link(props);
     });
 
-    rsx! {}
+    VNode::empty()
 }
 
 fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {

+ 1 - 0
packages/rsx-rosetta/src/lib.rs

@@ -131,6 +131,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
                     children: TemplateBody::new(vec![]),
                     brace: Default::default(),
                     dyn_idx: Default::default(),
+                    component_literal_dyn_idx: vec![],
                 });
 
                 std::mem::swap(child, &mut new_comp);

+ 138 - 0
packages/rsx/src/assign_dyn_ids.rs

@@ -0,0 +1,138 @@
+use crate::attribute::Attribute;
+use crate::{
+    AttributeValue, BodyNode, HotLiteral, HotReloadFormattedSegment, Segment, TemplateBody,
+};
+
+/// A visitor that assigns dynamic ids to nodes and attributes and accumulates paths to dynamic nodes and attributes
+struct DynIdVisitor<'a> {
+    body: &'a mut TemplateBody,
+    current_path: Vec<u8>,
+    dynamic_text_index: usize,
+    component_literal_index: usize,
+}
+
+impl<'a> DynIdVisitor<'a> {
+    fn new(body: &'a mut TemplateBody) -> Self {
+        Self {
+            body,
+            current_path: Vec::new(),
+            dynamic_text_index: 0,
+            component_literal_index: 0,
+        }
+    }
+
+    fn visit_children(&mut self, children: &[BodyNode]) {
+        for (idx, node) in children.iter().enumerate() {
+            self.current_path.push(idx as u8);
+            self.visit(node);
+            self.current_path.pop();
+        }
+    }
+
+    fn visit(&mut self, node: &BodyNode) {
+        match node {
+            // Just descend into elements - they're not dynamic
+            BodyNode::Element(el) => {
+                for (idx, attr) in el.merged_attributes.iter().enumerate() {
+                    if !attr.is_static_str_literal() {
+                        self.assign_path_to_attribute(attr, idx);
+                        if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &attr.value {
+                            self.assign_formatted_segment(lit);
+                        }
+                    }
+                }
+                // Assign formatted segments to the key which is not included in the merged_attributes
+                if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(fmted))) = el.key() {
+                    self.assign_formatted_segment(fmted);
+                }
+
+                self.visit_children(&el.children);
+            }
+
+            // Text nodes are dynamic if they contain dynamic segments
+            BodyNode::Text(txt) => {
+                if !txt.is_static() {
+                    self.assign_path_to_node(node);
+                    self.assign_formatted_segment(&txt.input);
+                }
+            }
+
+            // Raw exprs are always dynamic
+            BodyNode::RawExpr(_) | BodyNode::ForLoop(_) | BodyNode::IfChain(_) => {
+                self.assign_path_to_node(node)
+            }
+            BodyNode::Component(component) => {
+                self.assign_path_to_node(node);
+                let mut index = 0;
+                for property in &component.fields {
+                    if let AttributeValue::AttrLiteral(literal) = &property.value {
+                        if let HotLiteral::Fmted(segments) = literal {
+                            self.assign_formatted_segment(segments);
+                        }
+                        component.component_literal_dyn_idx[index]
+                            .set(self.component_literal_index);
+                        self.component_literal_index += 1;
+                        index += 1;
+                    }
+                }
+            }
+        };
+    }
+
+    /// Assign ids to a formatted segment
+    fn assign_formatted_segment(&mut self, segments: &HotReloadFormattedSegment) {
+        let mut dynamic_node_indexes = segments.dynamic_node_indexes.iter();
+        for segment in &segments.segments {
+            if let Segment::Formatted(segment) = segment {
+                dynamic_node_indexes
+                    .next()
+                    .unwrap()
+                    .set(self.dynamic_text_index);
+                self.dynamic_text_index += 1;
+                self.body.dynamic_text_segments.push(segment.clone());
+            }
+        }
+    }
+
+    /// Assign a path to a node and give it its dynamic index
+    /// This simplifies the ToTokens implementation for the macro to be a little less centralized
+    fn assign_path_to_node(&mut self, node: &BodyNode) {
+        // Assign the TemplateNode::Dynamic index to the node
+        node.set_dyn_idx(self.body.node_paths.len());
+
+        // And then save the current path as the corresponding path
+        self.body.node_paths.push(self.current_path.clone());
+    }
+
+    /// Assign a path to a attribute and give it its dynamic index
+    /// This simplifies the ToTokens implementation for the macro to be a little less centralized
+    pub(crate) fn assign_path_to_attribute(
+        &mut self,
+        attribute: &Attribute,
+        attribute_index: usize,
+    ) {
+        // Assign the dynamic index to the attribute
+        attribute.set_dyn_idx(self.body.attr_paths.len());
+
+        // And then save the current path as the corresponding path
+        self.body
+            .attr_paths
+            .push((self.current_path.clone(), attribute_index));
+    }
+}
+
+impl TemplateBody {
+    /// Cascade down path information into the children of this template
+    ///
+    /// This provides the necessary path and index information for the children of this template
+    /// so that they can render out their dynamic nodes correctly. Also does plumbing for things like
+    /// hotreloaded literals which need to be tracked on a per-template basis.
+    ///
+    /// This can only operate with knowledge of this template, not the surrounding callbody. Things like
+    /// wiring of ifmt literals need to be done at the callbody level since those final IDs need to
+    /// be unique to the entire app.
+    pub(crate) fn assign_paths_inner(&mut self, nodes: &[BodyNode]) {
+        let mut visitor = DynIdVisitor::new(self);
+        visitor.visit_children(nodes);
+    }
+}

+ 50 - 21
packages/rsx/src/attribute.rs

@@ -28,9 +28,6 @@ use syn::{
     Block, Expr, ExprClosure, ExprIf, Ident, Lit, LitBool, LitFloat, LitInt, LitStr, Token,
 };
 
-#[cfg(feature = "hot_reload")]
-use dioxus_core::prelude::TemplateAttribute;
-
 /// A property value in the from of a `name: value` pair with an optional comma.
 /// Note that the colon and value are optional in the case of shorthand attributes. We keep them around
 /// to support "lossless" parsing in case that ever might be useful.
@@ -120,6 +117,16 @@ impl Attribute {
         }
     }
 
+    /// Set the dynamic index of this attribute
+    pub fn set_dyn_idx(&self, idx: usize) {
+        self.dyn_idx.set(idx);
+    }
+
+    /// Get the dynamic index of this attribute
+    pub fn get_dyn_idx(&self) -> usize {
+        self.dyn_idx.get()
+    }
+
     pub fn span(&self) -> proc_macro2::Span {
         self.name.span()
     }
@@ -140,18 +147,15 @@ impl Attribute {
 
     pub fn ifmt(&self) -> Option<&IfmtInput> {
         match &self.value {
-            AttributeValue::AttrLiteral(lit) => match &lit.value {
-                HotLiteralType::Fmted(input) => Some(input),
-                _ => None,
-            },
+            AttributeValue::AttrLiteral(HotLiteral::Fmted(input)) => Some(input),
             _ => None,
         }
     }
 
     pub fn as_static_str_literal(&self) -> Option<(&AttributeName, &IfmtInput)> {
         match &self.value {
-            AttributeValue::AttrLiteral(lit) => match &lit.value {
-                HotLiteralType::Fmted(input) if input.is_static() => Some((&self.name, input)),
+            AttributeValue::AttrLiteral(lit) => match &lit {
+                HotLiteral::Fmted(input) if input.is_static() => Some((&self.name, input)),
                 _ => None,
             },
             _ => None,
@@ -162,11 +166,27 @@ impl Attribute {
         self.as_static_str_literal().is_some()
     }
 
+    #[cfg(feature = "hot_reload")]
+    pub(crate) fn html_tag_and_namespace<Ctx: crate::HotReloadingContext>(
+        &self,
+    ) -> (&'static str, Option<&'static str>) {
+        let attribute_name_rust = self.name.to_string();
+        let element_name = self.el_name.as_ref().unwrap();
+        let rust_name = match element_name {
+            ElementName::Ident(i) => i.to_string(),
+            ElementName::Custom(s) => return (intern(s.value()), None),
+        };
+
+        Ctx::map_attribute(&rust_name, &attribute_name_rust)
+            .unwrap_or((intern(attribute_name_rust.as_str()), None))
+    }
+
     #[cfg(feature = "hot_reload")]
     pub fn to_template_attribute<Ctx: crate::HotReloadingContext>(
         &self,
-        rust_name: &str,
-    ) -> TemplateAttribute {
+    ) -> dioxus_core::TemplateAttribute {
+        use dioxus_core::TemplateAttribute;
+
         // If it's a dynamic node, just return it
         // For dynamic attributes, we need to check the mapping to see if that mapping exists
         // todo: one day we could generate new dynamic attributes on the fly if they're a literal,
@@ -180,11 +200,8 @@ impl Attribute {
         }
 
         // Otherwise it's a static node and we can build it
-        let (_name, value) = self.as_static_str_literal().unwrap();
-        let attribute_name_rust = self.name.to_string();
-
-        let (name, namespace) = Ctx::map_attribute(rust_name, &attribute_name_rust)
-            .unwrap_or((intern(attribute_name_rust.as_str()), None));
+        let (_, value) = self.as_static_str_literal().unwrap();
+        let (name, namespace) = self.html_tag_and_namespace::<Ctx>();
 
         TemplateAttribute::Static {
             name,
@@ -303,8 +320,15 @@ impl Attribute {
             return true;
         }
 
-        if self.name.to_token_stream().to_string() == self.value.to_token_stream().to_string() {
-            return true;
+        // Or if it is a builtin attribute with a single ident value
+        if let (AttributeName::BuiltIn(name), AttributeValue::AttrExpr(expr)) =
+            (&self.name, &self.value)
+        {
+            if let Ok(Expr::Path(path)) = expr.as_expr() {
+                if path.path.get_ident() == Some(name) {
+                    return true;
+                }
+            }
         }
 
         false
@@ -537,7 +561,11 @@ impl IfAttributeValue {
                 return non_string_diagnostic(current_if_value.span());
             };
 
-            let HotLiteralType::Fmted(new) = &lit.value else {
+            let HotLiteral::Fmted(HotReloadFormattedSegment {
+                formatted_input: new,
+                ..
+            }) = &lit
+            else {
                 return non_string_diagnostic(current_if_value.span());
             };
 
@@ -554,8 +582,9 @@ impl IfAttributeValue {
                 }
                 // If the else value is a literal, then we need to append it to the expression and break
                 Some(AttributeValue::AttrLiteral(lit)) => {
-                    if let HotLiteralType::Fmted(new) = &lit.value {
-                        expression.extend(quote! { { #new.to_string() } });
+                    if let HotLiteral::Fmted(new) = &lit {
+                        let fmted = &new.formatted_input;
+                        expression.extend(quote! { { #fmted.to_string() } });
                         break;
                     } else {
                         return non_string_diagnostic(current_if_value.span());

+ 43 - 32
packages/rsx/src/component.rs

@@ -20,7 +20,7 @@ use crate::innerlude::*;
 use proc_macro2::TokenStream as TokenStream2;
 use proc_macro2_diagnostics::SpanDiagnosticExt;
 use quote::{quote, ToTokens, TokenStreamExt};
-use std::collections::HashSet;
+use std::{collections::HashSet, vec};
 use syn::{
     parse::{Parse, ParseStream},
     spanned::Spanned,
@@ -32,6 +32,7 @@ pub struct Component {
     pub name: syn::Path,
     pub generics: Option<AngleBracketedGenericArguments>,
     pub fields: Vec<Attribute>,
+    pub component_literal_dyn_idx: Vec<DynIdx>,
     pub spreads: Vec<Spread>,
     pub brace: token::Brace,
     pub children: TemplateBody,
@@ -56,12 +57,19 @@ impl Parse for Component {
             diagnostics,
         } = input.parse::<RsxBlock>()?;
 
+        let literal_properties_count = fields
+            .iter()
+            .filter(|attr| matches!(attr.value, AttributeValue::AttrLiteral(_)))
+            .count();
+        let component_literal_dyn_idx = vec![DynIdx::default(); literal_properties_count];
+
         let mut component = Self {
             dyn_idx: DynIdx::default(),
             children: TemplateBody::new(children),
             name,
             generics,
             fields,
+            component_literal_dyn_idx,
             brace,
             spreads,
             diagnostics,
@@ -71,7 +79,6 @@ impl Parse for Component {
         // validating it will dump diagnostics into the output
         component.validate_component_path();
         component.validate_fields();
-        component.validate_key();
         component.validate_component_spread();
 
         Ok(component)
@@ -167,38 +174,17 @@ impl Component {
         }
     }
 
-    /// Ensure only one key and that the key is not a static str
-    ///
-    /// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
-    fn validate_key(&mut self) {
-        let key = self.get_key();
-
-        if let Some(attr) = key {
-            let diagnostic = match &attr.value {
-                AttributeValue::AttrLiteral(ifmt) if ifmt.is_static() => {
-                    ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
-                }
-                AttributeValue::AttrLiteral(_) => return,
-                _ => attr
-                    .value
-                    .span()
-                    .error("Key must be in the form of a formatted string like `key: \"{value}\""),
-            };
-
-            self.diagnostics.push(diagnostic);
-        }
-    }
-
-    pub fn get_key(&self) -> Option<&Attribute> {
-        self.fields
-            .iter()
-            .find(|attr| matches!(&attr.name, AttributeName::BuiltIn(key) if key == "key"))
+    pub fn get_key(&self) -> Option<&AttributeValue> {
+        self.fields.iter().find_map(|attr| match &attr.name {
+            AttributeName::BuiltIn(key) if key == "key" => Some(&attr.value),
+            _ => None,
+        })
     }
 
     /// Ensure there's no duplicate props - this will be a compile error but we can move it to a
     /// diagnostic, thankfully
     ///
-    /// Also ensure there's no stringly typed propsa
+    /// Also ensure there's no stringly typed props
     fn validate_fields(&mut self) {
         let mut seen = HashSet::new();
 
@@ -271,9 +257,10 @@ impl Component {
     }
 
     fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
+        let mut dynamic_literal_index = 0;
         self.fields
             .iter()
-            .filter_map(|attr| {
+            .filter_map(move |attr| {
                 let Attribute { name, value, .. } = attr;
 
                 let attr = match name {
@@ -287,7 +274,30 @@ impl Component {
                     AttributeName::Spread(_) => return None,
                 };
 
-                Some((attr, value.to_token_stream()))
+                let release_value = value.to_token_stream();
+
+                // In debug mode, we try to grab the value from the dynamic literal pool if possible
+                let value = if let AttributeValue::AttrLiteral(literal) = &value {
+                    let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
+                    dynamic_literal_index += 1;
+                    let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
+                    quote! {
+                        {
+                            #[cfg(debug_assertions)]
+                            {
+                                #debug_value
+                            }
+                            #[cfg(not(debug_assertions))]
+                            {
+                                #release_value
+                            }
+                        }
+                    }
+                } else {
+                    release_value
+                };
+
+                Some((attr, value))
             })
             .collect()
     }
@@ -310,6 +320,7 @@ impl Component {
             fields: vec![],
             spreads: vec![],
             children: TemplateBody::new(vec![]),
+            component_literal_dyn_idx: vec![],
             dyn_idx: DynIdx::default(),
             diagnostics,
         }
@@ -429,7 +440,7 @@ fn generics_params() {
     let input_without_children = quote! {
          Outlet::<R> {}
     };
-    let component: CallBody = syn::parse2(input_without_children).unwrap();
+    let component: crate::CallBody = syn::parse2(input_without_children).unwrap();
     println!("{}", component.to_token_stream().pretty_unparse());
 }
 

+ 6 - 11
packages/rsx/src/element.rs

@@ -270,11 +270,9 @@ impl Element {
                 }
 
                 // Merge raw literals into the output
-                if let AttributeValue::AttrLiteral(lit) = &matching_attr.value {
-                    if let HotLiteralType::Fmted(new) = &lit.value {
-                        out.push_ifmt(new.clone());
-                        continue;
-                    }
+                if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &matching_attr.value {
+                    out.push_ifmt(lit.formatted_input.clone());
+                    continue;
                 }
 
                 // Merge `if cond { "abc" } else if ...` into the output
@@ -289,10 +287,7 @@ impl Element {
                 );
             }
 
-            let out_lit = HotLiteral {
-                value: HotLiteralType::Fmted(out),
-                hr_idx: Default::default(),
-            };
+            let out_lit = HotLiteral::Fmted(out.into());
 
             self.merged_attributes.push(Attribute {
                 name: attr.name.clone(),
@@ -305,11 +300,11 @@ impl Element {
         }
     }
 
-    pub(crate) fn key(&self) -> Option<&IfmtInput> {
+    pub(crate) fn key(&self) -> Option<&AttributeValue> {
         for attr in &self.raw_attributes {
             if let AttributeName::BuiltIn(name) = &attr.name {
                 if name == "key" {
-                    return attr.ifmt();
+                    return Some(&attr.value);
                 }
             }
         }

+ 0 - 0
packages/rsx/src/hot_reload/hot_reload_diff.rs → packages/rsx/src/hot_reload/collect.rs


+ 0 - 0
packages/rsx/src/hot_reload/hot_reloading_context.rs → packages/rsx/src/hot_reload/context.rs


+ 651 - 0
packages/rsx/src/hot_reload/diff.rs

@@ -0,0 +1,651 @@
+//! This module contains the diffing logic for rsx hot reloading.
+//!
+//! There's a few details that I wish we could've gotten right but we can revisit later:
+//!
+//! - Expanding an if chain is not possible - only its contents can be hot reloaded
+//!
+//! - Components that don't start with children can't be hot reloaded - IE going from `Comp {}` to `Comp { "foo" }`
+//!   is not possible. We could in theory allow this by seeding all Components with a `children` field.
+//!
+//! - Cross-templates hot reloading is not possible - multiple templates don't share the dynamic pool. This would require handling aliases
+//!   in hot reload diffing.
+//!
+//! - We've proven that binary patching is feasible but has a longer path to stabilization for all platforms.
+//!   Binary patching is pretty quick, actually, and *might* remove the need to literal hot reloading.
+//!   However, you could imagine a scenario where literal hot reloading would be useful without the
+//!   compiler in the loop. Ideally we can slash most of this code once patching is stable.
+//!
+//! ## Assigning/Scoring Templates
+//!
+//! We can clone most dynamic items from the last full rebuild:
+//! - Dynamic text segments: `div { width: "{x}%" } -> div { width: "{x}%", height: "{x}%" }`
+//! - Dynamic attributes: `div { width: dynamic } -> div { width: dynamic, height: dynamic }`
+//! - Dynamic nodes: `div { {children} } -> div { {children} {children} }`
+//!
+//! But we cannot clone rsx bodies themselves because we cannot hot reload the new rsx body:
+//! - `div { Component { "{text}" } } -> div { Component { "{text}" } Component { "hello" } }` // We can't create a template for both "{text}" and "hello"
+//!
+//! In some cases, two nodes with children are ambiguous. For example:
+//! ```rust, ignore
+//! rsx! {
+//!     div {
+//!         Component { "{text}" }
+//!         Component { "hello" }
+//!     }
+//! }
+//! ```
+//!
+//! Outside of the template, both components are compatible for hot reloading.
+//!
+//! After we create a list of all components with compatible names and props, we need to find the best match for the
+//! template.
+//!
+//!
+//! Dioxus uses a greedy algorithm to find the best match. We first try to create the child template with the dynamic context from the last full rebuild.
+//! Then we use the child template that leaves the least unused dynamic items in the pool to create the new template.
+//!
+//! For the example above:
+//! - Hot reloading `Component { "hello" }`:
+//!   - Try to hot reload the component body `"hello"` with the dynamic pool from `"{text}"`: Success with 1 unused dynamic item
+//!   - Try to hot reload the component body `"hello"` with the dynamic pool from `"hello"`: Success with 0 unused dynamic items
+//!   - We use the the template that leaves the least unused dynamic items in the pool - `"hello"`
+//! - Hot reloading `Component { "{text}" }`:
+//!   - Try to hot reload the component body `"{text}"` with the dynamic pool from `"{text}"`: Success with 0 unused dynamic items
+//!   - The `"hello"` template has already been hot reloaded, so we don't try to hot reload it again
+//!   - We use the the template that leaves the least unused dynamic items in the pool - `"{text}"`
+//!
+//! Greedy algorithms are optimal when:
+//! - The step we take reduces the problem size
+//! - The subproblem is optimal
+//!
+//! In this case, hot reloading a template removes it from the pool of templates we can use to hot reload the next template which reduces the problem size.
+//!
+//! The subproblem is optimal because the alternative is leaving less dynamic items for the remaining templates to hot reload which just makes it
+//! more difficult to match future templates.
+
+use crate::innerlude::*;
+use crate::HotReloadingContext;
+use dioxus_core::internal::{
+    FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute, HotReloadDynamicNode,
+    HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
+};
+use std::collections::HashMap;
+use std::hash::DefaultHasher;
+use std::hash::Hash;
+use std::hash::Hasher;
+
+use super::last_build_state::LastBuildState;
+
+/// A result of hot reloading
+///
+/// This contains information about what has changed so the hotreloader can apply the right changes
+#[non_exhaustive]
+#[derive(Debug, PartialEq, Clone)]
+pub struct HotReloadResult {
+    /// The state of the last full rebuild.
+    full_rebuild_state: LastBuildState,
+
+    /// The child templates we have already used. As we walk through the template tree, we will run into child templates.
+    /// Each of those child templates also need to be hot reloaded. We keep track of which ones we've already hotreloaded
+    /// to avoid diffing the same template twice against different new templates.
+    ///
+    /// ```rust, ignore
+    /// rsx! {
+    ///     Component { class: "{class}", "{text}" } // The children of a Component is a new template
+    ///     for item in items {
+    ///         "{item}" // The children of a for loop is a new template
+    ///     }
+    ///     if true {
+    ///         "{text}" // The children of an if chain is a new template
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// If we hotreload the component, we don't need to hotreload the for loop
+    ///
+    /// You should diff the result of this against the old template to see if you actually need to send down the result
+    pub templates: HashMap<usize, HotReloadedTemplate>,
+
+    /// The dynamic nodes for the current node
+    dynamic_nodes: Vec<HotReloadDynamicNode>,
+
+    /// The dynamic attributes for the current node
+    dynamic_attributes: Vec<HotReloadDynamicAttribute>,
+
+    /// The literal component properties for the current node
+    literal_component_properties: Vec<HotReloadLiteral>,
+}
+
+impl HotReloadResult {
+    /// Calculate the hot reload diff between two template bodies
+    pub fn new<Ctx: HotReloadingContext>(
+        full_rebuild_state: &TemplateBody,
+        new: &TemplateBody,
+        name: String,
+    ) -> Option<Self> {
+        let full_rebuild_state = LastBuildState::new(full_rebuild_state, name);
+        let mut s = Self {
+            full_rebuild_state,
+            templates: Default::default(),
+            dynamic_nodes: Default::default(),
+            dynamic_attributes: Default::default(),
+            literal_component_properties: Default::default(),
+        };
+
+        s.hotreload_body::<Ctx>(new)?;
+
+        Some(s)
+    }
+
+    fn extend(&mut self, other: Self) {
+        self.templates.extend(other.templates);
+    }
+
+    /// Walk the dynamic contexts and do our best to find hot reload-able changes between the two
+    /// sets of dynamic nodes/attributes. If there's a change we can't hot reload, we'll return None
+    ///
+    /// Otherwise, we pump out the list of templates that need to be updated. The templates will be
+    /// re-ordered such that the node paths will be adjusted to match the new template for every
+    /// existing dynamic node.
+    ///
+    /// ```ignore
+    /// old:
+    ///     [[0], [1], [2]]
+    ///     rsx! {
+    ///         "{one}"
+    ///         "{two}"
+    ///         "{three}"
+    ///     }
+    ///
+    /// new:
+    ///     [[0], [2], [1, 1]]
+    ///     rsx! {
+    ///        "{one}"
+    ///         div { "{three}" }
+    ///         "{two}"
+    ///    }
+    /// ```
+    ///
+    /// Generally we can't hot reload a node if:
+    /// - We add or modify a new rust expression
+    ///   - Adding a new formatted segment we haven't seen before
+    ///   - Adding a new dynamic node (loop, fragment, if chain, etc)
+    /// - We add a new component field
+    /// - We remove a component field
+    /// - We change the type of a component field
+    ///
+    /// If a dynamic node is removed, we don't necessarily need to kill hot reload - just unmounting it should be enough
+    /// If the dynamic node is re-added, we want to be able to find it again.
+    ///
+    /// This encourages the hot reloader to hot onto DynamicContexts directly instead of the CallBody since
+    /// you can preserve more information about the nodes as they've changed over time.
+    fn hotreload_body<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
+        // Quickly run through dynamic attributes first attempting to invalidate them
+        // Move over old IDs onto the new template
+        self.hotreload_attributes::<Ctx>(new)?;
+        let new_dynamic_attributes = std::mem::take(&mut self.dynamic_attributes);
+
+        // Now we can run through the dynamic nodes and see if we can hot reload them
+        // Move over old IDs onto the new template
+        self.hotreload_dynamic_nodes::<Ctx>(new)?;
+        let new_dynamic_nodes = std::mem::take(&mut self.dynamic_nodes);
+        let literal_component_properties = std::mem::take(&mut self.literal_component_properties);
+
+        let key = self.hot_reload_key(new)?;
+
+        let roots: Vec<_> = new
+            .roots
+            .iter()
+            .map(|node| node.to_template_node::<Ctx>())
+            .collect();
+        let roots: &[dioxus_core::TemplateNode] = intern(&*roots);
+
+        // Add the template name, the dyn index and the hash of the template to get a unique name
+        let name = {
+            let mut hasher = DefaultHasher::new();
+            key.hash(&mut hasher);
+            new_dynamic_attributes.hash(&mut hasher);
+            new_dynamic_nodes.hash(&mut hasher);
+            literal_component_properties.hash(&mut hasher);
+            roots.hash(&mut hasher);
+            let hash = hasher.finish();
+            let name = &self.full_rebuild_state.name;
+
+            format!("{}:{}-{}", name, hash, new.template_idx.get())
+        };
+        let name = Box::leak(name.into_boxed_str());
+
+        let template = HotReloadedTemplate::new(
+            name,
+            key,
+            new_dynamic_nodes,
+            new_dynamic_attributes,
+            literal_component_properties,
+            roots,
+        );
+
+        self.templates
+            .insert(self.full_rebuild_state.root_index.get(), template);
+
+        Some(())
+    }
+
+    fn hot_reload_key(&mut self, new: &TemplateBody) -> Option<Option<FmtedSegments>> {
+        match new.implicit_key() {
+            Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(value))) => Some(Some(
+                self.full_rebuild_state
+                    .hot_reload_formatted_segments(value)?,
+            )),
+            None => Some(None),
+            _ => None,
+        }
+    }
+
+    fn hotreload_dynamic_nodes<Ctx: HotReloadingContext>(
+        &mut self,
+        new: &TemplateBody,
+    ) -> Option<()> {
+        for new_node in new.dynamic_nodes() {
+            self.hot_reload_node::<Ctx>(new_node)?
+        }
+
+        Some(())
+    }
+
+    fn hot_reload_node<Ctx: HotReloadingContext>(&mut self, node: &BodyNode) -> Option<()> {
+        match node {
+            BodyNode::Text(text) => self.hotreload_text_node(text),
+            BodyNode::Component(component) => self.hotreload_component::<Ctx>(component),
+            BodyNode::ForLoop(forloop) => self.hotreload_for_loop::<Ctx>(forloop),
+            BodyNode::IfChain(ifchain) => self.hotreload_if_chain::<Ctx>(ifchain),
+            BodyNode::RawExpr(expr) => self.hotreload_raw_expr(expr),
+            BodyNode::Element(_) => Some(()),
+        }
+    }
+
+    fn hotreload_raw_expr(&mut self, expr: &ExprNode) -> Option<()> {
+        // Try to find the raw expr in the last build
+        let expr_index = self
+            .full_rebuild_state
+            .dynamic_nodes
+            .position(|node| match &node {
+                BodyNode::RawExpr(raw_expr) => raw_expr.expr == expr.expr,
+                _ => false,
+            })?;
+
+        // If we find it, push it as a dynamic node
+        self.dynamic_nodes
+            .push(HotReloadDynamicNode::Dynamic(expr_index));
+
+        Some(())
+    }
+
+    fn hotreload_for_loop<Ctx>(&mut self, forloop: &ForLoop) -> Option<()>
+    where
+        Ctx: HotReloadingContext,
+    {
+        // Find all for loops that have the same pattern and expression
+        let candidate_for_loops = self
+            .full_rebuild_state
+            .dynamic_nodes
+            .inner
+            .iter()
+            .enumerate()
+            .filter_map(|(index, node)| {
+                if let BodyNode::ForLoop(for_loop) = &node.inner {
+                    if for_loop.pat == forloop.pat && for_loop.expr == forloop.expr {
+                        return Some((index, for_loop));
+                    }
+                }
+                None
+            })
+            .collect::<Vec<_>>();
+
+        // Then find the one that has the least wasted dynamic items when hot reloading the body
+        let (index, best_call_body) = self.diff_best_call_body::<Ctx>(
+            candidate_for_loops
+                .iter()
+                .map(|(_, for_loop)| &for_loop.body),
+            &forloop.body,
+        )?;
+
+        // Push the new for loop as a dynamic node
+        self.dynamic_nodes
+            .push(HotReloadDynamicNode::Dynamic(candidate_for_loops[index].0));
+
+        self.extend(best_call_body);
+
+        Some(())
+    }
+
+    fn hotreload_text_node(&mut self, text_node: &TextNode) -> Option<()> {
+        // If it is static, it is already included in the template and we don't need to do anything
+        if text_node.input.is_static() {
+            return Some(());
+        }
+        // Otherwise, hot reload the formatted segments and push that as a dynamic node
+        let formatted_segments = self
+            .full_rebuild_state
+            .hot_reload_formatted_segments(&text_node.input)?;
+        self.dynamic_nodes
+            .push(HotReloadDynamicNode::Formatted(formatted_segments));
+        Some(())
+    }
+
+    /// Find the call body that minimizes the number of wasted dynamic items
+    ///
+    /// Returns the index of the best call body and the state of the best call body
+    fn diff_best_call_body<'a, Ctx>(
+        &self,
+        bodies: impl Iterator<Item = &'a TemplateBody>,
+        new_call_body: &TemplateBody,
+    ) -> Option<(usize, Self)>
+    where
+        Ctx: HotReloadingContext,
+    {
+        let mut best_score = usize::MAX;
+        let mut best_output = None;
+        for (index, body) in bodies.enumerate() {
+            // Skip templates we've already hotreloaded
+            if self.templates.contains_key(&body.template_idx.get()) {
+                continue;
+            }
+            if let Some(state) =
+                Self::new::<Ctx>(body, new_call_body, self.full_rebuild_state.name.clone())
+            {
+                let score = state.full_rebuild_state.unused_dynamic_items();
+                if score < best_score {
+                    best_score = score;
+                    best_output = Some((index, state));
+                }
+            }
+        }
+
+        best_output
+    }
+
+    fn hotreload_component<Ctx>(&mut self, component: &Component) -> Option<()>
+    where
+        Ctx: HotReloadingContext,
+    {
+        // First we need to find the component that matches the best in the last build
+        // We try each build and choose the option that wastes the least dynamic items
+        let components_with_matching_attributes: Vec<_> = self
+            .full_rebuild_state
+            .dynamic_nodes
+            .inner
+            .iter()
+            .enumerate()
+            .filter_map(|(index, node)| {
+                if let BodyNode::Component(comp) = &node.inner {
+                    return Some((
+                        index,
+                        comp,
+                        self.hotreload_component_fields(comp, component)?,
+                    ));
+                }
+                None
+            })
+            .collect();
+
+        let possible_bodies = components_with_matching_attributes
+            .iter()
+            .map(|(_, comp, _)| &comp.children);
+
+        let (index, new_body) =
+            self.diff_best_call_body::<Ctx>(possible_bodies, &component.children)?;
+
+        let (index, _, literal_component_properties) = &components_with_matching_attributes[index];
+        let index = *index;
+
+        self.full_rebuild_state.dynamic_nodes.inner[index]
+            .used
+            .set(true);
+
+        self.literal_component_properties
+            .extend(literal_component_properties.iter().cloned());
+
+        self.extend(new_body);
+
+        // Push the new component as a dynamic node
+        self.dynamic_nodes
+            .push(HotReloadDynamicNode::Dynamic(index));
+
+        Some(())
+    }
+
+    fn hotreload_component_fields(
+        &self,
+        old_component: &Component,
+        new_component: &Component,
+    ) -> Option<Vec<HotReloadLiteral>> {
+        // First check if the component is the same
+        if new_component.name != old_component.name {
+            return None;
+        }
+
+        // Then check if the fields are the same
+        if new_component.fields.len() != old_component.fields.len() {
+            return None;
+        }
+
+        let mut new_fields = new_component.fields.clone();
+        new_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
+        let mut old_fields = old_component.fields.clone();
+        old_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
+
+        let mut literal_component_properties = Vec::new();
+
+        for (new_field, old_field) in new_fields.iter().zip(old_fields.iter()) {
+            // Verify the names match
+            if new_field.name != old_field.name {
+                return None;
+            }
+
+            // Verify the values match
+            match (&new_field.value, &old_field.value) {
+                // If the values are both literals, we can try to hotreload them
+                (
+                    AttributeValue::AttrLiteral(new_value),
+                    AttributeValue::AttrLiteral(old_value),
+                ) => {
+                    // Make sure that the types are the same
+                    if std::mem::discriminant(new_value) != std::mem::discriminant(old_value) {
+                        return None;
+                    }
+                    let literal = self.full_rebuild_state.hotreload_hot_literal(new_value)?;
+                    literal_component_properties.push(literal);
+                }
+                _ => {
+                    if new_field.value != old_field.value {
+                        return None;
+                    }
+                }
+            }
+        }
+
+        Some(literal_component_properties)
+    }
+
+    /// Hot reload an if chain
+    fn hotreload_if_chain<Ctx: HotReloadingContext>(
+        &mut self,
+        new_if_chain: &IfChain,
+    ) -> Option<()> {
+        let mut best_if_chain = None;
+        let mut best_score = usize::MAX;
+
+        let if_chains = self
+            .full_rebuild_state
+            .dynamic_nodes
+            .inner
+            .iter()
+            .enumerate()
+            .filter_map(|(index, node)| {
+                if let BodyNode::IfChain(if_chain) = &node.inner {
+                    return Some((index, if_chain));
+                }
+                None
+            });
+
+        // Find the if chain that matches all of the conditions and wastes the least dynamic items
+        for (index, old_if_chain) in if_chains {
+            let Some(chain_templates) = Self::diff_if_chains::<Ctx>(
+                old_if_chain,
+                new_if_chain,
+                self.full_rebuild_state.name.clone(),
+            ) else {
+                continue;
+            };
+            let score = chain_templates
+                .iter()
+                .map(|t| t.full_rebuild_state.unused_dynamic_items())
+                .sum();
+            if score < best_score {
+                best_score = score;
+                best_if_chain = Some((index, chain_templates));
+            }
+        }
+
+        // If we found a hot reloadable if chain, hotreload it
+        let (index, chain_templates) = best_if_chain?;
+        // Mark the if chain as used
+        self.full_rebuild_state.dynamic_nodes.inner[index]
+            .used
+            .set(true);
+        // Merge the hot reload changes into the current state
+        for template in chain_templates {
+            self.extend(template);
+        }
+
+        // Push the new if chain as a dynamic node
+        self.dynamic_nodes
+            .push(HotReloadDynamicNode::Dynamic(index));
+
+        Some(())
+    }
+
+    /// Hot reload an if chain
+    fn diff_if_chains<Ctx: HotReloadingContext>(
+        old_if_chain: &IfChain,
+        new_if_chain: &IfChain,
+        name: String,
+    ) -> Option<Vec<Self>> {
+        // Go through each part of the if chain and find the best match
+        let mut old_chain = old_if_chain;
+        let mut new_chain = new_if_chain;
+
+        let mut chain_templates = Vec::new();
+
+        loop {
+            // Make sure the conditions are the same
+            if old_chain.cond != new_chain.cond {
+                return None;
+            }
+
+            // If the branches are the same, we can hotreload them
+            let hot_reload =
+                Self::new::<Ctx>(&old_chain.then_branch, &new_chain.then_branch, name.clone())?;
+            chain_templates.push(hot_reload);
+
+            // Make sure the if else branches match
+            match (
+                old_chain.else_if_branch.as_ref(),
+                new_chain.else_if_branch.as_ref(),
+            ) {
+                (Some(old), Some(new)) => {
+                    old_chain = old;
+                    new_chain = new;
+                }
+                (None, None) => {
+                    break;
+                }
+                _ => return None,
+            }
+        }
+        // Make sure the else branches match
+        match (&old_chain.else_branch, &new_chain.else_branch) {
+            (Some(old), Some(new)) => {
+                let template = Self::new::<Ctx>(old, new, name.clone())?;
+                chain_templates.push(template);
+            }
+            (None, None) => {}
+            _ => return None,
+        }
+
+        Some(chain_templates)
+    }
+
+    /// Take a new template body and return the attributes that can be hot reloaded from the last build
+    ///
+    /// IE if we shuffle attributes, remove attributes or add new attributes with the same dynamic segments, around we should be able to hot reload them.
+    ///
+    /// ```rust, ignore
+    /// rsx! {
+    ///     div { id: "{id}", class: "{class}", width, "Hi" }
+    /// }
+    ///
+    /// rsx! {
+    ///     div { width, class: "{class}", id: "{id} and {class}", "Hi" }
+    /// }
+    /// ```
+    fn hotreload_attributes<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
+        // Walk through each attribute and create a new HotReloadAttribute for each one
+        for new_attr in new.dynamic_attributes() {
+            // While we're here, if it's a literal and not a perfect score, it's a mismatch and we need to
+            // hotreload the literal
+            self.hotreload_attribute::<Ctx>(new_attr)?;
+        }
+
+        Some(())
+    }
+
+    /// Try to hot reload an attribute and return the new HotReloadAttribute
+    fn hotreload_attribute<Ctx: HotReloadingContext>(
+        &mut self,
+        attribute: &Attribute,
+    ) -> Option<()> {
+        let (tag, namespace) = attribute.html_tag_and_namespace::<Ctx>();
+
+        // If the attribute is a spread, try to grab it from the last build
+        // If it wasn't in the last build with the same name, we can't hot reload it
+        if let AttributeName::Spread(_) = &attribute.name {
+            let hot_reload_attribute = self
+                .full_rebuild_state
+                .dynamic_attributes
+                .position(|a| a.name == attribute.name && a.value == attribute.value)?;
+            self.dynamic_attributes
+                .push(HotReloadDynamicAttribute::Dynamic(hot_reload_attribute));
+
+            return Some(());
+        }
+
+        // Otherwise the attribute is named, try to hot reload the value
+        let value = match &attribute.value {
+            // If the attribute is a literal, we can generally hot reload it if the formatted segments exist in the last build
+            AttributeValue::AttrLiteral(literal) => {
+                // If it is static, it is already included in the template and we don't need to do anything
+                if literal.is_static() {
+                    return Some(());
+                }
+                // Otherwise, hot reload the literal and push that as a dynamic attribute
+                let hot_reload_literal = self.full_rebuild_state.hotreload_hot_literal(literal)?;
+                HotReloadAttributeValue::Literal(hot_reload_literal)
+            }
+            // If it isn't a literal, try to find an exact match for the attribute value from the last build
+            _ => {
+                let value_index = self.full_rebuild_state.dynamic_attributes.position(|a| {
+                    !matches!(a.name, AttributeName::Spread(_)) && a.value == attribute.value
+                })?;
+                HotReloadAttributeValue::Dynamic(value_index)
+            }
+        };
+
+        self.dynamic_attributes
+            .push(HotReloadDynamicAttribute::Named(NamedAttribute::new(
+                tag, namespace, value,
+            )));
+
+        Some(())
+    }
+}

+ 157 - 0
packages/rsx/src/hot_reload/last_build_state.rs

@@ -0,0 +1,157 @@
+use crate::innerlude::*;
+use dioxus_core::internal::{FmtSegment, FmtedSegments, HotReloadLiteral};
+use std::cell::Cell;
+
+/// A pool of items we can grab from during hot reloading.
+/// We have three different pools we can pull from:
+/// - Dynamic text segments (eg: "{class}")
+/// - Dynamic nodes (eg: {children})
+/// - Dynamic attributes (eg: ..spread )
+///
+/// As we try to create a new hot reloaded template, we will pull from these pools to create the new template. We mark
+/// each item as used the first time we use it in the new template. Once the new template if fully created, we can tally
+/// up how many items are unused to determine how well the new template matches the old template.
+///
+/// The template that matches best will leave the least unused items in the pool.
+#[derive(Debug, PartialEq, Clone)]
+pub(crate) struct BakedPool<T> {
+    pub inner: Vec<BakedItem<T>>,
+}
+
+impl<T> BakedPool<T> {
+    /// Create a new baked pool from an iterator of items
+    fn new(inner: impl IntoIterator<Item = T>) -> Self {
+        Self {
+            inner: inner.into_iter().map(BakedItem::new).collect(),
+        }
+    }
+
+    /// Find the first item in the pool that matches the condition and mark it as used
+    pub fn position(&self, condition: impl Fn(&T) -> bool) -> Option<usize> {
+        for (idx, baked_item) in self.inner.iter().enumerate() {
+            if condition(&baked_item.inner) {
+                baked_item.used.set(true);
+                return Some(idx);
+            }
+        }
+        None
+    }
+
+    /// Find the number of unused items in the pool
+    fn unused_dynamic_items(&self) -> usize {
+        self.inner
+            .iter()
+            .filter(|baked_item| !baked_item.used.get())
+            .count()
+    }
+}
+
+/// A single item in the baked item pool. We keep track if which items are used for scoring how well two templates match.
+#[derive(Debug, PartialEq, Clone)]
+pub(crate) struct BakedItem<T> {
+    pub inner: T,
+    pub used: Cell<bool>,
+}
+
+impl<T> BakedItem<T> {
+    fn new(inner: T) -> Self {
+        Self {
+            inner,
+            used: Cell::new(false),
+        }
+    }
+}
+
+/// The state of the last full rebuild.
+/// This object contains the pool of compiled dynamic segments we can pull from for hot reloading
+#[derive(Debug, PartialEq, Clone)]
+pub(crate) struct LastBuildState {
+    /// The formatted segments that were used in the last build. Eg: "{class}", "{id}"
+    ///
+    /// We are free to use each of these segments many times in the same build.
+    /// We just clone the result (assuming display + debug have no side effects)
+    pub dynamic_text_segments: BakedPool<FormattedSegment>,
+    /// The dynamic nodes that were used in the last build. Eg: div { {children} }
+    ///
+    /// We are also free to clone these nodes many times in the same build.
+    pub dynamic_nodes: BakedPool<BodyNode>,
+    /// The attributes that were used in the last build. Eg: div { class: "{class}" }
+    ///
+    /// We are also free to clone these nodes many times in the same build.
+    pub dynamic_attributes: BakedPool<Attribute>,
+    /// The component literal properties we can hot reload from the last build. Eg: Component { class: "{class}" }
+    ///
+    /// In the new build, we must assign each of these a value even if we no longer use the component.
+    /// The type must be the same as the last time we compiled the property
+    pub component_properties: Vec<HotLiteral>,
+    /// The root indexes of the last build
+    pub root_index: DynIdx,
+    /// The name of the original template
+    pub name: String,
+}
+
+impl LastBuildState {
+    /// Create a new LastBuildState from the given [`TemplateBody`]
+    pub fn new(body: &TemplateBody, name: String) -> Self {
+        let dynamic_text_segments = body.dynamic_text_segments.iter().cloned();
+        let dynamic_nodes = body.dynamic_nodes().cloned();
+        let dynamic_attributes = body.dynamic_attributes().cloned();
+        let component_properties = body.literal_component_properties().cloned().collect();
+        Self {
+            dynamic_text_segments: BakedPool::new(dynamic_text_segments),
+            dynamic_nodes: BakedPool::new(dynamic_nodes),
+            dynamic_attributes: BakedPool::new(dynamic_attributes),
+            component_properties,
+            root_index: body.template_idx.clone(),
+            name,
+        }
+    }
+
+    /// Return the number of unused dynamic items in the pool
+    pub fn unused_dynamic_items(&self) -> usize {
+        self.dynamic_text_segments.unused_dynamic_items()
+            + self.dynamic_nodes.unused_dynamic_items()
+            + self.dynamic_attributes.unused_dynamic_items()
+    }
+
+    /// Hot reload a hot literal
+    pub fn hotreload_hot_literal(&self, hot_literal: &HotLiteral) -> Option<HotReloadLiteral> {
+        match hot_literal {
+            // If the literal is a formatted segment, map the segments to the new formatted segments
+            HotLiteral::Fmted(segments) => {
+                let new_segments = self.hot_reload_formatted_segments(segments)?;
+                Some(HotReloadLiteral::Fmted(new_segments))
+            }
+            // Otherwise just pass the literal through unchanged
+            HotLiteral::Bool(b) => Some(HotReloadLiteral::Bool(b.value())),
+            HotLiteral::Float(f) => Some(HotReloadLiteral::Float(f.base10_parse().ok()?)),
+            HotLiteral::Int(i) => Some(HotReloadLiteral::Int(i.base10_parse().ok()?)),
+        }
+    }
+
+    pub fn hot_reload_formatted_segments(
+        &self,
+        new: &HotReloadFormattedSegment,
+    ) -> Option<FmtedSegments> {
+        // Go through each dynamic segment and look for a match in the formatted segments pool.
+        // If we find a match, we can hot reload the segment otherwise we need to do a full rebuild
+        let mut segments = Vec::new();
+        for segment in &new.segments {
+            match segment {
+                // If it is a literal, we can always hot reload it. Just add it to the segments
+                Segment::Literal(value) => {
+                    segments.push(FmtSegment::Literal {
+                        value: Box::leak(value.clone().into_boxed_str()),
+                    });
+                } // If it is a dynamic segment, we need to check if it exists in the formatted segments pool
+                Segment::Formatted(formatted) => {
+                    let index = self.dynamic_text_segments.position(|s| s == formatted)?;
+
+                    segments.push(FmtSegment::Dynamic { id: index });
+                }
+            }
+        }
+
+        Some(FmtedSegments::new(segments))
+    }
+}

+ 12 - 4
packages/rsx/src/hot_reload/mod.rs

@@ -1,9 +1,17 @@
 #[cfg(feature = "hot_reload")]
-mod hot_reload_diff;
+mod collect;
 #[cfg(feature = "hot_reload")]
-pub use hot_reload_diff::*;
+pub use collect::*;
 
 #[cfg(feature = "hot_reload_traits")]
-mod hot_reloading_context;
+mod context;
 #[cfg(feature = "hot_reload_traits")]
-pub use hot_reloading_context::*;
+pub use context::*;
+
+#[cfg(feature = "hot_reload")]
+mod diff;
+#[cfg(feature = "hot_reload")]
+pub use diff::*;
+
+#[cfg(feature = "hot_reload")]
+mod last_build_state;

+ 0 - 429
packages/rsx/src/hotreload.rs

@@ -1,429 +0,0 @@
-#![cfg(feature = "hot_reload")]
-
-//! This module contains hotreloading logic for rsx.
-//!
-//! There's a few details that I wish we could've gotten right but we can revisit later:
-//!
-//! - Empty rsx! blocks are written as `None` - it would be nice to be able to hot reload them
-//!
-//! - The byte index of the template is not the same as the byte index of the original template
-//!   this forces us to make up IDs on the fly. We should just find an ID naming scheme, but that
-//!   struggles when you have nested rsx! calls since file:line:col is the same for all expanded rsx!
-//!
-//! - There's lots of linear scans
-//!
-//! - Expanding an if chain is not possible - only its contents can be hot reloaded
-//!
-//! - Components that don't start with children can't be hotreloaded - IE going from `Comp {}` to `Comp { "foo" }`
-//!   is not possible. We could in theory allow this by seeding all Components with a `children` field.
-//!
-//! - Cross-templates hot reloading is not possible - multiple templates don't share the dynamic nodes.
-//!   This would require changes in core to work, I imagine.
-//!
-//! Future work
-//!
-//! - We've proven that binary patching is feasible but has a longer path to stabilization for all platforms.
-//!   Binary patching is pretty quick, actually, and *might* remove the need to literal hotreloading.
-//!   However, you could imagine a scenario where literal hotreloading would be useful without the
-//!   compiler in the loop. Ideally we can slash most of this code once patching is stable.
-//!
-//! - We could also allow adding arbitrary nodes/attributes at runtime. The template system doesn't
-//!   quite support that, unfortunately, since the number of dynamic nodes and attributes is baked into
-//!   the template, but if that changed we'd be okay.
-
-use crate::{innerlude::*, scoring::score_dynamic_node};
-use crate::{scoring::score_attribute, HotReloadingContext};
-use dioxus_core::{internal::HotReloadLiteral, Template};
-use std::collections::HashMap;
-
-/// The mapping of a node relative to the root of its containing template
-///
-/// IE [0, 1] would be the location of the h3 node in this template:
-/// ```rust, ignore
-/// rsx! {
-///     div {
-///         h1 { "title" }
-///         h3 { class: "{class}", "Hi" }
-///     }
-/// }
-/// ```
-type NodePath = Vec<u8>;
-
-/// The mapping of an attribute relative to the root of its containing template
-/// Order doesn't matter for attributes, you can render them in any order on a given node.a
-///
-/// IE [0, 1] would be the location of the `class` attribute on this template:
-/// ```rust, ignore
-/// rsx! {
-///     div {
-///         h1 { "title" }
-///         h3 { class: "{class}", "Hi" }
-///     }
-/// }
-/// ```
-type AttributePath = Vec<u8>;
-
-/// A result of hot reloading
-///
-/// This contains information about what has changed so the hotreloader can apply the right changes
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[non_exhaustive]
-#[derive(Debug, PartialEq, Clone)]
-pub struct HotReloadedTemplate {
-    /// List of inner templates that changed (nested blocks like for/if/component bodies)
-    pub templates: Vec<Template>,
-
-    /// Previously changed lits that we're going to use to invalidate the old literals
-    pub prev_lits: HashMap<String, HotReloadLiteral>,
-
-    /// A map of Signal IDs to the new literals
-    /// Eventually we'll want to move this to a more efficient data structure to have one signal per rsx! call
-    pub changed_lits: HashMap<String, HotReloadLiteral>,
-
-    // The location of the original call
-    // This should be in the form of `file:line:col:0` - 0 since this will be the base template
-    pub location: &'static str,
-}
-
-impl HotReloadedTemplate {
-    /// Calculate the hotreload diff between two callbodies
-    pub fn new<Ctx: HotReloadingContext>(
-        old: &CallBody,
-        new: &CallBody,
-        location: &'static str,
-        old_lits: HashMap<String, HotReloadLiteral>,
-    ) -> Option<Self> {
-        let mut s = Self {
-            templates: Default::default(),
-            changed_lits: Default::default(),
-            prev_lits: old_lits,
-            location,
-        };
-
-        s.hotreload_body::<Ctx>(&old.body, &new.body)?;
-
-        Some(s)
-    }
-
-    /// Walk the dynamic contexts and do our best to find hotreloadable changes between the two
-    /// sets of dynamic nodes/attributes. If there's a change we can't hotreload, we'll return None
-    ///
-    /// Otherwise, we pump out the list of templates that need to be updated. The templates will be
-    /// re-ordered such that the node paths will be adjusted to match the new template for every
-    /// existing dynamic node.
-    ///
-    /// ```ignore
-    /// old:
-    ///     [[0], [1], [2]]
-    ///     rsx! {
-    ///         "{one}"
-    ///         "{two}"
-    ///         "{three}"
-    ///     }
-    ///
-    /// new:
-    ///     [[0], [2], [1, 1]]
-    ///     rsx! {
-    ///        "{one}"
-    ///         div { "{three}" }
-    ///         "{two}"
-    ///    }
-    /// ```
-    ///
-    /// Generally we can't hotreload a node if:
-    /// - We add a truly dynaamic node (except maybe text nodes - but even then.. only if we've seen them before)
-    ///
-    /// If a dynamic node is removed, we don't necessarily need to kill hotreload - just unmounting it should be enough
-    /// If the dynamic node is re-added, we want to be able to find it again.
-    ///
-    /// This encourages the hotreloader to hot onto DynamicContexts directly instead of the CallBody since
-    /// you can preserve more information about the nodes as they've changed over time.
-    pub fn hotreload_body<Ctx: HotReloadingContext>(
-        &mut self,
-        old: &TemplateBody,
-        new: &TemplateBody,
-    ) -> Option<()> {
-        // Quickly run through dynamic attributes first attempting to invalidate them
-        // Move over old IDs onto the new template
-        let new_attribute_paths = self.hotreload_attributes(old, new)?;
-
-        // Now we can run through the dynamic nodes and see if we can hot reload them
-        // Move over old IDs onto the new template
-        let new_node_paths = self.hotreload_dynamic_nodes::<Ctx>(old, new)?;
-
-        // Now render the new template out. We've proven that it's a close enough match to the old template
-        //
-        // The paths will be different but the dynamic indexes will be the same
-        let template = new.to_template_with_custom_paths::<Ctx>(
-            intern(self.make_location(old.template_idx.get())),
-            new_node_paths,
-            new_attribute_paths,
-        );
-
-        self.templates.push(template);
-
-        Some(())
-    }
-
-    /// Take two dynamic contexts and return a mapping of dynamic attributes from the original to the new.
-    ///
-    /// IE if we shuffle attributes around we should be able to hot reload them.
-    /// Same thing with dropping dynamic attributes.
-    ///
-    /// Does not apply with moving the dynamic contents from one attribute to another.
-    ///
-    /// ```rust, ignore
-    /// rsx! {
-    ///     div { id: "{id}", class: "{class}", "Hi" }
-    /// }
-    ///
-    /// rsx! {
-    ///     div { class: "{class}", id: "{id}", "Hi" }
-    /// }
-    /// ```
-    fn hotreload_attributes(
-        &mut self,
-        old: &TemplateBody,
-        new: &TemplateBody,
-    ) -> Option<Vec<AttributePath>> {
-        // Build a stack of old attributes so we can pop them off as we find matches in the new attributes
-        //
-        // Note that we might have duplicate attributes! We use a stack just to make sure we don't lose them
-        // Also note that we use a vec + remove, but the idea is that in most cases we're removing from the end
-        // which is an O(1) operation. We could use a linked list or a queue, but I don't want any
-        // more complexity than necessary here since this can complex.
-        let mut old_attrs = PopVec::new(old.dynamic_attributes());
-
-        // Now we can run through the dynamic nodes and see if we can hot reload them
-        // Here we create the new attribute paths for the final template - we'll fill them in as we find matches
-        let mut attr_paths = vec![vec![]; old.attr_paths.len()];
-
-        // Note that we walk the new attributes - we can remove segments from formatted text so
-        // all `new` is a subset of `old`.
-        for new_attr in new.dynamic_attributes() {
-            // We're going to score the attributes based on their names and values
-            // This ensures that we can handle the majority of cases where the attributes are shuffled around
-            // or their contents have been stripped down
-            //
-            // A higher score is better - 0 is a mismatch, usize::MAX is a perfect match
-            // As we find matches, the complexity of the search should reduce, making this quadratic
-            // a little less painful
-            let (old_idx, score) =
-                old_attrs.highest_score(move |old_attr| score_attribute(old_attr, new_attr))?;
-
-            // Remove it from the stack so we don't match it again
-            let old_attr = old_attrs.remove(old_idx).unwrap();
-
-            // This old node will now need to take on the new path
-            attr_paths[old_attr.dyn_idx.get()] = new.attr_paths[new_attr.dyn_idx.get()].clone().0;
-
-            // Now move over the idx of the old to the new
-            //
-            // We're going to reuse the new CallBody to render the new template, so we have to make sure
-            // stuff like IDs are ported over properly
-            //
-            // it's a little dumb to modify the new one in place, but it us avoid a lot of complexity
-            // we should change the semantics of these methods to take the new one mutably, making it
-            // clear that we're going to modify it in place and use it render
-            new_attr.dyn_idx.set(old_attr.dyn_idx.get());
-
-            // While we're here, if it's a literal and not a perfect score, it's a mismatch and we need to
-            // hotreload the literal
-            self.hotreload_attribute(old_attr, new_attr, score)?;
-        }
-
-        Some(attr_paths)
-    }
-
-    fn hotreload_dynamic_nodes<Ctx: HotReloadingContext>(
-        &mut self,
-        old: &TemplateBody,
-        new: &TemplateBody,
-    ) -> Option<Vec<NodePath>> {
-        use BodyNode::*;
-        let mut old_nodes = PopVec::new(old.dynamic_nodes());
-
-        let mut node_paths = vec![vec![]; old.node_paths.len()];
-
-        for new_node in new.dynamic_nodes() {
-            // Find the best match for the new node - this is done by comparing the dynamic contents of the various nodes to
-            // find the best fit.
-            //
-            // We do this since two components/textnodes/attributes *might* be similar in terms of dynamic contents
-            // but not be the same node.
-            let (old_idx, score) =
-                old_nodes.highest_score(move |old_node| score_dynamic_node(old_node, new_node))?;
-
-            // Remove it from the stack so we don't match it again - this is O(1)
-            let old_node = old_nodes.remove(old_idx)?;
-
-            // This old node will now need to take on the new path in the new template
-            node_paths[old_node.get_dyn_idx()].clone_from(&new.node_paths[new_node.get_dyn_idx()]);
-
-            // But we also need to make sure the new node is taking on the old node's ID
-            new_node.set_dyn_idx(old_node.get_dyn_idx());
-
-            // Make sure we descend into the children, and then record any changed literals
-            match (old_node, new_node) {
-                // If the contents of the text changed, then we need to hotreload the text node
-                (Text(a), Text(b)) if score != usize::MAX => {
-                    self.hotreload_text_node(a, b)?;
-                }
-
-                // We want to attempt to  hotreload the component literals and the children
-                (Component(a), Component(b)) => {
-                    self.hotreload_component_fields(a, b)?;
-                    self.hotreload_body::<Ctx>(&a.children, &b.children)?;
-                }
-
-                // We don't reload the exprs or condition - just the bodies
-                (ForLoop(a), ForLoop(b)) => {
-                    self.hotreload_body::<Ctx>(&a.body, &b.body)?;
-                }
-
-                // Ensure the if chains are the same and then hotreload the bodies
-                // We don't handle new chains or "elses" just yet - but feasibly we could allow
-                // for an `else` chain to be added/removed.
-                //
-                // Our ifchain parser would need to be better to support this.
-                (IfChain(a), IfChain(b)) => {
-                    self.hotreload_ifchain::<Ctx>(a, b)?;
-                }
-
-                // Just assert we never get these cases - attributes are handled separately
-                (Element(_), Element(_)) => unreachable!("Elements are not dynamic nodes"),
-
-                _ => {}
-            }
-        }
-
-        Some(node_paths)
-    }
-
-    fn hotreload_text_node(&mut self, a: &TextNode, b: &TextNode) -> Option<()> {
-        let idx = a.hr_idx.get();
-        let location = self.make_location(idx);
-        let segments = IfmtInput::fmt_segments(&a.input, &b.input)?;
-        self.changed_lits
-            .insert(location.to_string(), HotReloadLiteral::Fmted(segments));
-
-        Some(())
-    }
-
-    fn hotreload_component_fields(&mut self, a: &Component, b: &Component) -> Option<()> {
-        // make sure both are the same length
-        if a.fields.len() != b.fields.len() {
-            return None;
-        }
-
-        let mut left_fields = a.fields.iter().collect::<Vec<_>>();
-        left_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
-
-        let mut right_fields = b.fields.iter().collect::<Vec<_>>();
-        right_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
-
-        // Walk the attributes looking for literals
-        // Those will have plumbing in the hotreloading code
-        // All others just get diffed via tokensa
-        for (old_attr, new_attr) in left_fields.iter().zip(right_fields.iter()) {
-            self.hotreload_attribute(old_attr, new_attr, score_attribute(old_attr, new_attr))?;
-        }
-
-        Some(())
-    }
-
-    fn hotreload_attribute(
-        &mut self,
-        old_attr: &Attribute,
-        new_attr: &Attribute,
-        score: usize,
-    ) -> Option<()> {
-        // If the score is 0, the name didn't match or the values didn't match
-        // A score of usize::MAX means the attributes are the same
-        if score == 0 {
-            return None;
-        }
-
-        // If it's a perfect match, we don't need to do anything special
-        // ... well actually if it's a lit we need to invalidate the old lit
-        // this is because a lit going from true -> false -> true doesn't count as a change from the diffing perspective
-        if score == usize::MAX {
-            // if score == usize::MAX && new_attr.as_lit().is_none() {
-            return Some(());
-        }
-
-        // Prep the new literal
-        let location = self.make_location(old_attr.as_lit().unwrap().hr_idx.get());
-
-        // If we have a perfect match and no old lit exists, then this didn't change
-        if score == usize::MAX && self.prev_lits.remove(&location).is_none() {
-            return Some(());
-        }
-
-        let out = match &new_attr.as_lit().unwrap().value {
-            HotLiteralType::Float(f) => HotReloadLiteral::Float(f.base10_parse().unwrap()),
-            HotLiteralType::Int(f) => HotReloadLiteral::Int(f.base10_parse().unwrap()),
-            HotLiteralType::Bool(f) => HotReloadLiteral::Bool(f.value),
-            HotLiteralType::Fmted(new) => HotReloadLiteral::Fmted(
-                IfmtInput::fmt_segments(old_attr.ifmt().unwrap(), new)
-                    .expect("Fmt segments to generate"),
-            ),
-        };
-
-        self.changed_lits.insert(location, out);
-
-        Some(())
-    }
-
-    fn make_location(&self, idx: usize) -> String {
-        format!("{}:{}", self.location.trim_end_matches(":0"), idx)
-    }
-
-    /// Hot reload an if chain
-    fn hotreload_ifchain<Ctx: HotReloadingContext>(
-        &mut self,
-        a: &IfChain,
-        b: &IfChain,
-    ) -> Option<bool> {
-        let matches = a.cond == b.cond;
-
-        if matches {
-            let (mut elif_a, mut elif_b) = (Some(a), Some(b));
-
-            loop {
-                // No point in continuing if we've hit the end of the chain
-                if elif_a.is_none() && elif_b.is_none() {
-                    break;
-                }
-
-                // We assume both exist branches exist
-                let (a, b) = (elif_a.take()?, elif_b.take()?);
-
-                // Write the `then` branch
-                self.hotreload_body::<Ctx>(&a.then_branch, &b.then_branch)?;
-
-                // If there's an elseif branch, we set that as the next branch
-                // Otherwise we continue to the else branch - which we assume both branches have
-                if let (Some(left), Some(right)) =
-                    (a.else_if_branch.as_ref(), b.else_if_branch.as_ref())
-                {
-                    elif_a = Some(left.as_ref());
-                    elif_b = Some(right.as_ref());
-                    continue;
-                }
-
-                // No else branches, that's fine
-                if a.else_branch.is_none() && b.else_branch.is_none() {
-                    break;
-                }
-
-                // Write out the else branch and then we're done
-                let (left, right) = (a.else_branch.as_ref()?, b.else_branch.as_ref()?);
-                self.hotreload_body::<Ctx>(left, right)?;
-                break;
-            }
-        }
-
-        Some(matches)
-    }
-}

+ 0 - 10
packages/rsx/src/ifchain.rs

@@ -1,6 +1,3 @@
-#[cfg(feature = "hot_reload")]
-use dioxus_core::TemplateNode;
-
 use crate::location::DynIdx;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::quote;
@@ -35,13 +32,6 @@ impl IfChain {
             f(else_branch);
         }
     }
-
-    #[cfg(feature = "hot_reload")]
-    pub fn to_template_node(&self) -> TemplateNode {
-        TemplateNode::Dynamic {
-            id: self.dyn_idx.get(),
-        }
-    }
 }
 
 impl Parse for IfChain {

+ 8 - 71
packages/rsx/src/ifmt.rs

@@ -1,6 +1,3 @@
-#[cfg(feature = "hot_reload")]
-use dioxus_core::internal::{FmtSegment, FmtedSegments};
-
 use proc_macro2::{Span, TokenStream};
 use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
 use std::{collections::HashMap, str::FromStr};
@@ -94,64 +91,6 @@ impl IfmtInput {
         map
     }
 
-    #[cfg(feature = "hot_reload")]
-    pub fn fmt_segments(old: &Self, new: &Self) -> Option<FmtedSegments> {
-        use crate::intern;
-
-        // Make sure all the dynamic segments of b show up in a
-        for segment in new.segments.iter() {
-            if segment.is_formatted() && !old.segments.contains(segment) {
-                return None;
-            }
-        }
-
-        // Collect all the formatted segments from the original
-        let mut out = vec![];
-
-        // the original list of formatted segments
-        let mut fmted = old
-            .segments
-            .iter()
-            .flat_map(|f| match f {
-                crate::Segment::Literal(_) => None,
-                crate::Segment::Formatted(f) => Some(f),
-            })
-            .cloned()
-            .map(Some)
-            .collect::<Vec<_>>();
-
-        for segment in new.segments.iter() {
-            match segment {
-                crate::Segment::Literal(lit) => {
-                    // create a &'static str by leaking the string
-                    let lit = intern(lit.clone().into_boxed_str());
-                    out.push(FmtSegment::Literal { value: lit });
-                }
-                crate::Segment::Formatted(fmt) => {
-                    // Find the formatted segment in the original
-                    // Set it to None when we find it so we don't re-render it on accident
-                    let idx = fmted
-                        .iter_mut()
-                        .position(|_s| {
-                            if let Some(s) = _s {
-                                if s == fmt {
-                                    *_s = None;
-                                    return true;
-                                }
-                            }
-
-                            false
-                        })
-                        .unwrap();
-
-                    out.push(FmtSegment::Dynamic { id: idx });
-                }
-            }
-        }
-
-        Some(FmtedSegments::new(out))
-    }
-
     fn is_simple_expr(&self) -> bool {
         self.segments.iter().all(|seg| match seg {
             Segment::Literal(_) => true,
@@ -272,6 +211,11 @@ impl IfmtInput {
 
 impl ToTokens for IfmtInput {
     fn to_tokens(&self, tokens: &mut TokenStream) {
+        // If the input is a string literal, we can just return it
+        if let Some(static_str) = self.to_static() {
+            return static_str.to_tokens(tokens);
+        }
+
         // Try to turn it into a single _.to_string() call
         if !cfg!(debug_assertions) {
             if let Some(single_dynamic) = self.try_to_string() {
@@ -284,7 +228,7 @@ impl ToTokens for IfmtInput {
         if self.is_simple_expr() {
             let raw = &self.source;
             tokens.extend(quote! {
-                ::std::format_args!(#raw)
+                ::std::format!(#raw)
             });
             return;
         }
@@ -323,7 +267,7 @@ impl ToTokens for IfmtInput {
 
         quote_spanned! {
             span =>
-            ::std::format_args!(
+            ::std::format!(
                 #format_literal
                 #(, #positional_args)*
             )
@@ -359,7 +303,7 @@ impl ToTokens for FormattedSegment {
         let (fmt, seg) = (&self.format_args, &self.segment);
         let fmt = format!("{{0:{fmt}}}");
         tokens.append_all(quote! {
-            format_args!(#fmt, #seg)
+            format!(#fmt, #seg)
         });
     }
 }
@@ -470,11 +414,4 @@ mod tests {
         println!("{}", input.to_string_with_quotes());
         assert!(input.is_static());
     }
-
-    #[test]
-    fn fmt_segments() {
-        let left = syn::parse2::<IfmtInput>(quote! { "thing {abc}" }).unwrap();
-        let right = syn::parse2::<IfmtInput>(quote! { "thing" }).unwrap();
-        let _segments = IfmtInput::fmt_segments(&left, &right).unwrap();
-    }
 }

+ 1 - 8
packages/rsx/src/lib.rs

@@ -49,6 +49,7 @@
 //! dioxus_elements::elements::di
 //! ```
 
+mod assign_dyn_ids;
 mod attribute;
 mod component;
 mod element;
@@ -63,13 +64,10 @@ mod text_node;
 
 mod diagnostics;
 mod expr_node;
-pub mod hotreload;
 mod ifmt;
 mod literal;
 mod location;
 mod partial_closure;
-mod reload_stack;
-mod scoring;
 mod util;
 
 // Re-export the namespaces into each other
@@ -105,16 +103,11 @@ pub(crate) mod innerlude {
     pub use crate::node::*;
     pub use crate::raw_expr::*;
     pub use crate::rsx_block::*;
-    pub use crate::rsx_call::*;
     pub use crate::template_body::*;
     pub use crate::text_node::*;
 
     pub use crate::diagnostics::*;
     pub use crate::ifmt::*;
     pub use crate::literal::*;
-    pub use crate::reload_stack::*;
     pub use crate::util::*;
-
-    #[cfg(feature = "hot_reload")]
-    pub use crate::hotreload::*;
 }

+ 112 - 144
packages/rsx/src/literal.rs

@@ -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 }
-            }
-
-            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),* ], )
-                }
+        match &self {
+            HotLiteral::Fmted(f) => {
+                f.formatted_input.to_tokens(out);
             }
-            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());

+ 3 - 5
packages/rsx/src/node.rs

@@ -1,6 +1,3 @@
-#[cfg(feature = "hot_reload")]
-use dioxus_core::TemplateNode;
-
 use crate::innerlude::*;
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use quote::ToTokens;
@@ -133,7 +130,8 @@ impl BodyNode {
     ///
     /// dioxus-core uses this to understand templates at compiletime
     #[cfg(feature = "hot_reload")]
-    pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> TemplateNode {
+    pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> dioxus_core::TemplateNode {
+        use dioxus_core::TemplateNode;
         match self {
             BodyNode::Element(el) => {
                 let rust_name = el.name.to_string();
@@ -153,7 +151,7 @@ impl BodyNode {
                     attrs: intern(
                         el.merged_attributes
                             .iter()
-                            .map(|attr| attr.to_template_attribute::<Ctx>(&rust_name))
+                            .map(|attr| attr.to_template_attribute::<Ctx>())
                             .collect::<Vec<_>>(),
                     ),
                 }

+ 0 - 113
packages/rsx/src/reload_stack.rs

@@ -1,113 +0,0 @@
-/// An array that's optimized for finding and removing elements that match a predicate.
-///
-/// Currently will do a linear search for the first element that matches the predicate.
-/// Uses a scan_start pointer to optimize the search such that future searches start from left-most
-/// non-None item, making it O(1) on average for sorted input.
-///
-/// The motivating factor here is that hashes are expensive and actually quite hard to maintain for
-/// callbody. Hashing would imply a number of nested invariants that are hard to maintain.
-///
-/// Deriving hash will start to slurp up private fields which is not what we want, so the comparison
-/// function is moved here to the reloadstack interface.
-pub struct PopVec<T> {
-    stack: Box<[Option<T>]>,
-    scan_start: usize,
-}
-
-impl<T> PopVec<T> {
-    pub fn new(f: impl Iterator<Item = T>) -> Self {
-        let stack = f.map(Some).collect();
-        Self {
-            stack,
-            scan_start: 0,
-        }
-    }
-
-    pub fn remove(&mut self, idx: usize) -> Option<T> {
-        let item = self.stack.get_mut(idx)?.take();
-
-        // move the scan_start pointer to the right-most non-none element
-        for i in self.scan_start..=idx {
-            if self.stack[i].is_some() {
-                break;
-            }
-            self.scan_start = i + 1;
-        }
-
-        item
-    }
-
-    pub fn pop_where(&mut self, f: impl Fn(&T) -> bool) -> Option<T> {
-        let idx = self
-            .stack
-            .iter()
-            .position(|x| if let Some(x) = x { f(x) } else { false })?;
-
-        self.remove(idx)
-    }
-
-    /// Returns the index and score of the highest scored element
-    ///
-    /// shortcircuits if the score is usize::MAX
-    /// returns None if the score was 0
-    pub fn highest_score(&self, score: impl Fn(&T) -> usize) -> Option<(usize, usize)> {
-        let mut highest_score = 0;
-        let mut best = None;
-
-        for (idx, x) in self.stack.iter().enumerate().skip(self.scan_start) {
-            if let Some(x) = x {
-                let scored = score(x);
-                if scored > highest_score {
-                    best = Some(idx);
-                    highest_score = scored;
-                }
-
-                if highest_score == usize::MAX {
-                    break;
-                }
-            }
-        }
-
-        if highest_score == 0 {
-            return None;
-        }
-
-        best.map(|idx| (idx, highest_score))
-    }
-
-    pub fn is_empty(&self) -> bool {
-        // next free is 0 when stack len = 1
-        self.scan_start == self.stack.len() - 1
-    }
-}
-
-#[test]
-fn searches_and_works() {
-    let mut stack = PopVec::new(vec![1, 2, 3, 4, 5].into_iter());
-
-    assert_eq!(stack.pop_where(|x| *x == 3), Some(3));
-    assert_eq!(stack.pop_where(|x| *x == 1), Some(1));
-    assert_eq!(stack.pop_where(|x| *x == 5), Some(5));
-    assert_eq!(stack.pop_where(|x| *x == 2), Some(2));
-    assert_eq!(stack.pop_where(|x| *x == 4), Some(4));
-    assert_eq!(stack.pop_where(|x| *x == 4), None);
-
-    assert!(stack.is_empty());
-}
-
-#[test]
-fn free_optimization_works() {
-    let mut stack = PopVec::new(vec![0, 1, 2, 3, 4, 5].into_iter());
-
-    _ = stack.remove(0);
-    assert_eq!(stack.scan_start, 1);
-
-    _ = stack.remove(1);
-    assert_eq!(stack.scan_start, 2);
-
-    _ = stack.remove(4);
-    assert_eq!(stack.scan_start, 2);
-
-    _ = stack.remove(2);
-    assert_eq!(stack.scan_start, 3);
-}

+ 5 - 33
packages/rsx/src/rsx_call.rs

@@ -4,7 +4,7 @@
 //! Currently the additional tooling doesn't do much.
 
 use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens};
+use quote::ToTokens;
 use std::{cell::Cell, fmt::Debug};
 use syn::{
     parse::{Parse, ParseStream},
@@ -25,7 +25,6 @@ use crate::{BodyNode, TemplateBody};
 #[derive(Debug, Clone)]
 pub struct CallBody {
     pub body: TemplateBody,
-    pub ifmt_idx: Cell<usize>,
     pub template_idx: Cell<usize>,
 }
 
@@ -38,10 +37,7 @@ impl Parse for CallBody {
 
 impl ToTokens for CallBody {
     fn to_tokens(&self, out: &mut TokenStream2) {
-        match self.body.is_empty() {
-            true => quote! { dioxus_core::VNode::empty() }.to_tokens(out),
-            false => self.body.to_tokens(out),
-        }
+        self.body.to_tokens(out)
     }
 }
 
@@ -52,7 +48,6 @@ impl CallBody {
     pub fn new(body: TemplateBody) -> Self {
         let body = CallBody {
             body,
-            ifmt_idx: Cell::new(0),
             template_idx: Cell::new(0),
         };
 
@@ -91,36 +86,17 @@ impl CallBody {
     ///
     /// Lots of wiring!
     ///
-    /// However, here, we only need to wire up ifmt and template IDs since TemplateBody will handle the rest.
+    /// However, here, we only need to wire up template IDs since TemplateBody will handle the rest.
     ///
     /// This is better though since we can save the relevant data on the structures themselves.
     fn cascade_hotreload_info(&self, nodes: &[BodyNode]) {
         for node in nodes.iter() {
             match node {
-                BodyNode::RawExpr(_) => { /* one day maybe provide hr here?*/ }
-
-                BodyNode::Text(text) => {
-                    // one day we could also provide HR here to allow dynamic parts on the fly
-                    if !text.is_static() {
-                        text.hr_idx.set(self.next_ifmt_idx());
-                    }
-                }
-
                 BodyNode::Element(el) => {
-                    // Walk the attributes looking for hotreload opportunities
-                    for attr in &el.merged_attributes {
-                        attr.with_literal(|lit| lit.hr_idx.set(self.next_ifmt_idx()));
-                    }
-
                     self.cascade_hotreload_info(&el.children);
                 }
 
                 BodyNode::Component(comp) => {
-                    // walk the props looking for hotreload opportunities
-                    for prop in comp.fields.iter() {
-                        prop.with_literal(|lit| lit.hr_idx.set(self.next_ifmt_idx()));
-                    }
-
                     comp.children.template_idx.set(self.next_template_idx());
                     self.cascade_hotreload_info(&comp.children.roots);
                 }
@@ -134,16 +110,12 @@ impl CallBody {
                     body.template_idx.set(self.next_template_idx());
                     self.cascade_hotreload_info(&body.roots)
                 }),
+
+                _ => {}
             }
         }
     }
 
-    fn next_ifmt_idx(&self) -> usize {
-        let idx = self.ifmt_idx.get();
-        self.ifmt_idx.set(idx + 1);
-        idx
-    }
-
     fn next_template_idx(&self) -> usize {
         let idx = self.template_idx.get();
         self.template_idx.set(idx + 1);

+ 0 - 330
packages/rsx/src/scoring.rs

@@ -1,330 +0,0 @@
-#![cfg(feature = "hot_reload")]
-
-use crate::{Attribute, AttributeValue, BodyNode, HotLiteralType, IfAttributeValue, IfmtInput};
-
-/// Take two nodes and return their similarity score
-///
-/// This is not normalized or anything, so longer nodes will have higher scores
-pub fn score_dynamic_node(old_node: &BodyNode, new_node: &BodyNode) -> usize {
-    use BodyNode::*;
-
-    match (old_node, new_node) {
-        (Element(_), Element(_)) => unreachable!("Elements are not dynamic nodes"),
-
-        (Text(old), Text(new)) => {
-            // We shouldn't be seeing static text nodes here
-            assert!(!old.input.is_static() && !new.input.is_static());
-            score_ifmt(&old.input, &new.input)
-        }
-
-        (RawExpr(old), RawExpr(new)) if old == new => usize::MAX,
-
-        (Component(old), Component(new))
-            if old.name == new.name
-                && old.generics == new.generics
-                && old.fields.len() == new.fields.len() =>
-        {
-            let mut score = 1;
-
-            // todo: there might be a bug here where Idents and Strings will result in a match
-            let mut left_fields = old.fields.iter().collect::<Vec<_>>();
-            left_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
-
-            let mut right_fields = new.fields.iter().collect::<Vec<_>>();
-            right_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
-
-            // Walk the attributes and score each one - if there's a zero we return zero
-            // circuit if we there's an attribute mismatch that can't be hotreloaded
-            for (left, right) in left_fields.iter().zip(right_fields.iter()) {
-                let scored = match score_attribute(left, right) {
-                    usize::MAX => 3,
-                    0 => return 0,
-                    a if a == usize::MAX - 1 => 2,
-                    a => a,
-                };
-
-                score += scored;
-            }
-
-            score
-        }
-
-        (ForLoop(a), ForLoop(b)) if a.pat == b.pat && a.expr == b.expr => {
-            // The bodies don't necessarily need to be the same, but we should throw some simple heuristics at them to
-            // encourage proper selection. For now just double check the templates are roughly the same
-            1 + (a.body.roots.len() == b.body.roots.len()) as usize
-                + (a.body.node_paths.len() == b.body.node_paths.len()) as usize
-                + (a.body.attr_paths.len() == b.body.attr_paths.len()) as usize
-        }
-
-        (IfChain(a), IfChain(b)) if a.cond == b.cond => {
-            // The bodies don't necessarily need to be the same, but we should throw some simple heuristics at them to
-            // encourage proper selection. For now just double check the templates are roughly the same
-            1 + (a.then_branch.roots.len() == b.then_branch.roots.len()) as usize
-                + (a.then_branch.node_paths.len() == b.then_branch.node_paths.len()) as usize
-                + (a.then_branch.attr_paths.len() == b.then_branch.attr_paths.len()) as usize
-        }
-
-        _ => 0,
-    }
-}
-
-pub fn score_attribute(old_attr: &Attribute, new_attr: &Attribute) -> usize {
-    if old_attr.name != new_attr.name {
-        return 0;
-    }
-
-    score_attr_value(&old_attr.value, &new_attr.value)
-}
-
-fn score_attr_value(old_attr: &AttributeValue, new_attr: &AttributeValue) -> usize {
-    use AttributeValue::*;
-    use HotLiteralType::*;
-
-    match (&old_attr, &new_attr) {
-        // For literals, the value itself might change, but what's more important is the
-        // structure of the literal. If the structure is the same, we can hotreload it
-        // Ideally the value doesn't change, but we're hoping that our stack approach
-        // Will prevent spurious reloads
-        //
-        // todo: maybe it's a good idea to modify the original in place?
-        // todo: float to int is a little weird case that we can try to support better
-        //       right now going from float to int or vice versa will cause a full rebuild
-        //       which can get confusing. if we can figure out a way to hotreload this, that'd be great
-        (AttrLiteral(left), AttrLiteral(right)) => {
-            // We assign perfect matches for token reuse, to minimize churn on the renderer
-            match (&left.value, &right.value) {
-                // Quick shortcut if there's no change
-                (Fmted(old), Fmted(new)) if old == new => usize::MAX,
-
-                // We can remove formatted bits but we can't add them. The scoring here must
-                // realize that every bit of the new formatted segment must be in the old formatted segment
-                (Fmted(old), Fmted(new)) => score_ifmt(old, new),
-
-                (Float(a), Float(b)) if a == b => usize::MAX,
-                (Float(_), Float(_)) => 1,
-
-                (Int(a), Int(b)) if a == b => usize::MAX,
-                (Int(_), Int(_)) => 1,
-
-                (Bool(a), Bool(b)) if a == b => usize::MAX,
-                (Bool(_), Bool(_)) => 1,
-                _ => 0,
-            }
-        }
-
-        (
-            IfExpr(IfAttributeValue {
-                condition: cond_a,
-                then_value: value_a,
-                else_value: else_value_a,
-            }),
-            IfExpr(IfAttributeValue {
-                condition: cond_b,
-                then_value: value_b,
-                else_value: else_value_b,
-            }),
-        ) if cond_a == cond_b => {
-            // If the condition is the same, we can hotreload it
-            score_attr_value(value_a, value_b)
-                + match (else_value_a, else_value_b) {
-                    (Some(a), Some(b)) => score_attr_value(a, b),
-                    (None, None) => 0,
-                    _ => usize::MAX,
-                }
-        }
-
-        // todo: we should try and score recursively if we can - templates need to propagate up their
-        // scores. That would lead to a time complexity explosion but can be helpful in some cases.
-        //
-        // If it's expression-type things, we give a perfect score if they match completely
-        _ if old_attr == new_attr => usize::MAX,
-
-        // If it's not a match, we give it a score of 0
-        _ => 0,
-    }
-}
-
-pub fn score_ifmt(old: &IfmtInput, new: &IfmtInput) -> usize {
-    // If they're the same by source, return max
-    if old == new {
-        return usize::MAX;
-    }
-
-    // Default score to 1 - an ifmt with no dynamic segments still technically has a score of 1
-    // since it's not disqualified, but it's not a perfect match
-    let mut score = 1;
-    let mut l_freq_map = old.dynamic_seg_frequency_map();
-
-    // Pluck out the dynamic segments from the other input
-    for seg in new.dynamic_segments() {
-        let Some(ct) = l_freq_map.get_mut(seg) else {
-            return 0;
-        };
-
-        *ct -= 1;
-
-        if *ct == 0 {
-            l_freq_map.remove(seg);
-        }
-
-        score += 1;
-    }
-
-    // If there's nothing remaining - a perfect match - return max -1
-    // We compared the sources to start, so we know they're different in some way
-    if l_freq_map.is_empty() {
-        usize::MAX - 1
-    } else {
-        score
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::PopVec;
-
-    use super::*;
-    use quote::quote;
-    use syn::parse2;
-
-    #[test]
-    fn score_components() {
-        let a: BodyNode = parse2(quote! {
-            for x in 0..1 {
-                SomeComponent {
-                    count: 19999123,
-                    enabled: false,
-                    title: "pxasd-5 {x}",
-                    flot: 1233.5,
-                    height: 100,
-                    width: 500,
-                    color: "reasdssdasd {x}",
-                    handler: move |e| {
-                        println!("clickeasdd!");
-                    },
-                    "sick!! asasdsd!lasdasasdasddasdkasjdlkasjdlk!! {x}"
-                }
-            }
-        })
-        .unwrap();
-
-        let b: BodyNode = parse2(quote! {
-            for x in 0..1 {
-                SomeComponent {
-                    count: 19999123,
-                    enabled: false,
-                    title: "pxasd-5 {x}",
-                    flot: 1233.5,
-                    height: 100,
-                    width: 500,
-                    color: "reasdssdasd {x}",
-                    handler: move |e| {
-                        println!("clickeasdd!");
-                    },
-                    "sick!! asasdsd!lasdasasdaasdasdsddasdkasjdlkasjdlk!! {x}"
-                }
-            }
-        })
-        .unwrap();
-
-        let score = score_dynamic_node(&a, &b);
-        assert_eq!(score, 4);
-    }
-
-    #[test]
-    fn score_attributes() {
-        let left: Attribute = parse2(quote! { attr: 123 }).unwrap();
-        let right: Attribute = parse2(quote! { attr: 123 }).unwrap();
-        assert_eq!(score_attribute(&left, &right), usize::MAX);
-
-        let left: Attribute = parse2(quote! { attr: 123 }).unwrap();
-        let right: Attribute = parse2(quote! { attr: 456 }).unwrap();
-        assert_eq!(score_attribute(&left, &right), 1);
-
-        // almost a perfect match
-        let left: Attribute = parse2(quote! { class: if count > 3 { "blah {abc}" } }).unwrap();
-        let right: Attribute = parse2(quote! { class: if count > 3 { "other {abc}" } }).unwrap();
-        assert_eq!(score_attribute(&left, &right), usize::MAX - 1);
-    }
-
-    /// Ensure the scoring algorithm works
-    ///
-    /// - usize::MAX is return for perfect overlap
-    /// - 0 is returned when the right case has segments not found in the first
-    /// - a number for the other cases where there is some non-perfect overlap
-    #[test]
-    fn ifmt_scoring() {
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{abc}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 2);
-
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{abc} {def}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), usize::MAX);
-
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{abc} {ghi}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 0);
-
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{abc} {def} {ghi}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 0);
-
-        let left: IfmtInput = "{abc} {def} {ghi}".parse().unwrap();
-        let right: IfmtInput = "{abc} {def}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 3);
-
-        let left: IfmtInput = "{abc}".parse().unwrap();
-        let right: IfmtInput = "{abc} {def}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 0);
-
-        let left: IfmtInput = "{abc} {abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{abc} {def}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 3);
-
-        let left: IfmtInput = "{abc} {abc}".parse().unwrap();
-        let right: IfmtInput = "{abc} {abc}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), usize::MAX);
-
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "{hij}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 0);
-
-        let left: IfmtInput = "{abc}".parse().unwrap();
-        let right: IfmtInput = "thing {abc}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), usize::MAX - 1);
-
-        let left: IfmtInput = "thing {abc}".parse().unwrap();
-        let right: IfmtInput = "{abc}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), usize::MAX - 1);
-
-        let left: IfmtInput = "{abc} {def}".parse().unwrap();
-        let right: IfmtInput = "thing {abc}".parse().unwrap();
-        assert_eq!(score_ifmt(&left, &right), 2);
-    }
-
-    #[test]
-    fn stack_scoring() {
-        let stack: PopVec<IfmtInput> = PopVec::new(
-            vec![
-                "{abc} {def}".parse().unwrap(),
-                "{def}".parse().unwrap(),
-                "{hij}".parse().unwrap(),
-            ]
-            .into_iter(),
-        );
-
-        let tests = vec![
-            "thing {def}".parse().unwrap(),
-            "thing {abc}".parse().unwrap(),
-            "thing {hij}".parse().unwrap(),
-        ];
-
-        for item in tests {
-            let score = stack.highest_score(|f| score_ifmt(f, &item));
-
-            dbg!(item, score);
-        }
-    }
-}

+ 178 - 179
packages/rsx/src/template_body.rs

@@ -19,9 +19,11 @@
 //! - The IDs of dynamic nodes relative to the template they live in. This is somewhat easy to track
 //!   but needs to happen on a per-template basis.
 //!
-//! - The unique ID of a hotreloadable literal (like ifmt or integers or strings, etc). This ID is
-//!   unique to the Callbody, not necessarily the template it lives in. This is similar to the
-//!   template ID
+//! - The IDs of formatted strings in debug mode only. Any formatted segments like "{x:?}" get pulled out
+//!   into a pool so we can move them around during hot reloading on a per-template basis.
+//!
+//! - The IDs of component property literals in debug mode only. Any component property literals like
+//!   1234 get pulled into the pool so we can hot reload them with the context of the literal pool.
 //!
 //! We solve this by parsing the structure completely and then doing a second pass that fills in IDs
 //! by walking the structure.
@@ -29,25 +31,22 @@
 //! This means you can't query the ID of any node "in a vacuum" - these are assigned once - but at
 //! least they're stable enough for the purposes of hotreloading
 //!
-//! The plumbing for hotreloadable literals could be template relative... ie "file:line:col:template:idx"
-//! That would be ideal if we could determine the the idx only relative to the template
-//!
 //! ```rust, ignore
 //! rsx! {
 //!     div {
 //!         class: "hello",
-//!         id: "node-{node_id}",    <--- hotreloadable with ID 0
+//!         id: "node-{node_id}",    <--- {node_id} has the formatted segment id 0 in the literal pool
 //!         ..props,                 <--- spreads are not reloadable
 //!
-//!         "Hello, world!           <--- not tracked but reloadable since it's just a string
+//!         "Hello, world!           <--- not tracked but reloadable in the template since it's just a string
 //!
-//!         for item in 0..10 {      <--- both 0 and 10 are technically reloadable...
-//!             div { "cool-{item}" }     <--- the ifmt here is also reloadable
+//!         for item in 0..10 {      <--- both 0 and 10 are technically reloadable, but we don't hot reload them today...
+//!             div { "cool-{item}" }     <--- {item} has the formatted segment id 1 in the literal pool
 //!         }
 //!
 //!         Link {
-//!             to: "/home", <-- hotreloadable since its a component prop
-//!             class: "link {is_ready}", <-- hotreloadable since its a formatted string as a prop
+//!             to: "/home", <-- hotreloadable since its a component prop literal (with component literal id 0)
+//!             class: "link {is_ready}", <-- {is_ready} has the formatted segment id 2 in the literal pool and the property has the component literal id 1
 //!             "Home" <-- hotreloadable since its a component child (via template)
 //!         }
 //!     }
@@ -58,9 +57,8 @@ use self::location::DynIdx;
 use crate::innerlude::Attribute;
 use crate::*;
 use proc_macro2::TokenStream as TokenStream2;
-
-#[cfg(feature = "hot_reload")]
-use dioxus_core::prelude::Template;
+use proc_macro2_diagnostics::SpanDiagnosticExt;
+use syn::parse_quote;
 
 type NodePath = Vec<u8>;
 type AttributePath = Vec<u8>;
@@ -81,8 +79,8 @@ pub struct TemplateBody {
     pub template_idx: DynIdx,
     pub node_paths: Vec<NodePath>,
     pub attr_paths: Vec<(AttributePath, usize)>,
+    pub dynamic_text_segments: Vec<FormattedSegment>,
     pub diagnostics: Diagnostics,
-    current_path: Vec<u8>,
 }
 
 impl Parse for TemplateBody {
@@ -101,9 +99,19 @@ impl Parse for TemplateBody {
 /// This is because the parsing phase filled in all the additional metadata we need
 impl ToTokens for TemplateBody {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        // If there are no roots, this is an empty template, so just return None
-        if self.roots.is_empty() {
-            return tokens.append_all(quote! { dioxus_core::VNode::empty() });
+        // If the nodes are completely empty, insert a placeholder node
+        // Core expects at least one node in the template to make it easier to replace
+        if self.is_empty() {
+            // Create an empty template body with a placeholder and diagnostics + the template index from the original
+            let empty = Self::new(vec![BodyNode::RawExpr(parse_quote! {()})]);
+            let default = Self {
+                diagnostics: self.diagnostics.clone(),
+                template_idx: self.template_idx.clone(),
+                ..empty
+            };
+            // And then render the default template body
+            default.to_tokens(tokens);
+            return;
         }
 
         // If we have an implicit key, then we need to write its tokens
@@ -112,34 +120,7 @@ impl ToTokens for TemplateBody {
             None => quote! { None },
         };
 
-        let TemplateBody { roots, .. } = self;
-        let roots = roots.iter().map(|node| match node {
-            BodyNode::Element(el) => quote! { #el },
-            BodyNode::Text(text) if text.is_static() => {
-                let text = text.input.to_static().unwrap();
-                quote! { dioxus_core::TemplateNode::Text { text: #text } }
-            }
-            BodyNode::Text(text) => {
-                let id = text.dyn_idx.get();
-                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
-            }
-            BodyNode::ForLoop(floop) => {
-                let id = floop.dyn_idx.get();
-                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
-            }
-            BodyNode::RawExpr(exp) => {
-                let id = exp.dyn_idx.get();
-                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
-            }
-            BodyNode::Component(exp) => {
-                let id = exp.dyn_idx.get();
-                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
-            }
-            BodyNode::IfChain(exp) => {
-                let id = exp.dyn_idx.get();
-                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
-            }
-        });
+        let roots = self.quote_roots();
 
         // Print paths is easy - just print the paths
         let node_paths = self.node_paths.iter().map(|it| quote!(&[#(#it),*]));
@@ -147,50 +128,76 @@ impl ToTokens for TemplateBody {
 
         // For printing dynamic nodes, we rely on the ToTokens impl
         // Elements have a weird ToTokens - they actually are the entrypoint for Template creation
-        let dynamic_nodes = self.node_paths.iter().map(|path| {
-            let node = self.get_dyn_node(path);
-            quote::quote! { #node }
-        });
+        let dynamic_nodes: Vec<_> = self.dynamic_nodes().collect();
 
         // We could add a ToTokens for Attribute but since we use that for both components and elements
         // They actually need to be different, so we just localize that here
-        let dyn_attr_printer = self
-            .attr_paths
-            .iter()
-            .map(|(path, idx)| self.get_dyn_attr(path, *idx).rendered_as_dynamic_attr());
+        let dyn_attr_printer: Vec<_> = self
+            .dynamic_attributes()
+            .map(|attr| attr.rendered_as_dynamic_attr())
+            .collect();
+
+        let dynamic_text = self.dynamic_text_segments.iter();
 
         let index = self.template_idx.get();
 
         let diagnostics = &self.diagnostics;
-
-        tokens.append_all(quote! {
-            dioxus_core::Element::Ok({
+        let hot_reload_mapping = self.hot_reload_mapping(quote! { ___TEMPLATE_NAME });
+
+        let vnode = quote! {
+            #[doc(hidden)] // vscode please stop showing these in symbol search
+            const ___TEMPLATE_NAME: &str = {
+                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!(), ':', #index)
+            };
+            #[cfg(not(debug_assertions))]
+            {
                 #[doc(hidden)] // vscode please stop showing these in symbol search
                 static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
-                    name: {
-                        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!(), ':', #index)
-                    },
+                    name: ___TEMPLATE_NAME,
                     roots: &[ #( #roots ),* ],
                     node_paths: &[ #( #node_paths ),* ],
                     attr_paths: &[ #( #attr_paths ),* ],
                 };
 
-                #diagnostics
-
-                {
-                    // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
-                    #[allow(clippy::let_and_return)]
-                    let __vnodes = dioxus_core::VNode::new(
-                        #key_tokens,
-                        ___TEMPLATE,
-                        Box::new([ #( #dynamic_nodes),* ]),
-                        Box::new([ #( #dyn_attr_printer ),* ]),
+                // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
+                #[allow(clippy::let_and_return)]
+                let __vnodes = dioxus_core::VNode::new(
+                    #key_tokens,
+                    ___TEMPLATE,
+                    Box::new([ #( #dynamic_nodes ),* ]),
+                    Box::new([ #( #dyn_attr_printer ),* ]),
+                );
+                __vnodes
+            }
+            #[cfg(debug_assertions)]
+            {
+                // The key is important here - we're creating a new GlobalSignal each call to this
+                // But the key is what's keeping it stable
+                let __template = GlobalSignal::with_key(
+                    || #hot_reload_mapping,
+                    ___TEMPLATE_NAME
+                );
+
+                __template.maybe_with_rt(|__template_read| {
+                    let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new(
+                        vec![ #( #dynamic_text.to_string() ),* ],
                     );
-                    __vnodes
-                }
+                    let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new(
+                        vec![ #( #dynamic_nodes ),* ],
+                        vec![ #( #dyn_attr_printer ),* ],
+                        __dynamic_literal_pool
+                    );
+                    __dynamic_value_pool.render_with(__template_read)
+                })
+            }
+        };
+        tokens.append_all(quote! {
+            dioxus_core::Element::Ok({
+                #diagnostics
 
+                #vnode
             })
         });
     }
@@ -207,12 +214,13 @@ impl TemplateBody {
             template_idx: DynIdx::default(),
             node_paths: Vec::new(),
             attr_paths: Vec::new(),
-            current_path: Vec::new(),
+            dynamic_text_segments: Vec::new(),
             diagnostics: Diagnostics::new(),
         };
 
         // Assign paths to all nodes in the template
         body.assign_paths_inner(&nodes);
+        body.validate_key();
 
         // And then save the roots
         body.roots = nodes;
@@ -220,118 +228,42 @@ impl TemplateBody {
         body
     }
 
-    /// Cascade down path information into the children of this template
-    ///
-    /// This provides the necessary path and index information for the children of this template
-    /// so that they can render out their dynamic nodes correctly. Also does plumbing for things like
-    /// hotreloaded literals which need to be tracked on a per-template basis.
-    ///
-    /// This can only operate with knowledge of this template, not the surrounding callbody. Things like
-    /// wiring of ifmt literals need to be done at the callbody level since those final IDs need to
-    /// be unique to the entire app.
-    fn assign_paths_inner(&mut self, nodes: &[BodyNode]) {
-        for (idx, node) in nodes.iter().enumerate() {
-            self.current_path.push(idx as u8);
-            match node {
-                // Just descend into elements - they're not dynamic
-                BodyNode::Element(el) => {
-                    for (idx, attr) in el.merged_attributes.iter().enumerate() {
-                        if !attr.is_static_str_literal() {
-                            attr.dyn_idx.set(self.attr_paths.len());
-                            self.attr_paths.push((self.current_path.clone(), idx));
-                        }
-                    }
-
-                    self.assign_paths_inner(&el.children)
-                }
-
-                // Text nodes are dynamic if they contain dynamic segments
-                BodyNode::Text(txt) => {
-                    if !txt.is_static() {
-                        self.assign_path_to(node);
-                    }
-                }
-
-                // Raw exprs are always dynamic
-                BodyNode::RawExpr(_)
-                | BodyNode::ForLoop(_)
-                | BodyNode::Component(_)
-                | BodyNode::IfChain(_) => self.assign_path_to(node),
-            };
-            self.current_path.pop();
-        }
-    }
-
-    /// Assign a path to a node and give it its dynamic index
-    /// This simplifies the ToTokens implementation for the macro to be a little less centralized
-    fn assign_path_to(&mut self, node: &BodyNode) {
-        // Assign the TemplateNode::Dynamic index to the node
-        node.set_dyn_idx(self.node_paths.len());
-
-        // And then save the current path as the corresponding path
-        self.node_paths.push(self.current_path.clone());
-    }
-
-    /// Create a new template from this TemplateBody
-    ///
-    /// Note that this will leak memory! We explicitly call `leak` on the vecs to match the format of
-    /// the `Template` struct.
-    #[cfg(feature = "hot_reload")]
-    pub fn to_template<Ctx: HotReloadingContext>(&self) -> Template {
-        self.to_template_with_custom_paths::<Ctx>(
-            "placeholder",
-            self.node_paths.clone(),
-            self.attr_paths.clone().into_iter().map(|v| v.0).collect(),
-        )
-    }
-
-    #[cfg(feature = "hot_reload")]
-    pub fn to_template_with_custom_paths<Ctx: HotReloadingContext>(
-        &self,
-        location: &'static str,
-        node_paths: Vec<NodePath>,
-        attr_paths: Vec<AttributePath>,
-    ) -> Template {
-        let roots = self
-            .roots
-            .iter()
-            .map(|node| node.to_template_node::<Ctx>())
-            .collect::<Vec<_>>();
-
-        Template {
-            name: location,
-            roots: intern(roots.as_slice()),
-            node_paths: intern(
-                node_paths
-                    .into_iter()
-                    .map(|path| intern(path.as_slice()))
-                    .collect::<Vec<_>>()
-                    .as_slice(),
-            ),
-            attr_paths: intern(
-                attr_paths
-                    .into_iter()
-                    .map(|path| intern(path.as_slice()))
-                    .collect::<Vec<_>>()
-                    .as_slice(),
-            ),
-        }
-    }
-
     pub fn is_empty(&self) -> bool {
         self.roots.is_empty()
     }
 
-    fn implicit_key(&self) -> Option<IfmtInput> {
+    pub(crate) fn implicit_key(&self) -> Option<&AttributeValue> {
         match self.roots.first() {
-            Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key().cloned(),
-            Some(BodyNode::Component(comp)) if self.roots.len() == 1 => {
-                comp.get_key().and_then(|f| f.ifmt().cloned())
-            }
+            Some(BodyNode::Element(el)) => el.key(),
+            Some(BodyNode::Component(comp)) => comp.get_key(),
             _ => None,
         }
     }
 
+    /// Ensure only one key and that the key is not a static str
+    ///
+    /// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
+    fn validate_key(&mut self) {
+        let key = self.implicit_key();
+
+        if let Some(attr) = key {
+            let diagnostic = match &attr {
+                AttributeValue::AttrLiteral(ifmt) => {
+                    if ifmt.is_static() {
+                        ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
+                    } else {
+                        return;
+                    }
+                }
+                _ => attr
+                    .span()
+                    .error("Key must be in the form of a formatted string like `key: \"{value}\""),
+            };
+
+            self.diagnostics.push(diagnostic);
+        }
+    }
+
     pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode {
         let mut node = self.roots.get(path[0] as usize).unwrap();
         for idx in path.iter().skip(1) {
@@ -356,4 +288,71 @@ impl TemplateBody {
     pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
         self.node_paths.iter().map(|path| self.get_dyn_node(path))
     }
+
+    fn quote_roots(&self) -> impl Iterator<Item = TokenStream2> + '_ {
+        self.roots.iter().map(|node| match node {
+            BodyNode::Element(el) => quote! { #el },
+            BodyNode::Text(text) if text.is_static() => {
+                let text = text.input.to_static().unwrap();
+                quote! { dioxus_core::TemplateNode::Text { text: #text } }
+            }
+            _ => {
+                let id = node.get_dyn_idx();
+                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
+            }
+        })
+    }
+
+    /// Iterate through the literal component properties of this rsx call in depth-first order
+    pub(crate) fn literal_component_properties(&self) -> impl Iterator<Item = &HotLiteral> + '_ {
+        self.dynamic_nodes()
+            .filter_map(|node| {
+                if let BodyNode::Component(component) = node {
+                    Some(component)
+                } else {
+                    None
+                }
+            })
+            .flat_map(|component| {
+                component.fields.iter().filter_map(|field| {
+                    if let AttributeValue::AttrLiteral(literal) = &field.value {
+                        Some(literal)
+                    } else {
+                        None
+                    }
+                })
+            })
+    }
+
+    fn hot_reload_mapping(&self, name: impl ToTokens) -> TokenStream2 {
+        let key = if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(key))) =
+            self.implicit_key()
+        {
+            quote! { Some(#key) }
+        } else {
+            quote! { None }
+        };
+        let roots = self.quote_roots();
+        let dynamic_nodes = self.dynamic_nodes().map(|node| {
+            let id = node.get_dyn_idx();
+            quote! { dioxus_core::internal::HotReloadDynamicNode::Dynamic(#id) }
+        });
+        let dyn_attr_printer = self.dynamic_attributes().map(|attr| {
+            let id = attr.get_dyn_idx();
+            quote! { dioxus_core::internal::HotReloadDynamicAttribute::Dynamic(#id) }
+        });
+        let component_values = self
+            .literal_component_properties()
+            .map(|literal| literal.quote_as_hot_reload_literal());
+        quote! {
+            dioxus_core::internal::HotReloadedTemplate::new(
+                #name,
+                #key,
+                vec![ #( #dynamic_nodes ),* ],
+                vec![ #( #dyn_attr_printer ),* ],
+                vec![ #( #component_values ),* ],
+                &[ #( #roots ),* ],
+            )
+        }
+    }
 }

+ 4 - 14
packages/rsx/src/text_node.rs

@@ -1,11 +1,7 @@
 #[cfg(feature = "hot_reload")]
 use dioxus_core::TemplateNode;
 
-use crate::{
-    literal::{HotLiteral, HotLiteralType},
-    location::DynIdx,
-    IfmtInput,
-};
+use crate::{literal::HotLiteral, location::DynIdx, HotReloadFormattedSegment, IfmtInput};
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use quote::ToTokens;
 use quote::{quote, TokenStreamExt};
@@ -17,8 +13,7 @@ use syn::{
 
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub struct TextNode {
-    pub input: IfmtInput,
-    pub hr_idx: DynIdx,
+    pub input: HotReloadFormattedSegment,
     pub dyn_idx: DynIdx,
 }
 
@@ -26,7 +21,6 @@ impl Parse for TextNode {
     fn parse(input: ParseStream) -> Result<Self> {
         Ok(Self {
             input: input.parse()?,
-            hr_idx: DynIdx::default(),
             dyn_idx: DynIdx::default(),
         })
     }
@@ -44,10 +38,7 @@ impl ToTokens for TextNode {
             // todo:
             // Use the RsxLiteral implementation to spit out a hotreloadable variant of this string
             // This is not super efficient since we're doing a bit of cloning
-            let as_lit = HotLiteral {
-                hr_idx: self.hr_idx.clone(),
-                value: HotLiteralType::Fmted(txt.clone()),
-            };
+            let as_lit = HotLiteral::Fmted(txt.clone());
 
             tokens.append_all(quote! {
                 dioxus_core::DynamicNode::Text(dioxus_core::VText::new( #as_lit ))
@@ -63,9 +54,8 @@ impl TextNode {
             segments: vec![],
         };
         Self {
-            input: ifmt,
+            input: ifmt.into(),
             dyn_idx: Default::default(),
-            hr_idx: Default::default(),
         }
     }
 

+ 1 - 1
packages/rsx/src/util.rs

@@ -11,7 +11,7 @@ use syn::{
     Ident,
 };
 
-/// interns a object into a static object, resusing the value if it already exists
+/// interns a object into a static object, reusing the value if it already exists
 #[cfg(feature = "hot_reload")]
 pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
     s: impl Into<Intern<T>>,

+ 383 - 120
packages/rsx/tests/hotreload_pattern.rs

@@ -1,9 +1,17 @@
 #![allow(unused)]
 
-use dioxus_core::{prelude::Template, VNode};
+use std::collections::HashMap;
+
+use dioxus_core::{
+    internal::{
+        FmtSegment, FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute,
+        HotReloadDynamicNode, HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
+    },
+    prelude::{Template, TemplateNode},
+    TemplateAttribute, VNode,
+};
 use dioxus_rsx::{
-    hot_reload::{diff_rsx, ChangedRsx},
-    hotreload::HotReloadedTemplate,
+    hot_reload::{self, diff_rsx, ChangedRsx, HotReloadResult},
     CallBody, HotReloadingContext,
 };
 use proc_macro2::TokenStream;
@@ -36,38 +44,34 @@ impl HotReloadingContext for Mock {
     }
 }
 
-fn boilerplate(old: TokenStream, new: TokenStream) -> Option<Vec<Template>> {
+fn hot_reload_from_tokens(
+    old: TokenStream,
+    new: TokenStream,
+) -> Option<HashMap<usize, HotReloadedTemplate>> {
     let old: CallBody = syn::parse2(old).unwrap();
     let new: CallBody = syn::parse2(new).unwrap();
 
-    let location = "file:line:col:0";
-    hotreload_callbody::<Mock>(&old, &new, location)
+    hotreload_callbody::<Mock>(&old, &new)
 }
 
-fn can_hotreload(old: TokenStream, new: TokenStream) -> Option<HotReloadedTemplate> {
-    let old: CallBody = syn::parse2(old).unwrap();
-    let new: CallBody = syn::parse2(new).unwrap();
-
-    let location = "file:line:col:0";
-    let results = HotReloadedTemplate::new::<Mock>(&old, &new, location, Default::default())?;
-    Some(results)
+fn can_hotreload(old: TokenStream, new: TokenStream) -> bool {
+    hot_reload_from_tokens(old, new).is_some()
 }
 
 fn hotreload_callbody<Ctx: HotReloadingContext>(
     old: &CallBody,
     new: &CallBody,
-    location: &'static str,
-) -> Option<Vec<Template>> {
-    let results = HotReloadedTemplate::new::<Ctx>(old, new, location, Default::default())?;
+) -> Option<HashMap<usize, HotReloadedTemplate>> {
+    let results = HotReloadResult::new::<Ctx>(&old.body, &new.body, Default::default())?;
     Some(results.templates)
 }
 
 fn callbody_to_template<Ctx: HotReloadingContext>(
     old: &CallBody,
     location: &'static str,
-) -> Option<Template> {
-    let results = HotReloadedTemplate::new::<Ctx>(old, old, location, Default::default())?;
-    Some(*results.templates.first().unwrap())
+) -> Option<HotReloadedTemplate> {
+    let mut results = HotReloadResult::new::<Ctx>(&old.body, &old.body, Default::default())?;
+    Some(results.templates.remove(&0).unwrap())
 }
 
 fn base_stream() -> TokenStream {
@@ -120,8 +124,8 @@ fn simple_for_loop() {
     let new_valid: CallBody = syn::parse2(new_valid).unwrap();
     let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
 
-    assert!(hotreload_callbody::<Mock>(&old, &new_valid, location).is_some());
-    assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
+    assert!(hotreload_callbody::<Mock>(&old, &new_valid).is_some());
+    assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
 }
 
 #[test]
@@ -140,23 +144,234 @@ fn valid_reorder() {
         }
     };
 
-    let location = "file:line:col:0";
     let new: CallBody = syn::parse2(new_valid).unwrap();
 
-    let valid = hotreload_callbody::<Mock>(&old, &new, location);
+    let valid = hotreload_callbody::<Mock>(&old, &new);
     assert!(valid.is_some());
     let templates = valid.unwrap();
 
     // Currently we return all the templates, even if they didn't change
     assert_eq!(templates.len(), 3);
 
-    let template = &templates[2];
+    let template = &templates[&0];
 
     // It's an inversion, so we should get them in reverse
-    assert_eq!(template.node_paths, &[&[0, 1], &[0, 0]]);
+    assert_eq!(
+        template.roots,
+        &[TemplateNode::Element {
+            tag: "div",
+            namespace: None,
+            attrs: &[],
+            children: &[
+                TemplateNode::Dynamic { id: 0 },
+                TemplateNode::Dynamic { id: 1 }
+            ]
+        }]
+    );
+    assert_eq!(
+        template.dynamic_nodes,
+        &[
+            HotReloadDynamicNode::Dynamic(1),
+            HotReloadDynamicNode::Dynamic(0)
+        ]
+    );
+}
+
+#[test]
+fn valid_new_node() {
+    // Adding a new dynamic node should be hot reloadable as long as the text was present in the old version
+    // of the rsx block
+    let old = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div { "item is {item}" }
+            }
+        }
+    };
+    let new = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div { "item is {item}" }
+                div { "item is also {item}" }
+            }
+        }
+    };
+
+    let templates = hot_reload_from_tokens(old, new).unwrap();
+
+    // Currently we return all the templates, even if they didn't change
+    assert_eq!(templates.len(), 2);
+
+    let template = &templates[&1];
+
+    // The new dynamic node should be created from the formatted segments pool
+    assert_eq!(
+        template.dynamic_nodes,
+        &[
+            HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
+                FmtSegment::Literal { value: "item is " },
+                FmtSegment::Dynamic { id: 0 }
+            ],)),
+            HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
+                FmtSegment::Literal {
+                    value: "item is also "
+                },
+                FmtSegment::Dynamic { id: 0 }
+            ],)),
+        ]
+    );
+}
+
+#[test]
+fn valid_new_dynamic_attribute() {
+    // Adding a new dynamic attribute should be hot reloadable as long as the text was present in the old version
+    // of the rsx block
+    let old = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div {
+                    class: "item is {item}"
+                }
+            }
+        }
+    };
+    let new = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div {
+                    class: "item is {item}"
+                }
+                div {
+                    class: "item is also {item}"
+                }
+            }
+        }
+    };
+
+    let templates = hot_reload_from_tokens(old, new).unwrap();
+
+    // Currently we return all the templates, even if they didn't change
+    assert_eq!(templates.len(), 2);
+
+    let template = &templates[&1];
+
+    // We should have a new dynamic attribute
+    assert_eq!(
+        template.roots,
+        &[
+            TemplateNode::Element {
+                tag: "div",
+                namespace: None,
+                attrs: &[TemplateAttribute::Dynamic { id: 0 }],
+                children: &[]
+            },
+            TemplateNode::Element {
+                tag: "div",
+                namespace: None,
+                attrs: &[TemplateAttribute::Dynamic { id: 1 }],
+                children: &[]
+            }
+        ]
+    );
 
-    // And the byte index should be the original template
-    assert_eq!(template.name, "file:line:col:0");
+    // The new dynamic attribute should be created from the formatted segments pool
+    assert_eq!(
+        template.dynamic_attributes,
+        &[
+            HotReloadDynamicAttribute::Named(NamedAttribute::new(
+                "class",
+                None,
+                HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
+                    vec![
+                        FmtSegment::Literal { value: "item is " },
+                        FmtSegment::Dynamic { id: 0 }
+                    ],
+                )))
+            )),
+            HotReloadDynamicAttribute::Named(NamedAttribute::new(
+                "class",
+                None,
+                HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
+                    vec![
+                        FmtSegment::Literal {
+                            value: "item is also "
+                        },
+                        FmtSegment::Dynamic { id: 0 }
+                    ],
+                )))
+            )),
+        ]
+    );
+}
+
+#[test]
+fn valid_move_dynamic_segment_between_nodes() {
+    // Hot reloading should let you move around a dynamic formatted segment between nodes
+    let old = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div {
+                    class: "item is {item}"
+                }
+            }
+        }
+    };
+    let new = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                "item is {item}"
+            }
+        }
+    };
+
+    let templates = hot_reload_from_tokens(old, new).unwrap();
+
+    // Currently we return all the templates, even if they didn't change
+    assert_eq!(templates.len(), 2);
+
+    let template = &templates[&1];
+
+    // We should have a new dynamic node and no attributes
+    assert_eq!(template.roots, &[TemplateNode::Dynamic { id: 0 }]);
+
+    // The new dynamic node should be created from the formatted segments pool
+    assert_eq!(
+        template.dynamic_nodes,
+        &[HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
+            FmtSegment::Literal { value: "item is " },
+            FmtSegment::Dynamic { id: 0 }
+        ])),]
+    );
+}
+
+#[test]
+fn valid_keys() {
+    let a = quote! {
+        div {
+            key: "{value}",
+        }
+    };
+
+    // we can clone dynamic nodes to hot reload them
+    let b = quote! {
+        div {
+            key: "{value}-1234",
+        }
+    };
+
+    let hot_reload = hot_reload_from_tokens(a, b).unwrap();
+
+    assert_eq!(hot_reload.len(), 1);
+
+    let template = &hot_reload[&0];
+
+    assert_eq!(
+        template.key,
+        Some(FmtedSegments::new(vec![
+            FmtSegment::Dynamic { id: 0 },
+            FmtSegment::Literal { value: "-1234" }
+        ]))
+    );
 }
 
 #[test]
@@ -227,63 +442,32 @@ fn invalid_cases() {
         syn::parse2(new_invalid_new_dynamic_internal).unwrap();
     let new_invalid_added: CallBody = syn::parse2(new_invalid_added).unwrap();
 
-    assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
-    assert!(
-        hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal, location).is_none()
-    );
+    assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
+    assert!(hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal).is_none());
 
-    let removed = hotreload_callbody::<Mock>(&old, &new_valid_removed, location);
-    assert!(removed.is_some());
-    let templates = removed.unwrap();
+    let templates = hotreload_callbody::<Mock>(&old, &new_valid_removed).unwrap();
 
     // we don't get the removed template back
     assert_eq!(templates.len(), 2);
-    let template = &templates[1];
+    let template = &templates.get(&0).unwrap();
 
     // We just completely removed the dynamic node, so it should be a "dud" path and then the placement
-    assert_eq!(template.node_paths, &[&[], &[0u8, 0] as &[u8]]);
+    assert_eq!(
+        template.roots,
+        &[TemplateNode::Element {
+            tag: "div",
+            namespace: None,
+            attrs: &[],
+            children: &[TemplateNode::Dynamic { id: 0 }]
+        }]
+    );
+    assert_eq!(template.dynamic_nodes, &[HotReloadDynamicNode::Dynamic(1)]);
 
     // Adding a new dynamic node should not be hot reloadable
-    let added = hotreload_callbody::<Mock>(&old, &new_invalid_added, location);
+    let added = hotreload_callbody::<Mock>(&old, &new_invalid_added);
     assert!(added.is_none());
 }
 
-#[test]
-fn new_names() {
-    let old = quote! {
-        div {
-            for item in vec![1, 2, 3] {
-                div { "asasddasdasd" }
-                div { "123" }
-            }
-        }
-    };
-
-    // Same order, just different contents
-    let new_valid_internal = quote! {
-        div {
-            for item in vec![1, 2, 3] {
-                div { "asasddasdasd" }
-                div { "456" }
-            }
-        }
-    };
-
-    let templates = boilerplate(old, new_valid_internal).unwrap();
-
-    // Getting back all the templates even though some might not have changed
-    // This is currently just a symptom of us not checking if anything has changed, but has no bearing
-    // on output really.
-    assert_eq!(templates.len(), 2);
-
-    // The ordering is going to be inverse since its a depth-first traversal
-    let external = &templates[1];
-    assert_eq!(external.name, "file:line:col:0");
-
-    let internal = &templates[0];
-    assert_eq!(internal.name, "file:line:col:1");
-}
-
 #[test]
 fn attributes_reload() {
     let old = quote! {
@@ -303,7 +487,7 @@ fn attributes_reload() {
         }
     };
 
-    let templates = boilerplate(old, new_valid_internal).unwrap();
+    let templates = hot_reload_from_tokens(old, new_valid_internal).unwrap();
 
     dbg!(templates);
 }
@@ -377,13 +561,12 @@ fn diffs_complex() {
     let old: CallBody = syn::parse2(old).unwrap();
     let new: CallBody = syn::parse2(new).unwrap();
 
-    let location = "file:line:col:0";
-    let templates = hotreload_callbody::<Mock>(&old, &new, location).unwrap();
+    let templates = hotreload_callbody::<Mock>(&old, &new).unwrap();
 }
 
 #[test]
 fn remove_node() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             svg {
                 Comp {}
@@ -398,12 +581,12 @@ fn remove_node() {
     )
     .unwrap();
 
-    dbg!(changed);
+    dbg!(valid);
 }
 
 #[test]
 fn if_chains() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             if cond {
                 "foo"
@@ -417,7 +600,7 @@ fn if_chains() {
     )
     .unwrap();
 
-    let very_complex_chain = boilerplate(
+    let very_complex_chain = hot_reload_from_tokens(
         quote! {
             if cond {
                 if second_cond {
@@ -448,7 +631,7 @@ fn if_chains() {
 
 #[test]
 fn component_bodies() {
-    let changed = boilerplate(
+    let valid = can_hotreload(
         quote! {
             Comp {
                 "foo"
@@ -459,16 +642,36 @@ fn component_bodies() {
                 "baz"
             }
         },
-    )
-    .unwrap();
+    );
 
-    dbg!(changed);
+    assert!(valid);
+}
+
+// We currently don't track aliasing which means we can't allow dynamic nodes/formatted segments to be moved between scopes
+#[test]
+fn moving_between_scopes() {
+    let valid = can_hotreload(
+        quote! {
+            for x in 0..10 {
+                for y in 0..10 {
+                    div { "x is {x}" }
+                }
+            }
+        },
+        quote! {
+            for x in 0..10 {
+                div { "x is {x}" }
+            }
+        },
+    );
+
+    assert!(!valid);
 }
 
 /// Everything reloads!
 #[test]
 fn kitch_sink_of_reloadability() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             div {
                 for i in 0..10 {
@@ -497,14 +700,14 @@ fn kitch_sink_of_reloadability() {
     )
     .unwrap();
 
-    dbg!(changed);
+    dbg!(valid);
 }
 
 /// Moving nodes inbetween multiple rsx! calls currently doesn't work
 /// Sad. Needs changes to core to work, and is technically flawed?
 #[test]
 fn entire_kitchen_sink() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             div {
                 for i in 0..10 {
@@ -534,12 +737,12 @@ fn entire_kitchen_sink() {
         },
     );
 
-    assert!(changed.is_none());
+    assert!(valid.is_none());
 }
 
 #[test]
 fn tokenstreams_and_locations() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             div { "hhi" }
             div {
@@ -589,12 +792,12 @@ fn tokenstreams_and_locations() {
         },
     );
 
-    dbg!(changed);
+    dbg!(valid);
 }
 
 #[test]
 fn ide_testcase() {
-    let changed = boilerplate(
+    let valid = hot_reload_from_tokens(
         quote! {
             div {
                 div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
@@ -613,7 +816,7 @@ fn ide_testcase() {
         },
     );
 
-    dbg!(changed);
+    dbg!(valid);
 }
 
 #[test]
@@ -635,7 +838,7 @@ fn assigns_ids() {
 
 #[test]
 fn simple_start() {
-    let changed = boilerplate(
+    let valid = can_hotreload(
         //
         quote! {
             div {
@@ -653,12 +856,12 @@ fn simple_start() {
         },
     );
 
-    dbg!(changed.unwrap());
+    assert!(valid);
 }
 
 #[test]
 fn complex_cases() {
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         quote! {
             div {
                 class: "Some {one}",
@@ -675,12 +878,12 @@ fn complex_cases() {
         },
     );
 
-    dbg!(changed.unwrap());
+    assert!(valid);
 }
 
 #[test]
 fn attribute_cases() {
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         quote! {
             div {
                 class: "Some {one}",
@@ -695,52 +898,59 @@ fn attribute_cases() {
             }
         },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
 
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { class: 123 } },
         quote! { div { class: 456 } },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
 
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { class: 123.0 } },
         quote! { div { class: 456.0 } },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
 
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { class: "asd {123}", } },
         quote! { div { class: "def", } },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
 }
 
 #[test]
 fn text_node_cases() {
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { "hello {world}" } },
         quote! { div { "world {world}" } },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
 
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { "hello {world}" } },
         quote! { div { "world" } },
     );
-    dbg!(changed.unwrap());
+    assert!(valid);
+
+    let valid = can_hotreload(
+        //
+        quote! { div { "hello {world}" } },
+        quote! { div { "world {world} {world}" } },
+    );
+    assert!(valid);
 
-    let changed = can_hotreload(
+    let valid = can_hotreload(
         //
         quote! { div { "hello" } },
         quote! { div { "world {world}" } },
     );
-    assert!(changed.is_none());
+    assert!(!valid);
 }
 
 #[test]
@@ -759,8 +969,8 @@ fn simple_carry() {
         "thing {hij}"
     };
 
-    let changed = can_hotreload(a, b);
-    dbg!(changed.unwrap());
+    let valid = can_hotreload(a, b);
+    assert!(valid);
 }
 
 #[test]
@@ -778,8 +988,8 @@ fn complex_carry_text() {
         "thing {hij}"
     };
 
-    let changed = can_hotreload(a, b);
-    dbg!(changed.unwrap());
+    let valid = can_hotreload(a, b);
+    assert!(valid);
 }
 
 #[test]
@@ -807,8 +1017,8 @@ fn complex_carry() {
         }
     };
 
-    let changed = can_hotreload(a, b);
-    dbg!(changed.unwrap());
+    let valid = can_hotreload(a, b);
+    assert!(valid);
 }
 
 #[test]
@@ -832,8 +1042,8 @@ fn component_with_lits() {
         }
     };
 
-    let changed = can_hotreload(a, b);
-    dbg!(changed.unwrap());
+    let valid = can_hotreload(a, b);
+    assert!(valid);
 }
 
 #[test]
@@ -859,6 +1069,59 @@ fn component_with_handlers() {
         }
     };
 
-    let changed = can_hotreload(a, b);
-    dbg!(changed.unwrap());
+    let valid = can_hotreload(a, b);
+    assert!(valid);
+}
+
+#[test]
+fn duplicating_dynamic_nodes() {
+    let a = quote! {
+        div {
+            {some_expr}
+        }
+    };
+
+    // we can clone dynamic nodes to hot reload them
+    let b = quote! {
+        div {
+            {some_expr}
+            {some_expr}
+        }
+    };
+
+    let valid = can_hotreload(a, b);
+    assert!(valid);
+}
+
+#[test]
+fn duplicating_dynamic_attributes() {
+    let a = quote! {
+        div {
+            width: value,
+        }
+    };
+
+    // we can clone dynamic nodes to hot reload them
+    let b = quote! {
+        div {
+            width: value,
+            height: value,
+        }
+    };
+
+    let valid = can_hotreload(a, b);
+    assert!(valid);
+}
+
+// We should be able to fill in empty nodes
+#[test]
+fn valid_fill_empty() {
+    let valid = can_hotreload(
+        quote! {},
+        quote! {
+            div { "x is 123" }
+        },
+    );
+
+    assert!(valid);
 }

+ 1 - 4
packages/rsx/tests/parsing.rs

@@ -1,4 +1,4 @@
-use dioxus_rsx::{hot_reload::Empty, CallBody};
+use dioxus_rsx::CallBody;
 use quote::ToTokens;
 
 use dioxus_rsx::PrettyUnparse;
@@ -40,9 +40,6 @@ fn callbody_ctx() {
     let cb: CallBody = syn::parse2(item).unwrap();
 
     dbg!(cb.template_idx.get());
-    dbg!(cb.ifmt_idx.get());
-
-    let _template = cb.body.to_template::<Empty>();
 }
 
 #[test]

+ 1 - 1
packages/ssr/src/cache.rs

@@ -126,7 +126,7 @@ impl StringCache {
 
         let mut cur_path = vec![];
 
-        for (root_idx, root) in template.template.get().roots.iter().enumerate() {
+        for (root_idx, root) in template.template.roots.iter().enumerate() {
             from_template_recursive(root, &mut cur_path, root_idx, true, &mut chain)?;
         }
 

+ 1 - 1
packages/ssr/src/renderer.rs

@@ -108,7 +108,7 @@ impl Renderer {
     ) -> std::fmt::Result {
         let entry = self
             .template_cache
-            .entry(template.template.get().id())
+            .entry(template.template.id())
             .or_insert_with(move || Arc::new(StringCache::from_template(template).unwrap()))
             .clone();
 

+ 1 - 1
packages/web/src/hydration/hydrate.rs

@@ -245,7 +245,7 @@ impl WebsysDom {
         ids: &mut Vec<u32>,
         to_mount: &mut Vec<ElementId>,
     ) -> Result<(), RehydrationError> {
-        for (i, root) in vnode.template.get().roots.iter().enumerate() {
+        for (i, root) in vnode.template.roots.iter().enumerate() {
             self.rehydrate_template_node(
                 dom,
                 vnode,