intrinsics pr-02

This commit is contained in:
bQUARKz 2026-03-03 04:55:12 +00:00
parent eb835bafee
commit 8545c67939
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 315 additions and 4 deletions

View File

@ -19,6 +19,8 @@ pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
/// Executed RET with an incorrect stack height (mismatch with function metadata).
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};

View File

@ -352,6 +352,13 @@ pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
emit_u16(CoreOpCode::Syscall as u16, &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())),
}

View File

@ -83,6 +83,10 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
// Hex id stable, avoids dependency on HAL metadata.
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)
let mut s = String::with_capacity(2 + imm.len() * 2);

View File

@ -12,7 +12,8 @@ mod value;
pub use abi::{
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 decoder::{decode_next, DecodeError};

View File

@ -682,7 +682,8 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
| OpCode::SetLocal
| OpCode::PopN
| OpCode::Hostcall
| OpCode::Syscall => {
| OpCode::Syscall
| OpCode::Intrinsic => {
pos += 4;
}
OpCode::PushI64 | OpCode::PushF64 => {

View File

@ -195,6 +195,10 @@ pub enum OpCode {
/// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall)
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).
/// Execution pauses until the next VSync.
FrameSync = 0x80,
@ -253,6 +257,7 @@ impl TryFrom<u16> for OpCode {
0x56 => Ok(OpCode::Sleep),
0x70 => Ok(OpCode::Syscall),
0x71 => Ok(OpCode::Hostcall),
0x72 => Ok(OpCode::Intrinsic),
0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
}
@ -312,6 +317,7 @@ impl OpCode {
OpCode::Sleep => 1,
OpCode::Syscall => 1,
OpCode::Hostcall => 1,
OpCode::Intrinsic => 1,
OpCode::FrameSync => 1,
}
}

View File

@ -513,6 +513,16 @@ impl OpCodeSpecExt for OpCode {
may_trap: true,
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 {
name: "FRAME_SYNC",
imm_bytes: 0,

View File

@ -42,6 +42,9 @@ fn disasm(bytes: &[u8]) -> String {
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
format!("{}", instr.imm_u32().unwrap())
}
CoreOpCode::Syscall | CoreOpCode::Intrinsic => {
format!("0x{}", hex::encode(instr.imm))
}
_ => format!("0x{}", hex::encode(instr.imm)),
};
line.push_str(&s);
@ -129,6 +132,20 @@ fn hostcall_roundtrips_with_decimal_index() {
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
mod hex {
pub fn encode(bytes: &[u8]) -> String {

View File

@ -180,6 +180,31 @@ impl IntrinsicMeta {
pub const fn key(&self) -> IntrinsicKey {
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);
@ -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> {
if args.len() != 4 {
return Err(IntrinsicExecutionError::ArityMismatch { expected: 4, got: args.len() });

View File

@ -1,3 +1,4 @@
use crate::lookup_intrinsic_by_id;
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt;
@ -59,6 +60,10 @@ pub enum VerifierError {
pc: usize,
id: u32,
},
InvalidIntrinsicId {
pc: usize,
id: u32,
},
TrailingBytes {
func_idx: usize,
at_pc: usize,
@ -271,6 +276,13 @@ impl Verifier {
})?;
(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),
};
@ -397,6 +409,13 @@ impl Verifier {
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
if spec.pushes > 0 {
@ -414,6 +433,7 @@ impl Verifier {
0
}
OpCode::Syscall => spec.pushes, // already added Unknowns above
OpCode::Intrinsic => spec.pushes, // already added Unknowns above
OpCode::Call => spec.pushes, // already added Unknowns above
_ => spec.pushes,
});
@ -1155,6 +1175,63 @@ mod tests {
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]
fn test_verifier_rejects_unpatched_hostcall() {
let mut code = Vec::new();

View File

@ -1,5 +1,6 @@
use crate::call_frame::CallFrame;
use crate::heap::{CoroutineState, Heap};
use crate::lookup_intrinsic_by_id;
use crate::object::ObjectKind;
use crate::roots::{RootVisitor, visit_value_for_roots};
use crate::scheduler::Scheduler;
@ -13,8 +14,8 @@ use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
use prometeu_bytecode::model::BytecodeModule;
use prometeu_bytecode::{
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB,
TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC,
TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
};
use prometeu_hal::syscalls::caps::NONE;
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 => {
// Marks the logical end of a frame: consume cycles and signal to the driver
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]
fn test_syscall_arg_underflow_trap() {
// GfxClear (0x1001) expects 1 arg