Skip to content

feat: add list ops with array length accessor and any/all/none quantifiers - #6

Merged
lucasnakano merged 2 commits into
mainfrom
nakano/quantifiers-op
Feb 19, 2026
Merged

lucasnakano merged 2 commits into
mainfrom
nakano/quantifiers-op

Conversation

@lucasnakano

@lucasnakano lucasnakano commented Feb 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Antes dessa mudança, o rule engine não conseguia operar sobre listas de objetos. O máximo que se podia fazer era verificar se uma lista existia (selections pr). Não era possível contar elementos, nem verificar condições sobre os objetos dentro da lista.

Isso era um bloqueio para casos de uso reais como validação de apostas, onde é necessário responder perguntas como:

  • "Quantas seleções essa aposta tem?"
  • "Alguma seleção tem evento cancelado?"
  • "Todas as seleções são do mesmo esporte?"

O que foi adicionado

.length — Acessor de tamanho em arrays via dot notation:

selections.length gt 3
data.items.length eq 0

any / all / none — Quantificadores que avaliam uma sub-expressão contra cada elemento de uma lista:

selections any (event_status eq "EVENT_STATUS_CANCELLED")
selections all (is_live eq true)
selections none (status eq "SELECTION_STATUS_LOSS")

Os quantificadores suportam sub-expressões compostas (and, or, not), aninhamento (lista dentro de lista), e combinação com .length e condições externas:

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 3.0)

Performance Impact

  • Performance improved (include benchmarks)
Operação Tempo Alocações
.length 22ns 0 allocs
any (3 elementos) 82ns 1 alloc*
all (3 elementos) 81ns 1 alloc*
none (3 elementos) 85ns 1 alloc*
Short-circuit any (1o de 100) 55ns 1 alloc*

*A alocação de 24B vem do boxing de []any em any no Go runtime — mesmo padrão que o operador in já existente.

Checklist

  • Code follows project style
  • Self-reviewed the code
  • Tests added for new functionality
  • README updated (if required)

Summary by CodeRabbit

  • New Features

    • Array length access (".length") and list quantifiers any/all/none with short-circuit semantics and support in expressions.
  • Tests

    • Extensive examples, fixtures, and new benchmarks, including zero-/low-allocation checks and short-circuit performance tests.
  • Documentation

    • Expanded syntax docs, examples, developer guidance, and updated benchmarking/performance and memory expectations.
  • Validation / Errors

    • New validation checks and clearer error messages for improper quantifier usage.

@coderabbitai

coderabbitai Bot commented Feb 15, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds array .length access and list quantifiers any, all, none to the rule language. Implements parser, validator, and evaluator support (quantifier parsing/validation/evaluation and array-length handling), new errors and a length constant, extensive tests/examples/benchmarks, and minor CI/docs tweaks.

Changes

Cohort / File(s) Summary
Tokens & Constants
token.go, constants.go
Added ANY, ALL, NONE token constants/keyword mappings and tokenStringMap entries; introduced internal lengthProperty = "length" constant.
Parser
parser.go, token.go
Recognizes quantifier operators; added parseQuantifierExpression and isQuantifierOperator; updated parse paths to accept quantifier syntax.
Validation & Errors
validator.go, errors.go
Added validation dispatch for quantifiers, validateQuantifierOperation (enforces identifier/property target), and new errors ErrQuantifierRequiresParens and ErrInvalidQuantifierTarget.
Evaluation Logic
evaluator.go
Implements .length array access (setArrayLengthResult) and quantifier evaluation (evaluateQuantifier, evaluateQuantifierAny/All/None) with short-circuit semantics; adjusts result coercion/array handling.
Tests & Fixtures
test/list_fixtures.go, test/rule_engine_test.go, example_test.go
Added extensive fixtures and example tests for array length, any/all/none, nested lists, and real-world scenarios; integrated new test groups and examples.
Benchmarks
benchmark_optimized_test.go
Added optimized-engine benchmarks covering array length, quantifiers (ANY/ALL/NONE), complex rules, short-circuit cases, and allocation checks.
Docs & CI
CLAUDE.md, .github/workflows/test.yml
Documentation updated for list operations and benchmarking; bumped golangci-lint-action version.
Minor Test/Formatting
race_condition_test.go, test/additional_edge_cases.go, test/edge_case_fixtures.go
Small assertion/formatting tweaks and test-data formatting with no behavioral changes.

Sequence Diagram(s)

sequenceDiagram
    participant Parser as Parser
    participant Validator as Validator
    participant Evaluator as Evaluator
    participant Context as DataContext

    rect rgba(200,200,255,0.5)
    Parser->>Parser: Tokenize input, detect ANY/ALL/NONE
    Parser->>Parser: parseQuantifierExpression(left) -> BinaryOp(quantifier, left, subExpr)
    end

    rect rgba(200,255,200,0.5)
    Validator->>Validator: Visit BinaryOp(quantifier,...)
    Validator->>Validator: validateQuantifierOperation(left) -> ensure identifier/property
    end

    rect rgba(255,200,200,0.5)
    Evaluator->>Context: resolve left operand -> value (expect array)
    Evaluator->>Evaluator: if final property == "length" -> return numeric length
    alt Quantifier = ANY
        Evaluator->>Evaluator: evaluateQuantifierAny() - true on first matching element
    else Quantifier = ALL
        Evaluator->>Evaluator: evaluateQuantifierAll() - false on first non-matching element
    else Quantifier = NONE
        Evaluator->>Evaluator: evaluateQuantifierNone() - false on first matching element
    end
    Evaluator->>Evaluator: return boolean result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature addition: list operations including array length accessor and quantifier operators.
