diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md
index d5d4edc3df..42208832b9 100644
--- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md
+++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md
@@ -32,6 +32,12 @@ return x;
Prefer terminal commands for file operations and keep snippets focused on Unity Editor state that existing uloop tools cannot inspect or change.
+## Known transpiler constraints
+
+- Literals inside recognized static local function bodies are kept inline automatically. Unsupported header shapes (generic `where` clauses, tuple return types, statement lambdas inside expression bodies) may still hoist literals and surface CS8421; remove `static` or rewrite the helper.
+- Static lambdas (`static x => ...`) cannot reference hoisted literals and surface CS8820; remove `static` from the lambda or use a non-static local function.
+- Integer literals are hoisted as `int` values. APIs that require `byte` components (for example `new Color32(255, 0, 0, 255)`) need explicit casts such as `(byte)255` even when plain Unity scripts accept uncast numeric literals.
+
## Shell Quoting
- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`.
diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md
index d5d4edc3df..42208832b9 100644
--- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md
+++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md
@@ -32,6 +32,12 @@ return x;
Prefer terminal commands for file operations and keep snippets focused on Unity Editor state that existing uloop tools cannot inspect or change.
+## Known transpiler constraints
+
+- Literals inside recognized static local function bodies are kept inline automatically. Unsupported header shapes (generic `where` clauses, tuple return types, statement lambdas inside expression bodies) may still hoist literals and surface CS8421; remove `static` or rewrite the helper.
+- Static lambdas (`static x => ...`) cannot reference hoisted literals and surface CS8820; remove `static` from the lambda or use a non-static local function.
+- Integer literals are hoisted as `int` values. APIs that require `byte` components (for example `new Color32(255, 0, 0, 255)`) need explicit casts such as `(byte)255` even when plain Unity scripts accept uncast numeric literals.
+
## Shell Quoting
- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`.
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionResponseFactoryTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionResponseFactoryTests.cs
index fc920c4566..6c53eb7d55 100644
--- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionResponseFactoryTests.cs
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionResponseFactoryTests.cs
@@ -213,5 +213,37 @@ public void CancelledResult_WhenMapped_ReturnsNeutralCancellationResponse()
Assert.That(response.CompilationErrors, Is.Empty);
Assert.That(response.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED));
}
+
+ ///
+ /// Verifies int-to-byte conversion failures include transpiler constraint guidance.
+ ///
+ [Test]
+ public void ConvertExecutionResultToResponse_WhenColor32ByteConversionFails_AddsTranspilerConstraintHint()
+ {
+ DynamicCodeExecutionResponseFactory factory = new();
+ ExecutionResult result = new()
+ {
+ Success = false,
+ ErrorMessage = "Compilation error occurred",
+ CompilationErrors = new List
+ {
+ new CompilationError
+ {
+ ErrorCode = "CS1503",
+ Message = "CS1503: Argument 1: cannot convert from 'int' to 'byte'",
+ Line = 2,
+ Column = 20
+ }
+ }
+ };
+
+ ExecuteDynamicCodeResponse response = factory.ConvertExecutionResultToResponse(
+ result,
+ "using UnityEngine;\nreturn new Color32(255, 0, 0, 255);");
+
+ Assert.That(response.Diagnostics[0].Hint, Does.Contain("Color32"));
+ Assert.That(response.Diagnostics[0].Suggestions, Contains.Item(
+ "Cast each component explicitly, for example: new Color32((byte)255, (byte)0, (byte)0, (byte)255)."));
+ }
}
}
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs
index 8fd222f37f..6dcdaa10e7 100644
--- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs
@@ -21,5 +21,81 @@ public void Rewrite_WhenInterpolatedHoleContainsNestedStringLiteral_ShouldKeepIn
Assert.That(result.Bindings, Is.Empty);
Assert.That(result.DeclarationLines, Is.Empty);
}
+
+ ///
+ /// Verifies literals inside a nested static local function stay inline while outer literals still hoist.
+ ///
+ [Test]
+ public void Rewrite_WhenIntegerLiteralIsInsideStaticLocalFunction_ShouldKeepLiteralInline()
+ {
+ string source = @"int Compute(int value)
+{
+ static int Double(int x) => x * 2;
+ return Double(value);
+}
+return Compute(3);";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Does.Contain("x * 2"));
+ Assert.That(result.RewrittenSource, Does.Contain("return Compute(__uloop_literal_0)"));
+ Assert.That(result.RewrittenSource, Does.Not.Contain("x * __uloop_literal"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(3));
+ }
+
+ ///
+ /// Verifies string literals inside a nested static local block body are not hoisted.
+ ///
+ [Test]
+ public void Rewrite_WhenStringLiteralIsInsideStaticLocalFunctionBlockBody_ShouldKeepLiteralInline()
+ {
+ string source = @"string Build()
+{
+ static string Label() { return ""ok""; }
+ return Label();
+}
+return Build();";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Does.Contain("return \"ok\""));
+ Assert.That(result.RewrittenSource, Does.Not.Contain("__uloop_literal_0"));
+ Assert.That(result.Bindings, Is.Empty);
+ }
+
+ ///
+ /// Verifies top-level expression-bodied static local functions keep literals inline at brace depth zero.
+ ///
+ [Test]
+ public void Rewrite_WhenTopLevelExpressionBodiedStaticLocalFunction_ShouldKeepLiteralInline()
+ {
+ string source = @"static int Double(int x) => x * 2;
+return Double(3);";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Does.Contain("x * 2"));
+ Assert.That(result.RewrittenSource, Does.Not.Contain("x * __uloop_literal"));
+ Assert.That(result.RewrittenSource, Does.Contain("return Double(__uloop_literal_0)"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(3));
+ }
+
+ ///
+ /// Verifies top-level block-bodied static local functions keep literals inline at brace depth zero.
+ ///
+ [Test]
+ public void Rewrite_WhenTopLevelBlockBodiedStaticLocalFunction_ShouldKeepLiteralInline()
+ {
+ string source = @"static string Label() { return ""ok""; }
+return Label();";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Does.Contain("return \"ok\""));
+ Assert.That(result.RewrittenSource, Does.Not.Contain("__uloop_literal_0"));
+ Assert.That(result.Bindings, Is.Empty);
+ }
}
}
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs
new file mode 100644
index 0000000000..51b457454d
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs
@@ -0,0 +1,58 @@
+using NUnit.Framework;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
+{
+ ///
+ /// Test fixture that verifies transpiler constraint hint generation.
+ ///
+ [TestFixture]
+ public class DynamicCodeTranspilerConstraintHintsTests
+ {
+ ///
+ /// Verifies CS8421 hoisted-literal failures suggest removing the static modifier.
+ ///
+ [Test]
+ public void TryBuildHint_WhenCs8421ReferencesHoistedLiteral_ShouldReturnStaticLocalFunctionGuidance()
+ {
+ (bool matched, string hint, string suggestion) = DynamicCodeTranspilerConstraintHints.TryBuildHint(
+ "CS8421",
+ "CS8421: A static local function cannot contain a reference to '__uloop_literal_0'.");
+
+ Assert.That(matched, Is.True);
+ Assert.That(hint, Does.Contain("recognized static local function bodies"));
+ Assert.That(suggestion, Is.EqualTo("Remove the `static` modifier from the local function."));
+ }
+
+ ///
+ /// Verifies CS8820 static lambda failures suggest removing the static modifier.
+ ///
+ [Test]
+ public void TryBuildHint_WhenCs8820ReferencesHoistedLiteral_ShouldReturnStaticLambdaGuidance()
+ {
+ (bool matched, string hint, string suggestion) = DynamicCodeTranspilerConstraintHints.TryBuildHint(
+ "CS8820",
+ "CS8820: A static anonymous function cannot contain a reference to '__uloop_literal_0'.");
+
+ Assert.That(matched, Is.True);
+ Assert.That(hint, Does.Contain("Static lambdas"));
+ Assert.That(suggestion, Is.EqualTo("Remove the `static` modifier from the lambda."));
+ }
+
+ ///
+ /// Verifies int-to-byte conversion failures include explicit cast guidance for Color32.
+ ///
+ [Test]
+ public void TryBuildHint_WhenCs1503CannotConvertIntToByte_ShouldReturnExplicitCastGuidance()
+ {
+ (bool matched, string hint, string suggestion) = DynamicCodeTranspilerConstraintHints.TryBuildHint(
+ "CS1503",
+ "CS1503: Argument 1: cannot convert from 'int' to 'byte'");
+
+ Assert.That(matched, Is.True);
+ Assert.That(hint, Does.Contain("Color32"));
+ Assert.That(suggestion, Does.Contain("(byte)255"));
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs.meta
new file mode 100644
index 0000000000..3b16043ea3
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTranspilerConstraintHintsTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 793fe6f168d864edb9afa78f79729471
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeExecutionResponseFactory.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeExecutionResponseFactory.cs
index 4f34b76f22..e140477806 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeExecutionResponseFactory.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeExecutionResponseFactory.cs
@@ -300,6 +300,18 @@ private static (string hint, List suggestions) GetHintAndSuggestions(
return (hint, suggestions);
default:
+ (bool matched, string constraintHint, string constraintSuggestion) = DynamicCodeTranspilerConstraintHints.TryBuildHint(
+ error.ErrorCode,
+ error.Message);
+ if (matched)
+ {
+ hint = constraintHint;
+ if (!string.IsNullOrEmpty(constraintSuggestion))
+ {
+ suggestions.Add(constraintSuggestion);
+ }
+ }
+
return (hint, suggestions);
}
}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
index d6d8b91810..cb5ff4942e 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
@@ -10,7 +10,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
///
internal static class DynamicCodeLiteralHoister
{
- private const string LiteralParameterPrefix = "__uloop_literal_";
+ internal const string LiteralParameterPrefix = "__uloop_literal_";
private static readonly Dictionary RegularStringEscapes = new()
{
{ '\'', '\'' },
@@ -30,6 +30,7 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
{
StringBuilder rewrittenSource = new(source.Length);
List bindings = new();
+ LiteralHoistScopeTracker scopeTracker = new();
int index = 0;
while (index < source.Length)
@@ -59,12 +60,49 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
continue;
}
- if (TryHoistRegularStringLiteral(source, rewrittenSource, bindings, ref index))
+ if (scopeTracker.TryConsumeStaticLocalFunctionHeader(source, index, rewrittenSource, ref index))
{
continue;
}
- if (TryHoistIntegerLiteral(source, rewrittenSource, bindings, ref index))
+ if (source[index] == '{')
+ {
+ scopeTracker.OnOpenBrace();
+ rewrittenSource.Append('{');
+ index++;
+ continue;
+ }
+
+ if (source[index] == '}')
+ {
+ scopeTracker.OnCloseBrace();
+ rewrittenSource.Append('}');
+ index++;
+ continue;
+ }
+
+ if (source[index] == ';')
+ {
+ scopeTracker.OnSemicolon();
+ rewrittenSource.Append(';');
+ index++;
+ continue;
+ }
+
+ if (scopeTracker.ShouldSuppressLiteralHoisting)
+ {
+ if (TryCopyRegularStringLiteral(source, rewrittenSource, ref index))
+ {
+ continue;
+ }
+ }
+ else if (TryHoistRegularStringLiteral(source, rewrittenSource, bindings, ref index))
+ {
+ continue;
+ }
+
+ if (!scopeTracker.ShouldSuppressLiteralHoisting
+ && TryHoistIntegerLiteral(source, rewrittenSource, bindings, ref index))
{
continue;
}
@@ -551,6 +589,47 @@ private static bool TryCopyCharLiteral(
return false;
}
+ private static bool TryCopyRegularStringLiteral(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ if (source[index] != '"')
+ {
+ return false;
+ }
+
+ if (index > 0 && (source[index - 1] == '@' || source[index - 1] == '$'))
+ {
+ return false;
+ }
+
+ int start = index;
+ index++;
+
+ while (index < source.Length)
+ {
+ char current = source[index];
+ if (current == '\\')
+ {
+ AdvanceEscapedLiteralSequence(source, ref index);
+ continue;
+ }
+
+ if (current == '"')
+ {
+ index++;
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ index++;
+ }
+
+ index = start;
+ return false;
+ }
+
private static bool TryHoistRegularStringLiteral(
string source,
StringBuilder rewrittenSource,
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs
new file mode 100644
index 0000000000..c6ee4033e9
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Builds compilation hints for execute-dynamic-code transpiler constraints.
+ ///
+ internal static class DynamicCodeTranspilerConstraintHints
+ {
+ internal static (bool Matched, string Hint, string Suggestion) TryBuildHint(
+ string errorCode,
+ string message)
+ {
+ if (string.Equals(errorCode, "CS8421", StringComparison.Ordinal)
+ && message.Contains(DynamicCodeLiteralHoister.LiteralParameterPrefix, StringComparison.Ordinal))
+ {
+ return (
+ true,
+ "Static local functions cannot reference hoisted literal parameters. "
+ + "Literals inside recognized static local function bodies are kept inline automatically; "
+ + "if this error persists, the header shape may be unsupported by the scanner.",
+ "Remove the `static` modifier from the local function.");
+ }
+
+ if (string.Equals(errorCode, "CS8820", StringComparison.Ordinal)
+ && message.Contains(DynamicCodeLiteralHoister.LiteralParameterPrefix, StringComparison.Ordinal))
+ {
+ return (
+ true,
+ "Static lambdas cannot reference hoisted literal parameters. "
+ + "Remove the `static` modifier from the lambda, or use a non-static local function instead.",
+ "Remove the `static` modifier from the lambda.");
+ }
+
+ if (string.Equals(errorCode, "CS1503", StringComparison.Ordinal)
+ && message.Contains("cannot convert from 'int' to 'byte'", StringComparison.Ordinal))
+ {
+ return (
+ true,
+ "Literal hoisting promotes integer literals to int values. "
+ + "Unity APIs such as Color32 expect byte components and do not accept implicit int-to-byte conversion "
+ + "after hoisting, even though plain numeric literals compile in normal Unity scripts.",
+ "Cast each component explicitly, for example: new Color32((byte)255, (byte)0, (byte)0, (byte)255).");
+ }
+
+ return (false, string.Empty, string.Empty);
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs.meta
new file mode 100644
index 0000000000..e053c03016
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeTranspilerConstraintHints.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b998634941c2049ec90c456eda0e3820
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs
new file mode 100644
index 0000000000..12a2b08ba7
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs
@@ -0,0 +1,128 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Tracks static local function bodies so literal hoisting does not inject outer-scope bindings.
+ ///
+ internal sealed class LiteralHoistScopeTracker
+ {
+ private readonly Stack _suppressedBodyBraceDepths = new();
+ private int _braceDepth;
+ private bool _pendingBlockBody;
+ private bool _inExpressionBody;
+
+ internal bool ShouldSuppressLiteralHoisting =>
+ _inExpressionBody || _suppressedBodyBraceDepths.Count > 0;
+
+ internal void OnOpenBrace()
+ {
+ _braceDepth++;
+ if (_pendingBlockBody)
+ {
+ _suppressedBodyBraceDepths.Push(_braceDepth);
+ _pendingBlockBody = false;
+ }
+ }
+
+ internal void OnCloseBrace()
+ {
+ if (_suppressedBodyBraceDepths.Count > 0
+ && _suppressedBodyBraceDepths.Peek() == _braceDepth)
+ {
+ _suppressedBodyBraceDepths.Pop();
+ }
+
+ _braceDepth--;
+ }
+
+ internal void OnSemicolon()
+ {
+ if (_inExpressionBody)
+ {
+ _inExpressionBody = false;
+ }
+ }
+
+ internal bool TryConsumeStaticLocalFunctionHeader(
+ string source,
+ int index,
+ StringBuilder rewrittenSource,
+ ref int nextIndex)
+ {
+ Debug.Assert(source != null, "source must not be null");
+ Debug.Assert(rewrittenSource != null, "rewrittenSource must not be null");
+
+ if (!IsKeywordAt(source, index, "static"))
+ {
+ return false;
+ }
+
+ int scanIndex = index + "static".Length;
+ if (!StaticLocalFunctionHeaderScanner.TrySkipHeader(source, scanIndex, out bool isExpressionBody, out int headerEndIndex))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, index, headerEndIndex - index);
+ if (isExpressionBody)
+ {
+ _inExpressionBody = true;
+ }
+ else
+ {
+ _pendingBlockBody = true;
+ }
+
+ nextIndex = headerEndIndex;
+ return true;
+ }
+
+ private static bool IsKeywordAt(string source, int index, string keyword)
+ {
+ if (index < 0 || index + keyword.Length > source.Length)
+ {
+ return false;
+ }
+
+ if (!HasIdentifierBoundaryBefore(source, index))
+ {
+ return false;
+ }
+
+ for (int offset = 0; offset < keyword.Length; offset++)
+ {
+ if (source[index + offset] != keyword[offset])
+ {
+ return false;
+ }
+ }
+
+ return HasIdentifierBoundaryAfter(source, index + keyword.Length);
+ }
+
+ private static bool HasIdentifierBoundaryBefore(string source, int index)
+ {
+ if (index <= 0)
+ {
+ return true;
+ }
+
+ char previous = source[index - 1];
+ return !char.IsLetterOrDigit(previous) && previous != '_';
+ }
+
+ private static bool HasIdentifierBoundaryAfter(string source, int index)
+ {
+ if (index >= source.Length)
+ {
+ return true;
+ }
+
+ char next = source[index];
+ return !char.IsLetterOrDigit(next) && next != '_';
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs.meta
new file mode 100644
index 0000000000..4c938157a9
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/LiteralHoistScopeTracker.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8a799fe95102c424585ae3e7ca4fc768
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs
new file mode 100644
index 0000000000..1ccc87fae1
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs
@@ -0,0 +1,202 @@
+using System.Diagnostics;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Detects static local function headers so literal hoisting can skip their bodies.
+ ///
+ ///
+ /// Unsupported header shapes leave literals hoistable and may still surface CS8421:
+ /// generic constraints with where clauses, tuple return types such as static (int, int) F(),
+ /// and expression bodies that contain statement lambdas (the first semicolon ends suppression early).
+ ///
+ internal static class StaticLocalFunctionHeaderScanner
+ {
+ internal static bool TrySkipHeader(
+ string source,
+ int startIndex,
+ out bool isExpressionBody,
+ out int headerEndIndex)
+ {
+ Debug.Assert(source != null, "source must not be null");
+
+ isExpressionBody = false;
+ headerEndIndex = startIndex;
+
+ int index = SkipWhitespace(source, startIndex);
+ if (!TrySkipReturnTypeAndName(source, ref index))
+ {
+ return false;
+ }
+
+ index = SkipWhitespace(source, index);
+ if (index >= source.Length || source[index] != '(')
+ {
+ return false;
+ }
+
+ if (!TrySkipParameterList(source, ref index))
+ {
+ return false;
+ }
+
+ index = SkipWhitespace(source, index);
+ if (index + 1 < source.Length && source[index] == '=' && source[index + 1] == '>')
+ {
+ isExpressionBody = true;
+ headerEndIndex = index + 2;
+ return true;
+ }
+
+ if (index < source.Length && source[index] == '{')
+ {
+ isExpressionBody = false;
+ headerEndIndex = index;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TrySkipReturnTypeAndName(string source, ref int index)
+ {
+ bool sawName = false;
+
+ while (index < source.Length)
+ {
+ index = SkipWhitespace(source, index);
+ if (index >= source.Length)
+ {
+ return false;
+ }
+
+ if (source[index] == '(')
+ {
+ return sawName;
+ }
+
+ if (!TrySkipIdentifier(source, ref index))
+ {
+ return false;
+ }
+
+ sawName = true;
+ index = SkipWhitespace(source, index);
+ if (index < source.Length && source[index] == '<')
+ {
+ if (!TrySkipGenericArguments(source, ref index))
+ {
+ return false;
+ }
+ }
+
+ if (index < source.Length && source[index] == '.')
+ {
+ index++;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool TrySkipParameterList(string source, ref int index)
+ {
+ if (index >= source.Length || source[index] != '(')
+ {
+ return false;
+ }
+
+ int depth = 0;
+ while (index < source.Length)
+ {
+ char current = source[index];
+ if (current == '(')
+ {
+ depth++;
+ }
+ else if (current == ')')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ index++;
+ return true;
+ }
+ }
+
+ index++;
+ }
+
+ return false;
+ }
+
+ private static bool TrySkipGenericArguments(string source, ref int index)
+ {
+ if (index >= source.Length || source[index] != '<')
+ {
+ return false;
+ }
+
+ int depth = 0;
+ while (index < source.Length)
+ {
+ char current = source[index];
+ if (current == '<')
+ {
+ depth++;
+ }
+ else if (current == '>')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ index++;
+ return true;
+ }
+ }
+
+ index++;
+ }
+
+ return false;
+ }
+
+ private static bool TrySkipIdentifier(string source, ref int index)
+ {
+ if (index >= source.Length)
+ {
+ return false;
+ }
+
+ char first = source[index];
+ if (!char.IsLetter(first) && first != '_')
+ {
+ return false;
+ }
+
+ index++;
+ while (index < source.Length)
+ {
+ char current = source[index];
+ if (!char.IsLetterOrDigit(current) && current != '_')
+ {
+ return true;
+ }
+
+ index++;
+ }
+
+ return true;
+ }
+
+ private static int SkipWhitespace(string source, int index)
+ {
+ while (index < source.Length && char.IsWhiteSpace(source[index]))
+ {
+ index++;
+ }
+
+ return index;
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs.meta
new file mode 100644
index 0000000000..c28ccff2ff
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/StaticLocalFunctionHeaderScanner.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 81fb44ffbd1714d23bfc26a3afd08aab
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md
index d5d4edc3df..42208832b9 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md
@@ -32,6 +32,12 @@ return x;
Prefer terminal commands for file operations and keep snippets focused on Unity Editor state that existing uloop tools cannot inspect or change.
+## Known transpiler constraints
+
+- Literals inside recognized static local function bodies are kept inline automatically. Unsupported header shapes (generic `where` clauses, tuple return types, statement lambdas inside expression bodies) may still hoist literals and surface CS8421; remove `static` or rewrite the helper.
+- Static lambdas (`static x => ...`) cannot reference hoisted literals and surface CS8820; remove `static` from the lambda or use a non-static local function.
+- Integer literals are hoisted as `int` values. APIs that require `byte` components (for example `new Color32(255, 0, 0, 255)`) need explicit casts such as `(byte)255` even when plain Unity scripts accept uncast numeric literals.
+
## Shell Quoting
- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`.