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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.2.1
version: v2.9.0
Comment thread
lucasnakano marked this conversation as resolved.
test:
name: Unit Tests
runs-on: ubuntu-latest
Expand Down
36 changes: 26 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,25 @@ The entire specification is defined through comprehensive test cases in `test/fi
- **Logical**: `not`, `and`, `or` with proper nesting
- **Attribute Comparisons**: Both flat and nested property comparisons
- **Nested Attributes**: Deep object navigation with dot notation
- **Array Length**: `.length` accessor for array size checks
- **List Quantifiers**: `any`, `all`, `none` operators for element-level conditions

### Rule Syntax Examples

```
x eq 10 // equality
score gt 100 and level lt 5 // logical operations
city co "York" // string contains
color in ["red","green","blue"] // membership
user.profile.age ge 18 // nested attributes
not (status eq "inactive") // negation
created_at dl 30 // within last 30 days
updated_at dg 365 // older than 365 days
```text
x eq 10 // equality
score gt 100 and level lt 5 // logical operations
city co "York" // string contains
color in ["red","green","blue"] // membership
user.profile.age ge 18 // nested attributes
not (status eq "inactive") // negation
created_at dl 30 // within last 30 days
updated_at dg 365 // older than 365 days
items.length gt 3 // array length
selections any (status eq "cancelled") // any element matches
selections all (is_valid eq true) // all elements match
selections none (is_fraud eq true) // no element matches
items any (price gt 100 and in_stock eq true) // compound sub-expression
```

