Skip to content

feat: add watch expression registry core - #1730

Merged
hatayama merged 3 commits into
feat/pause-point-watchfrom
feat/pause-point-watch-core
Jul 12, 2026
Merged

feat: add watch expression registry core#1730
hatayama merged 3 commits into
feat/pause-point-watchfrom
feat/pause-point-watch-core

Conversation

@hatayama

@hatayama hatayama commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary\n\n- add an in-memory WatchExpressionRegistry with ordered registration, immediate baseline evaluation, bounded history, and clear operations\n- detect paused Play Mode frame changes through an EditorApplication.update monitor with same-frame suppression and re-entry protection\n- reuse the execute-dynamic-code compiler to wrap expressions and convert only generated evaluator invocation failures into typed Result DTOs\n\n## Validation\n\n- Unity compile: 0 errors, 0 warnings\n- targeted EditMode tests: 9/9 passed\n- compile-only watch expression test covers the shared dynamic compiler without executing user code\n- evaluation failure test confirms exception type and message are retained as an explicit failure result

Review in cubic

Provide a pure C# registry for ordered per-frame watch evaluation, bounded history, immediate baselines, and re-entry protection. Reuse the dynamic-code compiler and convert only generated evaluator invocation failures into typed history results so Editor updates continue after user expression errors.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ed5dcbc6-9203-4608-b1da-490a615cb75b

📥 Commits

Reviewing files that changed from the base of the PR and between 25577d7 and fb7ec06.

📒 Files selected for processing (6)
  • Assets/Tests/Editor/WatchExpressionCompilerTests.cs
  • Assets/Tests/Editor/WatchExpressionRegistryTests.cs
  • Packages/src/Editor/FirstPartyTools/AssemblyInfo.cs
  • Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionEntry.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionRegistry.cs

📝 Walkthrough

Walkthrough

Introduces a watch-expression subsystem that compiles expressions, captures evaluation failures, stores bounded evaluation history, and evaluates registered expressions on paused Unity Editor frame changes. NUnit and Unity tests cover compiler, evaluator, and registry behavior.

Changes

Watch expressions

