From 08889c10b7450870a3cc1a1be37728cad61c2b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=B6nke?= Date: Sun, 19 Jul 2026 16:05:20 +0000 Subject: [PATCH] feat: support named templates and xsl:call-template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A named template compiles to a function registered under its name. xsl:call-template invokes it, running the template with the caller's context (item, position, size) as XSLT requires. Parameters — xsl:param on the template and xsl:with-param on the call — report as unsupported. --- xee-ir/src/compile.rs | 6 ++-- xee-ir/src/declaration_compiler.rs | 23 ++++++++++++- xee-ir/src/function_compiler.rs | 32 +++++++++++++++++- xee-ir/src/ir.rs | 16 +++++++++ xee-ir/src/lib.rs | 2 +- xee-ir/tests/test_xml_ir.rs | 10 ++++-- xee-xslt-compiler/src/ast_ir.rs | 50 +++++++++++++++++++++++++++- xee-xslt-compiler/tests/test_xslt.rs | 33 ++++++++++++++++++ 8 files changed, 164 insertions(+), 8 deletions(-) diff --git a/xee-ir/src/compile.rs b/xee-ir/src/compile.rs index 4a0c5df28..a4d3c2564 100644 --- a/xee-ir/src/compile.rs +++ b/xee-ir/src/compile.rs @@ -2,7 +2,7 @@ use ahash::HashMapExt; use xee_interpreter::{context::StaticContext, error::SpannedResult, interpreter::Program}; use crate::{ - declaration_compiler::{DeclarationCompiler, ModeIds}, + declaration_compiler::{DeclarationCompiler, ModeIds, TemplateIds}, ir, FunctionBuilder, FunctionCompiler, Scopes, }; @@ -11,7 +11,9 @@ pub fn compile_xpath(expr: ir::ExprS, static_context: StaticContext) -> SpannedR let mut scopes = Scopes::new(); let builder = FunctionBuilder::new(&mut program); let empty_mode_ids = ModeIds::new(); - let mut compiler = FunctionCompiler::new(builder, &mut scopes, &empty_mode_ids); + let empty_template_ids = TemplateIds::new(); + let mut compiler = + FunctionCompiler::new(builder, &mut scopes, &empty_mode_ids, &empty_template_ids); compiler.compile_expr(&expr)?; Ok(program) } diff --git a/xee-ir/src/declaration_compiler.rs b/xee-ir/src/declaration_compiler.rs index ca0147c33..5351b287c 100644 --- a/xee-ir/src/declaration_compiler.rs +++ b/xee-ir/src/declaration_compiler.rs @@ -29,6 +29,7 @@ impl RuleBuilder { } pub type ModeIds = HashMap; +pub type TemplateIds = HashMap; pub struct DeclarationCompiler<'a> { program: &'a mut interpreter::Program, @@ -36,6 +37,7 @@ pub struct DeclarationCompiler<'a> { rule_declaration_order: i64, rule_builders: HashMap>, mode_ids: ModeIds, + template_ids: TemplateIds, } impl<'a> DeclarationCompiler<'a> { @@ -46,12 +48,18 @@ impl<'a> DeclarationCompiler<'a> { rule_declaration_order: 0, rule_builders: HashMap::new(), mode_ids: HashMap::new(), + template_ids: HashMap::new(), } } fn function_compiler(&mut self) -> FunctionCompiler<'_> { let function_builder = FunctionBuilder::new(self.program); - FunctionCompiler::new(function_builder, &mut self.scopes, &self.mode_ids) + FunctionCompiler::new( + function_builder, + &mut self.scopes, + &self.mode_ids, + &self.template_ids, + ) } pub fn compile_declarations( @@ -62,6 +70,19 @@ impl<'a> DeclarationCompiler<'a> { // this early so any mode reference within apply-templates will resolve. self.compile_modes(declarations); + // Named templates compile to functions invoked by name through + // xsl:call-template. Register their ids before the rules and main, so a + // call resolves regardless of the caller's position. + for named_template in &declarations.named_templates { + let function_id = { + let mut function_compiler = self.function_compiler(); + function_compiler + .compile_function_id(&named_template.function_definition, (0..0).into())? + }; + self.template_ids + .insert(named_template.name.clone(), function_id); + } + for rule in &declarations.rules { self.compile_rule(rule)?; } diff --git a/xee-ir/src/function_compiler.rs b/xee-ir/src/function_compiler.rs index 70f4a30d2..f68d51ff1 100644 --- a/xee-ir/src/function_compiler.rs +++ b/xee-ir/src/function_compiler.rs @@ -6,7 +6,7 @@ use xee_interpreter::interpreter::instruction::Instruction; use xee_interpreter::span::SourceSpan; use xee_interpreter::{error, function, sequence}; -use crate::declaration_compiler::ModeIds; +use crate::declaration_compiler::{ModeIds, TemplateIds}; use crate::ir; use super::builder::{BackwardJumpRef, ForwardJumpRef, FunctionBuilder, JumpCondition}; @@ -17,6 +17,7 @@ pub(crate) type Scopes = scope::Scopes; pub struct FunctionCompiler<'a> { pub(crate) scopes: &'a mut Scopes, pub(crate) mode_ids: &'a ModeIds, + pub(crate) template_ids: &'a TemplateIds, pub(crate) builder: FunctionBuilder<'a>, } @@ -25,11 +26,13 @@ impl<'a> FunctionCompiler<'a> { builder: FunctionBuilder<'a>, scopes: &'a mut Scopes, mode_ids: &'a ModeIds, + template_ids: &'a TemplateIds, ) -> Self { Self { builder, scopes, mode_ids, + template_ids, } } @@ -90,6 +93,9 @@ impl<'a> FunctionCompiler<'a> { ir::Expr::ApplyTemplates(apply_templates) => { self.compile_apply_templates(apply_templates, span) } + ir::Expr::CallTemplate(call_template) => { + self.compile_call_template(call_template, span) + } ir::Expr::CopyShallow(copy_shallow) => self.compile_copy_shallow(copy_shallow, span), ir::Expr::CopyDeep(copy_deep) => self.compile_copy_deep(copy_deep, span), } @@ -342,6 +348,7 @@ impl<'a> FunctionCompiler<'a> { builder: nested_builder, scopes: self.scopes, mode_ids: self.mode_ids, + template_ids: self.template_ids, }; for param in &function_definition.params { @@ -438,6 +445,29 @@ impl<'a> FunctionCompiler<'a> { Ok(()) } + fn compile_call_template( + &mut self, + call_template: &ir::CallTemplate, + span: SourceSpan, + ) -> error::SpannedResult<()> { + let function_id = self + .template_ids + .get(&call_template.name) + .copied() + .ok_or_else(|| { + Error::Unsupported(format!("No template named {:?}", call_template.name)) + .with_span(span) + })?; + self.builder + .emit(Instruction::Closure(function_id.as_u16()), span); + for arg in &call_template.args { + self.compile_atom(arg)?; + } + self.builder + .emit(Instruction::Call(call_template.args.len() as u8), span); + Ok(()) + } + fn compile_lookup( &mut self, lookup: &ir::Lookup, diff --git a/xee-ir/src/ir.rs b/xee-ir/src/ir.rs index d00a2e2ee..5eb1e5184 100644 --- a/xee-ir/src/ir.rs +++ b/xee-ir/src/ir.rs @@ -56,6 +56,7 @@ pub enum Expr { XmlProcessingInstruction(XmlProcessingInstruction), XmlAppend(XmlAppend), ApplyTemplates(ApplyTemplates), + CallTemplate(CallTemplate), CopyShallow(CopyShallow), CopyDeep(CopyDeep), } @@ -360,6 +361,19 @@ pub struct Rule { pub function_definition: FunctionDefinition, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CallTemplate { + pub name: xmlname::OwnedName, + // The caller's context (item, position, last), forwarded to the template. + pub args: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NamedTemplate { + pub name: xmlname::OwnedName, + pub function_definition: FunctionDefinition, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ModeValue { Named(xmlname::OwnedName), @@ -373,6 +387,7 @@ pub struct Mode {} #[derive(Debug, Clone, PartialEq, Eq)] pub struct Declarations { pub rules: Vec, + pub named_templates: Vec, pub modes: HashMap, Mode>, pub functions: Vec, pub main: FunctionDefinition, @@ -383,6 +398,7 @@ impl Declarations { pub fn new(main: FunctionDefinition) -> Self { Self { rules: Vec::new(), + named_templates: Vec::new(), modes: HashMap::new(), functions: Vec::new(), main, diff --git a/xee-ir/src/lib.rs b/xee-ir/src/lib.rs index 603ebfc63..6523dba39 100644 --- a/xee-ir/src/lib.rs +++ b/xee-ir/src/lib.rs @@ -10,7 +10,7 @@ mod variables; pub use binding::{Binding, Bindings}; pub use builder::FunctionBuilder; pub use compile::{compile_xpath, compile_xslt}; -pub use declaration_compiler::ModeIds; +pub use declaration_compiler::{ModeIds, TemplateIds}; pub use function_compiler::FunctionCompiler; pub use scope::Scopes; diff --git a/xee-ir/tests/test_xml_ir.rs b/xee-ir/tests/test_xml_ir.rs index 1db04dd97..af1ec28c2 100644 --- a/xee-ir/tests/test_xml_ir.rs +++ b/xee-ir/tests/test_xml_ir.rs @@ -2,7 +2,7 @@ use ahash::HashMapExt; use insta::assert_debug_snapshot; use xee_interpreter::interpreter::{instruction::decode_instructions, Program}; -use xee_ir::{ir, FunctionBuilder, FunctionCompiler, ModeIds, Scopes}; +use xee_ir::{ir, FunctionBuilder, FunctionCompiler, ModeIds, Scopes, TemplateIds}; use xee_xpath_ast::span::Spanned; fn spanned(t: T) -> Spanned { @@ -85,7 +85,13 @@ fn test_generate_element() { let function_builder = FunctionBuilder::new(&mut program); let mut scopes = Scopes::new(); let empty_mode_ids = ModeIds::new(); - let mut compiler = FunctionCompiler::new(function_builder, &mut scopes, &empty_mode_ids); + let empty_template_ids = TemplateIds::new(); + let mut compiler = FunctionCompiler::new( + function_builder, + &mut scopes, + &empty_mode_ids, + &empty_template_ids, + ); compiler.compile_expr(&outer_expr).unwrap(); diff --git a/xee-xslt-compiler/src/ast_ir.rs b/xee-xslt-compiler/src/ast_ir.rs index a58500360..3a028ff02 100644 --- a/xee-xslt-compiler/src/ast_ir.rs +++ b/xee-xslt-compiler/src/ast_ir.rs @@ -181,7 +181,24 @@ impl<'a> IrConverter<'a> { }); Ok(()) } else { - Err(error::Error::Unsupported("Named templates not supported".to_string()).into()) + let name = template.name.clone().ok_or_else(|| { + error::Error::Unsupported( + "A template must have a match pattern or a name".to_string(), + ) + })?; + if !template.params.is_empty() { + return Err(error::Error::Unsupported( + "Template parameters are not supported yet".to_string(), + ) + .into()); + } + let function_definition = + self.sequence_constructor_function(&template.sequence_constructor)?; + declarations.named_templates.push(ir::NamedTemplate { + name, + function_definition, + }); + Ok(()) } } @@ -426,6 +443,7 @@ impl<'a> IrConverter<'a> { Namespace(namespace) => self.namespace(namespace), Comment(comment) => self.comment(comment), ProcessingInstruction(pi) => self.processing_instruction(pi), + CallTemplate(call_template) => self.call_template(call_template), // TODO: xsl:variable does not produce content and is handled // earlier already should be unreachable!() but at this point this // can be reached so return unsupported @@ -560,6 +578,36 @@ impl<'a> IrConverter<'a> { )) } + fn call_template( + &mut self, + call_template: &ast::CallTemplate, + ) -> error::SpannedResult { + if !call_template.with_params.is_empty() { + return Err(error::Error::Unsupported( + "Passing parameters with xsl:with-param is not supported yet".to_string(), + ) + .into()); + } + // A named template is a function taking the context (item, position, + // last); xsl:call-template keeps the caller's context, so forward it. + let context = self.variables.current_context_names().ok_or_else(|| { + error::Error::Unsupported("xsl:call-template outside a template context".to_string()) + })?; + let args = vec![ + Spanned::new(ir::Atom::Variable(context.item), (0..0).into()), + Spanned::new(ir::Atom::Variable(context.position), (0..0).into()), + Spanned::new(ir::Atom::Variable(context.last), (0..0).into()), + ]; + let bindings = Bindings::empty(); + Ok(bindings.bind_expr_no_span( + &mut self.variables, + ir::Expr::CallTemplate(ir::CallTemplate { + name: call_template.name.clone(), + args, + }), + )) + } + fn select_or_sequence_constructor( &mut self, instruction: &impl ast::SelectOrSequenceConstructor, diff --git a/xee-xslt-compiler/tests/test_xslt.rs b/xee-xslt-compiler/tests/test_xslt.rs index 946324eba..5074d94ec 100644 --- a/xee-xslt-compiler/tests/test_xslt.rs +++ b/xee-xslt-compiler/tests/test_xslt.rs @@ -1230,3 +1230,36 @@ fn test_basic_iterate_params() { "124" ); } + +#[test] +fn test_call_named_template() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + + hello +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), "hello"); +} + +#[test] +fn test_call_template_keeps_caller_context() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), "doc"); +}