intrinsics pr-02
This commit is contained in:
parent
eb835bafee
commit
8545c67939
@ -19,6 +19,8 @@ pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
|
|||||||
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
|
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
|
||||||
/// Executed RET with an incorrect stack height (mismatch with function metadata).
|
/// Executed RET with an incorrect stack height (mismatch with function metadata).
|
||||||
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
|
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
|
||||||
|
/// The intrinsic ID provided is not recognized by the runtime, or its metadata is invalid.
|
||||||
|
pub const TRAP_INVALID_INTRINSIC: u32 = 0x0000_000D;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|||||||
@ -352,6 +352,13 @@ pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
|
|||||||
emit_u16(CoreOpCode::Syscall as u16, &mut out);
|
emit_u16(CoreOpCode::Syscall as u16, &mut out);
|
||||||
emit_u32(parse_u32_any(ops)?, &mut out);
|
emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
}
|
}
|
||||||
|
"INTRINSIC" => {
|
||||||
|
if ops.is_empty() {
|
||||||
|
return Err(AsmError::MissingOperand(line.into()));
|
||||||
|
}
|
||||||
|
emit_u16(CoreOpCode::Intrinsic as u16, &mut out);
|
||||||
|
emit_u32(parse_u32_any(ops)?, &mut out);
|
||||||
|
}
|
||||||
|
|
||||||
other => return Err(AsmError::UnknownMnemonic(other.into())),
|
other => return Err(AsmError::UnknownMnemonic(other.into())),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,10 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
|
|||||||
// Hex id stable, avoids dependency on HAL metadata.
|
// Hex id stable, avoids dependency on HAL metadata.
|
||||||
format!("0x{:04x}", id)
|
format!("0x{:04x}", id)
|
||||||
}
|
}
|
||||||
|
CoreOpCode::Intrinsic => {
|
||||||
|
let id = u32::from_le_bytes(imm.try_into().unwrap());
|
||||||
|
format!("0x{:04x}", id)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Fallback: raw immediate hex (little-endian, as encoded)
|
// Fallback: raw immediate hex (little-endian, as encoded)
|
||||||
let mut s = String::with_capacity(2 + imm.len() * 2);
|
let mut s = String::with_capacity(2 + imm.len() * 2);
|
||||||
|
|||||||
@ -12,7 +12,8 @@ mod value;
|
|||||||
|
|
||||||
pub use abi::{
|
pub use abi::{
|
||||||
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC,
|
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_ILLEGAL_INSTRUCTION, TRAP_INVALID_FUNC,
|
||||||
TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE,
|
TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB,
|
||||||
|
TRAP_STACK_UNDERFLOW, TRAP_TYPE,
|
||||||
};
|
};
|
||||||
pub use assembler::{assemble, AsmError};
|
pub use assembler::{assemble, AsmError};
|
||||||
pub use decoder::{decode_next, DecodeError};
|
pub use decoder::{decode_next, DecodeError};
|
||||||
|
|||||||
@ -682,7 +682,8 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
|||||||
| OpCode::SetLocal
|
| OpCode::SetLocal
|
||||||
| OpCode::PopN
|
| OpCode::PopN
|
||||||
| OpCode::Hostcall
|
| OpCode::Hostcall
|
||||||
| OpCode::Syscall => {
|
| OpCode::Syscall
|
||||||
|
| OpCode::Intrinsic => {
|
||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
OpCode::PushI64 | OpCode::PushF64 => {
|
OpCode::PushI64 | OpCode::PushF64 => {
|
||||||
|
|||||||
@ -195,6 +195,10 @@ pub enum OpCode {
|
|||||||
/// Operand: syscall_id (u32)
|
/// Operand: syscall_id (u32)
|
||||||
/// Stack: [args...] -> [results...] (depends on syscall)
|
/// Stack: [args...] -> [results...] (depends on syscall)
|
||||||
Syscall = 0x70,
|
Syscall = 0x70,
|
||||||
|
/// Invokes a VM-owned intrinsic by final numeric id.
|
||||||
|
/// Operand: intrinsic_id (u32)
|
||||||
|
/// Stack: [args...] -> [results...] (depends on intrinsic metadata)
|
||||||
|
Intrinsic = 0x72,
|
||||||
/// Synchronizes the VM with the hardware frame (usually 60Hz).
|
/// Synchronizes the VM with the hardware frame (usually 60Hz).
|
||||||
/// Execution pauses until the next VSync.
|
/// Execution pauses until the next VSync.
|
||||||
FrameSync = 0x80,
|
FrameSync = 0x80,
|
||||||
@ -253,6 +257,7 @@ impl TryFrom<u16> for OpCode {
|
|||||||
0x56 => Ok(OpCode::Sleep),
|
0x56 => Ok(OpCode::Sleep),
|
||||||
0x70 => Ok(OpCode::Syscall),
|
0x70 => Ok(OpCode::Syscall),
|
||||||
0x71 => Ok(OpCode::Hostcall),
|
0x71 => Ok(OpCode::Hostcall),
|
||||||
|
0x72 => Ok(OpCode::Intrinsic),
|
||||||
0x80 => Ok(OpCode::FrameSync),
|
0x80 => Ok(OpCode::FrameSync),
|
||||||
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
|
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
|
||||||
}
|
}
|
||||||
@ -312,6 +317,7 @@ impl OpCode {
|
|||||||
OpCode::Sleep => 1,
|
OpCode::Sleep => 1,
|
||||||
OpCode::Syscall => 1,
|
OpCode::Syscall => 1,
|
||||||
OpCode::Hostcall => 1,
|
OpCode::Hostcall => 1,
|
||||||
|
OpCode::Intrinsic => 1,
|
||||||
OpCode::FrameSync => 1,
|
OpCode::FrameSync => 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -513,6 +513,16 @@ impl OpCodeSpecExt for OpCode {
|
|||||||
may_trap: true,
|
may_trap: true,
|
||||||
is_safepoint: false,
|
is_safepoint: false,
|
||||||
},
|
},
|
||||||
|
OpCode::Intrinsic => OpcodeSpec {
|
||||||
|
name: "INTRINSIC",
|
||||||
|
imm_bytes: 4,
|
||||||
|
pops: 0,
|
||||||
|
pushes: 0,
|
||||||
|
is_branch: false,
|
||||||
|
is_terminator: false,
|
||||||
|
may_trap: true,
|
||||||
|
is_safepoint: false,
|
||||||
|
},
|
||||||
OpCode::FrameSync => OpcodeSpec {
|
OpCode::FrameSync => OpcodeSpec {
|
||||||
name: "FRAME_SYNC",
|
name: "FRAME_SYNC",
|
||||||
imm_bytes: 0,
|
imm_bytes: 0,
|
||||||
|
|||||||
@ -42,6 +42,9 @@ fn disasm(bytes: &[u8]) -> String {
|
|||||||
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
|
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
|
||||||
format!("{}", instr.imm_u32().unwrap())
|
format!("{}", instr.imm_u32().unwrap())
|
||||||
}
|
}
|
||||||
|
CoreOpCode::Syscall | CoreOpCode::Intrinsic => {
|
||||||
|
format!("0x{}", hex::encode(instr.imm))
|
||||||
|
}
|
||||||
_ => format!("0x{}", hex::encode(instr.imm)),
|
_ => format!("0x{}", hex::encode(instr.imm)),
|
||||||
};
|
};
|
||||||
line.push_str(&s);
|
line.push_str(&s);
|
||||||
@ -129,6 +132,20 @@ fn hostcall_roundtrips_with_decimal_index() {
|
|||||||
assert_eq!(rebuilt, prog);
|
assert_eq!(rebuilt, prog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intrinsic_roundtrips_with_hex_id() {
|
||||||
|
let mut prog = Vec::new();
|
||||||
|
prog.extend(encode_instr(CoreOpCode::Intrinsic, Some(&0x1000u32.to_le_bytes())));
|
||||||
|
prog.extend(encode_instr(CoreOpCode::Halt, None));
|
||||||
|
|
||||||
|
let text = prometeu_bytecode::disassemble(&prog).expect("disasm intrinsic");
|
||||||
|
|
||||||
|
assert!(text.contains("INTRINSIC 0x1000"));
|
||||||
|
|
||||||
|
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble intrinsic");
|
||||||
|
assert_eq!(rebuilt, prog);
|
||||||
|
}
|
||||||
|
|
||||||
// Minimal hex helper to avoid extra deps in tests
|
// Minimal hex helper to avoid extra deps in tests
|
||||||
mod hex {
|
mod hex {
|
||||||
pub fn encode(bytes: &[u8]) -> String {
|
pub fn encode(bytes: &[u8]) -> String {
|
||||||
|
|||||||
@ -180,6 +180,31 @@ impl IntrinsicMeta {
|
|||||||
pub const fn key(&self) -> IntrinsicKey {
|
pub const fn key(&self) -> IntrinsicKey {
|
||||||
IntrinsicKey::new(self.owner, self.name, self.version)
|
IntrinsicKey::new(self.owner, self.name, self.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn arg_slots(&self) -> usize {
|
||||||
|
self.arg_layout.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ret_slots(&self) -> usize {
|
||||||
|
self.ret_layout.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_result_values(&self, values: &[Value]) -> Result<(), IntrinsicExecutionError> {
|
||||||
|
if values.len() != self.ret_slots() {
|
||||||
|
return Err(IntrinsicExecutionError::ArityMismatch {
|
||||||
|
expected: self.ret_slots(),
|
||||||
|
got: values.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, (value, expected)) in values.iter().zip(self.ret_layout.iter()).enumerate() {
|
||||||
|
if !value_matches_abi_type(value, *expected) {
|
||||||
|
return Err(IntrinsicExecutionError::TypeMismatch { index, expected: *expected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLOR: BuiltinTypeKey = BuiltinTypeKey::new("color", 1);
|
const COLOR: BuiltinTypeKey = BuiltinTypeKey::new("color", 1);
|
||||||
@ -352,6 +377,15 @@ fn expect_float_arg(args: &[Value], index: usize) -> Result<f64, IntrinsicExecut
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn value_matches_abi_type(value: &Value, expected: AbiType) -> bool {
|
||||||
|
match expected {
|
||||||
|
AbiType::Scalar(BuiltinScalarType::Int) => value.as_integer().is_some(),
|
||||||
|
AbiType::Scalar(BuiltinScalarType::Float) => value.as_float().is_some(),
|
||||||
|
AbiType::Scalar(BuiltinScalarType::Bool) => matches!(value, Value::Boolean(_)),
|
||||||
|
AbiType::Builtin(_) => value.as_integer().is_some(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn vec2_dot(args: &[Value]) -> Result<Vec<Value>, IntrinsicExecutionError> {
|
fn vec2_dot(args: &[Value]) -> Result<Vec<Value>, IntrinsicExecutionError> {
|
||||||
if args.len() != 4 {
|
if args.len() != 4 {
|
||||||
return Err(IntrinsicExecutionError::ArityMismatch { expected: 4, got: args.len() });
|
return Err(IntrinsicExecutionError::ArityMismatch { expected: 4, got: args.len() });
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::lookup_intrinsic_by_id;
|
||||||
use prometeu_bytecode::FunctionMeta;
|
use prometeu_bytecode::FunctionMeta;
|
||||||
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
||||||
use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt;
|
use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt;
|
||||||
@ -59,6 +60,10 @@ pub enum VerifierError {
|
|||||||
pc: usize,
|
pc: usize,
|
||||||
id: u32,
|
id: u32,
|
||||||
},
|
},
|
||||||
|
InvalidIntrinsicId {
|
||||||
|
pc: usize,
|
||||||
|
id: u32,
|
||||||
|
},
|
||||||
TrailingBytes {
|
TrailingBytes {
|
||||||
func_idx: usize,
|
func_idx: usize,
|
||||||
at_pc: usize,
|
at_pc: usize,
|
||||||
@ -271,6 +276,13 @@ impl Verifier {
|
|||||||
})?;
|
})?;
|
||||||
(syscall.args_count() as u16, syscall.results_count() as u16)
|
(syscall.args_count() as u16, syscall.results_count() as u16)
|
||||||
}
|
}
|
||||||
|
OpCode::Intrinsic => {
|
||||||
|
let id = instr.imm_u32().unwrap();
|
||||||
|
let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| {
|
||||||
|
VerifierError::InvalidIntrinsicId { pc: func_start + pc, id }
|
||||||
|
})?;
|
||||||
|
(intrinsic.arg_slots() as u16, intrinsic.ret_slots() as u16)
|
||||||
|
}
|
||||||
_ => (spec.pops, spec.pushes),
|
_ => (spec.pops, spec.pushes),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -397,6 +409,13 @@ impl Verifier {
|
|||||||
out_types.push(Unknown);
|
out_types.push(Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OpCode::Intrinsic => {
|
||||||
|
let id = instr.imm_u32().unwrap();
|
||||||
|
let intrinsic = lookup_intrinsic_by_id(id).unwrap();
|
||||||
|
for _ in 0..(intrinsic.ret_slots() as u16) {
|
||||||
|
out_types.push(Unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Default: push Unknown for any declared _pushes
|
// Default: push Unknown for any declared _pushes
|
||||||
if spec.pushes > 0 {
|
if spec.pushes > 0 {
|
||||||
@ -414,6 +433,7 @@ impl Verifier {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
OpCode::Syscall => spec.pushes, // already added Unknowns above
|
OpCode::Syscall => spec.pushes, // already added Unknowns above
|
||||||
|
OpCode::Intrinsic => spec.pushes, // already added Unknowns above
|
||||||
OpCode::Call => spec.pushes, // already added Unknowns above
|
OpCode::Call => spec.pushes, // already added Unknowns above
|
||||||
_ => spec.pushes,
|
_ => spec.pushes,
|
||||||
});
|
});
|
||||||
@ -1155,6 +1175,63 @@ mod tests {
|
|||||||
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifier_invalid_intrinsic_id() {
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.push(OpCode::Intrinsic as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||||||
|
|
||||||
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
assert_eq!(res, Err(VerifierError::InvalidIntrinsicId { pc: 0, id: 0xDEADBEEF }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifier_accepts_intrinsic_stack_effects() {
|
||||||
|
let mut code = Vec::new();
|
||||||
|
for value in [1i32, 2, 3, 4] {
|
||||||
|
code.push(OpCode::PushI32 as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
code.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
code.push(OpCode::Intrinsic as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
code.extend_from_slice(&0x1000u32.to_le_bytes());
|
||||||
|
code.push(OpCode::Ret as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
|
||||||
|
let functions = vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: code.len() as u32,
|
||||||
|
return_slots: 1,
|
||||||
|
..Default::default()
|
||||||
|
}];
|
||||||
|
let res = Verifier::verify(&code, &functions).expect("intrinsic program must verify");
|
||||||
|
assert!(res[0] >= 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifier_rejects_intrinsic_stack_underflow() {
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.push(OpCode::PushI32 as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
code.extend_from_slice(&1i32.to_le_bytes());
|
||||||
|
code.push(OpCode::Intrinsic as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
code.extend_from_slice(&0x1000u32.to_le_bytes());
|
||||||
|
code.push(OpCode::Halt as u8);
|
||||||
|
code.push(0x00);
|
||||||
|
|
||||||
|
let functions = vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: code.len() as u32,
|
||||||
|
..Default::default()
|
||||||
|
}];
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 6, opcode: OpCode::Intrinsic }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_verifier_rejects_unpatched_hostcall() {
|
fn test_verifier_rejects_unpatched_hostcall() {
|
||||||
let mut code = Vec::new();
|
let mut code = Vec::new();
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::call_frame::CallFrame;
|
use crate::call_frame::CallFrame;
|
||||||
use crate::heap::{CoroutineState, Heap};
|
use crate::heap::{CoroutineState, Heap};
|
||||||
|
use crate::lookup_intrinsic_by_id;
|
||||||
use crate::object::ObjectKind;
|
use crate::object::ObjectKind;
|
||||||
use crate::roots::{RootVisitor, visit_value_for_roots};
|
use crate::roots::{RootVisitor, visit_value_for_roots};
|
||||||
use crate::scheduler::Scheduler;
|
use crate::scheduler::Scheduler;
|
||||||
@ -13,8 +14,8 @@ use prometeu_bytecode::decode_next;
|
|||||||
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
||||||
use prometeu_bytecode::model::BytecodeModule;
|
use prometeu_bytecode::model::BytecodeModule;
|
||||||
use prometeu_bytecode::{
|
use prometeu_bytecode::{
|
||||||
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB,
|
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC,
|
||||||
TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
|
TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
|
||||||
};
|
};
|
||||||
use prometeu_hal::syscalls::caps::NONE;
|
use prometeu_hal::syscalls::caps::NONE;
|
||||||
use prometeu_hal::vm_fault::VmFault;
|
use prometeu_hal::vm_fault::VmFault;
|
||||||
@ -1391,6 +1392,81 @@ impl VirtualMachine {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OpCode::Intrinsic => {
|
||||||
|
let pc_at_intrinsic = start_pc as u32;
|
||||||
|
let id = instr
|
||||||
|
.imm_u32()
|
||||||
|
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||||
|
let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| {
|
||||||
|
self.trap(
|
||||||
|
TRAP_INVALID_INTRINSIC,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
format!("Unknown intrinsic: 0x{:08X}", id),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let args_count = intrinsic.arg_slots();
|
||||||
|
let mut args = Vec::with_capacity(args_count);
|
||||||
|
for _ in 0..args_count {
|
||||||
|
let value = self.pop().map_err(|_e| {
|
||||||
|
self.trap(
|
||||||
|
TRAP_STACK_UNDERFLOW,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
"Intrinsic argument stack underflow".to_string(),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
args.push(value);
|
||||||
|
}
|
||||||
|
args.reverse();
|
||||||
|
|
||||||
|
let results = (intrinsic.implementation)(&args).map_err(|err| match err {
|
||||||
|
crate::IntrinsicExecutionError::ArityMismatch { expected, got } => self.trap(
|
||||||
|
TRAP_INVALID_INTRINSIC,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
format!(
|
||||||
|
"Intrinsic {}.{} argument mismatch: expected {}, got {}",
|
||||||
|
intrinsic.owner, intrinsic.name, expected, got
|
||||||
|
),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
),
|
||||||
|
crate::IntrinsicExecutionError::TypeMismatch { index, expected } => self.trap(
|
||||||
|
TRAP_TYPE,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
format!(
|
||||||
|
"Intrinsic {}.{} argument {} type mismatch: expected {:?}",
|
||||||
|
intrinsic.owner, intrinsic.name, index, expected
|
||||||
|
),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
intrinsic.validate_result_values(&results).map_err(|err| match err {
|
||||||
|
crate::IntrinsicExecutionError::ArityMismatch { expected, got } => self.trap(
|
||||||
|
TRAP_INVALID_INTRINSIC,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
format!(
|
||||||
|
"Intrinsic {}.{} results mismatch: expected {}, got {}",
|
||||||
|
intrinsic.owner, intrinsic.name, expected, got
|
||||||
|
),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
),
|
||||||
|
crate::IntrinsicExecutionError::TypeMismatch { index, expected } => self.trap(
|
||||||
|
TRAP_INVALID_INTRINSIC,
|
||||||
|
OpCode::Intrinsic as u16,
|
||||||
|
format!(
|
||||||
|
"Intrinsic {}.{} result {} type mismatch: expected {:?}",
|
||||||
|
intrinsic.owner, intrinsic.name, index, expected
|
||||||
|
),
|
||||||
|
pc_at_intrinsic,
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for value in results {
|
||||||
|
self.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
OpCode::FrameSync => {
|
OpCode::FrameSync => {
|
||||||
// Marks the logical end of a frame: consume cycles and signal to the driver
|
// Marks the logical end of a frame: consume cycles and signal to the driver
|
||||||
self.cycles += OpCode::FrameSync.cycles();
|
self.cycles += OpCode::FrameSync.cycles();
|
||||||
@ -2584,6 +2660,82 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_intrinsic_vec2_dot_executes_without_syscalls() {
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
for value in [1.0f64, 2.0, 3.0, 4.0] {
|
||||||
|
rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&value.to_bits().to_le_bytes());
|
||||||
|
}
|
||||||
|
rom.extend_from_slice(&(OpCode::Intrinsic as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0x1000u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
let mut vm = new_test_vm(rom, vec![]);
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
assert!(matches!(report.reason, LogicalFrameEndingReason::Halted));
|
||||||
|
assert_eq!(vm.operand_stack, vec![Value::Float(11.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_intrinsic_vec2_length_executes_without_syscalls() {
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
for value in [3.0f64, 4.0] {
|
||||||
|
rom.extend_from_slice(&(OpCode::PushF64 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&value.to_bits().to_le_bytes());
|
||||||
|
}
|
||||||
|
rom.extend_from_slice(&(OpCode::Intrinsic as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0x1001u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
let mut vm = new_test_vm(rom, vec![]);
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
assert!(matches!(report.reason, LogicalFrameEndingReason::Halted));
|
||||||
|
assert_eq!(vm.operand_stack, vec![Value::Float(5.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_intrinsic_trap_is_distinct_from_syscall() {
|
||||||
|
let rom = assemble("INTRINSIC 0xDEADBEEF\nHALT").expect("assemble");
|
||||||
|
let mut vm = new_test_vm(rom, vec![]);
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, TRAP_INVALID_INTRINSIC);
|
||||||
|
assert_eq!(trap.opcode, OpCode::Intrinsic as u16);
|
||||||
|
assert!(trap.message.contains("Unknown intrinsic"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected intrinsic trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_intrinsic_argument_underflow_trap() {
|
||||||
|
let rom = assemble("PUSH_I32 1\nINTRINSIC 0x1000\nHALT").expect("assemble");
|
||||||
|
let mut vm = new_test_vm(rom, vec![]);
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, TRAP_STACK_UNDERFLOW);
|
||||||
|
assert_eq!(trap.opcode, OpCode::Intrinsic as u16);
|
||||||
|
assert!(trap.message.contains("Intrinsic argument stack underflow"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected intrinsic underflow trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_syscall_arg_underflow_trap() {
|
fn test_syscall_arg_underflow_trap() {
|
||||||
// GfxClear (0x1001) expects 1 arg
|
// GfxClear (0x1001) expects 1 arg
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user