Skip to content

fix: resolve templates of any size and throw RezResolutionException on runaway resolution - #34

Merged
DecSmith42 merged 5 commits into
mainfrom
claude/project-thread-vhd3ok
Oct 14, 2026
Merged

DecSmith42 merged 5 commits into
mainfrom
claude/project-thread-vhd3ok

Conversation

@DecSmith42

Copy link
Copy Markdown
Contributor

Requested by Dec · project thread

Before: the Executor rented fixed 4096-character buffers, so templates or resolved values longer than that threw ArgumentException. The recursion guard counted total substitutions, so a flat template with more than about 4096 independent placeholders threw even though nothing was recursive. When the guard tripped, it threw a bare System.Exception. The XML docs said a variable that resolves to itself hits the limit, but that case actually stops after one pass.

After: buffers are sized to what each pass needs and are always returned to the pool. The limit now applies to nesting depth: Executor.MaxResolutionDepth is 4096. A template with 10,000 flat placeholders resolves, while changing cycles (a = {b}, b = {a}) and ever-growing expansions (a = x{a}) still stop. They throw the new Invex.Rez.Abstraction.RezResolutionException, which derives from Exception, so existing catch blocks keep working. The docs describe the depth limit and the exception accurately.

How: each character carries the depth of the substitution that produced it, and a placeholder resolves one level deeper than the deepest character it covers. The renderer is split into TryFindSubstitution and Substitute. No public API was removed. New tests cover long templates, many flat placeholders, a chain at the maximum depth and one past it, growing expansions, and nested placeholders. All 107 net10.0 tests pass locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU


Generated by Claude Code

The executor rented fixed 4096-element buffers, so templates or rendered text
of 4095+ characters threw ArgumentException ("Destination is too short").
Buffers are now rented at the size each pass needs and always returned, even
when resolution throws.

The resolution depth limit now throws the new public RezResolutionException
(derives from Exception, so existing catch blocks still work). XML docs now
say a direct self-reference (a = {a}) stabilises and only cycles whose output
keeps changing hit the limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU
…ding

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU
The 4096 guard counted every substitution, so a flat template with more than
4097 placeholders threw RezResolutionException with no recursion at all.

Each character now carries the depth of the substitution that produced it
(0 for template text). A placeholder resolves at one more than the deepest
character it spans, and the guard throws only when that exceeds 4096. Changing
cycles (a = {b}, b = {a}) and ever-growing expansions (a = x{a}) still throw;
any number of independent placeholders does not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU
Copilot AI balanced review requested due to automatic review settings October 14, 2026 03:32

Copilot AI 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.

🟡 Changes recommended

Recursive tokenization can still terminate the process on deeply nested input, and failed buffer growth can return pooled arrays twice.

3 open findings
What changed in this PR

Updates resolution to support large templates while enforcing recursion by substitution depth.

Changes:

  • Adds dynamic pooled buffers and per-character depth tracking.
  • Introduces RezResolutionException.
  • Expands tests and documentation for limits and recursive behavior.
File Description
src/​Invex.Rez/​Compiler/​Executor.cs Implements buffer growth and depth tracking.
src/​Invex.Rez/​Compiler/​Renderer.cs Separates substitution discovery and rendering.
src/​Invex.Rez/​Abstraction/​RezResolutionException.cs Adds the public resolution exception.
src/​Invex.Rez/​Abstraction/​IResolver.cs Documents recursion behavior and exception.
src/​Invex.Rez/​Implementation/​Resolver.cs Corrects recursive-resolution documentation.
src/​Invex.Rez.Configuration/​IResolvableConfig.cs Documents propagated resolution failures.
tests/​Invex.Rez.Tests/​Compiler/​ExecutorTests.cs Covers size and depth boundaries.
tests/​Invex.Rez.Tests/​Implementation/​RezzerTests.cs Verifies the public exception behavior.
README.md Updates the recursion feature description.
docs/​syntax.md Documents depth semantics.
docs/​introduction.md Updates the architectural overview.
docs/​developer-guide.md Explains depth tracking and the exception.
api/​index.md Lists the new public exception.
AGENTS.md Updates repository guidance and contracts.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Invex.Rez/Compiler/Executor.cs
Comment thread src/Invex.Rez/Compiler/Executor.cs Outdated
Comment thread src/Invex.Rez/Compiler/Executor.cs Outdated
If ArrayPool.Rent threw while growing a buffer, the old array had already been
returned and the finally block would return it a second time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU
Tokenizer recursed once per open brace, so deeply nested or long runs of
unmatched braces overflowed the stack (100k '{' crashed the process). On main
the fixed 4096-char buffer kept nesting shallow enough; lifting it exposed the
overflow. Open braces are now tracked on a pooled explicit stack, producing
the same tokens as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy23TCq1bZkdPV2GRWfspU
Copilot AI balanced review requested due to automatic review settings October 14, 2026 03:38

Copilot AI 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.

🔵 Needs a closer look

Initial pooled-buffer acquisition is not exception-safe and can abandon previously rented arrays.

0 open findings

3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Initial pool rentals leak arrays when a later Rent fails

src/​Invex.Rez/​Compiler/​Executor.cs:57

All six initial pool rentals happen before the try. If any later Rent fails (most plausibly for a large template under memory pressure), every array already acquired is abandoned instead of being returned, contrary to the new ownership guarantee and making recovery from the allocation failure harder. Move acquisition under exception-safe ownership—for example, rent through a helper that returns previously acquired arrays on failure, or use nested try/finally scopes.

🧠 Review effort: Balanced

@DecSmith42
DecSmith42 merged commit 5ef6cad into main Oct 14, 2026
10 checks passed
@DecSmith42
DecSmith42 deleted the claude/project-thread-vhd3ok branch October 14, 2026 03:49
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.

3 participants