浏览代码

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 月之前
父节点
当前提交
34bdcd15cf
共有 40 个文件被更改,包括 2289 次插入1898 次删除
  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,