From 2720f105f1dfbd23708dbe43028e03a963c14df0 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Fri, 27 Mar 2026 22:21:12 +0100 Subject: [PATCH] Implement script element loading and execution (defer, async) Add script execution to the page loading pipeline: after HTML parsing, walk the DOM for + "#; + let doc = run_scripts(html); + + // Find the div and check its text content was modified. + let mut found = false; + fn find_div(doc: &Document, node: NodeId, found: &mut bool) { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("target") { + // Check child text node. + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + if text == "after" { + *found = true; + } + } + } + } + } + for child in doc.children(node) { + find_div(doc, child, found); + } + } + find_div(&doc, doc.root(), &mut found); + assert!(found, "script should have modified div text to 'after'"); + } + + #[test] + fn test_multiple_scripts_share_scope() { + let html = r#" + + +
placeholder
+ + "#; + let doc = run_scripts(html); + + fn find_result(doc: &Document, node: NodeId) -> Option { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("result") { + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + return Some(text.to_string()); + } + } + } + } + for child in doc.children(node) { + if let Some(result) = find_result(doc, child) { + return Some(result); + } + } + None + } + + let result = find_result(&doc, doc.root()); + assert_eq!(result.as_deref(), Some("43")); + } + + #[test] + fn test_unknown_type_not_executed() { + let html = r#" +
original
+ + "#; + let doc = run_scripts(html); + + fn find_target(doc: &Document, node: NodeId) -> Option { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("target") { + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + return Some(text.to_string()); + } + } + } + } + for child in doc.children(node) { + if let Some(result) = find_target(doc, child) { + return Some(result); + } + } + None + } + + let result = find_target(&doc, doc.root()); + assert_eq!(result.as_deref(), Some("original")); + } + + #[test] + fn test_type_text_javascript_executes() { + let html = r#" +
before
+ + "#; + let doc = run_scripts(html); + + fn find_target(doc: &Document, node: NodeId) -> Option { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("target") { + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + return Some(text.to_string()); + } + } + } + } + for child in doc.children(node) { + if let Some(result) = find_target(doc, child) { + return Some(result); + } + } + None + } + + let result = find_target(&doc, doc.root()); + assert_eq!(result.as_deref(), Some("after")); + } + + #[test] + fn test_script_error_does_not_crash() { + // A script with a runtime error should not prevent subsequent scripts. + let html = r#" +
before
+ + + "#; + let doc = run_scripts(html); + + fn find_target(doc: &Document, node: NodeId) -> Option { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("target") { + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + return Some(text.to_string()); + } + } + } + } + for child in doc.children(node) { + if let Some(result) = find_target(doc, child) { + return Some(result); + } + } + None + } + + let result = find_target(&doc, doc.root()); + assert_eq!(result.as_deref(), Some("after")); + } + + #[test] + fn test_empty_script_no_crash() { + let doc = run_scripts(""); + assert!(!doc.is_empty()); + } + + #[test] + fn test_defer_scripts_run_after_sync() { + // In our parse-first model, both sync and defer run after parsing. + // defer scripts with src are queued; defer inline scripts are treated + // as sync. Without external script loading in tests, we verify that + // defer inline scripts still execute (treated as sync per spec — + // defer only applies to external scripts). + let html = r#" +
0
+ + + "#; + let doc = run_scripts(html); + + fn find_result(doc: &Document, node: NodeId) -> Option { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("result") { + for child in doc.children(node) { + if let Some(text) = doc.text_content(child) { + return Some(text.to_string()); + } + } + } + } + for child in doc.children(node) { + if let Some(result) = find_result(doc, child) { + return Some(result); + } + } + None + } + + let result = find_result(&doc, doc.root()); + // defer on inline scripts is ignored per spec, so counter=1 runs first + assert_eq!(result.as_deref(), Some("1")); + } + + #[test] + fn test_script_create_element() { + let html = r#" +
+ + "#; + let doc = run_scripts(html); + + // Check that a

was added inside #container. + fn find_dynamic(doc: &Document, node: NodeId) -> bool { + if doc.tag_name(node) == Some("div") { + if doc.get_attribute(node, "id") == Some("container") { + for child in doc.children(node) { + if doc.tag_name(child) == Some("p") { + return true; + } + } + } + } + for child in doc.children(node) { + if find_dynamic(doc, child) { + return true; + } + } + false + } + + assert!( + find_dynamic(&doc, doc.root()), + "script should have appended a

to #container" + ); + } +} diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 5b81bce..23a3a36 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -23,6 +23,9 @@ struct FunctionCompiler { /// Set of local variable names that are captured by inner functions. /// Pre-populated before compilation by scanning inner function bodies. captured_names: HashSet, + /// True for the top-level script scope. Top-level `var` declarations + /// are also stored as globals so they persist across `execute()` calls. + is_top_level: bool, } #[derive(Debug, Clone)] @@ -63,6 +66,7 @@ impl FunctionCompiler { loop_stack: Vec::new(), upvalues: Vec::new(), captured_names: HashSet::new(), + is_top_level: false, } } @@ -707,6 +711,7 @@ fn collect_inner_captures_expr(expr: &Expr, caps: &mut HashSet) { /// Compile a parsed program into a top-level bytecode function. pub fn compile(program: &Program) -> Result { let mut fc = FunctionCompiler::new("

".into(), 0); + fc.is_top_level = true; // Pre-scan to find which top-level locals are captured by inner functions. fc.captured_names = collect_inner_captures(&program.body); @@ -1230,6 +1235,11 @@ fn compile_var_declarator( let tmp = fc.alloc_reg(); compile_expr(fc, init, tmp)?; fc.builder.emit_reg_reg(Op::CellStore, reg, tmp); + // Top-level var: also store as global for cross-script access. + if fc.is_top_level && kind == VarKind::Var { + let name_idx = fc.builder.add_name(name); + fc.builder.emit_store_global(name_idx, tmp); + } fc.free_reg(tmp); } // No init => cell stays undefined (already the default). @@ -1238,6 +1248,14 @@ fn compile_var_declarator( } else { fc.builder.emit_reg(Op::LoadUndefined, reg); } + + // Top-level var/let/const: also store as global so the value + // persists across separate `execute()` calls (multiple scripts + // sharing the same global scope). + if fc.is_top_level && !is_captured { + let name_idx = fc.builder.add_name(name); + fc.builder.emit_store_global(name_idx, reg); + } } _ => { // Destructuring: evaluate init, then bind patterns. diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index ef4249a..d503731 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -846,6 +846,24 @@ impl Vm { crate::dom_bridge::init_event_system(self); } + /// Detach the DOM document from the VM, returning it. + /// + /// This removes the `document` global and disconnects the DOM bridge. + /// Returns `None` if no document was attached or if there are outstanding + /// references to the bridge. + pub fn detach_document(&mut self) -> Option { + let bridge = self.dom_bridge.take()?; + self.globals.remove("document"); + match Rc::try_unwrap(bridge) { + Ok(bridge) => Some(bridge.document.into_inner()), + Err(rc) => { + // Something still holds a reference — reattach. + self.dom_bridge = Some(rc); + None + } + } + } + /// Set an instruction limit. The VM will return a RuntimeError after /// executing this many instructions. pub fn set_instruction_limit(&mut self, limit: u64) { -- 2.51.2