Skip to content

chore(compiler): speed up typecheck for deep else-if ladders (remove quadratic complexity)#1865

Open
klondikedragon wants to merge 9 commits into
vectordotdev:mainfrom
itlightning:perf/else-if-typecheck-flatten
Open

chore(compiler): speed up typecheck for deep else-if ladders (remove quadratic complexity)#1865
klondikedragon wants to merge 9 commits into
vectordotdev:mainfrom
itlightning:perf/else-if-typecheck-flatten

Conversation

@klondikedragon

@klondikedragon klondikedragon commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Hi! I 💖 vector and have been using it for many years. thank you!

This PR speeds up VRL compile / typecheck for programs with long nested else if chains by removing O(n^2) behavior for this case.

How we discovered it: We're using VRL to classify events in complex ways, which means some of our VRL has long else-if chains (40-60+ arms in several places). Our test suite (which runs validate over each VRL file + exercises data flow) went from a few seconds total to minutes as we've developed more VRL files. We found vector taking a long time (8+ seconds) to validate/compile some of our VRL files. Profiling pointed at Expression::apply_type_info / IfStatement / Kind / TypeState / LocalEnv clone and merge.

After these changes, these same VRL files validate/compile an order of magnitude faster (~8s to ~0.8s), taking our overall test suite run back from minutes to seconds.

While VRL compile-time is a one-time cost at startup, it's single-threaded and with our VRL, machines were taking 30+ seconds from vector startup before processing events. This removes that long delay.

I've contributed a smaller PR in the past, but this is my first time digging in to the compiler in VRL... I'm happy to adjust and discuss!

Change Type

  • Bug fix
  • New feature
  • Non-functional (chore, refactoring, docs)
  • Performance

Is this a breaking change?

  • Yes
  • No (no longer breaking after made further updates to PR)

Semantics note

After some updates to the PR, there are no changes to semantics. Additional tests were added to increase coverage of semantics for edge cases around if-else-if chains and related.

Problem

  • The parser converts else if to nested else { if ... }.
  • IfStatement::type_info clones TypeState, typechecks both sides, then merges.
  • Each nest re-walks the remaining ladder and deep-clones / merges large Kind trees and local bindings, so cost grows O(n^2) rather than a single linear pass over arms (O(n)).
  • Clones of certain large structures in the type check phase are expensive, causing further slowdown.

Approach

Ordered commits:

  1. Tests for if / else-if predicate local escape (documents intended semantics; else-if test alone is red until flatten).
  2. Flatten parser else { if ... } into a multi-arm IfStatement at compile so each arm is typechecked once. Main benefit that solves quadratic behavior.
  3. In-place exact object/array Kind inserts and assignment type updates (avoid deep-clone on every nested write).
  4. Arc + COW for Collection known maps.
  5. Compile timing example (examples/else_if_ladder_compile.rs) with ladder + --program and --env-width / --env-depth knobs.
  6. Arc + COW for LocalEnv bindings (this commit further speeds up ~2.7× on a large --program workload in that example driver, on top of the earlier commits).
  7. (pr fragment)
  8. Fix to make semantics the same as before: strip later else-if predicate-assigned locals after chain. After flattening, re-apply apply_child_scope from the post-arm-0 local env so bindings first introduced in later predicates are not definite after the chain (restoring nested else { if } Block semantics).

Benchmarks

Timings below are from my local release builds of examples/else_if_ladder_compile

Synthetic benchmark

cargo run --release --example else_if_ladder_compile -- \
  --arms 5,10,20,40,60 --fields 40 --warmup 1 --repeat 3

Performance after primary fix of removing quadratic complexity:

arms med_ms before any fix med_ms after flatten speedup
5 27 13 ~2×
10 73 25 ~3×
20 251 46 ~5×
40 890 94 ~9×
60 2003 147 ~13×

After doing in-place mutations of Kind plus cheap clones of Collection and LocalEnv (via Arc), the 60 arm test drops to med_ms=49.4:

$ cargo run --release --example else_if_ladder_compile -- --arms 5,10,20,40,60 --fields 40 --warmup 1 --repeat 3
else-if ladder compile (fields/arm=40, env=default, warmup=1, repeat=3)
  arms   src_bytes      min_ms      med_ms      max_ms      ms/arm^2
     5        5861         3.5         3.6         6.1        0.1449
    10       10676         6.5         8.1         8.3        0.0811
    20       20726        12.9        15.0        15.1        0.0376
    40       40826        33.4        34.4        36.0        0.0215
    60       60926        47.4        49.4        50.0        0.0137

So approximately a 40x speedup for the 60-arm case before to final after. The performance is also the same after the 8th commit.

Synthentic benchmarks are interesting, but how about a real-world case...

End-to-end Vector validate (bench-vrl-runtime.py)

Our CI harness in one of its steps uses vector validate over all of VRL scripts. We exposed this piece of it to benchmark this piece (with our large VRL it was causing big slowdowns). We built vector 0.57 with our branch cherry-picked to the version of VRL paired with 0.57, and compared it to stock vector on my Linux system (was version 0.56, sorry it's not quite the same, I could re-run if you want; other systems where I'm running 0.57 show almost identical behavior though, and the profiling data showed the hotspot was the type checking and VRL synthetic benchmarks confirm that):

