Skip to content

fix: ifFail dropping matchers, operator case, and quoted literals - #57

Merged
rapatao merged 1 commit into
mainfrom
fix/rule-correctness-and-bench-input
Aug 31, 2026
Merged

fix: ifFail dropping matchers, operator case, and quoted literals#57
rapatao merged 1 commit into
mainfrom
fix/rule-correctness-and-bench-input

Conversation

@rapatao

@rapatao rapatao commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

Four correctness defects in expression handling, a Kotlin engine speedup that
came out of fixing one of them, and a benchmark input fix that was pricing the
input's root kind as if it were input width.

Every defect below was reproduced first and has a regression test.

Bugs

ifFail discarded every matcher of a group. It rebuilt the expression from
left, operator and right only, so allMatch(...) ifFail FALSE returned a
fully empty expression, which then failed validation. It copies now.

A mixed case operator was unreachable by its own name. Operators are stored
under name().lowercase() but were looked up raw, so a custom operator named
myOperator could never be resolved. The lookup lowercases too.

A quoted literal spanning lines threw. The check was Regex("\".*\"") and
. does not match a newline, so the literal fell through to path resolution and
raised NoSuchElementException. The replacement is a startsWith and
endsWith check, which has neither the newline blind spot nor a regex on the
per-operand path.

Unknown operator: always printed null, because the message interpolated
the null local rather than the operator name.

Performance

Kotlin engine, three runs per configuration in one session, medians:

ops/s alloc per evaluation
before 2,225,259 849 B
after 2,598,127 375 B

An operand no longer runs toBigIntegerOrNull and toBigDecimalOrNull, the
second of which screens with a regex, unless it starts like a number, and paths
never do. Expression.isValid uses all rather than map plus firstOrNull,
which short circuits and stops allocating a list a group.

Benchmark input

The default input rooted at a data class while the wide variant rooted at a map,
so the two width columns differed by root kind as well as width. That is not
free: a map root is a hash lookup where a typed root is a reflective property
read, and it showed up as the Kotlin engine reading faster on the wider input.

Both root at a map now, and wideInput is built from narrowInput, so the two
cannot drift apart. BENCHMARKS.md is measured again throughout: all four tables,
the spread figures, the GC counts and the guidance.

Where the time goes is left alone. That measurement comes from a loop that is
not in this repository and cannot be reproduced from it.

CI

tests.yaml ran test on Java 17 against a Java 21 toolchain, which worked
only because the runner image happens to ship a 21. It runs check on 21 now,
which additionally enforces the detekt and 90% kover gates that were configured
but never actually executed.

Cleanup

  • assertSingleCaseForDebugging, a debugging leftover pinned to case index 122
    that duplicated a parameterized case and would break if the case list shrank
  • a BRANCH >= 90 kover bound declared twice
  • unused credentialsVersion and jacocoVersion properties

Verification

./gradlew clean check passes, which covers the tests, detekt and the kover
thresholds for every module.

Four defects, each reproduced before the change and covered by a regression
test:

* ifFail rebuilt the expression from left, operator and right only, so
  allMatch(...) ifFail FALSE returned an empty, invalid expression and silently
  discarded every matcher. It copies now.
* Operators were stored under name().lowercase() but looked up raw, so an
  operator whose name carried an uppercase letter was unreachable by its own
  name. The lookup lowercases too, which is what the stored form always meant.
* The quoted literal check used Regex("\".*\""), and . does not match a newline,
  so a literal spanning lines fell through to path resolution and threw
  NoSuchElementException. A startsWith and endsWith check has neither the
  newline blind spot nor the per-operand regex.
* requireNotNull(operator) { "Unknown operator: $operator" } interpolated the
  null local, so the message always read null.

Kotlin engine, three runs per configuration in one session, medians: 2,225,259
to 2,598,127 ops/s, and 849 B to 375 B per evaluation. An operand no longer
pays toBigIntegerOrNull and toBigDecimalOrNull, the second of which screens with
a regex, unless it starts like a number. Expression.isValid uses all rather than
map plus firstOrNull, which short circuits and stops allocating a list a group.

The benchmark default input rooted at a data class while the wide variant rooted
at a map, so the width columns priced the root kind as if it were width. Both
root at a map now, and the wide input is built from the narrow one so the two
cannot drift apart. BENCHMARKS.md is measured again throughout.

CI ran test on Java 17 against a Java 21 toolchain, which worked only because
the runner image ships a 21. It runs check on 21 now, which also enforces the
detekt and 90% kover gates that were configured but never executed.

Also removed a debugging leftover test pinned to case index 122, a duplicated
kover BRANCH bound, and two unused version properties.
@rapatao
rapatao merged commit 2ad6249 into main Aug 31, 2026
4 checks passed
@rapatao
rapatao deleted the fix/rule-correctness-and-bench-input branch August 31, 2026 22:23
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.

1 participant