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"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GenericOp {
Arg {
dst: u8,
index: u8,
},
Const {
dst: u8,
imm: u64,
},
Add {
dst: u8,
a: u8,
b: u8,
},
Move {
dst: u8,
src: u8,
},
GetProp {
dst: u8,
obj: u8,
site: u16,
},
SetProp {
obj: u8,
val: u8,
site: u16,
},
GetElem {
dst: u8,
obj: u8,
index: u8,
},
SetElem {
obj: u8,
index: u8,
val: u8,
},
ArrayLen {
dst: u8,
arr: u8,
},
Arith {
dst: u8,
a: u8,
b: u8,
op: u8,
},
Lt {
dst: u8,
a: u8,
b: u8,
},
StrictEq {
dst: u8,
a: u8,
b: u8,
},
ValueBin {
dst: u8,
a: u8,
b: u8,
op: u8,
},
Not {
dst: u8,
a: u8,
},
Jump {
target: usize,
},
JumpIfFalse {
cond: u8,
target: usize,
},
Call {
dst: u8,
func: u32,
site: u16,
},
CallNative {
dst: u8,
native: u16,
site: u16,
},
Ret {
src: u8,
},
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
type GenericLowering = (Vec<GenericOp>, Vec<alloc::boxed::Box<str>>, Vec<Vec<u8>>);
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[must_use]
pub fn lower_nbvm_generic(proto: &crate::nbvm::FnProto) -> Option<GenericLowering> {
use crate::nbvm::Op;
if proto.n_regs > 64 || proto.n_params > 32 || 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();
let mut keys: Vec<alloc::boxed::Box<str>> = Vec::new();
let mut call_args: Vec<Vec<u8>> = Vec::new();
for i in 0..proto.n_params {
out.push(GenericOp::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 d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::Const {
dst: d,
imm: value.to_bits(),
}
}
Op::AddValue { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::Add { dst: d, a, b }
}
Op::Move { dst, src } => {
let s = read(&written, *src)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::Move { dst: d, src: s }
}
Op::Sub { dst, a, b }
| Op::Mul { dst, a, b }
| Op::Div { dst, a, b }
| Op::Mod { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
let op = match op {
Op::Sub { .. } => crate::nbvm::GA_SUB,
Op::Mul { .. } => crate::nbvm::GA_MUL,
Op::Div { .. } => crate::nbvm::GA_DIV,
_ => crate::nbvm::GA_MOD,
};
GenericOp::Arith { dst: d, a, b, op }
}
Op::Lt { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::Lt { dst: d, a, b }
}
Op::StrictEq { dst, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::StrictEq { dst: d, a, b }
}
Op::ValueBin { dst, op, a, b } => {
let (a, b) = (read(&written, *a)?, read(&written, *b)?);
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::ValueBin {
dst: d,
a,
b,
op: *op,
}
}
Op::Not { dst, a } => {
let a = read(&written, *a)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::Not { dst: d, a }
}
Op::Jump { target } => GenericOp::Jump {
target: target.checked_add(proto.n_params)?,
},
Op::JumpIfFalse { cond, target } => GenericOp::JumpIfFalse {
cond: read(&written, *cond)?,
target: target.checked_add(proto.n_params)?,
},
Op::GetProp { dst, obj, key } => {
let o = read(&written, *obj)?;
let d = reg8(*dst)?;
let site = u16::try_from(keys.len()).ok()?;
keys.push(alloc::boxed::Box::from(key.as_str()));
written[*dst as usize] = true;
GenericOp::GetProp {
dst: d,
obj: o,
site,
}
}
Op::SetProp { obj, key, src } => {
let o = read(&written, *obj)?;
let v = read(&written, *src)?;
let site = u16::try_from(keys.len()).ok()?;
keys.push(alloc::boxed::Box::from(key.as_str()));
GenericOp::SetProp {
obj: o,
val: v,
site,
}
}
Op::GetKey { dst, obj, key } => {
let o = read(&written, *obj)?;
let idx = read(&written, *key)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::GetElem {
dst: d,
obj: o,
index: idx,
}
}
Op::SetKey { obj, key, src } => {
let o = read(&written, *obj)?;
let idx = read(&written, *key)?;
let v = read(&written, *src)?;
GenericOp::SetElem {
obj: o,
index: idx,
val: v,
}
}
Op::ArrayLen { dst, arr } => {
let a = read(&written, *arr)?;
let d = reg8(*dst)?;
written[*dst as usize] = true;
GenericOp::ArrayLen { dst: d, arr: a }
}
Op::Call { dst, func, args } => {
if args.len() > 32 {
return None;
}
let mut argregs: Vec<u8> = Vec::with_capacity(args.len());
for r in args {
argregs.push(read(&written, *r)?);
}
let d = reg8(*dst)?;
let site = u16::try_from(call_args.len()).ok()?;
call_args.push(argregs);
written[*dst as usize] = true;
GenericOp::Call {
dst: d,
func: *func,
site,
}
}
Op::CallNative { dst, native, args } => {
if args.len() > 32 {
return None;
}
let mut argregs: Vec<u8> = Vec::with_capacity(args.len());
for r in args {
argregs.push(read(&written, *r)?);
}
let d = reg8(*dst)?;
let site = u16::try_from(call_args.len()).ok()?;
call_args.push(argregs);
written[*dst as usize] = true;
GenericOp::CallNative {
dst: d,
native: *native,
site,
}
}
Op::Return { src } => GenericOp::Ret {
src: read(&written, *src)?,
},
_ => return None,
};
out.push(lowered);
}
if !matches!(out.last(), Some(GenericOp::Ret { .. })) {
return None;
}
for op in &out {
if let GenericOp::Jump { target } | GenericOp::JumpIfFalse { target, .. } = op
&& *target >= out.len()
{
return None;
}
}
Some((out, keys, call_args))
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub struct JitProto {
func: JitFunction,
n_params: usize,
kind: JitKind,
sites: Option<GenericSites>,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
struct GenericSites {
#[allow(dead_code)]
keys: Vec<alloc::boxed::Box<str>>,
caches: alloc::boxed::Box<[core::cell::UnsafeCell<crate::ic::PropertyCache>]>,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub struct GenericHelpers {
pub add: JitAddHelper,
pub get: JitGetPropHelper,
pub set: JitSetPropHelper,
pub get_elem: JitAddHelper,
pub set_elem: JitBinHelper,
pub array_len: JitTruthyHelper,
pub array_len_dense: JitTruthyHelper,
pub arith: JitBinHelper,
pub value_bin: JitBinHelper,
pub lt: JitAddHelper,
pub strict_eq: JitAddHelper,
pub truthy: JitTruthyHelper,
pub call: JitCallHelper,
pub call_native: JitCallNativeHelper,
pub arena: JitArenaHelper,
pub array_unrestricted: JitTruthyHelper,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[repr(C)]
pub struct JitArena {
pub base: *const u8,
pub len: usize,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitArenaHelper = extern "C" fn(*mut core::ffi::c_void) -> JitArena;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug)]
pub(crate) struct JitLayout {
pub slot_stride: i32,
pub slot_occupied_disc: u8,
pub cell_object_disc: u8,
pub obj_shaped_disc: u8,
pub off_slot_gen: i32,
pub off_cell_tag: i32,
pub off_od_tag: i32,
pub off_shape: i32,
pub off_slots_ptr: i32,
pub off_slots_len: i32,
pub cache_shape_off: i32,
pub cache_slot_off: i32,
pub cache_hits_off: i32,
pub cell_array_disc: u8,
pub off_arr_ptr: i32,
pub off_arr_len: i32,
pub off_frozen: i32,
pub off_readonly_len: i32,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn compute_jit_layout() -> &'static JitLayout {
use std::sync::OnceLock;
static LAYOUT: OnceLock<JitLayout> = OnceLock::new();
LAYOUT.get_or_init(|| {
let slot = crate::heap::jit_slot_layout(crate::cell::Cell::Object(
crate::object::Object::new(crate::shape::Shape::root()),
));
let cell = crate::cell::jit_cell_layout();
let obj = crate::object::jit_object_layout();
let cache = crate::ic::jit_cache_layout();
let cell_base = slot.value_off;
let obj_base = cell_base + cell.object_off;
let od_base = obj_base + obj.data_off;
let slots_base = od_base + obj.slots_off;
let to_i32 = |v: usize| i32::try_from(v).expect("jit layout offset fits in i32");
JitLayout {
slot_stride: to_i32(slot.stride),
slot_occupied_disc: slot.occupied_disc,
cell_object_disc: cell.object_disc,
obj_shaped_disc: obj.shaped_disc,
off_slot_gen: to_i32(slot.generation_off),
off_cell_tag: to_i32(cell_base),
off_od_tag: to_i32(od_base),
off_shape: to_i32(od_base + obj.shape_off),
off_slots_ptr: to_i32(slots_base + obj.vec_ptr_off),
off_slots_len: to_i32(slots_base + obj.vec_len_off),
cache_shape_off: to_i32(cache.shape_off),
cache_slot_off: to_i32(cache.slot_off),
cache_hits_off: to_i32(cache.hits_off),
cell_array_disc: cell.array_disc,
off_arr_ptr: to_i32(cell_base + cell.array_vec_off + obj.vec_ptr_off),
off_arr_len: to_i32(cell_base + cell.array_vec_off + obj.vec_len_off),
off_frozen: to_i32(obj_base + obj.frozen_off),
off_readonly_len: to_i32(obj_base + obj.readonly_len_off),
}
})
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitBinHelper = extern "C" fn(*mut core::ffi::c_void, u64, u64, u64) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitTruthyHelper = extern "C" fn(*mut core::ffi::c_void, u64) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitGetPropHelper =
extern "C" fn(*mut core::ffi::c_void, u64, *const u8, usize, *mut core::ffi::c_void) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitSetPropHelper = extern "C" fn(
*mut core::ffi::c_void,
u64,
*const u8,
usize,
*mut core::ffi::c_void,
u64,
) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy)]
pub struct SiteInfo {
key_ptr: *const u8,
key_len: usize,
cache_ptr: *mut core::ffi::c_void,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AbiKind {
Int,
Float,
Generic,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, PartialEq, Eq)]
enum JitKind {
Int,
Float,
Generic,
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitAddHelper = extern "C" fn(*mut core::ffi::c_void, u64, u64) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitCallHelper = extern "C" fn(*mut core::ffi::c_void, u64, u64, *const u64, usize) -> u64;
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
pub type JitCallNativeHelper = extern "C" fn(*mut core::ffi::c_void, u64, *const u64, usize) -> u64;
#[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,
sites: None,
});
}
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,
sites: None,
})
}
#[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))
}
JitKind::Generic => None,
}
}
#[must_use]
pub fn compile_generic(proto: &crate::nbvm::FnProto, helpers: &GenericHelpers) -> Option<Self> {
Self::compile_generic_with_registry(proto, helpers, &alloc::collections::BTreeMap::new())
}
#[must_use]
pub fn compile_generic_with_registry(
proto: &crate::nbvm::FnProto,
helpers: &GenericHelpers,
generic_registry: &alloc::collections::BTreeMap<u32, (u64, usize)>,
) -> Option<Self> {
let (ops, keys, call_args) = lower_nbvm_generic(proto)?;
let caches: alloc::boxed::Box<[core::cell::UnsafeCell<crate::ic::PropertyCache>]> = (0
..keys.len())
.map(|_| core::cell::UnsafeCell::new(crate::ic::PropertyCache::new()))
.collect();
let site_info: Vec<SiteInfo> = keys
.iter()
.zip(caches.iter())
.map(|(k, c)| SiteInfo {
key_ptr: k.as_ptr(),
key_len: k.len(),
cache_ptr: c.get().cast::<core::ffi::c_void>(),
})
.collect();
let func = JitFunction::compile_generic(
proto.n_regs,
proto.n_params,
&ops,
helpers,
&site_info,
&call_args,
generic_registry,
)?;
Some(Self {
func,
n_params: proto.n_params,
kind: JitKind::Generic,
sites: Some(GenericSites { keys, caches }),
})
}
#[must_use]
pub fn ic_hits(&self) -> u32 {
self.sites.as_ref().map_or(0, |s| {
s.caches
.iter()
.map(|c| {
#[allow(unsafe_code)]
unsafe {
(*c.get()).hits()
}
})
.sum()
})
}
#[must_use]
pub fn ic_misses(&self) -> u32 {
self.sites.as_ref().map_or(0, |s| {
s.caches
.iter()
.map(|c| {
#[allow(unsafe_code)]
unsafe {
(*c.get()).misses()
}
})
.sum()
})
}
#[must_use]
pub fn is_generic(&self) -> bool {
self.kind == JitKind::Generic
}
#[must_use]
pub fn abi_kind(&self) -> AbiKind {
match self.kind {
JitKind::Int => AbiKind::Int,
JitKind::Float => AbiKind::Float,
JitKind::Generic => AbiKind::Generic,
}
}
#[must_use]
pub fn call_generic_raw(
&self,
ctx: *mut core::ffi::c_void,
args: &[crate::nanbox::NanBox],
) -> Option<u64> {
if args.len() != self.n_params {
return None;
}
let bits: Vec<u64> = args.iter().map(|a| a.to_bits()).collect();
Some(self.func.call_generic(ctx, bits.as_ptr(), bits.len()))
}
}
#[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 movabs_argreg(&mut self, i: usize, imm: i64) {
let (rex, op): (u8, u8) = match i {
0 => (0x48, 0xbf), 1 => (0x48, 0xbe), 2 => (0x48, 0xba), 3 => (0x48, 0xb9), 4 => (0x49, 0xb8), 5 => (0x49, 0xb9), _ => return,
};
self.code.extend_from_slice(&[rex, op]);
self.code.extend_from_slice(&imm.to_le_bytes());
}
pub fn lea_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, 0x8d, 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]);
}
pub fn generic_prologue(&mut self, frame: u32) {
self.code.push(0x55); self.code.extend_from_slice(&[0x48, 0x89, 0xe5]); self.code.extend_from_slice(&[0x41, 0x57]); self.code.extend_from_slice(&[0x48, 0x81, 0xec]); self.code.extend_from_slice(&frame.to_le_bytes());
}
pub fn generic_epilogue(&mut self) {
self.code.extend_from_slice(&[0x48, 0x8d, 0x65, 0xf8]); self.code.extend_from_slice(&[0x41, 0x5f]); self.code.push(0x5d); self.code.push(0xc3); }
pub fn mov_r15_rdi(&mut self) {
self.code.extend_from_slice(&[0x49, 0x89, 0xff]);
}
pub fn mov_rdi_r15(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x89, 0xff]);
}
pub fn mov_rax_rsi_disp(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x8b, 0x86]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn and_rax_r11(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x21, 0xd8]);
}
pub fn ucomisd_xmm0_xmm0(&mut self) {
self.code.extend_from_slice(&[0x66, 0x0f, 0x2e, 0xc0]);
}
pub fn mov_rcx_rax(&mut self) {
self.code.extend_from_slice(&[0x48, 0x89, 0xc1]);
}
pub fn and_rcx_r11(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x21, 0xd9]);
}
pub fn cmp_rcx_r11(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x39, 0xd9]);
}
pub fn add_rax_r11(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x01, 0xd8]);
}
pub fn setne_rax(&mut self) {
self.code.extend_from_slice(&[0x0f, 0x95, 0xc0]); self.code.extend_from_slice(&[0x48, 0x0f, 0xb6, 0xc0]); }
pub fn emit_truthy(
&mut self,
disp: i32,
truthy_helper: u64,
qnan: u64,
tag_true: u64,
tag_false: u64,
) {
let numl = self.new_label();
let set1 = self.new_label();
let set0 = self.new_label();
let have = self.new_label();
self.load_rax(disp);
self.movabs_r11(qnan as i64);
self.mov_rcx_rax();
self.and_rcx_r11();
self.cmp_rcx_r11();
self.jne(numl); self.movabs_r11(tag_true as i64);
self.cmp_rax_r11();
self.je(set1);
self.movabs_r11(tag_false as i64);
self.cmp_rax_r11();
self.je(set0);
self.mov_rdi_r15();
self.load_argreg(1, disp);
self.movabs_rax(truthy_helper as i64);
self.call_rax();
self.jmp(have);
self.bind(numl);
self.movsd_xmm0_mem(disp);
self.zero_xmm1();
self.ucomisd_xmm0_xmm1();
self.setne_rax();
self.jmp(have);
self.bind(set1);
self.movabs_rax(1);
self.jmp(have);
self.bind(set0);
self.zero_rax();
self.bind(have);
}
pub fn mov_r10_rax(&mut self) {
self.code.extend_from_slice(&[0x49, 0x89, 0xc2]);
}
pub fn mov_r8_rax(&mut self) {
self.code.extend_from_slice(&[0x49, 0x89, 0xc0]);
}
pub fn shr_r8_imm(&mut self, imm: u8) {
self.code.extend_from_slice(&[0x49, 0xc1, 0xe8, imm]);
}
pub fn cmp_rax_rdx(&mut self) {
self.code.extend_from_slice(&[0x48, 0x39, 0xd0]);
}
pub fn jae(&mut self, label: Label) {
self.code.extend_from_slice(&[0x0f, 0x83]);
self.emit_rel32(label);
}
pub fn add_rax_r10(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x01, 0xd0]);
}
pub fn cmp_byte_at_rcx_imm(&mut self, disp: i32, imm: u8) {
self.code.extend_from_slice(&[0x80, 0xb9]);
self.code.extend_from_slice(&disp.to_le_bytes());
self.code.push(imm);
}
pub fn cmp_word_at_rcx_r8w(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x66, 0x44, 0x39, 0x81]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_rax_at_rcx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x8b, 0x81]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn cmp_rax_at_rcx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x3b, 0x81]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_r11_at_rcx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x4c, 0x8b, 0x99]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn cmp_rax_at_r11(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x49, 0x3b, 0x83]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_eax_at_r11(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x41, 0x8b, 0x83]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_rax_at_r11_rax8(&mut self) {
self.code.extend_from_slice(&[0x49, 0x8b, 0x04, 0xc3]);
}
pub fn add_dword_at_r11_imm(&mut self, disp: i32, imm: u8) {
self.code.extend_from_slice(&[0x41, 0x83, 0x83]);
self.code.extend_from_slice(&disp.to_le_bytes());
self.code.push(imm);
}
pub fn load_rdx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x48, 0x8b, 0x95]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn mov_at_r11_rax8_rdx(&mut self) {
self.code.extend_from_slice(&[0x49, 0x89, 0x14, 0xc3]);
}
pub fn cmp_qword_at_rcx_imm(&mut self, disp: i32, imm: u8) {
self.code.extend_from_slice(&[0x48, 0x83, 0xb9]);
self.code.extend_from_slice(&disp.to_le_bytes());
self.code.push(imm);
}
pub fn mov_r8_at_rcx(&mut self, disp: i32) {
self.code.extend_from_slice(&[0x4c, 0x8b, 0x81]);
self.code.extend_from_slice(&disp.to_le_bytes());
}
pub fn cmp_rax_r8(&mut self) {
self.code.extend_from_slice(&[0x4c, 0x39, 0xc0]);
}
pub fn cvttsd2si_rax_xmm0(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x48, 0x0f, 0x2c, 0xc0]);
}
pub fn cvtsi2sd_xmm1_rax(&mut self) {
self.code.extend_from_slice(&[0xf2, 0x48, 0x0f, 0x2a, 0xc8]);
}
}
#[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())
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[must_use]
pub fn compile_generic(
n_regs: usize,
n_params: usize,
ops: &[GenericOp],
helpers: &GenericHelpers,
sites: &[SiteInfo],
call_args: &[Vec<u8>],
generic_registry: &alloc::collections::BTreeMap<u32, (u64, usize)>,
) -> Option<Self> {
use crate::nanbox::NanBox;
if n_regs > 64 || n_params > 32 {
return None;
}
let add_helper = helpers.add as usize as u64;
let get_helper = helpers.get as usize as u64;
let set_helper = helpers.set as usize as u64;
let get_elem_helper = helpers.get_elem as usize as u64;
let set_elem_helper = helpers.set_elem as usize as u64;
let array_len_helper = helpers.array_len as usize as u64;
let array_len_dense_helper = helpers.array_len_dense as usize as u64;
let arith_helper = helpers.arith as usize as u64;
let value_bin_helper = helpers.value_bin as usize as u64;
let lt_helper = helpers.lt as usize as u64;
let strict_eq_helper = helpers.strict_eq as usize as u64;
let truthy_helper = helpers.truthy as usize as u64;
let call_helper = helpers.call as usize as u64;
let call_native_helper = helpers.call_native as usize as u64;
let arena_helper = helpers.arena as usize as u64;
let array_unrestricted_helper = helpers.array_unrestricted as usize as u64;
let layout = *compute_jit_layout();
let max_argc = call_args.iter().map(Vec::len).max().unwrap_or(0);
if max_argc > 32 {
return None;
}
const QNAN: u64 = 0x7ffc_0000_0000_0000;
const HANDLE_TAG: u64 = 0x8000_0000_0000_0000 | QNAN;
let canonical_nan = NanBox::number(f64::NAN).to_bits();
let tag_false = NanBox::boolean(false).to_bits();
let tag_true = NanBox::boolean(true).to_bits();
let disp = |r: u8| -((i32::from(r) + 2) * 8);
let argbuf_disp = |i: usize| -((n_regs as i32 + 1 + max_argc as i32 - i as i32) * 8);
let frame = ((((n_regs + max_argc) as u32) * 8 + 15) & !15) + 8;
let mut a = X64Assembler::new();
let labels: Vec<Label> = (0..ops.len()).map(|_| a.new_label()).collect();
let throw_exit = a.new_label();
a.generic_prologue(frame);
a.mov_r15_rdi(); a.zero_rax();
for r in 0..n_regs {
a.store_rax(disp(u8::try_from(r).ok()?));
}
let ok = |r: u8| (r as usize) < n_regs;
macro_rules! throw_check {
() => {{
a.movabs_r11(NanBox::jit_throw_bits() as i64);
a.cmp_rax_r11();
a.je(throw_exit);
}};
}
let mut has_ret = false;
for (op_idx, op) in ops.iter().enumerate() {
a.bind(labels[op_idx]);
match *op {
GenericOp::Arg { dst, index } => {
if !ok(dst) || index as usize >= n_params {
return None;
}
a.mov_rax_rsi_disp(i32::from(index) * 8);
a.store_rax(disp(dst));
}
GenericOp::Const { dst, imm } => {
if !ok(dst) {
return None;
}
a.movabs_rax(imm as i64);
a.store_rax(disp(dst));
}
GenericOp::Move { dst, src } => {
if !ok(dst) || !ok(src) {
return None;
}
a.load_rax(disp(src));
a.store_rax(disp(dst));
}
GenericOp::Add { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let slow = a.new_label();
let nan_fix = a.new_label();
let done = a.new_label();
a.movabs_r11(QNAN as i64);
a.load_rax(disp(ra));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow); a.load_rax(disp(rb));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow); a.movsd_xmm0_mem(disp(ra));
a.fbin_xmm0_mem(FBinOp::Add, disp(rb)); a.ucomisd_xmm0_xmm0();
a.jp(nan_fix);
a.movsd_mem_xmm0(disp(dst));
a.jmp(done);
a.bind(nan_fix);
a.movabs_rax(canonical_nan as i64);
a.store_rax(disp(dst));
a.jmp(done);
a.bind(slow);
a.mov_rdi_r15(); a.load_argreg(1, disp(ra)); a.load_argreg(2, disp(rb)); a.movabs_rax(add_helper as i64);
a.call_rax();
a.movabs_r11(NanBox::jit_throw_bits() as i64);
a.cmp_rax_r11();
a.je(throw_exit);
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::GetProp { dst, obj, site } => {
if !ok(dst) || !ok(obj) {
return None;
}
let s = sites.get(site as usize)?;
let helper = a.new_label();
let done = a.new_label();
a.mov_rdi_r15();
a.movabs_rax(arena_helper as i64);
a.call_rax();
a.mov_r10_rax(); a.load_rax(disp(obj)); a.movabs_r11(HANDLE_TAG as i64);
a.mov_rcx_rax();
a.and_rcx_r11();
a.cmp_rcx_r11();
a.jne(helper); a.mov_r8_rax();
a.shr_r8_imm(32);
a.movabs_r11(0xffff_ffff_i64);
a.and_rax_r11(); a.cmp_rax_rdx();
a.jae(helper); a.imul_rax_imm(layout.slot_stride);
a.add_rax_r10();
a.mov_rcx_rax(); a.cmp_byte_at_rcx_imm(0, layout.slot_occupied_disc);
a.jne(helper);
a.cmp_word_at_rcx_r8w(layout.off_slot_gen);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_cell_tag, layout.cell_object_disc);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_od_tag, layout.obj_shaped_disc);
a.jne(helper);
a.mov_rax_at_rcx(layout.off_shape);
a.movabs_r11(s.cache_ptr as i64); a.cmp_rax_at_r11(layout.cache_shape_off);
a.jne(helper);
a.mov_eax_at_r11(layout.cache_slot_off);
a.cmp_rax_at_rcx(layout.off_slots_len);
a.jae(helper);
a.mov_r11_at_rcx(layout.off_slots_ptr); a.mov_rax_at_r11_rax8(); a.movabs_r11(s.cache_ptr as i64);
a.add_dword_at_r11_imm(layout.cache_hits_off, 1);
a.store_rax(disp(dst));
a.jmp(done);
a.bind(helper);
a.mov_rdi_r15(); a.load_argreg(1, disp(obj)); a.movabs_argreg(2, s.key_ptr as i64); a.movabs_argreg(3, s.key_len as i64); a.movabs_argreg(4, s.cache_ptr as i64); a.movabs_rax(get_helper as i64);
a.call_rax();
a.movabs_r11(NanBox::jit_throw_bits() as i64);
a.cmp_rax_r11();
a.je(throw_exit);
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::SetProp { obj, val, site } => {
if !ok(obj) || !ok(val) {
return None;
}
let s = sites.get(site as usize)?;
let helper = a.new_label();
let done = a.new_label();
a.load_rax(disp(val));
a.movabs_r11(HANDLE_TAG as i64);
a.and_rax_r11();
a.cmp_rax_r11();
a.je(helper); a.mov_rdi_r15();
a.movabs_rax(arena_helper as i64);
a.call_rax();
a.mov_r10_rax(); a.load_rax(disp(obj)); a.movabs_r11(HANDLE_TAG as i64);
a.mov_rcx_rax();
a.and_rcx_r11();
a.cmp_rcx_r11();
a.jne(helper); a.mov_r8_rax();
a.shr_r8_imm(32); a.movabs_r11(0xffff_ffff_i64);
a.and_rax_r11(); a.cmp_rax_rdx();
a.jae(helper); a.imul_rax_imm(layout.slot_stride);
a.add_rax_r10();
a.mov_rcx_rax(); a.cmp_byte_at_rcx_imm(0, layout.slot_occupied_disc);
a.jne(helper);
a.cmp_word_at_rcx_r8w(layout.off_slot_gen);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_cell_tag, layout.cell_object_disc);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_od_tag, layout.obj_shaped_disc);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_frozen, 0);
a.jne(helper); a.cmp_qword_at_rcx_imm(layout.off_readonly_len, 0);
a.jne(helper); a.mov_rax_at_rcx(layout.off_shape);
a.movabs_r11(s.cache_ptr as i64); a.cmp_rax_at_r11(layout.cache_shape_off);
a.jne(helper);
a.mov_eax_at_r11(layout.cache_slot_off);
a.cmp_rax_at_rcx(layout.off_slots_len);
a.jae(helper);
a.mov_r11_at_rcx(layout.off_slots_ptr); a.load_rdx(disp(val)); a.mov_at_r11_rax8_rdx(); a.movabs_r11(s.cache_ptr as i64);
a.add_dword_at_r11_imm(layout.cache_hits_off, 1);
a.jmp(done);
a.bind(helper);
a.mov_rdi_r15(); a.load_argreg(1, disp(obj)); a.movabs_argreg(2, s.key_ptr as i64); a.movabs_argreg(3, s.key_len as i64); a.movabs_argreg(4, s.cache_ptr as i64); a.load_argreg(5, disp(val)); a.movabs_rax(set_helper as i64);
a.call_rax();
throw_check!();
a.bind(done);
}
GenericOp::GetElem { dst, obj, index } => {
if !ok(dst) || !ok(obj) || !ok(index) {
return None;
}
let helper = a.new_label();
let done = a.new_label();
a.mov_rdi_r15();
a.movabs_rax(arena_helper as i64);
a.call_rax();
a.mov_r10_rax(); a.load_rax(disp(obj)); a.movabs_r11(HANDLE_TAG as i64);
a.mov_rcx_rax();
a.and_rcx_r11();
a.cmp_rcx_r11();
a.jne(helper); a.mov_r8_rax();
a.shr_r8_imm(32); a.movabs_r11(0xffff_ffff_i64);
a.and_rax_r11(); a.cmp_rax_rdx();
a.jae(helper); a.imul_rax_imm(layout.slot_stride);
a.add_rax_r10();
a.mov_rcx_rax(); a.cmp_byte_at_rcx_imm(0, layout.slot_occupied_disc);
a.jne(helper);
a.cmp_word_at_rcx_r8w(layout.off_slot_gen);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_cell_tag, layout.cell_array_disc);
a.jne(helper); a.load_rax(disp(index));
a.movabs_r11(QNAN as i64);
a.and_rax_r11();
a.cmp_rax_r11();
a.je(helper); a.movsd_xmm0_mem(disp(index)); a.cvttsd2si_rax_xmm0(); a.cvtsi2sd_xmm1_rax(); a.ucomisd_xmm0_xmm1();
a.jne(helper); a.jp(helper); a.mov_r8_at_rcx(layout.off_arr_len);
a.cmp_rax_r8();
a.jae(helper); a.mov_r11_at_rcx(layout.off_arr_ptr); a.mov_rax_at_r11_rax8(); a.movabs_r11(NanBox::hole().to_bits() as i64);
a.cmp_rax_r11();
a.je(helper);
a.store_rax(disp(dst));
a.jmp(done);
a.bind(helper);
a.mov_rdi_r15(); a.load_argreg(1, disp(obj)); a.load_argreg(2, disp(index)); a.movabs_rax(get_elem_helper as i64);
a.call_rax();
a.movabs_r11(NanBox::jit_throw_bits() as i64);
a.cmp_rax_r11();
a.je(throw_exit);
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::SetElem { obj, index, val } => {
if !ok(obj) || !ok(index) || !ok(val) {
return None;
}
let helper = a.new_label();
let done = a.new_label();
a.load_rax(disp(val));
a.movabs_r11(HANDLE_TAG as i64);
a.and_rax_r11();
a.cmp_rax_r11();
a.je(helper); a.mov_rdi_r15();
a.load_argreg(1, disp(obj)); a.movabs_rax(array_unrestricted_helper as i64);
a.call_rax();
a.test_rax_rax();
a.je(helper); a.mov_rdi_r15();
a.movabs_rax(arena_helper as i64);
a.call_rax();
a.mov_r10_rax(); a.load_rax(disp(obj)); a.movabs_r11(HANDLE_TAG as i64);
a.mov_rcx_rax();
a.and_rcx_r11();
a.cmp_rcx_r11();
a.jne(helper);
a.mov_r8_rax();
a.shr_r8_imm(32); a.movabs_r11(0xffff_ffff_i64);
a.and_rax_r11(); a.cmp_rax_rdx();
a.jae(helper);
a.imul_rax_imm(layout.slot_stride);
a.add_rax_r10();
a.mov_rcx_rax(); a.cmp_byte_at_rcx_imm(0, layout.slot_occupied_disc);
a.jne(helper);
a.cmp_word_at_rcx_r8w(layout.off_slot_gen);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_cell_tag, layout.cell_array_disc);
a.jne(helper); a.load_rax(disp(index));
a.movabs_r11(QNAN as i64);
a.and_rax_r11();
a.cmp_rax_r11();
a.je(helper);
a.movsd_xmm0_mem(disp(index));
a.cvttsd2si_rax_xmm0();
a.cvtsi2sd_xmm1_rax();
a.ucomisd_xmm0_xmm1();
a.jne(helper);
a.jp(helper);
a.mov_r8_at_rcx(layout.off_arr_len);
a.cmp_rax_r8();
a.jae(helper);
a.mov_r11_at_rcx(layout.off_arr_ptr); a.load_rdx(disp(val)); a.mov_at_r11_rax8_rdx(); a.jmp(done);
a.bind(helper);
a.mov_rdi_r15(); a.load_argreg(1, disp(obj)); a.load_argreg(2, disp(index)); a.load_argreg(3, disp(val)); a.movabs_rax(set_elem_helper as i64);
a.call_rax();
throw_check!();
a.bind(done);
}
GenericOp::ArrayLen { dst, arr } => {
if !ok(dst) || !ok(arr) {
return None;
}
let helper = a.new_label();
let done = a.new_label();
a.mov_rdi_r15();
a.load_argreg(1, disp(arr)); a.movabs_rax(array_len_dense_helper as i64);
a.call_rax();
a.test_rax_rax();
a.je(helper); a.mov_rdi_r15();
a.movabs_rax(arena_helper as i64);
a.call_rax();
a.mov_r10_rax(); a.load_rax(disp(arr)); a.movabs_r11(HANDLE_TAG as i64);
a.mov_rcx_rax();
a.and_rcx_r11();
a.cmp_rcx_r11();
a.jne(helper); a.mov_r8_rax();
a.shr_r8_imm(32); a.movabs_r11(0xffff_ffff_i64);
a.and_rax_r11(); a.cmp_rax_rdx();
a.jae(helper); a.imul_rax_imm(layout.slot_stride);
a.add_rax_r10();
a.mov_rcx_rax(); a.cmp_byte_at_rcx_imm(0, layout.slot_occupied_disc);
a.jne(helper);
a.cmp_word_at_rcx_r8w(layout.off_slot_gen);
a.jne(helper);
a.cmp_byte_at_rcx_imm(layout.off_cell_tag, layout.cell_array_disc);
a.jne(helper); a.mov_rax_at_rcx(layout.off_arr_len); a.cvtsi2sd_xmm0_rax(); a.movsd_mem_xmm0(disp(dst));
a.jmp(done);
a.bind(helper);
a.mov_rdi_r15(); a.load_argreg(1, disp(arr)); a.movabs_rax(array_len_helper as i64);
a.call_rax();
throw_check!();
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::Arith {
dst,
a: ra,
b: rb,
op,
} => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let fbin = match op {
crate::nbvm::GA_SUB => Some(FBinOp::Sub),
crate::nbvm::GA_MUL => Some(FBinOp::Mul),
crate::nbvm::GA_DIV => Some(FBinOp::Div),
_ => None, };
let done = a.new_label();
let slow = a.new_label();
if let Some(fb) = fbin {
let nan_fix = a.new_label();
a.movabs_r11(QNAN as i64);
a.load_rax(disp(ra));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.load_rax(disp(rb));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.movsd_xmm0_mem(disp(ra));
a.fbin_xmm0_mem(fb, disp(rb));
a.ucomisd_xmm0_xmm0();
a.jp(nan_fix);
a.movsd_mem_xmm0(disp(dst));
a.jmp(done);
a.bind(nan_fix);
a.movabs_rax(canonical_nan as i64);
a.store_rax(disp(dst));
a.jmp(done);
}
a.bind(slow);
a.mov_rdi_r15();
a.load_argreg(1, disp(ra)); a.load_argreg(2, disp(rb)); a.movabs_argreg(3, i64::from(op)); a.movabs_rax(arith_helper as i64);
a.call_rax();
throw_check!();
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::Lt { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let slow = a.new_label();
let done = a.new_label();
a.movabs_r11(QNAN as i64);
a.load_rax(disp(ra));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.load_rax(disp(rb));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.movsd_xmm0_mem(disp(rb));
a.ucomisd_xmm0_mem(disp(ra));
a.seta_rax();
a.movabs_r11(tag_false as i64);
a.add_rax_r11(); a.store_rax(disp(dst));
a.jmp(done);
a.bind(slow);
a.mov_rdi_r15();
a.load_argreg(1, disp(ra));
a.load_argreg(2, disp(rb));
a.movabs_rax(lt_helper as i64);
a.call_rax();
throw_check!();
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::StrictEq { dst, a: ra, b: rb } => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let slow = a.new_label();
let done = a.new_label();
a.movabs_r11(QNAN as i64);
a.load_rax(disp(ra));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.load_rax(disp(rb));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.movsd_xmm0_mem(disp(ra));
a.ucomisd_xmm0_mem(disp(rb));
a.ordered_equal_rax();
a.movabs_r11(tag_false as i64);
a.add_rax_r11();
a.store_rax(disp(dst));
a.jmp(done);
a.bind(slow);
a.mov_rdi_r15();
a.load_argreg(1, disp(ra));
a.load_argreg(2, disp(rb));
a.movabs_rax(strict_eq_helper as i64);
a.call_rax();
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::ValueBin {
dst,
a: ra,
b: rb,
op,
} => {
if !ok(dst) || !ok(ra) || !ok(rb) {
return None;
}
let is_loose =
op == crate::nbvm::VB_LOOSE_EQ || op == crate::nbvm::VB_LOOSE_NEQ;
let done = a.new_label();
let slow = a.new_label();
if is_loose {
a.movabs_r11(QNAN as i64);
a.load_rax(disp(ra));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.load_rax(disp(rb));
a.and_rax_r11();
a.cmp_rax_r11();
a.je(slow);
a.movsd_xmm0_mem(disp(ra));
a.ucomisd_xmm0_mem(disp(rb));
a.ordered_equal_rax(); if op == crate::nbvm::VB_LOOSE_NEQ {
a.xor_rax_imm(1); }
a.movabs_r11(tag_false as i64);
a.add_rax_r11();
a.store_rax(disp(dst));
a.jmp(done);
}
a.bind(slow);
a.mov_rdi_r15();
a.load_argreg(1, disp(ra));
a.load_argreg(2, disp(rb));
a.movabs_argreg(3, i64::from(op)); a.movabs_rax(value_bin_helper as i64);
a.call_rax();
throw_check!();
a.store_rax(disp(dst));
a.bind(done);
}
GenericOp::Not { dst, a: ra } => {
if !ok(dst) || !ok(ra) {
return None;
}
a.emit_truthy(disp(ra), truthy_helper, QNAN, tag_true, tag_false);
a.xor_rax_imm(1); a.movabs_r11(tag_false as i64);
a.add_rax_r11(); a.store_rax(disp(dst));
}
GenericOp::Jump { target } => {
if target >= ops.len() {
return None;
}
a.jmp(labels[target]);
}
GenericOp::JumpIfFalse { cond, target } => {
if !ok(cond) || target >= ops.len() {
return None;
}
a.emit_truthy(disp(cond), truthy_helper, QNAN, tag_true, tag_false);
a.test_rax_rax();
a.je(labels[target]); }
GenericOp::Call { dst, func, site } => {
if !ok(dst) {
return None;
}
let args = call_args.get(site as usize)?;
for (i, &ar) in args.iter().enumerate() {
if !ok(ar) {
return None;
}
a.load_rax(disp(ar));
a.store_rax(argbuf_disp(i));
}
match generic_registry.get(&func) {
Some(&(addr, callee_n_params)) if args.len() >= callee_n_params => {
a.mov_rdi_r15(); a.lea_argreg(1, argbuf_disp(0)); a.movabs_argreg(2, args.len() as i64); a.movabs_rax(addr as i64);
a.call_rax();
}
_ => {
a.mov_rdi_r15(); a.movabs_argreg(1, i64::from(func)); a.movabs_argreg(2, NanBox::undefined().to_bits() as i64); a.lea_argreg(3, argbuf_disp(0)); a.movabs_argreg(4, args.len() as i64); a.movabs_rax(call_helper as i64);
a.call_rax();
}
}
throw_check!();
a.store_rax(disp(dst));
}
GenericOp::CallNative { dst, native, site } => {
if !ok(dst) {
return None;
}
let args = call_args.get(site as usize)?;
for (i, &ar) in args.iter().enumerate() {
if !ok(ar) {
return None;
}
a.load_rax(disp(ar));
a.store_rax(argbuf_disp(i));
}
a.mov_rdi_r15(); a.movabs_argreg(1, i64::from(native)); a.lea_argreg(2, argbuf_disp(0)); a.movabs_argreg(3, args.len() as i64); a.movabs_rax(call_native_helper as i64);
a.call_rax();
throw_check!();
a.store_rax(disp(dst));
}
GenericOp::Ret { src } => {
if !ok(src) {
return None;
}
a.load_rax(disp(src));
a.generic_epilogue();
has_ret = true;
}
}
}
if !has_ret {
return None;
}
a.bind(throw_exit);
a.movabs_rax(NanBox::jit_throw_bits() as i64);
a.generic_epilogue();
Self::from_code(&a.finish())
}
#[must_use]
pub fn from_machine_code(code: &[u8]) -> Option<Self> {
Self::from_code(code)
}
#[cfg(all(feature = "alloc", target_os = "linux", target_arch = "x86_64"))]
#[must_use]
pub fn call_generic(&self, ctx: *mut core::ffi::c_void, args: *const u64, n: usize) -> u64 {
#[allow(unsafe_code)]
let f: extern "C" fn(*mut core::ffi::c_void, *const u64, usize) -> u64 =
unsafe { core::mem::transmute(self.buf.ptr()) };
f(ctx, args, n)
}
#[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);
}
}
}