Our bench-vrl-runtime.py isn't part of the PR, it's something we wrote to interface with our VRL CI.

Same harness flags; PATH selects the Vector binary.

Stock (/usr/bin/vector 0.56.0 release on this machine):

PATH=/usr/bin:$PATH python3 tools/bench-vrl-runtime.py \
  --scenario mix --events 1 --arms full --work /tmp/sl-vrl-runtime-bench
corpus /tmp/sl-vrl-runtime-bench/mix-1.ndjson lines=1 bytes=548 from security-mix.ndjson
validate cfg-no_vrl.yaml: 0.07s
validate cfg-thin_sec.yaml: 8.13s
validate cfg-full.yaml: 8.07s

Patched (workspace Vector 0.57.0 release build pinned to this VRL):

PATH=~/itl/workspace/vector/target/release:$PATH python3 tools/bench-vrl-runtime.py \
  --scenario mix --events 1 --arms full --work /tmp/sl-bench-patched-rel
corpus /tmp/sl-bench-patched-rel/mix-1.ndjson lines=1 bytes=548 from security-mix.ndjson
validate cfg-no_vrl.yaml: 0.17s
validate cfg-thin_sec.yaml: 0.97s
validate cfg-full.yaml: 0.79s

So cfg-full validate ~8.07s → ~0.79s on that A/B.

How did you test this PR?

  • make all green
  • New unit tests: eight expressions/if_statement files: first-if predicate escape, an assert-heavy success file for cross-arm / type-merge / short-circuit cases, and E701 goldens for else-if pred escape, braced else { if }, else-if body escape, body-->later-pred isolation, multi-stmt else, and third-arm pred + final else
  • Synthetic benchmark driver else_if_ladder_compile
  • Benchmarks of VRL integrated into vector with our real-world VRL workload
  • Full CI suite with modified vector over our corpus of known-in--known-out --> no change

Does this PR include user facing changes?

  • Yes (big perf speedup). Please add a changelog fragment based on
    our guidelines.
  • No. A maintainer will apply the "no-changelog" label to this PR.

Checklist

AI disclosure

I used Cursor Grok 4.5 to help develop this PR (understanding the compiler code base, developing a strategy, writing code and tests). I've personally reviewed the code and believe it's correct. This is my first time in the VRL compiler codebase though, so I greatly appreciate your expertise in reviewing!

References

If you like this PR, consider reviewing my unrelated vector PRs I'm writing up for vectordotdev/vector#25810 and vectordotdev/vector#25815

Locals assigned in if and else-if predicates remain visible after
the chain; locks flatten semantics vs nested else-scope strip.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
Peel parser else { if ... } into multi-arm IfStatement so
typecheck visits each arm once instead of re-walking nested
suffixes. Keeps predicate side-effect order and join semantics.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
Avoid deep-cloning exact object/array Collections and TypeDefs on
every nested path write during progressive typecheck.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
Wrap known fields in SharedMap so TypeState forks clone cheaply
and writes copy-on-write through make_mut.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
Wall-clock harness for synthetic ladders and --program files,
plus --env-width/--env-depth/--env-seed-fields to seed a
spine+stubs ExternalEnv Kind for more realistic compile timing.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
TypeState forks (if arms, compile_expr snapshots) clone locals
cheaply until make_mut; ~2.7x faster compile on a large
--program workload in the ladder harness.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
@klondikedragon
klondikedragon requested a review from a team as a code owner July 23, 2026 19:43

@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: 76838b148a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/compiler/compiler.rs
Comment thread src/compiler/expression/if_statement.rs
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
Flattened typecheck treated later-predicate bindings as definite
after the chain. Re-apply apply_child_scope from post-arm0 locals
and expand if/else-if suite coverage.

Co-authored-by: Cursor Grok 4.5
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
@klondikedragon

Copy link
Copy Markdown
Contributor Author

Update: fixed so that the semantics are now exactly the same as before

Codex flagged that flattening else { if } into a multi-arm IfStatement made locals first assigned in a later predicate look definite after the chain (and treated braced else { if } like else if).

While that was an intentional choice in the original PR to make the behavior of else if predicates more like if predicates, because of how we were peeling, it would lead to an incorrect behavior for a block that did else { if (x = 1; true) { null } } x -- x should never escape there.

Fix: after merging arm states, re-apply apply_child_scope from the local env as it existed after arm 0’s predicate. Bindings first introduced by later predicates are dropped; updates to pre-existing locals still merge. Same rule the old nested else { if } Blocks enforced. Runtime still short-circuits predicates (no change).

Tests: expanded expressions/if_statement coverage (cross-arm visibility, non-escape goldens for else if / braced / body / multi-stmt else / third-arm+final else). Goal of these tests was to show no change in semantics/behavior across various edge cases involve if/else-if/else.

Running tests on main branch vrl: copied these new .vrl files onto main and confirmed all tests pass with unpatched vrl:

cargo run -q -p vrl-tests -- -p if_statement25/25 OK

So these goldens match pre-flatten nested semantics; this commit is not introducing new local-escape behavior and not changing semantics.

@klondikedragon

Copy link
Copy Markdown
Contributor Author

@codex review

@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: abe4e9fc7c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/compiler/compiler.rs Outdated
Signed-off-by: Klondike Dragon <klondikedragon@gmail.com>
@klondikedragon

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 799f72623a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

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