Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/uloop-execute-dynamic-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `'\''`.
Expand Down
6 changes: 6 additions & 0 deletions .claude/skills/uloop-execute-dynamic-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `'\''`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/// <summary>
/// Verifies int-to-byte conversion failures include transpiler constraint guidance.
/// </summary>
[Test]
public void ConvertExecutionResultToResponse_WhenColor32ByteConversionFails_AddsTranspilerConstraintHint()
{
DynamicCodeExecutionResponseFactory factory = new();
ExecutionResult result = new()
{
Success = false,
ErrorMessage = "Compilation error occurred",
CompilationErrors = new List<CompilationError>
{
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)."));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,81 @@ public void Rewrite_WhenInterpolatedHoleContainsNestedStringLiteral_ShouldKeepIn
Assert.That(result.Bindings, Is.Empty);
Assert.That(result.DeclarationLines, Is.Empty);
}

/// <summary>
/// Verifies literals inside a nested static local function stay inline while outer literals still hoist.
/// </summary>
[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));
}

/// <summary>
/// Verifies string literals inside a nested static local block body are not hoisted.
/// </summary>
[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);
}

/// <summary>
/// Verifies top-level expression-bodied static local functions keep literals inline at brace depth zero.
/// </summary>
[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));
}

/// <summary>
/// Verifies top-level block-bodied static local functions keep literals inline at brace depth zero.
/// </summary>
[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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
{
/// <summary>
/// Test fixture that verifies transpiler constraint hint generation.
/// </summary>
[TestFixture]
public class DynamicCodeTranspilerConstraintHintsTests
{
/// <summary>
/// Verifies CS8421 hoisted-literal failures suggest removing the static modifier.
/// </summary>
[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."));
}

/// <summary>
/// Verifies CS8820 static lambda failures suggest removing the static modifier.
/// </summary>
[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."));
}

/// <summary>
/// Verifies int-to-byte conversion failures include explicit cast guidance for Color32.
/// </summary>
[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"));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,18 @@ private static (string hint, List<string> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
/// </summary>
internal static class DynamicCodeLiteralHoister
{
private const string LiteralParameterPrefix = "__uloop_literal_";
internal const string LiteralParameterPrefix = "__uloop_literal_";
private static readonly Dictionary<char, char> RegularStringEscapes = new()
{
{ '\'', '\'' },
Expand All @@ -30,6 +30,7 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
{
StringBuilder rewrittenSource = new(source.Length);
List<HoistedLiteralBinding> bindings = new();
LiteralHoistScopeTracker scopeTracker = new();
int index = 0;

while (index < source.Length)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
Loading