Files
regorus/src/rvm/vm/functions.rs
Anand Krishnamoorthi 50c0215fdb Rvm optimizations (#620)
* 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>
2026-03-11 21:39:58 -05:00

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(&params))
}
ExecutionMode::Suspendable => self.execute_call_rule_suspendable(
params.dest,
params.func_rule_index,
Some(&params),
),
};
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(())
}
}