diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs
new file mode 100644
index 0000000000..622b6f19da
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs
@@ -0,0 +1,223 @@
+using NUnit.Framework;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
+{
+ ///
+ /// Characterization tests that lock current DynamicCodeLiteralHoister Rewrite outputs before god-class splits.
+ ///
+ [TestFixture]
+ public class DynamicCodeLiteralHoisterCharacterizationTests
+ {
+ ///
+ /// Pins integer literal hoisting for a simple return statement.
+ ///
+ [Test]
+ public void Rewrite_WhenIntegerLiteral_ShouldHoistToParameter()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("return 42;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].ParameterName, Is.EqualTo("__uloop_literal_0"));
+ Assert.That(result.Bindings[0].TypeName, Is.EqualTo("int"));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(42));
+ Assert.That(
+ result.DeclarationLines,
+ Is.EqualTo(new[]
+ {
+ "int __uloop_literal_0 = (int)parameters[\"__uloop_literal_0\"];"
+ }));
+ }
+
+ ///
+ /// Pins long-suffixed integer literal hoisting.
+ ///
+ [Test]
+ public void Rewrite_WhenLongLiteral_ShouldHoistAsLong()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("return 10L;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].TypeName, Is.EqualTo("long"));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(10L));
+ }
+
+ ///
+ /// Pins multiple integer literals hoisted in source order.
+ ///
+ [Test]
+ public void Rewrite_WhenMultipleIntegerLiterals_ShouldHoistInOrder()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("return 1 + 2;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0 + __uloop_literal_1;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(2));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(1));
+ Assert.That(result.Bindings[1].Value, Is.EqualTo(2));
+ }
+
+ ///
+ /// Pins regular string literal hoisting with the unescaped value stored in the binding.
+ ///
+ [Test]
+ public void Rewrite_WhenRegularStringLiteral_ShouldHoistUnescapedValue()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("return \"hello\";");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].TypeName, Is.EqualTo("string"));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo("hello"));
+ Assert.That(
+ result.DeclarationLines,
+ Is.EqualTo(new[]
+ {
+ "string __uloop_literal_0 = (string)parameters[\"__uloop_literal_0\"];"
+ }));
+ }
+
+ ///
+ /// Pins escape-sequence unescaping for regular string literals.
+ ///
+ [Test]
+ public void Rewrite_WhenStringLiteralHasEscape_ShouldStoreUnescapedBindingValue()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("return \"a\\nb\";");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo("a\nb"));
+ }
+
+ ///
+ /// Pins verbatim strings remaining inline without hoisting.
+ ///
+ [Test]
+ public void Rewrite_WhenVerbatimString_ShouldKeepInline()
+ {
+ string source = "return @\"verbatim\";";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ Assert.That(result.DeclarationLines, Is.Empty);
+ }
+
+ ///
+ /// Pins character literals remaining inline without hoisting.
+ ///
+ [Test]
+ public void Rewrite_WhenCharLiteral_ShouldKeepInline()
+ {
+ string source = "return 'x';";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ }
+
+ ///
+ /// Pins interpolated strings remaining opaque, including nested integer tokens.
+ ///
+ [Test]
+ public void Rewrite_WhenInterpolatedString_ShouldKeepOpaque()
+ {
+ string source = "return $\"hi{1}\";";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ }
+
+ ///
+ /// Pins decimal tokens remaining inline because the integer scanner stops at the decimal point.
+ ///
+ [Test]
+ public void Rewrite_WhenDecimalLiteral_ShouldKeepInline()
+ {
+ string source = "return 1.5;";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ }
+
+ ///
+ /// Pins sources with no hoistable literals remaining unchanged.
+ ///
+ [Test]
+ public void Rewrite_WhenNoHoistableLiteral_ShouldReturnOriginalSource()
+ {
+ string source = "return null;";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ Assert.That(result.DeclarationLines, Is.Empty);
+ }
+
+ ///
+ /// Pins comments remaining intact while trailing literals still hoist.
+ ///
+ [Test]
+ public void Rewrite_WhenCommentPrecedesLiteral_ShouldPreserveCommentAndHoistLiteral()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("// note\nreturn 7;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("// note\nreturn __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(7));
+ }
+
+ ///
+ /// Pins block-comment copy path: comment text stays intact while a following integer still hoists.
+ ///
+ [Test]
+ public void Rewrite_WhenBlockCommentPrecedesLiteral_ShouldPreserveCommentAndHoistLiteral()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite("/* c */ return 1;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("/* c */ return __uloop_literal_0;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(1));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo(1));
+ }
+
+ ///
+ /// Pins unterminated block comments consuming the remainder of the source through EOF.
+ ///
+ [Test]
+ public void Rewrite_WhenUnterminatedBlockComment_ShouldTreatRemainderAsComment()
+ {
+ string source = "/* unterminated";
+
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(source);
+
+ Assert.That(result.RewrittenSource, Is.EqualTo(source));
+ Assert.That(result.Bindings, Is.Empty);
+ Assert.That(result.DeclarationLines, Is.Empty);
+ }
+
+ ///
+ /// Pins mixed string and integer hoisting in a single expression.
+ ///
+ [Test]
+ public void Rewrite_WhenStringAndIntegerLiterals_ShouldHoistBoth()
+ {
+ HoistedLiteralRewriteResult result = DynamicCodeLiteralHoister.Rewrite(
+ "return \"count\" + 3;");
+
+ Assert.That(result.RewrittenSource, Is.EqualTo("return __uloop_literal_0 + __uloop_literal_1;"));
+ Assert.That(result.Bindings, Has.Count.EqualTo(2));
+ Assert.That(result.Bindings[0].Value, Is.EqualTo("count"));
+ Assert.That(result.Bindings[1].Value, Is.EqualTo(3));
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs.meta
new file mode 100644
index 0000000000..635d5c009e
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterCharacterizationTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4b4a1665891c4e1a9e8fd479e42b8d25
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs
new file mode 100644
index 0000000000..d613b95bfc
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs
@@ -0,0 +1,168 @@
+using NUnit.Framework;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
+{
+ ///
+ /// Characterization tests that lock current SourceShaper Analyze/WrapIfNeeded outputs before god-class splits.
+ ///
+ [TestFixture]
+ public class SourceShaperCharacterizationTests
+ {
+ ///
+ /// Pins Analyze classification for a plain top-level return script.
+ ///
+ [Test]
+ public void Analyze_WhenTopLevelReturn_ShouldMarkTopLevelStatementsOnly()
+ {
+ SourceShapeResult result = SourceShaper.Analyze("return 1;");
+
+ Assert.That(result.HasTopLevelStatements, Is.True);
+ Assert.That(result.HasTypeDeclaration, Is.False);
+ Assert.That(result.HasNamespaceDeclaration, Is.False);
+ Assert.That(result.UsingDirectives, Is.Empty);
+ Assert.That(result.TopLevelBodyBuilder.ToString().TrimEnd(), Is.EqualTo("return 1;"));
+ }
+
+ ///
+ /// Pins using-directive extraction and remaining body content.
+ ///
+ [Test]
+ public void Analyze_WhenUsingDirectiveThenReturn_ShouldExtractUsingAndBody()
+ {
+ SourceShapeResult result = SourceShaper.Analyze(
+ "using System.Text;\nreturn Encoding.UTF8;");
+
+ Assert.That(result.HasTopLevelStatements, Is.True);
+ Assert.That(result.UsingDirectives, Has.Count.EqualTo(1));
+ Assert.That(result.UsingDirectives[0], Is.EqualTo("using System.Text;"));
+ // why: Analyze keeps the newline that followed the using directive in TopLevelBodyBuilder
+ Assert.That(result.TopLevelBodyBuilder.ToString().TrimEnd(), Is.EqualTo("\nreturn Encoding.UTF8;"));
+ }
+
+ ///
+ /// Pins type-only sources as raw declarations without top-level statements.
+ ///
+ [Test]
+ public void Analyze_WhenTypeDeclarationOnly_ShouldMarkTypeDeclaration()
+ {
+ SourceShapeResult result = SourceShaper.Analyze("public class Example { }");
+
+ Assert.That(result.HasTypeDeclaration, Is.True);
+ Assert.That(result.HasTopLevelStatements, Is.False);
+ Assert.That(result.HasNamespaceDeclaration, Is.False);
+ }
+
+ ///
+ /// Pins namespace block sources as namespace declarations; nested types stay unmarked.
+ ///
+ [Test]
+ public void Analyze_WhenNamespaceDeclarationOnly_ShouldMarkNamespaceDeclaration()
+ {
+ SourceShapeResult result = SourceShaper.Analyze("namespace Demo { public class Example { } }");
+
+ Assert.That(result.HasNamespaceDeclaration, Is.True);
+ // why: nested types inside a skipped namespace block are not separately flagged
+ Assert.That(result.HasTypeDeclaration, Is.False);
+ Assert.That(result.HasTopLevelStatements, Is.False);
+ }
+
+ ///
+ /// Pins WrapIfNeeded passthrough for raw type declarations.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenTypeDeclarationOnly_ShouldReturnOriginalSource()
+ {
+ string source = "public class Example { }";
+
+ string wrapped = SourceShaper.WrapIfNeeded(
+ source,
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.EqualTo(source));
+ }
+
+ ///
+ /// Pins WrapIfNeeded rejecting mixed type + top-level statement sources.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenMixedTypeAndTopLevel_ShouldReturnNull()
+ {
+ string wrapped = SourceShaper.WrapIfNeeded(
+ "class Example { }\nreturn 1;",
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.Null);
+ }
+
+ ///
+ /// Pins WrapIfNeeded wrapping a script that already returns a value.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenTopLevelReturn_ShouldWrapWithUserBodyIntact()
+ {
+ string wrapped = SourceShaper.WrapIfNeeded(
+ "return 1;",
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.Not.Null);
+ Assert.That(wrapped, Does.Contain("namespace " + DynamicCodeConstants.DEFAULT_NAMESPACE));
+ Assert.That(wrapped, Does.Contain("public class " + DynamicCodeConstants.DEFAULT_CLASS_NAME));
+ Assert.That(wrapped, Does.Contain(WrapperTemplate.UserCodeStartMarker));
+ Assert.That(wrapped, Does.Contain(" return 1;"));
+ Assert.That(wrapped, Does.Contain(WrapperTemplate.UserCodeEndMarker));
+ Assert.That(wrapped, Does.Not.Contain("return null;"));
+ }
+
+ ///
+ /// Pins WrapIfNeeded injecting a trailing return null for scripts without an explicit return.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenNoReturn_ShouldAppendReturnNull()
+ {
+ string wrapped = SourceShaper.WrapIfNeeded(
+ "var x = 1;",
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.Not.Null);
+ Assert.That(wrapped, Does.Contain(" var x = 1;"));
+ Assert.That(wrapped, Does.Contain(" return null;"));
+ }
+
+ ///
+ /// Pins WrapIfNeeded wrapping an empty script as a null-returning method body.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenEmptySource_ShouldWrapReturnNullOnly()
+ {
+ string wrapped = SourceShaper.WrapIfNeeded(
+ "",
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.Not.Null);
+ Assert.That(wrapped, Does.Contain(" return null;"));
+ }
+
+ ///
+ /// Pins using-alias detection flowing into WrapIfNeeded alias tracking.
+ ///
+ [Test]
+ public void WrapIfNeeded_WhenUsingAlias_ShouldPreserveAliasDirectiveInWrapper()
+ {
+ string wrapped = SourceShaper.WrapIfNeeded(
+ "using Object = System.Object;\nreturn new Object();",
+ DynamicCodeConstants.DEFAULT_NAMESPACE,
+ DynamicCodeConstants.DEFAULT_CLASS_NAME);
+
+ Assert.That(wrapped, Is.Not.Null);
+ Assert.That(wrapped, Does.Contain("using Object = System.Object;"));
+ Assert.That(wrapped, Does.Contain(" return new Object();"));
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs.meta
new file mode 100644
index 0000000000..9fcf026b07
--- /dev/null
+++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperCharacterizationTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 252f02f7dd7c4f1a923ab54a4298153d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
index cb5ff4942e..c3945e4db5 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs
@@ -11,20 +11,6 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
internal static class DynamicCodeLiteralHoister
{
internal const string LiteralParameterPrefix = "__uloop_literal_";
- private static readonly Dictionary RegularStringEscapes = new()
- {
- { '\'', '\'' },
- { '"', '"' },
- { '\\', '\\' },
- { '0', '\0' },
- { 'a', '\a' },
- { 'b', '\b' },
- { 'f', '\f' },
- { 'n', '\n' },
- { 'r', '\r' },
- { 't', '\t' },
- { 'v', '\v' }
- };
public static HoistedLiteralRewriteResult Rewrite(string source)
{
@@ -35,27 +21,27 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
while (index < source.Length)
{
- if (TryCopyInterpolatedStringLiteral(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyInterpolatedStringLiteral(source, rewrittenSource, ref index))
{
continue;
}
- if (TryCopyVerbatimStringLiteral(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyVerbatimStringLiteral(source, rewrittenSource, ref index))
{
continue;
}
- if (TryCopyCharLiteral(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyCharLiteral(source, rewrittenSource, ref index))
{
continue;
}
- if (TryCopyLineComment(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyLineComment(source, rewrittenSource, ref index))
{
continue;
}
- if (TryCopyBlockComment(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyBlockComment(source, rewrittenSource, ref index))
{
continue;
}
@@ -91,7 +77,7 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
if (scopeTracker.ShouldSuppressLiteralHoisting)
{
- if (TryCopyRegularStringLiteral(source, rewrittenSource, ref index))
+ if (DynamicCodeLiteralSyntaxScanner.TryCopyRegularStringLiteral(source, rewrittenSource, ref index))
{
continue;
}
@@ -124,512 +110,6 @@ public static HoistedLiteralRewriteResult Rewrite(string source)
declarationLines);
}
- private static bool TryCopyInterpolatedStringLiteral(
- string source,
- StringBuilder rewrittenSource,
- ref int index)
- {
- int start = index;
- if (!TryAdvanceInterpolatedStringLiteral(source, ref index))
- {
- return false;
- }
-
- rewrittenSource.Append(source, start, index - start);
- return true;
- }
-
- private static bool TryCopyLineComment(
- string source,
- StringBuilder rewrittenSource,
- ref int index)
- {
- if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
- {
- return false;
- }
-
- while (index < source.Length)
- {
- char current = source[index];
- rewrittenSource.Append(current);
- index++;
- if (current == '\n')
- {
- break;
- }
- }
-
- return true;
- }
-
- private static bool TryCopyBlockComment(
- string source,
- StringBuilder rewrittenSource,
- ref int index)
- {
- if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
- {
- return false;
- }
-
- rewrittenSource.Append(source[index]);
- rewrittenSource.Append(source[index + 1]);
- index += 2;
-
- while (index < source.Length)
- {
- char current = source[index];
- rewrittenSource.Append(current);
- index++;
- if (current == '*' && index < source.Length && source[index] == '/')
- {
- rewrittenSource.Append('/');
- index++;
- break;
- }
- }
-
- return true;
- }
-
- private static bool TryCopyVerbatimStringLiteral(
- string source,
- StringBuilder rewrittenSource,
- ref int index)
- {
- if (source[index] != '@'
- || index + 1 >= source.Length
- || source[index + 1] != '"')
- {
- return false;
- }
-
- int start = index;
- index += 2;
-
- while (index < source.Length)
- {
- if (source[index] != '"')
- {
- index++;
- continue;
- }
-
- if (index + 1 < source.Length && source[index + 1] == '"')
- {
- index += 2;
- continue;
- }
-
- index++;
- rewrittenSource.Append(source, start, index - start);
- return true;
- }
-
- index = start;
- return false;
- }
-
- private static bool TryAdvanceInterpolatedStringLiteral(string source, ref int index)
- {
- int start = index;
- if (!TryMatchInterpolatedStringStart(source, ref index, out bool isVerbatim))
- {
- return false;
- }
-
- int interpolationDepth = 0;
- while (index < source.Length)
- {
- if (interpolationDepth > 0)
- {
- interpolationDepth = AdvanceInterpolatedExpressionSegment(
- source,
- ref index,
- interpolationDepth);
- continue;
- }
-
- InterpolatedStringAdvanceResult advanceResult =
- AdvanceInterpolatedStringContentSegment(source, index, isVerbatim);
- index = advanceResult.Index;
- interpolationDepth = advanceResult.InterpolationDepth;
- if (advanceResult.Completed)
- {
- return true;
- }
- }
-
- index = start;
- return false;
- }
-
- private static int AdvanceInterpolatedExpressionSegment(
- string source,
- ref int index,
- int interpolationDepth)
- {
- if (TryAdvanceInterpolatedExpressionToken(source, ref index))
- {
- return interpolationDepth;
- }
-
- char expressionCharacter = source[index];
- if (expressionCharacter == '{')
- {
- index++;
- return interpolationDepth + 1;
- }
-
- if (expressionCharacter == '}')
- {
- index++;
- return interpolationDepth - 1;
- }
-
- index++;
- return interpolationDepth;
- }
-
- private static InterpolatedStringAdvanceResult AdvanceInterpolatedStringContentSegment(
- string source,
- int index,
- bool isVerbatim)
- {
- char current = source[index];
- if (current == '{')
- {
- return index + 1 < source.Length && source[index + 1] == '{'
- ? new InterpolatedStringAdvanceResult(index + 2, 0, false)
- : new InterpolatedStringAdvanceResult(index + 1, 1, false);
- }
-
- if (current == '}')
- {
- return index + 1 < source.Length && source[index + 1] == '}'
- ? new InterpolatedStringAdvanceResult(index + 2, 0, false)
- : new InterpolatedStringAdvanceResult(index + 1, 0, false);
- }
-
- if (!isVerbatim && current == '\\')
- {
- int escapedIndex = index;
- AdvanceEscapedLiteralSequence(source, ref escapedIndex);
- return new InterpolatedStringAdvanceResult(escapedIndex, 0, false);
- }
-
- if (current != '"')
- {
- return new InterpolatedStringAdvanceResult(index + 1, 0, false);
- }
-
- if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"')
- {
- return new InterpolatedStringAdvanceResult(index + 2, 0, false);
- }
-
- return new InterpolatedStringAdvanceResult(index + 1, 0, true);
- }
-
- private static bool TryMatchInterpolatedStringStart(
- string source,
- ref int index,
- out bool isVerbatim)
- {
- isVerbatim = false;
-
- if (source[index] == '$')
- {
- if (index + 1 < source.Length && source[index + 1] == '"')
- {
- index += 2;
- return true;
- }
-
- if (index + 2 < source.Length && source[index + 1] == '@' && source[index + 2] == '"')
- {
- isVerbatim = true;
- index += 3;
- return true;
- }
-
- return false;
- }
-
- if (source[index] != '@')
- {
- return false;
- }
-
- if (index + 2 < source.Length && source[index + 1] == '$' && source[index + 2] == '"')
- {
- isVerbatim = true;
- index += 3;
- return true;
- }
-
- return false;
- }
-
- private static bool TryAdvanceInterpolatedExpressionToken(string source, ref int index)
- {
- if (TryAdvanceInterpolatedStringLiteral(source, ref index))
- {
- return true;
- }
-
- if (TryAdvanceVerbatimStringLiteral(source, ref index))
- {
- return true;
- }
-
- if (TryAdvanceRegularStringLiteral(source, ref index))
- {
- return true;
- }
-
- if (TryAdvanceCharLiteral(source, ref index))
- {
- return true;
- }
-
- if (TryAdvanceLineComment(source, ref index))
- {
- return true;
- }
-
- if (TryAdvanceBlockComment(source, ref index))
- {
- return true;
- }
-
- return false;
- }
-
- private static bool TryAdvanceLineComment(string source, ref int index)
- {
- if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
- {
- return false;
- }
-
- index += 2;
- while (index < source.Length && source[index] != '\n')
- {
- index++;
- }
-
- if (index < source.Length)
- {
- index++;
- }
-
- return true;
- }
-
- private static bool TryAdvanceBlockComment(string source, ref int index)
- {
- if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
- {
- return false;
- }
-
- index += 2;
- while (index < source.Length)
- {
- if (source[index] == '*' && index + 1 < source.Length && source[index + 1] == '/')
- {
- index += 2;
- return true;
- }
-
- index++;
- }
-
- return true;
- }
-
- private static bool TryAdvanceVerbatimStringLiteral(string source, ref int index)
- {
- if (source[index] != '@'
- || index + 1 >= source.Length
- || source[index + 1] != '"')
- {
- return false;
- }
-
- int start = index;
- index += 2;
-
- while (index < source.Length)
- {
- if (source[index] != '"')
- {
- index++;
- continue;
- }
-
- if (index + 1 < source.Length && source[index + 1] == '"')
- {
- index += 2;
- continue;
- }
-
- index++;
- return true;
- }
-
- index = start;
- return false;
- }
-
- private static bool TryAdvanceRegularStringLiteral(string source, 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++;
- return true;
- }
-
- index++;
- }
-
- index = start;
- return false;
- }
-
- private static bool TryAdvanceCharLiteral(string source, ref int index)
- {
- if (source[index] != '\'')
- {
- return false;
- }
-
- int start = index;
- index++;
-
- while (index < source.Length)
- {
- char current = source[index];
- if (current == '\\')
- {
- AdvanceEscapedLiteralSequence(source, ref index);
- continue;
- }
-
- if (current == '\'')
- {
- index++;
- return true;
- }
-
- index++;
- }
-
- index = start;
- return false;
- }
-
- private static bool TryCopyCharLiteral(
- string source,
- StringBuilder rewrittenSource,
- ref int index)
- {
- if (source[index] != '\'')
- {
- 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 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,
@@ -654,7 +134,7 @@ private static bool TryHoistRegularStringLiteral(
char current = source[index];
if (current == '\\')
{
- AdvanceEscapedLiteralSequence(source, ref index);
+ DynamicCodeRegularStringLiteralUnescaper.AdvanceEscapedLiteralSequence(source, ref index);
continue;
}
@@ -662,7 +142,7 @@ private static bool TryHoistRegularStringLiteral(
{
index++;
string literalToken = source.Substring(start, index - start);
- if (!TryUnescapeRegularStringLiteral(literalToken, out string value))
+ if (!DynamicCodeRegularStringLiteralUnescaper.TryUnescapeRegularStringLiteral(literalToken, out string value))
{
rewrittenSource.Append(literalToken);
return true;
@@ -766,218 +246,6 @@ private static string CreateParameterName(int index)
{
return $"{LiteralParameterPrefix}{index}";
}
-
- private static void AdvanceEscapedLiteralSequence(string source, ref int index)
- {
- index++;
- if (index >= source.Length)
- {
- return;
- }
-
- char escape = source[index];
- index++;
-
- switch (escape)
- {
- case 'u':
- index = Math.Min(source.Length, index + 4);
- break;
- case 'U':
- index = Math.Min(source.Length, index + 8);
- break;
- case 'x':
- index = AdvanceVariableLengthHexDigits(source, index, 4);
- break;
- }
- }
-
- private static int AdvanceVariableLengthHexDigits(string value, int index, int maxDigits)
- {
- int digitsConsumed = 0;
- while (index < value.Length && digitsConsumed < maxDigits && IsHexDigit(value[index]))
- {
- index++;
- digitsConsumed++;
- }
-
- return index;
- }
-
- private static bool TryUnescapeRegularStringLiteral(string token, out string value)
- {
- string inner = token.Substring(1, token.Length - 2);
- StringBuilder unescaped = new(inner.Length);
-
- for (int index = 0; index < inner.Length; index++)
- {
- char current = inner[index];
- if (current != '\\')
- {
- unescaped.Append(current);
- continue;
- }
-
- if (!TryAppendRegularStringEscape(inner, ref index, unescaped))
- {
- value = null;
- return false;
- }
- }
-
- value = unescaped.ToString();
- return true;
- }
-
- private static bool TryAppendRegularStringEscape(
- string inner,
- ref int index,
- StringBuilder unescaped)
- {
- if (index + 1 >= inner.Length)
- {
- return false;
- }
-
- index++;
- char escape = inner[index];
- if (RegularStringEscapes.ContainsKey(escape))
- {
- unescaped.Append(RegularStringEscapes[escape]);
- return true;
- }
-
- return TryAppendComplexRegularStringEscape(inner, ref index, unescaped, escape);
- }
-
- private static bool TryAppendComplexRegularStringEscape(
- string inner,
- ref int index,
- StringBuilder unescaped,
- char escape)
- {
- switch (escape)
- {
- case 'u':
- return TryAppendUnicodeEscape(inner, ref index, unescaped);
- case 'U':
- return TryAppendUtf32Escape(inner, ref index, unescaped);
- case 'x':
- return TryAppendVariableLengthHexEscape(inner, ref index, unescaped);
- default:
- return false;
- }
- }
-
- private static bool TryAppendUnicodeEscape(
- string inner,
- ref int index,
- StringBuilder unescaped)
- {
- if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue))
- {
- return false;
- }
-
- unescaped.Append((char)unicodeValue);
- return true;
- }
-
- private static bool TryAppendUtf32Escape(
- string inner,
- ref int index,
- StringBuilder unescaped)
- {
- if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) ||
- !IsValidUtf32CodePoint(codePoint))
- {
- return false;
- }
-
- unescaped.Append(char.ConvertFromUtf32(codePoint));
- return true;
- }
-
- private static bool TryAppendVariableLengthHexEscape(
- string inner,
- ref int index,
- StringBuilder unescaped)
- {
- if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue))
- {
- return false;
- }
-
- unescaped.Append((char)variableLengthValue);
- return true;
- }
-
- private static bool TryParseHexDigits(string value, ref int index, int digitCount, out int parsedValue)
- {
- int start = index + 1;
- if (start + digitCount > value.Length)
- {
- parsedValue = 0;
- return false;
- }
-
- string hex = value.Substring(start, digitCount);
- if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
- {
- return false;
- }
-
- index += digitCount;
- return true;
- }
-
- private static bool TryParseVariableLengthHexDigits(string value, ref int index, out int parsedValue)
- {
- int start = index + 1;
- int end = AdvanceVariableLengthHexDigits(value, start, 4);
- if (end == start)
- {
- parsedValue = 0;
- return false;
- }
-
- string hex = value.Substring(start, end - start);
- if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
- {
- return false;
- }
-
- index = end - 1;
- return true;
- }
-
- private static bool IsValidUtf32CodePoint(int value)
- {
- return value >= 0
- && value <= 0x10FFFF
- && (value < 0xD800 || value > 0xDFFF);
- }
-
- private static bool IsHexDigit(char value)
- {
- return (value >= '0' && value <= '9')
- || (value >= 'a' && value <= 'f')
- || (value >= 'A' && value <= 'F');
- }
-
- private readonly struct InterpolatedStringAdvanceResult
- {
- public InterpolatedStringAdvanceResult(int index, int interpolationDepth, bool completed)
- {
- Index = index;
- InterpolationDepth = interpolationDepth;
- Completed = completed;
- }
-
- public int Index { get; }
- public int InterpolationDepth { get; }
- public bool Completed { get; }
- }
}
///
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs
new file mode 100644
index 0000000000..3eb6d32775
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs
@@ -0,0 +1,436 @@
+using System.Text;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Scans C# source and copies or advances over literal and comment syntax segments.
+ ///
+ internal static class DynamicCodeLiteralSyntaxScanner
+ {
+ internal static bool TryCopyInterpolatedStringLiteral(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceInterpolatedStringLiteral(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ internal static bool TryCopyLineComment(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceLineComment(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ internal static bool TryCopyBlockComment(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceBlockComment(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ internal static bool TryCopyVerbatimStringLiteral(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceVerbatimStringLiteral(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ private static bool TryAdvanceInterpolatedStringLiteral(string source, ref int index)
+ {
+ int start = index;
+ if (!TryMatchInterpolatedStringStart(source, ref index, out bool isVerbatim))
+ {
+ return false;
+ }
+
+ int interpolationDepth = 0;
+ while (index < source.Length)
+ {
+ if (interpolationDepth > 0)
+ {
+ interpolationDepth = AdvanceInterpolatedExpressionSegment(
+ source,
+ ref index,
+ interpolationDepth);
+ continue;
+ }
+
+ InterpolatedStringAdvanceResult advanceResult =
+ AdvanceInterpolatedStringContentSegment(source, index, isVerbatim);
+ index = advanceResult.Index;
+ interpolationDepth = advanceResult.InterpolationDepth;
+ if (advanceResult.Completed)
+ {
+ return true;
+ }
+ }
+
+ index = start;
+ return false;
+ }
+
+ private static int AdvanceInterpolatedExpressionSegment(
+ string source,
+ ref int index,
+ int interpolationDepth)
+ {
+ if (TryAdvanceInterpolatedExpressionToken(source, ref index))
+ {
+ return interpolationDepth;
+ }
+
+ char expressionCharacter = source[index];
+ if (expressionCharacter == '{')
+ {
+ index++;
+ return interpolationDepth + 1;
+ }
+
+ if (expressionCharacter == '}')
+ {
+ index++;
+ return interpolationDepth - 1;
+ }
+
+ index++;
+ return interpolationDepth;
+ }
+
+ private static InterpolatedStringAdvanceResult AdvanceInterpolatedStringContentSegment(
+ string source,
+ int index,
+ bool isVerbatim)
+ {
+ char current = source[index];
+ if (current == '{')
+ {
+ return index + 1 < source.Length && source[index + 1] == '{'
+ ? new InterpolatedStringAdvanceResult(index + 2, 0, false)
+ : new InterpolatedStringAdvanceResult(index + 1, 1, false);
+ }
+
+ if (current == '}')
+ {
+ return index + 1 < source.Length && source[index + 1] == '}'
+ ? new InterpolatedStringAdvanceResult(index + 2, 0, false)
+ : new InterpolatedStringAdvanceResult(index + 1, 0, false);
+ }
+
+ if (!isVerbatim && current == '\\')
+ {
+ int escapedIndex = index;
+ DynamicCodeRegularStringLiteralUnescaper.AdvanceEscapedLiteralSequence(source, ref escapedIndex);
+ return new InterpolatedStringAdvanceResult(escapedIndex, 0, false);
+ }
+
+ if (current != '"')
+ {
+ return new InterpolatedStringAdvanceResult(index + 1, 0, false);
+ }
+
+ if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"')
+ {
+ return new InterpolatedStringAdvanceResult(index + 2, 0, false);
+ }
+
+ return new InterpolatedStringAdvanceResult(index + 1, 0, true);
+ }
+
+ private static bool TryMatchInterpolatedStringStart(
+ string source,
+ ref int index,
+ out bool isVerbatim)
+ {
+ isVerbatim = false;
+
+ if (source[index] == '$')
+ {
+ if (index + 1 < source.Length && source[index + 1] == '"')
+ {
+ index += 2;
+ return true;
+ }
+
+ if (index + 2 < source.Length && source[index + 1] == '@' && source[index + 2] == '"')
+ {
+ isVerbatim = true;
+ index += 3;
+ return true;
+ }
+
+ return false;
+ }
+
+ if (source[index] != '@')
+ {
+ return false;
+ }
+
+ if (index + 2 < source.Length && source[index + 1] == '$' && source[index + 2] == '"')
+ {
+ isVerbatim = true;
+ index += 3;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryAdvanceInterpolatedExpressionToken(string source, ref int index)
+ {
+ if (TryAdvanceInterpolatedStringLiteral(source, ref index))
+ {
+ return true;
+ }
+
+ if (TryAdvanceVerbatimStringLiteral(source, ref index))
+ {
+ return true;
+ }
+
+ if (TryAdvanceRegularStringLiteral(source, ref index))
+ {
+ return true;
+ }
+
+ if (TryAdvanceCharLiteral(source, ref index))
+ {
+ return true;
+ }
+
+ if (TryAdvanceLineComment(source, ref index))
+ {
+ return true;
+ }
+
+ if (TryAdvanceBlockComment(source, ref index))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryAdvanceLineComment(string source, ref int index)
+ {
+ if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
+ {
+ return false;
+ }
+
+ index += 2;
+ while (index < source.Length && source[index] != '\n')
+ {
+ index++;
+ }
+
+ if (index < source.Length)
+ {
+ index++;
+ }
+
+ return true;
+ }
+
+ private static bool TryAdvanceBlockComment(string source, ref int index)
+ {
+ if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
+ {
+ return false;
+ }
+
+ index += 2;
+ while (index < source.Length)
+ {
+ if (source[index] == '*' && index + 1 < source.Length && source[index + 1] == '/')
+ {
+ index += 2;
+ return true;
+ }
+
+ index++;
+ }
+
+ return true;
+ }
+
+ private static bool TryAdvanceVerbatimStringLiteral(string source, ref int index)
+ {
+ if (source[index] != '@'
+ || index + 1 >= source.Length
+ || source[index + 1] != '"')
+ {
+ return false;
+ }
+
+ int start = index;
+ index += 2;
+
+ while (index < source.Length)
+ {
+ if (source[index] != '"')
+ {
+ index++;
+ continue;
+ }
+
+ if (index + 1 < source.Length && source[index + 1] == '"')
+ {
+ index += 2;
+ continue;
+ }
+
+ index++;
+ return true;
+ }
+
+ index = start;
+ return false;
+ }
+
+ private static bool TryAdvanceRegularStringLiteral(string source, 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 == '\\')
+ {
+ DynamicCodeRegularStringLiteralUnescaper.AdvanceEscapedLiteralSequence(source, ref index);
+ continue;
+ }
+
+ if (current == '"')
+ {
+ index++;
+ return true;
+ }
+
+ index++;
+ }
+
+ index = start;
+ return false;
+ }
+
+ private static bool TryAdvanceCharLiteral(string source, ref int index)
+ {
+ if (source[index] != '\'')
+ {
+ return false;
+ }
+
+ int start = index;
+ index++;
+
+ while (index < source.Length)
+ {
+ char current = source[index];
+ if (current == '\\')
+ {
+ DynamicCodeRegularStringLiteralUnescaper.AdvanceEscapedLiteralSequence(source, ref index);
+ continue;
+ }
+
+ if (current == '\'')
+ {
+ index++;
+ return true;
+ }
+
+ index++;
+ }
+
+ index = start;
+ return false;
+ }
+
+ internal static bool TryCopyCharLiteral(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceCharLiteral(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ internal static bool TryCopyRegularStringLiteral(
+ string source,
+ StringBuilder rewrittenSource,
+ ref int index)
+ {
+ int start = index;
+ if (!TryAdvanceRegularStringLiteral(source, ref index))
+ {
+ return false;
+ }
+
+ rewrittenSource.Append(source, start, index - start);
+ return true;
+ }
+
+ private readonly struct InterpolatedStringAdvanceResult
+ {
+ public InterpolatedStringAdvanceResult(int index, int interpolationDepth, bool completed)
+ {
+ Index = index;
+ InterpolationDepth = interpolationDepth;
+ Completed = completed;
+ }
+
+ public int Index { get; }
+ public int InterpolationDepth { get; }
+ public bool Completed { get; }
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs.meta
new file mode 100644
index 0000000000..afd38cff3f
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralSyntaxScanner.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8cc5669e3cfb4d4e9698616c7cc6b83e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs
new file mode 100644
index 0000000000..9096c30b7d
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Unescapes regular C# string literal tokens into their decoded text values.
+ ///
+ internal static class DynamicCodeRegularStringLiteralUnescaper
+ {
+ private static readonly Dictionary RegularStringEscapes = new()
+ {
+ { '\'', '\'' },
+ { '"', '"' },
+ { '\\', '\\' },
+ { '0', '\0' },
+ { 'a', '\a' },
+ { 'b', '\b' },
+ { 'f', '\f' },
+ { 'n', '\n' },
+ { 'r', '\r' },
+ { 't', '\t' },
+ { 'v', '\v' }
+ };
+
+ internal static void AdvanceEscapedLiteralSequence(string source, ref int index)
+ {
+ index++;
+ if (index >= source.Length)
+ {
+ return;
+ }
+
+ char escape = source[index];
+ index++;
+
+ switch (escape)
+ {
+ case 'u':
+ index = Math.Min(source.Length, index + 4);
+ break;
+ case 'U':
+ index = Math.Min(source.Length, index + 8);
+ break;
+ case 'x':
+ index = AdvanceVariableLengthHexDigits(source, index, 4);
+ break;
+ }
+ }
+
+ private static int AdvanceVariableLengthHexDigits(string value, int index, int maxDigits)
+ {
+ int digitsConsumed = 0;
+ while (index < value.Length && digitsConsumed < maxDigits && IsHexDigit(value[index]))
+ {
+ index++;
+ digitsConsumed++;
+ }
+
+ return index;
+ }
+
+ internal static bool TryUnescapeRegularStringLiteral(string token, out string value)
+ {
+ string inner = token.Substring(1, token.Length - 2);
+ StringBuilder unescaped = new(inner.Length);
+
+ for (int index = 0; index < inner.Length; index++)
+ {
+ char current = inner[index];
+ if (current != '\\')
+ {
+ unescaped.Append(current);
+ continue;
+ }
+
+ if (!TryAppendRegularStringEscape(inner, ref index, unescaped))
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ value = unescaped.ToString();
+ return true;
+ }
+
+ private static bool TryAppendRegularStringEscape(
+ string inner,
+ ref int index,
+ StringBuilder unescaped)
+ {
+ if (index + 1 >= inner.Length)
+ {
+ return false;
+ }
+
+ index++;
+ char escape = inner[index];
+ if (RegularStringEscapes.ContainsKey(escape))
+ {
+ unescaped.Append(RegularStringEscapes[escape]);
+ return true;
+ }
+
+ return TryAppendComplexRegularStringEscape(inner, ref index, unescaped, escape);
+ }
+
+ private static bool TryAppendComplexRegularStringEscape(
+ string inner,
+ ref int index,
+ StringBuilder unescaped,
+ char escape)
+ {
+ switch (escape)
+ {
+ case 'u':
+ return TryAppendUnicodeEscape(inner, ref index, unescaped);
+ case 'U':
+ return TryAppendUtf32Escape(inner, ref index, unescaped);
+ case 'x':
+ return TryAppendVariableLengthHexEscape(inner, ref index, unescaped);
+ default:
+ return false;
+ }
+ }
+
+ private static bool TryAppendUnicodeEscape(
+ string inner,
+ ref int index,
+ StringBuilder unescaped)
+ {
+ if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue))
+ {
+ return false;
+ }
+
+ unescaped.Append((char)unicodeValue);
+ return true;
+ }
+
+ private static bool TryAppendUtf32Escape(
+ string inner,
+ ref int index,
+ StringBuilder unescaped)
+ {
+ if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) ||
+ !IsValidUtf32CodePoint(codePoint))
+ {
+ return false;
+ }
+
+ unescaped.Append(char.ConvertFromUtf32(codePoint));
+ return true;
+ }
+
+ private static bool TryAppendVariableLengthHexEscape(
+ string inner,
+ ref int index,
+ StringBuilder unescaped)
+ {
+ if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue))
+ {
+ return false;
+ }
+
+ unescaped.Append((char)variableLengthValue);
+ return true;
+ }
+
+ private static bool TryParseHexDigits(string value, ref int index, int digitCount, out int parsedValue)
+ {
+ int start = index + 1;
+ if (start + digitCount > value.Length)
+ {
+ parsedValue = 0;
+ return false;
+ }
+
+ string hex = value.Substring(start, digitCount);
+ if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
+ {
+ return false;
+ }
+
+ index += digitCount;
+ return true;
+ }
+
+ private static bool TryParseVariableLengthHexDigits(string value, ref int index, out int parsedValue)
+ {
+ int start = index + 1;
+ int end = AdvanceVariableLengthHexDigits(value, start, 4);
+ if (end == start)
+ {
+ parsedValue = 0;
+ return false;
+ }
+
+ string hex = value.Substring(start, end - start);
+ if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
+ {
+ return false;
+ }
+
+ index = end - 1;
+ return true;
+ }
+
+ private static bool IsValidUtf32CodePoint(int value)
+ {
+ return value >= 0
+ && value <= 0x10FFFF
+ && (value < 0xD800 || value > 0xDFFF);
+ }
+
+ private static bool IsHexDigit(char value)
+ {
+ return (value >= '0' && value <= '9')
+ || (value >= 'a' && value <= 'f')
+ || (value >= 'A' && value <= 'F');
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs.meta
new file mode 100644
index 0000000000..805d456920
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeRegularStringLiteralUnescaper.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7b3f43db33fa47e38cd96d4e15f1a27c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
index 92d6ef7bcd..757da064e1 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHost.cs
@@ -17,126 +17,9 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
internal static class SharedRoslynCompilerWorkerHost
{
private const int SharedCompilerWorkerMaxAttempts = 2;
- private const string RoslynWorkerSourceFileName = "RoslynCompilerWorker.cs";
- private const string RoslynWorkerAssemblyFileName = "RoslynCompilerWorker.dll";
- private const string RoslynWorkerCompileResponseFileName = "RoslynCompilerWorker.rsp";
private static readonly SharedRoslynCompilerWorkerSession ServiceValue = new();
- ///
- /// Carries the result data produced by Worker Attempt behavior.
- ///
- private sealed class WorkerAttemptResult
- {
- public CompilerMessage[] Messages { get; }
-
- public bool ShouldRetry { get; }
-
- public string FailureReason { get; }
-
- public object FailureContext { get; }
-
- private WorkerAttemptResult(
- CompilerMessage[] messages,
- bool shouldRetry,
- string failureReason,
- object failureContext)
- {
- Messages = messages;
- ShouldRetry = shouldRetry;
- FailureReason = failureReason;
- FailureContext = failureContext;
- }
-
- public bool Succeeded => Messages != null;
-
- public static WorkerAttemptResult Successful(CompilerMessage[] messages)
- {
- return new WorkerAttemptResult(messages, false, null, null);
- }
-
- public static WorkerAttemptResult RetryableFailure(string failureReason, object failureContext)
- {
- return new WorkerAttemptResult(null, true, failureReason, failureContext);
- }
-
- public static WorkerAttemptResult NonRetryableFailure(string failureReason, object failureContext)
- {
- return new WorkerAttemptResult(null, false, failureReason, failureContext);
- }
- }
-
- ///
- /// Carries the result data produced by Worker Startup behavior.
- ///
- private sealed class WorkerStartupResult
- {
- public bool IsReady { get; }
-
- public bool IsRetryable { get; }
-
- public string FailureReason { get; }
-
- public object FailureContext { get; }
-
- private WorkerStartupResult(
- bool isReady,
- bool isRetryable,
- string failureReason,
- object failureContext)
- {
- IsReady = isReady;
- IsRetryable = isRetryable;
- FailureReason = failureReason;
- FailureContext = failureContext;
- }
-
- public static WorkerStartupResult Ready()
- {
- return new WorkerStartupResult(true, false, null, null);
- }
-
- public static WorkerStartupResult Failure(string failureReason, object failureContext)
- {
- return new WorkerStartupResult(false, true, failureReason, failureContext);
- }
-
- public static WorkerStartupResult ClosedLifecycleFailure()
- {
- return new WorkerStartupResult(
- false,
- false,
- "shared_worker_lifecycle_closed",
- new { reason = "lifecycle_generation_advanced" });
- }
- }
-
- ///
- /// Provides Worker Paths behavior for Unity CLI Loop.
- ///
- private sealed class WorkerPaths
- {
- public string DirectoryPath { get; }
-
- public string SourcePath { get; }
-
- public string AssemblyPath { get; }
-
- public string CompileResponseFilePath { get; }
-
- public WorkerPaths(
- string directoryPath,
- string sourcePath,
- string assemblyPath,
- string compileResponseFilePath)
- {
- DirectoryPath = directoryPath;
- SourcePath = sourcePath;
- AssemblyPath = assemblyPath;
- CompileResponseFilePath = compileResponseFilePath;
- }
- }
-
internal static void RegisterLifecycleForEditorStartup()
{
AssemblyReloadEvents.beforeAssemblyReload -= ShutdownForReload;
@@ -216,7 +99,8 @@ private static async Task TryCompileOnceAsync(
Action markBuildFinished,
Action incrementBuildCount)
{
- WorkerStartupResult startupResult = await EnsureWorkerReadyAsync(
+ WorkerStartupResult startupResult = await SharedRoslynCompilerWorkerHostProcess.EnsureWorkerReadyAsync(
+ ServiceValue,
externalCompilerPaths,
lifecycleGenerationAtStart).ConfigureAwait(false);
if (!startupResult.IsReady)
@@ -241,117 +125,6 @@ private static async Task TryCompileOnceAsync(
incrementBuildCount).ConfigureAwait(false);
}
- private static async Task EnsureWorkerReadyAsync(
- ExternalCompilerPaths externalCompilerPaths,
- int lifecycleGenerationAtStart)
- {
- WorkerStartupResult earlyResult = ServiceValue.ExecuteWithStateLock(() =>
- {
- if (!ServiceValue.IsLifecycleGenerationCurrentLocked(lifecycleGenerationAtStart))
- {
- return WorkerStartupResult.ClosedLifecycleFailure();
- }
-
- if (ServiceValue.HasLiveProcessLocked())
- {
- return WorkerStartupResult.Ready();
- }
-
- return null;
- });
- if (earlyResult != null)
- {
- return earlyResult;
- }
-
- WorkerPaths workerPaths = CreateWorkerPaths();
- SynchronizeWorkerSource(workerPaths);
-
- WorkerStartupResult workerAssemblyResult = await EnsureWorkerAssemblyBuiltAsync(
- externalCompilerPaths,
- workerPaths).ConfigureAwait(false);
- if (!workerAssemblyResult.IsReady)
- {
- return workerAssemblyResult;
- }
-
- return StartWorkerProcess(
- externalCompilerPaths,
- workerPaths,
- lifecycleGenerationAtStart);
- }
-
- private static async Task EnsureWorkerAssemblyBuiltAsync(
- ExternalCompilerPaths externalCompilerPaths,
- WorkerPaths workerPaths)
- {
- if (File.Exists(workerPaths.AssemblyPath))
- {
- return WorkerStartupResult.Ready();
- }
-
- // Why outside state lock: worker DLL compile can take seconds; shutdown must still kill
- // an already-running shared worker without waiting on this build.
- // Why Task.Run (via CompileWorkerAssemblyAsync): WaitForExit(timeout) is synchronous and
- // this path can still run on the Unity main thread before the first await when the
- // compile gate is acquired without yielding.
- SharedRoslynCompilerWorkerAssemblyBuilder.WorkerAssemblyBuildResult buildResult =
- await ServiceValue.CompileWorkerAssemblyAsync(
- externalCompilerPaths,
- workerPaths.SourcePath,
- workerPaths.AssemblyPath,
- workerPaths.CompileResponseFilePath).ConfigureAwait(false);
- if (!buildResult.StartedSuccessfully)
- {
- return WorkerStartupResult.Failure(
- buildResult.FailureReason,
- buildResult.FailureContext);
- }
-
- if (!HasErrors(buildResult.Messages))
- {
- return WorkerStartupResult.Ready();
- }
-
- SharedRoslynCompilerWorkerAssemblyBuilder.DeleteWorkerAssemblyIfPresent(workerPaths.AssemblyPath);
- return WorkerStartupResult.Failure(
- "worker_build_failed",
- new
- {
- first_error = FindFirstErrorMessage(buildResult.Messages),
- worker_source_path = workerPaths.SourcePath
- });
- }
-
- private static WorkerStartupResult StartWorkerProcess(
- ExternalCompilerPaths externalCompilerPaths,
- WorkerPaths workerPaths,
- int lifecycleGenerationAtStart)
- {
- ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
- return ServiceValue.ExecuteWithStateLock(() =>
- {
- if (!ServiceValue.IsLifecycleGenerationCurrentLocked(lifecycleGenerationAtStart))
- {
- return WorkerStartupResult.ClosedLifecycleFailure();
- }
-
- bool started = ServiceValue.StartProcessLocked(startInfo);
- if (!started)
- {
- return WorkerStartupResult.Failure(
- "worker_start_failed",
- new
- {
- dotnet_host_path = externalCompilerPaths.DotnetHostPath,
- worker_assembly_path = workerPaths.AssemblyPath
- });
- }
-
- return WorkerStartupResult.Ready();
- });
- }
-
private static async Task InvokeWorkerOnceAsync(
string requestFilePath,
CancellationToken ct,
@@ -461,91 +234,6 @@ private static async Task ReadWorkerResponseAsync(
return WorkerAttemptResult.Successful(compilerMessages);
}
- private static WorkerPaths CreateWorkerPaths()
- {
- string workerDirectoryPath = GetWorkerDirectoryPath();
- Directory.CreateDirectory(workerDirectoryPath);
- ServiceValue.ExecuteWithStateLock(
- () => ServiceValue.RecordWorkerDirectoryLocked(workerDirectoryPath));
- return new WorkerPaths(
- workerDirectoryPath,
- Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
- Path.Combine(workerDirectoryPath, RoslynWorkerAssemblyFileName),
- Path.Combine(workerDirectoryPath, RoslynWorkerCompileResponseFileName));
- }
-
- private static string GetWorkerDirectoryPath()
- {
- return Path.Combine(
- Path.GetTempPath(),
- "UnityCliLoopCompilation",
- $"RoslynWorker-{Process.GetCurrentProcess().Id}");
- }
-
- private static void SynchronizeWorkerSource(WorkerPaths workerPaths)
- {
- string workerSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource();
- if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource)
- {
- return;
- }
-
- File.WriteAllText(workerPaths.SourcePath, workerSource);
- if (File.Exists(workerPaths.AssemblyPath))
- {
- File.Delete(workerPaths.AssemblyPath);
- }
- }
-
- private static ProcessStartInfo CreateWorkerStartInfo(
- ExternalCompilerPaths externalCompilerPaths,
- WorkerPaths workerPaths)
- {
- ProcessStartInfo startInfo = new()
- {
- FileName = externalCompilerPaths.DotnetHostPath,
- Arguments = "exec"
- + " --runtimeconfig " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
- + " --depsfile " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(externalCompilerPaths.CompilerDepsFilePath)
- + " " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(workerPaths.AssemblyPath),
- WorkingDirectory = workerPaths.DirectoryPath,
- UseShellExecute = false,
- RedirectStandardInput = true,
- RedirectStandardOutput = true,
- RedirectStandardError = false,
- CreateNoWindow = true
- };
-
- SharedRoslynCompilerWorkerAssemblyBuilder.ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
- return startInfo;
- }
-
- private static bool HasErrors(IReadOnlyCollection messages)
- {
- foreach (CompilerMessage message in messages)
- {
- if (message.type == CompilerMessageType.Error)
- {
- return true;
- }
- }
-
- return false;
- }
-
- private static string FindFirstErrorMessage(IReadOnlyCollection messages)
- {
- foreach (CompilerMessage message in messages)
- {
- if (message.type == CompilerMessageType.Error)
- {
- return message.message;
- }
- }
-
- return string.Empty;
- }
-
private static void ShutdownForReload()
{
Shutdown();
@@ -585,7 +273,7 @@ internal static void SetResponseTimeoutMillisecondsForTests(int timeoutMilliseco
private static void Shutdown()
{
- ServiceValue.Shutdown(GetWorkerDirectoryPath());
+ ServiceValue.Shutdown(SharedRoslynCompilerWorkerHostProcess.GetWorkerDirectoryPath());
}
private static WorkerAttemptResult CreateRetryableWorkerCommunicationFailure(
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs
new file mode 100644
index 0000000000..9d88227b5a
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs
@@ -0,0 +1,247 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using UnityEditor.Compilation;
+
+using io.github.hatayama.UnityCliLoop.ToolContracts;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Provides worker process paths, source sync, assembly build, and startup for the shared Roslyn compiler worker.
+ ///
+ internal static class SharedRoslynCompilerWorkerHostProcess
+ {
+ private const string RoslynWorkerSourceFileName = "RoslynCompilerWorker.cs";
+ private const string RoslynWorkerAssemblyFileName = "RoslynCompilerWorker.dll";
+ private const string RoslynWorkerCompileResponseFileName = "RoslynCompilerWorker.rsp";
+
+ ///
+ /// Provides Worker Paths behavior for Unity CLI Loop.
+ ///
+ internal sealed class WorkerPaths
+ {
+ public string DirectoryPath { get; }
+
+ public string SourcePath { get; }
+
+ public string AssemblyPath { get; }
+
+ public string CompileResponseFilePath { get; }
+
+ public WorkerPaths(
+ string directoryPath,
+ string sourcePath,
+ string assemblyPath,
+ string compileResponseFilePath)
+ {
+ DirectoryPath = directoryPath;
+ SourcePath = sourcePath;
+ AssemblyPath = assemblyPath;
+ CompileResponseFilePath = compileResponseFilePath;
+ }
+ }
+
+ internal static string GetWorkerDirectoryPath()
+ {
+ return Path.Combine(
+ Path.GetTempPath(),
+ "UnityCliLoopCompilation",
+ $"RoslynWorker-{Process.GetCurrentProcess().Id}");
+ }
+
+ internal static WorkerPaths CreateWorkerPaths(SharedRoslynCompilerWorkerSession session)
+ {
+ string workerDirectoryPath = GetWorkerDirectoryPath();
+ Directory.CreateDirectory(workerDirectoryPath);
+ session.ExecuteWithStateLock(
+ () => session.RecordWorkerDirectoryLocked(workerDirectoryPath));
+ return new WorkerPaths(
+ workerDirectoryPath,
+ Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
+ Path.Combine(workerDirectoryPath, RoslynWorkerAssemblyFileName),
+ Path.Combine(workerDirectoryPath, RoslynWorkerCompileResponseFileName));
+ }
+
+ internal static void SynchronizeWorkerSource(WorkerPaths workerPaths)
+ {
+ string workerSource = SharedRoslynCompilerWorkerProtocol.CreateProgramSource();
+ if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource)
+ {
+ return;
+ }
+
+ File.WriteAllText(workerPaths.SourcePath, workerSource);
+ if (File.Exists(workerPaths.AssemblyPath))
+ {
+ File.Delete(workerPaths.AssemblyPath);
+ }
+ }
+
+ internal static ProcessStartInfo CreateWorkerStartInfo(
+ ExternalCompilerPaths externalCompilerPaths,
+ WorkerPaths workerPaths)
+ {
+ ProcessStartInfo startInfo = new()
+ {
+ FileName = externalCompilerPaths.DotnetHostPath,
+ Arguments = "exec"
+ + " --runtimeconfig " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
+ + " --depsfile " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(externalCompilerPaths.CompilerDepsFilePath)
+ + " " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(workerPaths.AssemblyPath),
+ WorkingDirectory = workerPaths.DirectoryPath,
+ UseShellExecute = false,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = false,
+ CreateNoWindow = true
+ };
+
+ SharedRoslynCompilerWorkerAssemblyBuilder.ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
+ return startInfo;
+ }
+
+ internal static async Task EnsureWorkerReadyAsync(
+ SharedRoslynCompilerWorkerSession session,
+ ExternalCompilerPaths externalCompilerPaths,
+ int lifecycleGenerationAtStart)
+ {
+ WorkerStartupResult earlyResult = session.ExecuteWithStateLock(() =>
+ {
+ if (!session.IsLifecycleGenerationCurrentLocked(lifecycleGenerationAtStart))
+ {
+ return WorkerStartupResult.ClosedLifecycleFailure();
+ }
+
+ if (session.HasLiveProcessLocked())
+ {
+ return WorkerStartupResult.Ready();
+ }
+
+ return null;
+ });
+ if (earlyResult != null)
+ {
+ return earlyResult;
+ }
+
+ WorkerPaths workerPaths = CreateWorkerPaths(session);
+ SynchronizeWorkerSource(workerPaths);
+
+ WorkerStartupResult workerAssemblyResult = await EnsureWorkerAssemblyBuiltAsync(
+ session,
+ externalCompilerPaths,
+ workerPaths).ConfigureAwait(false);
+ if (!workerAssemblyResult.IsReady)
+ {
+ return workerAssemblyResult;
+ }
+
+ return StartWorkerProcess(
+ session,
+ externalCompilerPaths,
+ workerPaths,
+ lifecycleGenerationAtStart);
+ }
+
+ internal static async Task EnsureWorkerAssemblyBuiltAsync(
+ SharedRoslynCompilerWorkerSession session,
+ ExternalCompilerPaths externalCompilerPaths,
+ WorkerPaths workerPaths)
+ {
+ if (File.Exists(workerPaths.AssemblyPath))
+ {
+ return WorkerStartupResult.Ready();
+ }
+
+ // Why outside state lock: worker DLL compile can take seconds; shutdown must still kill
+ // an already-running shared worker without waiting on this build.
+ // Why Task.Run (via CompileWorkerAssemblyAsync): WaitForExit(timeout) is synchronous and
+ // this path can still run on the Unity main thread before the first await when the
+ // compile gate is acquired without yielding.
+ SharedRoslynCompilerWorkerAssemblyBuilder.WorkerAssemblyBuildResult buildResult =
+ await session.CompileWorkerAssemblyAsync(
+ externalCompilerPaths,
+ workerPaths.SourcePath,
+ workerPaths.AssemblyPath,
+ workerPaths.CompileResponseFilePath).ConfigureAwait(false);
+ if (!buildResult.StartedSuccessfully)
+ {
+ return WorkerStartupResult.Failure(
+ buildResult.FailureReason,
+ buildResult.FailureContext);
+ }
+
+ if (!HasErrors(buildResult.Messages))
+ {
+ return WorkerStartupResult.Ready();
+ }
+
+ SharedRoslynCompilerWorkerAssemblyBuilder.DeleteWorkerAssemblyIfPresent(workerPaths.AssemblyPath);
+ return WorkerStartupResult.Failure(
+ "worker_build_failed",
+ new
+ {
+ first_error = FindFirstErrorMessage(buildResult.Messages),
+ worker_source_path = workerPaths.SourcePath
+ });
+ }
+
+ internal static WorkerStartupResult StartWorkerProcess(
+ SharedRoslynCompilerWorkerSession session,
+ ExternalCompilerPaths externalCompilerPaths,
+ WorkerPaths workerPaths,
+ int lifecycleGenerationAtStart)
+ {
+ ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
+ return session.ExecuteWithStateLock(() =>
+ {
+ if (!session.IsLifecycleGenerationCurrentLocked(lifecycleGenerationAtStart))
+ {
+ return WorkerStartupResult.ClosedLifecycleFailure();
+ }
+
+ bool started = session.StartProcessLocked(startInfo);
+ if (!started)
+ {
+ return WorkerStartupResult.Failure(
+ "worker_start_failed",
+ new
+ {
+ dotnet_host_path = externalCompilerPaths.DotnetHostPath,
+ worker_assembly_path = workerPaths.AssemblyPath
+ });
+ }
+
+ return WorkerStartupResult.Ready();
+ });
+ }
+
+ private static bool HasErrors(IReadOnlyCollection messages)
+ {
+ foreach (CompilerMessage message in messages)
+ {
+ if (message.type == CompilerMessageType.Error)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static string FindFirstErrorMessage(IReadOnlyCollection messages)
+ {
+ foreach (CompilerMessage message in messages)
+ {
+ if (message.type == CompilerMessageType.Error)
+ {
+ return message.message;
+ }
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs.meta
new file mode 100644
index 0000000000..7c53c1d410
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostProcess.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 350910eb8ec944c6a51c98d4bf06e5c9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs
new file mode 100644
index 0000000000..e6eadfe316
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs
@@ -0,0 +1,92 @@
+using UnityEditor.Compilation;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Carries the result data produced by Worker Attempt behavior.
+ ///
+ internal sealed class WorkerAttemptResult
+ {
+ public CompilerMessage[] Messages { get; }
+
+ public bool ShouldRetry { get; }
+
+ public string FailureReason { get; }
+
+ public object FailureContext { get; }
+
+ private WorkerAttemptResult(
+ CompilerMessage[] messages,
+ bool shouldRetry,
+ string failureReason,
+ object failureContext)
+ {
+ Messages = messages;
+ ShouldRetry = shouldRetry;
+ FailureReason = failureReason;
+ FailureContext = failureContext;
+ }
+
+ public bool Succeeded => Messages != null;
+
+ public static WorkerAttemptResult Successful(CompilerMessage[] messages)
+ {
+ return new WorkerAttemptResult(messages, false, null, null);
+ }
+
+ public static WorkerAttemptResult RetryableFailure(string failureReason, object failureContext)
+ {
+ return new WorkerAttemptResult(null, true, failureReason, failureContext);
+ }
+
+ public static WorkerAttemptResult NonRetryableFailure(string failureReason, object failureContext)
+ {
+ return new WorkerAttemptResult(null, false, failureReason, failureContext);
+ }
+ }
+
+ ///
+ /// Carries the result data produced by Worker Startup behavior.
+ ///
+ internal sealed class WorkerStartupResult
+ {
+ public bool IsReady { get; }
+
+ public bool IsRetryable { get; }
+
+ public string FailureReason { get; }
+
+ public object FailureContext { get; }
+
+ private WorkerStartupResult(
+ bool isReady,
+ bool isRetryable,
+ string failureReason,
+ object failureContext)
+ {
+ IsReady = isReady;
+ IsRetryable = isRetryable;
+ FailureReason = failureReason;
+ FailureContext = failureContext;
+ }
+
+ public static WorkerStartupResult Ready()
+ {
+ return new WorkerStartupResult(true, false, null, null);
+ }
+
+ public static WorkerStartupResult Failure(string failureReason, object failureContext)
+ {
+ return new WorkerStartupResult(false, true, failureReason, failureContext);
+ }
+
+ public static WorkerStartupResult ClosedLifecycleFailure()
+ {
+ return new WorkerStartupResult(
+ false,
+ false,
+ "shared_worker_lifecycle_closed",
+ new { reason = "lifecycle_generation_advanced" });
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs.meta
new file mode 100644
index 0000000000..02665f28d5
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SharedRoslynCompilerWorkerHostResults.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8656547f5b694b52ab83e26e76c8ab10
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs
index e958654dc0..800bc78139 100644
--- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs
@@ -36,7 +36,7 @@ public static SourceShapeResult Analyze(string source)
}
else
{
- pos = AdvanceInsideBlock(source, pos, ref braceDepth);
+ pos = SourceTokenScanner.AdvanceInsideBlock(source, pos, ref braceDepth);
}
}
@@ -70,12 +70,12 @@ private static SourceTopLevelStep AnalyzeTopLevelSourceStep(
int pos,
int braceDepth)
{
- if (TryMatchLineComment(source, pos, out int afterComment))
+ if (SourceTokenScanner.TryMatchLineComment(source, pos, out int afterComment))
{
return new SourceTopLevelStep(afterComment, braceDepth);
}
- if (TryMatchBlockComment(source, pos, out int afterBlock))
+ if (SourceTokenScanner.TryMatchBlockComment(source, pos, out int afterBlock))
{
return new SourceTopLevelStep(afterBlock, braceDepth);
}
@@ -100,7 +100,7 @@ private static SourceTopLevelStep AnalyzeTopLevelSourceStep(
}
int segmentStart = pos;
- int afterUsing = SkipWhitespaceAndComments(source, pos + 5);
+ int afterUsing = SourceTokenScanner.SkipWhitespaceAndComments(source, pos + 5);
if (StartsWithKeyword(source, afterUsing, "static"))
{
return AddUsingDirectiveStep(source, segmentStart, braceDepth, result);
@@ -121,7 +121,7 @@ private static SourceTopLevelStep AnalyzeTopLevelSourceStep(
int braceDepth,
SourceShapeResult result)
{
- int usingPos = SkipWhitespaceAndComments(source, pos + 6);
+ int usingPos = SourceTokenScanner.SkipWhitespaceAndComments(source, pos + 6);
if (!StartsWithKeyword(source, usingPos, "using"))
{
return null;
@@ -136,7 +136,7 @@ private static SourceTopLevelStep AddUsingDirectiveStep(
int braceDepth,
SourceShapeResult result)
{
- int semiEnd = FindSemicolon(source, segmentStart);
+ int semiEnd = SourceTokenScanner.FindSemicolon(source, segmentStart);
RegisterUsingDirective(result, source, segmentStart, semiEnd);
return new SourceTopLevelStep(semiEnd + 1, braceDepth);
}
@@ -163,7 +163,7 @@ private static string ExtractUsingAliasName(string source, int segmentStart, int
int position = segmentStart;
if (StartsWithKeyword(source, position, "global"))
{
- position = SkipWhitespaceAndComments(source, position + "global".Length);
+ position = SourceTokenScanner.SkipWhitespaceAndComments(source, position + "global".Length);
}
if (!StartsWithKeyword(source, position, "using"))
@@ -171,7 +171,7 @@ private static string ExtractUsingAliasName(string source, int segmentStart, int
return null;
}
- position = SkipWhitespaceAndComments(source, position + "using".Length);
+ position = SourceTokenScanner.SkipWhitespaceAndComments(source, position + "using".Length);
if (StartsWithKeyword(source, position, "static"))
{
return null;
@@ -183,7 +183,7 @@ private static string ExtractUsingAliasName(string source, int segmentStart, int
return null;
}
- int equalsPosition = SkipWhitespaceAndComments(source, aliasName.EndPosition);
+ int equalsPosition = SourceTokenScanner.SkipWhitespaceAndComments(source, aliasName.EndPosition);
if (equalsPosition > semiEnd || source[equalsPosition] != '=')
{
return null;
@@ -225,29 +225,6 @@ private static bool IsIdentifierPart(char value)
return char.IsLetterOrDigit(value) || value == '_';
}
- private static int SkipWhitespaceAndComments(string source, int pos)
- {
- int current = SkipWhitespace(source, pos);
- while (true)
- {
- if (TryMatchLineComment(source, current, out int afterLine))
- {
- current = SkipWhitespace(source, afterLine);
- continue;
- }
-
- if (TryMatchBlockComment(source, current, out int afterBlock))
- {
- current = SkipWhitespace(source, afterBlock);
- continue;
- }
-
- break;
- }
-
- return current;
- }
-
private static SourceTopLevelStep? TryAnalyzeTopLevelDeclaration(
string source,
int pos,
@@ -281,7 +258,7 @@ private static int SkipWhitespaceAndComments(string source, int pos)
return null;
}
- int afterAttr = SkipAttributeBlock(source, pos);
+ int afterAttr = SourceTokenScanner.SkipAttributeBlock(source, pos);
int nextNonWs = SkipWhitespace(source, afterAttr);
int declarationStart = SkipAccessModifiers(source, nextNonWs);
if (declarationStart >= source.Length || !IsTypeDeclarationKeyword(source, declarationStart))
@@ -322,7 +299,7 @@ private static SourceTopLevelStep AnalyzeTopLevelStatement(
{
result.HasTopLevelStatements = true;
int nextBraceDepth = braceDepth;
- int stmtEnd = FindStatementEnd(source, pos, ref nextBraceDepth);
+ int stmtEnd = SourceTokenScanner.FindStatementEnd(source, pos, ref nextBraceDepth);
int originalLineNumber1Based = GetLineNumber1Based(source, pos);
PadTopLevelBodyBuilderToOriginalLine(result, originalLineNumber1Based);
string statementText = source.Substring(pos, stmtEnd - pos + 1).TrimEnd();
@@ -378,7 +355,7 @@ private static int CountLinesInText(string text)
private static SourceTopLevelStep SkipTopLevelBlock(string source, int pos, int braceDepth)
{
int nextBraceDepth = braceDepth;
- int nextPosition = SkipBlock(source, pos, ref nextBraceDepth);
+ int nextPosition = SourceTokenScanner.SkipBlock(source, pos, ref nextBraceDepth);
return new SourceTopLevelStep(nextPosition, nextBraceDepth);
}
@@ -414,10 +391,7 @@ public static string WrapIfNeeded(string source, string namespaceName, string cl
internal static int SkipWhitespace(string s, int pos)
{
- Debug.Assert(s != null, "s must not be null");
- Debug.Assert(pos >= 0, "pos must be non-negative");
- while (pos < s.Length && char.IsWhiteSpace(s[pos])) pos++;
- return pos;
+ return SourceTokenScanner.SkipWhitespace(s, pos);
}
internal static bool StartsWithKeyword(string s, int pos, string keyword)
@@ -471,380 +445,9 @@ private static int SkipAccessModifiers(string s, int pos)
return pos;
}
- private static bool TryMatchLineComment(string s, int pos, out int afterComment)
- {
- afterComment = pos;
- if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '/')
- {
- int end = pos + 2;
- while (end < s.Length && s[end] != '\n') end++;
- if (end < s.Length) end++; // skip \n
- afterComment = end;
- return true;
- }
- return false;
- }
-
- private static bool TryMatchBlockComment(string s, int pos, out int afterBlock)
- {
- afterBlock = pos;
- if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '*')
- {
- int end = pos + 2;
- while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
- afterBlock = end + 2 < s.Length ? end + 2 : s.Length;
- return true;
- }
- return false;
- }
-
- private static int FindSemicolon(string s, int pos)
- {
- while (pos < s.Length)
- {
- if (s[pos] == ';') return pos;
- pos = AdvanceOneToken(s, pos);
- }
- return s.Length - 1;
- }
-
- private static int FindStatementEnd(string s, int pos, ref int braceDepth)
- {
- while (pos < s.Length)
- {
- char c = s[pos];
- if (c == '{')
- {
- braceDepth++;
- pos++;
- while (pos < s.Length && braceDepth > 0)
- {
- pos = AdvanceInsideBlock(s, pos, ref braceDepth);
- }
- return pos - 1;
- }
- if (c == ';') return pos;
- pos = AdvanceOneToken(s, pos);
- }
- return s.Length - 1;
- }
-
- private static int SkipBlock(string s, int pos, ref int braceDepth)
- {
- // Advance past keywords until we hit the opening brace
- while (pos < s.Length && s[pos] != '{')
- {
- if (s[pos] == ';') return pos + 1; // forward declaration
- pos = AdvanceOneToken(s, pos);
- }
- if (pos < s.Length && s[pos] == '{')
- {
- braceDepth++;
- pos++;
- while (pos < s.Length && braceDepth > 0)
- {
- pos = AdvanceInsideBlock(s, pos, ref braceDepth);
- }
- }
- return pos;
- }
-
- private static int SkipAttributeBlock(string s, int pos)
- {
- Debug.Assert(s[pos] == '[', "SkipAttributeBlock must start at '['");
- int depth = 1;
- pos++;
- while (pos < s.Length && depth > 0)
- {
- if (s[pos] == '[') depth++;
- else if (s[pos] == ']') depth--;
- else pos = AdvanceOneToken(s, pos) - 1; // -1 because loop will pos++ via fallthrough
- pos++;
- }
- return pos;
- }
-
- private static int AdvanceInsideBlock(string s, int pos, ref int braceDepth)
- {
- char c = s[pos];
- if (c == '{') { braceDepth++; return pos + 1; }
- if (c == '}') { braceDepth--; return pos + 1; }
- return AdvanceOneToken(s, pos);
- }
-
internal static int AdvanceOneTokenPublic(string s, int pos)
{
- return AdvanceOneToken(s, pos);
- }
-
- private static int AdvanceOneToken(string s, int pos)
- {
- if (pos >= s.Length) return s.Length;
-
- (bool matched, int nextPosition) = TryAdvanceLineComment(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceBlockComment(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceVerbatimString(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceRawString(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceRegularString(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceCharLiteral(s, pos);
- if (matched) return nextPosition;
- (matched, nextPosition) = TryAdvanceInterpolatedString(s, pos);
- if (matched) return nextPosition;
-
- return pos + 1;
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceLineComment(string s, int pos)
- {
- if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '/')
- {
- return (false, pos);
- }
-
- int end = pos + 2;
- while (end < s.Length && s[end] != '\n') end++;
- return (true, end < s.Length ? end + 1 : s.Length);
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceBlockComment(string s, int pos)
- {
- if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '*')
- {
- return (false, pos);
- }
-
- int end = pos + 2;
- while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
- return (true, end + 2 < s.Length ? end + 2 : s.Length);
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceVerbatimString(string s, int pos)
- {
- if (pos + 1 >= s.Length || s[pos] != '@' || s[pos + 1] != '"')
- {
- return (false, pos);
- }
-
- return (true, SkipVerbatimString(s, pos + 2));
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceRawString(string s, int pos)
- {
- if (pos + 2 >= s.Length || s[pos] != '"' || s[pos + 1] != '"' || s[pos + 2] != '"')
- {
- return (false, pos);
- }
-
- return (true, SkipRawString(s, pos + 3));
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceRegularString(string s, int pos)
- {
- if (s[pos] != '"')
- {
- return (false, pos);
- }
-
- return (true, SkipRegularString(s, pos + 1));
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceCharLiteral(string s, int pos)
- {
- if (s[pos] != '\'')
- {
- return (false, pos);
- }
-
- return (true, SkipCharLiteral(s, pos + 1));
- }
-
- private static (bool Matched, int NextPosition) TryAdvanceInterpolatedString(string s, int pos)
- {
- if (pos + 1 >= s.Length || s[pos] != '$' || s[pos + 1] != '"')
- {
- return (false, pos);
- }
-
- return (true, SkipInterpolatedString(s, pos + 2));
- }
-
- private static int SkipInterpolatedString(string s, int pos)
- {
- int end = pos;
- while (end < s.Length)
- {
- if (s[end] == '\\')
- {
- end += 2;
- continue;
- }
-
- if (s[end] == '{')
- {
- if (end + 1 < s.Length && s[end + 1] == '{')
- {
- end += 2;
- continue;
- }
-
- end = SkipInterpolationHole(s, end + 1);
- continue;
- }
-
- if (s[end] == '"' && end + 1 < s.Length && s[end + 1] == '"')
- {
- end += 2;
- continue;
- }
-
- if (s[end] == '"')
- {
- return end + 1;
- }
-
- end++;
- }
-
- return s.Length;
- }
-
- private static int SkipInterpolationHole(string s, int pos)
- {
- int depth = 1;
- int end = pos;
-
- while (end < s.Length && depth > 0)
- {
- int afterLiteral = SkipInterpolationHoleLiteral(s, end);
- if (afterLiteral != end)
- {
- end = afterLiteral;
- continue;
- }
-
- if (s[end] == '{')
- {
- depth++;
- end++;
- continue;
- }
-
- if (s[end] == '}')
- {
- depth--;
- end++;
- continue;
- }
-
- end++;
- }
-
- return end;
- }
-
- private static int SkipInterpolationHoleLiteral(string s, int end)
- {
- if (s[end] == '\\')
- {
- return end + 2;
- }
-
- if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"')
- {
- return SkipVerbatimString(s, end + 2);
- }
-
- if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"')
- {
- return SkipInterpolatedString(s, end + 2);
- }
-
- if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"')
- {
- return SkipRawString(s, end + 3);
- }
-
- if (s[end] == '"')
- {
- return SkipRegularString(s, end + 1);
- }
-
- return s[end] == '\'' ? SkipCharLiteral(s, end + 1) : end;
- }
-
- private static int SkipRegularString(string s, int pos)
- {
- int end = pos;
- while (end < s.Length && s[end] != '"')
- {
- if (s[end] == '\\')
- {
- end++;
- }
-
- end++;
- }
-
- return end < s.Length ? end + 1 : s.Length;
- }
-
- private static int SkipVerbatimString(string s, int pos)
- {
- int end = pos;
- while (end < s.Length)
- {
- if (s[end] == '"')
- {
- if (end + 1 < s.Length && s[end + 1] == '"')
- {
- end += 2;
- continue;
- }
-
- return end + 1;
- }
-
- end++;
- }
-
- return s.Length;
- }
-
- private static int SkipRawString(string s, int pos)
- {
- int end = pos;
- while (end + 2 < s.Length)
- {
- if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"')
- {
- return end + 3;
- }
-
- end++;
- }
-
- return s.Length;
- }
-
- private static int SkipCharLiteral(string s, int pos)
- {
- int end = pos;
- while (end < s.Length && s[end] != '\'')
- {
- if (s[end] == '\\')
- {
- end++;
- }
-
- end++;
- }
-
- return end < s.Length ? end + 1 : s.Length;
+ return SourceTokenScanner.AdvanceOneToken(s, pos);
}
private readonly struct SourceTopLevelStep
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs
new file mode 100644
index 0000000000..852e816555
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs
@@ -0,0 +1,413 @@
+using System.Diagnostics;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Low-level C# source token scanner for skipping strings, comments, and brace-delimited regions.
+ ///
+ internal static class SourceTokenScanner
+ {
+ internal static int SkipWhitespace(string s, int pos)
+ {
+ Debug.Assert(s != null, "s must not be null");
+ Debug.Assert(pos >= 0, "pos must be non-negative");
+ while (pos < s.Length && char.IsWhiteSpace(s[pos])) pos++;
+ return pos;
+ }
+
+ internal static int SkipWhitespaceAndComments(string source, int pos)
+ {
+ // why: own SkipWhitespace so SourceTokenScanner does not depend back on SourceShaper
+ int current = SkipWhitespace(source, pos);
+ while (true)
+ {
+ if (TryMatchLineComment(source, current, out int afterLine))
+ {
+ current = SkipWhitespace(source, afterLine);
+ continue;
+ }
+
+ if (TryMatchBlockComment(source, current, out int afterBlock))
+ {
+ current = SkipWhitespace(source, afterBlock);
+ continue;
+ }
+
+ break;
+ }
+
+ return current;
+ }
+
+ internal static bool TryMatchLineComment(string s, int pos, out int afterComment)
+ {
+ afterComment = pos;
+ if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '/')
+ {
+ int end = pos + 2;
+ while (end < s.Length && s[end] != '\n') end++;
+ if (end < s.Length) end++; // skip \n
+ afterComment = end;
+ return true;
+ }
+ return false;
+ }
+
+ internal static bool TryMatchBlockComment(string s, int pos, out int afterBlock)
+ {
+ afterBlock = pos;
+ if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '*')
+ {
+ int end = pos + 2;
+ while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
+ afterBlock = end + 2 < s.Length ? end + 2 : s.Length;
+ return true;
+ }
+ return false;
+ }
+
+ internal static int FindSemicolon(string s, int pos)
+ {
+ while (pos < s.Length)
+ {
+ if (s[pos] == ';') return pos;
+ pos = AdvanceOneToken(s, pos);
+ }
+ return s.Length - 1;
+ }
+
+ internal static int FindStatementEnd(string s, int pos, ref int braceDepth)
+ {
+ while (pos < s.Length)
+ {
+ char c = s[pos];
+ if (c == '{')
+ {
+ braceDepth++;
+ pos++;
+ while (pos < s.Length && braceDepth > 0)
+ {
+ pos = AdvanceInsideBlock(s, pos, ref braceDepth);
+ }
+ return pos - 1;
+ }
+ if (c == ';') return pos;
+ pos = AdvanceOneToken(s, pos);
+ }
+ return s.Length - 1;
+ }
+
+ internal static int SkipBlock(string s, int pos, ref int braceDepth)
+ {
+ // Advance past keywords until we hit the opening brace
+ while (pos < s.Length && s[pos] != '{')
+ {
+ if (s[pos] == ';') return pos + 1; // forward declaration
+ pos = AdvanceOneToken(s, pos);
+ }
+ if (pos < s.Length && s[pos] == '{')
+ {
+ braceDepth++;
+ pos++;
+ while (pos < s.Length && braceDepth > 0)
+ {
+ pos = AdvanceInsideBlock(s, pos, ref braceDepth);
+ }
+ }
+ return pos;
+ }
+
+ internal static int SkipAttributeBlock(string s, int pos)
+ {
+ Debug.Assert(s[pos] == '[', "SkipAttributeBlock must start at '['");
+ int depth = 1;
+ pos++;
+ while (pos < s.Length && depth > 0)
+ {
+ if (s[pos] == '[') depth++;
+ else if (s[pos] == ']') depth--;
+ else pos = AdvanceOneToken(s, pos) - 1; // -1 because loop will pos++ via fallthrough
+ pos++;
+ }
+ return pos;
+ }
+
+ internal static int AdvanceInsideBlock(string s, int pos, ref int braceDepth)
+ {
+ char c = s[pos];
+ if (c == '{') { braceDepth++; return pos + 1; }
+ if (c == '}') { braceDepth--; return pos + 1; }
+ return AdvanceOneToken(s, pos);
+ }
+
+ internal static int AdvanceOneToken(string s, int pos)
+ {
+ if (pos >= s.Length) return s.Length;
+
+ (bool matched, int nextPosition) = TryAdvanceLineComment(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceBlockComment(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceVerbatimString(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceRawString(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceRegularString(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceCharLiteral(s, pos);
+ if (matched) return nextPosition;
+ (matched, nextPosition) = TryAdvanceInterpolatedString(s, pos);
+ if (matched) return nextPosition;
+
+ return pos + 1;
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceLineComment(string s, int pos)
+ {
+ if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '/')
+ {
+ return (false, pos);
+ }
+
+ int end = pos + 2;
+ while (end < s.Length && s[end] != '\n') end++;
+ return (true, end < s.Length ? end + 1 : s.Length);
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceBlockComment(string s, int pos)
+ {
+ if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '*')
+ {
+ return (false, pos);
+ }
+
+ int end = pos + 2;
+ while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
+ return (true, end + 2 < s.Length ? end + 2 : s.Length);
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceVerbatimString(string s, int pos)
+ {
+ if (pos + 1 >= s.Length || s[pos] != '@' || s[pos + 1] != '"')
+ {
+ return (false, pos);
+ }
+
+ return (true, SkipVerbatimString(s, pos + 2));
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceRawString(string s, int pos)
+ {
+ if (pos + 2 >= s.Length || s[pos] != '"' || s[pos + 1] != '"' || s[pos + 2] != '"')
+ {
+ return (false, pos);
+ }
+
+ return (true, SkipRawString(s, pos + 3));
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceRegularString(string s, int pos)
+ {
+ if (s[pos] != '"')
+ {
+ return (false, pos);
+ }
+
+ return (true, SkipRegularString(s, pos + 1));
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceCharLiteral(string s, int pos)
+ {
+ if (s[pos] != '\'')
+ {
+ return (false, pos);
+ }
+
+ return (true, SkipCharLiteral(s, pos + 1));
+ }
+
+ private static (bool Matched, int NextPosition) TryAdvanceInterpolatedString(string s, int pos)
+ {
+ if (pos + 1 >= s.Length || s[pos] != '$' || s[pos + 1] != '"')
+ {
+ return (false, pos);
+ }
+
+ return (true, SkipInterpolatedString(s, pos + 2));
+ }
+
+ private static int SkipInterpolatedString(string s, int pos)
+ {
+ int end = pos;
+ while (end < s.Length)
+ {
+ if (s[end] == '\\')
+ {
+ end += 2;
+ continue;
+ }
+
+ if (s[end] == '{')
+ {
+ if (end + 1 < s.Length && s[end + 1] == '{')
+ {
+ end += 2;
+ continue;
+ }
+
+ end = SkipInterpolationHole(s, end + 1);
+ continue;
+ }
+
+ if (s[end] == '"' && end + 1 < s.Length && s[end + 1] == '"')
+ {
+ end += 2;
+ continue;
+ }
+
+ if (s[end] == '"')
+ {
+ return end + 1;
+ }
+
+ end++;
+ }
+
+ return s.Length;
+ }
+
+ private static int SkipInterpolationHole(string s, int pos)
+ {
+ int depth = 1;
+ int end = pos;
+
+ while (end < s.Length && depth > 0)
+ {
+ int afterLiteral = SkipInterpolationHoleLiteral(s, end);
+ if (afterLiteral != end)
+ {
+ end = afterLiteral;
+ continue;
+ }
+
+ if (s[end] == '{')
+ {
+ depth++;
+ end++;
+ continue;
+ }
+
+ if (s[end] == '}')
+ {
+ depth--;
+ end++;
+ continue;
+ }
+
+ end++;
+ }
+
+ return end;
+ }
+
+ private static int SkipInterpolationHoleLiteral(string s, int end)
+ {
+ if (s[end] == '\\')
+ {
+ return end + 2;
+ }
+
+ if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"')
+ {
+ return SkipVerbatimString(s, end + 2);
+ }
+
+ if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"')
+ {
+ return SkipInterpolatedString(s, end + 2);
+ }
+
+ if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"')
+ {
+ return SkipRawString(s, end + 3);
+ }
+
+ if (s[end] == '"')
+ {
+ return SkipRegularString(s, end + 1);
+ }
+
+ return s[end] == '\'' ? SkipCharLiteral(s, end + 1) : end;
+ }
+
+ private static int SkipRegularString(string s, int pos)
+ {
+ int end = pos;
+ while (end < s.Length && s[end] != '"')
+ {
+ if (s[end] == '\\')
+ {
+ end++;
+ }
+
+ end++;
+ }
+
+ return end < s.Length ? end + 1 : s.Length;
+ }
+
+ private static int SkipVerbatimString(string s, int pos)
+ {
+ int end = pos;
+ while (end < s.Length)
+ {
+ if (s[end] == '"')
+ {
+ if (end + 1 < s.Length && s[end + 1] == '"')
+ {
+ end += 2;
+ continue;
+ }
+
+ return end + 1;
+ }
+
+ end++;
+ }
+
+ return s.Length;
+ }
+
+ private static int SkipRawString(string s, int pos)
+ {
+ int end = pos;
+ while (end + 2 < s.Length)
+ {
+ if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"')
+ {
+ return end + 3;
+ }
+
+ end++;
+ }
+
+ return s.Length;
+ }
+
+ private static int SkipCharLiteral(string s, int pos)
+ {
+ int end = pos;
+ while (end < s.Length && s[end] != '\'')
+ {
+ if (s[end] == '\\')
+ {
+ end++;
+ }
+
+ end++;
+ }
+
+ return end < s.Length ? end + 1 : s.Length;
+ }
+ }
+}
diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs.meta
new file mode 100644
index 0000000000..1673eda69e
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceTokenScanner.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f54478544d13434cac1dcbdfb53103c4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: