Skip to main content

oxidize_pdf/forms/
javascript_engine.rs

1//! Limited JavaScript engine for form calculations
2//!
3//! This module provides a very limited JavaScript interpreter that only
4//! supports basic arithmetic and field references for security reasons.
5
6use crate::error::PdfError;
7use std::collections::HashMap;
8
9/// Limited JavaScript engine for form calculations
10#[derive(Debug, Clone)]
11pub struct JavaScriptEngine {
12    /// Variables/field values
13    variables: HashMap<String, f64>,
14    /// Reference to calculation engine for field access
15    field_getter: Option<FieldGetter>,
16}
17
18/// Function to get field values
19type FieldGetter = fn(&str) -> Option<f64>;
20
21/// JavaScript token types
22#[derive(Debug, Clone, PartialEq)]
23#[allow(dead_code)]
24enum Token {
25    Number(f64),
26    Identifier(String),
27    Plus,
28    Minus,
29    Multiply,
30    Divide,
31    LeftParen,
32    RightParen,
33    Equals,
34    NotEquals,
35    LessThan,
36    LessThanEquals,
37    GreaterThan,
38    GreaterThanEquals,
39    And,
40    Or,
41    Not,
42    If,
43    Else,
44    Return,
45    Semicolon,
46    Comma,
47    Dot,
48    #[allow(clippy::upper_case_acronyms)]
49    EOF,
50}
51
52/// JavaScript parser for limited expressions
53struct Parser {
54    tokens: Vec<Token>,
55    current: usize,
56}
57
58/// Abstract syntax tree node
59#[derive(Debug, Clone)]
60#[allow(dead_code)]
61enum ASTNode {
62    Number(f64),
63    Identifier(String),
64    BinaryOp {
65        op: BinaryOperator,
66        left: Box<ASTNode>,
67        right: Box<ASTNode>,
68    },
69    UnaryOp {
70        op: UnaryOperator,
71        operand: Box<ASTNode>,
72    },
73    FieldAccess {
74        object: String,
75        field: String,
76    },
77    FunctionCall {
78        name: String,
79        args: Vec<ASTNode>,
80    },
81    Conditional {
82        condition: Box<ASTNode>,
83        then_expr: Box<ASTNode>,
84        else_expr: Option<Box<ASTNode>>,
85    },
86}
87
88#[derive(Debug, Clone, PartialEq)]
89enum BinaryOperator {
90    Add,
91    Subtract,
92    Multiply,
93    Divide,
94    Equals,
95    NotEquals,
96    LessThan,
97    LessThanEquals,
98    GreaterThan,
99    GreaterThanEquals,
100    And,
101    Or,
102}
103
104#[derive(Debug, Clone, PartialEq)]
105enum UnaryOperator {
106    Negate,
107    Not,
108}
109
110#[allow(clippy::derivable_impls)]
111impl Default for JavaScriptEngine {
112    fn default() -> Self {
113        Self {
114            variables: HashMap::new(),
115            field_getter: None,
116        }
117    }
118}
119
120impl JavaScriptEngine {
121    /// Create a new JavaScript engine
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Set a variable value
127    pub fn set_variable(&mut self, name: impl Into<String>, value: f64) {
128        self.variables.insert(name.into(), value);
129    }
130
131    /// Set field getter function
132    pub fn set_field_getter(&mut self, getter: FieldGetter) {
133        self.field_getter = Some(getter);
134    }
135
136    /// Evaluate a JavaScript expression
137    pub fn evaluate(&self, code: &str) -> Result<f64, PdfError> {
138        // Tokenize
139        let tokens = self.tokenize(code)?;
140
141        // Parse
142        let mut parser = Parser::new(tokens);
143        let ast = parser.parse()?;
144
145        // Evaluate
146        self.eval_node(&ast)
147    }
148
149    /// Tokenize JavaScript code
150    fn tokenize(&self, code: &str) -> Result<Vec<Token>, PdfError> {
151        let mut tokens = Vec::new();
152        let mut chars = code.chars().peekable();
153
154        while let Some(ch) = chars.next() {
155            match ch {
156                ' ' | '\t' | '\n' | '\r' => continue,
157                '+' => tokens.push(Token::Plus),
158                '-' => tokens.push(Token::Minus),
159                '*' => tokens.push(Token::Multiply),
160                '/' => {
161                    if chars.peek() == Some(&'/') {
162                        // Skip line comment
163                        chars.next();
164                        for c in chars.by_ref() {
165                            if c == '\n' {
166                                break;
167                            }
168                        }
169                    } else {
170                        tokens.push(Token::Divide);
171                    }
172                }
173                '(' => tokens.push(Token::LeftParen),
174                ')' => tokens.push(Token::RightParen),
175                '=' => {
176                    if chars.peek() == Some(&'=') {
177                        chars.next();
178                        tokens.push(Token::Equals);
179                    }
180                }
181                '!' => {
182                    if chars.peek() == Some(&'=') {
183                        chars.next();
184                        tokens.push(Token::NotEquals);
185                    } else {
186                        tokens.push(Token::Not);
187                    }
188                }
189                '<' => {
190                    if chars.peek() == Some(&'=') {
191                        chars.next();
192                        tokens.push(Token::LessThanEquals);
193                    } else {
194                        tokens.push(Token::LessThan);
195                    }
196                }
197                '>' => {
198                    if chars.peek() == Some(&'=') {
199                        chars.next();
200                        tokens.push(Token::GreaterThanEquals);
201                    } else {
202                        tokens.push(Token::GreaterThan);
203                    }
204                }
205                '&' => {
206                    if chars.peek() == Some(&'&') {
207                        chars.next();
208                        tokens.push(Token::And);
209                    }
210                }
211                '|' => {
212                    if chars.peek() == Some(&'|') {
213                        chars.next();
214                        tokens.push(Token::Or);
215                    }
216                }
217                ';' => tokens.push(Token::Semicolon),
218                ',' => tokens.push(Token::Comma),
219                '.' => tokens.push(Token::Dot),
220                '0'..='9' => {
221                    let mut num_str = String::new();
222                    num_str.push(ch);
223                    while let Some(&next_ch) = chars.peek() {
224                        if next_ch.is_ascii_digit() || next_ch == '.' {
225                            num_str.push(chars.next().ok_or_else(|| {
226                                PdfError::InvalidFormat(
227                                    "Unexpected end of number literal".to_string(),
228                                )
229                            })?);
230                        } else {
231                            break;
232                        }
233                    }
234                    let num = num_str
235                        .parse::<f64>()
236                        .map_err(|_| PdfError::InvalidFormat("Invalid number".to_string()))?;
237                    tokens.push(Token::Number(num));
238                }
239                'a'..='z' | 'A'..='Z' | '_' => {
240                    let mut ident = String::new();
241                    ident.push(ch);
242                    while let Some(&next_ch) = chars.peek() {
243                        if next_ch.is_alphanumeric() || next_ch == '_' {
244                            ident.push(chars.next().ok_or_else(|| {
245                                PdfError::InvalidFormat("Unexpected end of identifier".to_string())
246                            })?);
247                        } else {
248                            break;
249                        }
250                    }
251
252                    // Check for keywords
253                    let token = match ident.as_str() {
254                        "if" => Token::If,
255                        "else" => Token::Else,
256                        "return" => Token::Return,
257                        _ => Token::Identifier(ident),
258                    };
259                    tokens.push(token);
260                }
261                _ => {
262                    // Ignore other characters
263                }
264            }
265        }
266
267        tokens.push(Token::EOF);
268        Ok(tokens)
269    }
270
271    /// Evaluate an AST node
272    fn eval_node(&self, node: &ASTNode) -> Result<f64, PdfError> {
273        match node {
274            ASTNode::Number(n) => Ok(*n),
275            ASTNode::Identifier(name) => {
276                // Try variables first
277                if let Some(&value) = self.variables.get(name) {
278                    return Ok(value);
279                }
280
281                // Try field getter
282                if let Some(getter) = self.field_getter {
283                    if let Some(value) = getter(name) {
284                        return Ok(value);
285                    }
286                }
287
288                // Default to 0
289                Ok(0.0)
290            }
291            ASTNode::BinaryOp { op, left, right } => {
292                let left_val = self.eval_node(left)?;
293                let right_val = self.eval_node(right)?;
294
295                match op {
296                    BinaryOperator::Add => Ok(left_val + right_val),
297                    BinaryOperator::Subtract => Ok(left_val - right_val),
298                    BinaryOperator::Multiply => Ok(left_val * right_val),
299                    BinaryOperator::Divide => {
300                        if right_val != 0.0 {
301                            Ok(left_val / right_val)
302                        } else {
303                            Ok(0.0)
304                        }
305                    }
306                    BinaryOperator::Equals => Ok(if left_val == right_val { 1.0 } else { 0.0 }),
307                    BinaryOperator::NotEquals => Ok(if left_val != right_val { 1.0 } else { 0.0 }),
308                    BinaryOperator::LessThan => Ok(if left_val < right_val { 1.0 } else { 0.0 }),
309                    BinaryOperator::LessThanEquals => {
310                        Ok(if left_val <= right_val { 1.0 } else { 0.0 })
311                    }
312                    BinaryOperator::GreaterThan => Ok(if left_val > right_val { 1.0 } else { 0.0 }),
313                    BinaryOperator::GreaterThanEquals => {
314                        Ok(if left_val >= right_val { 1.0 } else { 0.0 })
315                    }
316                    BinaryOperator::And => Ok(if left_val != 0.0 && right_val != 0.0 {
317                        1.0
318                    } else {
319                        0.0
320                    }),
321                    BinaryOperator::Or => Ok(if left_val != 0.0 || right_val != 0.0 {
322                        1.0
323                    } else {
324                        0.0
325                    }),
326                }
327            }
328            ASTNode::UnaryOp { op, operand } => {
329                let val = self.eval_node(operand)?;
330                match op {
331                    UnaryOperator::Negate => Ok(-val),
332                    UnaryOperator::Not => Ok(if val == 0.0 { 1.0 } else { 0.0 }),
333                }
334            }
335            ASTNode::FieldAccess { object, field } => {
336                // For "this.field" access
337                if object == "this" {
338                    if let Some(getter) = self.field_getter {
339                        if let Some(value) = getter(field) {
340                            return Ok(value);
341                        }
342                    }
343                }
344                Ok(0.0)
345            }
346            ASTNode::FunctionCall { name, args } => {
347                // Support basic math functions
348                match name.as_str() {
349                    "Math.min" => {
350                        let values: Result<Vec<f64>, _> =
351                            args.iter().map(|arg| self.eval_node(arg)).collect();
352                        let values = values?;
353                        Ok(values.iter().cloned().fold(f64::INFINITY, f64::min))
354                    }
355                    "Math.max" => {
356                        let values: Result<Vec<f64>, _> =
357                            args.iter().map(|arg| self.eval_node(arg)).collect();
358                        let values = values?;
359                        Ok(values.iter().cloned().fold(f64::NEG_INFINITY, f64::max))
360                    }
361                    "Math.round" => {
362                        if let Some(arg) = args.first() {
363                            Ok(self.eval_node(arg)?.round())
364                        } else {
365                            Ok(0.0)
366                        }
367                    }
368                    "Math.floor" => {
369                        if let Some(arg) = args.first() {
370                            Ok(self.eval_node(arg)?.floor())
371                        } else {
372                            Ok(0.0)
373                        }
374                    }
375                    "Math.ceil" => {
376                        if let Some(arg) = args.first() {
377                            Ok(self.eval_node(arg)?.ceil())
378                        } else {
379                            Ok(0.0)
380                        }
381                    }
382                    "Math.abs" => {
383                        if let Some(arg) = args.first() {
384                            Ok(self.eval_node(arg)?.abs())
385                        } else {
386                            Ok(0.0)
387                        }
388                    }
389                    _ => Ok(0.0),
390                }
391            }
392            ASTNode::Conditional {
393                condition,
394                then_expr,
395                else_expr,
396            } => {
397                let cond_val = self.eval_node(condition)?;
398                if cond_val != 0.0 {
399                    self.eval_node(then_expr)
400                } else if let Some(else_expr) = else_expr {
401                    self.eval_node(else_expr)
402                } else {
403                    Ok(0.0)
404                }
405            }
406        }
407    }
408}
409
410impl Parser {
411    fn new(tokens: Vec<Token>) -> Self {
412        Self { tokens, current: 0 }
413    }
414
415    fn parse(&mut self) -> Result<ASTNode, PdfError> {
416        self.parse_expression()
417    }
418
419    fn parse_expression(&mut self) -> Result<ASTNode, PdfError> {
420        self.parse_conditional()
421    }
422
423    fn parse_conditional(&mut self) -> Result<ASTNode, PdfError> {
424        let expr = self.parse_logical_or()?;
425
426        // Check for ternary conditional (? :)
427        // For simplicity, we'll skip this for now
428
429        Ok(expr)
430    }
431
432    fn parse_logical_or(&mut self) -> Result<ASTNode, PdfError> {
433        let mut left = self.parse_logical_and()?;
434
435        while self.current_token() == Some(&Token::Or) {
436            self.advance();
437            let right = self.parse_logical_and()?;
438            left = ASTNode::BinaryOp {
439                op: BinaryOperator::Or,
440                left: Box::new(left),
441                right: Box::new(right),
442            };
443        }
444
445        Ok(left)
446    }
447
448    fn parse_logical_and(&mut self) -> Result<ASTNode, PdfError> {
449        let mut left = self.parse_equality()?;
450
451        while self.current_token() == Some(&Token::And) {
452            self.advance();
453            let right = self.parse_equality()?;
454            left = ASTNode::BinaryOp {
455                op: BinaryOperator::And,
456                left: Box::new(left),
457                right: Box::new(right),
458            };
459        }
460
461        Ok(left)
462    }
463
464    fn parse_equality(&mut self) -> Result<ASTNode, PdfError> {
465        let mut left = self.parse_relational()?;
466
467        while let Some(token) = self.current_token() {
468            let op = match token {
469                Token::Equals => BinaryOperator::Equals,
470                Token::NotEquals => BinaryOperator::NotEquals,
471                _ => break,
472            };
473
474            self.advance();
475            let right = self.parse_relational()?;
476            left = ASTNode::BinaryOp {
477                op,
478                left: Box::new(left),
479                right: Box::new(right),
480            };
481        }
482
483        Ok(left)
484    }
485
486    fn parse_relational(&mut self) -> Result<ASTNode, PdfError> {
487        let mut left = self.parse_additive()?;
488
489        while let Some(token) = self.current_token() {
490            let op = match token {
491                Token::LessThan => BinaryOperator::LessThan,
492                Token::LessThanEquals => BinaryOperator::LessThanEquals,
493                Token::GreaterThan => BinaryOperator::GreaterThan,
494                Token::GreaterThanEquals => BinaryOperator::GreaterThanEquals,
495                _ => break,
496            };
497
498            self.advance();
499            let right = self.parse_additive()?;
500            left = ASTNode::BinaryOp {
501                op,
502                left: Box::new(left),
503                right: Box::new(right),
504            };
505        }
506
507        Ok(left)
508    }
509
510    fn parse_additive(&mut self) -> Result<ASTNode, PdfError> {
511        let mut left = self.parse_multiplicative()?;
512
513        while let Some(token) = self.current_token() {
514            let op = match token {
515                Token::Plus => BinaryOperator::Add,
516                Token::Minus => BinaryOperator::Subtract,
517                _ => break,
518            };
519
520            self.advance();
521            let right = self.parse_multiplicative()?;
522            left = ASTNode::BinaryOp {
523                op,
524                left: Box::new(left),
525                right: Box::new(right),
526            };
527        }
528
529        Ok(left)
530    }
531
532    fn parse_multiplicative(&mut self) -> Result<ASTNode, PdfError> {
533        let mut left = self.parse_unary()?;
534
535        while let Some(token) = self.current_token() {
536            let op = match token {
537                Token::Multiply => BinaryOperator::Multiply,
538                Token::Divide => BinaryOperator::Divide,
539                _ => break,
540            };
541
542            self.advance();
543            let right = self.parse_unary()?;
544            left = ASTNode::BinaryOp {
545                op,
546                left: Box::new(left),
547                right: Box::new(right),
548            };
549        }
550
551        Ok(left)
552    }
553
554    fn parse_unary(&mut self) -> Result<ASTNode, PdfError> {
555        if let Some(token) = self.current_token() {
556            match token {
557                Token::Minus => {
558                    self.advance();
559                    let operand = self.parse_unary()?;
560                    return Ok(ASTNode::UnaryOp {
561                        op: UnaryOperator::Negate,
562                        operand: Box::new(operand),
563                    });
564                }
565                Token::Not => {
566                    self.advance();
567                    let operand = self.parse_unary()?;
568                    return Ok(ASTNode::UnaryOp {
569                        op: UnaryOperator::Not,
570                        operand: Box::new(operand),
571                    });
572                }
573                _ => {}
574            }
575        }
576
577        self.parse_primary()
578    }
579
580    fn parse_primary(&mut self) -> Result<ASTNode, PdfError> {
581        if let Some(token) = self.current_token().cloned() {
582            match token {
583                Token::Number(n) => {
584                    self.advance();
585                    Ok(ASTNode::Number(n))
586                }
587                Token::Identifier(name) => {
588                    self.advance();
589
590                    // Check for field access or function call
591                    if self.current_token() == Some(&Token::Dot) {
592                        self.advance();
593                        if let Some(Token::Identifier(field)) = self.current_token().cloned() {
594                            self.advance();
595
596                            // Check for function call
597                            if self.current_token() == Some(&Token::LeftParen) {
598                                self.advance();
599                                let args = self.parse_arguments()?;
600                                self.expect(Token::RightParen)?;
601                                return Ok(ASTNode::FunctionCall {
602                                    name: format!("{}.{}", name, field),
603                                    args,
604                                });
605                            } else {
606                                return Ok(ASTNode::FieldAccess {
607                                    object: name,
608                                    field,
609                                });
610                            }
611                        }
612                    }
613
614                    Ok(ASTNode::Identifier(name))
615                }
616                Token::LeftParen => {
617                    self.advance();
618                    let expr = self.parse_expression()?;
619                    self.expect(Token::RightParen)?;
620                    Ok(expr)
621                }
622                _ => Err(PdfError::InvalidFormat("Unexpected token".to_string())),
623            }
624        } else {
625            Err(PdfError::InvalidFormat(
626                "Unexpected end of input".to_string(),
627            ))
628        }
629    }
630
631    fn parse_arguments(&mut self) -> Result<Vec<ASTNode>, PdfError> {
632        let mut args = Vec::new();
633
634        if self.current_token() != Some(&Token::RightParen) {
635            loop {
636                args.push(self.parse_expression()?);
637
638                if self.current_token() == Some(&Token::Comma) {
639                    self.advance();
640                } else {
641                    break;
642                }
643            }
644        }
645
646        Ok(args)
647    }
648
649    fn current_token(&self) -> Option<&Token> {
650        self.tokens.get(self.current)
651    }
652
653    fn advance(&mut self) {
654        self.current += 1;
655    }
656
657    fn expect(&mut self, expected: Token) -> Result<(), PdfError> {
658        if self.current_token() == Some(&expected) {
659            self.advance();
660            Ok(())
661        } else {
662            Err(PdfError::InvalidFormat(format!(
663                "Expected {:?}, got {:?}",
664                expected,
665                self.current_token()
666            )))
667        }
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    #[test]
676    fn test_simple_arithmetic() {
677        let engine = JavaScriptEngine::new();
678
679        assert_eq!(engine.evaluate("2 + 3").unwrap(), 5.0);
680        assert_eq!(engine.evaluate("10 - 4").unwrap(), 6.0);
681        assert_eq!(engine.evaluate("3 * 4").unwrap(), 12.0);
682        assert_eq!(engine.evaluate("15 / 3").unwrap(), 5.0);
683    }
684
685    #[test]
686    fn test_parentheses() {
687        let engine = JavaScriptEngine::new();
688
689        assert_eq!(engine.evaluate("2 * (3 + 4)").unwrap(), 14.0);
690        assert_eq!(engine.evaluate("(10 - 2) / 4").unwrap(), 2.0);
691    }
692
693    #[test]
694    fn test_variables() {
695        let mut engine = JavaScriptEngine::new();
696        engine.set_variable("x", 10.0);
697        engine.set_variable("y", 5.0);
698
699        assert_eq!(engine.evaluate("x + y").unwrap(), 15.0);
700        assert_eq!(engine.evaluate("x * 2 - y").unwrap(), 15.0);
701    }
702
703    #[test]
704    fn test_comparison() {
705        let engine = JavaScriptEngine::new();
706
707        assert_eq!(engine.evaluate("5 > 3").unwrap(), 1.0);
708        assert_eq!(engine.evaluate("2 < 1").unwrap(), 0.0);
709        assert_eq!(engine.evaluate("3 == 3").unwrap(), 1.0);
710        assert_eq!(engine.evaluate("4 != 4").unwrap(), 0.0);
711    }
712
713    #[test]
714    fn test_logical_operators() {
715        let engine = JavaScriptEngine::new();
716
717        assert_eq!(engine.evaluate("1 && 1").unwrap(), 1.0);
718        assert_eq!(engine.evaluate("1 && 0").unwrap(), 0.0);
719        assert_eq!(engine.evaluate("0 || 1").unwrap(), 1.0);
720        assert_eq!(engine.evaluate("0 || 0").unwrap(), 0.0);
721    }
722
723    #[test]
724    fn test_math_functions() {
725        let engine = JavaScriptEngine::new();
726
727        assert_eq!(engine.evaluate("Math.min(5, 3, 7)").unwrap(), 3.0);
728        assert_eq!(engine.evaluate("Math.max(5, 3, 7)").unwrap(), 7.0);
729        assert_eq!(engine.evaluate("Math.round(3.7)").unwrap(), 4.0);
730        assert_eq!(engine.evaluate("Math.floor(3.7)").unwrap(), 3.0);
731        assert_eq!(engine.evaluate("Math.ceil(3.2)").unwrap(), 4.0);
732        assert_eq!(engine.evaluate("Math.abs(-5)").unwrap(), 5.0);
733    }
734
735    // =============================================================================
736    // RIGOROUS TESTS FOR UNCOVERED EDGE CASES
737    // =============================================================================
738
739    #[test]
740    fn test_division_by_zero() {
741        let engine = JavaScriptEngine::new();
742
743        // Division by zero should return 0.0, not NaN or panic
744        let result = engine
745            .evaluate("10 / 0")
746            .expect("Division by zero must not panic");
747        assert_eq!(result, 0.0, "Division by zero must return 0.0");
748
749        // More complex expression with division by zero
750        let result = engine
751            .evaluate("(5 + 5) / (2 - 2)")
752            .expect("Must handle division by zero");
753        assert_eq!(result, 0.0);
754    }
755
756    #[test]
757    fn test_unary_minus_operator() {
758        let engine = JavaScriptEngine::new();
759
760        // Simple negation
761        assert_eq!(
762            engine.evaluate("-5").unwrap(),
763            -5.0,
764            "Unary minus must negate"
765        );
766
767        // Negation of expression
768        assert_eq!(engine.evaluate("-(3 + 2)").unwrap(), -5.0);
769
770        // Double negation
771        assert_eq!(
772            engine.evaluate("--7").unwrap(),
773            7.0,
774            "Double negation must cancel out"
775        );
776
777        // Negation in complex expression
778        assert_eq!(engine.evaluate("10 + -5").unwrap(), 5.0);
779    }
780
781    #[test]
782    fn test_unary_not_operator() {
783        let engine = JavaScriptEngine::new();
784
785        // NOT of zero (falsy) -> 1.0 (truthy)
786        assert_eq!(engine.evaluate("!0").unwrap(), 1.0, "NOT of 0 must be 1");
787
788        // NOT of non-zero (truthy) -> 0.0 (falsy)
789        assert_eq!(engine.evaluate("!5").unwrap(), 0.0, "NOT of 5 must be 0");
790
791        // Double NOT
792        assert_eq!(
793            engine.evaluate("!!10").unwrap(),
794            1.0,
795            "Double NOT of truthy must be 1"
796        );
797
798        // NOT in expression
799        assert_eq!(engine.evaluate("!0 && 1").unwrap(), 1.0);
800    }
801
802    #[test]
803    fn test_line_comments() {
804        let engine = JavaScriptEngine::new();
805
806        // Single line comment
807        let result = engine
808            .evaluate("5 + 3 // This is a comment")
809            .expect("Comments must be ignored");
810        assert_eq!(result, 8.0, "Comments must not affect evaluation");
811
812        // Comment in middle of expression (on new line conceptually)
813        let result = engine
814            .evaluate("10 // comment\n * 2")
815            .expect("Comments must be skipped");
816        assert_eq!(result, 20.0);
817    }
818
819    #[test]
820    fn test_field_getter_integration() {
821        let mut engine = JavaScriptEngine::new();
822
823        // Define a field getter function
824        fn test_getter(field_name: &str) -> Option<f64> {
825            match field_name {
826                "price" => Some(100.0),
827                "quantity" => Some(5.0),
828                "tax" => Some(0.08),
829                _ => None,
830            }
831        }
832
833        engine.set_field_getter(test_getter);
834
835        // Test field access
836        assert_eq!(
837            engine.evaluate("price").unwrap(),
838            100.0,
839            "Field getter must resolve field names"
840        );
841
842        assert_eq!(engine.evaluate("price * quantity").unwrap(), 500.0);
843
844        // Test field with addition
845        assert_eq!(engine.evaluate("price * (1 + tax)").unwrap(), 108.0);
846
847        // Non-existent field should return 0.0
848        assert_eq!(
849            engine.evaluate("nonexistent").unwrap(),
850            0.0,
851            "Unknown fields must return 0.0"
852        );
853    }
854
855    #[test]
856    fn test_this_field_access() {
857        let mut engine = JavaScriptEngine::new();
858
859        fn field_getter(field_name: &str) -> Option<f64> {
860            match field_name {
861                "total" => Some(250.0),
862                "discount" => Some(0.10),
863                _ => None,
864            }
865        }
866
867        engine.set_field_getter(field_getter);
868
869        // Test "this.field" syntax
870        // Note: Actual evaluation depends on implementation
871        // For now, testing that it doesn't panic
872        let result = engine.evaluate("this.total");
873        assert!(result.is_ok(), "this.field syntax must be supported");
874    }
875
876    #[test]
877    fn test_math_functions_with_empty_args() {
878        let engine = JavaScriptEngine::new();
879
880        // Math functions with no args should return 0.0 or appropriate defaults
881        let result = engine
882            .evaluate("Math.round()")
883            .expect("Empty args must not panic");
884        assert_eq!(result, 0.0, "Math.round() with no args must return 0.0");
885
886        let result = engine
887            .evaluate("Math.floor()")
888            .expect("Empty args must not panic");
889        assert_eq!(result, 0.0);
890
891        let result = engine
892            .evaluate("Math.ceil()")
893            .expect("Empty args must not panic");
894        assert_eq!(result, 0.0);
895
896        let result = engine
897            .evaluate("Math.abs()")
898            .expect("Empty args must not panic");
899        assert_eq!(result, 0.0);
900    }
901
902    #[test]
903    fn test_math_min_max_with_single_value() {
904        let engine = JavaScriptEngine::new();
905
906        // Min/max with single value
907        assert_eq!(
908            engine.evaluate("Math.min(42)").unwrap(),
909            42.0,
910            "Math.min with single arg must return that value"
911        );
912
913        assert_eq!(engine.evaluate("Math.max(42)").unwrap(), 42.0);
914    }
915
916    #[test]
917    fn test_complex_nested_expressions() {
918        let engine = JavaScriptEngine::new();
919
920        // Deeply nested parentheses
921        let result = engine
922            .evaluate("((((5))))")
923            .expect("Nested parens must work");
924        assert_eq!(result, 5.0);
925
926        // Complex mixed operators with precedence
927        let result = engine.evaluate("2 + 3 * 4 - 5 / 5").unwrap();
928        assert_eq!(
929            result, 13.0,
930            "Operator precedence must be correct: 2 + 12 - 1 = 13"
931        );
932
933        // Nested function calls
934        let result = engine.evaluate("Math.max(Math.min(10, 5), 3)").unwrap();
935        assert_eq!(result, 5.0, "Math.max(5, 3) = 5");
936    }
937
938    #[test]
939    fn test_comparison_with_equals_vs_assignment() {
940        let engine = JavaScriptEngine::new();
941
942        // == should be comparison, not assignment
943        assert_eq!(
944            engine.evaluate("5 == 5").unwrap(),
945            1.0,
946            "== must be comparison returning 1.0 (true)"
947        );
948
949        assert_eq!(
950            engine.evaluate("5 == 3").unwrap(),
951            0.0,
952            "== must return 0.0 (false) for unequal values"
953        );
954
955        // <= and >= operators
956        assert_eq!(engine.evaluate("5 <= 5").unwrap(), 1.0);
957        assert_eq!(engine.evaluate("5 >= 5").unwrap(), 1.0);
958        assert_eq!(engine.evaluate("3 <= 5").unwrap(), 1.0);
959        assert_eq!(engine.evaluate("5 >= 3").unwrap(), 1.0);
960    }
961
962    #[test]
963    fn test_logical_operators_chaining() {
964        let engine = JavaScriptEngine::new();
965
966        // Chained AND operators
967        assert_eq!(
968            engine.evaluate("1 && 1 && 1").unwrap(),
969            1.0,
970            "All truthy values ANDed must return 1.0"
971        );
972
973        assert_eq!(
974            engine.evaluate("1 && 0 && 1").unwrap(),
975            0.0,
976            "Any falsy value ANDed must return 0.0"
977        );
978
979        // Chained OR operators
980        assert_eq!(
981            engine.evaluate("0 || 0 || 1").unwrap(),
982            1.0,
983            "Any truthy value ORed must return 1.0"
984        );
985
986        assert_eq!(engine.evaluate("0 || 0 || 0").unwrap(), 0.0);
987
988        // Mixed AND/OR
989        assert_eq!(
990            engine.evaluate("1 && 1 || 0").unwrap(),
991            1.0,
992            "(1 AND 1) OR 0 = 1"
993        );
994
995        assert_eq!(
996            engine.evaluate("0 || 1 && 1").unwrap(),
997            1.0,
998            "0 OR (1 AND 1) = 1"
999        );
1000    }
1001
1002    #[test]
1003    fn test_whitespace_handling() {
1004        let engine = JavaScriptEngine::new();
1005
1006        // Extra whitespace
1007        assert_eq!(
1008            engine.evaluate("  5   +   3  ").unwrap(),
1009            8.0,
1010            "Extra whitespace must be ignored"
1011        );
1012
1013        // Tabs and newlines
1014        assert_eq!(engine.evaluate("5\t+\n3").unwrap(), 8.0);
1015
1016        // No whitespace
1017        assert_eq!(engine.evaluate("5+3*2").unwrap(), 11.0);
1018    }
1019
1020    #[test]
1021    fn test_decimal_numbers() {
1022        let engine = JavaScriptEngine::new();
1023
1024        // Simple decimal
1025        assert_eq!(engine.evaluate("3.14").unwrap(), 3.14);
1026
1027        // Decimal in expression
1028        assert_eq!(engine.evaluate("10.5 + 2.5").unwrap(), 13.0);
1029
1030        // Multiple decimals in operations
1031        assert_eq!(engine.evaluate("0.1 + 0.2").unwrap(), 0.30000000000000004); // Float precision
1032
1033        // Leading zero
1034        assert_eq!(engine.evaluate("0.5 * 10").unwrap(), 5.0);
1035    }
1036
1037    #[test]
1038    fn test_error_handling_invalid_syntax() {
1039        let engine = JavaScriptEngine::new();
1040
1041        // Missing operand
1042        let result = engine.evaluate("5 +");
1043        assert!(result.is_err(), "Incomplete expression must return error");
1044
1045        // Mismatched parentheses
1046        let result = engine.evaluate("(5 + 3");
1047        assert!(result.is_err(), "Unclosed parenthesis must return error");
1048
1049        // Empty input
1050        let result = engine.evaluate("");
1051        assert!(result.is_err(), "Empty expression must return error");
1052    }
1053
1054    #[test]
1055    fn test_unknown_function() {
1056        let engine = JavaScriptEngine::new();
1057
1058        // Unknown function should return 0.0, not panic
1059        let result = engine
1060            .evaluate("UnknownFunction()")
1061            .expect("Unknown functions must not panic");
1062        assert_eq!(result, 0.0, "Unknown functions must return 0.0");
1063
1064        // Unknown Math function
1065        let result = engine
1066            .evaluate("Math.unknownFunc(5)")
1067            .expect("Must not panic");
1068        assert_eq!(result, 0.0);
1069    }
1070}