Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2025-08-??

### 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 in `Predicator.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
```elixir
# 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` - Use `functions:` option instead
- `Predicator.clear_custom_functions/0` - No longer needed
- `Predicator.list_custom_functions/0` - No longer needed
- `Predicator.Functions.Registry` module - Entire registry system removed

#### Migration Guide
1. **Replace registry calls**: Convert `register_function` calls to function maps passed to `evaluate/3`
2. **Update function definitions**: Ensure functions return `{:ok, result}` or `{:error, message}`
3. **Remove initialization code**: Delete any registry setup from application startup
4. **Update tests**: Replace registry-based setup with evaluation-time function passing

#### Technical Implementation
- **Evaluator Enhancement**: Modified to accept `:functions` option and merge with system functions
- **SystemFunctions Refactor**: Added `all_functions/0` to provide system functions in evaluator format
- **Clean Architecture**: Removed ETS-based global registry and associated complexity
- **Backward Compatibility**: `evaluate/2` functions continue to work unchanged for expressions without custom functions

### Security
- **Improved Isolation**: Custom functions scoped to individual evaluation calls
- **No Global State**: Eliminates potential race conditions and global state mutations

### Performance
- **Reduced Overhead**: No ETS lookups or global registry management
- **Better Concurrency**: Thread-safe by design with no shared state

## [1.1.0] - 2025-08-20

### Added
Expand Down
56 changes: 36 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ list → "[" ( expression ( "," expression )* )? "]"
- **StringVisitor**: Converts AST back to strings
- **InstructionsVisitor**: Converts AST to executable instructions
- **Functions** (`lib/predicator/functions/`): Function system components
- **SystemFunctions**: Built-in system functions (len, upper, abs, max, etc.)
- **Registry**: Custom function registration and dispatch
- **SystemFunctions**: Built-in system functions (len, upper, abs, max, etc.) provided via `all_functions/0`
- **Main API** (`lib/predicator.ex`): Public interface with convenience functions

## Development Commands
Expand Down Expand Up @@ -98,13 +97,12 @@ lib/predicator/
├── lexer.ex # Tokenization with position tracking
├── parser.ex # Recursive descent parser
├── compiler.ex # AST to instructions conversion
├── evaluator.ex # Instruction execution engine
├── evaluator.ex # Instruction execution engine with custom function support
├── visitor.ex # Visitor behavior definition
├── types.ex # Type specifications
├── application.ex # OTP application
├── application.ex # OTP application (simplified - no registry init)
├── functions/ # Function system components
│ ├── system_functions.ex # Built-in functions (len, upper, abs, etc.)
│ └── registry.ex # Function registration and dispatch
│ └── system_functions.ex # Built-in functions (len, upper, abs, etc.)
└── visitors/ # AST transformation modules
├── string_visitor.ex # AST to string decompilation
└── instructions_visitor.ex # AST to instructions conversion
Expand All @@ -115,28 +113,33 @@ test/predicator/
├── compiler_test.exs
├── evaluator_test.exs
├── predicator_test.exs # Integration tests
├── functions/ # Function system tests
│ ├── system_functions_test.exs
│ └── registry_test.exs
└── visitors/ # Visitor tests
├── string_visitor_test.exs
└── instructions_visitor_test.exs
```

## Recent Additions (2025)

### Function Call System
- **Built-in Functions**: System functions automatically available
### Function System (v2.0.0 - Architecture Overhaul)
- **Built-in Functions**: System functions automatically available in all evaluations
- **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)`
- **Custom Functions**: Register anonymous functions with `Predicator.register_function/3`
- **Function Registry**: ETS-based registry with arity validation and error handling
- **Custom Functions**: Provided per evaluation via `functions:` option in `evaluate/3`
- **Function Format**: `%{name => {arity, function}}` where function takes `[args], context` and returns `{:ok, result}` or `{:error, message}`
- **Function Merging**: Custom functions merged with system functions, allowing overrides
- **Thread Safety**: No global state - functions scoped to individual evaluation calls
- **Examples**:
- `len(name) > 5`
- `upper(status) = "ACTIVE"`
- `year(created_date) = 2024`
- `max(score1, score2) > 85`
```elixir
custom_functions = %{
"double" => {1, fn [n], _context -> {:ok, n * 2} end},
"len" => {1, fn [_], _context -> {:ok, "custom_override"} end} # Override built-in
}

Predicator.evaluate("double(score) > 100", %{"score" => 60}, functions: custom_functions)
Predicator.evaluate("len('anything')", %{}, functions: custom_functions) # Uses override
Predicator.evaluate("len('hello')", %{}) # Uses built-in (returns 5)
```

