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;
|
||||
/// 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};
|
||||
|
||||
|
||||
@ -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())),
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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() });
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user