Skip to content

feat: alt-retry journaling for LOOP_ALT chains (v2-gamma), SQL_MYSQL chain win restored - #128

Merged
jbachorik merged 2 commits into
mainfrom
feat/loop-alt-alt-retry
Sep 16, 2026
Merged

jbachorik merged 2 commits into
mainfrom
feat/loop-alt-alt-retry

Conversation

@jbachorik

Copy link
Copy Markdown
Collaborator

Follow-up to #127

#127 deliberately declined retryable LOOP_ALT bodies into the deterministic-chain family ((?:a|ab)+-style alternation loops where one body can match a proper prefix of another — including SQL_MYSQL's escaped-literal bodies \\" | [^"]), and accepted the resulting SQL_MYSQL tradeoff (~0.44x on SqlMysqlFind). This PR re-admits those shapes with exact JDK backtracking order.

How it works

  • Journal: each iteration records (end << 4) | bodyIndex (4 bits for the body, MAX_CHAIN_LIT_ALT <= 16).
  • Give-back handler (overlapping bodies): pops the last journaled iteration, re-enters the consume loop at that iteration's start with a floor past the committed body. That single construction reproduces the full JDK unwind: the popped iteration's later bodies are tried first; when they exhaust, the floor-gated consume fails and flows to consumeEnd, which re-runs the tail at the shortened boundary — exactly the iteration's exit choice — and the next pop retries the previous iteration. The min floor is enforced by the re-entered consumeEnd check (retrying the last iteration's alternatives at jc == min is allowed; dropping below min is not), which is precisely the JDK distinction between retrying an iteration and dropping an iteration.
  • Non-overlapping bodies keep the v2-beta boundary-only handler — SQL_ANSI, SQL_POSTGRESQL, LDAP and the XML shapes emit the same retry logic as before (the journal-slice base is a constant zero there).

Latent v2-beta bug fixed alongside

