use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use relon_codegen_llvm::{
populate_global_mappings, CapabilityVtable, LlvmAotEvaluator, SandboxConfig, SandboxTrapKind,
VtableSlot,
};
use relon_eval_api::{
Capabilities, CapabilityBit, Evaluator, NativeArgs, NativeFnCaps, RelonFunction, RuntimeError,
Value,
};
use relon_parser::TokenRange;
struct NoCb;
impl NativeFnCaps for NoCb {
fn call_relon(
&self,
_f: &Value,
_a: Vec<Value>,
_r: TokenRange,
) -> Result<Value, RuntimeError> {
Err(RuntimeError::Unsupported {
reason: "no cb".into(),
})
}
}
struct AddSeven;
impl RelonFunction for AddSeven {
fn call(&self, args: NativeArgs, _r: TokenRange) -> Result<Value, RuntimeError> {
match args.positional.first() {
Some(Value::Int(x)) => Ok(Value::Int(x + 7)),
_ => Err(RuntimeError::Unsupported {
reason: "AddSeven expects Int".into(),
}),
}
}
}
fn clock_add_options(grant_clock: bool) -> relon_analyzer::AnalyzeOptions {
let sig = relon_analyzer::FnSignature {
name: "clock_add".to_string(),
generics: Vec::new(),
params: vec![relon_analyzer::FnParam {
name: "_".into(),
ty: relon_analyzer::type_node_simple("Int"),
optional: false,
}],
return_type: relon_analyzer::type_node_simple("Int"),
variadic_tail: None,
};
let mut signatures = HashMap::new();
signatures.insert("clock_add".to_string(), sig);
let mut gate = relon_analyzer::NativeFnGate::default();
gate.reads_clock = true;
let mut gates = HashMap::new();
gates.insert("clock_add".to_string(), gate);
let mut names = HashSet::new();
names.insert("clock_add".to_string());
let mut caps = relon_analyzer::Capabilities::default();
caps.reads_clock = grant_clock;
relon_analyzer::AnalyzeOptions {
host_fn_names: names,
host_fn_signatures: signatures,
host_fn_gates: gates,
caps,
strict_mode: false,
..Default::default()
}
}
#[test]
fn emit_object_produces_a_linkable_native_artefact() {
let dir = std::env::temp_dir().join(format!("relon_aot_cap_gate_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mk tmp dir");
let out = dir.join("cap_gate_smoke.o");
let info = LlvmAotEvaluator::emit_object("#main(Int x) -> Int\nx + 1", "relon_main", &out)
.expect("emit_object");
assert_eq!(info.entry_symbol, "relon_main");
let meta = std::fs::metadata(&out).expect("object file exists");
assert!(meta.len() > 0, "emitted object must be non-empty");
let bytes = std::fs::read(&out).expect("read object");
assert_eq!(
&bytes[..4],
b"\x7fELF",
"emitted file must be an ELF object"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn capability_gate_is_emitted_into_the_buffer_protocol_native_ir() {
let opts = clock_add_options( true);
let ev = LlvmAotEvaluator::from_source_with_options("#main(Int x) -> Int\nclock_add(x)", &opts)
.expect("gated source must compile");
let dump = ev.emit_ir_dump();
assert!(
dump.contains("cap_mask"),
"gate IR missing the caps-bitmask AND:\n{dump}"
);
assert!(
dump.contains("cap_denied"),
"gate IR missing the denied compare:\n{dump}"
);
assert!(
dump.contains("cap_denied_trap"),
"gate IR missing the deny trap arm:\n{dump}"
);
assert!(
dump.contains("cap_granted"),
"gate IR missing the granted continuation:\n{dump}"
);
assert!(
dump.contains("relon_llvm_call_native"),
"gate IR missing the native dispatch helper:\n{dump}"
);
}
#[test]
fn sandbox_vtable_grant_state() {
let mut vt = CapabilityVtable::with_capacity(64);
assert!(!vt.is_granted(CapabilityBit::ReadsClock.bit_index()));
vt.grant(CapabilityBit::ReadsClock.bit_index());
assert!(vt.is_granted(CapabilityBit::ReadsClock.bit_index()));
assert_eq!(
vt.caps_mask(),
1i64 << CapabilityBit::ReadsClock.bit_index(),
"the runtime carrier is the caps bitmask"
);
}
#[test]
fn sandbox_vtable_deny_state_via_shared_gate() {
let denied = Capabilities::default();
let mut vt = CapabilityVtable::with_capacity(64);
let populated = vt.register_via_gate(&denied, CapabilityBit::ReadsClock);
assert!(!populated, "denied gate must leave the bit clear");
assert!(!vt.is_granted(CapabilityBit::ReadsClock.bit_index()));
assert_eq!(vt.caps_mask(), 0);
let granted = Capabilities::all_granted();
let mut vt2 = CapabilityVtable::with_capacity(64);
assert!(vt2.register_via_gate(&granted, CapabilityBit::ReadsClock));
assert!(vt2.is_granted(CapabilityBit::ReadsClock.bit_index()));
}
#[test]
fn sandbox_vtable_dispatch_state() {
let mut vt = CapabilityVtable::with_capacity(64);
assert!(vt.resolve_host_fn(0).is_none());
vt.register_host_fn(0, Arc::new(AddSeven));
assert_eq!(vt.host_fn_count(), 1);
let f = vt.resolve_host_fn(0).expect("registered callable");
let r = f
.call(
NativeArgs::from_positional(vec![Value::Int(35)], Arc::new(NoCb)),
TokenRange::default(),
)
.expect("dispatch");
assert_eq!(r, Value::Int(42));
}
#[test]
fn sandbox_trap_kind_lifts_capability_denied() {
let err = SandboxTrapKind::CapabilityDenied.to_runtime_error(TokenRange::default());
assert!(matches!(err, RuntimeError::CapabilityDenied { .. }));
assert_eq!(SandboxTrapKind::CapabilityDenied as u64, 3);
assert_eq!(
SandboxTrapKind::from_code(3),
SandboxTrapKind::CapabilityDenied
);
}
#[test]
fn sandbox_config_mirrors_cranelift_knobs() {
assert_eq!(
SandboxConfig::default(),
SandboxConfig {
bounds_check: true,
deadline_check: true,
capability_check: true,
div_check: true,
}
);
let u = SandboxConfig::unchecked();
assert!(!u.capability_check && !u.div_check && !u.bounds_check && !u.deadline_check);
}
#[test]
fn vtable_symbol_registry_resolves_host_helpers() {
let mappings = populate_global_mappings();
assert_eq!(mappings.len() as u32, VtableSlot::COUNT);
for (sym, addr) in mappings {
assert!(!sym.is_empty());
assert_ne!(addr, 0, "host helper {sym} must resolve to a real address");
}
assert_eq!(
VtableSlot::RelonCallNative.symbol(),
"relon_llvm_call_native"
);
}
use relon_codegen_cranelift::{
AotEvaluator, CapabilityVtable as CraneliftVtable, HostFnPtr,
SandboxConfig as CraneliftSandboxConfig,
};
use relon_ir::ir::{Func, IrType, Module as IrModule, Op, TaggedOp, NO_CAPABILITY_BIT};
unsafe extern "C" fn now_stub(_arg: i64) -> i64 {
1_700_000_000
}
fn build_checkcap_ir(cap_bit: u32) -> IrModule {
IrModule {
imports: vec![],
funcs: vec![Func {
name: "run_main".to_string(),
params: vec![IrType::I64],
ret: IrType::I64,
body: vec![
TaggedOp {
op: Op::CheckCap { cap_bit },
range: TokenRange::default(),
},
TaggedOp {
op: Op::LocalGet(0),
range: TokenRange::default(),
},
TaggedOp {
op: Op::Return,
range: TokenRange::default(),
},
],
range: TokenRange::default(),
}],
entry_func_index: Some(0),
closure_table: vec![],
}
}
#[test]
fn anchor_cranelift_denies_when_bit_ungranted() {
let ir = build_checkcap_ir(2);
let ev = AotEvaluator::from_ir_direct(ir, CraneliftSandboxConfig::default(), vec!["x".into()])
.expect("cranelift compile");
let mut args = HashMap::new();
args.insert("x".to_string(), Value::Int(99));
let err = ev.run_main(args).expect_err("ungranted bit must deny");
assert!(
matches!(err, RuntimeError::CapabilityDenied { .. }),
"expected CapabilityDenied, got {err:?}"
);
}
#[test]
fn anchor_cranelift_grants_when_bit_registered() {
let ir = build_checkcap_ir(2);
let mut ev =
AotEvaluator::from_ir_direct(ir, CraneliftSandboxConfig::default(), vec!["x".into()])
.expect("cranelift compile");
let mut vt = CraneliftVtable::with_capacity(64);
let fn_ptr: HostFnPtr = now_stub;
vt.register(2, fn_ptr);
ev.install_capabilities_mut(Arc::new(vt));
let mut args = HashMap::new();
args.insert("x".to_string(), Value::Int(99));
let result = ev.run_main(args).expect("granted bit must run the body");
assert_eq!(result, Value::Int(99));
}
#[test]
fn anchor_cranelift_no_capability_bit_elides_gate() {
let ir = build_checkcap_ir(NO_CAPABILITY_BIT);
let ev = AotEvaluator::from_ir_direct(ir, CraneliftSandboxConfig::default(), vec!["x".into()])
.expect("cranelift compile");
let mut args = HashMap::new();
args.insert("x".to_string(), Value::Int(99));
let result = ev.run_main(args).expect("sentinel must elide the gate");
assert_eq!(result, Value::Int(99));
}
#[test]
fn anchor_trap_numbering_matches_cranelift() {
use relon_codegen_cranelift::TrapKind as CraneliftTrapKind;
assert_eq!(
SandboxTrapKind::CapabilityDenied as u64,
CraneliftTrapKind::CapabilityDenied as u8 as u64
);
assert_eq!(
SandboxTrapKind::DivisionByZero as u64,
CraneliftTrapKind::DivisionByZero as u8 as u64
);
assert_eq!(
SandboxTrapKind::NumericOverflow as u64,
CraneliftTrapKind::NumericOverflow as u8 as u64
);
}