mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
* perf(rvm): fix O(n²) comprehension yield by mutating in-place Instead of cloning the entire accumulator collection on every yield iteration, use take_register + Rc::make_mut to get exclusive ownership and mutate in-place. This reduces comprehension yield from O(n²) to O(n) for both run-to-completion and suspendable execution modes. - Add RegoVM::take_register() helper that swaps register with Undefined - Comprehension yield now takes the accumulator, mutates via Rc::make_mut, and writes back — avoiding deep clones when refcount == 1 Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * perf(rvm): use take_register for ObjectSet, ArrayPush, SetAdd These instructions were cloning the container register (bumping Rc to 2), then calling as_object_mut/as_array_mut/as_set_mut which invokes Rc::make_mut — deep-cloning the entire collection since refcount > 1. Use take_register instead so the Rc refcount stays at 1, making Rc::make_mut a no-op and allowing in-place mutation. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * perf(rvm): remove unnecessary clones in rule caching - execute_call_rule_common: move final_value into cache instead of cloning, since it is not used afterwards - finalize_rule_frame_data: add comment clarifying the clone is needed because the value is both cached and returned - Remove unnecessary .clone() on result_from_rule when setting register Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * rvm: avoid RuleInfo clone per rule call Replace RuleInfo.clone() (which heap-allocates name, destructuring_blocks, and potentially function_info) with a cheap Arc<Program> clone (atomic refcount bump) followed by borrowing &RuleInfo from the local Arc. This eliminates per-rule-call heap allocations. Sites changed: - execute_call_rule_common: Arc clone + borrow - execute_call_rule_suspendable: Arc clone + borrow - finalize_rule_frame_data: Arc clone + borrow - handle_rule_break_event: inline Arc clone + borrow (was get_rule_info) - handle_rule_error_event: inline Arc clone + borrow (was get_rule_info) - Removed now-unused get_rule_info method Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * rvm: replace bincode with postcard for serialization Remove unlinked bincode dependency. Use postcard (already a dep for rvm feature) for all binary serialization/deserialization in program serialization and tests. Also adds rvm_benchmark benchmark. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * perf(rvm): cache dummy Span/Expr for builtin calls Every builtin call was allocating a Source (via from_contents), a Span, and N Ref<Expr> wrappers just to satisfy the builtin function signature. These dummy values are only used for error reporting context. Cache the dummy Span and Vec<Ref<Expr>> on the RegoVM struct. The Source and Span are created once on first builtin call; dummy Expr entries grow as needed and are reused across calls via mem::take/put-back pattern. This eliminates per-builtin-call heap allocations for Source (Rc + String + Vec<lines>), Span clones, and Rc<Expr> wrappers. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * perf(rvm): round 2 allocation reduction in builtins, entry points, virtual data - Cache builtin args Vec on RegoVM (mem::take/clear/put-back pattern) - Restructure builtins_cache as two-level map for clone-free lookup - Use IndexMap::get_index() in execute_entry_point_by_index - Use mutable Vec path stack in traverse_rule_tree_subobject (push/pop) - Walk data tree and rule-result paths by reference, clone only leaf - Use mem::replace in resume() instead of cloning ExecutionState Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix(rvm): address PR review feedback - Restore cached_builtin_args on all error/early-return paths in execute_builtin_call to preserve allocation reuse - Use 1-based line/col and \"<builtin>\" filename in dummy span for clearer diagnostics - Restore result register before returning errors in comprehension mode-mismatch branches (both run-to-completion and suspendable) - Avoid clone in resume() invalid-state error path by formatting debug string before moving state back --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
313 lines
12 KiB
Rust
313 lines
12 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
use crate::rvm::instructions::LiteralOrRegister;
|
|
use crate::value::Value;
|
|
use alloc::vec::Vec;
|
|
use core::convert::TryFrom as _;
|
|
|
|
use super::errors::{Result, VmError};
|
|
use super::machine::RegoVM;
|
|
|
|
impl RegoVM {
|
|
pub(super) fn execute_virtual_data_document_lookup_subobject(
|
|
&mut self,
|
|
path_components: &[LiteralOrRegister],
|
|
rule_tree_subobject: &Value,
|
|
) -> Result<Value> {
|
|
let mut root_path = Vec::new();
|
|
for component in path_components {
|
|
let key_value = self.literal_or_register_value(component)?;
|
|
root_path.push(key_value);
|
|
}
|
|
|
|
// Walk data tree by reference, only clone the leaf.
|
|
let mut data_ref = &self.data;
|
|
for path_component in &root_path {
|
|
data_ref = &data_ref[path_component];
|
|
}
|
|
|
|
let mut result_subobject = match *data_ref {
|
|
Value::Undefined => Value::new_object(),
|
|
_ => data_ref.clone(),
|
|
};
|
|
|
|
self.traverse_rule_tree_subobject(rule_tree_subobject, &mut result_subobject, &root_path)?;
|
|
|
|
Ok(result_subobject)
|
|
}
|
|
|
|
fn set_nested_value(target: &mut Value, path: &[Value], value: Value) -> Result<()> {
|
|
Self::set_nested_value_static(target, path, value)
|
|
}
|
|
|
|
fn set_nested_value_static(target: &mut Value, path: &[Value], value: Value) -> Result<()> {
|
|
let Some((head, tail)) = path.split_first() else {
|
|
*target = value;
|
|
return Ok(());
|
|
};
|
|
|
|
if *target == Value::Undefined {
|
|
*target = Value::new_object();
|
|
}
|
|
|
|
if let Value::Object(ref mut map) = *target {
|
|
if !map.contains_key(head) {
|
|
crate::Rc::make_mut(map).insert(head.clone(), Value::Undefined);
|
|
}
|
|
|
|
if let Some(next_target) = crate::Rc::make_mut(map).get_mut(head) {
|
|
Self::set_nested_value_static(next_target, tail, value)?;
|
|
}
|
|
} else {
|
|
return Err(VmError::InvalidRuleTreeEntry {
|
|
value: target.clone(),
|
|
pc: 0,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn traverse_rule_tree_subobject(
|
|
&mut self,
|
|
rule_tree_node: &Value,
|
|
result_subobject: &mut Value,
|
|
root_path: &[Value],
|
|
) -> Result<()> {
|
|
let mut relative_path = Vec::new();
|
|
self.traverse_rule_tree_subobject_with_path(
|
|
rule_tree_node,
|
|
result_subobject,
|
|
root_path,
|
|
&mut relative_path,
|
|
)
|
|
}
|
|
|
|
fn traverse_rule_tree_subobject_with_path(
|
|
&mut self,
|
|
rule_tree_node: &Value,
|
|
result_subobject: &mut Value,
|
|
root_path: &[Value],
|
|
relative_path: &mut Vec<Value>,
|
|
) -> Result<()> {
|
|
match *rule_tree_node {
|
|
Value::Number(ref rule_idx) => {
|
|
if let Some(rule_index) = rule_idx.as_u64() {
|
|
// Walk the evaluated cache using root_path then relative_path,
|
|
// without allocating a combined full_cache_path Vec.
|
|
let cached_result = {
|
|
let mut cache_lookup = &self.evaluated;
|
|
let mut path_exists = true;
|
|
|
|
for path_component in root_path.iter().chain(relative_path.iter()) {
|
|
if let Value::Object(ref map) = *cache_lookup {
|
|
if let Some(next_value) = map.get(path_component) {
|
|
cache_lookup = next_value;
|
|
} else {
|
|
path_exists = false;
|
|
break;
|
|
}
|
|
} else {
|
|
path_exists = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if path_exists {
|
|
if let Value::Object(ref map) = *cache_lookup {
|
|
map.get(&Value::Undefined).cloned()
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
let rule_result = if let Some(cached) = cached_result {
|
|
self.cache_hits =
|
|
self.checked_add_one(self.cache_hits, "cache hits counter")?;
|
|
cached
|
|
} else {
|
|
let temp_reg = u8::try_from(self.registers.len()).map_err(|_| {
|
|
VmError::RegisterIndexOutOfBounds {
|
|
index: u8::MAX,
|
|
pc: self.pc,
|
|
register_count: self.registers.len(),
|
|
}
|
|
})?;
|
|
self.registers.push(Value::Undefined);
|
|
let rule_index_u16 =
|
|
u16::try_from(rule_index).map_err(|_| VmError::InvalidRuleIndex {
|
|
rule_index: Value::Number(rule_idx.clone()),
|
|
pc: self.pc,
|
|
})?;
|
|
self.execute_call_rule_common(temp_reg, rule_index_u16, None)?;
|
|
let register_count = self.registers.len();
|
|
let result =
|
|
self.registers
|
|
.pop()
|
|
.ok_or(VmError::RegisterIndexOutOfBounds {
|
|
index: temp_reg,
|
|
pc: self.pc,
|
|
register_count,
|
|
})?;
|
|
|
|
// Build cache_path from root_path + relative_path + Undefined sentinel.
|
|
let mut cache_path: Vec<Value> = Vec::with_capacity(
|
|
root_path
|
|
.len()
|
|
.saturating_add(relative_path.len())
|
|
.saturating_add(1),
|
|
);
|
|
cache_path.extend_from_slice(root_path);
|
|
cache_path.extend_from_slice(relative_path);
|
|
cache_path.push(Value::Undefined);
|
|
Self::set_nested_value_static(
|
|
&mut self.evaluated,
|
|
&cache_path,
|
|
result.clone(),
|
|
)?;
|
|
|
|
result
|
|
};
|
|
|
|
Self::set_nested_value(result_subobject, relative_path, rule_result)?;
|
|
} else {
|
|
return Err(VmError::InvalidRuleIndex {
|
|
rule_index: Value::Number(rule_idx.clone()),
|
|
pc: self.pc,
|
|
});
|
|
}
|
|
}
|
|
Value::Object(ref obj) => {
|
|
for (key, value) in obj.iter() {
|
|
relative_path.push(key.clone());
|
|
self.traverse_rule_tree_subobject_with_path(
|
|
value,
|
|
result_subobject,
|
|
root_path,
|
|
relative_path,
|
|
)?;
|
|
relative_path.pop();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn execute_virtual_data_document_lookup(&mut self, params_index: u16) -> Result<()> {
|
|
let params = self
|
|
.program
|
|
.instruction_data
|
|
.get_virtual_data_document_lookup_params(params_index)
|
|
.ok_or(VmError::InvalidVirtualDataDocumentLookupParams {
|
|
index: params_index,
|
|
pc: self.pc,
|
|
available: self
|
|
.program
|
|
.instruction_data
|
|
.virtual_data_document_lookup_params
|
|
.len(),
|
|
})?
|
|
.clone();
|
|
|
|
let mut current_node = &self.program.rule_tree["data"];
|
|
let mut components_consumed = 0;
|
|
|
|
for (i, component) in params.path_components.iter().enumerate() {
|
|
let key_value = self.literal_or_register_value(component)?;
|
|
|
|
current_node = ¤t_node[&key_value];
|
|
components_consumed = self.checked_add_one(i, "path components traversed")?;
|
|
|
|
match *current_node {
|
|
Value::Undefined | Value::Number(_) => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
match *current_node {
|
|
Value::Number(ref rule_index_value) => {
|
|
if let Some(rule_index) = rule_index_value.as_u64() {
|
|
let rule_index =
|
|
u16::try_from(rule_index).map_err(|_| VmError::InvalidRuleIndex {
|
|
rule_index: Value::Number(rule_index_value.clone()),
|
|
pc: self.pc,
|
|
})?;
|
|
|
|
self.execute_call_rule_common(params.dest, rule_index, None)?;
|
|
|
|
if components_consumed < params.path_components.len() {
|
|
// Walk remaining path by reference, clone only the leaf.
|
|
let mut ref_val = self.get_register(params.dest)?;
|
|
|
|
for component in params.path_components.iter().skip(components_consumed) {
|
|
let key_value = self.literal_or_register_value(component)?;
|
|
ref_val = &ref_val[&key_value];
|
|
}
|
|
|
|
let leaf = ref_val.clone();
|
|
self.set_register(params.dest, leaf)?;
|
|
}
|
|
} else {
|
|
return Err(VmError::InvalidRuleIndex {
|
|
rule_index: Value::Number(rule_index_value.clone()),
|
|
pc: self.pc,
|
|
});
|
|
}
|
|
}
|
|
Value::Undefined | Value::Object(_)
|
|
if components_consumed != params.path_components.len() =>
|
|
{
|
|
// Walk data tree by reference, clone only the leaf.
|
|
let mut data_ref = &self.data;
|
|
|
|
for component in ¶ms.path_components {
|
|
let key_value = self.literal_or_register_value(component)?;
|
|
data_ref = &data_ref[&key_value];
|
|
}
|
|
|
|
let result = data_ref.clone();
|
|
self.set_register(params.dest, result)?;
|
|
}
|
|
Value::Object(_) => {
|
|
let rule_tree_subobject = current_node.clone();
|
|
|
|
let result = self.execute_virtual_data_document_lookup_subobject(
|
|
¶ms.path_components,
|
|
&rule_tree_subobject,
|
|
)?;
|
|
self.set_register(params.dest, result)?;
|
|
}
|
|
_ => {
|
|
return Err(VmError::InvalidRuleTreeEntry {
|
|
value: current_node.clone(),
|
|
pc: self.pc,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn literal_or_register_value(&self, source: &LiteralOrRegister) -> Result<Value> {
|
|
let value = match *source {
|
|
LiteralOrRegister::Literal(ref idx) => self
|
|
.program
|
|
.literals
|
|
.get(usize::from(*idx))
|
|
.ok_or(VmError::LiteralIndexOutOfBounds {
|
|
index: *idx,
|
|
pc: self.pc,
|
|
})?
|
|
.clone(),
|
|
LiteralOrRegister::Register(ref reg) => self.get_register(*reg)?.clone(),
|
|
};
|
|
|
|
Ok(value)
|
|
}
|
|
}
|