use alloc::vec::Vec;
const SAFE_INT_MAX: i64 = 9_007_199_254_740_992;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArithOp {
AddImm(i32),
SubImm(i32),
MulImm(i32),
AndImm(i32),
OrImm(i32),
XorImm(i32),
ShlImm(u8),
SarImm(u8),
Neg,
}
impl ArithOp {
#[must_use]
pub fn eval(self, acc: i64) -> i64 {
match self {
ArithOp::AddImm(n) => acc.wrapping_add(i64::from(n)),
ArithOp::SubImm(n) => acc.wrapping_sub(i64::from(n)),
ArithOp::MulImm(n) => acc.wrapping_mul(i64::from(n)),
ArithOp::AndImm(n) => acc & i64::from(n),
ArithOp::OrImm(n) => acc | i64::from(n),
ArithOp::XorImm(n) => acc ^ i64::from(n),
ArithOp::ShlImm(n) => acc.wrapping_shl(u32::from(n)),
ArithOp::SarImm(n) => acc.wrapping_shr(u32::from(n)),
ArithOp::Neg => acc.wrapping_neg(),
}
}
}
#[must_use]
pub fn eval_arith(ops: &[ArithOp], arg: i64) -> i64 {
ops.iter().fold(arg, |acc, op| op.eval(acc))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StackOp {
Arg(u8),
Const(i64),
Add,
Sub,
Mul,
}
#[must_use]
pub fn eval_stack(ops: &[StackOp], args: [i64; 2]) -> i64 {
let mut stack: Vec<i64> = Vec::new();
for op in ops {
match *op {
StackOp::Arg(i) => stack.push(args[i as usize & 1]),
StackOp::Const(n) => stack.push(n),
StackOp::Add | StackOp::Sub | StackOp::Mul => {
let b = stack.pop().unwrap_or(0);
let a = stack.pop().unwrap_or(0);
stack.push(match op {
StackOp::Add => a.wrapping_add(b),
StackOp::Sub => a.wrapping_sub(b),
_ => a.wrapping_mul(b),
});
}
}
}
stack.pop().unwrap_or(0)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BinOp2 {
Add,
Sub,
Mul,
And,
Or,
Xor,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShiftOp {
Shl,
Sar,
Shr,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FBinOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(missing_docs)] pub enum FloatOp {
Arg {
dst: u8,
index: u8,
},
Const {
dst: u8,
imm: f64,
},
Bin {
dst: u8,
a: u8,
b: u8,
op: FBinOp,
},
Mod {
dst: u8,
a: u8,
b: u8,
},
Move {
dst: u8,
src: u8,
},
Lt {
dst: u8,
a: u8,
b: u8,
},
JumpIfFalse {
cond: u8,
target: usize,
},
Jump {
target: usize,
},
Neg {
dst: u8,
a: u8,
},
Sqrt {
dst: u8,
a: u8,
},
Abs {
dst: u8,
a: u8,
},
Max {
dst: u8,
a: u8,
b: u8,
},
Min {
dst: u8,
a: u8,
b: u8,
},
Floor {
dst: u8,
a: u8,
},
Ceil {
dst: u8,
a: u8,
},
Trunc {
dst: u8,
a: u8,
},
Eqz {
dst: u8,
a: u8,
},
Eq {
dst: u8,
a: u8,
b: u8,
},
Ret {
src: u8,
},
}
#[must_use]
pub fn eval_float(ops: &[FloatOp], n_regs: usize, args: &[f64]) -> f64 {
let mut regs = alloc::vec![0.0f64; n_regs];
let mut pc = 0usize;
while pc < ops.len() {
match ops[pc] {
FloatOp::Arg { dst, index } => {
regs[dst as usize] = args.get(index as usize).copied().unwrap_or(0.0);
pc += 1;
}
FloatOp::Const { dst, imm } => {
regs[dst as usize] = imm;
pc += 1;
}
FloatOp::Bin { dst, a, b, op } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = match op {
FBinOp::Add => x + y,
FBinOp::Sub => x - y,
FBinOp::Mul => x * y,
FBinOp::Div => x / y,
};
pc += 1;
}
FloatOp::Mod { dst, a, b } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = x - (x / y).trunc() * y;
pc += 1;
}
FloatOp::Move { dst, src } => {
regs[dst as usize] = regs[src as usize];
pc += 1;
}
FloatOp::Lt { dst, a, b } => {
regs[dst as usize] = f64::from(u8::from(regs[a as usize] < regs[b as usize]));
pc += 1;
}
FloatOp::JumpIfFalse { cond, target } => {
if regs[cond as usize] == 0.0 {
pc = target;
} else {
pc += 1;
}
}
FloatOp::Jump { target } => pc = target,
FloatOp::Neg { dst, a } => {
regs[dst as usize] = -regs[a as usize];
pc += 1;
}
FloatOp::Sqrt { dst, a } => {
regs[dst as usize] = regs[a as usize].sqrt();
pc += 1;
}
FloatOp::Abs { dst, a } => {
regs[dst as usize] = regs[a as usize].abs();
pc += 1;
}
FloatOp::Max { dst, a, b } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = if x.is_nan() || y.is_nan() {
f64::NAN
} else if x > y {
x
} else if y > x {
y
} else if x.is_sign_positive() {
x } else {
y
};
pc += 1;
}
FloatOp::Min { dst, a, b } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = if x.is_nan() || y.is_nan() {
f64::NAN
} else if x < y {
x
} else if y < x {
y
} else if x.is_sign_negative() {
x } else {
y
};
pc += 1;
}
FloatOp::Floor { dst, a } => {
regs[dst as usize] = regs[a as usize].floor();
pc += 1;
}
FloatOp::Trunc { dst, a } => {
regs[dst as usize] = regs[a as usize].trunc();
pc += 1;
}
FloatOp::Ceil { dst, a } => {
regs[dst as usize] = regs[a as usize].ceil();
pc += 1;
}
FloatOp::Eqz { dst, a } => {
let x = regs[a as usize];
regs[dst as usize] = f64::from(u8::from(x == 0.0 || x.is_nan()));
pc += 1;
}
FloatOp::Eq { dst, a, b } => {
regs[dst as usize] = f64::from(u8::from(regs[a as usize] == regs[b as usize]));
pc += 1;
}
FloatOp::Ret { src } => return regs[src as usize],
}
}
0.0
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(missing_docs)] pub enum RegOp {
Arg { dst: u8, index: u8 },
Const { dst: u8, imm: i64 },
Bin { dst: u8, a: u8, b: u8, op: BinOp2 },
Move { dst: u8, src: u8 },
Lt { dst: u8, a: u8, b: u8 },
Eqz { dst: u8, a: u8 },
Eq { dst: u8, a: u8, b: u8 },
Neg { dst: u8, a: u8 },
BitNot32 { dst: u8, a: u8 },
Mod { dst: u8, a: u8, b: u8 },
Bit32 { dst: u8, a: u8, b: u8, op: BinOp2 },
Shift32 { dst: u8, a: u8, b: u8, op: ShiftOp },
JumpIfFalse { cond: u8, target: usize },
Jump { target: usize },
Ret { src: u8 },
Call {
dst: u8,
code_ptr: u64,
n_args: u8,
args: [u8; 6],
},
}
#[must_use]
pub fn eval_reg(ops: &[RegOp], n_regs: usize, args: &[i64]) -> i64 {
let mut regs = alloc::vec![0i64; n_regs];
let mut pc = 0usize;
while pc < ops.len() {
match ops[pc] {
RegOp::Arg { dst, index } => {
regs[dst as usize] = args.get(index as usize).copied().unwrap_or(0);
pc += 1;
}
RegOp::Const { dst, imm } => {
regs[dst as usize] = imm;
pc += 1;
}
RegOp::Bin { dst, a, b, op } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = match op {
BinOp2::Add => x.wrapping_add(y),
BinOp2::Sub => x.wrapping_sub(y),
BinOp2::Mul => x.wrapping_mul(y),
BinOp2::And => x & y,
BinOp2::Or => x | y,
BinOp2::Xor => x ^ y,
};
pc += 1;
}
RegOp::Move { dst, src } => {
regs[dst as usize] = regs[src as usize];
pc += 1;
}
RegOp::Lt { dst, a, b } => {
regs[dst as usize] = i64::from(regs[a as usize] < regs[b as usize]);
pc += 1;
}
RegOp::Eqz { dst, a } => {
regs[dst as usize] = i64::from(regs[a as usize] == 0);
pc += 1;
}
RegOp::Eq { dst, a, b } => {
regs[dst as usize] = i64::from(regs[a as usize] == regs[b as usize]);
pc += 1;
}
RegOp::Neg { dst, a } => {
regs[dst as usize] = regs[a as usize].wrapping_neg();
pc += 1;
}
RegOp::BitNot32 { dst, a } => {
regs[dst as usize] = i64::from(!(regs[a as usize] as i32));
pc += 1;
}
RegOp::Mod { dst, a, b } => {
let (x, y) = (regs[a as usize], regs[b as usize]);
regs[dst as usize] = if y == 0 { 0 } else { x.wrapping_rem(y) };
pc += 1;
}
RegOp::Bit32 { dst, a, b, op } => {
let (x, y) = (regs[a as usize] as i32, regs[b as usize] as i32);
let v = match op {
BinOp2::And => x & y,
BinOp2::Or => x | y,
BinOp2::Xor => x ^ y,
_ => x & y,
};
regs[dst as usize] = i64::from(v);
pc += 1;
}
RegOp::Shift32 { dst, a, b, op } => {
let count = (regs[b as usize] as u32) & 31;
regs[dst as usize] = match op {
ShiftOp::Shl => i64::from((regs[a as usize] as i32).wrapping_shl(count)),
ShiftOp::Sar => i64::from((regs[a as usize] as i32).wrapping_shr(count)),
ShiftOp::Shr => i64::from((regs[a as usize] as u32).wrapping_shr(count)),
};
pc += 1;
}
RegOp::JumpIfFalse { cond, target } => {
if regs[cond as usize] == 0 {
pc = target;
} else {
pc += 1;
}
}
RegOp::Jump { target } => pc = target,
RegOp::Ret { src } => return regs[src as usize],
RegOp::Call { .. } => unreachable!("eval_reg does not evaluate Call ops"),
}
}
0
}
#[must_use]
pub fn optimize_reg(ops: &[RegOp], n_regs: usize) -> Vec<RegOp> {
dce_reg(©_propagate(&fold_constants(ops, n_regs), n_regs))
}
#[must_use]
pub fn copy_propagate(ops: &[RegOp], n_regs: usize) -> Vec<RegOp> {
let mut is_target = alloc::vec![false; ops.len()];
for op in ops {
if let RegOp::JumpIfFalse { target, .. } | RegOp::Jump { target } = op
&& *target < is_target.len()
{
is_target[*target] = true;
}
}
let mut copy_of: Vec<Option<u8>> = alloc::vec![None; n_regs];
let resolve = |copy_of: &[Option<u8>], r: u8| copy_of[r as usize].unwrap_or(r);
let invalidate = |copy_of: &mut [Option<u8>], w: u8| {
for c in copy_of.iter_mut() {
if *c == Some(w) {
*c = None;
}
}
copy_of[w as usize] = None;
};
let mut out = Vec::with_capacity(ops.len());
for (i, op) in ops.iter().enumerate() {
if is_target[i] {
copy_of.iter_mut().for_each(|c| *c = None);
}
let rewritten = match *op {
RegOp::Bin { dst, a, b, op } => RegOp::Bin {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
op,
},
RegOp::Lt { dst, a, b } => RegOp::Lt {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
},
RegOp::Eqz { dst, a } => RegOp::Eqz {
dst,
a: resolve(©_of, a),
},
RegOp::Eq { dst, a, b } => RegOp::Eq {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
},
RegOp::Neg { dst, a } => RegOp::Neg {
dst,
a: resolve(©_of, a),
},
RegOp::BitNot32 { dst, a } => RegOp::BitNot32 {
dst,
a: resolve(©_of, a),
},
RegOp::Mod { dst, a, b } => RegOp::Mod {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
},
RegOp::Bit32 { dst, a, b, op } => RegOp::Bit32 {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
op,
},
RegOp::Shift32 { dst, a, b, op } => RegOp::Shift32 {
dst,
a: resolve(©_of, a),
b: resolve(©_of, b),
op,
},
RegOp::Move { dst, src } => RegOp::Move {
dst,
src: resolve(©_of, src),
},
RegOp::JumpIfFalse { cond, target } => RegOp::JumpIfFalse {
cond: resolve(©_of, cond),
target,
},
RegOp::Ret { src } => RegOp::Ret {
src: resolve(©_of, src),
},
other => other,
};
match rewritten {
RegOp::Move { dst, src } => {
invalidate(&mut copy_of, dst);
if src != dst {
copy_of[dst as usize] = Some(src);
}
}
RegOp::Const { dst, .. }
| RegOp::Bin { dst, .. }
| RegOp::Lt { dst, .. }
| RegOp::Eqz { dst, .. }
| RegOp::Eq { dst, .. }
| RegOp::Neg { dst, .. }
| RegOp::BitNot32 { dst, .. }
| RegOp::Mod { dst, .. }
| RegOp::Bit32 { dst, .. }
| RegOp::Shift32 { dst, .. }
| RegOp::Call { dst, .. }
| RegOp::Arg { dst, .. } => invalidate(&mut copy_of, dst),
RegOp::JumpIfFalse { .. } | RegOp::Jump { .. } => {
copy_of.iter_mut().for_each(|c| *c = None);
}
RegOp::Ret { .. } => {}
}
out.push(rewritten);
}
out
}
#[must_use]
pub fn dce_reg(ops: &[RegOp]) -> Vec<RegOp> {
use alloc::collections::BTreeSet;
let mut used: BTreeSet<u8> = BTreeSet::new();
for op in ops {
match *op {
RegOp::Bin { a, b, .. }
| RegOp::Lt { a, b, .. }
| RegOp::Eq { a, b, .. }
| RegOp::Mod { a, b, .. }
| RegOp::Bit32 { a, b, .. }
| RegOp::Shift32 { a, b, .. } => {
used.insert(a);
used.insert(b);
}
RegOp::Move { src, .. }
| RegOp::Eqz { a: src, .. }
| RegOp::Neg { a: src, .. }
| RegOp::BitNot32 { a: src, .. } => {
used.insert(src);
}
RegOp::JumpIfFalse { cond, .. } => {
used.insert(cond);
}
RegOp::Ret { src } => {
used.insert(src);
}
RegOp::Call { n_args, args, .. } => {
for a in &args[..n_args as usize] {
used.insert(*a);
}
}
RegOp::Const { .. } | RegOp::Arg { .. } | RegOp::Jump { .. } => {}
}
}
let keep = |op: &RegOp| match *op {
RegOp::Ret { .. } | RegOp::Jump { .. } | RegOp::JumpIfFalse { .. } | RegOp::Call { .. } => {
true
}
RegOp::Const { dst, .. }
| RegOp::Bin { dst, .. }
| RegOp::Move { dst, .. }
| RegOp::Lt { dst, .. }
| RegOp::Eqz { dst, .. }
| RegOp::Eq { dst, .. }
| RegOp::Neg { dst, .. }
| RegOp::BitNot32 { dst, .. }
| RegOp::Mod { dst, .. }
| RegOp::Bit32 { dst, .. }
| RegOp::Shift32 { dst, .. }
| RegOp::Arg { dst, .. } => used.contains(&dst),
};
let mut newpos = alloc::vec![0usize; ops.len() + 1];
let mut n = 0;
for (i, op) in ops.iter().enumerate() {
newpos[i] = n;
if keep(op) {
n += 1;
}
}
newpos[ops.len()] = n;
let mut out = Vec::with_capacity(n);
for op in ops.iter().filter(|o| keep(o)) {
out.push(match *op {
RegOp::JumpIfFalse { cond, target } => RegOp::JumpIfFalse {
cond,
target: newpos.get(target).copied().unwrap_or(n),
},
RegOp::Jump { target } => RegOp::Jump {
target: newpos.get(target).copied().unwrap_or(n),
},
other => other,
});
}
out
}
fn op_regs(op: &RegOp) -> (Option<u8>, [Option<u8>; 2]) {
match *op {
RegOp::Arg { dst, .. } | RegOp::Const { dst, .. } => (Some(dst), [None, None]),
RegOp::Move { dst, src } => (Some(dst), [Some(src), None]),
RegOp::Bin { dst, a, b, .. }
| RegOp::Lt { dst, a, b }
| RegOp::Eq { dst, a, b }
| RegOp::Mod { dst, a, b }
| RegOp::Bit32 { dst, a, b, .. }
| RegOp::Shift32 { dst, a, b, .. } => (Some(dst), [Some(a), Some(b)]),
RegOp::Eqz { dst, a } | RegOp::Neg { dst, a } | RegOp::BitNot32 { dst, a } => {
(Some(dst), [Some(a), None])
}
RegOp::JumpIfFalse { cond, .. } => (None, [Some(cond), None]),
RegOp::Ret { src } => (None, [Some(src), None]),
RegOp::Jump { .. } => (None, [None, None]),
RegOp::Call { dst, .. } => (Some(dst), [None, None]),
}
}
#[must_use]
pub fn allocate_reg(ops: &[RegOp], n_regs: usize) -> (Vec<RegOp>, usize) {
let mut first = alloc::vec![usize::MAX; n_regs];
let mut last = alloc::vec![0usize; n_regs];
for (i, op) in ops.iter().enumerate() {
let (dst, srcs) = op_regs(op);
for r in dst.into_iter().chain(srcs.into_iter().flatten()) {
let r = r as usize;
first[r] = first[r].min(i);
last[r] = last[r].max(i);
}
}
loop {
let mut changed = false;
for (j, op) in ops.iter().enumerate() {
let (RegOp::Jump { target } | RegOp::JumpIfFalse { target, .. }) = *op else {
continue;
};
if target > j {
continue; }
for r in 0..n_regs {
if first[r] != usize::MAX && first[r] <= j && last[r] >= target {
let (nf, nl) = (first[r].min(target), last[r].max(j));
if nf != first[r] || nl != last[r] {
first[r] = nf;
last[r] = nl;
changed = true;
}
}
}
}
if !changed {
break;
}
}
let mut intervals: Vec<(usize, usize, u8)> = (0..n_regs)
.filter(|&r| first[r] != usize::MAX)
.map(|r| (first[r], last[r], r as u8))
.collect();
intervals.sort_unstable();
let mut mapping = alloc::vec![0u8; n_regs];
let mut slot_end: Vec<usize> = Vec::new(); for (start, end, vreg) in intervals {
let slot = slot_end.iter().position(|&e| e < start).unwrap_or_else(|| {
slot_end.push(0);
slot_end.len() - 1
});
slot_end[slot] = end;
mapping[vreg as usize] = slot as u8;
}
let new_n = slot_end.len().max(1);
let m = |r: u8| mapping[r as usize];
let out = ops
.iter()
.map(|op| match *op {
RegOp::Arg { dst, index } => RegOp::Arg { dst: m(dst), index },
RegOp::Const { dst, imm } => RegOp::Const { dst: m(dst), imm },
RegOp::Move { dst, src } => RegOp::Move {
dst: m(dst),
src: m(src),
},
RegOp::Bin { dst, a, b, op } => RegOp::Bin {
dst: m(dst),
a: m(a),
b: m(b),
op,
},
RegOp::Lt { dst, a, b } => RegOp::Lt {
dst: m(dst),
a: m(a),
b: m(b),
},
RegOp::Eqz { dst, a } => RegOp::Eqz {
dst: m(dst),
a: m(a),
},
RegOp::Eq { dst, a, b } => RegOp::Eq {
dst: m(dst),
a: m(a),
b: m(b),
},
RegOp::Neg { dst, a } => RegOp::Neg {
dst: m(dst),
a: m(a),
},
RegOp::BitNot32 { dst, a } => RegOp::BitNot32 {
dst: m(dst),
a: m(a),
},
RegOp::Mod { dst, a, b } => RegOp::Mod {
dst: m(dst),
a: m(a),
b: m(b),
},
RegOp::Bit32 { dst, a, b, op } => RegOp::Bit32 {
dst: m(dst),
a: m(a),
b: m(b),
op,
},
RegOp::Shift32 { dst, a, b, op } => RegOp::Shift32 {
dst: m(dst),
a: m(a),
b: m(b),
op,
},
RegOp::JumpIfFalse { cond, target } => RegOp::JumpIfFalse {
cond: m(cond),
target,
},
RegOp::Jump { target } => RegOp::Jump { target },
RegOp::Ret { src } => RegOp::Ret { src: m(src) },
RegOp::Call {
dst,
code_ptr,
n_args,
args,
} => RegOp::Call {
dst: m(dst),
code_ptr,
n_args,
args: args.map(m),
},
})
.collect();
(out, new_n)
}
#[must_use]
pub fn fold_constants(ops: &[RegOp], n_regs: usize) -> Vec<RegOp> {
let mut is_target = alloc::vec![false; ops.len()];
for op in ops {
if let RegOp::JumpIfFalse { target, .. } | RegOp::Jump { target } = op
&& *target < is_target.len()
{
is_target[*target] = true;
}
}
let mut known: Vec<Option<i64>> = alloc::vec![None; n_regs];
let mut out = Vec::with_capacity(ops.len());
for (i, op) in ops.iter().enumerate() {
if is_target[i] {
known.iter_mut().for_each(|k| *k = None);
}
let lowered = match *op {
RegOp::Const { dst, imm } => {
known[dst as usize] = Some(imm);
RegOp::Const { dst, imm }
}
RegOp::Move { dst, src } => {
if let Some(v) = known[src as usize] {
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Move { dst, src }
}
}
RegOp::Bin { dst, a, b, op } => {
let (ka, kb) = (known[a as usize], known[b as usize]);
if let (Some(x), Some(y)) = (ka, kb) {
let v = match op {
BinOp2::Add => x.wrapping_add(y),
BinOp2::Sub => x.wrapping_sub(y),
BinOp2::Mul => x.wrapping_mul(y),
BinOp2::And => x & y,
BinOp2::Or => x | y,
BinOp2::Xor => x ^ y,
};
if (-SAFE_INT_MAX..=SAFE_INT_MAX).contains(&v) {
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Bin { dst, a, b, op }
}
} else if let Some(simplified) = simplify_bin(dst, a, b, op, ka, kb, &mut known) {
simplified
} else {
known[dst as usize] = None;
RegOp::Bin { dst, a, b, op }
}
}
RegOp::Lt { dst, a, b } => {
if let (Some(x), Some(y)) = (known[a as usize], known[b as usize]) {
let v = i64::from(x < y);
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Lt { dst, a, b }
}
}
RegOp::Eqz { dst, a } => {
if let Some(x) = known[a as usize] {
let v = i64::from(x == 0);
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Eqz { dst, a }
}
}
RegOp::Eq { dst, a, b } => {
if let (Some(x), Some(y)) = (known[a as usize], known[b as usize]) {
let v = i64::from(x == y);
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Eq { dst, a, b }
}
}
RegOp::Neg { dst, a } => {
match known[a as usize] {
Some(x) if (-SAFE_INT_MAX..=SAFE_INT_MAX).contains(&x.wrapping_neg()) => {
let v = x.wrapping_neg();
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
}
_ => {
known[dst as usize] = None;
RegOp::Neg { dst, a }
}
}
}
RegOp::BitNot32 { dst, a } => {
if let Some(x) = known[a as usize] {
let v = i64::from(!(x as i32));
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::BitNot32 { dst, a }
}
}
RegOp::Mod { dst, a, b } => {
match (known[a as usize], known[b as usize]) {
(Some(x), Some(y)) if y != 0 => {
let v = x.wrapping_rem(y);
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
}
_ => {
known[dst as usize] = None;
RegOp::Mod { dst, a, b }
}
}
}
RegOp::Bit32 { dst, a, b, op } => {
if let (Some(x), Some(y)) = (known[a as usize], known[b as usize]) {
let (x, y) = (x as i32, y as i32);
let v = i64::from(match op {
BinOp2::And => x & y,
BinOp2::Or => x | y,
BinOp2::Xor => x ^ y,
_ => x & y,
});
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Bit32 { dst, a, b, op }
}
}
RegOp::Shift32 { dst, a, b, op } => {
if let (Some(x), Some(y)) = (known[a as usize], known[b as usize]) {
let count = (y as u32) & 31;
let v = match op {
ShiftOp::Shl => i64::from((x as i32).wrapping_shl(count)),
ShiftOp::Sar => i64::from((x as i32).wrapping_shr(count)),
ShiftOp::Shr => i64::from((x as u32).wrapping_shr(count)),
};
known[dst as usize] = Some(v);
RegOp::Const { dst, imm: v }
} else {
known[dst as usize] = None;
RegOp::Shift32 { dst, a, b, op }
}
}
RegOp::Arg { dst, .. } => {
known[dst as usize] = None;
*op
}
RegOp::JumpIfFalse { .. } | RegOp::Jump { .. } => {
known.iter_mut().for_each(|k| *k = None);
*op
}
RegOp::Ret { .. } => *op,
RegOp::Call { dst, .. } => {
known[dst as usize] = None;
*op
}
};
out.push(lowered);
}
out
}
fn simplify_bin(
dst: u8,
a: u8,
b: u8,
op: BinOp2,
ka: Option<i64>,
kb: Option<i64>,
known: &mut [Option<i64>],
) -> Option<RegOp> {
use BinOp2::{Add, And, Mul, Or, Sub, Xor};
let mov = |known: &mut [Option<i64>], r: u8| {
known[dst as usize] = known[r as usize];
Some(RegOp::Move { dst, src: r })
};
let con = |known: &mut [Option<i64>], c: i64| {
known[dst as usize] = Some(c);
Some(RegOp::Const { dst, imm: c })
};
if let Some(y) = kb {
match (op, y) {
(Add, 0) | (Sub, 0) | (Or, 0) | (Xor, 0) | (Mul, 1) | (And, -1) => {
return mov(known, a);
}
(Mul, 0) | (And, 0) => return con(known, 0),
(Or, -1) => return con(known, -1),
_ => {}
}
}
if let Some(x) = ka {
match (op, x) {
(Add, 0) | (Or, 0) | (Xor, 0) | (Mul, 1) | (And, -1) => return mov(known, b),
(Mul, 0) | (And, 0) => return con(known, 0),
(Or, -1) => return con(known, -1),
_ => {}
}
}
if a == b {
match op {
Sub | Xor => return con(known, 0),
And | Or => return mov(known, a),
_ => {}
}
}
None
}
#[cfg(feature = "alloc")]
fn nanbox_int(v: crate::nanbox::NanBox) -> Option<i64> {
match v.unpack() {
crate::nanbox::Unpacked::Number(n) if n.is_finite() => {
let i = n as i64;
if (i as f64) == n
&& (-9.007_199_254_740_992e15..=9.007_199_254_740_992e15).contains(&n)
{
Some(i)
} else {
None
}
}
_ => None,
}
}
#[cfg(feature = "alloc")]
#[must_use]
pub fn lower_nbvm(proto: &crate::nbvm::FnProto) -> Option<Vec<RegOp>> {
lower_nbvm_with(proto, &alloc::collections::BTreeMap::new())
}
#[must_use]
pub fn lower_nbvm_with(
proto: &crate::nbvm::FnProto,
registry: &alloc::collections::BTreeMap<u32, u64>,
) -> Option<Vec<RegOp>> {
use crate::nbvm::Op;
if proto.n_regs > 64 || proto.n_params > 6 || proto.n_captures != 0 {
return None;
}
let reg8 = |r: crate::nbvm::Reg| -> Option<u8> {
if (r as usize) < proto.n_regs {
u8::try_from(r).ok()
} else {
None
}
};
let mut out = Vec::new();
let mut written = alloc::vec![false; proto.n_regs];
for w in written.iter_mut().take(proto.n_params) {
*w = true;
}
let read = |w: &[bool], r: crate::nbvm::Reg| -> Option<u8> {
let r8 = reg8(r)?;
if *w.get(r as usize)? { Some(r8) } else { None }
};
for i in 0..proto.n_params {
out.push(RegOp::Arg {
dst: u8::try_from(i).ok()?,
index: u8::try_from(i).ok()?,
});
}
for op in &proto.ops {
let lowered = match op {
Op::LoadConst { dst, value } => {
let imm = nanbox_int(*value)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Const { dst: d, imm }
}
Op::Add { dst, a, b } | Op::AddValue { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Bin {
dst: d,
a,
b,
op: BinOp2::Add,
}
}
Op::Sub { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Bin {
dst: d,
a,
b,
op: BinOp2::Sub,
}
}
Op::Mul { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Bin {
dst: d,
a,
b,
op: BinOp2::Mul,
}
}
Op::Mod { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Mod { dst: d, a, b }
}
Op::Move { dst, src } => {
let s = read(&written, *src)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Move { dst: d, src: s }
}
Op::Lt { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Lt { dst: d, a, b }
}
Op::Not { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Eqz { dst: d, a }
}
Op::StrictEq { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Eq { dst: d, a, b }
}
Op::ValueBin { dst, op, a, b } if *op == crate::nbvm::VB_LOOSE_EQ => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Eq { dst: d, a, b }
}
Op::Neg { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Neg { dst: d, a }
}
Op::BitNot { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::BitNot32 { dst: d, a }
}
Op::ValueBin { dst, op, a, b }
if matches!(
*op,
crate::nbvm::VB_BIT_AND | crate::nbvm::VB_BIT_OR | crate::nbvm::VB_BIT_XOR
) =>
{
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
let bop = match *op {
crate::nbvm::VB_BIT_AND => BinOp2::And,
crate::nbvm::VB_BIT_OR => BinOp2::Or,
_ => BinOp2::Xor,
};
RegOp::Bit32 {
dst: d,
a,
b,
op: bop,
}
}
Op::ValueBin { dst, op, a, b }
if matches!(
*op,
crate::nbvm::VB_SHL | crate::nbvm::VB_SHR | crate::nbvm::VB_USHR
) =>
{
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
let sop = match *op {
crate::nbvm::VB_SHL => ShiftOp::Shl,
crate::nbvm::VB_SHR => ShiftOp::Sar,
_ => ShiftOp::Shr,
};
RegOp::Shift32 {
dst: d,
a,
b,
op: sop,
}
}
Op::JumpIfFalse { cond, target } => RegOp::JumpIfFalse {
cond: read(&written, *cond)?,
target: target.checked_add(proto.n_params)?,
},
Op::Jump { target } => RegOp::Jump {
target: target.checked_add(proto.n_params)?,
},
Op::Return { src } => RegOp::Ret {
src: read(&written, *src)?,
},
Op::Call { dst, func, args } => {
if args.len() > 6 {
return None;
}
let code_ptr = registry.get(func).copied()?;
let mut argregs = [0u8; 6];
for (i, r) in args.iter().enumerate() {
argregs[i] = read(&written, *r)?;
}
let d = reg8(*dst)?;
written[*dst as usize] = true;
RegOp::Call {
dst: d,
code_ptr,
n_args: args.len() as u8,
args: argregs,
}
}
_ => return None,
};
out.push(lowered);
}
if !matches!(out.last(), Some(RegOp::Ret { .. })) {
return None;
}
for op in &out {
if let RegOp::JumpIfFalse { target, .. } | RegOp::Jump { target } = op
&& *target >= out.len()
{
return None;
}
}
Some(out)
}
#[cfg(feature = "alloc")]
fn nanbox_f64(v: crate::nanbox::NanBox) -> Option<f64> {
match v.unpack() {
crate::nanbox::Unpacked::Number(n) if n.is_finite() => Some(n),
_ => None,
}
}
#[cfg(all(feature = "alloc", target_arch = "x86_64"))]
fn has_sse41() -> bool {
std::is_x86_feature_detected!("sse4.1")
}
#[cfg(all(feature = "alloc", not(target_arch = "x86_64")))]
fn has_sse41() -> bool {
false
}
#[cfg(feature = "alloc")]
#[must_use]
pub fn lower_nbvm_float(proto: &crate::nbvm::FnProto) -> Option<Vec<FloatOp>> {
use crate::nbvm::Op;
if proto.n_regs > 64 || proto.n_params > 4 || proto.n_captures != 0 {
return None;
}
let reg8 = |r: crate::nbvm::Reg| -> Option<u8> {
((r as usize) < proto.n_regs).then(|| u8::try_from(r).ok())?
};
let mut written = alloc::vec![false; proto.n_regs];
for w in written.iter_mut().take(proto.n_params) {
*w = true;
}
let read = |w: &[bool], r: crate::nbvm::Reg| -> Option<u8> {
let r8 = reg8(r)?;
if *w.get(r as usize)? { Some(r8) } else { None }
};
let mut out = Vec::new();
for i in 0..proto.n_params {
out.push(FloatOp::Arg {
dst: u8::try_from(i).ok()?,
index: u8::try_from(i).ok()?,
});
}
let bin = |w: &mut [bool], dst, a, b, op| -> Option<FloatOp> {
let (a, b) = (read(w, a)?, read(w, b)?);
let d = reg8(dst)?;
w[dst as usize] = true;
Some(FloatOp::Bin { dst: d, a, b, op })
};
for op in &proto.ops {
let lowered = match op {
Op::LoadConst { dst, value } => {
let imm = nanbox_f64(*value)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Const { dst: d, imm }
}
Op::Add { dst, a, b } | Op::AddValue { dst, a, b } => {
bin(&mut written, *dst, *a, *b, FBinOp::Add)?
}
Op::Sub { dst, a, b } => bin(&mut written, *dst, *a, *b, FBinOp::Sub)?,
Op::Mul { dst, a, b } => bin(&mut written, *dst, *a, *b, FBinOp::Mul)?,
Op::Div { dst, a, b } => bin(&mut written, *dst, *a, *b, FBinOp::Div)?,
Op::Mod { dst, a, b } if has_sse41() => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Mod { dst: d, a, b }
}
Op::Move { dst, src } => {
let s = read(&written, *src)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Move { dst: d, src: s }
}
Op::Lt { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Lt { dst: d, a, b }
}
Op::Neg { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Neg { dst: d, a }
}
Op::Not { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Eqz { dst: d, a }
}
Op::StrictEq { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
FloatOp::Eq { dst: d, a, b }
}
Op::CallNative { dst, native, args }
if (*native == crate::nbvm::NB_MATH_SQRT
|| *native == crate::nbvm::NB_MATH_ABS)
&& args.len() == 1 =>
{
let a = read(&written, args[0])?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
if *native == crate::nbvm::NB_MATH_SQRT {
FloatOp::Sqrt { dst: d, a }
} else {
FloatOp::Abs { dst: d, a }
}
}
Op::CallNative { dst, native, args }
if (*native == crate::nbvm::NB_MATH_FLOOR
|| *native == crate::nbvm::NB_MATH_CEIL
|| *native == crate::nbvm::NB_MATH_TRUNC)
&& args.len() == 1
&& has_sse41() =>
{
let a = read(&written, args[0])?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
if *native == crate::nbvm::NB_MATH_FLOOR {
FloatOp::Floor { dst: d, a }
} else if *native == crate::nbvm::NB_MATH_CEIL {
FloatOp::Ceil { dst: d, a }
} else {
FloatOp::Trunc { dst: d, a }
}
}
Op::CallNative { dst, native, args }
if (*native == crate::nbvm::NB_MATH_MAX || *native == crate::nbvm::NB_MATH_MIN)
&& args.len() == 2 =>
{
let (a, b) = (read(&written, args[0])?, read(&written, args[1])?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
if *native == crate::nbvm::NB_MATH_MAX {
FloatOp::Max { dst: d, a, b }
} else {
FloatOp::Min { dst: d, a, b }
}
}
Op::JumpIfFalse { cond, target } => FloatOp::JumpIfFalse {
cond: read(&written, *cond)?,
target: target.checked_add(proto.n_params)?,
},
Op::Jump { target } => FloatOp::Jump {
target: target.checked_add(proto.n_params)?,
},
Op::Return { src } => FloatOp::Ret {
src: read(&written, *src)?,
},
_ => return None,
};
out.push(lowered);
}
if !matches!(out.last(), Some(FloatOp::Ret { .. })) {
return None;
}
for op in &out {
if let FloatOp::JumpIfFalse { target, .. } | FloatOp::Jump { target } = op
&& *target >= out.len()
{
return None;
}
}
Some(out)
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub struct JitProto {
func: JitFunction,
n_params: usize,
kind: JitKind,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, PartialEq, Eq)]
enum JitKind {
Int,
Float,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
impl JitProto {
#[must_use]
pub fn compile(proto: &crate::nbvm::FnProto) -> Option<Self> {
Self::compile_with_registry(proto, &alloc::collections::BTreeMap::new())
}
#[must_use]
pub fn compile_with_registry(
proto: &crate::nbvm::FnProto,
registry: &alloc::collections::BTreeMap<u32, u64>,
) -> Option<Self> {
if let Some(ops) = lower_nbvm_with(proto, registry) {
let has_call = ops.iter().any(|o| matches!(o, RegOp::Call { .. }));
let func = if has_call {
JitFunction::compile_reg(proto.n_regs, proto.n_params, &ops)?
} else {
let ops = optimize_reg(&ops, proto.n_regs);
let (ops, n_regs) = allocate_reg(&ops, proto.n_regs);
JitFunction::compile_reg(n_regs, proto.n_params, &ops)?
};
return Some(Self {
func,
n_params: proto.n_params,
kind: JitKind::Int,
});
}
let ops = lower_nbvm_float(proto)?;
let func = JitFunction::compile_float(proto.n_regs, proto.n_params, &ops)?;
Some(Self {
func,
n_params: proto.n_params,
kind: JitKind::Float,
})
}
#[must_use]
pub fn code_ptr(&self) -> usize {
self.func.code_ptr()
}
#[must_use]
pub fn call_guarded(&self, args: &[crate::nanbox::NanBox]) -> Option<crate::nanbox::NanBox> {
if args.len() != self.n_params {
return None;
}
match self.kind {
JitKind::Int => {
let mut ints = [0i64; 6];
for (slot, a) in ints.iter_mut().zip(args.iter()) {
*slot = nanbox_int(*a)?;
}
let r = self.func.call_args(&ints[..self.n_params]);
if (-SAFE_INT_MAX..=SAFE_INT_MAX).contains(&r) {
Some(crate::nanbox::NanBox::number(r as f64))
} else {
None
}
}
JitKind::Float => {
let mut fs = [0.0f64; 4];
for (slot, a) in fs.iter_mut().zip(args.iter()) {
*slot = match a.unpack() {
crate::nanbox::Unpacked::Number(n) => n,
_ => return None,
};
}
let r = self.func.call_args_f64(&fs[..self.n_params]);
Some(crate::nanbox::NanBox::number(r))
}
}
}
}
#[must_use]
pub fn eval_sum_1_to_n(n: i64) -> i64 {
let mut acc = 0i64;
let mut i = n;
while i > 0 {
acc = acc.wrapping_add(i);
i -= 1;
}
acc
}
#[derive(Default)]
pub struct X64Assembler {
code: Vec<u8>,
labels: Vec<usize>,
fixups: Vec<(usize, usize)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Label(usize);
impl X64Assembler {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn code(&self) -> &[u8] {
&self.code
}
pub fn new_label(&mut self) -> Label {
self.labels.push(usize::MAX);
Label(self.labels.len() - 1)
}
pub fn bind(&mut self, label: Label) {
self.labels[label.0] = self.code.len();
}
#[must_use]
pub fn finish(mut self) -> Vec<u8> {
for (at, label) in core::mem::take(&mut self.fixups) {
let target = self.labels[label];
debug_assert_ne!(target, usize::MAX, "unbound label in jump");
let rel = (target as i64) - (at as i64 + 4);
let bytes = (rel as i32).to_le_bytes();
self.code[at..at + 4].copy_from_slice(&bytes);
}
self.code
}
fn emit_rel32(&mut self, label: Label) {
self.fixups.push((self.code.len(), label.0));
self.code.extend_from_slice(&[0, 0, 0, 0]);
}
pub fn cmp_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x3d]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn test_rcx_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x85, 0xc9]);
}
pub fn mov_rcx_rdi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x89, 0xf9]);
}
pub fn zero_rax(&mut self) {
self.code.extend_from_slice(&[0x48, 0x31, 0xc0]);
}
pub fn add_rax_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x01, 0xc8]);
}
pub fn dec_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0xff, 0xc9]);
}
pub fn jmp(&mut self, label: Label) {
self.code.push(0xe9);
self.emit_rel32(label);
}
pub fn jle(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x8e]);
self.emit_rel32(label);
}
pub fn jg(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x8f]);
self.emit_rel32(label);
}
pub fn je(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x84]);
self.emit_rel32(label);
}
pub fn push_rdi(&mut self) {
self.code.push(0x57);
}
pub fn push_rsi(&mut self) {
self.code.push(0x56);
}
pub fn push_rax(&mut self) {
self.code.push(0x50);
}
pub fn pop_rax(&mut self) {
self.code.push(0x58);
}
pub fn pop_rcx(&mut self) {
self.code.push(0x59);
}
pub fn movabs_rax(&mut self, imm: i64) {
self.code.extend_from_slice(&[0x48, 0xb8]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn sub_rax_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x29, 0xc8]);
}
pub fn imul_rax_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x0f, 0xaf, 0xc1]);
}
pub fn prologue(&mut self, frame: u32) {
self.code.push(0x55); self.code.extend_from_slice(&[0x48, 0x89, 0xe5]); self.code.extend_from_slice(&[0x48, 0x81, 0xec]); self.code.extend_from_slice(&frame.to_le_bytes());
}
pub fn epilogue(&mut self) {
self.code.push(0xc9); self.code.push(0xc3); }
pub fn store_arg(&mut self, arg: usize, disp: i32) {
match arg {
0 => self.code.extend_from_slice(&[0x48, 0x89, 0xbd]), 1 => self.code.extend_from_slice(&[0x48, 0x89, 0xb5]), 2 => self.code.extend_from_slice(&[0x48, 0x89, 0x95]), 3 => self.code.extend_from_slice(&[0x48, 0x89, 0x8d]), 4 => self.code.extend_from_slice(&[0x4c, 0x89, 0x85]), _ => self.code.extend_from_slice(&[0x4c, 0x89, 0x8d]), }
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn load_rax(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x8b, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn store_rax(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x89, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn load_rcx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x8b, 0x8d]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn to_int32_rax(&mut self) {
self.code.extend_from_slice(&[0x48, 0x63, 0xc0]);
}
pub fn to_int32_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x63, 0xc9]);
}
pub fn bit_rax_rcx(&mut self, op: BinOp2) {
let opc = match op {
BinOp2::And => 0x21,
BinOp2::Or => 0x09,
BinOp2::Xor => 0x31,
_ => 0x21,
};
self.code.extend_from_slice(&[0x48, opc, 0xc8]);
}
pub fn not_eax(&mut self) {
self.code.extend_from_slice(&[0xf7, 0xd0]);
}
pub fn cqo(&mut self) {
self.code.extend_from_slice(&[0x48, 0x99]);
}
pub fn idiv_rcx(&mut self) {
self.code.extend_from_slice(&[0x48, 0xf7, 0xf9]);
}
pub fn mov_rax_rdx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x89, 0xd0]);
}
pub fn shift_eax_cl(&mut self, op: ShiftOp) {
let modrm = match op {
ShiftOp::Shl => 0xe0, ShiftOp::Sar => 0xf8, ShiftOp::Shr => 0xe8, };
self.code.extend_from_slice(&[0xd3, modrm]);
}
pub fn cmp_rax_mem(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x3b, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn movabs_r11(&mut self, imm: i64) {
self.code.extend_from_slice(&[0x49, 0xbb]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn movabs_r10(&mut self, imm: i64) {
self.code.extend_from_slice(&[0x49, 0xba]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn cmp_rax_r11(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x39, 0xd8]);
}
pub fn cmp_rax_r10(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x39, 0xd0]);
}
pub fn jo(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x80]);
self.emit_rel32(label);
}
pub fn jl(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x8c]);
self.emit_rel32(label);
}
pub fn test_rax_rax(&mut self) {
self.code.extend_from_slice(&[0x48, 0x85, 0xc0]);
}
pub fn setl_rax(&mut self) {
self.code.extend_from_slice(&[0x0f, 0x9c, 0xc0]); self.code.extend_from_slice(&[0x48, 0x0f, 0xb6, 0xc0]); }
pub fn sete_rax(&mut self) {
self.code.extend_from_slice(&[0x0f, 0x94, 0xc0]); self.code.extend_from_slice(&[0x48, 0x0f, 0xb6, 0xc0]); }
pub fn op_rax_mem(&mut self, op: BinOp2, disp: i32) {
match op {
BinOp2::Add => self.code.extend_from_slice(&[0x48, 0x03, 0x85]),
BinOp2::Sub => self.code.extend_from_slice(&[0x48, 0x2b, 0x85]),
BinOp2::Mul => self.code.extend_from_slice(&[0x48, 0x0f, 0xaf, 0x85]),
BinOp2::And => self.code.extend_from_slice(&[0x48, 0x23, 0x85]),
BinOp2::Or => self.code.extend_from_slice(&[0x48, 0x0b, 0x85]),
BinOp2::Xor => self.code.extend_from_slice(&[0x48, 0x33, 0x85]),
}
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn movsd_xmm0_mem(&mut self, disp: i32) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x10, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn movsd_mem_xmm0(&mut self, disp: i32) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x11, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn store_arg_f64(&mut self, arg: usize, disp: i32) {
let modrm = match arg {
0 => 0x85,
1 => 0x8d,
2 => 0x95,
_ => 0x9d,
};
self.code.extend_from_slice(&[0xf2, 0x0f, 0x11, modrm]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn ucomisd_xmm0_mem(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x2e, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn zero_xmm1(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x57, 0xc9]);
}
pub fn zero_xmm0(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x57, 0xc0]);
}
pub fn sqrtsd_xmm0(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x51, 0xc0]);
}
pub fn roundsd_xmm0(&mut self, mode: u8) {
self.code
.extend_from_slice(&[0x66, 0x0f, 0x3a, 0x0b, 0xc0, mode]);
}
pub fn movsd_xmm1_mem(&mut self, disp: i32) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x10, 0x8d]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn maxsd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x5f, 0xc1]);
}
pub fn minsd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x5d, 0xc1]);
}
pub fn andpd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x54, 0xc1]);
}
pub fn orpd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x56, 0xc1]);
}
pub fn addsd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x58, 0xc1]);
}
pub fn subsd_xmm1_xmm0(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x5c, 0xc8]);
}
pub fn movsd_mem_xmm1(&mut self, disp: i32) {
self.code.extend_from_slice(&[0xf2, 0x0f, 0x11, 0x8d]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn jp(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x8a]);
self.emit_rel32(label);
}
pub fn jne(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x85]);
self.emit_rel32(label);
}
pub fn abs_xmm0(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x76, 0xc9]); self.code.extend_from_slice(&[0x66, 0x0f, 0x73, 0xd1, 0x01]); self.code.extend_from_slice(&[0x66, 0x0f, 0x54, 0xc1]); }
pub fn ordered_equal_rax(&mut self) {
self.code.extend_from_slice(&[0x0f, 0x94, 0xc0]); self.code.extend_from_slice(&[0x0f, 0x9b, 0xc1]); self.code.extend_from_slice(&[0x20, 0xc8]); self.code.extend_from_slice(&[0x48, 0x0f, 0xb6, 0xc0]); }
pub fn ucomisd_xmm0_xmm1(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x2e, 0xc1]);
}
pub fn seta_rax(&mut self) {
self.code.extend_from_slice(&[0x0f, 0x97, 0xc0]); self.code.extend_from_slice(&[0x48, 0x0f, 0xb6, 0xc0]); }
pub fn cvtsi2sd_xmm0_rax(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x48, 0x0f, 0x2a, 0xc0]);
}
pub fn fbin_xmm0_mem(&mut self, op: FBinOp, disp: i32) {
let opcode = match op {
FBinOp::Add => 0x58,
FBinOp::Sub => 0x5c,
FBinOp::Mul => 0x59,
FBinOp::Div => 0x5e,
};
self.code.extend_from_slice(&[0xf2, 0x0f, opcode, 0x85]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_rax_rdi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x89, 0xf8]);
}
pub fn mov_rax_rsi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x89, 0xf0]);
}
pub fn add_rax_rsi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x01, 0xf0]);
}
pub fn sub_rax_rsi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x29, 0xf0]);
}
pub fn imul_rax_rsi(&mut self) {
self.code.extend_from_slice(&[0x48, 0x0f, 0xaf, 0xc6]);
}
pub fn add_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x05]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn sub_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x2d]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn and_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x25]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn or_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x0d]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn xor_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x35]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn imul_rax_imm(&mut self, imm: i32) {
self.code.extend_from_slice(&[0x48, 0x69, 0xc0]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn shl_rax_imm(&mut self, imm: u8) {
self.code.extend_from_slice(&[0x48, 0xc1, 0xe0, imm]);
}
pub fn sar_rax_imm(&mut self, imm: u8) {
self.code.extend_from_slice(&[0x48, 0xc1, 0xf8, imm]);
}
pub fn neg_rax(&mut self) {
self.code.extend_from_slice(&[0x48, 0xf7, 0xd8]);
}
pub fn ret(&mut self) {
self.code.push(0xc3);
}
pub fn call_rax(&mut self) {
self.code.extend_from_slice(&[0xff, 0xd0]);
}
pub fn load_argreg(&mut self, i: usize, disp: i32) {
let (rex, modrm): (u8, u8) = match i {
0 => (0x48, 0xbd), 1 => (0x48, 0xb5), 2 => (0x48, 0x95), 3 => (0x48, 0x8d), 4 => (0x4c, 0x85), 5 => (0x4c, 0x8d), _ => return,
};
self.code.extend_from_slice(&[rex, 0x8b, modrm]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn sub_rsp_imm8(&mut self, imm: u8) {
self.code.extend_from_slice(&[0x48, 0x83, 0xec, imm]);
}
pub fn add_rsp_imm8(&mut self, imm: u8) {
self.code.extend_from_slice(&[0x48, 0x83, 0xc4, imm]);
}
}
#[must_use]
pub const fn available() -> bool {
cfg!(all(target_os = "linux", target_arch = "x86_64"))
}
pub struct JitFunction {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
buf: exec::ExecBuffer,
}
impl JitFunction {
#[must_use]
pub fn compile_arith(ops: &[ArithOp]) -> Option<Self> {
let mut a = X64Assembler::new();
a.mov_rax_rdi();
for op in ops {
match *op {
ArithOp::AddImm(n) => a.add_rax_imm(n),
ArithOp::SubImm(n) => a.sub_rax_imm(n),
ArithOp::MulImm(n) => a.imul_rax_imm(n),
ArithOp::AndImm(n) => a.and_rax_imm(n),
ArithOp::OrImm(n) => a.or_rax_imm(n),
ArithOp::XorImm(n) => a.xor_rax_imm(n),
ArithOp::ShlImm(n) => a.shl_rax_imm(n),
ArithOp::SarImm(n) => a.sar_rax_imm(n),
ArithOp::Neg => a.neg_rax(),
}
}
a.ret();
Self::from_code(&a.finish())
}
#[must_use]
pub fn compile_binary(op: BinOp) -> Option<Self> {
let mut a = X64Assembler::new();
a.mov_rax_rdi();
match op {
BinOp::Add => a.add_rax_rsi(),
BinOp::Sub => a.sub_rax_rsi(),
BinOp::Mul => a.imul_rax_rsi(),
}
a.ret();
Self::from_code(&a.finish())
}
#[must_use]
pub fn compile_sum_1_to_n() -> Option<Self> {
let mut a = X64Assembler::new();
let loop_top = a.new_label();
let done = a.new_label();
a.zero_rax();
a.mov_rcx_rdi();
a.bind(loop_top);
a.test_rcx_rcx();
a.jle(done);
a.add_rax_rcx();
a.dec_rcx();
a.jmp(loop_top);
a.bind(done);
a.ret();
Self::from_code(&a.finish())
}
#[must_use]
pub fn compile_stack(ops: &[StackOp]) -> Option<Self> {
let mut depth: i32 = 0;
for op in ops {
match op {
StackOp::Arg(_) | StackOp::Const(_) => depth += 1,
StackOp::Add | StackOp::Sub | StackOp::Mul => {
if depth < 2 {
return None;
}
depth -= 1;
}
}
}
if depth != 1 {
return None;
}
let mut a = X64Assembler::new();
for op in ops {
match *op {
StackOp::Arg(0) => a.push_rdi(),
StackOp::Arg(_) => a.push_rsi(),
StackOp::Const(n) => {
a.movabs_rax(n);
a.push_rax();
}
StackOp::Add | StackOp::Sub | StackOp::Mul => {
a.pop_rcx(); a.pop_rax(); match op {
StackOp::Add => a.add_rax_rcx(),
StackOp::Sub => a.sub_rax_rcx(),
_ => a.imul_rax_rcx(),
}
a.push_rax();
}
}
}
a.pop_rax();
a.ret();
Self::from_code(&a.finish())
}
#[must_use]
pub fn compile_reg(n_regs: usize, n_args: usize, ops: &[RegOp]) -> Option<Self> {
if n_regs > 64 || n_args > 6 {
return None;
}
let ok_reg = |r: u8| (r as usize) < n_regs;
let disp = |r: u8| -((i32::from(r) + 1) * 8);
let frame = ((n_regs as u32 * 8) + 15) & !15;
let mut a = X64Assembler::new();
let labels: Vec<Label> = (0..ops.len()).map(|_| a.new_label()).collect();
let deopt = a.new_label();
a.prologue(frame);
a.zero_rax();
for r in 0..n_regs {
a.store_rax(-((r as i32 + 1) * 8));
}
a.movabs_r11(SAFE_INT_MAX);
a.movabs_r10(-SAFE_INT_MAX);
macro_rules! guard {
($asm:expr, $ovf:expr) => {{
if $ovf {
$asm.jo(deopt);
}
$asm.cmp_rax_r11();
$asm.jg(deopt);
$asm.cmp_rax_r10();
$asm.jl(deopt);
}};
}
let mut has_ret = false;
for (i, op) in ops.iter().enumerate() {
a.bind(labels[i]);
match *op {
RegOp::Arg { dst, index } => {
if !ok_reg(dst) || index as usize >= n_args {
return None;
}
a.store_arg(index as usize, disp(dst));
}
RegOp::Const { dst, imm } => {
if !ok_reg(dst) {
return None;
}
a.movabs_rax(imm);
a.store_rax(disp(dst));
}
RegOp::Bin {
dst,
a: ra,
b: rb,
op,
} => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rax(disp(ra));
a.op_rax_mem(op, disp(rb));
let can_overflow = matches!(op, BinOp2::Add | BinOp2::Sub | BinOp2::Mul);
guard!(a, can_overflow);
a.store_rax(disp(dst));
}
RegOp::Move { dst, src } => {
if !ok_reg(dst) || !ok_reg(src) {
return None;
}
a.load_rax(disp(src));
a.store_rax(disp(dst));
}
RegOp::Lt { dst, a: ra, b: rb } => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rax(disp(ra));
a.cmp_rax_mem(disp(rb));
a.setl_rax();
a.store_rax(disp(dst));
}
RegOp::BitNot32 { dst, a: ra } => {
if !ok_reg(dst) || !ok_reg(ra) {
return None;
}
a.load_rax(disp(ra));
a.not_eax();
a.to_int32_rax();
a.store_rax(disp(dst));
}
RegOp::Mod { dst, a: ra, b: rb } => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rcx(disp(rb));
a.test_rcx_rcx();
a.je(deopt);
a.load_rax(disp(ra));
a.cqo();
a.idiv_rcx();
a.mov_rax_rdx();
a.store_rax(disp(dst));
}
RegOp::Eqz { dst, a: ra } => {
if !ok_reg(dst) || !ok_reg(ra) {
return None;
}
a.load_rax(disp(ra));
a.test_rax_rax();
a.sete_rax(); a.store_rax(disp(dst));
}
RegOp::Eq { dst, a: ra, b: rb } => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rax(disp(ra));
a.cmp_rax_mem(disp(rb));
a.sete_rax(); a.store_rax(disp(dst));
}
RegOp::Neg { dst, a: ra } => {
if !ok_reg(dst) || !ok_reg(ra) {
return None;
}
a.load_rax(disp(ra));
a.neg_rax();
guard!(a, true); a.store_rax(disp(dst));
}
RegOp::Bit32 {
dst,
a: ra,
b: rb,
op,
} => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rax(disp(ra));
a.to_int32_rax();
a.load_rcx(disp(rb));
a.to_int32_rcx();
a.bit_rax_rcx(op);
a.store_rax(disp(dst));
}
RegOp::Shift32 {
dst,
a: ra,
b: rb,
op,
} => {
if !ok_reg(dst) || !ok_reg(ra) || !ok_reg(rb) {
return None;
}
a.load_rax(disp(ra));
a.load_rcx(disp(rb));
a.shift_eax_cl(op);
if matches!(op, ShiftOp::Shl | ShiftOp::Sar) {
a.to_int32_rax(); }
a.store_rax(disp(dst));
}
RegOp::JumpIfFalse { cond, target } => {
if !ok_reg(cond) || target >= ops.len() {
return None;
}
a.load_rax(disp(cond));
a.test_rax_rax();
a.je(labels[target]); }
RegOp::Jump { target } => {
if target >= ops.len() {
return None;
}
a.jmp(labels[target]);
}
RegOp::Ret { src } => {
if !ok_reg(src) {
return None;
}
a.load_rax(disp(src));
a.epilogue();
has_ret = true;
}
RegOp::Call {
dst,
code_ptr,
n_args,
args,
} => {
if !ok_reg(dst) || n_args as usize > 6 {
return None;
}
for (i, &arg) in args[..n_args as usize].iter().enumerate() {
if !ok_reg(arg) {
return None;
}
a.load_argreg(i, disp(arg));
}
a.movabs_rax(code_ptr as i64);
a.call_rax();
a.movabs_r11(SAFE_INT_MAX);
a.movabs_r10(-SAFE_INT_MAX);
guard!(a, false);
a.store_rax(disp(dst));
}
}
}
if !has_ret {
return None;
}
a.bind(deopt);
a.movabs_rax(i64::MAX);
a.epilogue();
Self::from_code(&a.finish())
}
#[must_use]
pub fn compile_float(n_regs: usize, n_args: usize, ops: &[FloatOp]) -> Option<Self> {
if n_regs > 64 || n_args > 4 {
return None;
}
let ok = |r: u8| (r as usize) < n_regs;
let disp = |r: u8| -((i32::from(r) + 1) * 8);
let frame = ((n_regs as u32 * 8) + 15) & !15;
let mut a = X64Assembler::new();
let labels: Vec<Label> = (0..ops.len()).map(|_| a.new_label()).collect();
a.prologue(frame);
let mut has_ret = false;
for (i, op) in ops.iter().enumerate() {
a.bind(labels[i]);
match *op {
FloatOp::Arg { dst, index } => {
if !ok(dst) || index as usize >= n_args {
return None;
}
a.store_arg_f64(index as usize, disp(dst));
}
FloatOp::Const { dst, imm } => {
if !ok(dst) {
return None;
}
a.movabs_rax(imm.to_bits() as i64);
a.store_rax(disp(dst));
}
FloatOp::Bin {
dst,
a: ra,
b: rb,
op,
} => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
a.movsd_xmm0_mem(disp(ra));
a.fbin_xmm0_mem(op, disp(rb));
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Move { dst, src } => {
if !ok(dst) || !ok(src) {
return None;
}
a.movsd_xmm0_mem(disp(src));
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Lt { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
a.movsd_xmm0_mem(disp(rb));
a.ucomisd_xmm0_mem(disp(ra));
a.seta_rax();
a.cvtsi2sd_xmm0_rax();
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::JumpIfFalse { cond, target } => {
if !ok(cond) || target >= ops.len() {
return None;
}
a.movsd_xmm0_mem(disp(cond));
a.zero_xmm1();
a.ucomisd_xmm0_xmm1();
a.je(labels[target]);
}
FloatOp::Jump { target } => {
if target >= ops.len() {
return None;
}
a.jmp(labels[target]);
}
FloatOp::Neg { dst, a: ra } => {
if !ok(dst) || !ok(ra) {
return None;
}
a.zero_xmm0();
a.fbin_xmm0_mem(FBinOp::Sub, disp(ra));
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Sqrt { dst, a: ra } => {
if !ok(dst) || !ok(ra) {
return None;
}
a.movsd_xmm0_mem(disp(ra));
a.sqrtsd_xmm0();
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Floor { dst, a: ra }
| FloatOp::Ceil { dst, a: ra }
| FloatOp::Trunc { dst, a: ra } => {
if !ok(dst) || !ok(ra) || !has_sse41() {
return None;
}
let mode = match op {
FloatOp::Floor { .. } => 0x09,
FloatOp::Ceil { .. } => 0x0a,
_ => 0x0b,
};
a.movsd_xmm0_mem(disp(ra));
a.roundsd_xmm0(mode);
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Mod { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) || !has_sse41() {
return None;
}
a.movsd_xmm1_mem(disp(ra)); a.movsd_xmm0_mem(disp(ra)); a.fbin_xmm0_mem(FBinOp::Div, disp(rb)); a.roundsd_xmm0(0x0b); a.fbin_xmm0_mem(FBinOp::Mul, disp(rb)); a.subsd_xmm1_xmm0(); a.movsd_mem_xmm1(disp(dst));
}
FloatOp::Abs { dst, a: ra } => {
if !ok(dst) || !ok(ra) {
return None;
}
a.movsd_xmm0_mem(disp(ra));
a.abs_xmm0();
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Max { dst, a: ra, b: rb } | FloatOp::Min { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let is_max = matches!(op, FloatOp::Max { .. });
let nan = a.new_label();
let neq = a.new_label();
let done = a.new_label();
a.movsd_xmm0_mem(disp(ra));
a.movsd_xmm1_mem(disp(rb));
a.ucomisd_xmm0_xmm1();
a.jp(nan);
a.jne(neq);
if is_max {
a.andpd_xmm0_xmm1();
} else {
a.orpd_xmm0_xmm1();
}
a.jmp(done);
a.bind(neq);
if is_max {
a.maxsd_xmm0_xmm1();
} else {
a.minsd_xmm0_xmm1();
}
a.jmp(done);
a.bind(nan);
a.addsd_xmm0_xmm1();
a.bind(done);
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Eqz { dst, a: ra } => {
if !ok(dst) || !ok(ra) {
return None;
}
a.movsd_xmm0_mem(disp(ra));
a.zero_xmm1();
a.ucomisd_xmm0_xmm1();
a.sete_rax();
a.cvtsi2sd_xmm0_rax();
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Eq { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
a.movsd_xmm0_mem(disp(ra));
a.ucomisd_xmm0_mem(disp(rb));
a.ordered_equal_rax();
a.cvtsi2sd_xmm0_rax();
a.movsd_mem_xmm0(disp(dst));
}
FloatOp::Ret { src } => {
if !ok(src) {
return None;
}
a.movsd_xmm0_mem(disp(src)); a.epilogue();
has_ret = true;
}
}
}
if !has_ret {
return None;
}
Self::from_code(&a.finish())
}
#[must_use]
pub fn from_machine_code(code: &[u8]) -> Option<Self> {
Self::from_code(code)
}
#[must_use]
pub fn code_ptr(&self) -> usize {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
self.buf.ptr() as usize
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
0
}
}
#[must_use]
pub fn call_args_f64(&self, args: &[f64]) -> f64 {
let mut a = [0.0f64; 4];
for (slot, v) in a.iter_mut().zip(args.iter()) {
*slot = *v;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
#[allow(unsafe_code)]
let f: extern "C" fn(f64, f64, f64, f64) -> f64 =
unsafe { core::mem::transmute(self.buf.ptr()) };
f(a[0], a[1], a[2], a[3])
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
let _ = a;
unreachable!("JitFunction cannot be constructed on this target")
}
}
#[must_use]
pub fn call_args(&self, args: &[i64]) -> i64 {
let mut a = [0i64; 6];
for (slot, v) in a.iter_mut().zip(args.iter()) {
*slot = *v;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
#[allow(unsafe_code)]
let f: extern "C" fn(i64, i64, i64, i64, i64, i64) -> i64 =
unsafe { core::mem::transmute(self.buf.ptr()) };
f(a[0], a[1], a[2], a[3], a[4], a[5])
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
let _ = a;
unreachable!("JitFunction cannot be constructed on this target")
}
}
#[must_use]
pub fn call1(&self, a: i64) -> i64 {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
#[allow(unsafe_code)]
let f: extern "C" fn(i64) -> i64 = unsafe { core::mem::transmute(self.buf.ptr()) };
f(a)
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
let _ = a;
unreachable!("JitFunction cannot be constructed on this target")
}
}
#[must_use]
pub fn call2(&self, a: i64, b: i64) -> i64 {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
#[allow(unsafe_code)]
let f: extern "C" fn(i64, i64) -> i64 = unsafe { core::mem::transmute(self.buf.ptr()) };
f(a, b)
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
let _ = (a, b);
unreachable!("JitFunction cannot be constructed on this target")
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn from_code(code: &[u8]) -> Option<Self> {
exec::ExecBuffer::new(code).map(|buf| Self { buf })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn from_code(_code: &[u8]) -> Option<Self> {
None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BinOp {
Add,
Sub,
Mul,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod exec {
const PROT_RW: usize = 0x1 | 0x2;
const PROT_RX: usize = 0x1 | 0x4;
const MAP_PRIVATE_ANON: usize = 0x02 | 0x20;
const SYS_MMAP: usize = 9;
const SYS_MPROTECT: usize = 10;
const SYS_MUNMAP: usize = 11;
#[allow(unsafe_code)]
unsafe fn syscall6(
n: usize,
a1: usize,
a2: usize,
a3: usize,
a4: usize,
a5: usize,
a6: usize,
) -> isize {
let ret: isize;
unsafe {
core::arch::asm!(
"syscall",
inlateout("rax") n as isize => ret,
in("rdi") a1,
in("rsi") a2,
in("rdx") a3,
in("r10") a4,
in("r8") a5,
in("r9") a6,
out("rcx") _,
out("r11") _,
options(nostack, preserves_flags),
);
}
ret
}
pub(super) struct ExecBuffer {
ptr: *mut u8,
len: usize,
}
impl ExecBuffer {
pub(super) fn new(code: &[u8]) -> Option<Self> {
if code.is_empty() {
return None;
}
let page = 4096;
let len = code.len().div_ceil(page) * page;
#[allow(unsafe_code)]
let raw = unsafe {
syscall6(
SYS_MMAP,
0,
len,
PROT_RW,
MAP_PRIVATE_ANON,
usize::MAX, 0,
)
};
if (-4095..0).contains(&raw) {
return None;
}
let ptr = raw as *mut u8;
#[allow(unsafe_code)]
unsafe {
core::ptr::copy_nonoverlapping(code.as_ptr(), ptr, code.len());
}
#[allow(unsafe_code)]
let prot = unsafe { syscall6(SYS_MPROTECT, ptr as usize, len, PROT_RX, 0, 0, 0) };
if prot < 0 {
#[allow(unsafe_code)]
unsafe {
syscall6(SYS_MUNMAP, ptr as usize, len, 0, 0, 0, 0);
}
return None;
}
Some(Self { ptr, len })
}
#[must_use]
pub(super) fn ptr(&self) -> *const u8 {
self.ptr
}
}
impl Drop for ExecBuffer {
fn drop(&mut self) {
#[allow(unsafe_code)]
unsafe {
syscall6(SYS_MUNMAP, self.ptr as usize, self.len, 0, 0, 0, 0);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn arith_op_eval_matches_native_when_available() {
let ops = vec![
ArithOp::AddImm(5),
ArithOp::MulImm(3),
ArithOp::SubImm(2),
ArithOp::XorImm(0x0f),
ArithOp::Neg,
];
let interp = eval_arith(&ops, 7);
if let Some(f) = JitFunction::compile_arith(&ops) {
assert_eq!(f.call1(7), interp, "JIT must match the interpreter");
} else {
assert!(!available(), "compile only fails when JIT is unavailable");
}
}
#[test]
fn assembler_emits_expected_bytes() {
let mut a = X64Assembler::new();
a.mov_rax_rdi();
a.add_rax_rsi();
a.ret();
assert_eq!(a.code(), &[0x48, 0x89, 0xf8, 0x48, 0x01, 0xf0, 0xc3]);
}
#[test]
fn jit_arithmetic_runs_natively() {
if !available() {
return;
}
let ops = [ArithOp::AddImm(10), ArithOp::MulImm(2), ArithOp::SubImm(3)];
let f = JitFunction::compile_arith(&ops).expect("jit available");
for x in [-5i64, 0, 1, 100, 1_000_000] {
assert_eq!(f.call1(x), (x + 10) * 2 - 3);
assert_eq!(f.call1(x), eval_arith(&ops, x));
}
}
#[test]
fn jit_binary_ops_run_natively() {
if !available() {
return;
}
let add = JitFunction::compile_binary(BinOp::Add).unwrap();
let sub = JitFunction::compile_binary(BinOp::Sub).unwrap();
let mul = JitFunction::compile_binary(BinOp::Mul).unwrap();
assert_eq!(add.call2(20, 22), 42);
assert_eq!(sub.call2(50, 8), 42);
assert_eq!(mul.call2(6, 7), 42);
assert_eq!(mul.call2(-3, 4), -12);
}
#[test]
fn stack_machine_compiles_and_runs() {
let prog = [
StackOp::Arg(0),
StackOp::Arg(1),
StackOp::Add,
StackOp::Arg(0),
StackOp::Const(3),
StackOp::Sub,
StackOp::Mul,
];
let oracle = |a: i64, b: i64| (a + b) * (a - 3);
for (a, b) in [(7, 2), (10, -4), (0, 0), (-5, 5), (1000, 1)] {
assert_eq!(eval_stack(&prog, [a, b]), oracle(a, b));
if let Some(f) = JitFunction::compile_stack(&prog) {
assert_eq!(f.call2(a, b), oracle(a, b), "jit stack ({a},{b})");
}
}
}
#[test]
fn stack_machine_rejects_malformed() {
assert!(
JitFunction::compile_stack(&[StackOp::Add]).is_none(),
"underflow"
);
assert!(
JitFunction::compile_stack(&[StackOp::Arg(0), StackOp::Arg(1)]).is_none(),
"two results"
);
assert!(JitFunction::compile_stack(&[]).is_none(), "empty");
}
#[test]
fn register_machine_compiles_and_runs() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Arg { dst: 1, index: 1 },
RegOp::Arg { dst: 2, index: 2 },
RegOp::Bin {
dst: 3,
a: 0,
b: 1,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 3,
a: 3,
b: 2,
op: BinOp2::Mul,
},
RegOp::Bin {
dst: 3,
a: 3,
b: 0,
op: BinOp2::Sub,
},
RegOp::Ret { src: 3 },
];
let oracle = |a: i64, b: i64, c: i64| (a + b) * c - a;
for (a, b, c) in [
(2, 3, 4),
(10, -5, 2),
(0, 0, 9),
(-7, 7, -1),
(100, 1, 1000),
] {
assert_eq!(eval_reg(&ops, 4, &[a, b, c]), oracle(a, b, c));
if let Some(f) = JitFunction::compile_reg(4, 3, &ops) {
assert_eq!(
f.call_args(&[a, b, c]),
oracle(a, b, c),
"jit reg ({a},{b},{c})"
);
}
}
}
#[test]
fn register_machine_uses_constants_and_many_regs() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 100 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Mul,
},
RegOp::Const { dst: 3, imm: 7 },
RegOp::Bin {
dst: 4,
a: 2,
b: 3,
op: BinOp2::Or,
},
RegOp::Bin {
dst: 5,
a: 4,
b: 0,
op: BinOp2::Xor,
},
RegOp::Ret { src: 5 },
];
let oracle = |a: i64| ((a * 100) | 7) ^ a;
for a in [0i64, 1, 5, 42, -3, 12345] {
assert_eq!(eval_reg(&ops, 6, &[a]), oracle(a));
if let Some(f) = JitFunction::compile_reg(6, 1, &ops) {
assert_eq!(f.call_args(&[a]), oracle(a), "jit reg const ({a})");
}
}
}
#[test]
fn lowers_real_nbvm_constant_function() {
let program = crate::parser::Parser::parse_program("1 + 2 * 3").expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let lowered = lower_nbvm(&protos[0]).expect("top-level should lower");
assert_eq!(eval_reg(&lowered, protos[0].n_regs, &[]), 7);
if let Some(f) = JitFunction::compile_reg(protos[0].n_regs, 0, &lowered) {
assert_eq!(f.call_args(&[]), 7, "JIT runs real compiled bytecode");
}
}
#[test]
fn lowers_and_jits_a_real_arithmetic_function() {
let src = "function f(a, b) { return a * b + a - b; } f;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let mut tested = false;
for p in &protos {
if p.n_params != 2 {
continue;
}
if let Some(lowered) = lower_nbvm(p) {
let oracle = |a: i64, b: i64| a * b + a - b;
for (a, b) in [(2, 3), (10, -4), (0, 7), (-5, -5), (123, 2)] {
let via_ir = eval_reg(&lowered, p.n_regs, &[a, b]);
assert_eq!(via_ir, oracle(a, b), "lowered IR matches semantics");
if let Some(f) = JitFunction::compile_reg(p.n_regs, 2, &lowered) {
assert_eq!(f.call_args(&[a, b]), oracle(a, b), "JIT matches ({a},{b})");
}
}
tested = true;
}
}
assert!(tested, "expected an integer arithmetic proto to lower");
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn jit_deopts_on_overflow_and_range() {
use crate::nanbox::{NanBox, Unpacked};
let src = "function f(a, b) { return a * b; } f;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let p = protos.iter().find(|p| p.n_params == 2).unwrap();
let jit = JitProto::compile(p).expect("f compiles");
let r = jit
.call_guarded(&[NanBox::number(1000.0), NanBox::number(1000.0)])
.unwrap();
assert_eq!(r.unpack(), Unpacked::Number(1_000_000.0));
let big = (1i64 << 30) as f64;
assert!(
jit.call_guarded(&[NanBox::number(big), NanBox::number(big)])
.is_none(),
"a product beyond 2^53 must deopt, not return a wrapped i64"
);
let huge = 3_000_000_000.0; assert!(
jit.call_guarded(&[NanBox::number(huge), NanBox::number(huge)])
.is_none(),
"i64-overflowing product must deopt"
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn jit_compiles_a_real_loop() {
use crate::nanbox::{NanBox, Unpacked};
let src = "function f(n){ let s = 0; for (let i = 0; i < n; i = i + 1) { s = s + i; } return s; } f;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let p = protos.iter().find(|p| p.n_params == 1).expect("f's proto");
let lowered = lower_nbvm(p).expect("loop should lower (Lt/JumpIfFalse/Jump)");
for n in [0i64, 1, 5, 10, 50] {
assert_eq!(
eval_reg(&lowered, p.n_regs, &[n]),
n * (n - 1) / 2,
"sum 0..{n}"
);
}
let jit = JitProto::compile(p).expect("loop JIT-compiles");
for n in [0i64, 1, 5, 10, 50, 100] {
let r = jit
.call_guarded(&[NanBox::number(n as f64)])
.expect("native loop");
assert_eq!(
r.unpack(),
Unpacked::Number((n * (n - 1) / 2) as f64),
"jit sum 0..{n}"
);
}
}
#[test]
fn float_jit_compiles_a_loop() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 }, FloatOp::Const { dst: 1, imm: 0.0 }, FloatOp::Const { dst: 2, imm: 0.0 }, FloatOp::Const { dst: 3, imm: 0.5 }, FloatOp::Lt { dst: 4, a: 2, b: 0 }, FloatOp::JumpIfFalse { cond: 4, target: 9 }, FloatOp::Bin {
dst: 1,
a: 1,
b: 2,
op: FBinOp::Add,
}, FloatOp::Bin {
dst: 2,
a: 2,
b: 3,
op: FBinOp::Add,
}, FloatOp::Jump { target: 4 }, FloatOp::Ret { src: 1 }, ];
let oracle = |n: f64| {
let (mut s, mut x) = (0.0, 0.0);
while x < n {
s += x;
x += 0.5;
}
s
};
for n in [0.0, 1.0, 3.0, 5.0, 10.0] {
assert_eq!(eval_float(&ops, 5, &[n]), oracle(n));
if let Some(f) = JitFunction::compile_float(5, 1, &ops) {
assert_eq!(f.call_args_f64(&[n]), oracle(n), "float loop n={n}");
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_jitproto_runs_division_function() {
use crate::nanbox::{NanBox, Unpacked};
let src = "function f(a, b) { return (a + b) * a / b; } f;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let p = protos.iter().find(|p| p.n_params == 2).unwrap();
assert!(
lower_nbvm(p).is_none(),
"division shouldn't take the integer path"
);
assert!(lower_nbvm_float(p).is_some(), "should take the float path");
let jit = JitProto::compile(p).expect("float JIT");
let oracle = |a: f64, b: f64| (a + b) * a / b;
for (a, b) in [(1.5, 2.5), (10.0, 4.0), (-3.0, 0.5), (7.0, 7.0)] {
let r = jit
.call_guarded(&[NanBox::number(a), NanBox::number(b)])
.expect("number args run natively");
match r.unpack() {
Unpacked::Number(v) => assert!((v - oracle(a, b)).abs() < 1e-12, "{v}"),
_ => panic!("expected number"),
}
}
assert!(
jit.call_guarded(&[NanBox::null(), NanBox::number(1.0)])
.is_none()
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn jit_proto_end_to_end_with_deopt_guard() {
use crate::nanbox::{NanBox, Unpacked};
let src = "function f(a, b) { return a * b + a - b; } f;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
let p = protos.iter().find(|p| p.n_params == 2).expect("f's proto");
let jit = JitProto::compile(p).expect("f should JIT-compile");
let r = jit
.call_guarded(&[NanBox::number(6.0), NanBox::number(7.0)])
.expect("integer args run natively");
assert_eq!(r.unpack(), Unpacked::Number(41.0));
let r = jit
.call_guarded(&[NanBox::number(-3.0), NanBox::number(4.0)])
.unwrap();
assert_eq!(r.unpack(), Unpacked::Number(-3.0 * 4.0 + -3.0 - 4.0));
assert!(
jit.call_guarded(&[NanBox::number(1.5), NanBox::number(2.0)])
.is_none(),
"non-integer arg must deopt"
);
assert!(
jit.call_guarded(&[NanBox::boolean(true), NanBox::number(2.0)])
.is_none(),
"boolean arg must deopt"
);
assert!(
jit.call_guarded(&[NanBox::number(1.0)]).is_none(),
"arity mismatch deopts"
);
}
#[test]
fn lower_rejects_read_before_write() {
use crate::nbvm::{FnProto, Op};
let proto = FnProto {
ops: alloc::vec![Op::Mul { dst: 1, a: 0, b: 2 }, Op::Return { src: 1 },],
n_regs: 3,
n_params: 1,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
};
assert!(
lower_nbvm(&proto).is_none(),
"read-before-write must not lower"
);
let ok = FnProto {
ops: alloc::vec![
Op::LoadConst {
dst: 2,
value: crate::nanbox::NanBox::number(3.0)
},
Op::Mul { dst: 1, a: 0, b: 2 },
Op::Return { src: 1 },
],
n_regs: 3,
n_params: 1,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
};
assert!(lower_nbvm(&ok).is_some(), "written-then-read should lower");
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn lower_nbvm_wires_a_static_call() {
use crate::nbvm::{FnProto, Op};
let b = FnProto {
ops: alloc::vec![
Op::LoadConst {
dst: 1,
value: crate::nanbox::NanBox::number(3.0)
},
Op::Mul { dst: 2, a: 0, b: 1 },
Op::Return { src: 2 },
],
n_regs: 3,
n_params: 1,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
};
let bjit = JitProto::compile(&b).expect("compile B");
let b_ptr = bjit.code_ptr();
assert_ne!(b_ptr, 0);
let mut registry = alloc::collections::BTreeMap::new();
registry.insert(7u32, b_ptr as u64);
let a = FnProto {
ops: alloc::vec![
Op::Call {
dst: 1,
func: 7,
args: alloc::vec![0]
}, Op::Add { dst: 2, a: 1, b: 0 }, Op::Return { src: 2 },
],
n_regs: 3,
n_params: 1,
n_captures: 0,
rest_from: None,
is_async: false,
length: 0,
name: alloc::string::String::new(),
};
assert!(lower_nbvm(&a).is_none(), "unregistered call must bail");
let lowered = lower_nbvm_with(&a, ®istry).expect("registered call lowers");
assert!(
lowered.iter().any(|o| matches!(o, RegOp::Call { .. })),
"emits a Call op"
);
let ajit = JitProto::compile_with_registry(&a, ®istry).expect("compile A");
for x in [0i64, 1, 5, 12] {
let r = ajit
.call_guarded(&[crate::nanbox::NanBox::number(x as f64)])
.expect("no deopt");
assert_eq!(
r.unpack(),
crate::nanbox::Unpacked::Number((4 * x) as f64),
"A({x})"
);
}
}
#[test]
fn does_not_lower_non_integer_functions() {
let src = "function g(a){ return Math.max(a, 1); } g;";
let program = crate::parser::Parser::parse_program(src).expect("parse");
let protos = crate::nbvm::compile_program(&program).expect("compile");
for p in &protos {
if p.n_params == 1 {
assert!(lower_nbvm(p).is_none(), "g uses a call, must not lower");
}
}
}
#[test]
fn optimize_reg_folds_constants() {
let ops = [
RegOp::Const { dst: 0, imm: 2 },
RegOp::Const { dst: 1, imm: 3 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 3,
a: 2,
b: 0,
op: BinOp2::Mul,
},
RegOp::Ret { src: 3 },
];
let opt = optimize_reg(&ops, 4);
assert_eq!(opt.len(), 2, "folded + DCE'd to two ops: {opt:?}");
assert!(matches!(opt[0], RegOp::Const { imm: 10, .. }));
assert!(matches!(opt[1], RegOp::Ret { .. }));
assert_eq!(eval_reg(&opt, 4, &[]), eval_reg(&ops, 4, &[]));
assert_eq!(eval_reg(&opt, 4, &[]), 10);
}
#[test]
fn register_allocator_reuses_slots() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Bin {
dst: 1,
a: 0,
b: 0,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 2,
a: 1,
b: 1,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 3,
a: 2,
b: 2,
op: BinOp2::Add,
},
RegOp::Ret { src: 3 },
];
let (alloc, n) = allocate_reg(&ops, 4);
assert!(n <= 2, "allocated to {n} slots: {alloc:?}");
for x in [0i64, 1, 3, -5, 100] {
assert_eq!(eval_reg(&alloc, n, &[x]), eval_reg(&ops, 4, &[x]));
assert_eq!(eval_reg(&alloc, n, &[x]), 8 * x);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn allocated_program_runs_natively() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Arg { dst: 1, index: 1 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 3,
a: 2,
b: 0,
op: BinOp2::Mul,
},
RegOp::Ret { src: 3 },
];
let (alloc, n) = allocate_reg(&ops, 4);
let f = JitFunction::compile_reg(n, 2, &alloc).unwrap();
let oracle = |a: i64, b: i64| (a + b) * a;
for (a, b) in [(3, 4), (10, -2), (0, 5)] {
assert_eq!(f.call_args(&[a, b]), oracle(a, b), "alloc native ({a},{b})");
}
}
#[test]
fn allocator_preserves_a_loop() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 0 },
RegOp::Const { dst: 2, imm: 0 },
RegOp::Const { dst: 3, imm: 1 },
RegOp::Lt { dst: 4, a: 2, b: 0 },
RegOp::JumpIfFalse { cond: 4, target: 9 },
RegOp::Bin {
dst: 1,
a: 1,
b: 2,
op: BinOp2::Add,
},
RegOp::Bin {
dst: 2,
a: 2,
b: 3,
op: BinOp2::Add,
},
RegOp::Jump { target: 4 },
RegOp::Ret { src: 1 },
];
let (alloc, n) = allocate_reg(&ops, 5);
for x in [0i64, 1, 5, 10, 50] {
assert_eq!(
eval_reg(&alloc, n, &[x]),
x * (x - 1) / 2,
"loop sum 0..{x}"
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn bit32_matches_js_toint32_semantics() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Arg { dst: 1, index: 1 },
RegOp::Bit32 {
dst: 2,
a: 0,
b: 1,
op: BinOp2::And,
},
RegOp::Ret { src: 2 },
];
let (alloc, n) = allocate_reg(&optimize_reg(&ops, 3), 3);
let f = JitFunction::compile_reg(n, 2, &alloc).unwrap();
for (a, b) in [
(0xFF, 0x0F),
(0xF0F0, 0x0FF0),
(1 << 32 | 0xF0, 0xFF),
(-1, 0x1234),
] {
let expect = i64::from((a as i32) & (b as i32));
assert_eq!(f.call_args(&[a, b]), expect, "({a:#x} & {b:#x})");
assert_eq!(eval_reg(&ops, 3, &[a, b]), expect);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn mod_compiles_and_deopts_on_zero() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Arg { dst: 1, index: 1 },
RegOp::Mod { dst: 2, a: 0, b: 1 },
RegOp::Ret { src: 2 },
];
let (alloc, n) = allocate_reg(&optimize_reg(&ops, 3), 3);
let f = JitFunction::compile_reg(n, 2, &alloc).unwrap();
for (a, b) in [(17, 5), (-17, 5), (17, -5), (100, 10), (3, 7)] {
let expect = a % b;
assert_eq!(f.call_args(&[a, b]), expect, "{a} % {b}");
assert_eq!(eval_reg(&ops, 3, &[a, b]), expect);
}
assert_eq!(f.call_args(&[5, 0]), i64::MAX, "% 0 must deopt");
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn shift32_matches_js_semantics() {
let run = |op: ShiftOp, a: i64, b: i64| -> i64 {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Arg { dst: 1, index: 1 },
RegOp::Shift32 {
dst: 2,
a: 0,
b: 1,
op,
},
RegOp::Ret { src: 2 },
];
let (alloc, n) = allocate_reg(&optimize_reg(&ops, 3), 3);
let f = JitFunction::compile_reg(n, 2, &alloc).unwrap();
let got = f.call_args(&[a, b]);
assert_eq!(got, eval_reg(&ops, 3, &[a, b]), "jit vs oracle");
got
};
assert_eq!(run(ShiftOp::Shl, 1, 4), 16);
assert_eq!(run(ShiftOp::Shl, 1, 33), 2);
assert_eq!(run(ShiftOp::Sar, -8, 1), -4);
assert_eq!(run(ShiftOp::Shr, -1, 0), 0xFFFF_FFFF);
assert_eq!(run(ShiftOp::Shr, -8, 1), 0x7FFF_FFFC);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn eqz_compiles_and_runs() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 5 },
RegOp::Lt { dst: 2, a: 0, b: 1 },
RegOp::Eqz { dst: 2, a: 2 },
RegOp::Ret { src: 2 },
];
let opt = optimize_reg(&ops, 3);
let (alloc, n) = allocate_reg(&opt, 3);
let f = JitFunction::compile_reg(n, 1, &alloc).unwrap();
for x in [0i64, 4, 5, 6, 100, -3] {
let expect = i64::from(x >= 5); assert_eq!(f.call1(x), expect, "!(x<5) at x={x}");
assert_eq!(eval_reg(&ops, 3, &[x]), expect);
}
}
#[test]
fn strength_reduction_identities() {
type Case = (&'static [RegOp], fn(i64) -> i64);
let cases: &[Case] = &[
(
&[
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 0 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Add,
},
RegOp::Ret { src: 2 },
],
|x| x,
),
(
&[
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 1 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Mul,
},
RegOp::Ret { src: 2 },
],
|x| x,
),
(
&[
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 0 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Mul,
},
RegOp::Ret { src: 2 },
],
|_x| 0,
),
(
&[
RegOp::Arg { dst: 0, index: 0 },
RegOp::Bin {
dst: 2,
a: 0,
b: 0,
op: BinOp2::Sub,
},
RegOp::Ret { src: 2 },
],
|_x| 0,
),
(
&[
RegOp::Arg { dst: 0, index: 0 },
RegOp::Bin {
dst: 2,
a: 0,
b: 0,
op: BinOp2::Xor,
},
RegOp::Ret { src: 2 },
],
|_x| 0,
),
];
for (ops, oracle) in cases {
let opt = optimize_reg(ops, 3);
for x in [0i64, 5, -7, 1234] {
assert_eq!(eval_reg(&opt, 3, &[x]), oracle(x));
assert_eq!(eval_reg(&opt, 3, &[x]), eval_reg(ops, 3, &[x]));
}
}
}
#[test]
fn copy_propagation_forwards_moves() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Move { dst: 1, src: 0 },
RegOp::Move { dst: 2, src: 1 },
RegOp::Bin {
dst: 3,
a: 2,
b: 2,
op: BinOp2::Add,
},
RegOp::Ret { src: 3 },
];
let prop = copy_propagate(&ops, 4);
assert!(
matches!(prop[3], RegOp::Bin { a: 0, b: 0, .. }),
"operands forwarded to the root: {prop:?}"
);
let opt = optimize_reg(&ops, 4);
assert!(
!opt.iter().any(|o| matches!(o, RegOp::Move { .. })),
"moves eliminated: {opt:?}"
);
for x in [0i64, 3, -7, 1000] {
assert_eq!(eval_reg(&opt, 4, &[x]), eval_reg(&ops, 4, &[x]));
assert_eq!(eval_reg(&opt, 4, &[x]), x + x);
}
}
#[test]
fn copy_propagation_invalidates_on_overwrite() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Move { dst: 1, src: 0 },
RegOp::Const { dst: 0, imm: 99 },
RegOp::Ret { src: 1 },
];
let opt = optimize_reg(&ops, 2);
for x in [5i64, -3, 42] {
assert_eq!(
eval_reg(&opt, 2, &[x]),
x,
"r1 keeps the pre-overwrite value"
);
}
}
#[test]
fn dce_removes_dead_ops_and_remaps_branches() {
let ops = [
RegOp::Arg { dst: 0, index: 0 }, RegOp::Const { dst: 9, imm: 999 }, RegOp::Const { dst: 1, imm: 0 }, RegOp::Lt { dst: 2, a: 1, b: 0 }, RegOp::JumpIfFalse { cond: 2, target: 6 }, RegOp::Jump { target: 6 }, RegOp::Ret { src: 1 }, ];
let opt = dce_reg(&ops);
assert!(
!opt.iter().any(|o| matches!(o, RegOp::Const { dst: 9, .. })),
"dead const removed: {opt:?}"
);
for x in [0i64, 5, -3] {
assert_eq!(eval_reg(&opt, 10, &[x]), eval_reg(&ops, 10, &[x]));
}
}
#[test]
fn optimize_reg_preserves_arg_dependent_and_branches() {
let ops = [
RegOp::Arg { dst: 0, index: 0 },
RegOp::Const { dst: 1, imm: 10 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Mul,
},
RegOp::Ret { src: 2 },
];
let opt = optimize_reg(&ops, 3);
assert!(
matches!(opt[2], RegOp::Bin { .. }),
"arg-dependent op not folded"
);
for x in [0i64, 3, -7, 1000] {
assert_eq!(eval_reg(&opt, 3, &[x]), eval_reg(&ops, 3, &[x]));
assert_eq!(eval_reg(&opt, 3, &[x]), x * 10);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn optimized_program_runs_natively() {
let ops = [
RegOp::Const { dst: 0, imm: 6 },
RegOp::Const { dst: 1, imm: 7 },
RegOp::Bin {
dst: 2,
a: 0,
b: 1,
op: BinOp2::Mul,
},
RegOp::Ret { src: 2 },
];
let opt = optimize_reg(&ops, 3);
let f = JitFunction::compile_reg(3, 0, &opt).unwrap();
assert_eq!(f.call_args(&[]), 42, "folded 6*7 runs natively");
}
#[test]
fn register_machine_rejects_malformed() {
assert!(JitFunction::compile_reg(2, 1, &[RegOp::Ret { src: 5 }]).is_none());
assert!(JitFunction::compile_reg(2, 1, &[RegOp::Const { dst: 0, imm: 1 }]).is_none());
assert!(JitFunction::compile_reg(2, 1, &[RegOp::Arg { dst: 0, index: 3 }]).is_none());
}
#[test]
fn float_jit_compiles_and_runs_with_division() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Arg { dst: 1, index: 1 },
FloatOp::Bin {
dst: 2,
a: 0,
b: 1,
op: FBinOp::Add,
},
FloatOp::Bin {
dst: 2,
a: 2,
b: 0,
op: FBinOp::Mul,
},
FloatOp::Bin {
dst: 2,
a: 2,
b: 1,
op: FBinOp::Div,
},
FloatOp::Ret { src: 2 },
];
let oracle = |a: f64, b: f64| (a + b) * a / b;
for (a, b) in [(1.5, 2.5), (10.0, 4.0), (-3.5, 0.5), (7.0, 7.0), (0.1, 0.3)] {
assert!((eval_float(&ops, 3, &[a, b]) - oracle(a, b)).abs() < 1e-12);
if let Some(f) = JitFunction::compile_float(3, 2, &ops) {
let got = f.call_args_f64(&[a, b]);
assert!(
(got - oracle(a, b)).abs() < 1e-12,
"jit f64 ({a},{b}): {got}"
);
}
}
}
#[test]
fn float_jit_modulo() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Arg { dst: 1, index: 1 },
FloatOp::Mod { dst: 0, a: 0, b: 1 },
FloatOp::Ret { src: 0 },
];
let oracle = |a: f64, b: f64| a % b; for (a, b) in [
(10.0, 3.0),
(10.5, 3.0),
(-10.5, 3.0),
(10.5, -3.0),
(7.0, 7.0),
(1.0, 0.25),
(5.5, 2.2),
] {
let want = oracle(a, b);
assert!(
(eval_float(&ops, 2, &[a, b]) - want).abs() < 1e-12,
"oracle ({a} % {b})"
);
if let Some(f) = JitFunction::compile_float(2, 2, &ops) {
let got = f.call_args_f64(&[a, b]);
assert!(
(got - want).abs() < 1e-12,
"jit ({a} % {b}): {got} vs {want}"
);
}
}
}
#[test]
fn float_jit_constants() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Const { dst: 1, imm: 0.5 },
FloatOp::Const { dst: 2, imm: 1.25 },
FloatOp::Bin {
dst: 0,
a: 0,
b: 1,
op: FBinOp::Mul,
},
FloatOp::Bin {
dst: 0,
a: 0,
b: 2,
op: FBinOp::Add,
},
FloatOp::Ret { src: 0 },
];
for x in [4.0, -2.0, 0.0, 100.5] {
let expect = x * 0.5 + 1.25;
assert_eq!(eval_float(&ops, 3, &[x]), expect);
if let Some(f) = JitFunction::compile_float(3, 1, &ops) {
assert_eq!(f.call_args_f64(&[x]), expect, "const f64 ({x})");
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn jit_code_calls_another_jit_function() {
let b = JitFunction::compile_arith(&[ArithOp::MulImm(2)]).expect("compile B");
let b_ptr = b.code_ptr();
assert_ne!(b_ptr, 0);
let mut a = X64Assembler::new();
a.sub_rsp_imm8(8);
a.movabs_rax(b_ptr as i64);
a.call_rax();
a.add_rsp_imm8(8);
a.add_rax_imm(1);
a.ret();
let af = JitFunction::from_machine_code(&a.finish()).expect("compile A");
for x in [0i64, 1, 5, -3, 1000] {
assert_eq!(af.call1(x), 2 * x + 1, "A({x}) via native call to B");
}
assert_eq!(b.call1(21), 42);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_sqrt_and_abs_native() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Abs { dst: 1, a: 0 },
FloatOp::Sqrt { dst: 2, a: 1 },
FloatOp::Ret { src: 2 },
];
let f = JitFunction::compile_float(3, 1, &ops).unwrap();
for x in [4.0f64, -9.0, 2.0, 0.0, -0.0, 1e6, 0.25] {
let expect = x.abs().sqrt();
let got = f.call_args_f64(&[x]);
assert!(
(got - expect).abs() < 1e-12 || (got.is_nan() && expect.is_nan()),
"sqrt(|{x}|): got {got}, want {expect}"
);
assert_eq!(eval_float(&ops, 3, &[x]), expect);
}
let neg = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Sqrt { dst: 1, a: 0 },
FloatOp::Ret { src: 1 },
];
let g = JitFunction::compile_float(2, 1, &neg).unwrap();
assert!(g.call_args_f64(&[-1.0]).is_nan(), "sqrt(-1) is NaN");
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_floor_ceil_native() {
if !std::is_x86_feature_detected!("sse4.1") {
return; }
let floor = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Floor { dst: 1, a: 0 },
FloatOp::Ret { src: 1 },
];
let ceil = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Ceil { dst: 1, a: 0 },
FloatOp::Ret { src: 1 },
];
let ff = JitFunction::compile_float(2, 1, &floor).unwrap();
let fc = JitFunction::compile_float(2, 1, &ceil).unwrap();
for x in [3.7f64, -3.2, 2.0, -0.4, 1e6, -7.999] {
assert_eq!(
ff.call_args_f64(&[x]).to_bits(),
x.floor().to_bits(),
"floor({x})"
);
assert_eq!(
fc.call_args_f64(&[x]).to_bits(),
x.ceil().to_bits(),
"ceil({x})"
);
assert_eq!(eval_float(&floor, 2, &[x]), x.floor());
assert_eq!(eval_float(&ceil, 2, &[x]), x.ceil());
}
let trunc = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Trunc { dst: 1, a: 0 },
FloatOp::Ret { src: 1 },
];
let ft = JitFunction::compile_float(2, 1, &trunc).unwrap();
for x in [3.7f64, -3.7, 3.2, -0.5, 2.0, -0.0, 1e6, -7.999] {
assert_eq!(
ft.call_args_f64(&[x]).to_bits(),
x.trunc().to_bits(),
"trunc({x})"
);
assert_eq!(eval_float(&trunc, 2, &[x]).to_bits(), x.trunc().to_bits());
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_min_max_js_semantics() {
let max = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Arg { dst: 1, index: 1 },
FloatOp::Max { dst: 2, a: 0, b: 1 },
FloatOp::Ret { src: 2 },
];
let min = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Arg { dst: 1, index: 1 },
FloatOp::Min { dst: 2, a: 0, b: 1 },
FloatOp::Ret { src: 2 },
];
let fmax = JitFunction::compile_float(3, 2, &max).unwrap();
let fmin = JitFunction::compile_float(3, 2, &min).unwrap();
let cases = [
(3.0f64, 5.0),
(5.0, 3.0),
(-2.0, -7.0),
(1.5, 1.5),
(0.0, -0.0),
(-0.0, 0.0),
(f64::INFINITY, 1.0),
(f64::NAN, 1.0),
(1.0, f64::NAN),
];
for (x, y) in cases {
assert_eq!(
fmax.call_args_f64(&[x, y]).to_bits(),
eval_float(&max, 3, &[x, y]).to_bits(),
"max({x}, {y})"
);
assert_eq!(
fmin.call_args_f64(&[x, y]).to_bits(),
eval_float(&min, 3, &[x, y]).to_bits(),
"min({x}, {y})"
);
}
assert_eq!(
fmax.call_args_f64(&[0.0, -0.0]).to_bits(),
0.0f64.to_bits(),
"max(+0,-0)=+0"
);
assert_eq!(
fmin.call_args_f64(&[0.0, -0.0]).to_bits(),
(-0.0f64).to_bits(),
"min(+0,-0)=-0"
);
assert!(
fmax.call_args_f64(&[f64::NAN, 1.0]).is_nan(),
"max(NaN,1)=NaN"
);
assert!(
fmin.call_args_f64(&[1.0, f64::NAN]).is_nan(),
"min(1,NaN)=NaN"
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_eq_is_nan_aware() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Arg { dst: 1, index: 1 },
FloatOp::Eq { dst: 2, a: 0, b: 1 },
FloatOp::Ret { src: 2 },
];
let f = JitFunction::compile_float(3, 2, &ops).unwrap();
let cases = [
(1.5, 1.5, 1.0),
(1.5, 2.5, 0.0),
(0.0, -0.0, 1.0), (f64::NAN, f64::NAN, 0.0), (f64::NAN, 1.0, 0.0),
(f64::INFINITY, f64::INFINITY, 1.0),
];
for (a, b, expect) in cases {
assert_eq!(f.call_args_f64(&[a, b]), expect, "{a} === {b}");
assert_eq!(eval_float(&ops, 3, &[a, b]), expect);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_eqz_handles_zero_and_nan() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Eqz { dst: 1, a: 0 },
FloatOp::Ret { src: 1 },
];
let f = JitFunction::compile_float(2, 1, &ops).unwrap();
for x in [0.0f64, -0.0, 1.5, -3.0, f64::NAN, f64::INFINITY] {
let expect = if x == 0.0 || x.is_nan() { 1.0 } else { 0.0 };
assert_eq!(f.call_args_f64(&[x]), expect, "!{x}");
assert_eq!(eval_float(&ops, 2, &[x]), expect);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn float_neg_compiles_and_runs() {
let ops = [
FloatOp::Arg { dst: 0, index: 0 },
FloatOp::Neg { dst: 1, a: 0 },
FloatOp::Const { dst: 2, imm: 1.5 },
FloatOp::Bin {
dst: 3,
a: 1,
b: 2,
op: FBinOp::Add,
},
FloatOp::Ret { src: 3 },
];
let f = JitFunction::compile_float(4, 1, &ops).unwrap();
for x in [0.0f64, 2.5, -3.25, 100.0] {
let expect = -x + 1.5;
assert_eq!(f.call_args_f64(&[x]), expect, "-{x} + 1.5");
assert_eq!(eval_float(&ops, 4, &[x]), expect);
}
}
#[test]
fn native_loop_sum() {
if !available() {
return;
}
let f = JitFunction::compile_sum_1_to_n().expect("jit available");
for n in [0i64, 1, 2, 5, 10, 100, 1000] {
assert_eq!(f.call1(n), n * (n + 1) / 2, "sum 1..={n}");
assert_eq!(f.call1(n), eval_sum_1_to_n(n));
}
assert_eq!(f.call1(-5), 0);
}
#[test]
fn label_backpatch_forward_and_backward() {
let mut a = X64Assembler::new();
let back = a.new_label();
let fwd = a.new_label();
a.bind(back);
a.zero_rax();
a.jmp(fwd); a.add_rax_imm(99); a.bind(fwd);
a.je(back); a.ret();
let code = a.finish();
let jmp_rel = i32::from_le_bytes([code[4], code[5], code[6], code[7]]);
assert!(jmp_rel >= 0, "forward jump is non-negative");
}
#[test]
fn shifts_and_bitwise() {
let ops = [ArithOp::ShlImm(4), ArithOp::OrImm(1), ArithOp::SarImm(1)];
let interp = eval_arith(&ops, 3);
assert_eq!(interp, ((3i64 << 4) | 1) >> 1);
if let Some(f) = JitFunction::compile_arith(&ops) {
assert_eq!(f.call1(3), interp);
}
}
}