use super::{JitCompiler, JitError, JitSettings, ModuleCompileMetadata};
use crate::atom::Atom;
use crate::jit::RootLocation;
use crate::jit::ir_common::{JIT_DEOPT_SENTINEL, X_REGISTER_COUNT};
use crate::jit::ir_exceptions::{
JIT_STATUS_DEOPT, JIT_STATUS_EXCEPTION, JIT_STATUS_NORMAL, JIT_STATUS_YIELD, JitReturn,
};
use crate::jit::type_info::{FunctionSignature, TypeDescriptor};
use crate::loader::decode::{BifOp, ComparisonOp, MapOp, Operand, TypeTestOp};
use crate::loader::{Instruction, LambdaEntry};
use crate::module::{Module, ModuleOrigin, ModuleRegistry, ResolvedImport, ResolvedImportTarget};
use crate::process::{JitRuntimeContext, Process};
use crate::term::Term;
use crate::term::boxed::{
Closure, Cons, Float, Map, Tuple, write_closure, write_cons, write_float, write_map,
write_tuple,
};
use std::collections::HashMap;
type RawJitFn = extern "C" fn(*mut u64, *mut Process) -> JitReturn;
fn call_native(native: &crate::jit::types::NativeCode, registers: &mut [u64]) -> u64 {
let mut process = Process::new(0, 233);
call_native_with_process(native, registers, &mut process)
}
fn call_native_with_process(
native: &crate::jit::types::NativeCode,
registers: &mut [u64],
process: &mut Process,
) -> u64 {
let returned = raw_jit_fn(native)(registers.as_mut_ptr(), process);
assert_eq!(returned.status, JIT_STATUS_NORMAL);
returned.value
}
fn call_native_with_process_x_regs(
native: &crate::jit::types::NativeCode,
process: &mut Process,
) -> u64 {
let registers = process.x_regs_mut().as_mut_ptr().cast::<u64>();
let returned = raw_jit_fn(native)(registers, process);
assert_eq!(returned.status, JIT_STATUS_NORMAL);
returned.value
}
fn call_native_status(
native: &crate::jit::types::NativeCode,
registers: &mut [u64],
process: &mut Process,
) -> JitReturn {
raw_jit_fn(native)(registers.as_mut_ptr(), process)
}
fn raw_jit_fn(native: &crate::jit::types::NativeCode) -> RawJitFn {
unsafe { std::mem::transmute(native.call_ptr()) }
}
fn test_lambda(
function: Atom,
arity: u8,
label: u32,
num_free: u32,
unique_id: u64,
) -> LambdaEntry {
LambdaEntry {
function,
arity,
label,
num_free,
unique_id,
}
}
fn heap_closure(
process: &mut Process,
module: Atom,
function_index: u64,
arity: u8,
generation: u64,
unique_id: u64,
free_vars: &[Term],
) -> Term {
let words = 7 + free_vars.len();
let ptr = process.heap_mut().alloc(words).expect("closure heap fits");
let heap = unsafe { std::slice::from_raw_parts_mut(ptr, words) };
write_closure(
heap,
module,
function_index,
arity,
generation,
unique_id,
free_vars,
)
.expect("closure layout fits")
}
fn heap_float(process: &mut Process, value: f64) -> Term {
let ptr = process.heap_mut().alloc(2).expect("float heap fits");
let heap = unsafe { std::slice::from_raw_parts_mut(ptr, 2) };
write_float(heap, value).expect("float layout fits")
}
fn heap_map(process: &mut Process, entries: &[(Term, Term)]) -> Term {
let words = 2 + entries.len() * 2;
let ptr = process.heap_mut().alloc(words).expect("map heap fits");
let heap = unsafe { std::slice::from_raw_parts_mut(ptr, words) };
let keys = entries.iter().map(|(key, _value)| *key).collect::<Vec<_>>();
let values = entries
.iter()
.map(|(_key, value)| *value)
.collect::<Vec<_>>();
write_map(heap, &keys, &values).expect("map layout fits")
}
fn test_module(name: Atom, code: Vec<Instruction>) -> Module {
let label_index = code
.iter()
.enumerate()
.filter_map(|(ip, instruction)| match instruction {
Instruction::Label { label } => Some((*label, ip)),
_ => None,
})
.collect();
Module {
name,
generation: 0,
origin: ModuleOrigin::Preloaded,
exports: HashMap::new(),
label_index,
code,
literals: Vec::new(),
constant_pool: Default::default(),
resolved_imports: Vec::new(),
lambdas: Vec::new(),
string_table: Vec::new(),
function_table: Vec::new(),
line_table: Vec::new(),
line_info: Vec::new(),
}
}
#[test]
fn compiles_return_only_function() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(&[Instruction::Return], Atom::MODULE, Atom::OK, 0)
.unwrap();
assert!(!native.call_ptr().is_null());
assert!(native.stack_maps().is_empty());
}
#[test]
fn compiled_move_writes_register_file() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Move {
source: Operand::Integer(42),
destination: Operand::X(1),
},
Instruction::Move {
source: Operand::X(1),
destination: Operand::Y(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![0; X_REGISTER_COUNT as usize + 1];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, 0);
assert_eq!(registers[1], Term::small_int(42).raw());
assert_eq!(
registers[X_REGISTER_COUNT as usize],
Term::small_int(42).raw()
);
}
#[test]
fn compiled_swap_reads_before_writing() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Swap {
left: Operand::X(0),
right: Operand::X(1),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![Term::small_int(2).raw(), Term::small_int(3).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(3).raw());
assert_eq!(registers[0], Term::small_int(3).raw());
assert_eq!(registers[1], Term::small_int(2).raw());
}
#[test]
fn compiled_add_returns_small_int_result() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(0),
Operand::Integer(2),
Operand::Integer(3),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![0; 1];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(5).raw());
assert_eq!(registers[0], Term::small_int(5).raw());
}
fn int_int_signature(name: &str) -> FunctionSignature {
FunctionSignature {
name: name.to_owned(),
arity: 2,
param_types: vec![TypeDescriptor::Int, TypeDescriptor::Int],
return_type: TypeDescriptor::Int,
}
}
#[test]
fn typed_add_returns_small_int_result() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = int_int_signature("add");
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(0),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
signature,
)
.unwrap();
let mut registers = vec![Term::small_int(5).raw(), Term::small_int(3).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(8).raw());
}
#[test]
fn typed_add_overflow_deopts_for_bignum_promotion() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = int_int_signature("add");
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(0),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
signature,
)
.unwrap();
let mut registers = vec![
Term::small_int(Term::SMALL_INT_MAX).raw(),
Term::small_int(1).raw(),
];
let mut process = Process::new(0, 233);
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_DEOPT);
assert_eq!(returned.value, JIT_DEOPT_SENTINEL as u64);
}
#[test]
fn typed_div_by_zero_takes_badarith_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = int_int_signature("div");
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(3),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
signature,
)
.unwrap();
let mut registers = vec![Term::small_int(10).raw(), Term::small_int(0).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::atom(Atom::BADARITH).raw());
}
#[test]
fn typed_mixed_known_unknown_arithmetic_materializes_known_operand_for_untyped_fallback() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "mixed_add".to_owned(),
arity: 2,
param_types: vec![TypeDescriptor::Int, TypeDescriptor::String],
return_type: TypeDescriptor::String,
};
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(0),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(99),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
signature,
)
.unwrap();
let mut registers = vec![Term::small_int(5).raw(), Term::small_int(3).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(8).raw());
}
#[test]
fn typed_div_min_by_minus_one_completes_without_overflow() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(3),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
int_int_signature("div"),
)
.unwrap();
let mut registers = vec![i64::MIN as u64, (-1i64) as u64];
let returned = call_native(&native, &mut registers);
assert_ne!(returned, JIT_DEOPT_SENTINEL as u64);
}
#[test]
fn typed_rem_by_zero_takes_badarith_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile_typed(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(4),
Operand::X(0),
Operand::X(1),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
int_int_signature("rem"),
)
.unwrap();
let mut registers = vec![Term::small_int(10).raw(), Term::small_int(0).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::atom(Atom::BADARITH).raw());
}
#[test]
fn compiled_add_at_end_falls_through_to_return_x0() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Label { label: 1 },
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(0),
Operand::Integer(2),
Operand::Integer(3),
Operand::X(0),
],
},
Instruction::Label { label: 9 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![0; 1];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(5).raw());
}
#[test]
fn compiled_multiply_overflow_takes_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Bif {
op: BifOp::Bif2,
operands: vec![
Operand::Label(9),
Operand::Unsigned(2),
Operand::Integer(Term::SMALL_INT_MAX),
Operand::Integer(Term::SMALL_INT_MAX),
Operand::X(0),
],
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(99),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![0; 1];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(99).raw());
}
#[test]
fn compiled_branch_takes_fail_label_on_false_comparison() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Comparison {
op: ComparisonOp::EqExact,
fail: Operand::Label(7),
left: Operand::Integer(1),
right: Operand::Integer(2),
},
Instruction::Move {
source: Operand::Integer(10),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::Integer(20),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![0; 1];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(20).raw());
}
#[test]
fn compiled_fconv_converts_small_integer_to_float_register() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fmove {
source: Operand::FloatRegister(0),
dest: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(42).raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let float = Float::new(Term::from_raw(registers[0])).expect("boxed float");
assert_eq!(float.value(), 42.0);
}
#[test]
fn compiled_fconv_accepts_boxed_float() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fmove {
source: Operand::FloatRegister(0),
dest: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let input = heap_float(&mut process, 2.75);
let mut registers = vec![input.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let float = Float::new(Term::from_raw(registers[0])).expect("boxed float");
assert_eq!(float.value(), 2.75);
}
#[test]
fn compiled_float_arithmetic_uses_float_registers() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fconv {
source: Operand::X(1),
dest: Operand::FloatRegister(1),
},
Instruction::Fadd {
fail: Operand::Label(9),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmove {
source: Operand::FloatRegister(2),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
let mut process = Process::new(0, 233);
let left = heap_float(&mut process, 1.5);
let right = heap_float(&mut process, 2.5);
let mut registers = vec![left.raw(), right.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let float = Float::new(Term::from_raw(registers[0])).expect("boxed float");
assert_eq!(float.value(), 4.0);
}
#[test]
fn compiled_fsub_fmul_and_fdiv_use_float_registers() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fconv {
source: Operand::X(1),
dest: Operand::FloatRegister(1),
},
Instruction::Fsub {
fail: Operand::Label(9),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmul {
fail: Operand::Label(9),
left: Operand::FloatRegister(2),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(3),
},
Instruction::Fdiv {
fail: Operand::Label(9),
left: Operand::FloatRegister(3),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(4),
},
Instruction::Fmove {
source: Operand::FloatRegister(4),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
let mut process = Process::new(0, 233);
let left = heap_float(&mut process, 5.5);
let right = heap_float(&mut process, 2.0);
let mut registers = vec![left.raw(), right.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let float = Float::new(Term::from_raw(registers[0])).expect("boxed float");
assert_eq!(float.value(), 3.5);
}
#[test]
fn compiled_fdiv_zero_takes_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fconv {
source: Operand::X(1),
dest: Operand::FloatRegister(1),
},
Instruction::Fdiv {
fail: Operand::Label(9),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmove {
source: Operand::FloatRegister(2),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
let mut process = Process::new(0, 233);
let left = heap_float(&mut process, 1.0);
let right = heap_float(&mut process, -0.0);
let mut registers = vec![left.raw(), right.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::atom(Atom::BADARITH).raw());
}
#[test]
fn compiled_fdiv_positive_zero_takes_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fconv {
source: Operand::X(1),
dest: Operand::FloatRegister(1),
},
Instruction::Fdiv {
fail: Operand::Label(9),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmove {
source: Operand::FloatRegister(2),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
let mut process = Process::new(0, 233);
let left = heap_float(&mut process, 1.0);
let right = heap_float(&mut process, 0.0);
let mut registers = vec![left.raw(), right.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::atom(Atom::BADARITH).raw());
}
#[test]
fn compiled_float_nan_or_inf_result_takes_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fconv {
source: Operand::X(1),
dest: Operand::FloatRegister(1),
},
Instruction::Fadd {
fail: Operand::Label(9),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmove {
source: Operand::FloatRegister(2),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
let mut process = Process::new(0, 233);
let left = heap_float(&mut process, f64::MAX);
let right = heap_float(&mut process, f64::MAX);
let mut registers = vec![left.raw(), right.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::atom(Atom::BADARITH).raw());
}
#[test]
fn compiled_fnegate_negates_float_register() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fnegate {
fail: Operand::Label(9),
source: Operand::FloatRegister(0),
dest: Operand::FloatRegister(1),
},
Instruction::Fmove {
source: Operand::FloatRegister(1),
dest: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARITH)),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let input = heap_float(&mut process, -2.75);
let mut registers = vec![input.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let float = Float::new(Term::from_raw(registers[0])).expect("boxed float");
assert_eq!(float.value(), 2.75);
}
#[test]
fn compiled_float_boxing_emits_safepoint() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fmove {
source: Operand::FloatRegister(0),
dest: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0)]
);
}
#[test]
fn compiled_put_list_emits_safepoint_and_allocates_cons() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutList {
head: Operand::X(0),
tail: Operand::Atom(None),
destination: Operand::X(1),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(native.stack_maps()[0].offset_from_entry, 0);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0), RootLocation::Register(1)]
);
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(7).raw(), Term::NIL.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(7).raw());
let cons = Cons::new(Term::from_raw(registers[1])).unwrap();
assert_eq!(cons.head(), Term::small_int(7));
assert_eq!(cons.tail(), Term::NIL);
}
#[test]
fn compiled_get_list_destructures_constructed_cons() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutList {
head: Operand::Integer(11),
tail: Operand::Atom(None),
destination: Operand::X(1),
},
Instruction::GetList {
source: Operand::X(1),
head: Operand::X(2),
tail: Operand::X(3),
},
Instruction::Move {
source: Operand::X(2),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
let mut process = Process::new(0, 233);
let mut registers = vec![0; 4];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(11).raw());
assert_eq!(registers[2], Term::small_int(11).raw());
assert_eq!(registers[3], Term::NIL.raw());
}
#[test]
fn typed_put_list_stores_tagged_int_and_typed_head_load_returns_tagged_result() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "typed_list".to_owned(),
arity: 1,
param_types: vec![TypeDescriptor::Int],
return_type: TypeDescriptor::Int,
};
let native = compiler
.compile_typed(
&[
Instruction::PutList {
head: Operand::X(0),
tail: Operand::Atom(None),
destination: Operand::X(1),
},
Instruction::GetHd {
source: Operand::X(1),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
signature,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(31).raw(), 0];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(31).raw());
let cons = Cons::new(Term::from_raw(registers[1])).unwrap();
assert_eq!(cons.head(), Term::small_int(31));
}
#[test]
fn compiled_get_list_read_is_pure_and_emits_no_safepoint() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::GetList {
source: Operand::X(1),
head: Operand::X(2),
tail: Operand::X(3),
},
Instruction::Move {
source: Operand::X(3),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert!(native.stack_maps().is_empty());
let mut cons_words = [0; 2];
let cons = write_cons(&mut cons_words, Term::small_int(23), Term::NIL).unwrap();
let mut registers = vec![0, cons.raw(), 0, 0];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::NIL.raw());
assert_eq!(registers[2], Term::small_int(23).raw());
assert_eq!(registers[3], Term::NIL.raw());
}
#[test]
fn compiled_get_hd_and_get_tl_read_cons_fields() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutList {
head: Operand::Integer(17),
tail: Operand::X(4),
destination: Operand::X(1),
},
Instruction::GetHd {
source: Operand::X(1),
destination: Operand::X(2),
},
Instruction::GetTl {
source: Operand::X(1),
destination: Operand::X(3),
},
Instruction::Move {
source: Operand::X(3),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
let mut process = Process::new(0, 233);
let mut registers = vec![0, 0, 0, 0, Term::NIL.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::NIL.raw());
assert_eq!(registers[2], Term::small_int(17).raw());
assert_eq!(registers[3], Term::NIL.raw());
}
#[test]
fn compiled_get_hd_and_get_tl_reads_are_pure_and_emit_no_safepoint() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::GetHd {
source: Operand::X(1),
destination: Operand::X(2),
},
Instruction::GetTl {
source: Operand::X(1),
destination: Operand::X(3),
},
Instruction::Move {
source: Operand::X(2),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert!(native.stack_maps().is_empty());
let mut cons_words = [0; 2];
let cons = write_cons(&mut cons_words, Term::small_int(29), Term::NIL).unwrap();
let mut registers = vec![0, cons.raw(), 0, 0];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(29).raw());
assert_eq!(registers[2], Term::small_int(29).raw());
assert_eq!(registers[3], Term::NIL.raw());
}
#[test]
fn compiled_put_tuple2_emits_safepoint_and_allocates_tuple() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutTuple2 {
destination: Operand::X(2),
elements: Operand::List(vec![Operand::X(0), Operand::Integer(9)]),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(native.stack_maps()[0].offset_from_entry, 0);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0), RootLocation::Register(2)]
);
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(4).raw(), 0, 0];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(4).raw());
let tuple = Tuple::new(Term::from_raw(registers[2])).unwrap();
assert_eq!(tuple.arity(), 2);
assert_eq!(tuple.get(0), Some(Term::small_int(4)));
assert_eq!(tuple.get(1), Some(Term::small_int(9)));
}
#[test]
fn compiled_put_map_assoc_emits_safepoint_and_allocates_map() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::MapOp {
op: MapOp::PutMapAssoc,
operands: vec![
Operand::Label(1),
Operand::X(1),
Operand::X(4),
Operand::Unsigned(0),
Operand::List(vec![
Operand::X(0),
Operand::X(2),
Operand::Atom(Some(Atom::ERROR)),
Operand::X(3),
]),
],
},
Instruction::Return,
Instruction::Label { label: 1 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(native.stack_maps()[0].offset_from_entry, 0);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![
RootLocation::Register(1),
RootLocation::Register(0),
RootLocation::Register(2),
RootLocation::Register(3),
RootLocation::Register(4),
]
);
let mut process = Process::new(0, 233);
let empty = heap_map(&mut process, &[]);
let mut registers = vec![
Term::atom(Atom::OK).raw(),
empty.raw(),
Term::small_int(1).raw(),
Term::small_int(2).raw(),
0,
];
let _returned = call_native_with_process(&native, &mut registers, &mut process);
let map = Map::new(Term::from_raw(registers[4])).unwrap();
assert_eq!(map.len(), 2);
assert_eq!(map.get(Term::atom(Atom::OK)), Some(Term::small_int(1)));
assert_eq!(map.get(Term::atom(Atom::ERROR)), Some(Term::small_int(2)));
}
#[test]
fn compiled_put_map_assoc_allocates_empty_map() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::MapOp {
op: MapOp::PutMapAssoc,
operands: vec![
Operand::Label(1),
Operand::X(1),
Operand::X(0),
Operand::Unsigned(0),
Operand::List(vec![]),
],
},
Instruction::Return,
Instruction::Label { label: 1 },
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut process = Process::new(0, 233);
let empty = heap_map(&mut process, &[]);
let mut registers = vec![0, empty.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, registers[0]);
let map = Map::new(Term::from_raw(registers[0])).unwrap();
assert!(map.is_empty());
}
#[test]
fn compiled_put_map_exact_updates_existing_key_and_fails_missing_key() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::MapOp {
op: MapOp::PutMapExact,
operands: vec![
Operand::Label(1),
Operand::X(1),
Operand::X(0),
Operand::Unsigned(0),
Operand::List(vec![Operand::Atom(Some(Atom::OK)), Operand::X(2)]),
],
},
Instruction::Return,
Instruction::Label { label: 1 },
Instruction::Move {
source: Operand::X(3),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut process = Process::new(0, 233);
let source = heap_map(&mut process, &[(Term::atom(Atom::OK), Term::small_int(1))]);
let mut registers = vec![0, source.raw(), Term::small_int(2).raw(), Term::NIL.raw()];
let _returned = call_native_with_process(&native, &mut registers, &mut process);
let updated = Map::new(Term::from_raw(registers[0])).unwrap();
assert_eq!(updated.get(Term::atom(Atom::OK)), Some(Term::small_int(2)));
assert_eq!(
Map::new(source).unwrap().get(Term::atom(Atom::OK)),
Some(Term::small_int(1))
);
let missing_source = heap_map(
&mut process,
&[(Term::atom(Atom::ERROR), Term::small_int(1))],
);
let mut registers = vec![
0,
missing_source.raw(),
Term::small_int(3).raw(),
Term::NIL.raw(),
];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::NIL.raw());
}
#[test]
fn compiled_map_pattern_ops_are_read_only_and_emit_no_safepoints() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::MapOp {
op: MapOp::HasMapFields,
operands: vec![
Operand::Label(1),
Operand::X(1),
Operand::List(vec![Operand::Atom(Some(Atom::OK))]),
],
},
Instruction::MapOp {
op: MapOp::GetMapElements,
operands: vec![
Operand::Label(1),
Operand::X(1),
Operand::List(vec![Operand::Atom(Some(Atom::OK)), Operand::X(0)]),
],
},
Instruction::Return,
Instruction::Label { label: 1 },
Instruction::Move {
source: Operand::X(2),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert!(native.stack_maps().is_empty());
let mut process = Process::new(0, 233);
let source = heap_map(&mut process, &[(Term::atom(Atom::OK), Term::small_int(42))]);
let mut registers = vec![0, source.raw(), Term::NIL.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(42).raw());
let missing = heap_map(
&mut process,
&[(Term::atom(Atom::ERROR), Term::small_int(42))],
);
let mut registers = vec![0, missing.raw(), Term::NIL.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::NIL.raw());
}
#[test]
fn typed_put_tuple_stores_tagged_int_and_typed_element_load_returns_tagged_result() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "typed_tuple".to_owned(),
arity: 1,
param_types: vec![TypeDescriptor::Int],
return_type: TypeDescriptor::Int,
};
let native = compiler
.compile_typed(
&[
Instruction::PutTuple2 {
destination: Operand::X(1),
elements: Operand::List(vec![Operand::X(0)]),
},
Instruction::GetTupleElement {
source: Operand::X(1),
index: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
signature,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(41).raw(), 0];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(41).raw());
let tuple = Tuple::new(Term::from_raw(registers[1])).unwrap();
assert_eq!(tuple.get(0), Some(Term::small_int(41)));
}
#[test]
fn compiled_get_tuple_element_reads_constructed_tuple() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutTuple2 {
destination: Operand::X(1),
elements: Operand::List(vec![Operand::Integer(4), Operand::Integer(9)]),
},
Instruction::GetTupleElement {
source: Operand::X(1),
index: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
let mut process = Process::new(0, 233);
let mut registers = vec![0; 2];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(9).raw());
let tuple = Tuple::new(Term::from_raw(registers[1])).unwrap();
assert_eq!(tuple.get(0), Some(Term::small_int(4)));
assert_eq!(tuple.get(1), Some(Term::small_int(9)));
}
#[test]
fn compiled_allocation_with_tiny_heap_survives_gc() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::PutTuple2 {
destination: Operand::X(1),
elements: Operand::List(vec![Operand::X(0)]),
},
Instruction::PutTuple2 {
destination: Operand::X(2),
elements: Operand::List(vec![Operand::X(1), Operand::Integer(8)]),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 2);
let mut process = Process::new(0, 2);
process.set_x_reg(0, Term::small_int(3));
let returned = call_native_with_process_x_regs(&native, &mut process);
assert_eq!(returned, Term::small_int(3).raw());
let outer = Tuple::new(process.x_reg(2)).unwrap();
assert_eq!(outer.arity(), 2);
let inner = Tuple::new(outer.get(0).unwrap()).unwrap();
assert_eq!(inner.get(0), Some(Term::small_int(3)));
assert_eq!(outer.get(1), Some(Term::small_int(8)));
}
#[test]
fn compiled_make_fun_captures_two_free_vars_and_records_safepoint_roots() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let lambdas = vec![test_lambda(Atom::OK, 1, 7, 2, 0xfeed)];
let native = compiler
.compile_module_function(
&[
Instruction::MakeFun {
operands: vec![Operand::Unsigned(0)],
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
ModuleCompileMetadata {
lambdas: &lambdas,
generation: 9,
},
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(native.stack_maps()[0].offset_from_entry, 0);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0), RootLocation::Register(1),]
);
let mut process = Process::new(0, 233);
let mut registers = vec![Term::small_int(11).raw(), Term::atom(Atom::ERROR).raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
let closure = Closure::new(Term::from_raw(returned)).expect("closure term");
assert_eq!(registers[0], returned);
assert_eq!(closure.module(), Some(Atom::MODULE));
assert_eq!(closure.function_index(), 0);
assert_eq!(closure.arity(), 1);
assert_eq!(closure.num_free(), 2);
assert_eq!(closure.generation(), 9);
assert_eq!(closure.unique_id(), 0xfeed);
assert_eq!(closure.free_var(0), Some(Term::small_int(11)));
assert_eq!(closure.free_var(1), Some(Term::atom(Atom::ERROR)));
}
#[test]
fn compiled_make_fun_creates_valid_zero_capture_closure() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let lambdas = vec![test_lambda(Atom::OK, 0, 7, 0, 0xbeef)];
let native = compiler
.compile_module_function(
&[
Instruction::MakeFun {
operands: vec![
Operand::Unsigned(0),
Operand::Unsigned(0),
Operand::Unsigned(0),
],
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
ModuleCompileMetadata {
lambdas: &lambdas,
generation: 4,
},
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0)]
);
let mut process = Process::new(0, 233);
let mut registers = vec![0];
let returned = call_native_with_process(&native, &mut registers, &mut process);
let closure = Closure::new(Term::from_raw(returned)).expect("closure term");
assert_eq!(closure.module(), Some(Atom::MODULE));
assert_eq!(closure.arity(), 0);
assert_eq!(closure.num_free(), 0);
assert_eq!(closure.free_var(0), None);
}
#[test]
fn compiled_call_fun_copies_free_vars_after_explicit_args_and_returns_value() {
let caller_atom = Atom::MODULE;
let function_atom = Atom::OK;
let unique_id = 0xabcddcba;
let mut module = test_module(
caller_atom,
vec![
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::X(1),
destination: Operand::X(0),
},
Instruction::Return,
],
);
module
.lambdas
.push(test_lambda(function_atom, 1, 7, 1, unique_id));
module.function_table.push((0, function_atom, 1));
let registry = ModuleRegistry::new();
let module = registry.insert(module);
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[Instruction::CallFun {
arity: Operand::Unsigned(1),
}],
caller_atom,
function_atom,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let closure = heap_closure(
&mut process,
caller_atom,
0,
1,
module.generation(),
unique_id,
&[Term::small_int(99)],
);
process.set_current_module(module.clone());
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
module.as_ref() as *const Module,
®istry as *const ModuleRegistry,
std::ptr::null(),
)));
let mut registers = vec![Term::small_int(5).raw(), closure.raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(99).raw());
assert_eq!(process.x_reg(0), Term::small_int(99));
assert_eq!(process.x_reg(1), Term::small_int(99));
}
#[test]
fn compiled_apply_wrong_arity_raises_badarity() {
let caller_atom = Atom::MODULE;
let function_atom = Atom::OK;
let unique_id = 0x1234;
let mut module = test_module(
caller_atom,
vec![Instruction::Label { label: 7 }, Instruction::Return],
);
module
.lambdas
.push(test_lambda(function_atom, 2, 7, 0, unique_id));
let registry = ModuleRegistry::new();
let module = registry.insert(module);
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[Instruction::Apply {
arity: Operand::Unsigned(1),
}],
caller_atom,
function_atom,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let closure = heap_closure(
&mut process,
caller_atom,
0,
2,
module.generation(),
unique_id,
&[],
);
process.set_current_module(module.clone());
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
module.as_ref() as *const Module,
®istry as *const ModuleRegistry,
std::ptr::null(),
)));
let mut registers = vec![Term::small_int(5).raw(), closure.raw()];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_EXCEPTION);
assert_eq!(returned.value, Term::atom(Atom::BADARITY).raw());
let exception = process
.current_exception()
.expect("badarity exception state");
assert_eq!(exception.class, Term::atom(Atom::ERROR));
assert_eq!(exception.reason, Term::atom(Atom::BADARITY));
}
#[test]
fn compiled_make_fun_survives_gc_before_apply() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let lambdas = vec![test_lambda(Atom::OK, 0, 7, 1, 0xcafe)];
let native = compiler
.compile_module_function(
&[
Instruction::MakeFun {
operands: vec![Operand::Unsigned(0)],
},
Instruction::PutTuple2 {
destination: Operand::X(2),
elements: Operand::List(vec![Operand::X(0)]),
},
Instruction::CallFun {
arity: Operand::Unsigned(0),
},
],
Atom::MODULE,
Atom::OK,
0,
ModuleCompileMetadata {
lambdas: &lambdas,
generation: 0,
},
)
.unwrap();
assert_eq!(native.stack_maps().len(), 2);
assert_eq!(
native.stack_maps()[1].live_roots,
vec![RootLocation::Register(0), RootLocation::Register(2)]
);
}
#[test]
fn compiled_is_integer_distinguishes_integer_from_atom() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::TypeTest {
op: TypeTestOp::IsInteger,
fail: Operand::Label(7),
value: Operand::X(0),
},
Instruction::Move {
source: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut integer_registers = vec![Term::small_int(42).raw()];
assert_eq!(
call_native(&native, &mut integer_registers),
Term::small_int(1).raw()
);
let mut atom_registers = vec![Term::atom(Atom::OK).raw()];
assert_eq!(
call_native(&native, &mut atom_registers),
Term::small_int(0).raw()
);
}
#[test]
fn typed_is_integer_guard_elides_known_int_and_returns_tagged_value() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "guard".to_owned(),
arity: 1,
param_types: vec![TypeDescriptor::Int],
return_type: TypeDescriptor::Int,
};
let native = compiler
.compile_typed(
&[
Instruction::TypeTest {
op: TypeTestOp::IsInteger,
fail: Operand::Label(7),
value: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
signature,
)
.unwrap();
let mut registers = vec![Term::small_int(55).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(55).raw());
}
#[test]
fn typed_is_atom_guard_on_known_int_jumps_to_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "guard".to_owned(),
arity: 1,
param_types: vec![TypeDescriptor::Int],
return_type: TypeDescriptor::Int,
};
let native = compiler
.compile_typed(
&[
Instruction::TypeTest {
op: TypeTestOp::IsAtom,
fail: Operand::Label(7),
value: Operand::X(0),
},
Instruction::Move {
source: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
signature,
)
.unwrap();
let mut registers = vec![Term::small_int(55).raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(0).raw());
}
#[test]
fn typed_test_arity_uses_known_tuple_length() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let signature = FunctionSignature {
name: "arity".to_owned(),
arity: 1,
param_types: vec![TypeDescriptor::Tuple(vec![
TypeDescriptor::Int,
TypeDescriptor::Int,
])],
return_type: TypeDescriptor::Int,
};
let native = compiler
.compile_typed(
&[
Instruction::TestArity {
fail: Operand::Label(7),
tuple: Operand::X(0),
arity: Operand::Integer(3),
},
Instruction::Move {
source: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 7 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
signature,
)
.unwrap();
let mut tuple_words = [0; 3];
let tuple = write_tuple(&mut tuple_words, &[Term::small_int(1), Term::small_int(2)]).unwrap();
let mut registers = vec![tuple.raw()];
let returned = call_native(&native, &mut registers);
assert_eq!(returned, Term::small_int(0).raw());
}
#[test]
fn compiled_pattern_match_on_ok_tuple() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::IsTaggedTuple {
fail: Operand::Label(9),
value: Operand::X(0),
arity: Operand::Unsigned(2),
tag: Operand::Atom(Some(Atom::OK)),
},
Instruction::Move {
source: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut tuple_words = [0; 3];
let tuple = write_tuple(
&mut tuple_words,
&[Term::atom(Atom::OK), Term::small_int(42)],
)
.unwrap();
let mut registers = vec![tuple.raw()];
assert_eq!(
call_native(&native, &mut registers),
Term::small_int(1).raw()
);
}
#[test]
fn compiled_select_val_dispatches_matching_atom() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::SelectVal {
value: Operand::X(0),
fail: Operand::Label(9),
list: Operand::List(vec![
Operand::Atom(Some(Atom::OK)),
Operand::Label(2),
Operand::Integer(7),
Operand::Label(3),
]),
},
Instruction::Label { label: 2 },
Instruction::Move {
source: Operand::Integer(20),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 3 },
Instruction::Move {
source: Operand::Integer(30),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(90),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![Term::atom(Atom::OK).raw()];
assert_eq!(
call_native(&native, &mut registers),
Term::small_int(20).raw()
);
}
#[test]
fn compiled_select_val_does_not_fall_through_after_match() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::SelectVal {
value: Operand::X(0),
fail: Operand::Label(9),
list: Operand::List(vec![Operand::Integer(7), Operand::Label(2)]),
},
Instruction::Move {
source: Operand::Integer(99),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 2 },
Instruction::Move {
source: Operand::Integer(20),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(90),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut registers = vec![Term::small_int(7).raw()];
assert_eq!(
call_native(&native, &mut registers),
Term::small_int(20).raw()
);
}
#[test]
fn compiled_zero_arity_is_tagged_tuple_takes_fail_label() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::IsTaggedTuple {
fail: Operand::Label(9),
value: Operand::X(0),
arity: Operand::Unsigned(0),
tag: Operand::Atom(Some(Atom::OK)),
},
Instruction::Move {
source: Operand::Integer(1),
destination: Operand::X(0),
},
Instruction::Return,
Instruction::Label { label: 9 },
Instruction::Move {
source: Operand::Integer(0),
destination: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut tuple_words = [0; 1];
let tuple = write_tuple(&mut tuple_words, &[]).unwrap();
let mut registers = vec![tuple.raw()];
assert_eq!(
call_native(&native, &mut registers),
Term::small_int(0).raw()
);
}
#[test]
fn compiled_external_call_falls_back_to_interpreter_and_returns_value() {
let caller_atom = Atom::MODULE;
let target_atom = Atom::ERROR;
let function_atom = Atom::OK;
let mut caller = test_module(
caller_atom,
vec![Instruction::CallExtOnly {
arity: Operand::Unsigned(1),
import: Operand::Unsigned(0),
}],
);
caller.resolved_imports.push(ResolvedImport {
module: target_atom,
function: function_atom,
arity: 1,
target: ResolvedImportTarget::Code {
module: target_atom,
label: 1,
},
});
let mut target = test_module(
target_atom,
vec![
Instruction::Label { label: 1 },
Instruction::Move {
source: Operand::X(0),
destination: Operand::X(0),
},
Instruction::Return,
],
);
target.exports.insert((function_atom, 1), 1);
let registry = ModuleRegistry::new();
let caller = registry.insert(caller);
let _target = registry.insert(target);
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(&caller.code, caller_atom, function_atom, 1)
.unwrap();
let mut process = Process::new(0, 233);
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
caller.as_ref() as *const Module,
®istry as *const ModuleRegistry,
std::ptr::null(),
)));
let mut registers = vec![Term::small_int(17).raw()];
let returned = call_native_with_process(&native, &mut registers, &mut process);
assert_eq!(returned, Term::small_int(17).raw());
assert_eq!(registers[0], Term::small_int(17).raw());
}
#[test]
fn compiled_try_catches_interpreted_exception_and_exposes_payload() {
let caller_atom = Atom::MODULE;
let target_atom = Atom::ERROR;
let function_atom = Atom::OK;
let mut caller = test_module(
caller_atom,
vec![
Instruction::Try {
destination: Operand::Y(0),
label: Operand::Label(20),
},
Instruction::Move {
source: Operand::Atom(Some(Atom::ERROR)),
destination: Operand::X(0),
},
Instruction::Move {
source: Operand::Atom(Some(Atom::BADARG)),
destination: Operand::X(1),
},
Instruction::Move {
source: Operand::Atom(None),
destination: Operand::X(2),
},
Instruction::CallExtOnly {
arity: Operand::Unsigned(3),
import: Operand::Unsigned(0),
},
Instruction::Label { label: 20 },
Instruction::TryCase {
source: Operand::Y(0),
},
Instruction::Return,
],
);
caller.resolved_imports.push(ResolvedImport {
module: target_atom,
function: function_atom,
arity: 3,
target: ResolvedImportTarget::Code {
module: target_atom,
label: 1,
},
});
let mut target = test_module(
target_atom,
vec![
Instruction::Label { label: 1 },
Instruction::RawRaise,
Instruction::Return,
],
);
target.exports.insert((function_atom, 3), 1);
let registry = ModuleRegistry::new();
let caller = registry.insert(caller);
let _target = registry.insert(target);
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(&caller.code, caller_atom, function_atom, 0)
.unwrap();
let mut process = Process::new(0, 233);
process.set_current_module(caller.clone());
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
caller.as_ref() as *const Module,
®istry as *const ModuleRegistry,
std::ptr::null(),
)));
let mut registers = vec![0; X_REGISTER_COUNT as usize + 3];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_NORMAL);
assert_eq!(returned.value, Term::atom(Atom::ERROR).raw());
assert_eq!(registers[0], Term::atom(Atom::ERROR).raw());
assert_eq!(registers[1], Term::atom(Atom::BADARG).raw());
assert_eq!(registers[2], Term::NIL.raw());
assert_eq!(process.current_exception(), None);
}
#[test]
fn compiled_external_exception_without_try_propagates_status_and_frame() {
let caller_atom = Atom::MODULE;
let target_atom = Atom::ERROR;
let function_atom = Atom::OK;
let mut caller = test_module(
caller_atom,
vec![Instruction::CallExtOnly {
arity: Operand::Unsigned(3),
import: Operand::Unsigned(0),
}],
);
caller.resolved_imports.push(ResolvedImport {
module: target_atom,
function: function_atom,
arity: 3,
target: ResolvedImportTarget::Code {
module: target_atom,
label: 1,
},
});
let mut target = test_module(
target_atom,
vec![
Instruction::Label { label: 1 },
Instruction::RawRaise,
Instruction::Return,
],
);
target.exports.insert((function_atom, 3), 1);
let registry = ModuleRegistry::new();
let caller = registry.insert(caller);
let _target = registry.insert(target);
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(&caller.code, caller_atom, function_atom, 0)
.unwrap();
let mut process = Process::new(0, 233);
process.set_current_module(caller.clone());
process.set_jit_runtime_context(Some(JitRuntimeContext::new(
caller.as_ref() as *const Module,
®istry as *const ModuleRegistry,
std::ptr::null(),
)));
let mut registers = vec![
Term::atom(Atom::ERROR).raw(),
Term::atom(Atom::BADARG).raw(),
Term::NIL.raw(),
];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_EXCEPTION);
assert_eq!(returned.value, Term::atom(Atom::BADARG).raw());
let exception = process
.current_exception()
.expect("exception state preserved");
assert_eq!(exception.class, Term::atom(Atom::ERROR));
assert_eq!(exception.reason, Term::atom(Atom::BADARG));
assert!(
process
.raw_stacktrace()
.iter()
.any(|entry| entry.mfa == Some((caller_atom, function_atom, 0)))
);
}
#[test]
fn compiled_local_call_charges_reduction_and_yields_when_exhausted() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::Label { label: 1 },
Instruction::CallOnly {
arity: Operand::Unsigned(0),
label: Operand::Label(1),
},
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut process = Process::new(0, 233);
process.reset_reductions(3);
let mut registers = vec![0];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_YIELD);
assert_eq!(
returned.value,
crate::jit::runtime::JIT_YIELD_SENTINEL as u64,
);
assert_eq!(process.reduction_counter(), 0);
}
#[test]
fn compiled_external_call_returns_deopt_sentinel_without_runtime_context() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[Instruction::CallExtOnly {
arity: Operand::Unsigned(0),
import: Operand::Unsigned(0),
}],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
let mut process = Process::new(0, 233);
let mut registers = vec![0];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_DEOPT);
assert_eq!(returned.value, JIT_DEOPT_SENTINEL as u64);
assert_eq!(registers[0], 0);
}
#[test]
fn compiles_message_send_and_selective_receive_opcodes() {
let compiler = JitCompiler::new(JitSettings).unwrap();
compiler
.compile(
&[
Instruction::Send,
Instruction::Label { label: 1 },
Instruction::LoopRec {
fail: Operand::Label(2),
destination: Operand::X(0),
},
Instruction::RemoveMessage,
Instruction::Return,
Instruction::LoopRecEnd {
fail: Operand::Label(1),
},
Instruction::Label { label: 2 },
Instruction::Wait {
fail: Operand::Label(1),
},
],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
}
#[test]
fn compiled_send_records_destination_and_message_safepoint_roots() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[Instruction::Send, Instruction::Return],
Atom::MODULE,
Atom::OK,
2,
)
.unwrap();
assert_eq!(native.stack_maps().len(), 1);
assert_eq!(native.stack_maps()[0].offset_from_entry, 0);
assert_eq!(
native.stack_maps()[0].live_roots,
vec![RootLocation::Register(0), RootLocation::Register(1)]
);
}
#[test]
fn compiles_wait_timeout_and_timeout_opcodes() {
let compiler = JitCompiler::new(JitSettings).unwrap();
compiler
.compile(
&[
Instruction::Label { label: 1 },
Instruction::LoopRec {
fail: Operand::Label(2),
destination: Operand::X(0),
},
Instruction::LoopRecEnd {
fail: Operand::Label(1),
},
Instruction::Label { label: 2 },
Instruction::WaitTimeout {
fail: Operand::Label(3),
timeout: Operand::Unsigned(0),
},
Instruction::Label { label: 3 },
Instruction::Timeout,
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap();
}
#[test]
fn recv_marker_opcodes_compile_to_deopt() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let native = compiler
.compile(
&[
Instruction::RecvMarkerUse {
marker: Operand::X(0),
},
Instruction::Return,
],
Atom::MODULE,
Atom::OK,
1,
)
.unwrap();
let mut process = Process::new(0, 233);
let mut registers = vec![Term::NIL.raw()];
let returned = call_native_status(&native, &mut registers, &mut process);
assert_eq!(returned.status, JIT_STATUS_DEOPT);
assert_eq!(returned.value, JIT_DEOPT_SENTINEL as u64);
}
#[test]
fn reports_unsupported_opcode() {
let compiler = JitCompiler::new(JitSettings).unwrap();
let error = compiler
.compile(
&[Instruction::Generic {
opcode: 255,
name: "unknown",
operands: Vec::new(),
}],
Atom::MODULE,
Atom::OK,
0,
)
.unwrap_err();
assert_eq!(
error,
JitError::UnsupportedOpcode {
opcode: "unknown (255)".to_owned()
}
);
}