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 `'\''`.