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>
158 lines
5.6 KiB
Rust
158 lines
5.6 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
use crate::builtins;
|
|
use crate::value::Value;
|
|
|
|
use super::errors::{Result, VmError};
|
|
use super::execution_model::ExecutionMode;
|
|
use super::machine::RegoVM;
|
|
|
|
impl RegoVM {
|
|
pub(super) fn execute_function_call(&mut self, params_index: u16) -> Result<()> {
|
|
let params = self
|
|
.program
|
|
.instruction_data
|
|
.get_function_call_params(params_index)
|
|
.cloned()
|
|
.ok_or(VmError::InvalidFunctionCallParamsIndex {
|
|
index: params_index,
|
|
pc: self.pc,
|
|
available: self.program.instruction_data.function_call_params.len(),
|
|
})?;
|
|
let call_result = match self.execution_mode {
|
|
ExecutionMode::RunToCompletion => {
|
|
self.execute_call_rule_common(params.dest, params.func_rule_index, Some(¶ms))
|
|
}
|
|
ExecutionMode::Suspendable => self.execute_call_rule_suspendable(
|
|
params.dest,
|
|
params.func_rule_index,
|
|
Some(¶ms),
|
|
),
|
|
};
|
|
|
|
call_result?;
|
|
|
|
self.memory_check()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn execute_builtin_call(&mut self, params_index: u16) -> Result<()> {
|
|
let program = self.program.clone();
|
|
let params = program
|
|
.instruction_data
|
|
.get_builtin_call_params(params_index)
|
|
.ok_or(VmError::InvalidBuiltinCallParamsIndex {
|
|
index: params_index,
|
|
pc: self.pc,
|
|
available: program.instruction_data.builtin_call_params.len(),
|
|
})?
|
|
.clone();
|
|
let builtin_info = program.get_builtin_info(params.builtin_index).ok_or(
|
|
VmError::InvalidBuiltinInfoIndex {
|
|
index: params.builtin_index,
|
|
pc: self.pc,
|
|
available: program.builtin_info_table.len(),
|
|
},
|
|
)?;
|
|
|
|
let mut args = core::mem::take(&mut self.cached_builtin_args);
|
|
args.clear();
|
|
for &arg_reg in params.arg_registers().iter() {
|
|
let arg_value = self.get_register(arg_reg)?.clone();
|
|
args.push(arg_value);
|
|
}
|
|
|
|
let expected_args = builtin_info.num_args;
|
|
let actual_args = args.len();
|
|
if u16::try_from(actual_args).unwrap_or(u16::MAX) != expected_args {
|
|
self.cached_builtin_args = args;
|
|
return Err(VmError::BuiltinArgumentMismatch {
|
|
expected: expected_args,
|
|
actual: actual_args,
|
|
pc: self.pc,
|
|
});
|
|
}
|
|
|
|
if args.iter().any(|a| a == &Value::Undefined) {
|
|
self.cached_builtin_args = args;
|
|
self.set_register(params.dest, Value::Undefined)?;
|
|
self.memory_check()?;
|
|
return Ok(());
|
|
}
|
|
|
|
// Extract everything we need from program before releasing the borrow.
|
|
let builtin_fn = match program.get_resolved_builtin(params.builtin_index) {
|
|
Some(fcn) => fcn.0,
|
|
None => {
|
|
self.cached_builtin_args = args;
|
|
return Err(VmError::BuiltinNotResolved {
|
|
name: builtin_info.name.clone(),
|
|
pc: self.pc,
|
|
});
|
|
}
|
|
};
|
|
let cache_name = builtins::must_cache(builtin_info.name.as_str());
|
|
drop(program);
|
|
|
|
self.ensure_dummy_exprs(args.len())?;
|
|
let dummy_span = self.get_dummy_span()?.clone();
|
|
// Take the dummy_exprs vec out of self so we can pass it to the builtin
|
|
// while still calling &mut self methods afterwards.
|
|
let dummy_exprs = core::mem::take(&mut self.dummy_exprs);
|
|
|
|
if let Some(name) = cache_name {
|
|
if let Some(entries) = self.builtins_cache.get(name) {
|
|
for entry in entries {
|
|
if entry.0.as_slice() == args.as_slice() {
|
|
let cached = entry.1.clone();
|
|
self.dummy_exprs = dummy_exprs;
|
|
self.cached_builtin_args = args;
|
|
self.set_register(params.dest, cached)?;
|
|
self.memory_check()?;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let result = match builtin_fn(
|
|
&dummy_span,
|
|
dummy_exprs.get(..args.len()).unwrap_or(&[]),
|
|
&args,
|
|
self.strict_builtin_errors,
|
|
) {
|
|
Ok(value) => value,
|
|
Err(_) if !self.strict_builtin_errors => Value::Undefined,
|
|
Err(err) => {
|
|
self.dummy_exprs = dummy_exprs;
|
|
self.cached_builtin_args = args;
|
|
return Err(err.into());
|
|
}
|
|
};
|
|
|
|
// Put dummy_exprs back for reuse.
|
|
self.dummy_exprs = dummy_exprs;
|
|
|
|
if let Some(name) = cache_name {
|
|
// Move args into the cache. The now-empty (zero-capacity) Vec is
|
|
// stored back in cached_builtin_args; the next call will re-allocate.
|
|
// This is acceptable because cache inserts are rare (once per unique
|
|
// argument set) while the hot path (cache hit above) reuses the Vec.
|
|
let cache_args = core::mem::take(&mut args);
|
|
self.cached_builtin_args = args;
|
|
self.builtins_cache
|
|
.entry(name)
|
|
.or_default()
|
|
.push((cache_args, result.clone()));
|
|
self.set_register(params.dest, result)?;
|
|
} else {
|
|
self.cached_builtin_args = args;
|
|
self.set_register(params.dest, result)?;
|
|
}
|
|
|
|
self.memory_check()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|