### Date and DateTime Support
- **Syntax**: `#2024-01-15#` (date), `#2024-01-15T10:30:00Z#` (datetime)
Expand Down Expand Up @@ -168,6 +171,20 @@ test/predicator/
- `user.settings.theme = "dark" AND user.profile.active`
- **Backwards Compatible**: Simple variable names work exactly as before

## 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 in `evaluate/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`

### 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 identifier `user.profile.name`
- **Solution**: Use proper nested data structures instead of flat keys with dots

## Common Tasks

### Adding New Operators
Expand Down Expand Up @@ -200,8 +217,7 @@ test/predicator/
- **Property Testing**: Comprehensive input validation
- **Error Path Testing**: All error conditions covered
- **Round-trip Testing**: AST → String → AST consistency
- **Current Test Count**: 428 tests (64 doctests + 364 regular tests)
- **Coverage**: 92.6% overall, 100% on critical components
- **Current Test Count**: 569 tests (65 doctests + 504 regular tests)

## Code Standards

Expand Down Expand Up @@ -230,4 +246,4 @@ test/predicator/
### Development Environment
- Elixir ~> 1.11 required
- All dependencies in development/test only
- No runtime dependencies for core functionality
- No runtime dependencies for core functionality
68 changes: 45 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ iex> Predicator.evaluate("'coding' in user.hobbies", list_context)
Predicator uses a multi-stage compilation pipeline:

```
Expression String Lexer → Parser → Compiler → Evaluator
↓ ↓ ↓
Expression String Lexer → Parser → Compiler → Evaluator
↓ ↓ ↓
'score > 85 OR admin' → Tokens → AST → Instructions → Result
```

Expand Down Expand Up @@ -278,38 +278,60 @@ iex> Predicator.evaluate("score AND", %{})

## Advanced Usage

### Custom Function Registration
### Custom Functions

You can register your own custom functions for use in expressions:
You can provide custom functions when evaluating expressions using the `functions:` option:

```elixir
# Register a simple function
Predicator.register_function("double", 1, fn [n], _context ->
{:ok, n * 2}
end)
# Define custom functions in a map
custom_functions = %{
"double" => {1, fn [n], _context -> {:ok, n * 2} end},
"user_role" => {0, fn [], context ->
{:ok, Map.get(context, "current_user_role", "guest")}
end},
"divide" => {2, fn [a, b], _context ->
if b == 0 do
{:error, "Division by zero"}
else
{:ok, a / b}
end
end}
}

# Use in expressions
iex> Predicator.evaluate("double(score) > 100", %{"score" => 60})
# Use custom functions in expressions
iex> Predicator.evaluate("double(score) > 100", %{"score" => 60}, functions: custom_functions)
{:ok, true}

# Context-aware function
Predicator.register_function("user_role", 0, fn [], context ->
{:ok, Map.get(context, "current_user_role", "guest")}
end)
iex> Predicator.evaluate("user_role() = 'admin'", %{"current_user_role" => "admin"}, functions: custom_functions)
{:ok, true}

iex> Predicator.evaluate("user_role() = 'admin'", %{"current_user_role" => "admin"})
iex> Predicator.evaluate("divide(10, 2) = 5", %{}, functions: custom_functions)
{:ok, true}

# Function with error handling
Predicator.register_function("divide", 2, fn [a, b], _context ->
if b == 0 do
{:error, "Division by zero"}
else
{:ok, a / b}
end
end)
iex> Predicator.evaluate("divide(10, 0)", %{}, functions: custom_functions)
{:error, "Division by zero"}

# Custom functions can override built-in functions
override_functions = %{
"len" => {1, fn [_], _context -> {:ok, "custom_result"} end}
}

iex> Predicator.evaluate("len('anything')", %{}, functions: override_functions)
{:ok, "custom_result"}

# Without custom functions, built-ins work as expected
iex> Predicator.evaluate("len('hello')", %{})
{:ok, 5}
```

#### Function Format

Custom functions must follow this format:
- **Map Key**: Function name (string)
- **Map Value**: `{arity, function}` tuple where:
- `arity`: Number of arguments the function expects (integer)
- `function`: Anonymous function that takes `[args], context` and returns `{:ok, result}` or `{:error, message}`

### String Formatting Options

The StringVisitor supports multiple formatting modes:
Expand Down
Loading