Layer / File(s) Summary
Compile and evaluate expressions
Packages/src/Editor/FirstPartyTools/AssemblyInfo.cs, Packages/src/Editor/FirstPartyTools/Watch/*Result.cs, WatchExpressionCompiler.cs, CompiledWatchExpressionEvaluator.cs, IWatchExpressionEvaluator.cs, Assets/Tests/Editor/WatchExpressionCompilerTests.cs, Assets/Tests/Editor/WatchExpressionEvaluationTests.cs
Adds compilation and evaluation result types, dynamic source compilation, reflective evaluator invocation, exception conversion, internal test visibility, and tests for successful, invalid, and throwing expressions.
Register and evaluate watch history
Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionEntry.cs, WatchExpressionHistoryEntry.cs, WatchExpressionRegistry.cs, WatchRegistrationResult.cs, IWatchEditorStateProvider.cs, Assets/Tests/Editor/WatchExpressionRegistryTests.cs
Adds ordered watch registration, paused-frame evaluation, bounded history, removal APIs, dropped-entry tracking, and deterministic registry tests.
Connect evaluation to Unity Editor updates
Packages/src/Editor/FirstPartyTools/Watch/UnityWatchEditorStateProvider.cs, WatchExpressionStepMonitor.cs
Maps Unity Editor state to the watch state provider and subscribes registry evaluation to EditorApplication.update with guarded start and stop operations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WatchExpressionStepMonitor
  participant WatchExpressionRegistry
  participant UnityWatchEditorStateProvider
  participant IWatchExpressionEvaluator
  WatchExpressionStepMonitor->>WatchExpressionRegistry: EvaluateIfFrameChanged()
  WatchExpressionRegistry->>UnityWatchEditorStateProvider: read paused state and frame count
  WatchExpressionRegistry->>IWatchExpressionEvaluator: Evaluate()
  IWatchExpressionEvaluator-->>WatchExpressionRegistry: WatchEvaluationResult
  WatchExpressionRegistry->>WatchExpressionRegistry: record bounded history
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding the watch expression registry core.
Description check ✅ Passed The description accurately summarizes the registry, monitor, compiler reuse, and validation, so it aligns with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pause-point-watch-core

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs (2)

14-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Debug.Assert precondition checks to the constructor.

As an internal class, null instance or evaluateMethod are programmer errors. Debug.Assert would surface these early during development rather than as a later NullReferenceException inside Evaluate(). Based on learnings, internal Editor code should use Debug.Assert for preconditions as part of Contract Programming and Fail Fast.

🔧 Suggested addition
 using System;
 using System.Reflection;
+using UnityEngine;
 
 namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
 {
     internal sealed class CompiledWatchExpressionEvaluator : IWatchExpressionEvaluator
     {
         private readonly object _instance;
         private readonly MethodInfo _evaluateMethod;
 
         public CompiledWatchExpressionEvaluator(object instance, MethodInfo evaluateMethod)
         {
+            Debug.Assert(instance != null, "instance must not be null");
+            Debug.Assert(evaluateMethod != null, "evaluateMethod must not be null");
             _instance = instance;
             _evaluateMethod = evaluateMethod;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs`
around lines 14 - 18, Add Debug.Assert precondition checks at the start of
CompiledWatchExpressionEvaluator’s constructor to validate that instance and
evaluateMethod are non-null before assigning them to _instance and
_evaluateMethod.

Source: Learnings


20-39: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider narrowing the catch to TargetInvocationException.

catch (Exception) converts infrastructure exceptions from MethodInfo.Invoke (e.g., MethodAccessException, TargetException) into user-code failure results, masking genuine bugs. The expected exception is TargetInvocationException wrapping user code. Based on learnings, avoid broad defensive try-catch for unexpected exceptions; only catch expected, domain-specific exceptions and let genuine bugs surface.

The practical risk is low since the compiler validates the method before invocation, so marking as optional.

🔧 Suggested narrowing
         public WatchEvaluationResult Evaluate()
         {
-            // Why: only the generated user-code Invoke may throw during normal watch evaluation;
-            // converting that exception keeps the Editor update loop alive and preserves the error in history.
-            try
-            {
-                object value = _evaluateMethod.Invoke(_instance, null);
-                return WatchEvaluationResult.SuccessResult(value);
-            }
-            catch (Exception exception)
-            {
-                Exception userException = exception is TargetInvocationException
-                    && exception.InnerException != null
-                    ? exception.InnerException
-                    : exception;
-                return WatchEvaluationResult.FailureResult(
-                    userException.GetType().FullName ?? userException.GetType().Name,
-                    userException.Message);
-            }
+            // Why: TargetInvocationException wraps user-code exceptions from the generated Evaluate method;
+            // converting it keeps the Editor update loop alive and preserves the error in history.
+            // Infrastructure exceptions from Invoke are not caught — they surface as bugs.
+            try
+            {
+                object value = _evaluateMethod.Invoke(_instance, null);
+                return WatchEvaluationResult.SuccessResult(value);
+            }
+            catch (TargetInvocationException tie) when (tie.InnerException != null)
+            {
+                Exception userException = tie.InnerException;
+                return WatchEvaluationResult.FailureResult(
+                    userException.GetType().FullName ?? userException.GetType().Name,
+                    userException.Message);
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs`
around lines 20 - 39, In CompiledWatchExpressionEvaluator.Evaluate, narrow the
catch from Exception to TargetInvocationException so only user-code invocation
failures are converted into WatchEvaluationResult.FailureResult. Preserve
unwrapping the InnerException when present, and allow infrastructure exceptions
from MethodInfo.Invoke to propagate.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Assets/Tests/Editor/WatchExpressionRegistryTests.cs`:
- Around line 55-69: Correct the test
`EvaluateIfFrameChanged_WhenNotPaused_DoesNotEvaluate` to leave
`stateProvider.IsPaused` false before calling `EvaluateIfFrameChanged`, then
assert that no history entries or evaluator calls occur. Preserve the existing
paused evaluation behavior in a separate test if needed, rather than validating
it under the not-paused test name.

In `@Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionStepMonitor.cs`:
- Around line 13-16: Add an ArgumentNullException guard for the registry
parameter in WatchExpressionStepMonitor’s constructor before assigning it to
_registry, matching WatchExpressionRegistry’s stateProvider validation pattern
and preserving fail-fast behavior.

---

Nitpick comments:
In
`@Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs`:
- Around line 14-18: Add Debug.Assert precondition checks at the start of
CompiledWatchExpressionEvaluator’s constructor to validate that instance and
evaluateMethod are non-null before assigning them to _instance and
_evaluateMethod.
- Around line 20-39: In CompiledWatchExpressionEvaluator.Evaluate, narrow the
catch from Exception to TargetInvocationException so only user-code invocation
failures are converted into WatchEvaluationResult.FailureResult. Preserve
unwrapping the InnerException when present, and allow infrastructure exceptions
from MethodInfo.Invoke to propagate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f260fcf-e1e8-4676-900e-d21af3902520

📥 Commits

Reviewing files that changed from the base of the PR and between 0b5e72e and 25577d7.

⛔ Files ignored due to path filters (17)
  • Assets/Tests/Editor/WatchExpressionCompilerTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/WatchExpressionEvaluationTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/WatchExpressionRegistryTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/AssemblyInfo.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/IWatchEditorStateProvider.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/IWatchExpressionEvaluator.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/UnityWatchEditorStateProvider.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchCompilationResult.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchEvaluationResult.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionCompiler.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionEntry.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionHistoryEntry.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionRegistry.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionStepMonitor.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Watch/WatchRegistrationResult.cs.meta is excluded by none and included by none
📒 Files selected for processing (16)
  • Assets/Tests/Editor/WatchExpressionCompilerTests.cs
  • Assets/Tests/Editor/WatchExpressionEvaluationTests.cs
  • Assets/Tests/Editor/WatchExpressionRegistryTests.cs
  • Packages/src/Editor/FirstPartyTools/AssemblyInfo.cs
  • Packages/src/Editor/FirstPartyTools/Watch/CompiledWatchExpressionEvaluator.cs
  • Packages/src/Editor/FirstPartyTools/Watch/IWatchEditorStateProvider.cs
  • Packages/src/Editor/FirstPartyTools/Watch/IWatchExpressionEvaluator.cs
  • Packages/src/Editor/FirstPartyTools/Watch/UnityWatchEditorStateProvider.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchCompilationResult.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchEvaluationResult.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionCompiler.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionEntry.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionHistoryEntry.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionRegistry.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionStepMonitor.cs
  • Packages/src/Editor/FirstPartyTools/Watch/WatchRegistrationResult.cs

Comment thread Assets/Tests/Editor/WatchExpressionRegistryTests.cs
Comment on lines +13 to +16
public WatchExpressionStepMonitor(WatchExpressionRegistry registry)
{
_registry = registry;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add null check for registry in the constructor.

If registry is null, OnEditorUpdate throws NullReferenceException on every EditorApplication.update tick. WatchExpressionRegistry validates its stateProvider parameter with ArgumentNullException — this constructor should follow the same pattern for consistency and fail-fast behavior.

🛡️ Proposed fix
         public WatchExpressionStepMonitor(WatchExpressionRegistry registry)
         {
-            _registry = registry;
+            _registry = registry ?? throw new ArgumentNullException(nameof(registry));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public WatchExpressionStepMonitor(WatchExpressionRegistry registry)
{
_registry = registry;
}
public WatchExpressionStepMonitor(WatchExpressionRegistry registry)
{
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/src/Editor/FirstPartyTools/Watch/WatchExpressionStepMonitor.cs`
around lines 13 - 16, Add an ArgumentNullException guard for the registry
parameter in WatchExpressionStepMonitor’s constructor before assigning it to
_registry, matching WatchExpressionRegistry’s stateProvider validation pattern
and preserving fail-fast behavior.

hatayama added 2 commits July 12, 2026 19:01
Keep per-watch frame progress independent so registering a new expression cannot skip existing watches. Bound compile-only test waits, document every test behavior, add direct clear coverage, and cache compiled evaluator delegates to keep reflection out of the per-frame path.
Restore the evaluator field indentation so the test follows the surrounding formatting without changing behavior.
@hatayama
hatayama merged commit 6d7111e into feat/pause-point-watch Jul 12, 2026
1 check passed
@hatayama
hatayama deleted the feat/pause-point-watch-core branch July 12, 2026 10:03
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