mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Builds on #57. Swap Value::Object's payload from Rc<BTreeMap<Value, Value>> to Rc<Object> and migrate all call sites to the Object API. as_object / as_object_mut keep their names but return &Object / &mut Object. The mutable accessor handles Rc::make_mut internally, so callers no longer do it themselves. Object grows into_value() and From<Object> for Value. Value's serializer now delegates to Object::serialize, dropping a duplicate non-string-key stringification path. RVM IterationState::Object is rewritten around ObjectCursor: O(log n) steps over a shared Rc<Object>, no eager pair snapshot. Snapshot independence is preserved by Rc copy-on-write; setup_next_iteration advances the cursor inline and advance() becomes a no-op for this variant. A new iteration_state_object_is_snapshot_independent_of_source test covers CoW against a mutated alias. Value::Set still wraps Rc<BTreeSet<Value>>; the matching Set abstraction and its swap ship in follow-up PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
203 lines
7.1 KiB
Rust
203 lines
7.1 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
use crate::rvm::instructions::{ComprehensionMode, LoopMode};
|
|
use crate::value::Value;
|
|
use crate::value::{Object, ObjectCursor};
|
|
use crate::Rc;
|
|
use alloc::collections::BTreeSet;
|
|
use alloc::vec::Vec;
|
|
|
|
/// Loop execution context for managing iteration state
|
|
#[derive(Debug, Clone)]
|
|
pub struct LoopContext {
|
|
pub mode: LoopMode,
|
|
pub iteration_state: IterationState,
|
|
pub key_reg: u8,
|
|
pub value_reg: u8,
|
|
pub result_reg: u8,
|
|
pub body_start: u16,
|
|
pub loop_end: u16,
|
|
pub loop_next_pc: u16, // PC of the LoopNext instruction to avoid searching
|
|
pub body_resume_pc: usize,
|
|
pub success_count: usize,
|
|
pub total_iterations: usize,
|
|
pub current_iteration_failed: bool, // Track if current iteration had condition failures
|
|
}
|
|
|
|
/// Iterator state for different collection types.
|
|
///
|
|
/// Snapshot independence for `Object` is provided by the shared
|
|
/// `Rc<Object>` — `Rc::make_mut` on an aliased Rc allocates a new
|
|
/// collection, leaving the iterator's Rc pointing at the original
|
|
/// pre-mutation state. The `ObjectCursor` is opaque and resumes in
|
|
/// O(log n) for the BTree backend.
|
|
///
|
|
/// `Set` continues to use the pre-existing snapshot-by-cloned-key
|
|
/// approach (`current_item` + `first_iteration`); migration of `Set`
|
|
/// to a cursor-based iterator ships with the `Set` storage abstraction
|
|
/// in a follow-up PR.
|
|
#[derive(Debug, Clone)]
|
|
pub enum IterationState {
|
|
Array {
|
|
items: Rc<Vec<Value>>,
|
|
index: usize,
|
|
},
|
|
Object {
|
|
obj: Rc<Object>,
|
|
cursor: ObjectCursor,
|
|
},
|
|
Set {
|
|
items: Rc<BTreeSet<Value>>,
|
|
current_item: Option<Value>,
|
|
first_iteration: bool,
|
|
},
|
|
/// Virtual single-element iteration for non-collection values.
|
|
/// Used by Azure Policy's `[*]` on scalar/null fields: presents a single
|
|
/// "virtual" element to iterate over, which is always `Null` regardless
|
|
/// of the underlying source value.
|
|
Single {
|
|
consumed: bool,
|
|
},
|
|
}
|
|
|
|
impl IterationState {
|
|
pub(super) const fn advance(&mut self) {
|
|
match *self {
|
|
Self::Array { ref mut index, .. } => {
|
|
// Array iteration uses `usize` as the cursor and advances via
|
|
// `saturating_add(1)`. A cursor already at `usize::MAX` here
|
|
// means a stuck (non-progressing) iteration was emitted by
|
|
// malformed bytecode; assert in debug to surface it loudly.
|
|
debug_assert!(
|
|
*index < usize::MAX,
|
|
"IterationState::Array index already at usize::MAX on advance"
|
|
);
|
|
*index = index.saturating_add(1);
|
|
}
|
|
// For Object the cursor advances inside `setup_next_iteration`
|
|
// when it pulls the next item via `Object::next`, so `advance`
|
|
// is a no-op for the cursor-backed Object variant.
|
|
Self::Object { .. } => {}
|
|
Self::Set {
|
|
ref mut first_iteration,
|
|
..
|
|
} => {
|
|
*first_iteration = false;
|
|
}
|
|
Self::Single {
|
|
ref mut consumed, ..
|
|
} => {
|
|
// `Single` yields exactly once; advancing a consumed Single
|
|
// means the compiler emitted a redundant LoopNext.
|
|
debug_assert!(
|
|
!*consumed,
|
|
"IterationState::Single advanced after consumption"
|
|
);
|
|
*consumed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct CallRuleContext {
|
|
pub return_pc: usize,
|
|
pub dest_reg: u8,
|
|
pub result_reg: u8,
|
|
pub rule_index: u16,
|
|
pub rule_type: crate::rvm::program::RuleType,
|
|
pub current_definition_index: usize,
|
|
pub current_body_index: usize,
|
|
}
|
|
|
|
/// Context for tracking active comprehensions
|
|
#[derive(Debug, Clone)]
|
|
pub(super) struct ComprehensionContext {
|
|
/// Type of comprehension (Array, Set, Object)
|
|
pub(super) mode: ComprehensionMode,
|
|
/// Register storing the comprehension result collection
|
|
pub(super) result_reg: u8,
|
|
/// Register holding the current iteration key
|
|
pub(super) key_reg: u8,
|
|
/// Register holding the current iteration value
|
|
pub(super) value_reg: u8,
|
|
/// Jump target for comprehension body start
|
|
pub(super) body_start: u16,
|
|
/// Jump target for comprehension end
|
|
pub(super) comprehension_end: u16,
|
|
/// Iteration state when comprehension manages iteration itself (None when driven by LoopStart/LoopNext)
|
|
pub(super) iteration_state: Option<IterationState>,
|
|
/// Resume location for the parent frame once this comprehension completes
|
|
pub(super) resume_pc: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(
|
|
clippy::expect_used,
|
|
clippy::unwrap_used,
|
|
clippy::unreachable,
|
|
clippy::pattern_type_mismatch,
|
|
clippy::shadow_unrelated,
|
|
clippy::panic
|
|
)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::value::Object;
|
|
|
|
/// IterationState::Object holds an `Rc<Object>` plus an opaque cursor.
|
|
/// Mutating an aliased Rc via `Rc::make_mut` allocates a new collection
|
|
/// (CoW) so the in-flight iterator's source is unaffected.
|
|
#[test]
|
|
fn iteration_state_object_is_snapshot_independent_of_source() {
|
|
let mut obj = Object::new();
|
|
obj.insert(Value::from("a"), Value::from(1));
|
|
obj.insert(Value::from("b"), Value::from(2));
|
|
obj.insert(Value::from("c"), Value::from(3));
|
|
|
|
let source = Value::Object(Rc::new(obj));
|
|
|
|
let snapshot_obj = match &source {
|
|
Value::Object(o) => Rc::clone(o),
|
|
_ => unreachable!(),
|
|
};
|
|
let state = IterationState::Object {
|
|
obj: Rc::clone(&snapshot_obj),
|
|
cursor: snapshot_obj.cursor(),
|
|
};
|
|
|
|
// Mutate a clone of the source mid-iteration.
|
|
let mut alias = source.clone();
|
|
let inner = alias.as_object_mut().expect("object");
|
|
inner.insert(Value::from("a"), Value::from(999));
|
|
inner.insert(Value::from("d"), Value::from(4));
|
|
inner.remove(&Value::from("b"));
|
|
|
|
// Drain the snapshot via the cursor — must still report the original
|
|
// 3 entries with original values.
|
|
let mut collected: Vec<(Value, Value)> = Vec::new();
|
|
if let IterationState::Object {
|
|
ref obj,
|
|
mut cursor,
|
|
} = state
|
|
{
|
|
while let Some((k, v)) = obj.next(&mut cursor) {
|
|
collected.push((k.clone(), v.clone()));
|
|
}
|
|
} else {
|
|
unreachable!();
|
|
}
|
|
assert_eq!(collected.len(), 3);
|
|
assert!(collected.contains(&(Value::from("a"), Value::from(1))));
|
|
assert!(collected.contains(&(Value::from("b"), Value::from(2))));
|
|
assert!(collected.contains(&(Value::from("c"), Value::from(3))));
|
|
assert!(!collected.iter().any(|kv| kv.0 == Value::from("d")));
|
|
|
|
// The original source Value (untouched) is also unchanged.
|
|
let src_obj = source.as_object().expect("object");
|
|
assert_eq!(src_obj.len(), 3);
|
|
assert_eq!(src_obj.get(&Value::from("a")), Some(&Value::from(1)));
|
|
}
|
|
}
|