Skip to content

fix: Pause points inside local functions now show this and every variable they use from the enclosing scopes - #3157

Merged
hatayama merged 2 commits into
mainfrom
fix/pause-point-closure-frame-this
Oct 5, 2026
Merged

hatayama merged 2 commits into
mainfrom
fix/pause-point-closure-frame-this

Conversation

@hatayama

@hatayama hatayama commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

User Impact

  • Before: when a local function captured variables, a pause point in it could miss most of them. In the issue's shape (a loop body declaring a lambda that captures the loop variable plus a local function that uses the loop variable, a method parameter, and an instance field), only the loop variable was captured: no this, no instance fields, no method parameter. The same gap appeared in two more shapes found while fixing it:
    • a local function of an ordinary instance method showed this but lost the enclosing method's locals;
    • a local function that uses two enclosing scopes (for example the method and an if block) showed only the variables of one of them.
  • Before: the enable and status responses listed an unnamed entry (ref/out/in parameter cannot be boxed) under NotCapturableVariables for these local functions, which looked like a capture bug.
  • Before: a --hit-when condition on such a variable could not find it, so the marker reported an evaluation error and hit regardless of the condition.
  • After: all of those variables are captured with their values, this resolves to the real instance, the unnamed entry is gone, and --hit-when evaluates the condition against the captured value.

Changes

  • The compiler passes a capturing local function one by-ref closure struct per enclosing scope it uses, after its own declared parameters. The injected capture call now boxes every one of those structs and hands them over as a separate argument, reading each from its own argument slot (shifted by one for the hidden this of an instance method). Before, only one struct was passed, and only when the local function was static.
  • The variable collector walks the closure holder and every struct, lists their variables after locals and parameters but before this and the instance fields (so the count cap keeps favoring variables), and takes this from the first link to the instance it finds, whichever struct carries it.
  • A by-ref closure struct argument is no longer reported as a parameter that cannot be captured, on both the compiled and the hot-reload resolution paths. Ordinary ref/out/in parameters are still reported as before.

