refactor: extract core objects into a standalone package
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m34s
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user