Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[3.5.0] - 2025-09-09
Added
Adds milliseconds support to duration system
- New 'ms' unit support in lexer, parser, and evaluator
- Duration.to_milliseconds/1 function for high-precision calculations
- Pattern matching guards for automatic precision selection
- Smart DateTime arithmetic (millisecond vs second precision)
- Comprehensive test coverage with 89 new tests
- Refactors evaluator to use Duration module functions (DRY)
Examples
- 500ms ago, 2s750ms from now
- #2024-01-15T10:30:00.000Z# + 1s500ms
- Automatic precision: ms > 0 triggers millisecond precision
[3.4.0] - 2025-09-09
Added
Durations and relative date/time arithmetic
- New duration literals and relative date expressions (e.g.,
3d ago
,2w from now
,next 1mo
,last 1y
) - Date and DateTime arithmetic using durations (e.g.,
#2024-01-10# + 5d
,#2024-01-15T10:30:00Z# - 2h
) - Grammar additions:
duration
andrelative_date
productions - Full pipeline support (lexer, parser, compiler, evaluator, string visitor) with tests
Examples
Predicator.evaluate("created_at > 3d ago", %{"created_at" => ~U[2024-01-20 00:00:00Z]})
Predicator.evaluate("due_at < 2w from now", %{"due_at" => Date.add(Date.utc_today(), 10)})
Predicator.evaluate("#2024-01-10# + 5d = #2024-01-15#", %{})
Predicator.evaluate("#2024-01-15T10:30:00Z# - 2h < #2024-01-15T10:30:00Z#", %{})
Documentation
- Updated EBNF grammar in docs
- Added AGENTS.md with model-agnostic agent guidance;
CLAUDE.md
now references the same content
[3.3.0] - 2025-08-31
Added
- Depends on Jason library
[3.2.0] - 2025-08-31
Added
Strict Equality Operators
- New Operators: Added
===
(strict equality) and!==
(strict inequality) operators - Type-Safe Comparisons: Strict operators compare both value and type, unlike loose equality
- Round-Trip Preservation: Operators maintain their exact form during parse/decompile cycles
- Complete Pipeline Support: Full lexer, parser, evaluator, and visitor implementation
- Comprehensive Testing: 23 tests covering all aspects of strict equality functionality
Examples
# Strict equality - same type and value required
Predicator.evaluate("5 === 5", %{}) # {:ok, true}
Predicator.evaluate("5 === '5'", %{}) # {:ok, false} - different types
# Strict inequality - true when type or value differs
Predicator.evaluate("5 !== '5'", %{}) # {:ok, true} - different types
Predicator.evaluate("1 !== true", %{}) # {:ok, true} - different types
# Operator distinction preserved
Predicator.parse("x = y") |> elem(1) |> Predicator.decompile() # "x = y"
Predicator.parse("x == y") |> elem(1) |> Predicator.decompile() # "x == y"
Predicator.parse("x === y") |> elem(1) |> Predicator.decompile() # "x === y"
Technical Implementation
- Lexer: Added
:strict_equal
and:strict_ne
token types with proper precedence - Parser: Extended comparison grammar to support strict operators
- Evaluator: Added
STRICT_EQ
andSTRICT_NE
instruction handlers - StringVisitor: Added decompilation support for round-trip accuracy
- Type Safety: Works with all data types including
:undefined
values
[3.1.0] - 2025-08-30
Added
JavaScript-Style Object Literals (Complete Implementation)
- Object Literal Syntax: Full support for JavaScript-style object notation with
{key: value}
syntax - Multiple Key Types: Both identifier keys (
name: "John"
) and string keys ("first name": "John"
) - Nested Objects: Unlimited nesting depth for complex data structures
- All Value Types: Objects support all Predicator value types (strings, numbers, booleans, dates, lists, expressions)
- Object Comparisons: Full equality and inequality operations between objects
- Integration: Seamless compatibility with all existing features (functions, operators, property access)
Object Literal Examples
# Basic object creation
Predicator.evaluate("{}", %{}) # {:ok, %{}}
Predicator.evaluate("{name: \"John\", age: 30}", %{}) # {:ok, %{"name" => "John", "age" => 30}}
# Variable references and expressions
Predicator.evaluate("{user: name, total: price + tax}", %{"name" => "Alice", "price" => 100, "tax" => 10})
# {:ok, %{"user" => "Alice", "total" => 110}}
# Nested objects
Predicator.evaluate("{user: {name: \"Bob\", role: \"admin\"}, active: true}", %{})
# {:ok, %{"user" => %{"name" => "Bob", "role" => "admin"}, "active" => true}}
# String keys for complex property names
Predicator.evaluate("{\"first name\": \"John\", \"last-name\": \"Doe\"}", %{})
# {:ok, %{"first name" => "John", "last-name" => "Doe"}}
# Object comparisons
Predicator.evaluate("{score: 85} == user_data", %{"user_data" => %{"score" => 85}})
# {:ok, true}
Complete Pipeline Support
- Lexer: Added
{
,}
,:
token recognition - Parser: Full object grammar with proper precedence and error handling
- Instructions: Stack-based
object_new
andobject_set
instruction execution - Evaluator: Efficient object construction and comparison operations
- String Visitor: Bidirectional transformation support (AST ↔ string representation)
- Type System: Enhanced type matching for object equality comparisons
Integration Features
- Function Integration: Objects work as function parameters and return values
- Property Access: Objects integrate with dot notation (
obj.property
) and bracket access (obj["key"]
) - Boolean Logic: Objects support all logical operations (AND, OR, NOT)
- Arithmetic: Object properties can contain arithmetic expressions and results
- Date Support: Objects can contain date/datetime literals and date function results
- Custom Functions: Objects work seamlessly with user-defined functions
Quality and Testing
- 886 Total Tests: Comprehensive test coverage including edge cases and integration scenarios
- 91.8% Coverage: High test coverage across all components
- Parser Error Handling: Robust error recovery for malformed object syntax
- Performance Tested: Validated with large objects and repeated evaluations
- Production Ready: Full quality assurance (formatting, linting, type checking)
[3.0.0] - 2025-08-25
Added
Location Expressions for SCXML Assignment Operations (Phase 2 Complete)
- SCXML Location Expressions: Complete implementation of location path resolution for SCXML
<assign>
operations - New API Function:
Predicator.context_location/3
- resolves assignable location paths from expressions - Location Path Resolution: Returns navigation paths like
["user", "name"]
,["items", 0, "property"]
for SCXML assignment targets - Assignment Validation: Distinguishes valid assignment targets (l-values) from computed expressions (r-values)
- Core Module:
Predicator.ContextLocation
with comprehensive location resolution logic and error handling - Structured Error Handling:
Predicator.Errors.LocationError
with detailed error types and context information
Location Expression Examples
# Valid assignment targets resolve to location paths
Predicator.context_location("user.profile.name", %{}) # {:ok, ["user", "profile", "name"]}
Predicator.context_location("items[0]", %{}) # {:ok, ["items", 0]}
Predicator.context_location("data['users'][index]['profile']", %{"index" => 2}) # {:ok, ["data", "users", 2, "profile"]}
# Invalid assignment targets return structured errors
Predicator.context_location("len(name)", %{}) # {:error, %LocationError{type: :not_assignable}}
Predicator.context_location("42", %{}) # {:error, %LocationError{type: :not_assignable}}
Predicator.context_location("score + 1", %{}) # {:error, %LocationError{type: :not_assignable}}
Error Types and Validation
:not_assignable
: Expression cannot be used as assignment target (literals, functions, computed expressions):invalid_node
: Unknown or unsupported AST node type encountered during resolution:undefined_variable
: Variable referenced in bracket key is not defined in evaluation context:invalid_key
: Bracket key is not a valid string or integer type:computed_key
: Computed expressions cannot be used as assignment target keys
Assignable vs Non-Assignable Classifications
- ✅ Valid Assignment Targets: Simple identifiers, property access, bracket access, mixed notation
user
,score
,config.database.host
items[0]
,user['profile']
,data["settings"]
user.settings['theme']
,data['users'][0].profile
- ❌ Invalid Assignment Targets: Literals, function calls, computed expressions
42
,"hello"
,true
,#2024-01-15#
len(name)
,upper(role)
,max(a, b)
score + 1
,items[i + 1]
,score > 85
Technical Implementation
- Full Location Resolution: Recursive resolution of nested property access and bracket access
- Mixed Notation Support: Complete support for expressions like
user.settings['theme']
anddata['users'][0].name
- Variable Key Resolution: Bracket keys can reference context variables for dynamic access patterns
- Context Integration: Uses existing evaluation context for variable key resolution
- Comprehensive Testing: 49 comprehensive tests covering all location resolution scenarios and error cases
Type Coercion and Float Support
- Float Literal Support: Extended lexer to parse floating-point numbers (e.g.,
3.14
,0.5
) - Float Token Type: Added
:float
token type to distinguish from integers - Parser Float Handling: Updated parser to handle float tokens and create appropriate AST nodes
- Arithmetic with Floats: All arithmetic operations now support both integers and floats
- Addition, subtraction, multiplication work seamlessly with mixed numeric types
- Division returns float when needed, integer when evenly divisible
- Modulo remains integer-only as per mathematical conventions
- String Concatenation with
+
Operator: Implemented JavaScript-like type coercion"Hello" + "World"
→"HelloWorld"
(string concatenation)"Count: " + 5
→"Count: 5"
(string + number coercion)42 + " items"
→"42 items"
(number + string coercion)
- Type Coercion Rules:
- Number + Number → Numeric addition (supports mixed int/float)
- String + String → String concatenation
- String + Number → String concatenation (number converted to string)
- Number + String → String concatenation (number converted to string)
- Comparison Enhancements: Numbers of different types (int/float) can be compared
- Unary Minus for Floats: Unary minus operator now works with floating-point numbers
- Error Message Updates: Updated error messages from "integer" to "number" where appropriate
- Comprehensive Testing: Added 28 new tests covering all type coercion scenarios
Changed
Property Access Parsing Architecture Overhaul (Breaking Changes)
- Complete Dot Notation Reimplementation: Transformed from dotted identifiers to proper property access AST nodes
- Lexer Breaking Change: Dots removed from valid identifier characters, now parsed as separate tokens
Parser Grammar Enhancement: Added property access grammar
postfix → primary ( "[" expression "]" | "." IDENTIFIER )*
- New AST Structure: Expressions like
user.email
now parsed as{:property_access, {:identifier, "user"}, "email"}
- Instruction Pipeline: Evaluation generates separate
load
andaccess
instructions instead of singleload
with dotted name - Mixed Notation Support: Enables complex expressions like
user.settings['theme']
anddata['users'][0].profile
Breaking Changes
v3.0.0 - Property Access Parsing Overhaul
This is a major breaking change affecting how dot notation is parsed and evaluated:
⚠️ Context Key Impact: Context keys containing dots (e.g., "user.email"
) will no longer match dot notation expressions (user.email
). The expression user.email
is now parsed as property access requiring nested structure %{"user" => %{"email" => "..."}}
Migration Required:
# BEFORE (v2.2.0 and earlier) - WILL NO LONGER WORK
context = %{"user.email" => "john@example.com"}
Predicator.evaluate("user.email = 'john@example.com'", context) # No longer matches
# AFTER (v3.0.0+) - Use proper nested structures
context = %{"user" => %{"email" => "john@example.com"}}
Predicator.evaluate("user.email = 'john@example.com'", context) # Works correctly
Technical Changes:
- Lexer: Dots no longer valid in identifier characters, parsed as separate
:dot
tokens - Parser: New property access AST nodes
{:property_access, left_node, property}
- Evaluator: New
access
instruction handler, removed dotted identifier support fromload_from_context
- Instructions:
user.email
generates[["load", "user"], ["access", "email"]]
instead of[["load", "user.email"]]
Benefits:
- Enables mixed notation:
user.settings['theme']
,data['users'][0].name
- Supports SCXML location expressions for assignment operations
- Proper property access semantics for complex data structures
- Foundation for advanced SCXML datamodel integration
[2.2.0] - 2025-08-24
Added
Bracket Access and Property Access Enhancement
- Complete Bracket Notation Support: Implemented full bracket access functionality (
obj['key']
,arr[0]
,obj[variable]
) - Parser Extensions: Added postfix parsing for bracket access with recursive chaining support
- Grammar Enhancement: Updated grammar with postfix operations:
unary → postfix
,postfix → primary ( "[" expression "]" )*
- New AST Node Type: Added
{:bracket_access, object, key}
AST node for bracket access expressions - Evaluator Support: Implemented
["bracket_access"]
instruction with comprehensive evaluation logic - Mixed Access Patterns: Full support for chained access like
data['users'][0]['name']
- Array Indexing: Complete array access with bounds checking (
items[0]
,scores[index]
) - Dynamic Key Access: Support for variable and expression-based keys (
obj[key]
,items[i + 1]
) - Type Safety: Comprehensive error handling for invalid key types with structured error messages
- String Visitor Support: Added round-trip string conversion for bracket access expressions
- Comprehensive Testing: Added 12 new parser tests covering all bracket access scenarios
Error Handling Architecture Refactoring
- Modular Error Structure: Refactored monolithic error handling into individual error modules under
lib/predicator/errors/
- Shared Error Utilities: Created
Predicator.Errors
module with common utility functions for consistent error formatting - Individual Error Modules: Split error handling into focused modules:
Predicator.Errors.TypeMismatchError
- Type validation and mismatch errorsPredicator.Errors.EvaluationError
- Runtime evaluation errors (division by zero, insufficient operands)Predicator.Errors.UndefinedVariableError
- Variable access errorsPredicator.Errors.ParseError
- Expression parsing and syntax errors
- Consistent Error Messages: Unified error message formatting across all error types
- Code Quality Improvements: Resolved all credo issues with proper module aliasing and organization
[2.1.0] - 2025-08-24
Added
Arithmetic and Unary Operations (Complete Implementation)
- Full Arithmetic Support: Complete parsing and evaluation pipeline for arithmetic expressions
- Binary operations:
+
(addition),-
(subtraction),*
(multiplication),/
(division),%
(modulo) - Unary operations:
-
(unary minus),!
(unary bang/logical NOT)
- Binary operations:
- Proper Precedence: Mathematical precedence handling (unary → multiplication → addition → equality → comparison)
- Instruction Execution: Stack-based evaluator with 7 new instruction handlers
- Error Handling: Division by zero protection, type checking, comprehensive error messages
- Pattern Matching: Idiomatic Elixir implementation using pattern matching for clean code
[2.0.0] - 2025-08-21
Changed
Custom Function Architecture Overhaul
- Breaking Change: Removed global function registry system in favor of evaluation-time function parameters
- New API: Custom functions now passed via
functions:
option inPredicator.evaluate/3
calls - Function Format: Custom functions use
%{name => {arity, function}}
format where function takes[args], context
and returns{:ok, result}
or{:error, message}
- Thread Safety: Eliminated global state for improved concurrency and thread safety
- Function Merging: SystemFunctions always available with custom functions merged in, allowing overrides
- Simplified Startup: No application-level function registry initialization required
Examples
# Old registry-based approach (removed)
Predicator.register_function("double", 1, fn [n], _context -> {:ok, n * 2} end)
Predicator.evaluate("double(21)", %{})
# New evaluation-time approach
custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
Predicator.evaluate("double(21)", %{}, functions: custom_functions)
# Custom functions can override built-ins
custom_len = %{"len" => {1, fn [_], _context -> {:ok, "custom_result"} end}}
Predicator.evaluate("len('anything')", %{}, functions: custom_len) # {:ok, "custom_result"}
Removed APIs
Predicator.register_function/3
- Usefunctions:
option insteadPredicator.clear_custom_functions/0
- No longer neededPredicator.list_custom_functions/0
- No longer neededPredicator.Functions.Registry
module - Entire registry system removed
Breaking Changes
v2.0.0 - Custom Function Architecture Overhaul
- Removed: Global function registry system (
Predicator.Functions.Registry
module) - Removed:
Predicator.register_function/3
,Predicator.clear_custom_functions/0
,Predicator.list_custom_functions/0
- Changed: Custom functions now passed via
functions:
option inevaluate/3
calls instead of global registration - Benefit: Thread-safe, no global state, per-evaluation function scoping
- Migration: Replace registry calls with function maps passed to
evaluate/3
[1.1.0] - 2025-08-20
Added
Nested Data Structure Access
- Dot Notation Support: Access deeply nested data structures using dot notation syntax
- Enhanced Lexer: Extended identifier tokenization to include dots (
.
) as valid characters - Recursive Context Loading: Added
load_nested_value/2
function for traversing nested maps - Mixed Key Type Support: Works seamlessly with string keys, atom keys, or mixed key types
- Graceful Error Handling: Returns
:undefined
for missing paths or non-map intermediate values - Unlimited Nesting Depth: Support for arbitrarily deep nested structures
Single Quote String Support
- Dual Quote Types: Added support for single-quoted strings (
'hello'
) alongside double-quoted strings ("hello"
) - Quote Type Preservation: Round-trip parsing and decompilation preserves original quote type
- Enhanced Lexer: Extended string tokenization to handle both quote types with proper escaping
- AST Enhancement: New
{:string_literal, value, quote_type}
AST node for quote-aware string handling - Escape Sequences: Full escape sequence support in both quote types (
\'
,\"
,\n
,\t
, etc.)
Breaking Changes
v1.1.0 - Nested Access Parsing
- Changed: Variables containing dots (e.g.,
"user.email"
) now parsed as nested access paths - Impact: Context keys like
"user.profile.name"
will no longer match identifieruser.profile.name
- Solution: Use proper nested data structures instead of flat keys with dots
[1.0.1] - 2025-08-20
Documentation
- Fixes main page for Hex docs
[1.0.0] - 2025-08-19
Added
Core Language Features
- Comparison Operators: Full support for
>
,<
,>=
,<=
,=
,!=
with proper type handling - Logical Operators: Case-insensitive
AND
/and
,OR
/or
,NOT
/not
with correct precedence - Data Types:
- Numbers (integers):
42
,-17
- Strings (double-quoted):
"hello"
,"world"
- Booleans:
true
,false
- Date literals:
#2024-01-15#
(ISO 8601 format) - DateTime literals:
#2024-01-15T10:30:00Z#
(ISO 8601 with timezone) - List literals:
[1, 2, 3]
,["admin", "manager"]
- Identifiers:
score
,user_name
,is_active
- Numbers (integers):
Advanced Operations
- Membership Operators:
in
for element-in-collection testing (role in ["admin", "manager"]
)contains
for collection-contains-element testing ([1, 2, 3] contains 2
)
- Parenthesized Expressions: Full support with proper precedence handling
- Plain Boolean Expressions: Support for bare identifiers (
active
,expired
) without explicit= true
Function System
- Built-in System Functions:
- String functions:
len(string)
,upper(string)
,lower(string)
,trim(string)
- Numeric functions:
abs(number)
,max(a, b)
,min(a, b)
- Date functions:
year(date)
,month(date)
,day(date)
- String functions:
- Custom Function Registration: Register anonymous functions with
Predicator.register_function/3
- Function Registry: ETS-based registry with automatic arity validation and error handling
- Context-Aware Functions: Functions receive evaluation context for dynamic behavior
Architecture & Performance
- Multi-Stage Compilation Pipeline: Expression → Lexer → Parser → Compiler → Instructions → Evaluator
- Compile-Once, Evaluate-Many: Pre-compile expressions for repeated evaluation
- Stack-Based Evaluator: Efficient instruction execution with minimal overhead
- Comprehensive Error Handling: Detailed error messages with line/column positioning
Developer Experience
- String Decompilation: Convert AST back to readable expressions with formatting options
- Multiple Evaluation APIs:
evaluate/2
- Returns{:ok, result}
or{:error, message}
evaluate!/2
- Returns result directly or raises exceptioncompile/1
- Pre-compile expressions to instructionsparse/1
- Parse expressions to AST for inspection
- Formatting Options: Configurable spacing (
:normal
,:compact
,:verbose
) and parentheses (:minimal
,:explicit
,:none
)
Breaking Changes
⚠️ COMPLETE LIBRARY REWRITE ⚠️
Version 1.0.0 is a complete rewrite of the Predicator library with entirely new:
- API design and function signatures
- Expression syntax and grammar
- Internal architecture and data structures
- Feature set and capabilities
Migration Guide
Migration from versions < 1.0.0 has NOT been tested and is NOT guaranteed to work.
If you are upgrading from a pre-1.0.0 version:
- Treat this as a new library adoption, not an upgrade
- Review all documentation - APIs have completely changed
- Test thoroughly in development environments
- Expect to rewrite all integration code
- Plan for significant refactoring of existing expressions
Future 1.x.x versions will maintain backwards compatibility and include proper migration guides.
For detailed information about upcoming features and development roadmap, see the project README.