Verification

  • uloop compile: success, no new warnings.

  • Red before the fix, green after: SourcePausePointClosureFrameCaptureTests (new, 11 tests). It covers the issue's loop-closure shape, an instance method's local function, a static local function (regression guard for Pause point in a local function of a hot-reloaded method shows no this #3066, passing before and after), a local function using two scopes, an instance local function whose structs follow its own declared parameter, a --hit-when condition on a struct-only variable, and the NotCapturableVariables report for each shape.

  • Green: SourcePausePointCaptureTests (23, including three new collector tests for multiple structs, ordering, and a receiver found in a later struct), HotReloadPausePointContractTests (25, including a new hot-reload test for the issue's loop-closure shape and the existing fix: Pause points in a hot-reloaded method's local functions now show the instance as this #3071 test), and SourcePausePointTruncationAggregateTests, SourcePausePointVariableFormatterTests, PausePointToolModeTests, SourcePausePointNotCapturableParametersTests, PausePointNotCapturableVariablesTests, SourcePausePointPatcherTests, SourcePausePointPostLineCaptureTests (140 together).

  • CA1502 complexity check and the file-length check: no findings.

  • Mutation check: making every struct load from the first struct's argument slot fails 3 of the new tests (the second struct's variable reads 4 or a garbage value instead of 3). The multi-struct tests call with values that make each struct's variables differ, so a wrong slot cannot reproduce the expected values.

Not verified

  • The new hot-reload test was not observed failing before the fix: changing the capture signature makes the old code not compile against the new tests. It goes through the same injection branch as the compiled loop-closure test, which was observed failing before the fix.
  • No mutation run that drops the one-slot shift for an instance method's hidden this: that IL would load an int argument as a pointer and could crash the Editor. The test whose structs follow a declared parameter would read the wrong argument in that case.

A local function that captures variables gets them, and often the instance,
in by-ref closure structs. The injection only passed one of those structs,
and only when the local function was static. A local function compiled as an
instance method of a lambda's closure class therefore showed neither `this`
nor the variables of the method scope, an instance local function lost the
enclosing locals, and a local function using two scopes lost one of them.

- The injection now boxes every by-ref closure struct and passes them to
  Capture as a separate array, loading each from its own argument slot
  (shifted by one for the hidden `this` of an instance method).
- The collector walks the instance holder and every struct, lists their
  variables before the instance state, and takes `this` from the first link
  it finds, so the count cap still favors variables over fields.
- A closure struct argument is no longer reported as a ref parameter that
  cannot be captured, since its variables are captured through it.
- A hit-when condition on a variable that only a struct carries is now
  evaluated instead of failing to find it.

Closes #3072
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: hatayama/unity-cli-loop/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7ecf5eff-0feb-41a0-9778-c0135d438ef8
📥 Commits

Reviewing files that changed from the base of the PR and between 1a11df6 and ce83095.

📒 Files selected for processing (1)
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointClosureFrameCaptureTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Pause point capture now passes closure frames to variable collection. The collector scans those frames to report captured variables and resolve this. Tests cover local functions across closure shapes, hot reload, and hit-when conditions.

Changes

Closure-frame pause point capture

Layer / File(s) Summary
Discover and pass closure frames
Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointClosureFrameArgument.cs, Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointInjectionEmitter.cs, Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCapture.cs
The emitter finds all by-reference closure-frame arguments and adds boxed frames to the capture payload. Capture and CaptureFrame accept the closure-frame array.
Resolve closure variables and instance
Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableCollector.cs, Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCaptureEligibility.cs, Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs
The collector scans closure frames and holder links before adding the resolved instance and its fields. Capture eligibility omits closure-frame parameters from not-capturable reports.
Validate closure-frame capture
Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs, Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointTruncationAggregateTests.cs, Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherClosureFrameLocalFunctionFixture.cs, Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointClosureFrameCaptureTests.cs, Assets/Tests/Editor/HotReload/HotReloadPausePointContractTests.cs, Assets/Tests/Editor/PausePointToolModeTests.cs
Tests cover frame-variable ordering, this resolution, local-function captures, resolver eligibility, hit-when evaluation, and hot-reloaded local functions. Existing capture calls pass the added frame-array argument.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant SourcePausePointInjectionEmitter
  participant SourcePausePointCapture
  participant SourcePausePointVariableCollector
  SourcePausePointInjectionEmitter->>SourcePausePointCapture: Pass instance, closure frames, parameters, and locals
  SourcePausePointCapture->>SourcePausePointVariableCollector: Collect captured variables and resolve this
Loading

Merge Risk: ⚪ Minimal · up to ce830

Closure-frame values flow through pause-point capture in the expected slots, and the supplied tests cover nested and hot-reloaded local functions. No actionable merge-blocking risk remains beyond normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to ce830

The change exposes additional enclosing-scope values through existing pause-point functionality. No new externally reachable operation or security-control bypass was identified, and existing hit and cleanup behavior remains unchanged. End-to-end authorization and interrupted-capture behavior were not fully established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The evidenced exposure is additional enclosing-scope data in the affected Unity editor's pause-point snapshots and latest raw capture. Boxed frames do not write back into closure structs, but captured reference-valued entries still identify live objects. Tenant, remote-service, credential, and deployment exposure cannot be bounded from the inspected flow.

Trust Boundaries and Controls

  • observed — The existing enabling path accepts marker or source-location inputs, validates capture settings, and resolves a method before patching. Publication rechecks marker existence, expiry, and enabled state, rejecting disabled or cleared late hits. These are capture-state controls, not proof of transport authentication or client authorization.

Resilience and Maintainability Implications

  • observed — The existing latest-capture holder publishes its value dictionary atomically. Explicit clear checks capture ownership before dropping raw references, and test reset clears the holder. The PR adds values to this mechanism without changing its ownership or cleanup implementation. Existing formatting also catches exceptions from user-defined ToString calls.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.40% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 73 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #3072 requires pause points in local functions to capture this, instance fields, and variables from every enclosing scope in compiled and hot-reloaded methods. The capture injection passes eac…
Out of Scope Changes check ✅ Passed The capture, injection, collector, and closure-frame eligibility changes implement issue #3072. The API call updates, fixtures, and tests support those changes. The static-local-function regression co…
Title check ✅ Passed The title clearly summarizes the main change: pause points in local functions now capture this and variables from enclosing scopes.
Description check ✅ Passed The description explains the capture issue, the changes, their user impact, and the reported verification. It is directly related to the changeset.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

With delta 1, offset and inner were both 2, so a capture that read every
struct from the first struct's argument still produced the expected values.
Calling with 2 makes them 4 and 3, so each struct must come from its own
argument for the tests to pass.
@hatayama
hatayama merged commit 4cbce9d into main Oct 5, 2026
17 checks passed
@hatayama
hatayama deleted the fix/pause-point-closure-frame-this branch October 5, 2026 04:05
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.

Pause point in a local function of a closure class misses this when the instance is in a by-ref closure struct

1 participant