Files
endorphin-aster-vm/crates/core/src/eval.rs
T

1236 lines
48 KiB
Rust

use crate::ExecutionProgress;
use crate::frame::{Coroutine, CoroutineState, NativeReturn, StackEntry, SuspendedCall};
use crate::{
CompareOp, EndorCellObject, EndorCodeObject, EndorFrame, EndorFunctionObject,
EndorStringObject, ExecutionLimits, ExecutionLocation, Instruction, ObjectRef, Opcode, Value,
Vm, VmError,
};
use crate::{CoroutineProgress, ObjectPayload};
use crate::{NativeContext, NativeResult};
enum PreparedCall {
Value(Value),
Frame(EndorFrame),
}
fn invalid(offset: usize, reason: &'static str) -> VmError {
VmError::InvalidBytecode { offset, reason }
}
impl Vm {
/// Execute a code object in a globals dictionary. Like CPython's EvalCode
/// entry path, this constructs a temporary function and enters its frame.
/// Object results own one external reference: release_value() when done.
pub fn execute(&mut self, code: ObjectRef, globals: ObjectRef) -> Result<Value, VmError> {
self.execute_with_limits(code, globals, ExecutionLimits::default())
}
pub fn execute_with_limits(
&mut self,
code: ObjectRef,
globals: ObjectRef,
limits: ExecutionLimits,
) -> Result<Value, VmError> {
self.last_traceback.clear();
let name = self
.payload::<EndorCodeObject>(code, "code")?
.code()
.name()
.to_owned();
let function = self.new_function(name, code, globals, Vec::new(), Vec::new())?;
let result = self.call_with_limits(function, &[], limits);
self.release(function)?;
result
}
/// Dispatch both language and host functions. Arguments are borrowed;
/// every Object result owns one external reference, including native
/// callbacks that return one of their borrowed arguments.
pub fn call(&mut self, function: ObjectRef, arguments: &[Value]) -> Result<Value, VmError> {
self.call_with_limits(function, arguments, ExecutionLimits::default())
}
/// Allow result allocation while rejecting mutations of objects that were
/// already alive on entry. All reads share the call's normal frame budget.
/// Returned read handles are borrowed dependency identities, not owned roots.
pub fn call_read_only_with_limits(
&mut self,
function: ObjectRef,
arguments: &[Value],
limits: ExecutionLimits,
) -> Result<(Value, Vec<ObjectRef>), VmError> {
let scope = self.objects.begin_read_only();
let result = self.call_with_limits(function, arguments, limits);
let reads = self.objects.end_read_only(scope)?;
result.map(|value| (value, reads))
}
pub fn call_with_limits(
&mut self,
function: ObjectRef,
arguments: &[Value],
limits: ExecutionLimits,
) -> Result<Value, VmError> {
if self.suspended_call.is_some() {
return Err(VmError::UnsupportedOperation(
"call while read-only execution is suspended",
));
}
self.last_traceback.clear();
let mut executed = 0;
let frame = match self.prepare_call(function, arguments.to_vec(), &mut executed, limits)? {
PreparedCall::Value(value) => return Ok(value),
PreparedCall::Frame(frame) => frame,
};
if limits.max_frames == 0 {
self.clear_frame(frame);
return Err(VmError::ExecutionLimit("frames"));
}
let mut frames = vec![frame];
let result = self
.eval_frames(&mut frames, limits, &mut executed, usize::MAX)
.map(|value| value.expect("unbounded slice completes or fails"));
if result.is_err() {
self.last_traceback = frames
.iter()
.map(|frame| ExecutionLocation {
function: frame.name.clone(),
filename: frame.executable.filename().into(),
first_line: frame.executable.first_line(),
instruction_offset: frame.current_offset,
})
.collect();
}
while let Some(frame) = frames.pop() {
self.clear_frame(frame);
}
result
}
/// Begin a resumable read-only call. One VM owns at most one continuation.
/// A slice is a soft instruction quantum: native callbacks and delegation
/// complete atomically and remain subject to the cumulative hard limits.
pub fn start_read_only_call(
&mut self,
function: ObjectRef,
arguments: &[Value],
limits: ExecutionLimits,
slice: usize,
) -> Result<ExecutionProgress, VmError> {
if slice == 0 {
return Err(VmError::InvalidArgument("execution slice must be positive"));
}
if self.suspended_call.is_some() || self.objects.is_read_only() {
return Err(VmError::UnsupportedOperation(
"start suspended call inside another execution",
));
}
self.last_traceback.clear();
let scope = self.objects.begin_read_only();
let mut executed = 0;
let prepared = self.prepare_call(function, arguments.to_vec(), &mut executed, limits);
match prepared {
Ok(PreparedCall::Value(value)) => {
let reads = match self.objects.end_read_only(scope) {
Ok(reads) => reads,
Err(error) => {
self.drop_root(value);
return Err(error);
}
};
Ok(ExecutionProgress::Complete {
value,
reads,
instructions: executed,
})
}
Ok(PreparedCall::Frame(frame)) => {
if limits.max_frames == 0 {
self.clear_frame(frame);
self.objects.end_read_only(scope)?;
return Err(VmError::ExecutionLimit("frames"));
}
self.suspended_call = Some(SuspendedCall {
frames: vec![frame],
scope,
limits,
executed,
});
// Preparation counts toward the first quantum as well.
self.resume_read_only_to(slice)
}
Err(error) => {
self.objects.end_read_only(scope)?;
Err(error)
}
}
}
pub fn has_suspended_call(&self) -> bool {
self.suspended_call.is_some()
}
/// Resume existing frames, never replay earlier instructions. The read-only
/// scope remains active across Pending; preexisting heap writes stay barred.
pub fn resume_read_only_call(&mut self, slice: usize) -> Result<ExecutionProgress, VmError> {
if slice == 0 {
return Err(VmError::InvalidArgument("execution slice must be positive"));
}
let executed = self
.suspended_call
.as_ref()
.ok_or(VmError::InvalidArgument("no suspended call"))?
.executed;
self.resume_read_only_to(executed.saturating_add(slice))
}
fn resume_read_only_to(&mut self, slice_end: usize) -> Result<ExecutionProgress, VmError> {
let mut call = self
.suspended_call
.take()
.ok_or(VmError::InvalidArgument("no suspended call"))?;
let result = self.eval_frames(&mut call.frames, call.limits, &mut call.executed, slice_end);
if let Ok(None) = result {
let instructions = call.executed;
self.suspended_call = Some(call);
return Ok(ExecutionProgress::Pending { instructions });
}
if result.is_err() {
self.last_traceback = call
.frames
.iter()
.map(|frame| ExecutionLocation {
function: frame.name.clone(),
filename: frame.executable.filename().into(),
first_line: frame.executable.first_line(),
instruction_offset: frame.current_offset,
})
.collect();
}
while let Some(frame) = call.frames.pop() {
self.clear_frame(frame);
}
let reads = self.objects.end_read_only(call.scope);
match (result, reads) {
(Ok(Some(value)), Ok(reads)) => Ok(ExecutionProgress::Complete {
value,
reads,
instructions: call.executed,
}),
(Ok(Some(value)), Err(error)) => {
self.drop_root(value);
Err(error)
}
(Err(error), _) => Err(error),
(Ok(None), _) => unreachable!(),
}
}
/// Release all retained execution roots and close the read-only scope.
pub fn cancel_read_only_call(&mut self) -> Result<bool, VmError> {
let Some(mut call) = self.suspended_call.take() else {
return Ok(false);
};
while let Some(frame) = call.frames.pop() {
self.clear_frame(frame);
}
self.objects.end_read_only(call.scope)?;
Ok(true)
}
/// Prepare a managed independently resumable call. Native delegation may
/// run during preparation; source instructions start only on resume.
/// The returned handle owns one external reference, like every allocation.
pub fn new_coroutine(
&mut self,
function: ObjectRef,
arguments: &[Value],
limits: ExecutionLimits,
) -> Result<ObjectRef, VmError> {
self.check_coroutine_entry()?;
self.last_traceback.clear();
let mut executed = 0;
let prepared = self.prepare_call(function, arguments.to_vec(), &mut executed, limits)?;
let state = match prepared {
PreparedCall::Frame(frame) => {
if limits.max_frames == 0 {
self.clear_frame(frame);
return Err(VmError::ExecutionLimit("frames"));
}
CoroutineState::Frames(std::sync::Arc::new(vec![frame]))
}
PreparedCall::Value(value) => CoroutineState::Ready(value),
};
// Allocate traced edges before releasing the original execution roots.
let coroutine = Coroutine {
state,
limits,
executed,
};
let mut roots = Vec::new();
coroutine.trace(&mut |reference| roots.push(reference));
let result = self.allocate(coroutine);
for root in roots {
self.release(root)?;
}
result
}
fn check_coroutine_entry(&self) -> Result<(), VmError> {
if self.has_suspended_call() || self.objects.is_read_only() {
return Err(VmError::UnsupportedOperation(
"coroutine operation inside read-only execution",
));
}
Ok(())
}
/// Resume one managed call, preserving cumulative hard limits. Other calls
/// and heap writes may execute between Pending results. No prior instruction
/// is replayed. Completion returns an owned value and consumes the frames.
pub fn resume_coroutine(
&mut self,
reference: ObjectRef,
slice: usize,
) -> Result<CoroutineProgress, VmError> {
self.retain(reference)?;
let result = self.resume_coroutine_inner(reference, slice);
self.release(reference)?;
result
}
fn resume_coroutine_inner(
&mut self,
reference: ObjectRef,
slice: usize,
) -> Result<CoroutineProgress, VmError> {
self.check_coroutine_entry()?;
if slice == 0 {
return Err(VmError::InvalidArgument("execution slice must be positive"));
}
let coroutine = self.payload::<Coroutine>(reference, "coroutine")?;
let limits = coroutine.limits;
let mut executed = coroutine.executed;
let state = match &coroutine.state {
CoroutineState::Frames(frames) => CoroutineState::Frames(frames.clone()),
CoroutineState::Ready(value) => CoroutineState::Ready(value.clone()),
_ => return Err(VmError::InvalidArgument("coroutine is running or finished")),
};
let mut roots = Vec::new();
match &state {
CoroutineState::Frames(frames) => {
for frame in frames.iter() {
frame.trace_roots(&mut |root| roots.push(root));
}
}
CoroutineState::Ready(Value::Object(root)) => roots.push(*root),
_ => {}
}
// Transfer edges to execution roots transactionally, before replacing
// the managed payload. Duplicate edges must retain duplicate roots.
let mut retained = Vec::new();
for root in roots {
if let Err(error) = self.retain(root) {
for root in retained {
self.release(root)?;
}
return Err(error);
}
retained.push(root);
}
if let Err(error) = self.replace_payload(
reference,
Coroutine {
state: CoroutineState::Running,
limits,
executed,
},
) {
for root in retained {
self.release(root)?;
}
return Err(error);
}
self.last_traceback.clear();
let mut frames = match state {
CoroutineState::Frames(frames) => {
std::sync::Arc::try_unwrap(frames).expect("private frame storage has one owner")
}
CoroutineState::Ready(value) => {
if let Err(error) = self.replace_payload(
reference,
Coroutine {
state: CoroutineState::Finished,
limits,
executed,
},
) {
self.drop_root(value);
return Err(error);
}
return Ok(CoroutineProgress::Complete {
value,
instructions: executed,
});
}
_ => unreachable!(),
};
let slice_end = executed.saturating_add(slice);
let result = self.eval_frames(&mut frames, limits, &mut executed, slice_end);
if let Ok(None) = result {
let frames = std::sync::Arc::new(frames);
let mut roots = Vec::new();
for frame in frames.iter() {
frame.trace_roots(&mut |root| roots.push(root));
}
let published = self.replace_payload(
reference,
Coroutine {
state: CoroutineState::Frames(frames),
limits,
executed,
},
);
for root in roots {
self.release(root)?;
}
published?;
return Ok(CoroutineProgress::Pending {
instructions: executed,
});
}
if result.is_err() {
self.last_traceback = frames
.iter()
.map(|frame| ExecutionLocation {
function: frame.name.clone(),
filename: frame.executable.filename().into(),
first_line: frame.executable.first_line(),
instruction_offset: frame.current_offset,
})
.collect();
}
while let Some(frame) = frames.pop() {
self.clear_frame(frame);
}
let finished = self.replace_payload(
reference,
Coroutine {
state: CoroutineState::Finished,
limits,
executed,
},
);
match result {
Ok(Some(value)) => {
if let Err(error) = finished {
self.drop_root(value);
return Err(error);
}
Ok(CoroutineProgress::Complete {
value,
instructions: executed,
})
}
Err(error) => {
finished?;
Err(error)
}
Ok(None) => unreachable!(),
}
}
/// Cancellation releases managed frame edges; dropping an unreachable
/// coroutine has the same ownership effect, including cycles through locals.
/// Writes performed by previous quanta are never rolled back.
pub fn cancel_coroutine(&mut self, reference: ObjectRef) -> Result<bool, VmError> {
self.check_coroutine_entry()?;
let coroutine = self.payload::<Coroutine>(reference, "coroutine")?;
if matches!(coroutine.state, CoroutineState::Running) {
return Err(VmError::InvalidArgument("coroutine is running"));
}
if matches!(coroutine.state, CoroutineState::Finished) {
return Ok(false);
}
let limits = coroutine.limits;
let executed = coroutine.executed;
self.replace_payload(
reference,
Coroutine {
state: CoroutineState::Finished,
limits,
executed,
},
)?;
Ok(true)
}
/// Outer-to-inner execution locations from the last failed call/execute.
/// These are copied metadata, not heap references or language exceptions.
pub fn last_traceback(&self) -> &[ExecutionLocation] {
&self.last_traceback
}
pub fn release_value(&mut self, value: Value) -> Result<(), VmError> {
if let Value::Object(reference) = value {
self.release(reference)?;
}
Ok(())
}
fn retain_value(&mut self, value: &Value) -> Result<(), VmError> {
self.validate_value(value)?;
if let Value::Object(reference) = value {
self.retain(*reference)?;
}
Ok(())
}
fn drop_root(&mut self, value: Value) {
self.release_value(value)
.expect("execution owns every object root it releases");
}
fn clear_frame(&mut self, frame: EndorFrame) {
self.clear_native_returns(frame.native_returns);
for entry in frame.stack {
if let StackEntry::Value(value) = entry {
self.drop_root(value);
}
}
for value in frame.localsplus.into_iter().flatten() {
self.drop_root(value);
}
self.release(frame.function)
.expect("frame owns its function root");
if let Some(reference) = frame.return_type.and_then(|kind| kind.reference()) {
self.release(reference)
.expect("frame owns its return type root");
}
}
fn prepare_frame(
&mut self,
function: ObjectRef,
arguments: &[Value],
) -> Result<EndorFrame, VmError> {
let data = self
.payload::<EndorFunctionObject>(function, "function")?
.clone();
let executable = self
.payload::<EndorCodeObject>(data.code(), "code")?
.shared_code();
let maximum = executable.parameters().len();
let required = maximum - data.defaults().len();
if arguments.len() < required || arguments.len() > maximum {
return Err(VmError::ArgumentCount {
name: data.name().into(),
required,
maximum,
provided: arguments.len(),
});
}
for value in arguments {
self.validate_value(value)?;
}
let receiver = arguments
.first()
.or_else(|| {
if required == 0 {
data.defaults().first()
} else {
None
}
})
.and_then(|value| match value {
Value::Object(receiver) => Some(*receiver),
_ => None,
});
let typed_method = if let Some(receiver) = receiver {
self.objects
.check_method_contract_call(function, receiver, self.source_type_checks)?
} else {
false
};
let mut return_type = None;
if (self.source_type_checks || typed_method)
&& let Some(signature) = executable.source_signature()
{
for (index, expected_name) in signature.parameters.iter().enumerate() {
let value = arguments
.get(index)
.unwrap_or_else(|| &data.defaults()[index - required]);
let expected = self
.objects
.annotation_type(function, expected_name, receiver)?;
if !self.objects.matches_annotation(value, expected)? {
return Err(VmError::TypeMismatch {
function: data.name().into(),
position: format!("parameter {}", executable.parameters()[index]),
expected: expected_name.clone(),
actual: self.objects.value_type_name(value)?,
});
}
}
return_type = Some(self.objects.annotation_type(
function,
&signature.return_type,
receiver,
)?);
}
self.retain(function)?;
if let Some(reference) = return_type.and_then(|kind| kind.reference())
&& let Err(error) = self.retain(reference)
{
self.release(function)?;
return Err(error);
}
let mut frame = EndorFrame {
return_type,
native_returns: Vec::new(),
function,
previous: None,
code: data.code(),
globals: data.globals(),
builtins: data.builtins(),
name: data.name().into(),
instruction_offset: 0,
current_offset: 0,
localsplus: vec![None; executable.slot_count()],
stack: Vec::with_capacity(executable.stack_size()),
executable,
};
let initialized = (|| -> Result<(), VmError> {
if let Some(receiver) = receiver {
for (name, expected) in self.objects.method_trait_returns(function, receiver)? {
self.push_native_return(
&mut frame.native_returns,
NativeReturn { name, expected },
)?;
}
}
let defaults = data.defaults().iter().skip(arguments.len() - required);
for (slot, value) in frame
.localsplus
.iter_mut()
.zip(arguments.iter().chain(defaults))
{
self.retain_value(value)?;
*slot = Some(value.clone());
}
// Captured local cells occupy the original local slots, as in
// CPython's localsplus layout. Free cells follow the local slots.
let local_count = frame.executable.layout().locals.len();
for (index, slot) in frame.localsplus[..local_count].iter_mut().enumerate() {
if frame.executable.is_cell_slot(index) {
let cell = self.new_cell(slot.clone())?;
if let Some(old) = slot.replace(Value::Object(cell)) {
self.drop_root(old);
}
}
}
let first_free = frame.executable.layout().locals.len();
for (index, cell) in data.closure().iter().enumerate() {
self.retain(*cell)?;
frame.localsplus[first_free + index] = Some(Value::Object(*cell));
}
Ok(())
})();
if let Err(error) = initialized {
self.clear_frame(frame);
return Err(error);
}
Ok(frame)
}
fn invoke_native(
&mut self,
function: ObjectRef,
arguments: &[Value],
) -> Result<NativeResult, VmError> {
self.object(function)?;
for argument in arguments {
self.validate_value(argument)?;
}
if let Some(Value::Object(receiver)) = arguments.first() {
self.objects.check_method_contract_call(
function,
*receiver,
self.source_type_checks,
)?;
}
if let Some(arity) = self.objects.call_arity(function)?
&& !arity.accepts(arguments.len())
{
return Err(VmError::ArgumentCount {
name: self.object(function)?.payload().kind_name().into(),
required: arity.required,
maximum: arity.maximum,
provided: arguments.len(),
});
}
if let Some(callable) = self.object(function)?.payload().native_callable() {
return NativeContext::execute(&mut self.objects, function, arguments, callable);
}
let signature = self.objects.native_signature(function)?;
let name = self.object(function)?.payload().kind_name().to_owned();
if let Some(signature) = &signature {
signature.check_arguments(&self.objects, &name, arguments)?;
}
let result = self.object(function)?.payload().call(arguments)?;
if let Some(signature) = &signature {
signature.check_return(&self.objects, &name, &result)?;
}
self.retain_value(&result)?;
Ok(NativeResult::Return(result))
}
fn prepare_call(
&mut self,
mut function: ObjectRef,
mut arguments: Vec<Value>,
executed: &mut usize,
limits: ExecutionLimits,
) -> Result<PreparedCall, VmError> {
let mut owned = false;
let mut returns = Vec::new();
let result = (|| loop {
if self
.object(function)?
.payload()
.as_any()
.is::<EndorFunctionObject>()
{
let mut frame = self.prepare_frame(function, &arguments)?;
let mut local = std::mem::take(&mut frame.native_returns);
frame.native_returns = std::mem::take(&mut returns);
frame.native_returns.append(&mut local);
return Ok(PreparedCall::Frame(frame));
}
if *executed >= limits.max_instructions {
return Err(VmError::ExecutionLimit("instructions"));
}
*executed += 1;
if let Some(signature) = self.objects.native_signature(function)?
&& !signature.returns.is_any()
{
let check = NativeReturn {
name: self.object(function)?.payload().kind_name().into(),
expected: signature.returns,
};
self.push_native_return(&mut returns, check)?;
}
if let Some(Value::Object(receiver)) = arguments.first() {
for (name, expected) in self.objects.method_trait_returns(function, *receiver)? {
self.push_native_return(&mut returns, NativeReturn { name, expected })?;
}
}
match self.invoke_native(function, &arguments)? {
NativeResult::Return(value) => {
for check in returns.iter().rev() {
if let Err(error) = check.check(&self.objects, &value) {
self.drop_root(value);
return Err(error);
}
}
return Ok(PreparedCall::Value(value));
}
NativeResult::Invoke {
function: next,
arguments: values,
} => {
if owned {
self.release(function)?;
for value in arguments.drain(..) {
self.release_value(value)?;
}
}
function = next;
arguments = values;
owned = true;
}
}
})();
self.clear_native_returns(returns);
if owned {
self.release(function)
.expect("delegated call owns its function");
for value in arguments {
self.drop_root(value);
}
}
result
}
fn push_native_return(
&mut self,
returns: &mut Vec<NativeReturn>,
check: NativeReturn,
) -> Result<(), VmError> {
let mut retained = Vec::new();
for reference in check.expected.references() {
if let Err(error) = self.retain(reference) {
for reference in retained {
self.release(reference)?;
}
return Err(error);
}
retained.push(reference);
}
returns.push(check);
Ok(())
}
fn clear_native_returns(&mut self, returns: Vec<NativeReturn>) {
for check in returns {
for reference in check.expected.references() {
self.release(reference)
.expect("native return check owns its type");
}
}
}
fn stack_value(frame: &EndorFrame, index: usize) -> Result<Value, VmError> {
match frame.stack.get(index) {
Some(StackEntry::Value(value)) => Ok(value.clone()),
_ => Err(invalid(
frame.current_offset,
"NULL call marker used as a value",
)),
}
}
fn top_value(frame: &EndorFrame) -> Result<Value, VmError> {
Self::stack_value(frame, frame.stack.len() - 1)
}
fn assignment_value(
&mut self,
previous: Option<Value>,
incoming: Value,
) -> Result<Value, VmError> {
if let (Some(Value::Object(old)), Value::Object(new)) = (&previous, &incoming)
&& (old == new || self.objects.assign_from(*old, *new)?)
{
return Ok(Value::Object(*old));
}
Ok(incoming)
}
fn push_borrowed(&mut self, frame: &mut EndorFrame, value: Value) -> Result<(), VmError> {
self.retain_value(&value)?;
frame.stack.push(StackEntry::Value(value));
Ok(())
}
fn discard(&mut self, frame: &mut EndorFrame, count: usize) {
for _ in 0..count {
if let StackEntry::Value(value) = frame.stack.pop().expect("verified stack height") {
self.drop_root(value);
}
}
}
fn cell_slot(&self, frame: &EndorFrame, index: usize) -> Result<ObjectRef, VmError> {
match &frame.localsplus[index] {
Some(Value::Object(reference)) => {
self.payload::<EndorCellObject>(*reference, "cell")?;
Ok(*reference)
}
_ => Err(invalid(
frame.current_offset,
"cell slot is not initialized",
)),
}
}
fn value_is_true(&self, value: &Value) -> Result<bool, VmError> {
match value {
Value::Nil => Ok(false),
Value::Bool(value) => Ok(*value),
Value::Text(value) => Ok(!value.is_empty()),
Value::Number(value) => Ok(!value.is_zero()),
Value::Object(reference) => self.is_true(*reference),
}
}
fn text_value(&self, value: &Value) -> Result<Option<String>, VmError> {
Ok(match value {
Value::Text(text) => Some(text.clone()),
Value::Object(reference) => self
.object(*reference)?
.payload()
.as_any()
.downcast_ref::<EndorStringObject>()
.map(|text| text.as_str().to_owned()),
_ => None,
})
}
fn compare_values(
&self,
left: &Value,
right: &Value,
operation: CompareOp,
) -> Result<bool, VmError> {
if matches!(left, Value::Number(_)) || matches!(right, Value::Number(_)) {
return Err(VmError::UnsupportedOperation("numeric comparison"));
}
if let (Some(left), Some(right)) = (self.text_value(left)?, self.text_value(right)?) {
return Ok(match operation {
CompareOp::Eq => left == right,
CompareOp::Ne => left != right,
CompareOp::Lt => left < right,
CompareOp::Le => left <= right,
CompareOp::Gt => left > right,
CompareOp::Ge => left >= right,
});
}
if let (Value::Object(left), Value::Object(right)) = (left, right) {
return self.compare(*left, *right, operation);
}
match operation {
CompareOp::Eq => Ok(left == right),
CompareOp::Ne => Ok(left != right),
_ => Err(VmError::UnsupportedOperation("ordering")),
}
}
fn build_map(&mut self, frame: &EndorFrame, count: usize) -> Result<ObjectRef, VmError> {
let dictionary = self.new_dict()?;
let result = (|| -> Result<(), VmError> {
let start = frame.stack.len() - count * 2;
for index in 0..count {
let key = Self::stack_value(frame, start + index * 2)?;
let value = Self::stack_value(frame, start + index * 2 + 1)?;
self.dict_set(dictionary, key, value)?;
}
Ok(())
})();
if let Err(error) = result {
self.release(dictionary)?;
return Err(error);
}
Ok(dictionary)
}
fn make_function(&mut self, frame: &EndorFrame, argument: u32) -> Result<ObjectRef, VmError> {
let (default_count, closure_count) = Instruction::function_counts(argument);
let Value::Object(code) = Self::top_value(frame)? else {
return Err(VmError::ObjectKindMismatch("code"));
};
let name = self
.payload::<EndorCodeObject>(code, "code")?
.code()
.name()
.to_owned();
let start = frame.stack.len() - default_count - closure_count - 1;
let defaults = (start..start + default_count)
.map(|index| Self::stack_value(frame, index))
.collect::<Result<Vec<_>, _>>()?;
let closure = (start + default_count..frame.stack.len() - 1)
.map(|index| match Self::stack_value(frame, index)? {
Value::Object(cell) => Ok(cell),
_ => Err(VmError::ObjectKindMismatch("cell")),
})
.collect::<Result<Vec<_>, _>>()?;
// Keep every input root on the stack until allocation has validated
// and retained all function edges. Failure leaves cleanup to the frame.
self.new_function(name, code, frame.globals, defaults, closure)
}
fn build_class(&mut self, frame: &EndorFrame) -> Result<ObjectRef, VmError> {
let start = frame.stack.len() - 4;
let Value::Text(name) = Self::stack_value(frame, start)? else {
return Err(VmError::ObjectKindMismatch("class name"));
};
let Value::Object(base) = Self::stack_value(frame, start + 1)? else {
return Err(VmError::ObjectKindMismatch("base Type"));
};
let Value::Object(fields) = Self::stack_value(frame, start + 2)? else {
return Err(VmError::ObjectKindMismatch("field map"));
};
let Value::Object(methods) = Self::stack_value(frame, start + 3)? else {
return Err(VmError::ObjectKindMismatch("method map"));
};
let fields = self
.dict_items(fields)?
.iter()
.map(|(name, type_name)| match (name, type_name) {
(Value::Text(name), Value::Text(type_name)) => Ok(crate::ClassField {
name: name.clone(),
type_name: type_name.clone(),
}),
_ => Err(VmError::ObjectKindMismatch("class field annotation")),
})
.collect::<Result<Vec<_>, _>>()?;
let methods = self
.dict_items(methods)?
.iter()
.map(|(name, function)| match (name, function) {
(Value::Text(name), Value::Object(function)) => Ok((name.clone(), *function)),
_ => Err(VmError::ObjectKindMismatch("class method")),
})
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
self.objects
.new_class_with_scope(name, base, fields, methods, Some(frame.function))
}
fn eval_frames(
&mut self,
frames: &mut Vec<EndorFrame>,
limits: ExecutionLimits,
executed: &mut usize,
slice_end: usize,
) -> Result<Option<Value>, VmError> {
loop {
if *executed >= limits.max_instructions {
return Err(VmError::ExecutionLimit("instructions"));
}
if *executed >= slice_end {
return Ok(None);
}
*executed += 1;
let frame = frames.last_mut().expect("an evaluation frame is active");
let decoded = frame.executable.instruction_at(frame.instruction_offset)?;
frame.current_offset = decoded.offset;
frame.instruction_offset = decoded.next_offset;
let opcode = decoded.instruction.opcode;
let argument = decoded.instruction.argument as usize;
match opcode {
Opcode::Nop => {}
Opcode::LoadConst => {
let value = frame.executable.constants()[argument].clone();
self.push_borrowed(frame, value)?;
}
Opcode::LoadFast => {
let value = frame.localsplus[argument].clone().ok_or_else(|| {
VmError::UnboundLocal(frame.executable.slot_name(argument).into())
})?;
self.push_borrowed(frame, value)?;
}
Opcode::StoreFast => {
let value = self.assignment_value(
frame.localsplus[argument].clone(),
Self::top_value(frame)?,
)?;
self.retain_value(&value)?;
self.discard(frame, 1);
if let Some(old) = frame.localsplus[argument].replace(value) {
self.drop_root(old);
}
}
Opcode::DeleteFast => {
let value = frame.localsplus[argument].take().ok_or_else(|| {
VmError::UnboundLocal(frame.executable.slot_name(argument).into())
})?;
self.drop_root(value);
}
Opcode::LoadGlobal => {
let name = &frame.executable.layout().names[argument];
let key = Value::Text(name.clone());
let mut value = self.dict_get(frame.globals, &key)?;
if value.is_none()
&& let Some(builtins) = frame.builtins
{
value = self.dict_get(builtins, &key)?;
}
let value = value.ok_or_else(|| VmError::NameNotFound(name.clone()))?;
self.push_borrowed(frame, value)?;
}
Opcode::StoreGlobal => {
let key = Value::Text(frame.executable.layout().names[argument].clone());
let value = self.assignment_value(
self.dict_get(frame.globals, &key)?,
Self::top_value(frame)?,
)?;
self.dict_set(frame.globals, key, value)?;
self.discard(frame, 1);
}
Opcode::DeleteGlobal => {
let name = &frame.executable.layout().names[argument];
if !self.dict_delete(frame.globals, &Value::Text(name.clone()))? {
return Err(VmError::NameNotFound(name.clone()));
}
}
Opcode::LoadDeref | Opcode::LoadClosure => {
let cell = self.cell_slot(frame, argument)?;
let value = if opcode == Opcode::LoadClosure {
Value::Object(cell)
} else {
self.cell_get(cell).map_err(|error| {
if error == VmError::EmptyCell {
VmError::UnboundLocal(frame.executable.slot_name(argument).into())
} else {
error
}
})?
};
self.push_borrowed(frame, value)?;
}
Opcode::StoreDeref => {
let cell = self.cell_slot(frame, argument)?;
let value =
self.assignment_value(self.cell_get(cell).ok(), Self::top_value(frame)?)?;
self.cell_set(cell, value)?;
self.discard(frame, 1);
}
Opcode::DeleteDeref => {
let cell = self.cell_slot(frame, argument)?;
if self.cell_get(cell).is_err() {
return Err(VmError::UnboundLocal(
frame.executable.slot_name(argument).into(),
));
}
self.cell_clear(cell)?;
}
Opcode::PopTop => self.discard(frame, 1),
Opcode::Copy => {
let entry = frame.stack[frame.stack.len() - argument].clone();
if let StackEntry::Value(value) = &entry {
self.retain_value(value)?;
}
frame.stack.push(entry);
}
Opcode::Swap => {
let length = frame.stack.len();
frame.stack.swap(length - 1, length - argument);
}
Opcode::PushNull => frame.stack.push(StackEntry::Null),
Opcode::Call => {
let start = frame.stack.len() - argument - 2;
let Value::Object(function) = Self::stack_value(frame, start)? else {
return Err(VmError::UnsupportedOperation("call"));
};
let mut arguments = Vec::with_capacity(argument + 1);
if let StackEntry::Value(value) = &frame.stack[start + 1] {
arguments.push(value.clone());
}
for index in start + 2..frame.stack.len() {
arguments.push(Self::stack_value(frame, index)?);
}
match self.prepare_call(function, arguments, executed, limits)? {
PreparedCall::Frame(mut callee) => {
if frames.len() >= limits.max_frames {
self.clear_frame(callee);
return Err(VmError::ExecutionLimit("frames"));
}
callee.previous = Some(frames.len() - 1);
self.discard(frames.last_mut().unwrap(), argument + 2);
frames.push(callee);
}
PreparedCall::Value(value) => {
self.discard(frames.last_mut().unwrap(), argument + 2);
frames
.last_mut()
.unwrap()
.stack
.push(StackEntry::Value(value));
}
}
}
Opcode::ReturnValue => {
let result = Self::top_value(frame)?;
if let Some(expected) = frame.return_type
&& !self.objects.matches_annotation(&result, expected)?
{
return Err(VmError::TypeMismatch {
function: frame.name.clone(),
position: "return".into(),
expected: frame
.executable
.source_signature()
.expect("checked source signature")
.return_type
.clone(),
actual: self.objects.value_type_name(&result)?,
});
}
for check in frame.native_returns.iter().rev() {
check.check(&self.objects, &result)?;
}
frame.stack.pop();
self.clear_frame(frames.pop().unwrap());
if let Some(caller) = frames.last_mut() {
caller.stack.push(StackEntry::Value(result));
} else {
return Ok(Some(result));
}
}
Opcode::JumpForward | Opcode::JumpBackward => {
frame.instruction_offset = decoded.jump_target()?
}
Opcode::PopJumpIfFalse | Opcode::PopJumpIfTrue => {
let truth = self.value_is_true(&Self::top_value(frame)?)?;
self.discard(frame, 1);
if truth == (opcode == Opcode::PopJumpIfTrue) {
frame.instruction_offset = decoded.jump_target()?;
}
}
Opcode::ToBool | Opcode::UnaryNot => {
let truth = self.value_is_true(&Self::top_value(frame)?)?;
self.discard(frame, 1);
frame.stack.push(StackEntry::Value(Value::Bool(
if opcode == Opcode::UnaryNot {
!truth
} else {
truth
},
)));
}
Opcode::CompareOp => {
let left = Self::stack_value(frame, frame.stack.len() - 2)?;
let right = Self::top_value(frame)?;
let result = self.compare_values(
&left,
&right,
Instruction::compare_operation(decoded.instruction.argument).unwrap(),
)?;
self.discard(frame, 2);
frame.stack.push(StackEntry::Value(Value::Bool(result)));
}
Opcode::LoadAttr => {
let Value::Object(owner) = Self::top_value(frame)? else {
return Err(VmError::ObjectKindMismatch("object"));
};
let value =
self.bind_attr(owner, &frame.executable.layout().names[argument])?;
self.discard(frame, 1);
frame.stack.push(StackEntry::Value(value));
}
Opcode::BuildClass => {
let class = self.build_class(frame)?;
self.discard(frame, 4);
frame.stack.push(StackEntry::Value(Value::Object(class)));
}
Opcode::StoreAttr => {
let Value::Object(owner) = Self::top_value(frame)? else {
return Err(VmError::ObjectKindMismatch("object"));
};
let value = Self::stack_value(frame, frame.stack.len() - 2)?;
self.set_attr(
owner,
frame.executable.layout().names[argument].clone(),
value,
)?;
self.discard(frame, 2);
}
Opcode::BuildMap => {
let dictionary = self.build_map(frame, argument)?;
self.discard(frame, argument * 2);
frame
.stack
.push(StackEntry::Value(Value::Object(dictionary)));
}
Opcode::MakeFunction => {
let function = self.make_function(frame, decoded.instruction.argument)?;
let (defaults, closure) =
Instruction::function_counts(decoded.instruction.argument);
self.discard(frame, defaults + closure + 1);
frame.stack.push(StackEntry::Value(Value::Object(function)));
}
Opcode::ExtendedArg => unreachable!("EXTENDED_ARG was decoded before evaluation"),
}
}
}
}