Pārlūkot izejas kodu

feat(expressions): add parameterized expression support with $N syntax

Add parse_with_params() method to ExpressionParser that accepts variadic
Element parameters. Parameters are referenced using $0, $1, $2, etc.
placeholders in expression strings.

This enables creating reusable expression templates with bound values
that are resolved at parse time rather than evaluation time.

- Add PARAMETER token type to ExpressionTokenizer
- Add ParameterExpression class for parameter placeholders
- Add visit_parameter to ExpressionVisitor
- Update ExpressionType enum with PARAMETER type
- Add comprehensive tests for parameter usage scenarios
Billy Barrow 1 mēnesi atpakaļ
vecāks
revīzija
dc782a4e7e

+ 44 - 1
src/lib/Expressions/ExpressionParser.vala

@@ -32,6 +32,7 @@ namespace Invercargill.Expressions {
 
         private Token[] _tokens;
         private int _position;
+        private Element[] _params;
 
         /**
          * Creates a new parser for the given token stream.
@@ -41,11 +42,12 @@ namespace Invercargill.Expressions {
         public ExpressionParser(Series<Token> tokens) {
             _tokens = tokens.to_array();
             _position = 0;
+            _params = new Element[0];
         }
 
         /**
          * Parses an expression string and returns the expression tree.
-         * 
+         *
          * @param input The expression string to parse
          * @return The root of the expression tree
          * @throws ExpressionError if parsing fails
@@ -57,6 +59,33 @@ namespace Invercargill.Expressions {
             return parser.parse_expression();
         }
 
+        /**
+         * Parses an expression string with positional parameters.
+         *
+         * Parameters are referenced using $0, $1, $2, etc. syntax.
+         *
+         * @param input The expression string to parse
+         * @param ... Element values for $0, $1, $2, etc. (variadic)
+         * @return The root of the expression tree
+         * @throws ExpressionError if parsing fails or parameter index is out of range
+         */
+        public static Expression parse_with_params(string input, ...) throws ExpressionError {
+            var tokenizer = new ExpressionTokenizer(input);
+            var tokens = tokenizer.tokenize_all();
+            var parser = new ExpressionParser(tokens);
+            
+            // Extract variadic arguments
+            var va_list = va_list();
+            var params_list = new Series<Element>();
+            Element? param;
+            while ((param = va_list.arg<Element?>()) != null) {
+                params_list.add(param);
+            }
+            parser._params = params_list.to_array();
+            
+            return parser.parse_expression();
+        }
+
         /**
          * Parses the token stream and returns the expression tree.
          * 
@@ -356,6 +385,20 @@ namespace Invercargill.Expressions {
                 return new LiteralExpression(new NullElement());
             }
 
+            // Parameter placeholder: $0, $1, etc.
+            if (match(TokenType.PARAMETER)) {
+                var token = previous();
+                int index = int.parse(token.value);
+                
+                if (index < 0 || index >= _params.length) {
+                    throw new ExpressionError.INVALID_SYNTAX(
+                        @"Parameter index $$index out of range"
+                    );
+                }
+                
+                return new ParameterExpression(index, _params[index]);
+            }
+
             // Variable or standalone function call
             if (match(TokenType.IDENTIFIER)) {
                 var token = previous();

+ 28 - 0
src/lib/Expressions/ExpressionTokenizer.vala

@@ -17,6 +17,9 @@ namespace Invercargill.Expressions {
         // Identifiers and keywords
         IDENTIFIER,
 
+        // Parameter placeholder
+        PARAMETER,  // $0, $1, $2, etc.
+
         // Operators
         PLUS,           // +
         MINUS,          // -
@@ -57,6 +60,7 @@ namespace Invercargill.Expressions {
                 case FALSE: return "FALSE";
                 case NULL_LITERAL: return "NULL";
                 case IDENTIFIER: return "IDENTIFIER";
+                case PARAMETER: return "PARAMETER";
                 case PLUS: return "+";
                 case MINUS: return "-";
                 case STAR: return "*";
@@ -258,6 +262,11 @@ namespace Invercargill.Expressions {
                 return new Token(TokenType.MINUS, "-", start_pos);
             }
 
+            // Parameter placeholder ($0, $1, etc.)
+            if (c == '$') {
+                return read_parameter();
+            }
+
             // String literals
             if (c == '"' || c == '\'') {
                 return read_string(c);
@@ -393,6 +402,25 @@ namespace Invercargill.Expressions {
                     return new Token(TokenType.IDENTIFIER, value, start_pos);
             }
         }
+
+        private Token read_parameter() throws ExpressionError {
+            int start_pos = _position;
+            _position++; // Skip $
+
+            if (_position >= _length || !_input[_position].isdigit()) {
+                throw new ExpressionError.INVALID_SYNTAX(
+                    @"Expected digit after '$$' at position $start_pos"
+                );
+            }
+
+            var sb = new StringBuilder();
+            while (_position < _length && _input[_position].isdigit()) {
+                sb.append_c(_input[_position]);
+                _position++;
+            }
+
+            return new Token(TokenType.PARAMETER, sb.str, start_pos);
+        }
     }
 
 }

+ 2 - 0
src/lib/Expressions/ExpressionType.vala

@@ -7,6 +7,7 @@ namespace Invercargill.Expressions {
     public enum ExpressionType {
         LITERAL,
         VARIABLE,
+        PARAMETER,
         BINARY,
         UNARY,
         TERNARY,
@@ -20,6 +21,7 @@ namespace Invercargill.Expressions {
             switch (this) {
                 case LITERAL: return "Literal";
                 case VARIABLE: return "Variable";
+                case PARAMETER: return "Parameter";
                 case BINARY: return "Binary";
                 case UNARY: return "Unary";
                 case TERNARY: return "Ternary";

+ 5 - 0
src/lib/Expressions/ExpressionVisitor.vala

@@ -25,6 +25,11 @@ namespace Invercargill.Expressions {
          */
         public virtual void visit_variable(VariableExpression expr) {}
 
+        /**
+         * Visit a parameter expression.
+         */
+        public virtual void visit_parameter(ParameterExpression expr) {}
+
         /**
          * Visit a binary expression.
          */

+ 52 - 0
src/lib/Expressions/Expressions/ParameterExpression.vala

@@ -0,0 +1,52 @@
+
+namespace Invercargill.Expressions {
+
+    /**
+     * Expression that references a positional parameter.
+     * 
+     * Parameter expressions represent values passed at parse time
+     * using positional placeholders like $0, $1, etc.
+     */
+    public class ParameterExpression : Object, Expression {
+
+        public ExpressionType expression_type { 
+            get { return ExpressionType.PARAMETER; } 
+        }
+
+        /**
+         * The zero-based index of the parameter.
+         */
+        public int index { get; construct set; }
+
+        /**
+         * The element value bound to this parameter.
+         */
+        public Element value { get; construct set; }
+
+        /**
+         * Creates a new parameter expression.
+         * 
+         * @param index The zero-based parameter index
+         * @param value The Element value for this parameter
+         */
+        public ParameterExpression(int index, Element value) {
+            Object(index: index, value: value);
+        }
+
+        public Element evaluate(EvaluationContext context) throws ExpressionError {
+            return value;
+        }
+
+        public void accept(ExpressionVisitor visitor) {
+            visitor.visit_parameter(this);
+        }
+
+        public override Type? get_return_type() {
+            return value.type();
+        }
+
+        public override string to_expression_string() {
+            return @"$$index";
+        }
+    }
+}

+ 1 - 0
src/lib/meson.build

@@ -168,6 +168,7 @@ sources += files('Expressions/StringFunctionAccessor.vala')
 
 sources += files('Expressions/Expressions/LiteralExpression.vala')
 sources += files('Expressions/Expressions/VariableExpression.vala')
+sources += files('Expressions/Expressions/ParameterExpression.vala')
 sources += files('Expressions/Expressions/BinaryExpression.vala')
 sources += files('Expressions/Expressions/UnaryExpression.vala')
 sources += files('Expressions/Expressions/TernaryExpression.vala')

+ 135 - 0
src/tests/Integration/Expressions.vala

@@ -2169,4 +2169,139 @@ void expression_tests() {
         // random number instead of "3"
         assert(value == "Length: 3");
     });