## Development Commands
Expand Down Expand Up @@ -111,11 +118,20 @@ All functionality is validated through the comprehensive test suite in `test/fix
- **Membership Operations**: Use strict type checking (no cross-type matching)
- **Large Integer Support**: Preserve precision for integers > 2^53 using dual storage

### List Operations
- **Array Length**: Access `.length` on array properties (e.g., `items.length gt 3`)
- **Quantifier `any`**: True if any element matches the sub-expression (short-circuits on first match)
- **Quantifier `all`**: True if all elements match (vacuous truth for empty arrays, short-circuits on first non-match)
- **Quantifier `none`**: True if no element matches (true for empty arrays, short-circuits on first match)
- **Sub-expressions**: Quantifiers support full expressions including `and`, `or`, `not`, all comparison operators
- **Nested access**: Works with nested properties (e.g., `data.items any (status eq "active")`)
- **Reserved words**: `any`, `all`, `none` are reserved keywords (cannot be used as field names)

### Zero-Allocation Implementation
- **EvalResult Structure**: Pre-allocated typed result structure to avoid interface boxing
- **Memory Reuse**: Single evaluator instance with reusable result buffer
- **AST Caching**: Pre-compiled rules stored in lock-free concurrent map
- **Allocation Verification**: All benchmarks must show 0 allocs/op
- **Allocation Verification**: All benchmarks must show 0 allocs/op (array operations may have 1 alloc from Go runtime `[]any` handling)

Comment thread
lucasnakano marked this conversation as resolved.
### Performance Benchmarking
- **Baseline Comparison**: Must outperform nikunjy/rules by 100x minimum
Expand Down
277 changes: 277 additions & 0 deletions benchmark_optimized_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ func BenchmarkOptimizedEngineSimple(b *testing.B) {
// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand All @@ -37,6 +45,14 @@ func BenchmarkOptimizedEngineComplex(b *testing.B) {
// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand All @@ -55,6 +71,14 @@ func BenchmarkOptimizedEngineStringOps(b *testing.B) {
// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand All @@ -76,6 +100,15 @@ func BenchmarkOptimizedEngineInOperator(b *testing.B) {
// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// 1 alloc (24 B) from []any interface boxing in Go runtime.
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 1 {
b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
}
Comment on lines +103 to +110

@coderabbitai coderabbitai Bot Feb 16, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce 0 allocs/op for the in benchmark.

This currently tolerates allocations, which violates the benchmark requirement.

Proposed fix
-	// 1 alloc (24 B) from []any interface boxing in Go runtime.
 	if allocs := testing.AllocsPerRun(1, func() {
 		_, _ = engine.Evaluate(rule, ctx)
-	}); allocs > 1 {
-		b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
+	}); allocs != 0 {
+		b.Fatalf("expected 0 allocs/op, got %f", allocs)
 	}

As per coding guidelines, "Allocation Verification: All benchmarks must show 0 allocs/op".

🤖 Prompt for AI Agents
In `@benchmark_optimized_test.go` around lines 103 - 110, The benchmark currently
allows up to 1 allocation by checking "allocs > 1"; change the assertion to
enforce zero allocations by checking "allocs != 0" or "allocs > 0" and update
the failure message to "expected 0 allocs/op, got %f". Apply this change around
the testing.AllocsPerRun(1, func() { _, _ = engine.Evaluate(rule, ctx) }) block
and keep b.ReportAllocs() and the same call to engine.Evaluate(rule, ctx).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in operator benchmark correctly allows 1 allocation (24 B). This is a pre-existing behavior — not introduced by this PR. The allocation comes from Go's runtime boxing of a []any slice header (24 bytes) into an any interface field (EvalResult.OriginalValue).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lucasnakano Understood. You're right that the in operator's allocation is pre-existing and the check correctly reflects that. The 24 B allocation from boxing []any into the interface is a Go runtime behavior that applies to both the existing in operator and the new quantifier operations.

Since this is a known limitation and not a regression, the allocs > 1 check is appropriate for this benchmark.


🔥 Failed to add learnings

Learnings cannot be added due to server error. Please try again.


b.ResetTimer()

for range b.N {
Expand All @@ -102,6 +135,14 @@ func BenchmarkOptimizedEngineNestedProps(b *testing.B) {
// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand All @@ -117,6 +158,234 @@ func BenchmarkOptimizedStandalone(b *testing.B) {
ctx := D{"x": 10}
rule := "x eq 10"

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark array length evaluation for zero allocations.
func BenchmarkOptimizedEngineArrayLength(b *testing.B) {
engine := NewEngine()
ctx := D{
"selections": []any{
D{"id": "1"},
D{"id": "2"},
D{"id": "3"},
},
}
rule := "selections.length gt 2"

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark any quantifier evaluation (1 alloc, 24 B from []any interface boxing).
func BenchmarkOptimizedEngineQuantifierAny(b *testing.B) {
engine := NewEngine()
ctx := D{
"selections": []any{
D{"event_status": "EVENT_STATUS_IN_PROGRESS", "is_live": true},
D{"event_status": "EVENT_STATUS_CANCELLED", "is_live": false},
D{"event_status": "EVENT_STATUS_FINISHED", "is_live": false},
},
}
rule := `selections any (event_status eq "EVENT_STATUS_CANCELLED")`

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// 1 alloc (24 B) from []any interface boxing in Go runtime.
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 1 {
b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark all quantifier evaluation (1 alloc, 24 B from []any interface boxing).
func BenchmarkOptimizedEngineQuantifierAll(b *testing.B) {
engine := NewEngine()
ctx := D{
"items": []any{
D{"is_valid": true},
D{"is_valid": true},
D{"is_valid": true},
},
}
rule := "items all (is_valid eq true)"

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// 1 alloc (24 B) from []any interface boxing in Go runtime.
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 1 {
b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark none quantifier evaluation (1 alloc, 24 B from []any interface boxing).
func BenchmarkOptimizedEngineQuantifierNone(b *testing.B) {
engine := NewEngine()
ctx := D{
"items": []any{
D{"status": "active"},
D{"status": "pending"},
D{"status": "completed"},
},
}
rule := `items none (status eq "error")`

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// 1 alloc (24 B) from []any interface boxing in Go runtime.
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 1 {
b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark complex real-world betting rule with length + quantifiers.
func BenchmarkOptimizedEngineComplexBetting(b *testing.B) {
engine := NewEngine()
ctx := D{
"bet_type": "BET_TYPE_MULTIPLE",
"customer_data": D{
"is_vip": true,
},
"selections": []any{
D{
"event_status": "EVENT_STATUS_IN_PROGRESS",
"is_live": true,
"odd": 2.5,
"provider": "PROVIDER_SPORTRADAR",
},
D{"event_status": "EVENT_STATUS_NOT_STARTED", "is_live": false, "odd": 1.8, "provider": "PROVIDER_RAMP"},
D{
"event_status": "EVENT_STATUS_IN_PROGRESS",
"is_live": true,
"odd": 3.1,
"provider": "PROVIDER_SPORTRADAR",
},
},
}
rule := `bet_type eq "BET_TYPE_MULTIPLE" and selections.length ge 2 and selections none (event_status eq "EVENT_STATUS_CANCELLED") and selections any (is_live eq true and odd gt 2.0)`

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// Multiple allocs expected from []any interface boxing (one per quantifier).
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 2 {
b.Fatalf("expected <= 2 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
result, err := engine.Evaluate(rule, ctx)
if err != nil || !result {
b.Fatalf("Expected true result, got %v, %v", result, err)
}
}
}

// Benchmark quantifier with short-circuit (first element matches).
func BenchmarkOptimizedEngineQuantifierAnyShortCircuit(b *testing.B) {
engine := NewEngine()

// Create array with 100 elements, first one matches
items := make([]any, 100)

items[0] = D{"status": "target"}
for i := 1; i < 100; i++ {
items[i] = D{"status": "other"}
}

ctx := D{"items": items}
rule := `items any (status eq "target")`

// Pre-compile rule
engine.AddQuery(rule)

b.ReportAllocs()

// 1 alloc (24 B) from []any interface boxing in Go runtime.
if allocs := testing.AllocsPerRun(1, func() {
_, _ = engine.Evaluate(rule, ctx)
}); allocs > 1 {
b.Fatalf("expected <= 1 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand All @@ -135,6 +404,14 @@ func BenchmarkZeroAllocEvaluatorDirect(b *testing.B) {
NewNumberLiteralNode(10))
ctx := D{"x": 10}

b.ReportAllocs()

if allocs := testing.AllocsPerRun(1, func() {
_, _ = evaluator.Evaluate(ast, ctx)
}); allocs != 0 {
b.Fatalf("expected 0 allocs/op, got %f", allocs)
}

b.ResetTimer()

for range b.N {
Expand Down
Loading