use crate::error::ExecError;
use crate::gc::GcError;
use crate::interpreter::InstructionOutcome;
use crate::loader::decode::Operand;
use crate::module::Module;
use crate::process::{Process, ProcessError};
use crate::term::boxed::{Float, write_float};
use super::core;
const BOXED_FLOAT_WORDS: usize = 2;
pub fn fmove(
process: &mut Process,
module: &Module,
source: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
match (source, dest) {
(Operand::FloatRegister(_), Operand::FloatRegister(_)) => {
let value = read_float_register(process, source)?;
write_float_register(process, dest, value)?;
}
(Operand::FloatRegister(_), _) if is_term_destination(dest) => {
let value = read_float_register(process, source)?;
let term = allocate_boxed_float(process, value)?;
core::write_term(process, dest, term)?;
}
(_, Operand::FloatRegister(_)) => {
let value = read_boxed_float(process, module, source)?;
write_float_register(process, dest, value)?;
}
_ => return Err(ExecError::InvalidOperand("fmove")),
}
Ok(InstructionOutcome::Continue)
}
pub fn fconv(
process: &mut Process,
module: &Module,
source: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let value = read_number_as_float(process, module, source)?;
write_float_register(process, dest, value)?;
Ok(InstructionOutcome::Continue)
}
pub fn fadd(
process: &mut Process,
left: &Operand,
right: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
arithmetic(process, left, right, dest, |left, right| Ok(left + right))
}
pub fn fsub(
process: &mut Process,
left: &Operand,
right: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
arithmetic(process, left, right, dest, |left, right| Ok(left - right))
}
pub fn fmul(
process: &mut Process,
left: &Operand,
right: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
arithmetic(process, left, right, dest, |left, right| Ok(left * right))
}
pub fn fdiv(
process: &mut Process,
left: &Operand,
right: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
arithmetic(process, left, right, dest, |left, right| {
if is_zero(right) {
Err(ExecError::Badarith)
} else {
Ok(left / right)
}
})
}
fn is_zero(value: f64) -> bool {
value.to_bits() & 0x7fff_ffff_ffff_ffff == 0
}
pub fn fnegate(
process: &mut Process,
source: &Operand,
dest: &Operand,
) -> Result<InstructionOutcome, ExecError> {
let value = read_float_register(process, source)?;
write_float_register(process, dest, -value)?;
Ok(InstructionOutcome::Continue)
}
fn arithmetic(
process: &mut Process,
left: &Operand,
right: &Operand,
dest: &Operand,
op: impl FnOnce(f64, f64) -> Result<f64, ExecError>,
) -> Result<InstructionOutcome, ExecError> {
let left = read_float_register(process, left)?;
let right = read_float_register(process, right)?;
let value = op(left, right)?;
write_float_register(process, dest, value)?;
Ok(InstructionOutcome::Continue)
}
fn is_term_destination(operand: &Operand) -> bool {
match operand {
Operand::X(_) | Operand::Y(_) => true,
Operand::TypedRegister { register, .. } => is_term_destination(register),
_ => false,
}
}
fn read_float_register(process: &Process, operand: &Operand) -> Result<f64, ExecError> {
let Operand::FloatRegister(index) = operand else {
return Err(ExecError::InvalidOperand("float register source"));
};
let index = u16::try_from(*index).map_err(|_| ExecError::InvalidOperand("float register"))?;
process.get_float_reg(index).map_err(process_error_to_exec)
}
fn write_float_register(
process: &mut Process,
operand: &Operand,
value: f64,
) -> Result<(), ExecError> {
let Operand::FloatRegister(index) = operand else {
return Err(ExecError::InvalidOperand("float register destination"));
};
let index = u16::try_from(*index).map_err(|_| ExecError::InvalidOperand("float register"))?;
process
.set_float_reg(index, value)
.map_err(process_error_to_exec)
}
fn read_boxed_float(
process: &Process,
module: &Module,
operand: &Operand,
) -> Result<f64, ExecError> {
let term = core::read_term(process, module, operand)?;
Float::new(term)
.map(Float::value)
.ok_or(ExecError::Badarith)
}
fn read_number_as_float(
process: &Process,
module: &Module,
operand: &Operand,
) -> Result<f64, ExecError> {
let term = core::read_term(process, module, operand)?;
if let Some(integer) = term.as_small_int() {
return Ok(integer as f64);
}
Float::new(term)
.map(Float::value)
.ok_or(ExecError::Badarith)
}
fn allocate_boxed_float(process: &mut Process, value: f64) -> Result<crate::term::Term, ExecError> {
let ptr = crate::gc::alloc(process, BOXED_FLOAT_WORDS).map_err(gc_error_to_exec)?;
let heap = core::heap_slice(ptr, BOXED_FLOAT_WORDS);
write_float(heap, value).ok_or(ExecError::Badarg)
}
fn process_error_to_exec(error: ProcessError) -> ExecError {
match error {
ProcessError::InvalidFloatRegister { .. } => ExecError::InvalidOperand("float register"),
ProcessError::InvalidStatusTransition { .. } => ExecError::Badarg,
}
}
fn gc_error_to_exec(error: GcError) -> ExecError {
match error {
GcError::HeapFull(error) => ExecError::from(error),
GcError::InvalidObjectHeader(_) => ExecError::Badarg,
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::atom::Atom;
use crate::loader::Instruction;
use crate::module::{Module, ModuleOrigin};
use crate::term::Term;
use crate::term::boxed::{Float, write_float};
use super::*;
fn module() -> Module {
Module {
name: Atom::OK,
generation: 0,
origin: ModuleOrigin::Preloaded,
exports: HashMap::new(),
label_index: HashMap::new(),
code: Vec::new(),
literals: Vec::new(),
constant_pool: Default::default(),
resolved_imports: Vec::new(),
lambdas: Vec::new(),
string_table: Vec::new(),
line_info: Vec::new(),
function_table: Vec::new(),
line_table: Vec::new(),
}
}
fn set_x_float(process: &mut Process, register: u16, value: f64) {
let ptr = process
.heap_mut()
.alloc(BOXED_FLOAT_WORDS)
.expect("test process has room for float");
let heap = core::heap_slice(ptr, BOXED_FLOAT_WORDS);
let term = write_float(heap, value).expect("test float fits in two words");
process.set_x_reg(register, term);
}
#[test]
fn fmove_moves_boxed_float_to_float_register() {
let module = module();
let mut process = Process::new(0, 16);
set_x_float(&mut process, 0, 2.75);
assert_eq!(
fmove(
&mut process,
&module,
&Operand::X(0),
&Operand::FloatRegister(0),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(0), Ok(2.75));
}
#[test]
fn fmove_moves_float_register_to_boxed_float_term() {
let module = module();
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, 2.75), Ok(()));
assert_eq!(
fmove(
&mut process,
&module,
&Operand::FloatRegister(0),
&Operand::X(0),
),
Ok(InstructionOutcome::Continue)
);
let float = Float::new(process.x_reg(0)).expect("boxed float");
assert_eq!(float.value(), 2.75);
}
#[test]
fn fconv_converts_small_integer_to_float_register() {
let module = module();
let mut process = Process::new(0, 16);
process.set_x_reg(0, Term::small_int(42));
assert_eq!(
fconv(
&mut process,
&module,
&Operand::X(0),
&Operand::FloatRegister(0),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(0), Ok(42.0));
}
#[test]
fn fconv_accepts_boxed_float_and_rejects_non_number() {
let module = module();
let mut process = Process::new(0, 16);
set_x_float(&mut process, 0, 2.5);
process.set_x_reg(1, Term::atom(Atom::OK));
assert_eq!(
fconv(
&mut process,
&module,
&Operand::X(0),
&Operand::FloatRegister(0),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(0), Ok(2.5));
assert_eq!(
fconv(
&mut process,
&module,
&Operand::X(1),
&Operand::FloatRegister(0),
),
Err(ExecError::Badarith)
);
}
#[test]
fn float_arithmetic_handles_normal_values() {
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, 1.5), Ok(()));
assert_eq!(process.set_float_reg(1, 2.5), Ok(()));
assert_eq!(
fadd(
&mut process,
&Operand::FloatRegister(0),
&Operand::FloatRegister(1),
&Operand::FloatRegister(2),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(2), Ok(4.0));
assert_eq!(
fsub(
&mut process,
&Operand::FloatRegister(1),
&Operand::FloatRegister(0),
&Operand::FloatRegister(3),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(3), Ok(1.0));
assert_eq!(
fmul(
&mut process,
&Operand::FloatRegister(0),
&Operand::FloatRegister(1),
&Operand::FloatRegister(4),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(4), Ok(3.75));
}
#[test]
fn fdiv_by_zero_is_badarith() {
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, 10.0), Ok(()));
assert_eq!(process.set_float_reg(1, 0.0), Ok(()));
assert_eq!(
fdiv(
&mut process,
&Operand::FloatRegister(0),
&Operand::FloatRegister(1),
&Operand::FloatRegister(2),
),
Err(ExecError::Badarith)
);
}
#[test]
fn fdiv_and_fnegate_handle_float_values() {
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, 10.0), Ok(()));
assert_eq!(process.set_float_reg(1, 2.0), Ok(()));
assert_eq!(process.set_float_reg(3, 2.75), Ok(()));
assert_eq!(
fdiv(
&mut process,
&Operand::FloatRegister(0),
&Operand::FloatRegister(1),
&Operand::FloatRegister(2),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(2), Ok(5.0));
assert_eq!(
fnegate(
&mut process,
&Operand::FloatRegister(3),
&Operand::FloatRegister(4),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(4), Ok(-2.75));
}
#[test]
fn arithmetic_preserves_nan_and_infinity() {
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, f64::NAN), Ok(()));
assert_eq!(process.set_float_reg(1, 1.0), Ok(()));
assert_eq!(process.set_float_reg(2, f64::INFINITY), Ok(()));
assert_eq!(
fadd(
&mut process,
&Operand::FloatRegister(0),
&Operand::FloatRegister(1),
&Operand::FloatRegister(3),
),
Ok(InstructionOutcome::Continue)
);
assert!(process.get_float_reg(3).is_ok_and(f64::is_nan));
assert_eq!(
fmul(
&mut process,
&Operand::FloatRegister(2),
&Operand::FloatRegister(1),
&Operand::FloatRegister(4),
),
Ok(InstructionOutcome::Continue)
);
assert_eq!(process.get_float_reg(4), Ok(f64::INFINITY));
}
#[test]
fn fmove_to_x_register_allocates_through_gc() {
let module = module();
let mut process = Process::new(0, 1);
assert_eq!(process.set_float_reg(0, 6.25), Ok(()));
assert_eq!(
fmove(
&mut process,
&module,
&Operand::FloatRegister(0),
&Operand::X(0),
),
Ok(InstructionOutcome::Continue)
);
let float = Float::new(process.x_reg(0)).expect("boxed float after GC allocation");
assert_eq!(float.value(), 6.25);
}
#[test]
fn fmove_to_invalid_term_destination_does_not_allocate() {
let module = module();
let mut process = Process::new(0, 16);
assert_eq!(process.set_float_reg(0, 6.25), Ok(()));
let used_before = process.heap().young_used();
assert_eq!(
fmove(
&mut process,
&module,
&Operand::FloatRegister(0),
&Operand::Unsigned(0),
),
Err(ExecError::InvalidOperand("fmove"))
);
assert_eq!(process.heap().young_used(), used_before);
}
#[test]
fn float_dispatch_style_instruction_sequence_adds_one_point_zero() {
let module = module();
let mut process = Process::new(0, 16);
process.set_x_reg(0, Term::small_int(41));
assert_eq!(process.set_float_reg(1, 1.0), Ok(()));
let instructions = [
Instruction::Fconv {
source: Operand::X(0),
dest: Operand::FloatRegister(0),
},
Instruction::Fadd {
fail: Operand::Label(0),
left: Operand::FloatRegister(0),
right: Operand::FloatRegister(1),
dest: Operand::FloatRegister(2),
},
Instruction::Fmove {
source: Operand::FloatRegister(2),
dest: Operand::X(0),
},
];
for instruction in instructions {
match instruction {
Instruction::Fconv { source, dest } => {
assert_eq!(
fconv(&mut process, &module, &source, &dest),
Ok(InstructionOutcome::Continue)
);
}
Instruction::Fadd {
left, right, dest, ..
} => {
assert_eq!(
fadd(&mut process, &left, &right, &dest),
Ok(InstructionOutcome::Continue)
);
}
Instruction::Fmove { source, dest } => {
assert_eq!(
fmove(&mut process, &module, &source, &dest),
Ok(InstructionOutcome::Continue)
);
}
_ => unreachable!("test only builds float instructions"),
}
}
let float = Float::new(process.x_reg(0)).expect("boxed float result");
assert_eq!(float.value(), 42.0);
}
}