+
+    // ==================== Parameterized Expression Tests ====================
+
+    Test.add_func("/invercargill/expressions/parameter_single", () => {
+        // Single parameter: $0 > 5
+        var expr = ExpressionParser.parse_with_params("$0 > 5",
+            new NativeElement<int>(10)
+        );
+        var context = new EvaluationContext(new PropertyDictionary());
+        var result = expr.evaluate(context);
+        
+        bool value;
+        assert(result.try_get_as<bool>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_multiple", () => {
+        // Multiple parameters: $0 + $1
+        var expr = ExpressionParser.parse_with_params("$0 + $1",
+            new NativeElement<int>(3),
+            new NativeElement<int>(4)
+        );
+        var context = new EvaluationContext(new PropertyDictionary());
+        var result = expr.evaluate(context);
+        
+        int64? value;
+        assert(result.try_get_as<int64?>(out value));
+        assert(value == 7);
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_with_variable", () => {
+        // Parameter with variable: x > $0
+        var expr = ExpressionParser.parse_with_params("x > $0",
+            new NativeElement<int>(5)
+        );
+        var props = new PropertyDictionary();
+        props.set("x", new NativeElement<int>(10));
+        var context = new EvaluationContext(props);
+        var result = expr.evaluate(context);
+        
+        bool value;
+        assert(result.try_get_as<bool>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_string", () => {
+        // String parameter: $0 == "hello"
+        var expr = ExpressionParser.parse_with_params("$0 == \"hello\"",
+            new NativeElement<string>("hello")
+        );
+        var context = new EvaluationContext(new PropertyDictionary());
+        var result = expr.evaluate(context);
+        
+        bool value;
+        assert(result.try_get_as<bool>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_complex_object", () => {
+        // Complex object parameter: $0.name
+        var person = new ExprTestPerson("Alice", 30);
+        var expr = ExpressionParser.parse_with_params("$0.name",
+            new NativeElement<ExprTestPerson>(person)
+        );
+        var context = new EvaluationContext(new PropertyDictionary());
+        var result = expr.evaluate(context);
+        
+        string value;
+        assert(result.try_get_as<string>(out value));
+        assert(value == "Alice");
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_in_ternary", () => {
+        // Parameter in ternary: $0 ? "yes" : "no"
+        var expr = ExpressionParser.parse_with_params("$0 ? \"yes\" : \"no\"",
+            new NativeElement<bool>(true)
+        );
+        var context = new EvaluationContext(new PropertyDictionary());
+        var result = expr.evaluate(context);
+        
+        string value;
+        assert(result.try_get_as<string>(out value));
+        assert(value == "yes");
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_in_lambda", () => {
+        // Parameter in lambda: items.where(i => i.rank > $0)
+        var persons = new Series<ExprTestPerson>();
+        persons.add(new ExprTestPerson("Alice", 30, 5));
+        persons.add(new ExprTestPerson("Bob", 25, 1));
+        persons.add(new ExprTestPerson("Charlie", 35, 3));
+        
+        var expr = ExpressionParser.parse_with_params("items.where(i => i.rank > $0)",
+            new NativeElement<int>(2)
+        );
+        var props = new PropertyDictionary();
+        props.set("items", new NativeElement<Enumerable<ExprTestPerson>?>(persons));
+        var context = new EvaluationContext(props);
+        var result = expr.evaluate(context);
+        
+        Elements elements;
+        assert(result.try_get_as<Elements>(out elements));
+        
+        var count = 0;
+        elements.iterate(e => count++);
+        assert(count == 2); // Alice (rank 5) and Charlie (rank 3)
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_range_check", () => {
+        // Range check with two parameters: $0 <= x && x <= $1
+        var expr = ExpressionParser.parse_with_params("$0 <= x && x <= $1",
+            new NativeElement<int>(0),
+            new NativeElement<int>(100)
+        );
+        var props = new PropertyDictionary();
+        props.set("x", new NativeElement<int>(50));
+        var context = new EvaluationContext(props);
+        var result = expr.evaluate(context);
+        
+        bool value;
+        assert(result.try_get_as<bool>(out value));
+        assert(value == true);
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_expression_string", () => {
+        // Test to_expression_string returns $N format
+        var expr = new ParameterExpression(0, new NativeElement<int>(42));
+        assert(expr.to_expression_string() == "$0");
+    });
+
+    Test.add_func("/invercargill/expressions/parameter_expression_type", () => {
+        // Test expression_type is PARAMETER
+        var expr = new ParameterExpression(0, new NativeElement<int>(42));
+        assert(expr.expression_type == ExpressionType.PARAMETER);
+    });
 }