diff --git a/crates/pdf-color-space/src/device_n_color_space.rs b/crates/pdf-color-space/src/device_n_color_space.rs index 64c6286d..205ed673 100644 --- a/crates/pdf-color-space/src/device_n_color_space.rs +++ b/crates/pdf-color-space/src/device_n_color_space.rs @@ -151,7 +151,7 @@ mod tests { let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Cyan"), name("Magenta")].into()), name("DeviceRGB"), - function_stream("pop pop 0.1 0.2 0.3", 3), + function_stream("{ pop pop 0.1 0.2 0.3 }", 3), ]); let parsed = read_device_n(arr).unwrap(); @@ -178,7 +178,7 @@ mod tests { let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Spot")].into()), name("DeviceCMYK"), - function_stream("pop 0.2 0.4 0.6 0.8", 4), + function_stream("{ pop 0.2 0.4 0.6 0.8 }", 4), attrs, ]); @@ -197,7 +197,7 @@ mod tests { let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Cyan"), name("Magenta"), name("Yellow")].into()), name("DeviceRGB"), - function_stream("pop pop pop 0.25 0.5 0.75", 3), + function_stream("{ pop pop pop 0.25 0.5 0.75 }", 3), ]); let parsed = read_device_n(arr).unwrap(); @@ -218,7 +218,7 @@ mod tests { let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Cyan"), name("Magenta"), name("Yellow")].into()), name("DeviceRGB"), - function_stream("pop pop 0.25 0.5", 2), + function_stream("{ pop pop 0.25 0.5 }", 2), ]); let parsed = read_device_n(arr).unwrap(); @@ -243,7 +243,7 @@ mod tests { let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Cyan"), name("Magenta")].into()), name("DeviceRGB"), - function_stream("pop pop 0.1 0.2 0.3", 3), + function_stream("{ pop pop 0.1 0.2 0.3 }", 3), ]); let parsed = read_device_n(arr).unwrap(); diff --git a/crates/pdf-function/src/postscript_calculator.rs b/crates/pdf-function/src/postscript_calculator.rs index 5ae59084..1a585a1e 100644 --- a/crates/pdf-function/src/postscript_calculator.rs +++ b/crates/pdf-function/src/postscript_calculator.rs @@ -86,12 +86,8 @@ impl FromPdfObject for PostScriptCalculatorFunction { let domain = dictionary.required_intervals(b"Domain")?; let range = dictionary.required_intervals(b"Range")?; - // Parse PostScript code: add spaces around braces for tokenization - let code_str = String::from_utf8_lossy(context.stream().raw_data()); - let code_with_spaces = code_str.replace('{', " { ").replace('}', " } "); - let tokens: Vec<&str> = code_with_spaces.split_whitespace().collect(); - let operators = - pdf_postscript::parser::parse_tokens(&tokens).map_err(FunctionReadError::from)?; + let operators = pdf_postscript::parser::parse(context.stream().raw_data()) + .map_err(FunctionReadError::from)?; Ok(Self { operators, diff --git a/crates/pdf-image/src/image_decoder.rs b/crates/pdf-image/src/image_decoder.rs index 0f8d3941..5d55f032 100644 --- a/crates/pdf-image/src/image_decoder.rs +++ b/crates/pdf-image/src/image_decoder.rs @@ -301,7 +301,7 @@ mod tests { ), ), ])), - b"4 3 roll pop".to_vec(), + b"{ 4 3 roll pop }".to_vec(), ); ObjectVariant::Array( @@ -546,7 +546,7 @@ mod tests { ), ), ])), - b"pop 1 1 1".to_vec(), + b"{ pop 1 1 1 }".to_vec(), ); let dictionary = Dictionary::new(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), diff --git a/crates/pdf-postscript/benches/calculator.rs b/crates/pdf-postscript/benches/calculator.rs index e5ffa496..830bf7e9 100644 --- a/crates/pdf-postscript/benches/calculator.rs +++ b/crates/pdf-postscript/benches/calculator.rs @@ -7,7 +7,7 @@ use std::hint::black_box; use criterion::{Criterion, criterion_group, criterion_main}; -use pdf_postscript::{calculator::execute, operator::Operator, parser::parse_tokens, value::Value}; +use pdf_postscript::{calculator::execute, operator::Operator, parser, value::Value}; const SAMPLES: usize = 256; @@ -25,9 +25,7 @@ const STACK: &str = "{ 3 copy 2 index 1 index add 4 1 roll mul exch div 5 1 roll 3 copy pop pop pop pop 4 2 roll add add exch pop }"; fn parse(code: &str) -> Vec { - let code = code.replace('{', " { ").replace('}', " } "); - parse_tokens(&code.split_whitespace().collect::>()) - .expect("benchmark program should parse") + parser::parse(code.as_bytes()).expect("benchmark program should parse") } fn benchmark_program(criterion: &mut Criterion, name: &str, code: &str, inputs: usize) { diff --git a/crates/pdf-postscript/src/calculator.rs b/crates/pdf-postscript/src/calculator.rs index 5655eb94..bb619e13 100644 --- a/crates/pdf-postscript/src/calculator.rs +++ b/crates/pdf-postscript/src/calculator.rs @@ -1,12 +1,18 @@ -use crate::{operator::Operator, parser::parse_tokens, value::Value}; +use crate::{operator::Operator, parser::parse, value::Value}; use num_traits::ToPrimitive; use thiserror::Error; /// Errors that can occur while executing a PostScript-like calculator program. #[derive(Debug, Error, PartialEq)] pub enum CalcError { - #[error("unexpected end of block stack")] - EmptyBlockStack, + #[error("program is not enclosed in a '{{ }}' procedure")] + MissingProgramProcedure, + #[error("unbalanced braces or tokens outside the program procedure")] + UnbalancedBraces, + #[error("procedure is not an operand of 'if' or 'ifelse'")] + UnexpectedProcedure, + #[error("unexpected delimiter '{}' in program", char::from(*.0))] + UnexpectedDelimiter(u8), #[error("missing procedure block before 'if' operator")] MissingIfBlock, #[error("missing two procedure blocks before 'ifelse' operator")] @@ -25,8 +31,6 @@ pub enum CalcError { RollCountTooLarge { n: usize, size: usize }, #[error("invalid copy count n={n} larger than stack size {size}")] CopyCountTooLarge { n: usize, size: usize }, - #[error("token index overflow while parsing")] - TokenIndexOverflow, #[error("arithmetic overflow in {op} operation")] ArithmeticOverflow { op: &'static str }, #[error("operand for {op} must be an integer (no fraction) within valid range, got {value}")] @@ -466,15 +470,12 @@ pub fn execute(input_stack: Vec, ops: &[Operator]) -> Result, Ok(stack.values) } -/// Convenience helper that tokenizes & parses a PostScript-like `code` string -/// and then invokes [`execute`]. +/// Convenience helper that parses a Type 4 `program` (one `{ ... }` procedure) +/// with [`parse`] and then invokes [`execute`]. /// -/// The `input_stack` supplies initial operands (in bottom-to-top order). The -/// `code` string can contain numeric literals, the supported operators, and -/// procedure blocks delimited by `{` and `}` used by `if` / `ifelse`. -pub fn evaluate_postscript(input_stack: &[Value], code: &str) -> Result, CalcError> { - let code = code.replace("{", " { ").replace("}", " } "); - let ops = parse_tokens(&code.split_whitespace().collect::>())?; +/// The `input_stack` supplies initial operands (in bottom-to-top order). +pub fn evaluate_postscript(input_stack: &[Value], program: &[u8]) -> Result, CalcError> { + let ops = parse(program)?; execute(input_stack.to_vec(), &ops) } @@ -622,442 +623,6 @@ fn checked_real(value: f64, op: &'static str) -> Result { } } -#[cfg(all(test, not(test)))] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - fn assert_approx_eq(actual: f64, expected: f64) { - let delta = (actual - expected).abs(); - assert!( - delta < 1e-12, - "expected {expected}, got {actual}, delta {delta}" - ); - } - - #[test] - fn test_parse_simple_operators() { - let tokens = vec!["add", "sub", "mul", "div"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![Operator::Add, Operator::Sub, Operator::Mul, Operator::Div] - ); - } - - #[test] - fn test_parse_numbers() { - let tokens = vec!["1", "2.5", "-3"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![ - Operator::Number(1.0), - Operator::Number(2.5), - Operator::Number(-3.0) - ] - ); - } - - #[test] - fn test_parse_if_block() { - let tokens = vec!["{", "2", "3", "add", "}", "if"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![Operator::If(vec![ - Operator::Number(2.0), - Operator::Number(3.0), - Operator::Add - ])] - ); - } - - #[test] - fn test_parse_ifelse_block() { - let tokens = vec![ - "{", "2", "3", "add", "}", "{", "4", "5", "add", "}", "ifelse", - ]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![Operator::IfElse( - vec![Operator::Number(2.0), Operator::Number(3.0), Operator::Add], - vec![Operator::Number(4.0), Operator::Number(5.0), Operator::Add] - )] - ); - } - - #[test] - fn test_parse_nested_blocks() { - let tokens = vec!["{", "1", "{", "2", "3", "add", "}", "if", "}", "if"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![Operator::If(vec![ - Operator::Number(1.0), - Operator::If(vec![ - Operator::Number(2.0), - Operator::Number(3.0), - Operator::Add - ]) - ])] - ); - } - - #[test] - fn test_parse_invalid_number() { - let tokens = vec!["foo"]; - let err = parse_tokens(&tokens).unwrap_err(); - assert!(matches!(err, CalcError::InvalidNumber(_))); - } - - #[test] - fn test_parse_logical_operators() { - let tokens = vec![ - "eq", "ne", "gt", "lt", "ge", "le", "and", "or", "xor", "not", - ]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![ - Operator::Eq, - Operator::Ne, - Operator::Gt, - Operator::Lt, - Operator::Ge, - Operator::Le, - Operator::And, - Operator::Or, - Operator::Xor, - Operator::Not - ] - ); - } - - #[test] - fn test_parse_transcendental_operators() { - let tokens = vec!["sin", "cos", "tan", "atan", "log", "floor"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!( - ops, - vec![ - Operator::Sin, - Operator::Cos, - Operator::Tan, - Operator::Atan, - Operator::Log, - Operator::Floor - ] - ); - } - - #[test] - fn test_parse_cvr_operator() { - let tokens = vec!["cvr"]; - let ops = parse_tokens(&tokens).unwrap(); - assert_eq!(ops, vec![Operator::Cvr]); - } - - #[test] - fn test_add() { - let result = evaluate_postscript(&[2.0, 3.0], "add").unwrap(); - assert_eq!(result, vec![5.0]); - } - - #[test] - fn test_sub() { - let result = evaluate_postscript(&[5.0, 2.0], "sub").unwrap(); - assert_eq!(result, vec![3.0]); - } - - #[test] - fn test_mul() { - let result = evaluate_postscript(&[4.0, 3.0], "mul").unwrap(); - assert_eq!(result, vec![12.0]); - } - - #[test] - fn test_div() { - let result = evaluate_postscript(&[8.0, 2.0], "div").unwrap(); - assert_eq!(result, vec![4.0]); - } - - #[test] - fn test_cvr_is_no_op_for_real_stack_values() { - let result = evaluate_postscript(&[8.5], "cvr").unwrap(); - assert_eq!(result, vec![8.5]); - } - - #[test] - fn test_dup() { - let result = evaluate_postscript(&[7.0], "dup").unwrap(); - assert_eq!(result, vec![7.0, 7.0]); - } - - #[test] - fn test_exch() { - let result = evaluate_postscript(&[1.0, 2.0], "exch").unwrap(); - assert_eq!(result, vec![2.0, 1.0]); - } - - #[test] - fn test_pop() { - let result = evaluate_postscript(&[1.0, 2.0, 3.0], "pop").unwrap(); - assert_eq!(result, vec![1.0, 2.0]); - } - - #[test] - fn test_eq() { - let result = evaluate_postscript(&[2.0, 2.0], "eq").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[2.0, 3.0], "eq").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_ne() { - let result = evaluate_postscript(&[2.0, 3.0], "ne").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[2.0, 2.0], "ne").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_gt() { - let result = evaluate_postscript(&[3.0, 2.0], "gt").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[2.0, 3.0], "gt").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_lt() { - let result = evaluate_postscript(&[2.0, 3.0], "lt").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[3.0, 2.0], "lt").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_ge() { - let result = evaluate_postscript(&[3.0, 2.0], "ge").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[2.0, 2.0], "ge").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[1.0, 2.0], "ge").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_le() { - let result = evaluate_postscript(&[2.0, 3.0], "le").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[2.0, 2.0], "le").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[3.0, 2.0], "le").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_and() { - let result = evaluate_postscript(&[1.0, 1.0], "and").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[1.0, 0.0], "and").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_or() { - let result = evaluate_postscript(&[0.0, 1.0], "or").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[0.0, 0.0], "or").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_xor() { - let result = evaluate_postscript(&[1.0, 0.0], "xor").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[1.0, 1.0], "xor").unwrap(); - assert_eq!(result, vec![0.0]); - let result = evaluate_postscript(&[0.0, 0.0], "xor").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_not() { - let result = evaluate_postscript(&[0.0], "not").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[1.0], "not").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_if_true() { - let result = evaluate_postscript(&[1.0], "{ 2 3 add } if").unwrap(); - assert_eq!(result, vec![5.0]); - } - - #[test] - fn test_if_false() { - let result = evaluate_postscript(&[0.0], "{ 2 3 add } if").unwrap(); - assert_eq!(result, Vec::::new()); - } - - #[test] - fn test_ifelse_true() { - let result = evaluate_postscript(&[1.0], "{ 2 3 add } { 4 5 add } ifelse").unwrap(); - assert_eq!(result, vec![5.0]); - } - - #[test] - fn test_ifelse_false() { - let result = evaluate_postscript(&[0.0], "{ 2 3 add } { 4 5 add } ifelse").unwrap(); - assert_eq!(result, vec![9.0]); - } - - #[test] - fn test_nested_blocks() { - let result = evaluate_postscript(&[1.0], "{ 1 { 2 3 add } if } if").unwrap(); - assert_eq!(result, vec![5.0]); - } - - #[test] - fn test_complex_expression() { - let result = evaluate_postscript(&[2.0, 3.0, 4.0], "add mul").unwrap(); - assert_eq!(result, vec![14.0]); - } - - #[test] - fn test_copy() { - let result = evaluate_postscript(&[1.0, 2.0, 3.0], "2 copy").unwrap(); - assert_eq!(result, vec![1.0, 2.0, 3.0, 2.0, 3.0]); - } - - #[test] - fn test_index() { - let result = evaluate_postscript(&[1.0, 2.0, 3.0, 4.0], "2 index").unwrap(); - assert_eq!(result, vec![1.0, 2.0, 3.0, 4.0, 2.0]); - } - - #[test] - fn test_roll() { - let result = evaluate_postscript(&[1.0, 2.0, 3.0, 4.0, 5.0], "3 1 roll").unwrap(); - assert_eq!(result, vec![1.0, 2.0, 5.0, 3.0, 4.0]); - let result = evaluate_postscript(&[1.0, 2.0, 3.0, 4.0, 5.0], "4 -2 roll").unwrap(); - assert_eq!(result, vec![1.0, 4.0, 5.0, 2.0, 3.0]); - let result = evaluate_postscript(&[1.0, 2.0, 3.0, 4.0, 5.0], "0 7 roll").unwrap(); - assert_eq!(result, vec![1.0, 2.0, 3.0, 4.0, 5.0]); - } - - #[test] - fn test_sqrt() { - let result = evaluate_postscript(&[9.0], "sqrt").unwrap(); - assert_eq!(result, vec![3.0]); - let result = evaluate_postscript(&[2.25], "sqrt").unwrap(); - assert_eq!(result, vec![1.5]); - } - - #[test] - fn test_sin() { - let result = evaluate_postscript(&[30.0], "sin").unwrap(); - assert_approx_eq(result[0], 0.5); - let result = evaluate_postscript(&[90.0], "sin").unwrap(); - assert_approx_eq(result[0], 1.0); - } - - #[test] - fn test_cos() { - let result = evaluate_postscript(&[60.0], "cos").unwrap(); - assert_approx_eq(result[0], 0.5); - let result = evaluate_postscript(&[180.0], "cos").unwrap(); - assert_approx_eq(result[0], -1.0); - } - - #[test] - fn test_tan() { - let result = evaluate_postscript(&[45.0], "tan").unwrap(); - assert_approx_eq(result[0], 1.0); - } - - #[test] - fn test_atan() { - let result = evaluate_postscript(&[1.0, 1.0], "atan").unwrap(); - assert_approx_eq(result[0], 45.0); - let result = evaluate_postscript(&[1.0, 0.0], "atan").unwrap(); - assert_approx_eq(result[0], 90.0); - } - - #[test] - fn test_log() { - let result = evaluate_postscript(&[std::f64::consts::E], "log").unwrap(); - assert_approx_eq(result[0], 1.0); - let result = evaluate_postscript(&[1.0], "log").unwrap(); - assert_approx_eq(result[0], 0.0); - } - - #[test] - fn test_log_domain_error() { - let err = evaluate_postscript(&[0.0], "log").unwrap_err(); - assert!(matches!(err, CalcError::LogDomainError { value } if value == 0.0)); - let err = evaluate_postscript(&[-1.0], "log").unwrap_err(); - assert!(matches!(err, CalcError::LogDomainError { value } if value == -1.0)); - } - - #[test] - fn test_floor() { - let result = evaluate_postscript(&[3.7], "floor").unwrap(); - assert_eq!(result, vec![3.0]); - let result = evaluate_postscript(&[-2.1], "floor").unwrap(); - assert_eq!(result, vec![-3.0]); - } - - #[test] - fn test_truncate() { - let result = evaluate_postscript(&[3.7], "truncate").unwrap(); - assert_eq!(result, vec![3.0]); - let result = evaluate_postscript(&[-2.9], "truncate").unwrap(); - assert_eq!(result, vec![-2.0]); - let result = evaluate_postscript(&[0.0], "truncate").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_abs() { - let result = evaluate_postscript(&[-5.0], "abs").unwrap(); - assert_eq!(result, vec![5.0]); - let result = evaluate_postscript(&[3.2], "abs").unwrap(); - assert_eq!(result, vec![3.2]); - let result = evaluate_postscript(&[0.0], "abs").unwrap(); - assert_eq!(result, vec![0.0]); - } - - #[test] - fn test_cvi() { - let result = evaluate_postscript(&[3.7], "cvi").unwrap(); - assert_eq!(result, vec![3.0]); - let result = evaluate_postscript(&[-2.9], "cvi").unwrap(); - assert_eq!(result, vec![-2.0]); - let result = evaluate_postscript(&[0.0], "cvi").unwrap(); - assert_eq!(result, vec![0.0]); - let result = evaluate_postscript(&[5.0], "cvi").unwrap(); - assert_eq!(result, vec![5.0]); - } - - #[test] - fn test_mod() { - let result = evaluate_postscript(&[10.0, 3.0], "mod").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[-10.0, 3.0], "mod").unwrap(); - assert_eq!(result, vec![-1.0]); - let result = evaluate_postscript(&[10.0, -3.0], "mod").unwrap(); - assert_eq!(result, vec![1.0]); - let result = evaluate_postscript(&[0.0, 3.0], "mod").unwrap(); - assert_eq!(result, vec![0.0]); - } -} - #[cfg(test)] #[allow( clippy::unwrap_used, @@ -1081,8 +646,7 @@ mod typed_tests { #[test] fn parses_typed_literals_and_new_operators() { - let tokens = vec!["1", "2.5", "-3", "true", "false", "idiv", "bitshift"]; - let ops = parse_tokens(&tokens).unwrap(); + let ops = parse(b"{ 1 2.5 -3 true false idiv bitshift }").unwrap(); assert_eq!( ops, vec![ @@ -1099,25 +663,25 @@ mod typed_tests { #[test] fn integer_arithmetic_preserves_integer_type() { - let result = evaluate_postscript(&[], "2 3 add 4 mul 5 sub").unwrap(); + let result = evaluate_postscript(&[], b"{ 2 3 add 4 mul 5 sub }").unwrap(); assert_eq!(result, vec![Value::Integer(15)]); } #[test] fn mixed_arithmetic_promotes_to_real() { - let result = evaluate_postscript(&[], "2 3.5 add").unwrap(); + let result = evaluate_postscript(&[], b"{ 2 3.5 add }").unwrap(); assert_eq!(result, vec![Value::Real(5.5)]); } #[test] fn div_always_returns_real() { - let result = evaluate_postscript(&[], "8 2 div").unwrap(); + let result = evaluate_postscript(&[], b"{ 8 2 div }").unwrap(); assert_eq!(result, vec![Value::Real(4.0)]); } #[test] fn idiv_truncates_toward_zero() { - let result = evaluate_postscript(&[], "7 3 idiv -7 3 idiv 7 -3 idiv").unwrap(); + let result = evaluate_postscript(&[], b"{ 7 3 idiv -7 3 idiv 7 -3 idiv }").unwrap(); assert_eq!( result, vec![Value::Integer(2), Value::Integer(-2), Value::Integer(-2)] @@ -1126,10 +690,10 @@ mod typed_tests { #[test] fn idiv_rejects_zero_and_real_operands() { - let err = evaluate_postscript(&[], "1 0 idiv").unwrap_err(); + let err = evaluate_postscript(&[], b"{ 1 0 idiv }").unwrap_err(); assert_eq!(err, CalcError::DivisionByZero); - let err = evaluate_postscript(&[], "1.0 1 idiv").unwrap_err(); + let err = evaluate_postscript(&[], b"{ 1.0 1 idiv }").unwrap_err(); assert!(matches!( err, CalcError::InvalidIntegerOperand { @@ -1141,28 +705,28 @@ mod typed_tests { #[test] fn bitshift_shifts_left_and_arithmetic_right() { - let result = evaluate_postscript(&[], "3 2 bitshift -8 -1 bitshift").unwrap(); + let result = evaluate_postscript(&[], b"{ 3 2 bitshift -8 -1 bitshift }").unwrap(); assert_eq!(result, vec![Value::Integer(12), Value::Integer(-4)]); } #[test] fn bitshift_rejects_invalid_shift_amounts() { - let err = evaluate_postscript(&[], "1 32 bitshift").unwrap_err(); + let err = evaluate_postscript(&[], b"{ 1 32 bitshift }").unwrap_err(); assert_eq!(err, CalcError::ArithmeticOverflow { op: "bitshift" }); } #[test] fn boolean_control_flow_uses_bool_values() { - let result = evaluate_postscript(&[], "true { 2 3 add } if").unwrap(); + let result = evaluate_postscript(&[], b"{ true { 2 3 add } if }").unwrap(); assert_eq!(result, vec![Value::Integer(5)]); - let result = evaluate_postscript(&[], "false { 2 } { 3 } ifelse").unwrap(); + let result = evaluate_postscript(&[], b"{ false { 2 } { 3 } ifelse }").unwrap(); assert_eq!(result, vec![Value::Integer(3)]); } #[test] fn control_flow_rejects_numeric_condition() { - let err = evaluate_postscript(&[], "1 { 2 } if").unwrap_err(); + let err = evaluate_postscript(&[], b"{ 1 { 2 } if }").unwrap_err(); assert!(matches!( err, CalcError::InvalidOperandType { @@ -1175,19 +739,19 @@ mod typed_tests { #[test] fn comparisons_and_logical_ops_return_bools() { - let result = evaluate_postscript(&[], "2 3 lt true xor").unwrap(); + let result = evaluate_postscript(&[], b"{ 2 3 lt true xor }").unwrap(); assert_eq!(result, vec![Value::Bool(false)]); } #[test] fn integer_bitwise_ops_use_integer_type() { - let result = evaluate_postscript(&[], "6 3 and 4 or 7 xor not").unwrap(); + let result = evaluate_postscript(&[], b"{ 6 3 and 4 or 7 xor not }").unwrap(); assert_eq!(result, vec![Value::Integer(-2)]); } #[test] fn stack_ops_accept_integer_counts() { - let result = evaluate_postscript(&[], "1 2 3 2 copy 4 5 3 1 roll").unwrap(); + let result = evaluate_postscript(&[], b"{ 1 2 3 2 copy 4 5 3 1 roll }").unwrap(); assert_eq!( result, vec![ @@ -1204,7 +768,8 @@ mod typed_tests { #[test] fn cvi_cvr_floor_truncate_and_abs_convert_types() { - let result = evaluate_postscript(&[], "3.7 cvi -2.1 floor 5 cvr -2.9 truncate").unwrap(); + let result = + evaluate_postscript(&[], b"{ 3.7 cvi -2.1 floor 5 cvr -2.9 truncate }").unwrap(); assert_eq!( result, vec![ @@ -1215,13 +780,13 @@ mod typed_tests { ] ); - let result = evaluate_postscript(&[], "-5 abs 3.25 abs").unwrap(); + let result = evaluate_postscript(&[], b"{ -5 abs 3.25 abs }").unwrap(); assert_eq!(result, vec![Value::Integer(5), Value::Real(3.25)]); } #[test] fn transcendental_ops_still_return_reals() { - let result = evaluate_postscript(&[], "9 sqrt 30 sin").unwrap(); + let result = evaluate_postscript(&[], b"{ 9 sqrt 30 sin }").unwrap(); assert_real_approx_eq(result[0], 3.0); assert_real_approx_eq(result[1], 0.5); } diff --git a/crates/pdf-postscript/src/lib.rs b/crates/pdf-postscript/src/lib.rs index 9eef2074..f675d8ea 100644 --- a/crates/pdf-postscript/src/lib.rs +++ b/crates/pdf-postscript/src/lib.rs @@ -3,7 +3,7 @@ //! This crate provides: //! * A small set of PostScript arithmetic, comparison, logical, transcendental //! and stack manipulation operators (see [`operator::Operator`]). -//! * A parser (`parser::parse_tokens`) that turns token slices into an +//! * A parser (`parser::parse`) that lexes Type 4 program bytes into an //! executable operator list supporting nested procedure blocks for `if` / `ifelse`. //! * An interpreter (`calculator::execute` / `calculator::evaluate_postscript`) that //! evaluates the operators against a typed operand stack. diff --git a/crates/pdf-postscript/src/operator.rs b/crates/pdf-postscript/src/operator.rs index cd3fda80..082ac6dc 100644 --- a/crates/pdf-postscript/src/operator.rs +++ b/crates/pdf-postscript/src/operator.rs @@ -1,4 +1,4 @@ -use crate::value::Value; +use crate::{calculator::CalcError, value::Value}; /// Represents a subset of PostScript operators (and literals) supported by the PDF /// PostScript-like calculator in this crate. Each variant corresponds to an @@ -104,3 +104,54 @@ pub enum Operator { /// Literal value. Number(Value), } + +/// Reads one word of a Type 4 program as an operator name or a literal. +/// `if` and `ifelse` take procedure operands, so the parser handles them. +impl TryFrom<&[u8]> for Operator { + type Error = CalcError; + + fn try_from(word: &[u8]) -> Result { + Ok(match word { + b"add" => Self::Add, + b"sub" => Self::Sub, + b"mul" => Self::Mul, + b"div" => Self::Div, + b"idiv" => Self::Idiv, + b"dup" => Self::Dup, + b"exch" => Self::Exch, + b"pop" => Self::Pop, + b"eq" => Self::Eq, + b"ne" => Self::Ne, + b"gt" => Self::Gt, + b"lt" => Self::Lt, + b"ge" => Self::Ge, + b"le" => Self::Le, + b"and" => Self::And, + b"or" => Self::Or, + b"xor" => Self::Xor, + b"not" => Self::Not, + b"bitshift" => Self::Bitshift, + b"copy" => Self::Copy, + b"index" => Self::Index, + b"roll" => Self::Roll, + b"sqrt" => Self::Sqrt, + b"sin" => Self::Sin, + b"cos" => Self::Cos, + b"tan" => Self::Tan, + b"atan" => Self::Atan, + b"exp" => Self::Exp, + b"ln" => Self::Ln, + b"log" => Self::Log, + b"cvi" => Self::Cvi, + b"cvr" => Self::Cvr, + b"mod" => Self::Mod, + b"floor" => Self::Floor, + b"ceiling" => Self::Ceiling, + b"round" => Self::Round, + b"truncate" => Self::Truncate, + b"abs" => Self::Abs, + b"neg" => Self::Neg, + literal => Self::Number(Value::try_from(literal)?), + }) + } +} diff --git a/crates/pdf-postscript/src/parser.rs b/crates/pdf-postscript/src/parser.rs index 65a303cb..59db6919 100644 --- a/crates/pdf-postscript/src/parser.rs +++ b/crates/pdf-postscript/src/parser.rs @@ -1,135 +1,150 @@ -use crate::{calculator::CalcError, operator::Operator, value::Value}; +use std::mem; -/// Represents a block of operations. -struct ProgramBlock { - ops: Vec>, +use crate::{calculator::CalcError, operator::Operator}; + +/// PDF whitespace bytes, which separate tokens. +const WHITESPACE: &[u8] = b"\0\t\n\x0C\r "; + +/// PostScript delimiter bytes, which end a word. +const DELIMITERS: &[u8] = b"()<>[]{}/%"; + +/// One lexical token of a Type 4 program. +enum Token<'a> { + /// `{`, the start of a procedure. + ProcBegin, + /// `}`, the end of a procedure. + ProcEnd, + /// An operator name or a literal. + Word(&'a [u8]), } -impl ProgramBlock { - /// Create a new empty program block. - fn new() -> Self { - Self { ops: vec![vec![]] } - } +/// Splits Type 4 program bytes into tokens, skipping whitespace and `%` comments. +struct Lexer<'a> { + bytes: &'a [u8], +} - /// Push a new operator onto the current block. - fn push(&mut self, op: Operator) -> Result<(), CalcError> { - self.ops - .last_mut() - .ok_or(CalcError::EmptyBlockStack)? - .push(op); - Ok(()) +impl<'a> Lexer<'a> { + /// Consumes and returns the leading bytes that satisfy `matches`. + fn take_while(&mut self, matches: impl Fn(&u8) -> bool) -> &'a [u8] { + let len = self + .bytes + .iter() + .position(|byte| !matches(byte)) + .unwrap_or(self.bytes.len()); + let (taken, rest) = self + .bytes + .split_at_checked(len) + .unwrap_or((self.bytes, &[])); + self.bytes = rest; + taken } +} - /// Pop the current block and return it. - fn pop(&mut self) -> Option> { - self.ops.pop() - } +impl<'a> Iterator for Lexer<'a> { + type Item = Result, CalcError>; - /// Push a new block onto the stack. - fn push_block(&mut self) -> Result<(), CalcError> { - self.ops.push(vec![]); - Ok(()) + fn next(&mut self) -> Option { + loop { + self.take_while(|byte| WHITESPACE.contains(byte)); + let (&first, rest) = self.bytes.split_first()?; + let token = match first { + // A comment runs to the end of the line. + b'%' => { + self.take_while(|byte| !matches!(byte, b'\r' | b'\n')); + continue; + } + b'{' => Ok(Token::ProcBegin), + b'}' => Ok(Token::ProcEnd), + _ if DELIMITERS.contains(&first) => Err(CalcError::UnexpectedDelimiter(first)), + _ => { + let word = self.take_while(|byte| { + !WHITESPACE.contains(byte) && !DELIMITERS.contains(byte) + }); + return Some(Ok(Token::Word(word))); + } + }; + self.bytes = rest; + return Some(token); + } } } -/// Parse a linear slice of token strings into a flat `Vec`. -/// -/// The grammar handled is intentionally small: numeric literals, the subset of -/// operators defined in [`Operator`], and procedure blocks delimited by `{` and -/// `}` which are collected and stored inside `Operator::If` / `Operator::IfElse`. -/// -/// Block parsing uses a stack (`block_stack`) so nesting is supported (only as -/// needed by tests for `if` / `ifelse`). A closing brace currently acts as a -/// marker; actual attachment to control operators happens when encountering -/// `if` / `ifelse`. -pub fn parse_tokens(tokens: &[&str]) -> Result, CalcError> { - let mut i = 0; - let mut block_stack = ProgramBlock::new(); - while i < tokens.len() { - match tokens - .get(i) - .copied() - .ok_or(CalcError::TokenIndexOverflow)? - { - "add" => block_stack.push(Operator::Add)?, - "sub" => block_stack.push(Operator::Sub)?, - "mul" => block_stack.push(Operator::Mul)?, - "div" => block_stack.push(Operator::Div)?, - "idiv" => block_stack.push(Operator::Idiv)?, - "dup" => block_stack.push(Operator::Dup)?, - "exch" => block_stack.push(Operator::Exch)?, - "pop" => block_stack.push(Operator::Pop)?, - "eq" => block_stack.push(Operator::Eq)?, - "ne" => block_stack.push(Operator::Ne)?, - "gt" => block_stack.push(Operator::Gt)?, - "lt" => block_stack.push(Operator::Lt)?, - "ge" => block_stack.push(Operator::Ge)?, - "le" => block_stack.push(Operator::Le)?, - "and" => block_stack.push(Operator::And)?, - "or" => block_stack.push(Operator::Or)?, - "xor" => block_stack.push(Operator::Xor)?, - "not" => block_stack.push(Operator::Not)?, - "bitshift" => block_stack.push(Operator::Bitshift)?, - "if" => { - let block1 = block_stack.pop().ok_or(CalcError::MissingIfBlock)?; - block_stack.push(Operator::If(block1))?; - } - "ifelse" => { - let block1 = block_stack.pop().ok_or(CalcError::MissingIfElseBlocks)?; - let block2 = block_stack.pop().ok_or(CalcError::MissingIfElseBlocks)?; - block_stack.push(Operator::IfElse(block2, block1))?; - } - "{" => { - block_stack.push_block()?; +/// A procedure being read, with the finished procedures that wait for `if` or `ifelse`. +#[derive(Default)] +struct Procedure { + ops: Vec, + operands: Vec>, +} + +impl Procedure { + /// Appends the operator a word names. `if` and `ifelse` take the pending + /// procedures as operands; any other word must not follow a procedure. + fn push_word(&mut self, word: &[u8]) -> Result<(), CalcError> { + let op = match word { + b"if" => { + let [then]: [Vec; 1] = mem::take(&mut self.operands) + .try_into() + .map_err(|_| CalcError::MissingIfBlock)?; + Operator::If(then) } - "}" => {} - "copy" => block_stack.push(Operator::Copy)?, - "index" => block_stack.push(Operator::Index)?, - "roll" => block_stack.push(Operator::Roll)?, - "sqrt" => block_stack.push(Operator::Sqrt)?, - "sin" => block_stack.push(Operator::Sin)?, - "cos" => block_stack.push(Operator::Cos)?, - "tan" => block_stack.push(Operator::Tan)?, - "atan" => block_stack.push(Operator::Atan)?, - "exp" => block_stack.push(Operator::Exp)?, - "ln" => block_stack.push(Operator::Ln)?, - "log" => block_stack.push(Operator::Log)?, - "cvi" => block_stack.push(Operator::Cvi)?, - "cvr" => block_stack.push(Operator::Cvr)?, - "mod" => block_stack.push(Operator::Mod)?, - "floor" => block_stack.push(Operator::Floor)?, - "ceiling" => block_stack.push(Operator::Ceiling)?, - "round" => block_stack.push(Operator::Round)?, - "truncate" => block_stack.push(Operator::Truncate)?, - "abs" => block_stack.push(Operator::Abs)?, - "neg" => block_stack.push(Operator::Neg)?, - "true" => block_stack.push(Operator::Number(Value::Bool(true)))?, - "false" => block_stack.push(Operator::Number(Value::Bool(false)))?, - t => { - let value = parse_number(t)?; - block_stack.push(Operator::Number(value))?; + b"ifelse" => { + let [then, otherwise]: [Vec; 2] = mem::take(&mut self.operands) + .try_into() + .map_err(|_| CalcError::MissingIfElseBlocks)?; + Operator::IfElse(then, otherwise) } - } - i = i.checked_add(1).ok_or(CalcError::TokenIndexOverflow)?; + word if self.operands.is_empty() => Operator::try_from(word)?, + _ => return Err(CalcError::UnexpectedProcedure), + }; + self.ops.push(op); + Ok(()) } - block_stack.pop().ok_or(CalcError::EmptyBlockStack) + /// Returns the operators of a procedure whose closing `}` was reached. + fn finish(self) -> Result, CalcError> { + if self.operands.is_empty() { + Ok(self.ops) + } else { + Err(CalcError::UnexpectedProcedure) + } + } } -fn parse_number(token: &str) -> Result { - if token.contains('.') || token.contains('e') || token.contains('E') { - let value = token - .parse::() - .map_err(|_| CalcError::InvalidNumber(token.to_string()))?; - if value.is_finite() { - Ok(Value::Real(value)) - } else { - Err(CalcError::InvalidNumber(token.to_string())) +/// Parses a Type 4 function program into the operators of its outer procedure. +/// +/// The program is one `{ ... }` procedure, optionally surrounded by whitespace +/// and `%` comments. Nested procedures are only allowed as the operands of +/// `if` and `ifelse`, which store them as `Operator::If` / `Operator::IfElse`. +/// Nesting is tracked with an explicit stack, so deep nesting cannot overflow +/// the native stack. +pub fn parse(program: &[u8]) -> Result, CalcError> { + let mut tokens = Lexer { bytes: program }; + if !matches!(tokens.next().transpose()?, Some(Token::ProcBegin)) { + return Err(CalcError::MissingProgramProcedure); + } + + let mut procedures = vec![Procedure::default()]; + while let Some(token) = tokens.next().transpose()? { + match token { + Token::ProcBegin => procedures.push(Procedure::default()), + Token::Word(word) => procedures + .last_mut() + .ok_or(CalcError::UnbalancedBraces)? + .push_word(word)?, + Token::ProcEnd => { + let finished = procedures + .pop() + .ok_or(CalcError::UnbalancedBraces)? + .finish()?; + match procedures.last_mut() { + Some(parent) => parent.operands.push(finished), + // The outer procedure closed; only whitespace and comments may follow. + None if tokens.next().is_none() => return Ok(finished), + None => return Err(CalcError::UnbalancedBraces), + } + } } - } else { - token - .parse::() - .map(Value::Integer) - .map_err(|_| CalcError::InvalidNumber(token.to_string())) } + + Err(CalcError::UnbalancedBraces) } diff --git a/crates/pdf-postscript/src/value.rs b/crates/pdf-postscript/src/value.rs index b9f0d045..8d68df9b 100644 --- a/crates/pdf-postscript/src/value.rs +++ b/crates/pdf-postscript/src/value.rs @@ -59,3 +59,41 @@ impl Value { } } } + +/// Parses a literal word of a Type 4 program: `true`, `false`, a radix integer +/// (`base#digits`), a decimal integer, or a real. An integer outside the 32-bit +/// range reads as a real, as in PostScript. +impl TryFrom<&[u8]> for Value { + type Error = CalcError; + + fn try_from(word: &[u8]) -> Result { + let invalid = || CalcError::InvalidNumber(String::from_utf8_lossy(word).into_owned()); + let text = core::str::from_utf8(word).map_err(|_| invalid())?; + match text { + "true" => return Ok(Self::Bool(true)), + "false" => return Ok(Self::Bool(false)), + _ => {} + } + if let Some((base, digits)) = text.split_once('#') { + let base = base + .parse::() + .ok() + .filter(|base| (2..=36).contains(base)) + .ok_or_else(invalid)?; + // The digits are an unsigned 32-bit pattern, so `16#FFFFFFFF` reads as -1. + return u32::from_str_radix(digits, base) + .ok() + .filter(|_| !digits.starts_with(['+', '-'])) + .map(|value| Self::Integer(value.cast_signed())) + .ok_or_else(invalid); + } + if let Ok(value) = text.parse::() { + return Ok(Self::Integer(value)); + } + text.parse::() + .ok() + .filter(|value| value.is_finite()) + .map(Self::Real) + .ok_or_else(invalid) + } +}