#![forbid(unsafe_code)]
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
enum Instruction {
NumberLiteral(f64),
IntLiteral(i64),
BoolLiteral(bool),
Add,
Sub,
Mul,
Div,
Idiv,
Mod,
Neg,
Abs,
Ceiling,
Floor,
Round,
Truncate,
Sqrt,
Exp,
Ln,
Log,
Sin,
Cos,
Atan,
Eq,
Ne,
Gt,
Ge,
Lt,
Le,
And,
Or,
Xor,
Not,
Bitshift,
Cvi,
Cvr,
Dup,
Exch,
Pop,
Copy,
Index,
Roll,
ProcedureBody(Vec<Instruction>),
If(Vec<Instruction>),
IfElse(Vec<Instruction>, Vec<Instruction>),
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Value {
Int(i64),
Real(f64),
Bool(bool),
}
impl Value {
fn as_real(self) -> Result<f64> {
match self {
Value::Int(i) => Ok(i as f64),
Value::Real(r) => Ok(r),
Value::Bool(_) => Err(typecheck("expected numeric, got boolean")),
}
}
fn as_int(self) -> Result<i64> {
match self {
Value::Int(i) => Ok(i),
Value::Real(_) => Err(typecheck("expected integer, got real")),
Value::Bool(_) => Err(typecheck("expected integer, got boolean")),
}
}
fn as_bool(self) -> Result<bool> {
match self {
Value::Bool(b) => Ok(b),
_ => Err(typecheck("expected boolean")),
}
}
fn to_output(self) -> f64 {
match self {
Value::Int(i) => i as f64,
Value::Real(r) => r,
Value::Bool(b) => {
if b {
1.0
} else {
0.0
}
},
}
}
}
pub const MAX_PARSE_DEPTH: u32 = 32;
pub const MAX_STACK: usize = 256;
pub const MAX_INSTRUCTIONS: usize = 100_000;
fn parse(program: &[u8]) -> Result<Vec<Instruction>> {
let s = std::str::from_utf8(program)
.map_err(|e| Error::InvalidPdf(format!("Type 4 function is not valid UTF-8: {e}")))?;
let s = s.trim();
if !s.starts_with('{') || !s.ends_with('}') {
return Err(Error::InvalidPdf("Type 4 function must be enclosed in { }".into()));
}
let inner = &s[1..s.len() - 1];
parse_body(inner, 1)
}
fn parse_body(s: &str, depth: u32) -> Result<Vec<Instruction>> {
if depth > MAX_PARSE_DEPTH {
return Err(Error::InvalidPdf(format!(
"Type 4 parse depth limit exceeded (max {MAX_PARSE_DEPTH})"
)));
}
let mut instructions = Vec::new();
let mut chars = s.char_indices().peekable();
while let Some(&(i, c)) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
if c == '{' {
chars.next();
let start = if let Some(&(idx, _)) = chars.peek() {
idx
} else {
return Err(Error::InvalidPdf("Unclosed brace in Type 4 function".into()));
};
let mut brace_depth = 1u32;
let mut end = start;
for (j, ch) in chars.by_ref() {
if ch == '{' {
brace_depth += 1;
} else if ch == '}' {
brace_depth -= 1;
if brace_depth == 0 {
end = j;
break;
}
}
}
if brace_depth != 0 {
return Err(Error::InvalidPdf("Unclosed brace in Type 4 function".into()));
}
let body = parse_body(&s[start..end], depth + 1)?;
instructions.push(Instruction::ProcedureBody(body));
continue;
}
let start = i;
while let Some(&(_, tc)) = chars.peek() {
if tc.is_whitespace() || tc == '{' || tc == '}' {
break;
}
chars.next();
}
let end = if let Some(&(idx, _)) = chars.peek() {
idx
} else {
s.len()
};
let token = &s[start..end];
instructions.push(parse_token(token)?);
}
resolve_conditionals(&mut instructions)?;
Ok(instructions)
}
fn parse_token(token: &str) -> Result<Instruction> {
match token {
"add" => Ok(Instruction::Add),
"sub" => Ok(Instruction::Sub),
"mul" => Ok(Instruction::Mul),
"div" => Ok(Instruction::Div),
"idiv" => Ok(Instruction::Idiv),
"mod" => Ok(Instruction::Mod),
"neg" => Ok(Instruction::Neg),
"abs" => Ok(Instruction::Abs),
"ceiling" => Ok(Instruction::Ceiling),
"floor" => Ok(Instruction::Floor),
"round" => Ok(Instruction::Round),
"truncate" => Ok(Instruction::Truncate),
"sqrt" => Ok(Instruction::Sqrt),
"exp" => Ok(Instruction::Exp),
"ln" => Ok(Instruction::Ln),
"log" => Ok(Instruction::Log),
"sin" => Ok(Instruction::Sin),
"cos" => Ok(Instruction::Cos),
"atan" => Ok(Instruction::Atan),
"eq" => Ok(Instruction::Eq),
"ne" => Ok(Instruction::Ne),
"gt" => Ok(Instruction::Gt),
"ge" => Ok(Instruction::Ge),
"lt" => Ok(Instruction::Lt),
"le" => Ok(Instruction::Le),
"and" => Ok(Instruction::And),
"or" => Ok(Instruction::Or),
"xor" => Ok(Instruction::Xor),
"not" => Ok(Instruction::Not),
"bitshift" => Ok(Instruction::Bitshift),
"cvi" => Ok(Instruction::Cvi),
"cvr" => Ok(Instruction::Cvr),
"true" => Ok(Instruction::BoolLiteral(true)),
"false" => Ok(Instruction::BoolLiteral(false)),
"dup" => Ok(Instruction::Dup),
"exch" => Ok(Instruction::Exch),
"pop" => Ok(Instruction::Pop),
"copy" => Ok(Instruction::Copy),
"index" => Ok(Instruction::Index),
"roll" => Ok(Instruction::Roll),
"if" | "ifelse" => Ok(if token == "if" {
Instruction::If(vec![])
} else {
Instruction::IfElse(vec![], vec![])
}),
_ => parse_numeric_literal(token),
}
}
fn parse_numeric_literal(token: &str) -> Result<Instruction> {
if let Ok(i) = token.parse::<i64>() {
return Ok(Instruction::IntLiteral(i));
}
let val: f64 = token
.parse()
.map_err(|_| Error::InvalidPdf(format!("Unknown Type 4 token: {token}")))?;
if !val.is_finite() {
return Err(Error::InvalidPdf(format!(
"Type 4 numeric literal must be finite, got: {token}"
)));
}
Ok(Instruction::NumberLiteral(val))
}
fn resolve_conditionals(instructions: &mut Vec<Instruction>) -> Result<()> {
let mut i = 0;
while i < instructions.len() {
match &instructions[i] {
Instruction::If(body) if body.is_empty() => {
if i == 0 {
return Err(Error::InvalidPdf(
"Type 4 `if` without preceding procedure body".into(),
));
}
match instructions.remove(i - 1) {
Instruction::ProcedureBody(body) => {
instructions[i - 1] = Instruction::If(body);
},
_ => {
return Err(Error::InvalidPdf(
"Type 4 `if` requires a procedure body".into(),
));
},
}
},
Instruction::IfElse(true_b, false_b) if true_b.is_empty() && false_b.is_empty() => {
if i < 2 {
return Err(Error::InvalidPdf(
"Type 4 `ifelse` without two preceding procedure bodies".into(),
));
}
let false_branch = match instructions.remove(i - 1) {
Instruction::ProcedureBody(body) => body,
_ => {
return Err(Error::InvalidPdf(
"Type 4 `ifelse` requires two procedure bodies".into(),
))
},
};
let true_branch = match instructions.remove(i - 2) {
Instruction::ProcedureBody(body) => body,
_ => {
return Err(Error::InvalidPdf(
"Type 4 `ifelse` requires two procedure bodies".into(),
))
},
};
instructions[i - 2] = Instruction::IfElse(true_branch, false_branch);
i = i.saturating_sub(1);
},
_ => {
i += 1;
},
}
}
if instructions
.iter()
.any(|ins| matches!(ins, Instruction::ProcedureBody(_)))
{
return Err(Error::InvalidPdf(
"Type 4 orphan procedure body: { ... } not consumed by if/ifelse".into(),
));
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct Program {
instructions: Vec<Instruction>,
}
impl Program {
pub fn compile(bytes: &[u8]) -> Result<Self> {
Ok(Self {
instructions: parse(bytes)?,
})
}
pub fn evaluate(&self, inputs: &[f64]) -> Result<Vec<f64>> {
if inputs.len() > MAX_STACK {
return Err(Error::Type4Runtime(format!(
"Type 4 stack overflow: {} inputs exceeds max {MAX_STACK}",
inputs.len()
)));
}
const I64_MAX_PLUS_ONE_AS_F64: f64 = 9_223_372_036_854_775_808.0;
let mut stack: Vec<Value> = inputs
.iter()
.map(|&v| {
if v.is_finite()
&& v.fract() == 0.0
&& v >= i64::MIN as f64
&& v < I64_MAX_PLUS_ONE_AS_F64
{
Value::Int(v as i64)
} else {
Value::Real(v)
}
})
.collect();
let mut budget = MAX_INSTRUCTIONS;
execute(&self.instructions, &mut stack, &mut budget)?;
Ok(stack.into_iter().map(Value::to_output).collect())
}
pub fn evaluate_clamped(
&self,
inputs: &[f64],
domain: &[[f64; 2]],
range: &[[f64; 2]],
) -> Result<Vec<f64>> {
let clamped_inputs: Vec<f64> = inputs
.iter()
.enumerate()
.map(|(i, &v)| {
if let Some(&[lo, hi]) = domain.get(i) {
safe_clamp(v, lo, hi)
} else {
v
}
})
.collect();
let mut result = self.evaluate(&clamped_inputs)?;
for (i, val) in result.iter_mut().enumerate() {
if let Some(&[lo, hi]) = range.get(i) {
*val = safe_clamp(*val, lo, hi);
}
}
Ok(result)
}
}
pub fn evaluate_type4(program: &[u8], inputs: &[f64]) -> Result<Vec<f64>> {
Program::compile(program)?.evaluate(inputs)
}
pub fn evaluate_type4_clamped(
program: &[u8],
inputs: &[f64],
domain: &[[f64; 2]],
range: &[[f64; 2]],
) -> Result<Vec<f64>> {
Program::compile(program)?.evaluate_clamped(inputs, domain, range)
}
fn safe_clamp(v: f64, lo: f64, hi: f64) -> f64 {
if lo.is_nan() || hi.is_nan() {
return v;
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
v.clamp(lo, hi)
}
fn push_checked(stack: &mut Vec<Value>, v: Value) -> Result<()> {
if stack.len() >= MAX_STACK {
return Err(Error::Type4Runtime(format!("Type 4 stack overflow (max {MAX_STACK})")));
}
stack.push(v);
Ok(())
}
fn execute(instructions: &[Instruction], stack: &mut Vec<Value>, budget: &mut usize) -> Result<()> {
for instr in instructions {
if *budget == 0 {
return Err(Error::Type4Runtime(format!(
"Type 4 instruction budget exceeded (max {MAX_INSTRUCTIONS})"
)));
}
*budget -= 1;
match instr {
Instruction::NumberLiteral(v) => push_checked(stack, Value::Real(*v))?,
Instruction::IntLiteral(i) => push_checked(stack, Value::Int(*i))?,
Instruction::BoolLiteral(b) => push_checked(stack, Value::Bool(*b))?,
Instruction::Add => numeric_binary(stack, |a, b| Ok(a + b))?,
Instruction::Sub => numeric_binary(stack, |a, b| Ok(a - b))?,
Instruction::Mul => numeric_binary(stack, |a, b| Ok(a * b))?,
Instruction::Div => numeric_binary(stack, |a, b| Ok(a / b))?,
Instruction::Idiv => {
let b = pop(stack)?.as_int()?;
let a = pop(stack)?.as_int()?;
if b == 0 {
return Err(Error::Type4Runtime("Type 4 idiv by zero".into()));
}
let q = a
.checked_div(b)
.ok_or_else(|| Error::Type4Runtime("Type 4 idiv integer overflow".into()))?;
stack.push(Value::Int(q));
},
Instruction::Mod => {
let b = pop(stack)?.as_int()?;
let a = pop(stack)?.as_int()?;
if b == 0 {
return Err(Error::Type4Runtime("Type 4 mod by zero".into()));
}
let r = a
.checked_rem(b)
.ok_or_else(|| Error::Type4Runtime("Type 4 mod integer overflow".into()))?;
stack.push(Value::Int(r));
},
Instruction::Neg => {
let v = pop(stack)?;
match v {
Value::Int(i) => {
let n = i.checked_neg().ok_or_else(|| {
Error::Type4Runtime("Type 4 integer overflow in neg".into())
})?;
stack.push(Value::Int(n));
},
Value::Real(r) => stack.push(Value::Real(-r)),
Value::Bool(_) => return Err(typecheck("neg expects a number")),
}
},
Instruction::Abs => {
let v = pop(stack)?;
match v {
Value::Int(i) => {
let n = i.checked_abs().ok_or_else(|| {
Error::Type4Runtime("Type 4 integer overflow in abs".into())
})?;
stack.push(Value::Int(n));
},
Value::Real(r) => stack.push(Value::Real(r.abs())),
Value::Bool(_) => return Err(typecheck("abs expects a number")),
}
},
Instruction::Ceiling => real_unary_preserve(stack, |a| Ok(a.ceil()))?,
Instruction::Floor => real_unary_preserve(stack, |a| Ok(a.floor()))?,
Instruction::Round => real_unary_preserve(stack, |a| Ok((a + 0.5).floor()))?,
Instruction::Truncate => real_unary_preserve(stack, |a| Ok(a.trunc()))?,
Instruction::Sqrt => real_unary(stack, |a| {
if a < 0.0 || a.is_nan() {
Err(Error::Type4Runtime("Type 4 sqrt of negative".into()))
} else {
Ok(a.sqrt())
}
})?,
Instruction::Exp => numeric_binary(stack, |base, exp| Ok(base.powf(exp)))?,
Instruction::Ln => real_unary(stack, |a| {
if a <= 0.0 || a.is_nan() {
Err(Error::Type4Runtime("Type 4 ln of non-positive".into()))
} else {
Ok(a.ln())
}
})?,
Instruction::Log => real_unary(stack, |a| {
if a <= 0.0 || a.is_nan() {
Err(Error::Type4Runtime("Type 4 log of non-positive".into()))
} else {
Ok(a.log10())
}
})?,
Instruction::Sin => real_unary(stack, |a| Ok(a.to_radians().sin()))?,
Instruction::Cos => real_unary(stack, |a| Ok(a.to_radians().cos()))?,
Instruction::Atan => {
let den = pop(stack)?.as_real()?;
let num = pop(stack)?.as_real()?;
if num == 0.0 && den == 0.0 {
return Err(Error::Type4Runtime("Type 4 atan undefined for (0, 0)".into()));
}
let mut deg = num.atan2(den).to_degrees();
if deg < 0.0 {
deg += 360.0;
}
if deg >= 360.0 {
deg -= 360.0;
}
stack.push(Value::Real(deg));
},
Instruction::Eq => {
let b = pop(stack)?;
let a = pop(stack)?;
stack.push(Value::Bool(values_equal(a, b)));
},
Instruction::Ne => {
let b = pop(stack)?;
let a = pop(stack)?;
stack.push(Value::Bool(!values_equal(a, b)));
},
Instruction::Gt => comparison(stack, |o| o == std::cmp::Ordering::Greater)?,
Instruction::Ge => comparison(stack, |o| o != std::cmp::Ordering::Less)?,
Instruction::Lt => comparison(stack, |o| o == std::cmp::Ordering::Less)?,
Instruction::Le => comparison(stack, |o| o != std::cmp::Ordering::Greater)?,
Instruction::And => bool_or_bitwise(stack, |a, b| a && b, |a, b| a & b)?,
Instruction::Or => bool_or_bitwise(stack, |a, b| a || b, |a, b| a | b)?,
Instruction::Xor => bool_or_bitwise(stack, |a, b| a != b, |a, b| a ^ b)?,
Instruction::Not => {
let v = pop(stack)?;
match v {
Value::Bool(b) => stack.push(Value::Bool(!b)),
Value::Int(i) => stack.push(Value::Int(!i)),
Value::Real(_) => {
return Err(typecheck("not expects boolean or integer"));
},
}
},
Instruction::Bitshift => {
let shift = pop(stack)?.as_int()?;
let val = pop(stack)?.as_int()?;
let result = if shift >= 64 || shift <= -64 {
0
} else if shift >= 0 {
val.wrapping_shl(shift as u32)
} else {
((val as u64) >> (-shift) as u32) as i64
};
stack.push(Value::Int(result));
},
Instruction::Cvi => {
let v = pop(stack)?;
match v {
Value::Int(i) => stack.push(Value::Int(i)),
Value::Real(r) => {
if !r.is_finite() {
return Err(Error::Type4Runtime(
"Type 4 cvi: input is not finite".into(),
));
}
let t = r.trunc();
const I64_MAX_PLUS_ONE_AS_F64: f64 = 9_223_372_036_854_775_808.0;
if t < i64::MIN as f64 || t >= I64_MAX_PLUS_ONE_AS_F64 {
return Err(Error::Type4Runtime("Type 4 cvi: integer overflow".into()));
}
stack.push(Value::Int(t as i64));
},
Value::Bool(_) => return Err(typecheck("cvi expects a number")),
}
},
Instruction::Cvr => {
let v = pop(stack)?;
match v {
Value::Int(i) => stack.push(Value::Real(i as f64)),
Value::Real(r) => stack.push(Value::Real(r)),
Value::Bool(_) => return Err(typecheck("cvr expects a number")),
}
},
Instruction::Dup => {
let a = *stack.last().ok_or_else(underflow)?;
push_checked(stack, a)?;
},
Instruction::Exch => {
let b = pop(stack)?;
let a = pop(stack)?;
stack.push(b);
stack.push(a);
},
Instruction::Pop => {
pop(stack)?;
},
Instruction::Copy => {
let n = pop_count(stack, "copy")?;
if n > stack.len() {
return Err(underflow());
}
if stack.len().checked_add(n).is_none_or(|new| new > MAX_STACK) {
return Err(Error::Type4Runtime(format!(
"Type 4 stack overflow during copy (max {MAX_STACK})"
)));
}
let start = stack.len() - n;
let copied: Vec<Value> = stack[start..].to_vec();
stack.extend_from_slice(&copied);
},
Instruction::Index => {
let n = pop_count(stack, "index")?;
if n >= stack.len() {
return Err(underflow());
}
let val = stack[stack.len() - 1 - n];
stack.push(val);
},
Instruction::Roll => {
let j = pop(stack)?.as_int()?;
let n = pop_count(stack, "roll")?;
if n > stack.len() {
return Err(underflow());
}
if n > 0 {
let start = stack.len() - n;
let slice = &mut stack[start..];
let len = slice.len() as i64;
let shift = j.rem_euclid(len) as usize;
slice.rotate_right(shift);
}
},
Instruction::If(body) => {
let cond = pop(stack)?.as_bool()?;
if cond {
execute(body, stack, budget)?;
}
},
Instruction::IfElse(true_branch, false_branch) => {
let cond = pop(stack)?.as_bool()?;
if cond {
execute(true_branch, stack, budget)?;
} else {
execute(false_branch, stack, budget)?;
}
},
Instruction::ProcedureBody(_) => {
return Err(Error::Type4Runtime(
"Type 4 internal error: ProcedureBody reached execute".into(),
));
},
}
}
Ok(())
}
fn pop(stack: &mut Vec<Value>) -> Result<Value> {
stack.pop().ok_or_else(underflow)
}
fn pop_count(stack: &mut Vec<Value>, op: &str) -> Result<usize> {
let v = pop(stack)?.as_int()?;
if v < 0 {
return Err(Error::Type4Runtime(format!("Type 4 {op}: negative count {v}")));
}
Ok(v as usize)
}
fn underflow() -> Error {
Error::Type4Runtime("Type 4 stack underflow".into())
}
fn typecheck(msg: &str) -> Error {
Error::Type4Runtime(format!("Type 4 typecheck: {msg}"))
}
fn real_unary(stack: &mut Vec<Value>, f: impl FnOnce(f64) -> Result<f64>) -> Result<()> {
let a = pop(stack)?.as_real()?;
stack.push(Value::Real(f(a)?));
Ok(())
}
fn real_unary_preserve(stack: &mut Vec<Value>, f: impl FnOnce(f64) -> Result<f64>) -> Result<()> {
let v = pop(stack)?;
match v {
Value::Int(i) => stack.push(Value::Int(i)),
Value::Real(r) => stack.push(Value::Real(f(r)?)),
Value::Bool(_) => return Err(typecheck("expected number, got boolean")),
}
Ok(())
}
fn numeric_binary(stack: &mut Vec<Value>, f: impl FnOnce(f64, f64) -> Result<f64>) -> Result<()> {
let b = pop(stack)?;
let a = pop(stack)?;
let af = a.as_real()?;
let bf = b.as_real()?;
let result = f(af, bf)?;
if matches!(a, Value::Int(_))
&& matches!(b, Value::Int(_))
&& result.is_finite()
&& result.fract() == 0.0
&& result >= i64::MIN as f64
&& result <= i64::MAX as f64
{
stack.push(Value::Int(result as i64));
} else {
stack.push(Value::Real(result));
}
Ok(())
}
fn comparison(stack: &mut Vec<Value>, pred: impl FnOnce(std::cmp::Ordering) -> bool) -> Result<()> {
let b = pop(stack)?.as_real()?;
let a = pop(stack)?.as_real()?;
let ord = a
.partial_cmp(&b)
.ok_or_else(|| Error::Type4Runtime("Type 4 comparison with NaN".into()))?;
stack.push(Value::Bool(pred(ord)));
Ok(())
}
fn values_equal(a: Value, b: Value) -> bool {
match (a, b) {
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Bool(_), _) | (_, Value::Bool(_)) => false,
_ => a.as_real().ok() == b.as_real().ok(),
}
}
fn bool_or_bitwise(
stack: &mut Vec<Value>,
boolean: impl FnOnce(bool, bool) -> bool,
bitwise: impl FnOnce(i64, i64) -> i64,
) -> Result<()> {
let b = pop(stack)?;
let a = pop(stack)?;
match (a, b) {
(Value::Bool(x), Value::Bool(y)) => stack.push(Value::Bool(boolean(x, y))),
(Value::Int(x), Value::Int(y)) => stack.push(Value::Int(bitwise(x, y))),
_ => return Err(typecheck("and/or/xor require matching boolean or integer operands")),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: &[f64], b: &[f64], eps: f64) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < eps)
}
#[test]
fn linear_ramp_tint_transform() {
let prog = b"{ dup 0.84 mul exch 0.00 exch dup 0.44 mul exch 0.21 mul }";
let result = evaluate_type4(prog, &[0.5]).unwrap();
assert!(approx_eq(&result, &[0.42, 0.0, 0.22, 0.105], 1e-9), "got {result:?}");
}
#[test]
fn identity_empty_program() {
let result = evaluate_type4(b"{ }", &[0.7]).unwrap();
assert!(approx_eq(&result, &[0.7], 1e-9), "got {result:?}");
}
#[test]
fn constant_output() {
let prog = b"{ pop 1.0 0.0 0.0 0.0 }";
let result = evaluate_type4(prog, &[0.5]).unwrap();
assert_eq!(result, vec![1.0, 0.0, 0.0, 0.0]);
}
#[test]
fn conditional_ifelse() {
let prog = b"{ dup 0.5 gt { pop 1.0 } { 0.0 exch } ifelse }";
let high = evaluate_type4(prog, &[0.8]).unwrap();
assert_eq!(high, vec![1.0]);
let low = evaluate_type4(prog, &[0.3]).unwrap();
assert!(approx_eq(&low, &[0.0, 0.3], 1e-9), "got {low:?}");
}
#[test]
fn conditional_if() {
let prog = b"{ dup 0.5 gt { 1.0 add } if }";
let high = evaluate_type4(prog, &[0.8]).unwrap();
assert!(approx_eq(&high, &[1.8], 1e-9), "got {high:?}");
let low = evaluate_type4(prog, &[0.3]).unwrap();
assert!(approx_eq(&low, &[0.3], 1e-9), "got {low:?}");
}
#[test]
fn domain_range_clamping() {
let prog = b"{ 2.0 mul }";
let result = evaluate_type4_clamped(prog, &[1.5], &[[0.0, 1.0]], &[[0.0, 1.0]]).unwrap();
assert_eq!(result, vec![1.0]);
}
#[test]
fn stack_underflow_returns_error() {
let prog = b"{ add }";
let err = evaluate_type4(prog, &[]).unwrap_err();
assert!(err.to_string().contains("stack underflow"), "got: {err}");
}
#[test]
fn arithmetic_operators() {
assert_eq!(evaluate_type4(b"{ add }", &[3.0, 4.0]).unwrap(), vec![7.0]);
assert_eq!(evaluate_type4(b"{ sub }", &[10.0, 3.0]).unwrap(), vec![7.0]);
assert_eq!(evaluate_type4(b"{ mul }", &[3.0, 4.0]).unwrap(), vec![12.0]);
assert_eq!(evaluate_type4(b"{ div }", &[10.0, 4.0]).unwrap(), vec![2.5]);
assert_eq!(evaluate_type4(b"{ idiv }", &[10.0, 3.0]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ mod }", &[10.0, 3.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ neg }", &[5.0]).unwrap(), vec![-5.0]);
assert_eq!(evaluate_type4(b"{ abs }", &[-5.0]).unwrap(), vec![5.0]);
assert_eq!(evaluate_type4(b"{ ceiling }", &[3.2]).unwrap(), vec![4.0]);
assert_eq!(evaluate_type4(b"{ floor }", &[3.8]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ round }", &[3.5]).unwrap(), vec![4.0]);
assert_eq!(evaluate_type4(b"{ truncate }", &[3.9]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ sqrt }", &[9.0]).unwrap(), vec![3.0]);
}
#[test]
fn trig_operators() {
let sin_result = evaluate_type4(b"{ sin }", &[90.0]).unwrap();
assert!((sin_result[0] - 1.0).abs() < 1e-9);
let cos_result = evaluate_type4(b"{ cos }", &[0.0]).unwrap();
assert!((cos_result[0] - 1.0).abs() < 1e-9);
let atan_result = evaluate_type4(b"{ atan }", &[1.0, 1.0]).unwrap();
assert!((atan_result[0] - 45.0).abs() < 1e-9);
}
#[test]
fn log_operators() {
let ln_result = evaluate_type4(b"{ ln }", &[std::f64::consts::E]).unwrap();
assert!((ln_result[0] - 1.0).abs() < 1e-9);
let log_result = evaluate_type4(b"{ log }", &[100.0]).unwrap();
assert!((log_result[0] - 2.0).abs() < 1e-9);
}
#[test]
fn exp_operator() {
let result = evaluate_type4(b"{ exp }", &[2.0, 10.0]).unwrap();
assert!((result[0] - 1024.0).abs() < 1e-9);
}
#[test]
fn comparison_operators() {
assert_eq!(evaluate_type4(b"{ eq }", &[1.0, 1.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ eq }", &[1.0, 2.0]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ ne }", &[1.0, 2.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ gt }", &[2.0, 1.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ ge }", &[2.0, 2.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ lt }", &[1.0, 2.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ le }", &[2.0, 2.0]).unwrap(), vec![1.0]);
}
#[test]
fn boolean_operators() {
assert_eq!(evaluate_type4(b"{ true false and }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ true false or }", &[]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ true true xor }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ true not }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ false not }", &[]).unwrap(), vec![1.0]);
}
#[test]
fn bitwise_operators() {
assert_eq!(evaluate_type4(b"{ 12 10 and }", &[]).unwrap(), vec![8.0]);
assert_eq!(evaluate_type4(b"{ 12 10 or }", &[]).unwrap(), vec![14.0]);
assert_eq!(evaluate_type4(b"{ 8 2 bitshift }", &[]).unwrap(), vec![32.0]);
assert_eq!(evaluate_type4(b"{ 32 -2 bitshift }", &[]).unwrap(), vec![8.0]);
}
#[test]
fn stack_manipulation() {
assert_eq!(evaluate_type4(b"{ dup }", &[5.0]).unwrap(), vec![5.0, 5.0]);
assert_eq!(evaluate_type4(b"{ exch }", &[1.0, 2.0]).unwrap(), vec![2.0, 1.0]);
assert_eq!(evaluate_type4(b"{ pop }", &[1.0, 2.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ 2 copy }", &[1.0, 2.0]).unwrap(), vec![1.0, 2.0, 1.0, 2.0]);
assert_eq!(evaluate_type4(b"{ 1 index }", &[1.0, 2.0]).unwrap(), vec![1.0, 2.0, 1.0]);
}
#[test]
fn roll_operator() {
assert_eq!(evaluate_type4(b"{ 3 1 roll }", &[1.0, 2.0, 3.0]).unwrap(), vec![3.0, 1.0, 2.0]);
assert_eq!(
evaluate_type4(b"{ 3 -1 roll }", &[1.0, 2.0, 3.0]).unwrap(),
vec![2.0, 3.0, 1.0]
);
}
#[test]
fn bool_literals() {
assert_eq!(evaluate_type4(b"{ true }", &[]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ false }", &[]).unwrap(), vec![0.0]);
}
#[test]
fn division_by_zero_follows_ieee_754() {
let pos = evaluate_type4(b"{ div }", &[1.0, 0.0]).unwrap();
assert_eq!(pos.len(), 1);
assert!(pos[0].is_infinite() && pos[0] > 0.0, "expected +inf, got {pos:?}");
let neg = evaluate_type4(b"{ div }", &[-1.0, 0.0]).unwrap();
assert_eq!(neg.len(), 1);
assert!(neg[0].is_infinite() && neg[0] < 0.0, "expected -inf, got {neg:?}");
let nan = evaluate_type4(b"{ div }", &[0.0, 0.0]).unwrap();
assert_eq!(nan.len(), 1);
assert!(nan[0].is_nan(), "expected NaN, got {nan:?}");
assert!(evaluate_type4(b"{ idiv }", &[1.0, 0.0]).is_err());
assert!(evaluate_type4(b"{ mod }", &[1.0, 0.0]).is_err());
}
#[test]
fn int_min_neg_and_abs_error() {
let neg = format!("{{ {} neg }}", i64::MIN);
let err = evaluate_type4(neg.as_bytes(), &[]).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
let abs = format!("{{ {} abs }}", i64::MIN);
let err = evaluate_type4(abs.as_bytes(), &[]).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
}
#[test]
fn invalid_program_missing_braces() {
let err = evaluate_type4(b"dup mul", &[1.0]).unwrap_err();
assert!(err.to_string().contains("{ }"), "got: {err}");
}
#[test]
fn nested_conditionals() {
let prog =
b"{ dup 0.5 gt { dup 0.8 gt { pop 1.0 } { pop 0.75 } ifelse } { pop 0.0 } ifelse }";
assert_eq!(evaluate_type4(prog, &[0.9]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(prog, &[0.6]).unwrap(), vec![0.75]);
assert_eq!(evaluate_type4(prog, &[0.3]).unwrap(), vec![0.0]);
}
#[test]
fn real_world_spot_color_transforms() {
let prog = b"{ 0 exch dup 0.78 mul exch 0.35 mul 0 }";
let result = evaluate_type4(prog, &[1.0]).unwrap();
assert!(approx_eq(&result, &[0.0, 0.78, 0.35, 0.0], 1e-9), "got {result:?}");
}
#[test]
fn negative_number_literal() {
let result = evaluate_type4(b"{ -3.5 add }", &[10.0]).unwrap();
assert!(approx_eq(&result, &[6.5], 1e-9), "got {result:?}");
}
#[test]
fn plrm_examples() {
let cases: &[(&[u8], &[f64], &[f64], &str)] = &[
(b"{ atan }", &[-100.0, 0.0], &[270.0], "atan negative-num zero-den"),
(b"{ atan }", &[-1.0, -1.0], &[225.0], "atan third quadrant"),
(b"{ atan }", &[0.0, 1.0], &[0.0], "atan first axis"),
(b"{ atan }", &[1.0, 1.0], &[45.0], "atan first quadrant"),
(b"{ atan }", &[0.0, -1.0], &[180.0], "atan negative-x axis"),
(b"{ round }", &[-6.5], &[-6.0], "round negative half toward +inf"),
(b"{ round }", &[6.5], &[7.0], "round positive half toward +inf"),
(b"{ round }", &[-0.5], &[0.0], "round -0.5"),
(b"{ round }", &[0.5], &[1.0], "round 0.5"),
(b"{ idiv }", &[-7.0, 2.0], &[-3.0], "idiv negative"),
(b"{ mod }", &[-7.0, 2.0], &[-1.0], "mod negative dividend"),
(b"{ truncate }", &[-6.5], &[-6.0], "truncate negative"),
];
for (prog, inp, want, desc) in cases {
let got = evaluate_type4(prog, inp).unwrap_or_else(|e| panic!("{desc}: {e}"));
assert!(approx_eq(&got, want, 1e-9), "case: {desc}\n got: {got:?}\n want: {want:?}");
}
}
#[test]
fn not_distinguishes_bool_from_int() {
assert_eq!(evaluate_type4(b"{ true not }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ false not }", &[]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ 52 not }", &[]).unwrap(), vec![-53.0]);
assert_eq!(evaluate_type4(b"{ 1 not }", &[]).unwrap(), vec![-2.0]);
assert_eq!(evaluate_type4(b"{ 0 not }", &[]).unwrap(), vec![-1.0]);
}
#[test]
fn and_or_xor_dispatch_on_type() {
assert_eq!(evaluate_type4(b"{ true true and }", &[]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ 12 10 and }", &[]).unwrap(), vec![8.0]);
assert!(evaluate_type4(b"{ true 1 and }", &[]).is_err());
assert!(evaluate_type4(b"{ 1 true or }", &[]).is_err());
}
#[test]
fn integer_only_ops_reject_real_literals() {
assert!(evaluate_type4(b"{ 5.5 2 idiv }", &[]).is_err());
assert!(evaluate_type4(b"{ 5 2.5 idiv }", &[]).is_err());
assert!(evaluate_type4(b"{ 5 2.0 mod }", &[]).is_err());
assert!(evaluate_type4(b"{ 5.0 2 mod }", &[]).is_err());
assert!(evaluate_type4(b"{ 1.0 not }", &[]).is_err());
assert!(evaluate_type4(b"{ 3.0 1 bitshift }", &[]).is_err());
assert!(evaluate_type4(b"{ 3 1.0 bitshift }", &[]).is_err());
}
#[test]
fn integer_valued_inputs_accepted_by_integer_ops() {
assert_eq!(evaluate_type4(b"{ idiv }", &[10.0, 3.0]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ mod }", &[10.0, 3.0]).unwrap(), vec![1.0]);
assert_eq!(evaluate_type4(b"{ bitshift }", &[1.0, 4.0]).unwrap(), vec![16.0]);
}
#[test]
fn errors_not_panics() {
assert!(evaluate_type4(b"{ sqrt }", &[-1.0]).is_err());
assert!(evaluate_type4(b"{ ln }", &[0.0]).is_err());
assert!(evaluate_type4(b"{ ln }", &[-1.0]).is_err());
assert!(evaluate_type4(b"{ log }", &[0.0]).is_err());
assert!(evaluate_type4(b"{ log }", &[-1.0]).is_err());
let r = evaluate_type4_clamped(b"{ }", &[0.5], &[[1.0, 0.0]], &[]).unwrap();
assert_eq!(r, vec![0.5]);
let r =
evaluate_type4_clamped(b"{ }", &[0.5], &[[f64::NAN, 1.0]], &[[0.0, f64::NAN]]).unwrap();
assert_eq!(r, vec![0.5]);
assert_eq!(evaluate_type4(b"{ 1 64 bitshift }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ 1 100 bitshift }", &[]).unwrap(), vec![0.0]);
assert_eq!(evaluate_type4(b"{ 1 -64 bitshift }", &[]).unwrap(), vec![0.0]);
let prog = format!("{{ {} -1 idiv }}", i64::MIN);
assert!(evaluate_type4(prog.as_bytes(), &[]).is_err());
assert!(evaluate_type4(b"{ inf }", &[]).is_err());
assert!(evaluate_type4(b"{ NaN }", &[]).is_err());
assert!(evaluate_type4(b"{ 7.5 2 idiv }", &[]).is_err());
assert!(evaluate_type4(b"{ 7 2.5 mod }", &[]).is_err());
assert!(evaluate_type4(b"{ -1 copy }", &[1.0]).is_err());
assert!(evaluate_type4(b"{ -1 index }", &[1.0]).is_err());
assert!(evaluate_type4(b"{ -1 1 roll }", &[1.0, 2.0]).is_err());
assert!(evaluate_type4(b"{ atan }", &[0.0, 0.0]).is_err());
}
#[test]
fn cvi_truncates_toward_zero() {
assert_eq!(evaluate_type4(b"{ cvi }", &[3.2]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ cvi }", &[-3.2]).unwrap(), vec![-3.0]);
assert_eq!(evaluate_type4(b"{ cvi }", &[3.0]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ 3.5 cvi }", &[]).unwrap(), vec![3.0]);
assert_eq!(evaluate_type4(b"{ -3.5 cvi }", &[]).unwrap(), vec![-3.0]);
}
#[test]
fn cvr_makes_typed_real() {
let err = evaluate_type4(b"{ 3 cvr 2 idiv }", &[]).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
assert_eq!(evaluate_type4(b"{ 3.5 cvr }", &[]).unwrap(), vec![3.5]);
assert_eq!(evaluate_type4(b"{ 3.5 cvi 2 idiv }", &[]).unwrap(), vec![1.0]);
}
#[test]
fn cvi_rejects_bool_and_non_finite() {
assert!(evaluate_type4(b"{ true cvi }", &[]).is_err());
assert!(evaluate_type4(b"{ true cvr }", &[]).is_err());
assert!(evaluate_type4(b"{ 1 0 div cvi }", &[]).is_err());
assert!(evaluate_type4(b"{ 0 0 div cvi }", &[]).is_err());
assert_eq!(evaluate_type4(b"{ 1 0 div cvr }", &[]).unwrap()[0], f64::INFINITY);
}
#[test]
fn program_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Program>();
}
#[test]
fn program_compile_once_evaluate_many() {
let program = Program::compile(b"{ dup mul }").expect("compile");
for i in 0..1000 {
let x = (i as f64) * 0.001;
let out = program.evaluate(&[x]).expect("eval");
assert_eq!(out.len(), 1);
let want = x * x;
assert!((out[0] - want).abs() < 1e-12, "i={i}: got {out:?}, want {want}");
}
}
#[test]
fn program_evaluate_clamped_matches_wrapper() {
let program = Program::compile(b"{ 2.0 mul }").expect("compile");
let direct = program
.evaluate_clamped(&[1.5], &[[0.0, 1.0]], &[[0.0, 1.0]])
.expect("direct");
let via_fn = evaluate_type4_clamped(b"{ 2.0 mul }", &[1.5], &[[0.0, 1.0]], &[[0.0, 1.0]])
.expect("via_fn");
assert_eq!(direct, via_fn);
}
#[test]
fn parse_depth_limit_enforced() {
let deep = format!("{{{}}}", "{".repeat(50)) + &"}".repeat(50);
let err = evaluate_type4(deep.as_bytes(), &[]).unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)), "got: {err}");
assert!(err.to_string().contains("depth"), "got: {err}");
}
#[test]
fn runtime_stack_overflow_caught() {
let prog = "{ ".to_string() + &"1 ".repeat(300) + "}";
let err = evaluate_type4(prog.as_bytes(), &[]).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
assert!(err.to_string().contains("stack overflow"), "got: {err}");
let ok = "{ ".to_string() + &"1 ".repeat(200) + "}";
assert!(evaluate_type4(ok.as_bytes(), &[]).is_ok());
}
#[test]
fn instruction_budget_caught() {
let mut body = String::from("{ ");
for _ in 0..100_001 {
body.push_str("dup pop ");
}
body.push('}');
let err = evaluate_type4(body.as_bytes(), &[1.0]).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
assert!(err.to_string().contains("instruction budget"), "got: {err}");
}
#[test]
fn input_count_capped() {
let many: Vec<f64> = (0..(MAX_STACK + 1)).map(|i| i as f64).collect();
let err = evaluate_type4(b"{ }", &many).unwrap_err();
assert!(matches!(err, Error::Type4Runtime(_)), "got: {err}");
}
#[test]
fn orphan_procedure_body_rejected_at_parse() {
let err = evaluate_type4(b"{ 1 { 2 } 3 }", &[]).unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)), "got: {err}");
assert!(err.to_string().contains("orphan"), "got: {err}");
}
#[test]
fn orphan_procedure_body_alone_rejected() {
let err = evaluate_type4(b"{ { 1 2 add } }", &[]).unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)), "got: {err}");
}
#[test]
fn atan_full_range() {
for &(num, den, want) in &[
(0.0, 1.0, 0.0),
(1.0, 1.0, 45.0),
(1.0, 0.0, 90.0),
(1.0, -1.0, 135.0),
(0.0, -1.0, 180.0),
(-1.0, -1.0, 225.0),
(-1.0, 0.0, 270.0),
(-1.0, 1.0, 315.0),
(-100.0, 0.0, 270.0),
] {
let got = evaluate_type4(b"{ atan }", &[num, den]).unwrap();
assert!((got[0] - want).abs() < 1e-9, "atan({num}, {den}) = {got:?}, want {want}");
assert!(got[0] >= 0.0 && got[0] < 360.0, "atan out of [0, 360): {got:?}");
}
}
#[test]
fn parse_depth_limit_returns_invalid_pdf() {
let mut bytes = Vec::new();
bytes.extend(std::iter::repeat_n(b'{', 50));
bytes.extend(std::iter::repeat_n(b'}', 50));
match evaluate_type4(&bytes, &[]) {
Err(Error::InvalidPdf(_)) => {}, Err(Error::Type4Runtime(s)) => {
panic!("parse depth should error as InvalidPdf, not Type4Runtime: {s}")
},
Err(other) => panic!("unexpected error: {other}"),
Ok(out) => panic!("should have errored, got {out:?}"),
}
}
#[test]
fn cvi_rejects_two_pow_63() {
let pow_63: f64 = 9_223_372_036_854_775_808.0; assert_eq!(pow_63, i64::MAX as f64, "test setup: 2^63 == i64::MAX as f64");
let result = evaluate_type4(b"{ cvi }", &[pow_63]);
match result {
Err(Error::Type4Runtime(s)) if s.contains("cvi") => {}, Err(other) => panic!("expected Type4Runtime(cvi overflow), got: {other}"),
Ok(v) => panic!(
"2^63 cvi should overflow; got {v:?} (likely saturated to i64::MAX = {})",
i64::MAX
),
}
}
#[test]
fn cvi_accepts_two_pow_63_minus_one() {
let near_max: f64 = 9_223_372_036_854_774_784.0; let result = evaluate_type4(b"{ cvi }", &[near_max]).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0] > 0.0 && result[0] < i64::MAX as f64);
}
}