refactor: extract core objects into a standalone package
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s

This commit is contained in:
2026-10-19 13:33:24 +08:00
parent 78fc859825
commit f77b8370ea
40 changed files with 631 additions and 202 deletions
+389
View File
@@ -0,0 +1,389 @@
use std::any::Any;
use std::collections::{BTreeSet, VecDeque};
use std::sync::Arc;
use crate::builtins::trace_value;
use crate::instruction::{compare_operation, decode, function_counts, invalid};
use crate::{DecodedInstruction, Instruction, ObjectPayload, ObjectRef, Opcode, Value, VmError};
/// Local slots start with positional parameters. Captured locals occupy their
/// original slots as cells; free-variable cells follow all local slots.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CodeLayout {
pub parameters: Vec<String>,
pub locals: Vec<String>,
pub cell_variables: Vec<String>,
pub free_variables: Vec<String>,
pub names: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct EndorCode {
bytecode: Vec<u8>,
instructions: Vec<DecodedInstruction>,
constants: Vec<Value>,
layout: CodeLayout,
stack_size: usize,
name: String,
filename: String,
first_line: usize,
}
impl EndorCode {
pub fn new(
bytecode: Vec<u8>,
constants: Vec<Value>,
parameters: Vec<String>,
free_variables: Vec<String>,
) -> Result<Self, VmError> {
Self::with_layout(
bytecode,
constants,
CodeLayout {
locals: parameters.clone(),
parameters,
free_variables,
..CodeLayout::default()
},
)
}
pub fn from_instructions(
instructions: &[Instruction],
constants: Vec<Value>,
layout: CodeLayout,
) -> Result<Self, VmError> {
Self::with_layout(Instruction::encode_all(instructions)?, constants, layout)
}
pub fn with_layout(
bytecode: Vec<u8>,
constants: Vec<Value>,
layout: CodeLayout,
) -> Result<Self, VmError> {
validate_layout(&layout)?;
let instructions = decode(&bytecode)?;
let stack_size = verify(&instructions, constants.len(), &layout)?;
Ok(Self {
bytecode,
instructions,
constants,
layout,
stack_size,
name: "<code>".into(),
filename: "<endor>".into(),
first_line: 1,
})
}
pub fn with_location(
mut self,
name: impl Into<String>,
filename: impl Into<String>,
first_line: usize,
) -> Self {
self.name = name.into();
self.filename = filename.into();
self.first_line = first_line;
self
}
pub fn bytecode(&self) -> &[u8] {
&self.bytecode
}
pub fn instructions(&self) -> &[DecodedInstruction] {
&self.instructions
}
pub fn constants(&self) -> &[Value] {
&self.constants
}
pub fn parameters(&self) -> &[String] {
&self.layout.parameters
}
pub fn free_variables(&self) -> &[String] {
&self.layout.free_variables
}
pub fn layout(&self) -> &CodeLayout {
&self.layout
}
pub fn stack_size(&self) -> usize {
self.stack_size
}
pub fn name(&self) -> &str {
&self.name
}
pub fn filename(&self) -> &str {
&self.filename
}
pub fn first_line(&self) -> usize {
self.first_line
}
pub fn slot_count(&self) -> usize {
self.layout.locals.len() + self.layout.free_variables.len()
}
/// Whether a local/free slot holds a managed closure cell.
pub fn is_cell_slot(&self, slot: usize) -> bool {
is_cell_slot(&self.layout, slot)
}
/// Name of a local/free slot used by validated instructions.
///
/// # Panics
/// Panics if the slot is outside this code's local/free-variable layout.
pub fn slot_name(&self, slot: usize) -> &str {
if slot < self.layout.locals.len() {
&self.layout.locals[slot]
} else {
&self.layout.free_variables[slot - self.layout.locals.len()]
}
}
/// Look up a logical instruction by its code-unit offset.
pub fn instruction_at(&self, offset: usize) -> Result<DecodedInstruction, VmError> {
instruction_index(&self.instructions, offset)
.map(|index| self.instructions[index])
.ok_or_else(|| {
invalid(
offset,
"instruction pointer is not on an instruction boundary",
)
})
}
}
/// Heap-allocated, immutable and shareable executable. Function objects hold a
/// managed reference to this object rather than embedding a copy of its code.
#[derive(Debug, Clone)]
pub struct EndorCodeObject {
code: Arc<EndorCode>,
}
impl EndorCodeObject {
pub fn new(code: EndorCode) -> Self {
Self {
code: Arc::new(code),
}
}
pub fn code(&self) -> &EndorCode {
&self.code
}
/// Share immutable executable data without copying constants or wordcode.
/// This Rust Arc does not retain heap objects referenced by constants;
/// callers must keep the managed code object rooted while using them.
pub fn shared_code(&self) -> Arc<EndorCode> {
Arc::clone(&self.code)
}
}
impl ObjectPayload for EndorCodeObject {
fn as_any(&self) -> &dyn Any {
self
}
fn kind_name(&self) -> &'static str {
"code"
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
for value in self.code.constants() {
trace_value(value, visit);
}
}
fn repr(&self, _identity: ObjectRef) -> Result<String, VmError> {
Ok(format!(
"<code {} at {}:{}>",
self.code.name(),
self.code.filename(),
self.code.first_line()
))
}
}
fn validate_layout(layout: &CodeLayout) -> Result<(), VmError> {
for names in [
&layout.parameters,
&layout.locals,
&layout.cell_variables,
&layout.free_variables,
&layout.names,
] {
let mut seen = BTreeSet::new();
for name in names {
if name.is_empty() || !seen.insert(name) {
return Err(VmError::InvalidArgument(
"empty or duplicate code variable name",
));
}
}
}
if !layout.locals.starts_with(&layout.parameters) {
return Err(VmError::InvalidArgument(
"parameter names must lead local slots",
));
}
if layout
.cell_variables
.iter()
.any(|name| !layout.locals.contains(name))
|| layout
.free_variables
.iter()
.any(|name| layout.locals.contains(name))
{
return Err(VmError::InvalidArgument(
"invalid local/cell/free-variable layout",
));
}
Ok(())
}
fn is_cell_slot(layout: &CodeLayout, slot: usize) -> bool {
if slot >= layout.locals.len() {
slot < layout.locals.len() + layout.free_variables.len()
} else {
layout.cell_variables.contains(&layout.locals[slot])
}
}
fn instruction_index(instructions: &[DecodedInstruction], offset: usize) -> Option<usize> {
instructions
.binary_search_by_key(&offset, |instruction| instruction.offset)
.ok()
}
pub(crate) fn jump_target(instruction: DecodedInstruction) -> Result<usize, VmError> {
let argument = instruction.instruction.argument as usize;
let target = if instruction.instruction.opcode == Opcode::JumpBackward {
instruction.next_offset.checked_sub(argument)
} else {
instruction.next_offset.checked_add(argument)
};
target.ok_or_else(|| invalid(instruction.offset, "jump offset overflow"))
}
/// Validate every operand, then propagate stack heights through reachable
/// control flow. Bad jumps, underflow, mismatched joins and fallthrough fail
/// before execution; loops must return to the same stack height.
fn verify(
instructions: &[DecodedInstruction],
constants: usize,
layout: &CodeLayout,
) -> Result<usize, VmError> {
for instruction in instructions {
let Instruction { opcode, argument } = instruction.instruction;
let index = argument as usize;
let valid = match opcode {
Opcode::LoadConst => index < constants,
Opcode::LoadFast | Opcode::StoreFast | Opcode::DeleteFast => {
index < layout.locals.len() && !is_cell_slot(layout, index)
}
Opcode::LoadDeref | Opcode::StoreDeref | Opcode::DeleteDeref | Opcode::LoadClosure => {
is_cell_slot(layout, index)
}
Opcode::LoadGlobal
| Opcode::StoreGlobal
| Opcode::DeleteGlobal
| Opcode::LoadAttr
| Opcode::StoreAttr => index < layout.names.len(),
Opcode::CompareOp => compare_operation(argument).is_some(),
Opcode::Copy => argument >= 1,
Opcode::Swap => argument >= 2,
Opcode::JumpForward
| Opcode::JumpBackward
| Opcode::PopJumpIfFalse
| Opcode::PopJumpIfTrue => {
instruction_index(instructions, jump_target(*instruction)?).is_some()
}
Opcode::ExtendedArg => false,
_ => true,
};
if !valid {
return Err(invalid(
instruction.offset,
"invalid operand or jump target",
));
}
}
let mut heights = vec![None; instructions.len()];
heights[0] = Some(0_usize);
let mut pending = VecDeque::from([0]);
let mut maximum = 0;
while let Some(index) = pending.pop_front() {
let instruction = instructions[index];
let height = heights[index].unwrap();
let Instruction { opcode, argument } = instruction.instruction;
let argument = argument as usize;
let (required, popped, pushed) = match opcode {
Opcode::LoadConst
| Opcode::LoadFast
| Opcode::LoadGlobal
| Opcode::LoadDeref
| Opcode::LoadClosure
| Opcode::PushNull => (0, 0, 1),
Opcode::StoreFast
| Opcode::StoreGlobal
| Opcode::PopTop
| Opcode::StoreDeref
| Opcode::PopJumpIfFalse
| Opcode::PopJumpIfTrue => (1, 1, 0),
Opcode::ToBool | Opcode::UnaryNot | Opcode::LoadAttr => (1, 1, 1),
Opcode::CompareOp => (2, 2, 1),
Opcode::StoreAttr => (2, 2, 0),
Opcode::Copy => (argument, 0, 1),
Opcode::Swap => (argument, 0, 0),
Opcode::Call => {
let consumed = argument
.checked_add(2)
.ok_or_else(|| invalid(instruction.offset, "call operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::BuildMap => {
let consumed = argument
.checked_mul(2)
.ok_or_else(|| invalid(instruction.offset, "map operand overflow"))?;
(consumed, consumed, 1)
}
Opcode::MakeFunction => {
let (defaults, closure) = function_counts(instruction.instruction.argument);
let consumed = defaults + closure + 1;
(consumed, consumed, 1)
}
Opcode::ReturnValue => (1, 1, 0),
_ => (0, 0, 0),
};
if height < required {
return Err(invalid(instruction.offset, "evaluation stack underflow"));
}
if opcode == Opcode::ReturnValue && height != 1 {
return Err(invalid(
instruction.offset,
"RETURN_VALUE must leave an empty frame stack",
));
}
let next_height = height - popped + pushed;
maximum = maximum.max(height).max(next_height);
let successors: Vec<usize> = match opcode {
Opcode::ReturnValue => Vec::new(),
Opcode::JumpForward | Opcode::JumpBackward => vec![jump_target(instruction)?],
Opcode::PopJumpIfFalse | Opcode::PopJumpIfTrue => {
vec![jump_target(instruction)?, instruction.next_offset]
}
_ => vec![instruction.next_offset],
};
for successor in successors {
let target = instruction_index(instructions, successor).ok_or_else(|| {
invalid(instruction.offset, "execution falls off the code object")
})?;
match heights[target] {
Some(existing) if existing != next_height => {
return Err(invalid(
successor,
"control-flow paths have different stack heights",
));
}
Some(_) => {}
None => {
heights[target] = Some(next_height);
pending.push_back(target);
}
}
}
}
Ok(maximum)
}