Description check ✅ Passed The description covers the problem, solution, performance benchmarks, and checklist items, though it does not strictly follow the provided template structure.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nakano/quantifiers-op

Comment @coderabbitai help to get the list of available commands and usage tips.

@lucasnakano lucasnakano changed the title feat: add list operations with array length accessor and any/all/none… feat: add list ops with array length accessor and any/all/none quantifiers Feb 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@benchmark_optimized_test.go`:
- Around line 155-178: The benchmark comment on
BenchmarkOptimizedEngineQuantifierAny incorrectly claims "for zero allocations"
but the quantifier evaluation incurs 1 allocation (24 B) due to []any boxing;
update the function comment (and analogous quantifier benchmark comments such as
for `all`/`none` if present) to accurately state "for 1 allocation from []any
boxing (24 B)" or similar, so the comment reflects actual allocation behavior;
locate the comment above the `BenchmarkOptimizedEngineQuantifierAny` function
(and matching quantifier benchmarks) and change the wording rather than altering
benchmark logic.
🧹 Nitpick comments (1)
CLAUDE.md (1)

43-57: Add language identifier to fenced code block.

Static analysis (markdownlint MD040) flags this code block as missing a language specifier. Since these are rule expressions, you could use a generic identifier or leave as plain text explicitly.

📝 Proposed fix
-```
+```text
 x eq 10                                          // equality

Comment thread benchmark_optimized_test.go Outdated
@lucasnakano
lucasnakano force-pushed the nakano/quantifiers-op branch from 8ef3746 to 3d1a63f Compare February 15, 2026 22:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/test.yml:
- Line 20: The workflow update changes the golangci-lint tool to "version:
v2.9.0" which is a large jump from v2.2.1; run the CI pipeline locally and in CI
to surface any new lint failures, review and fix/whitelist new issues reported
by golangci-lint (adjust config in .golangci.yml or suppress specific checks),
or if you're not ready to address new errors, revert or pin back to "v2.2.1"
temporarily; ensure the chosen approach leaves the pipeline green and update the
workflow line referencing "version: v2.9.0" accordingly.

In `@CLAUDE.md`:
- Around line 43-44: The fenced code block containing the example "x eq 10" is
missing a language tag which triggers markdownlint MD040; update the opening
fence from ``` to include a language tag (e.g., change ``` to ```text) for the
block that contains "x eq 10" so the fenced code block is annotated and the
linter warning is resolved.
- Around line 134-135: The current "Allocation Verification" rule in CLAUDE.md
includes a parenthetical exception "(array operations may have 1 alloc from Go
runtime `[]any` handling)" which weakens the 0 allocs/op requirement; remove
that exception and make the rule strictly "All benchmarks must show 0
allocs/op". Update the "Allocation Verification" header text to omit the
parenthetical, and then locate any corresponding benchmark descriptions/tests
that referenced or relied on the allowed 1 allocation and change the
implementations or assertions so they report 0 allocs/op instead (ensure any
mention of `[]any` handling is removed or replaced with guidance to avoid
allocations). Ensure documentation and tests consistently require and assert 0
allocs/op.
🧹 Nitpick comments (1)
evaluator.go (1)

471-475: Note behavioral difference between quantifiers for non-map elements.

ALL returns false for non-map elements (line 474), while ANY and NONE skip them with continue. This is intentional and correct: for ALL, every element must satisfy the predicate, so a non-evaluable element fails the condition. For ANY/NONE, we're looking for existence/absence of matches among evaluable elements.

Worth documenting this semantic distinction if not already covered.

Comment thread .github/workflows/test.yml
Comment thread CLAUDE.md Outdated
Comment thread CLAUDE.md
@lucasnakano
lucasnakano force-pushed the nakano/quantifiers-op branch from 3d1a63f to a7e3907 Compare February 15, 2026 23:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@benchmark_optimized_test.go`:
- Around line 130-295: Add explicit allocation verification to each new
benchmark (BenchmarkOptimizedEngineArrayLength,
BenchmarkOptimizedEngineQuantifierAny, BenchmarkOptimizedEngineQuantifierAll,
BenchmarkOptimizedEngineQuantifierNone, BenchmarkOptimizedEngineComplexBetting,
BenchmarkOptimizedEngineQuantifierAnyShortCircuit): call b.ReportAllocs() and
use testing.AllocsPerRun (with a reasonable iteration count) to assert allocs/op
== 0 for the evaluated rule/context before entering the timed loop (or before
b.ResetTimer()), and fail the benchmark with b.Fatalf if allocs != 0 so the
benchmark enforces the "0 allocs/op" guideline.

Comment thread benchmark_optimized_test.go

@heynemann heynemann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving to unblock but left comments within coderabbit threads

@lucasnakano
lucasnakano force-pushed the nakano/quantifiers-op branch from 74aafb4 to 0e3f61f Compare February 16, 2026 02:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@benchmark_optimized_test.go`:
- Around line 229-234: The benchmark currently allows allocations by asserting
allocs <= 1; change the allocation check to require zero allocations by
replacing the testing.AllocsPerRun check (the block that calls
testing.AllocsPerRun(1, func() { _, _ = engine.Evaluate(rule, ctx) })) to fail
when allocs != 0 (or allocs > 0) and update the b.Fatalf message accordingly;
apply the same change to the other benchmark blocks that call
testing.AllocsPerRun (the ones exercising engine.Evaluate with rule and ctx) so
all benchmarks enforce 0 allocs/op.
- Around line 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).

Comment on lines +103 to +110
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)
}

@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.

Comment thread benchmark_optimized_test.go
@lucasnakano
lucasnakano merged commit 5f5d2f6 into main Feb 19, 2026
7 checks passed
@lucasnakano
lucasnakano deleted the nakano/quantifiers-op branch February 19, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants