diff --git a/crates/xml-sec-xslt/tests/engine.rs b/crates/xml-sec-xslt/tests/engine.rs
index f48a17da..c4217e49 100644
--- a/crates/xml-sec-xslt/tests/engine.rs
+++ b/crates/xml-sec-xslt/tests/engine.rs
@@ -1315,9 +1315,18 @@ fn minimum_execution_owned_bytes_for_source(
stylesheet: &xml_sec_xslt::Stylesheet,
source: &Document,
source_bytes: usize,
+) -> usize {
+ minimum_owned_bytes_within(stylesheet, source, execution_budget(source_bytes))
+}
+
+/// The smallest owned-byte ceiling `limits` otherwise admits this execution under.
+fn minimum_owned_bytes_within(
+ stylesheet: &xml_sec_xslt::Stylesheet,
+ source: &Document,
+ limits: ExecutionBudget,
) -> usize {
let succeeds = |owned_bytes| {
- let mut budget = execution_budget(source_bytes);
+ let mut budget = limits;
budget.owned_bytes = owned_bytes;
stylesheet
.execute(
@@ -4540,17 +4549,17 @@ fn sequential_messages_release_temporary_fragment_memory() {
#[test]
fn retained_empty_messages_do_not_accumulate_setup_workspace() {
- // Empty messages retain their metered result entries, but that execution storage must not be
- // added to semantic-path workspace that has already been released after setup.
+ // Empty messages retain their metered result entries, and only those: nothing released
+ // after setup may be counted again for each message.
let count = 64;
let messages = "".repeat(count);
let stylesheet = compile(&format!(
r#"{messages}"#
));
- assert_eq!(
- minimum_execution_owned_bytes(&stylesheet, "messages"),
- minimum_execution_owned_bytes(&stylesheet, "baseline")
- );
+ let messages = minimum_execution_owned_bytes(&stylesheet, "messages");
+ let baseline = minimum_execution_owned_bytes(&stylesheet, "baseline");
+ let per_message = (messages - baseline) / count;
+ assert!(per_message < 512, "{per_message} bytes per message");
}
#[test]
@@ -6234,7 +6243,8 @@ fn function_allocation_failures_unwind_capture_scopes() {
#[test]
fn template_task_stack_does_not_accumulate_setup_workspace() {
// The iterative evaluator meters caller continuations, but the stack is execution-local and
- // must not accumulate semantic-path workspace that was released after setup.
+ // must not accumulate workspace that was released after setup. Each of the 64 frames costs
+ // its own continuation and nothing proportional to the setup.
let templates = (0..64)
.map(|index| {
if index == 63 {
@@ -6252,7 +6262,11 @@ fn template_task_stack_does_not_accumulate_setup_workspace() {
));
let leaf = minimum_execution_owned_bytes(&stylesheet, "leaf");
let chained = minimum_execution_owned_bytes(&stylesheet, "t0");
- assert_eq!(chained, leaf, "setup workspace leaked into task execution");
+ let per_frame = (chained - leaf) / 64;
+ assert!(
+ per_frame < 512,
+ "setup workspace leaked into task execution: {per_frame} bytes per frame"
+ );
}
#[test]
@@ -7044,30 +7058,22 @@ fn numeric_sort_workspace_is_metered_before_allocation() {
let stylesheet = compile(
r#""#,
);
+ let unsorted = compile(
+ r#""#,
+ );
let mut xml = String::from("");
for index in 0..1024 {
write!(xml, r#" "#, 1024 - index).expect("String write");
}
xml.push_str("");
let source = Document::parse(&xml, None).expect("source parses");
- let mut budget = execution_budget(xml.len());
- budget.owned_bytes = 2_200_000;
- assert!(matches!(
- stylesheet.execute(
- &source,
- &Parameters::new(),
- Arc::new(NoResolver),
- ExecutionOptions {
- budget,
- initial_mode: None,
- initial_template: None,
- },
- ),
- Err(Error::Budget {
- kind: BudgetKind::OwnedBytes,
- ..
- })
- ));
+ let sorted_bytes = minimum_execution_owned_bytes_for_source(&stylesheet, &source, xml.len());
+ let unsorted_bytes = minimum_execution_owned_bytes_for_source(&unsorted, &source, xml.len());
+ // At least the two f64 keys of every item, whatever else the sort holds.
+ assert!(
+ sorted_bytes >= unsorted_bytes + 1024 * 16,
+ "sorted {sorted_bytes}, unsorted {unsorted_bytes}"
+ );
}
#[test]
@@ -9927,26 +9933,20 @@ fn generate_id_cache_is_charged_to_retained_owned_memory() {
let stylesheet = compile(
r#""#,
);
- let mut budget = execution_budget(source_xml.len());
- budget.recursion_depth = 512;
- budget.owned_bytes = 1408 * 1024;
- let result = stylesheet.execute(
- &Document::parse(&source_xml, None).expect("deep source parses"),
- &Parameters::new(),
- Arc::new(NoResolver),
- ExecutionOptions {
- budget,
- initial_mode: None,
- initial_template: None,
- },
+ let uncached = compile(
+ r#""#,
+ );
+ let source = Document::parse(&source_xml, None).expect("deep source parses");
+ let mut limits = execution_budget(source_xml.len());
+ limits.recursion_depth = 512;
+ let cached_bytes = minimum_owned_bytes_within(&stylesheet, &source, limits);
+ let uncached_bytes = minimum_owned_bytes_within(&uncached, &source, limits);
+ // The cache keeps every node's path, one word per ancestor.
+ let path_words = (1..=192).sum::();
+ assert!(
+ cached_bytes >= uncached_bytes + path_words * std::mem::size_of::(),
+ "cached {cached_bytes}, uncached {uncached_bytes}"
);
- assert!(matches!(
- result,
- Err(Error::Budget {
- kind: BudgetKind::OwnedBytes,
- ..
- })
- ));
}
#[test]
@@ -11988,32 +11988,20 @@ fn apply_templates_retains_selected_nodes_inside_owned_memory_budget() {
}
#[test]
-fn retained_node_paths_consume_the_execution_memory_budget() {
- // Deep documents retain ancestor paths for cross-model identity; that quadratic storage must
- // remain inside the same OwnedBytes boundary as the semantic and XPath projections.
- let source_xml = format!("{}text{}", "".repeat(220), "".repeat(220));
- let source = Document::parse(&source_xml, None).expect("deep source parses");
+fn cross_model_identity_grows_linearly_with_document_depth() {
+ // Cross-model identity derives paths from the tree instead of retaining one per node,
+ // so a document four times deeper needs about four times the memory, not sixteen.
let stylesheet = compile(
r#""#,
);
- let mut budget = execution_budget(source_xml.len());
- budget.owned_bytes = 512 * 1024;
- assert!(matches!(
- stylesheet.execute(
- &source,
- &Parameters::new(),
- Arc::new(NoResolver),
- ExecutionOptions {
- budget,
- initial_mode: None,
- initial_template: None,
- },
- ),
- Err(Error::Budget {
- kind: BudgetKind::OwnedBytes,
- ..
- })
- ));
+ let minimum = |depth: usize| {
+ let source_xml = format!("{}text{}", "".repeat(depth), "".repeat(depth));
+ let source = Document::parse(&source_xml, None).expect("deep source parses");
+ minimum_execution_owned_bytes_for_source(&stylesheet, &source, source_xml.len())
+ };
+ let shallow = minimum(55);
+ let deep = minimum(220);
+ assert!(deep < shallow * 6, "depth 55: {shallow}, depth 220: {deep}");
}
#[test]
@@ -12044,10 +12032,9 @@ fn generic_large_nodeset_projection_preserves_deep_document_order() {
}
#[test]
-fn generic_large_nodeset_projection_charges_unselected_tree_walks() {
- // Small and large caller-provided sets can force reverse projection to inspect many
- // unselected siblings. Both sides of the 64-node cutoff must meter those walks rather
- // than only the selected result size.
+fn nodeset_projection_costs_the_selected_nodes_not_their_siblings() {
+ // Reverse projection goes straight to each selected node, so the last of 2048 siblings
+ // costs what the first does, and the work charged follows the size of the selection.
let source_xml = format!("{}", " ".repeat(2_048));
let source = Document::parse(&source_xml, None).expect("wide source parses");
for selected_count in [1, 64, 65] {
@@ -12067,10 +12054,9 @@ fn generic_large_nodeset_projection_charges_unselected_tree_walks() {
let stylesheet = compile(
r#""#,
);
- let mut budget = execution_budget(source_xml.len());
- budget.xpath_operations = 512;
-
- assert!(matches!(
+ let run = |xpath_operations| {
+ let mut budget = execution_budget(source_xml.len());
+ budget.xpath_operations = xpath_operations;
stylesheet.execute(
&source,
¶meters,
@@ -12080,7 +12066,13 @@ fn generic_large_nodeset_projection_charges_unselected_tree_walks() {
initial_mode: None,
initial_template: None,
},
- ),
+ )
+ };
+ let output = run(512).expect("the selection fits far below the sibling count");
+ let copies = String::from_utf8(output.serialized.bytes).expect("output is UTF-8");
+ assert_eq!(copies.matches(" ").count(), selected_count);
+ assert!(matches!(
+ run(selected_count - 1),
Err(Error::Budget {
kind: BudgetKind::XPathOperations,
..
@@ -12432,8 +12424,8 @@ fn optimized_child_selection_accounts_for_projected_node_storage() {
#[test]
fn identity_axis_shortcuts_do_not_raise_the_index_dominated_peak() {
- // The source projection indexes dominate the peak for attribute-heavy documents. Identity
- // selection must fit inside that already-accounted peak instead of adding retained storage.
+ // Selecting every attribute of an element pays for the selection itself, one small entry
+ // per attribute, and retains nothing proportional to the attributes' names or paths.
let mut attributes = String::new();
for index in 0..2_048 {
write!(attributes, r#" a{index}="""#).expect("String write");
@@ -12455,7 +12447,8 @@ fn identity_axis_shortcuts_do_not_raise_the_index_dominated_peak() {
source_xml.len(),
"selecting",
);
- assert_eq!(selecting, baseline);
+ let per_attribute = (selecting - baseline) / 2_048;
+ assert!(per_attribute < 64, "{per_attribute} bytes per attribute");
}
#[test]
diff --git a/src/sxd_document/dom_no_unsafe.rs b/src/sxd_document/dom_no_unsafe.rs
index 0b005652..1274a505 100644
--- a/src/sxd_document/dom_no_unsafe.rs
+++ b/src/sxd_document/dom_no_unsafe.rs
@@ -32,6 +32,19 @@ macro_rules! wrapper(
)
);
+// The node in an arena slot, the inverse of the slot an `identity_key` reports.
+macro_rules! at_slot(
+ ($name:ident, $arena:ident, $wrap:ident, $node:ident) => (
+ #[doc = concat!("The ", stringify!($node), " in arena `slot`, if the arena holds one there.")]
+ #[doc(hidden)]
+ #[must_use]
+ pub fn $name(self, slot: usize) -> Option<$node<'d>> {
+ (slot < self.storage.$arena.borrow().len())
+ .then(|| self.$wrap(raw::Index::from_identity_index(slot)))
+ }
+ )
+);
+
impl<'d> Document<'d> {
wrapper!(wrap_root, Root, raw::Root);
wrapper!(wrap_element, Element, raw::Element);
@@ -117,6 +130,16 @@ impl<'d> Document<'d> {
) -> ProcessingInstruction<'d> {
self.wrap_pi(self.storage.create_processing_instruction(target, value))
}
+
+ at_slot!(element_at_slot, elements, wrap_element, Element);
+ at_slot!(text_at_slot, texts, wrap_text, Text);
+ at_slot!(comment_at_slot, comments, wrap_comment, Comment);
+ at_slot!(
+ processing_instruction_at_slot,
+ processing_instructions,
+ wrap_pi,
+ ProcessingInstruction
+ );
}
impl<'d> PartialEq for Document<'d> {
diff --git a/src/sxd_document/raw_no_unsafe.rs b/src/sxd_document/raw_no_unsafe.rs
index f34f2d9a..38a32a51 100644
--- a/src/sxd_document/raw_no_unsafe.rs
+++ b/src/sxd_document/raw_no_unsafe.rs
@@ -101,6 +101,11 @@ impl Index {
self.idx
}
+ /// The handle of arena slot `idx`; the caller checks it is in bounds.
+ pub(super) fn from_identity_index(idx: usize) -> Self {
+ Self::new(idx)
+ }
+
fn new(idx: usize) -> Self {
Index {
idx,
diff --git a/src/xmldsig/transforms/xslt.rs b/src/xmldsig/transforms/xslt.rs
index ed16e053..4bf2c39b 100644
--- a/src/xmldsig/transforms/xslt.rs
+++ b/src/xmldsig/transforms/xslt.rs
@@ -1693,8 +1693,8 @@ fn run_stylesheet(
0,
)
})?;
- let reported = compiled.execute_reported(
- &source,
+ let reported = compiled.execute_owned_reported(
+ source,
&Parameters::new(),
// The query's remainder bounds the work of every kind together, so the engine
// stops where the query would refuse it rather than after the policy allows.
@@ -1705,7 +1705,6 @@ fn run_stylesheet(
initial_template: None,
},
);
- drop(source);
drop(compiled);
// A failed execution spends its work too, and later references and signatures
// of the operation draw on the same allowance. The error reports the totals
diff --git a/src/xmldsig/transforms/xslt/tests.rs b/src/xmldsig/transforms/xslt/tests.rs
index a0e5f6a1..db72a377 100644
--- a/src/xmldsig/transforms/xslt/tests.rs
+++ b/src/xmldsig/transforms/xslt/tests.rs
@@ -288,8 +288,22 @@ fn implicit_c14n_input_stays_in_the_xslt_workspace() {
let scratch = crate::c14n::scratch::Accounted(&memory);
let nodes = crate::xmldsig::types::NodeSet::entire_document_with_comments(&document)
.expect("accounted conversion projection");
- let budget = budget(admitted);
let parameter = super::XsltParameter::Stylesheet(&sheet);
+ // The least workspace the conversion itself runs in, if the engine's minimum is not enough.
+ let (mut refused, mut converts) = (admitted - 1, 1 << 22);
+ while converts - refused > 1 {
+ let middle = refused + (converts - refused) / 2;
+ let attempt = budget(middle);
+ if super::canonicalize_input(parameter, &document, &nodes.projection, &attempt, &scratch)
+ .is_ok()
+ {
+ converts = middle;
+ } else {
+ refused = middle;
+ }
+ }
+ assert!(converts < admitted + xml.len());
+ let budget = budget(converts);
let collected =
super::canonicalize_input(parameter, &document, &nodes.projection, &budget, &scratch)
.expect("conversion alone fits");
@@ -1222,8 +1236,8 @@ fn the_parsed_input_is_charged_to_the_workspace_once() {
let allowance = super::Allowance::new(&ResourcePolicy::default());
let executes = |owned: usize| {
compiled
- .execute_with_environment(
- &tree,
+ .execute_owned_reported(
+ tree.clone(),
&Parameters::new(),
ExecutionEnvironment::new(std::sync::Arc::new(NoResolver)),
ExecutionOptions {
@@ -1232,6 +1246,7 @@ fn the_parsed_input_is_charged_to_the_workspace_once() {
initial_template: None,
},
)
+ .outcome
.is_ok()
};
// The least owned-byte budget the engine runs this execution in.
diff --git a/src/xslt/runtime.rs b/src/xslt/runtime.rs
index 0fc1834a..1ae0423d 100644
--- a/src/xslt/runtime.rs
+++ b/src/xslt/runtime.rs
@@ -24,7 +24,8 @@ use super::lexical::{
use super::serializer::{serialize, serialize_fragment};
use super::xpath::{
CustomCallSession, EXSLT_COMMON_NS, Evaluator, EvaluatorSourceOptions, PreparedEvaluatorSource,
- SourceNode, XPathValue, parse_xpath_number, prepare_evaluator_source, xpath_number,
+ SourceNode, XPathValue, parse_xpath_number, prepare_evaluator_source,
+ prepare_owned_evaluator_source, xpath_number,
};
use super::{
Attribute, BudgetKind, Document, Error, ExecutionBudget, ExecutionEnvironment, ExecutionUsage,
@@ -204,7 +205,39 @@ impl Stylesheet {
environment: ExecutionEnvironment,
options: impl std::borrow::Borrow,
) -> Reported {
- let options = options.borrow();
+ self.run_reported(
+ Cow::Borrowed(source),
+ parameters,
+ environment,
+ options.borrow(),
+ )
+ }
+
+ /// [`Self::execute_reported`] for a caller that has no further use for `source`:
+ /// the execution takes the document instead of paying for its own copy.
+ #[must_use]
+ pub fn execute_owned_reported(
+ &self,
+ source: Document,
+ parameters: &Parameters,
+ environment: ExecutionEnvironment,
+ options: impl std::borrow::Borrow,
+ ) -> Reported {
+ self.run_reported(
+ Cow::Owned(source),
+ parameters,
+ environment,
+ options.borrow(),
+ )
+ }
+
+ fn run_reported(
+ &self,
+ source: Cow<'_, Document>,
+ parameters: &Parameters,
+ environment: ExecutionEnvironment,
+ options: &ExecutionOptions,
+ ) -> Reported {
let Reported { outcome, usage } =
self.start_execution(source, parameters, environment, options);
let mut state = match outcome {
@@ -231,7 +264,7 @@ impl Stylesheet {
fn start_execution(
&self,
- source: &Document,
+ source: Cow<'_, Document>,
parameters: &Parameters,
environment: ExecutionEnvironment,
options: &ExecutionOptions,
@@ -280,7 +313,7 @@ impl Stylesheet {
fn prepare_execution<'s, R: Resolver + 'static>(
&'s self,
- source: &Document,
+ source: Cow<'_, Document>,
parameters: &Parameters,
environment: &ExecutionEnvironment,
meter: &mut Meter,
@@ -305,7 +338,7 @@ impl Stylesheet {
.get(name)
.is_some_and(|global| global.is_parameter)
{
- validate_parameter_value(value, source)?;
+ validate_parameter_value(value, &source)?;
}
}
let source_options = EvaluatorSourceOptions {
@@ -313,12 +346,15 @@ impl Stylesheet {
whitespace: Arc::clone(&self.whitespace),
clock: environment.clock.as_ref().map(Arc::clone),
};
- let mut prepared = prepare_evaluator_source(
- source,
- environment.resolver.as_ref(),
- meter,
- &source_options,
- )?;
+ let resolver = environment.resolver.as_ref();
+ let mut prepared = match source {
+ Cow::Borrowed(source) => {
+ prepare_evaluator_source(source, resolver, meter, &source_options)
+ }
+ Cow::Owned(source) => {
+ prepare_owned_evaluator_source(source, resolver, meter, &source_options)
+ }
+ }?;
let prepared_parameters = PreparedParameters {
effective_globals,
source_remap: SourceParameterRemap {
@@ -388,7 +424,7 @@ impl Stylesheet {
) -> Result> {
let options = options.borrow();
let mut state = self
- .start_execution(source, parameters, environment, options)
+ .start_execution(Cow::Borrowed(source), parameters, environment, options)
.outcome?;
state.run_tree(options)?;
Ok(MeasuredTree {
diff --git a/src/xslt/xpath.rs b/src/xslt/xpath.rs
index 0bf9a3aa..38f83e76 100644
--- a/src/xslt/xpath.rs
+++ b/src/xslt/xpath.rs
@@ -230,7 +230,7 @@ pub(crate) fn prepare_evaluator_source(
options: &EvaluatorSourceOptions,
) -> Result {
let mut resource_identities = HashMap::new();
- let (mut document, include_remap) = if options.process_xinclude {
+ let (document, include_remap) = if options.process_xinclude {
let expanded = expand_xinclude_document(
source,
&XIncludeDocumentIdentity::InMemory(std::ptr::from_ref(source) as usize),
@@ -250,6 +250,30 @@ pub(crate) fn prepare_evaluator_source(
meter.charge(BudgetKind::OwnedBytes, source.estimated_clone_bytes())?;
(source.clone(), None)
};
+ finish_evaluator_source(document, include_remap, resource_identities, meter, options)
+}
+
+/// [`prepare_evaluator_source`] for a document the caller gives up: only `XInclude`
+/// expansion still needs a separate tree, and the caller's account already holds this one.
+pub(crate) fn prepare_owned_evaluator_source(
+ source: Document,
+ resolver: &dyn Resolver,
+ meter: &mut Meter,
+ options: &EvaluatorSourceOptions,
+) -> Result {
+ if options.process_xinclude {
+ return prepare_evaluator_source(&source, resolver, meter, options);
+ }
+ finish_evaluator_source(source, None, HashMap::new(), meter, options)
+}
+
+fn finish_evaluator_source(
+ mut document: Document,
+ include_remap: Option<(HashMap, usize)>,
+ resource_identities: HashMap,
+ meter: &mut Meter,
+ options: &EvaluatorSourceOptions,
+) -> Result {
let (whitespace_remap, whitespace_remap_owned_bytes) =
apply_source_whitespace_rules(&mut document, &options.whitespace, meter)?
.map_or((None, 0), |(remap, bytes)| (Some(remap), bytes));
@@ -505,7 +529,7 @@ struct CachedDocumentRoots {
fn seed_document_cache(
request: DocumentRequest,
root: NodeId,
- maps: &NodeMaps,
+ root_path: Option,
documents: &mut HashMap>,
document_roots: &mut HashMap,
index_bytes: &mut DocumentCacheIndexBytes,
@@ -517,10 +541,10 @@ fn seed_document_cache(
reserve_retained_hash_map_slot(document_roots, meter, &mut index_bytes.roots)?;
let mut roots = Vec::new();
let mut roots_bytes = 0;
- if let Some(path) = maps.forward.get(&root) {
+ if let Some(path) = root_path {
reserve_temporary_vec_slot(&mut roots, meter, &mut roots_bytes)?;
meter.charge(BudgetKind::OwnedBytes, path.owned_bytes())?;
- roots.push(path.clone());
+ roots.push(path);
}
let nodes = vec![root];
meter.charge(
@@ -791,7 +815,7 @@ fn seed_evaluator_documents(
seed_document_cache(
principal_request,
stylesheet_root,
- maps,
+ maps.path(source, &SourceNode::Node(stylesheet_root)),
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
@@ -810,7 +834,7 @@ fn seed_evaluator_documents(
seed_document_cache(
source_request,
source_root,
- maps,
+ maps.path(source, &SourceNode::Node(source_root)),
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
@@ -827,7 +851,7 @@ fn seed_evaluator_documents(
seed_document_cache(
request,
root,
- maps,
+ maps.path(source, &SourceNode::Node(root)),
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
@@ -955,8 +979,9 @@ impl Evaluator {
meter,
)?;
}
- let package = project_semantic_document(&source, meter)?;
- let maps = NodeMaps::new(&source, meter)?;
+ let mut slots = ProjectedSlots::default();
+ let package = project_semantic_document(&source, &mut slots, meter)?;
+ let maps = NodeMaps::new(&source, slots, meter)?;
let mut id_index = Vec::new();
extend_id_index(&source, &maps, 0, &mut id_index, meter)?;
let unparsed_entity_index = build_unparsed_entity_index(&source, meter)?;
@@ -964,7 +989,8 @@ impl Evaluator {
let mut node_base_uri_index_bytes = 0;
extend_node_base_uri_entries(
&source,
- maps.reverse.iter(),
+ &maps,
+ 0,
&mut node_base_uris,
&mut node_base_uri_index_bytes,
meter,
@@ -1088,7 +1114,7 @@ impl Evaluator {
debug_assert_eq!(value_reservation, value.capacity());
let Some(sxd) = self
.maps
- .to_sxd(self.package.as_document().root().into(), node)
+ .to_sxd(&self.source, self.package.as_document(), node)
else {
meter.release_owned_bytes(value_reservation);
return Ok(());
@@ -1221,7 +1247,7 @@ impl Evaluator {
}
pub(crate) fn document_order(&self, mut nodes: Vec) -> Vec {
- nodes.sort_by_key(|node| self.maps.order.get(node).copied());
+ nodes.sort_by_key(|node| self.maps.order(&self.source, node));
nodes.dedup();
nodes
}
@@ -1245,16 +1271,14 @@ impl Evaluator {
};
pending
.map(|pending| {
- let node = self
- .maps
- .reverse
- .get(&pending.node)
- .cloned()
+ let node = NodeMaps::source_of_path(&self.source, &pending.node, meter)?
.ok_or_else(|| Error::Dynamic("stylesheet function context is stale".into()))?;
let arguments = pending
.arguments
.iter()
- .map(|argument| self.project_deferred_value(argument, session, variables))
+ .map(|argument| {
+ self.project_deferred_value(argument, session, variables, meter)
+ })
.collect::>>()?;
Ok(CustomFunctionCall {
name: pending.name.clone(),
@@ -1307,7 +1331,7 @@ impl Evaluator {
Value::Boolean(_) | Value::Number(_) => 0,
Value::String(value) | Value::StoredExpression(value) => value.capacity(),
Value::NodeSet(nodes) => nodes.iter().try_fold(0usize, |total, node| {
- let path = self.maps.forward.get(node).ok_or_else(|| {
+ let path = self.maps.path(&self.source, node).ok_or_else(|| {
Error::Dynamic("stylesheet function result node is stale".into())
})?;
Ok::<_, Error>(total.saturating_add(path.owned_bytes()))
@@ -1328,6 +1352,7 @@ impl Evaluator {
value: &DeferredXPathValue,
session: &CustomCallSession,
variables: &dyn VariableBindings,
+ meter: &mut Meter,
) -> Result {
Ok(match value {
DeferredXPathValue::Boolean(value) => Value::Boolean(*value),
@@ -1337,7 +1362,7 @@ impl Evaluator {
paths
.iter()
.map(|path| {
- self.maps.reverse.get(path).cloned().ok_or_else(|| {
+ NodeMaps::source_of_path(&self.source, path, meter)?.ok_or_else(|| {
Error::Dynamic("stylesheet function argument node is stale".into())
})
})
@@ -1366,7 +1391,7 @@ impl Evaluator {
nodes
.iter()
.map(|node| {
- self.maps.forward.get(node).cloned().ok_or_else(|| {
+ self.maps.path(&self.source, node).ok_or_else(|| {
Error::Dynamic("stylesheet function result node is stale".into())
})
})
@@ -1418,7 +1443,14 @@ impl Evaluator {
.into_iter()
.find_map(sxd_document_no_unsafe::dom::ChildOfRoot::element)
.ok_or_else(|| Error::Dynamic("semantic projection root is missing".into()))?;
- project_logical_root(&self.source, logical_root, sxd_document, documents, meter)?;
+ project_logical_root(
+ &self.source,
+ logical_root,
+ sxd_document,
+ documents,
+ &mut self.maps.slots,
+ meter,
+ )?;
self.maps.extend(&self.source, first_new_node, meter)?;
extend_id_index(
&self.source,
@@ -1435,10 +1467,8 @@ impl Evaluator {
)?;
extend_node_base_uri_entries(
&self.source,
- self.maps
- .reverse
- .iter()
- .filter(|(_, node)| source_node_owner(node).0 >= first_new_node),
+ &self.maps,
+ first_new_node,
&mut self.node_base_uris.borrow_mut(),
&mut self.node_base_uri_index_bytes,
meter,
@@ -1488,12 +1518,12 @@ impl Evaluator {
let mut roots = Vec::new();
let mut roots_index_bytes = 0;
for node in &nodes {
- let Some(path) = self.maps.forward.get(node) else {
+ let Some(path) = self.maps.path(&self.source, node) else {
continue;
};
reserve_temporary_vec_slot(&mut roots, meter, &mut roots_index_bytes)?;
meter.charge(BudgetKind::OwnedBytes, path.owned_bytes())?;
- roots.push(path.clone());
+ roots.push(path);
}
self.document_roots.borrow_mut().insert(
request.clone(),
@@ -2191,6 +2221,8 @@ impl Evaluator {
result
}
+ /// Evaluate with the context node's path paid for in owned memory: the path
+ /// is held for the whole evaluation and released with it.
#[expect(
clippy::too_many_arguments,
reason = "the XPath engine requires every component of its dynamic context"
@@ -2204,6 +2236,38 @@ impl Evaluator {
variables: &dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
+ ) -> Result {
+ let path_bytes = self
+ .maps
+ .path_bytes(&self.source, node)
+ .ok_or_else(|| Error::Dynamic("XPath context node is stale".into()))?;
+ meter.charge(BudgetKind::OwnedBytes, path_bytes)?;
+ let result = self.evaluate_with_context_path(
+ expression,
+ node,
+ position,
+ size,
+ variables,
+ meter,
+ custom_calls,
+ );
+ meter.release_owned_bytes(path_bytes);
+ result
+ }
+
+ #[expect(
+ clippy::too_many_arguments,
+ reason = "the XPath engine requires every component of its dynamic context"
+ )]
+ fn evaluate_with_context_path(
+ &self,
+ expression: &Expression,
+ node: &SourceNode,
+ position: usize,
+ size: usize,
+ variables: &dyn VariableBindings,
+ meter: &mut Meter,
+ custom_calls: Option<&CustomCallSession>,
) -> Result {
let (context_node, context_path) = self.xpath_context_node(node, meter)?;
let uses_current = xpath_uses_current(expression);
@@ -2218,7 +2282,7 @@ impl Evaluator {
.then_some(self.stylesheet_functions.as_ref()),
bind_functions,
uses_current,
- context_path,
+ &context_path,
meter,
)?;
let rewrite = evaluation_rewrite(expression, uses_current);
@@ -2256,7 +2320,7 @@ impl Evaluator {
position,
size,
uses_current,
- context_path,
+ &context_path,
);
if bind_functions {
// XPath function objects must own their static namespace context. Share one
@@ -2353,15 +2417,16 @@ impl Evaluator {
&self,
node: &SourceNode,
meter: &mut Meter,
- ) -> Result<(nodeset::Node<'_>, &NodePath)> {
+ ) -> Result<(nodeset::Node<'_>, NodePath)> {
let document = self.package.as_document();
let context_path = self
.maps
- .forward
- .get(node)
+ .path(&self.source, node)
.ok_or_else(|| Error::Dynamic("XPath context node is stale".into()))?;
meter.charge(BudgetKind::XPathOperations, context_path.ordinary().len())?;
- let context_node = NodeMaps::resolve(document.root().into(), context_path)
+ let context_node = self
+ .maps
+ .to_sxd(&self.source, document, node)
.ok_or_else(|| Error::Dynamic("XPath context node is stale".into()))?;
Ok((context_node, context_path))
}
@@ -2572,7 +2637,7 @@ impl Evaluator {
Value::NodeSet(nodes) => {
let mut set = nodeset::Nodeset::new();
for reference in nodes {
- if let Some(node) = self.maps.to_sxd(document.root().into(), reference) {
+ if let Some(node) = self.maps.to_sxd(&self.source, document, reference) {
if !set.contains(node.clone()) {
let reservation = context
.reserve_temporary_allocation(std::mem::size_of_val(&node));
@@ -2653,12 +2718,7 @@ impl Evaluator {
// the two live representations from independent copies of the same remaining budget.
let live_context_bytes = context_workspace.saturating_add(context.string_allocation_used());
meter.charge(BudgetKind::OwnedBytes, live_context_bytes)?;
- let projected = self.maps.project_value(
- &self.source,
- &self.package.as_document().root().into(),
- value,
- meter,
- );
+ let projected = self.maps.project_value(&self.source, value, meter);
meter.release_owned_bytes(live_context_bytes);
projected
}
@@ -4001,7 +4061,11 @@ fn simple_absolute_element_pattern_path(pattern: &str) -> Option<&str> {
.then_some(remainder)
}
-fn project_semantic_document(source: &Document, meter: &mut Meter) -> Result {
+fn project_semantic_document(
+ source: &Document,
+ slots: &mut ProjectedSlots,
+ meter: &mut Meter,
+) -> Result {
meter.charge(BudgetKind::OwnedBytes, semantic_projection_size(source))?;
let package = Package::new();
let document = package.as_document();
@@ -4009,20 +4073,82 @@ fn project_semantic_document(source: &Document, meter: &mut Meter) -> Result(
+ document: SxdDocument<'d>,
+ kind: &NodeKind,
+ id: NodeId,
+ slots: &mut ProjectedSlots,
+ meter: &mut Meter,
+) -> Result> {
+ let NodeKind::Element {
+ name,
+ prefix,
+ attributes,
+ namespaces,
+ } = kind
+ else {
+ return Err(Error::Dynamic(format!(
+ "semantic node {id:?} is not an element"
+ )));
+ };
+ let element = document.create_element(QName::with_namespace_uri(
+ name.namespace.as_deref(),
+ &name.local,
+ ));
+ element.set_preferred_prefix(prefix.as_deref());
+ for namespace in namespaces.iter() {
+ if let Some(prefix) = &namespace.prefix {
+ element.register_prefix(prefix, &namespace.uri);
+ } else {
+ element.set_default_namespace_uri(Some(&namespace.uri));
+ element.register_prefix("", &namespace.uri);
+ }
+ }
+ for (index, attribute) in attributes.iter().enumerate() {
+ let projected = element.set_attribute_value(
+ QName::with_namespace_uri(attribute.name.namespace.as_deref(), &attribute.name.local),
+ &attribute.value,
+ );
+ projected.set_preferred_prefix(attribute.prefix.as_deref());
+ ProjectedSlots::record(
+ &mut slots.attributes,
+ projected.identity_key(),
+ (id, index),
+ meter,
+ )?;
+ }
+ slots.record_node(
+ |slots| &mut slots.elements,
+ element.identity_key(),
+ id,
+ meter,
+ )?;
+ Ok(element)
+}
+
fn project_logical_root<'d>(
source: &Document,
logical_root: NodeId,
document: SxdDocument<'d>,
documents: SxdElement<'d>,
+ slots: &mut ProjectedSlots,
meter: &mut Meter,
) -> Result<()> {
let wrapper = document.create_element(QName::new(DOCUMENT_ELEMENT));
documents.append_child(wrapper);
+ slots.record_node(
+ |slots| &mut slots.elements,
+ wrapper.identity_key(),
+ logical_root,
+ meter,
+ )?;
let root = source
.node(logical_root)
.ok_or_else(|| Error::Dynamic("missing logical document root".into()))?;
@@ -4044,45 +4170,40 @@ fn project_logical_root<'d>(
pending.push((*child, parent));
}
}
- NodeKind::Element {
- name,
- prefix,
- attributes,
- namespaces,
- } => {
- let element = document.create_element(QName::with_namespace_uri(
- name.namespace.as_deref(),
- &name.local,
- ));
- element.set_preferred_prefix(prefix.as_deref());
- for namespace in namespaces.iter() {
- if let Some(prefix) = &namespace.prefix {
- element.register_prefix(prefix, &namespace.uri);
- } else {
- element.set_default_namespace_uri(Some(&namespace.uri));
- element.register_prefix("", &namespace.uri);
- }
- }
- for attribute in attributes {
- let projected = element.set_attribute_value(
- QName::with_namespace_uri(
- attribute.name.namespace.as_deref(),
- &attribute.name.local,
- ),
- &attribute.value,
- );
- projected.set_preferred_prefix(attribute.prefix.as_deref());
- }
+ NodeKind::Element { .. } => {
+ let element = project_element(document, &node.kind, id, slots, meter)?;
parent.append_child(element);
for child in node.children.iter().rev() {
reserve_temporary_vec_slot(&mut pending, meter, &mut pending_reservation)?;
pending.push((*child, element));
}
}
- NodeKind::Text { value, .. } => parent.append_child(document.create_text(value)),
- NodeKind::Comment(value) => parent.append_child(document.create_comment(value)),
- NodeKind::ProcessingInstruction { target, value } => parent
- .append_child(document.create_processing_instruction(target, value.as_deref())),
+ NodeKind::Text { value, .. } => {
+ let text = document.create_text(value);
+ parent.append_child(text);
+ slots.record_node(|slots| &mut slots.texts, text.identity_key(), id, meter)?;
+ }
+ NodeKind::Comment(value) => {
+ let comment = document.create_comment(value);
+ parent.append_child(comment);
+ slots.record_node(
+ |slots| &mut slots.comments,
+ comment.identity_key(),
+ id,
+ meter,
+ )?;
+ }
+ NodeKind::ProcessingInstruction { target, value } => {
+ let instruction =
+ document.create_processing_instruction(target, value.as_deref());
+ parent.append_child(instruction);
+ slots.record_node(
+ |slots| &mut slots.instructions,
+ instruction.identity_key(),
+ id,
+ meter,
+ )?;
+ }
}
}
Ok(())
@@ -6163,28 +6284,86 @@ fn decode_resource_metered_inner(
})
}
+/// The semantic node behind each SXD arena slot, per arena. Projection fills
+/// these, so a node the evaluator returns maps back without walking its siblings.
+/// Every evaluator node lives in the one projection document (result fragments
+/// and loaded documents are imported into the semantic document and projected
+/// there), so the arena slot alone identifies it.
+#[derive(Default)]
+struct ProjectedSlots {
+ elements: Vec