Two sequential give-back LOOP_ALTs in one generated method (SQL_MYSQL's two quote-loop branches) both journaled from index 0 — the second loop clobbered the first's boundaries. Each give-back loop now journals into its own (len+1) slice of the per-thread scratch buffer; single-loop methods keep the original allocation size.

Analyzer

The body-overlap rejection is removed; checkChainDisjoint instead forces giveBack for overlapping bodies — a disjoint mixed-width loop's maximal end is not input-determined without body retry (e.g. (?:ab|a)+[bc] on "aab").

Correctness verification

  • 263,821-input brute-force parity vs JDK (0 divergences) across: q(?:a|ab)+c\b and the star variant (exhaustive over {q,a,b,c,x}^≤6), disjoint-mixed-width forced-give-back shapes, sequential-slice shapes (a(?:b|bc)*x(?:d|de)*z exhaustive), doubled quote loops, and the full SQL_MYSQL pattern on crafted literal inputs.
  • Budget safety intact: the exponential retry space (q(?:a|aa)+z on 40 a's) trips the per-retry budget charge and delegates to PikeVM with the correct answer — same accounting envelope as all existing retryable constructs.
  • Full suite + fuzz gate (budget 37) green; 5 new tests incl. an in-test 3,906-input exhaustive sweep and the journal-slice isolation case.

Benchmarks (JDK 21 Temurin, -wi 5 -i 5 -f 3, workspace-jb)

Benchmark (Reggie vs JDK) SHORT MEDIUM LONG
SqlMysqlFind — was 0.44–0.49x in #127 50.8x 32.2x 31.6x
SqlMysqlNoMatch — was 28.8x (BitState) 31.5x 27.0x 27.7x
SqlAnsiFind 44.7x 29.4x 29.8x
SqlAnsiNoMatch 30.3x 27.0x 28.1x
SqlPostgresqlFind 23.5x 28.2x 28.9x
SqlPostgresqlNoMatch 30.2x 27.0x 28.3x
LdapFind 3.8x 2.9x 1.4x
LdapNoMatch 4.2x 2.2x 1.7x
XmlTags 3.5x

SqlMysqlFind vs RE2J: 179x / 103x / 100x. All previously-won benchmarks held within noise — the journal-slice base adds only constant-zero adds on the non-overlapping paths.

Re-admits retryable LOOP_ALT bodies into the deterministic-chain family:
bodies where one alternative can match a proper prefix of another
((?:a|ab)+, MySQL's \\" | [^"] escaped literals) now journal their chosen
body per iteration and the give-back handler re-enters the consume loop at
the popped iteration with a floor past the committed body - JDK
backtracking order (retry the last iteration's later bodies first; a
floor-gated consume failure flows to consumeEnd and re-runs the tail at
the shortened boundary = the iteration's exit choice; the next pop retries
the previous iteration). The min floor is enforced by the re-entered
consumeEnd min check, which preserves the JDK distinction between
retrying an iteration's alternatives and dropping the iteration.

Journal entries encode (end << 4) | bodyIndex (MAX_CHAIN_LIT_ALT <= 16).
Non-overlapping give-back loops keep the v2-beta boundary-only handler
and plain journal values, so existing admitted patterns (SQL_ANSI,
SQL_POSTGRESQL, Ldap, XmlTags) emit the same retry logic as before (the
journal base indirection adds only constant-zero adds).

Also fixes a latent v2-beta boundary-clobber: sequential give-back
LOOP_ALTs in one generated method (MySQL's two quote-loop branches) both
journaled from index 0, so the second loop's entries overwrote the first's
- each give-back loop now journals into its own (len+1) slice of the
per-thread scratch buffer (single-loop methods keep the original size).

The analyzer no longer rejects overlapping bodies; checkChainDisjoint
forces giveBack for them instead (a disjoint mixed-width loop's maximal
end is not input-determined without body retry, e.g. (?:ab|a)+[bc] on
"aab").

Verified: 263,821-input brute-force parity vs JDK across the retryable,
star, forced-giveBack, sequential-slice, and MySQL shapes (0 divergences);
budget delegation on the exponential (?:a|aa)+ shape intact. SQL_MYSQL
routes back to DETERMINISTIC_CHAIN. Full suite + fuzz gate green.

Tests: loopAltAltRetryParity (incl. 3,906-input exhaustive sweep),
loopAltDisjointMixedWidthForcedGiveBackParity,
loopAltSequentialGiveBackJournalSlicesParity,
loopAltAltRetryOverflowDelegatesToPikeVM,
loopAltOverlappingBodiesAdmittedWithAltRetry; sqlMysql tests restored to
chain routing and chain parity with retry-triggering inputs.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T06:33:04.430310Z f7c292b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov-commenter

codecov-commenter commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.73418% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.9%. Comparing base (02dddb8) to head (072d8d3).

Files with missing lines Patch % Lines
...n/codegen/DeterministicChainBytecodeGenerator.java 98.7% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##              main    #128    +/-   ##
========================================
  Coverage     84.9%   84.9%            
  Complexity       1       1            
========================================
  Files          159     159            
  Lines        46752   46878   +126     
  Branches      6462    6485    +23     
========================================
+ Hits         39698   39812   +114     
- Misses        5096    5108    +12     
  Partials      1958    1958            
Files with missing lines Coverage Δ
...doghq/reggie/codegen/analysis/PatternAnalyzer.java 77.8% <100.0%> (ø)
...n/codegen/DeterministicChainBytecodeGenerator.java 96.1% <98.7%> (-0.6%) ⬇️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 02dddb8...072d8d3. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@datadog-prod-us1-4 datadog-prod-us1-4 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.

Datadog Autotest: FAIL

The packed journal removes high position bits at input offset 2^28, so retry can write past the journal on a valid large string. The new slice count also keeps one full-input slice for each separate branch, which can cause an avoidable heap failure.

Open Bits AI session

🤖 Datadog Autotest · Commit f7c292b · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7c292b261

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…try lengths

Addresses both PR review findings (Codex + Datadog Autotest, P2/P2):

Journal slices now bound the maximum simultaneously-live path, not the
syntactic sum. The constructor counted every give-back LOOP_ALT in the
tree - including loops in mutually exclusive top-level branches and
alternation bodies - so a legal 32-branch pattern with one loop per
branch requested 32x(len+1) ints (~1 GiB at 8M chars) before trying any
branch, retained per thread by chainScratch. Only one exclusive path can
have a live retry chain at a time, and a failed branch's journal is dead
once its retry chain unwinds into the next branch, so slice ordinals are
now reused across exclusive paths: the top-level branch loops and the
ALT_CHAIN body loop save/restore the EmitCtx ordinal counter (an
ALT_CHAIN ends at the deepest body's span so a sequential give-back loop
in the shared rest still stacks on the winning body's loops), and the
journal is sized by maxPathLoopAltGiveBack (sequential elements and
OPT/CAPTURE nested regions stack; alternation bodies take the max). This
also halves SQL_MYSQL's journal: its two quote loops live in exclusive
alternation branches and now share one (len+1) slice.

The alt-retry journal entry packs (end << 4) | bodyIndex, which only
represents ends below 1 << 27: at or above that the int shift discards
the high bits and the give-back decode restores wrapped (wrong)
positions. The "the journal would not fit memory" argument was wrong -
a 2^28-int journal (~1 GiB) fits large heaps, so the case is reachable.
Entry points that acquire a journal now guard on
len >= 1 << 27 for alt-retry patterns and delegate the whole call to the
PikeVM fallback (full-width positions, linear) before allocating.
Boundary-only journals (SQL_ANSI etc.) store plain ends and keep the
unguarded path.

Verified: the 263,821-input brute-force parity sweep is unchanged
(0 divergences); reflection-level sizing assertions (single loop 1,
exclusive branches share 1, sequential loops stack 2, ALT_CHAIN +
successor stack 2, SQL_MYSQL 1) and a 1,555-input exclusive-branch
journal-reuse parity sweep; the 2^27+10-char SQL_MYSQL probe delegates
(fallbackCount 0 -> 1) with the JDK answer while normal-size inputs
stay on the chain path. Full suites + fuzz gate green.
@jbachorik
jbachorik merged commit fd362c2 into main Sep 16, 2026
9 checks passed
@jbachorik
jbachorik deleted the feat/loop-alt-alt-retry branch September 16, 2026 07:35
@jbachorik jbachorik added this to the 0.4.0 milestone Sep 24, 2026
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