radixdb-procedural 1.1.0

Verified bounded procedural runtime contracts for RadixDB
Documentation
use radixdb_core::Value;

use crate::{
    BudgetOwner, Diagnostic, DiagnosticKind, ProceduralResult, RuntimeType, RuntimeValue, SlotId,
    SourceSpan,
};

pub(super) struct Frame {
    definitions: Vec<crate::SlotDefinition>,
    slots: Vec<Option<RuntimeValue>>,
    heap_bytes: u64,
    budget: BudgetOwner,
}

impl Frame {
    pub(super) fn new(definitions: &[crate::SlotDefinition], budget: BudgetOwner) -> Self {
        let slots = definitions
            .iter()
            .map(|definition| {
                matches!(definition.runtime_type(), RuntimeType::Collection { .. })
                    .then(|| RuntimeValue::Collection(Vec::new()))
            })
            .collect();
        Self {
            definitions: definitions.to_vec(),
            slots,
            heap_bytes: 0,
            budget,
        }
    }

    pub(super) fn slot_type(&self, slot: SlotId) -> ProceduralResult<&RuntimeType> {
        self.definitions
            .get(slot.0 as usize)
            .map(crate::SlotDefinition::runtime_type)
            .ok_or_else(|| invalid_runtime("verified slot disappeared at runtime"))
    }

    pub(super) fn read(&self, slot: SlotId) -> ProceduralResult<&RuntimeValue> {
        self.slots
            .get(slot.0 as usize)
            .and_then(Option::as_ref)
            .ok_or_else(|| invalid_runtime("verified initialized slot has no runtime value"))
    }

    pub(super) fn assign(
        &mut self,
        destination: SlotId,
        value: RuntimeValue,
        span: Option<&SourceSpan>,
    ) -> ProceduralResult<()> {
        self.assign_many(std::slice::from_ref(&destination), vec![value], span)
    }

    pub(super) fn assign_many(
        &mut self,
        destinations: &[SlotId],
        values: Vec<RuntimeValue>,
        span: Option<&SourceSpan>,
    ) -> ProceduralResult<()> {
        if destinations.len() != values.len() {
            return Err(attach_span(
                invalid_runtime("assignment width differs"),
                span,
            ));
        }
        let mut old_bytes = 0u64;
        let mut new_bytes = 0u64;
        for (destination, value) in destinations.iter().zip(&values) {
            let definition = self
                .definitions
                .get(destination.0 as usize)
                .ok_or_else(|| invalid_runtime("assignment destination is out of bounds"))?;
            if !definition.runtime_type().accepts(value) {
                return Err(attach_span(
                    Diagnostic::new(
                        DiagnosticKind::RuntimeInvalidIr,
                        "runtime value violates verified slot type",
                    )
                    .with_detail("slot", definition.name()),
                    span,
                ));
            }
            old_bytes = old_bytes.saturating_add(
                self.slots[destination.0 as usize]
                    .as_ref()
                    .map_or(0, RuntimeValue::owned_bytes),
            );
            new_bytes = new_bytes.saturating_add(value.owned_bytes());
        }
        if new_bytes > old_bytes {
            self.budget.charge_heap(new_bytes - old_bytes)?;
        }
        for (destination, value) in destinations.iter().zip(values) {
            self.slots[destination.0 as usize] = Some(value);
        }
        if old_bytes > new_bytes {
            self.budget.release_heap(old_bytes - new_bytes);
        }
        self.heap_bytes = self
            .heap_bytes
            .saturating_sub(old_bytes)
            .saturating_add(new_bytes);
        Ok(())
    }
}

impl Drop for Frame {
    fn drop(&mut self) {
        self.budget.release_heap(self.heap_bytes);
    }
}

pub(super) fn scalar(frame: &Frame, slot: SlotId) -> ProceduralResult<&Value> {
    let RuntimeValue::Scalar(value) = frame.read(slot)? else {
        return Err(invalid_runtime("verified scalar slot changed shape"));
    };
    Ok(value)
}

fn invalid_runtime(message: &'static str) -> Diagnostic {
    Diagnostic::new(DiagnosticKind::RuntimeInvalidIr, message)
}

fn attach_span(error: Diagnostic, span: Option<&SourceSpan>) -> Diagnostic {
    if error.primary_span().is_none() {
        error.with_primary_span(span.cloned())
    } else {
        error
    }
}