Skip to content

Commit 207d6c3

Browse files
💾 Feat(Compiler): 直接将脚本转换为可编译C#脚本并直接预编译,运行提速10000x(使用BrainFuck编译器脚本测试)
1 parent bd4593f commit 207d6c3

6 files changed

Lines changed: 1387 additions & 2 deletions

File tree

KitX Clients/KitX Core/KitX.Core.BluePrint.Test/Program.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ public static void Main(string[] args)
156156
Console.WriteLine("└──────────────────────────────────────────┘\n");
157157
RunExecutionTest(converter, reverseConverter, parser, sp, GetRawNestedScript(), helpers, "Test K");
158158
}
159+
160+
if (ShouldRunTest("L"))
161+
{
162+
Console.WriteLine("\n┌──────────────────────────────────────────┐");
163+
Console.WriteLine("│ Test L: Assembly Compilation │");
164+
Console.WriteLine("└──────────────────────────────────────────┘\n");
165+
RunAssemblyCompilationTest(reverseConverter, parser, sp);
166+
}
159167
}
160168

161169
private static void ParseArgs(string[] args)
@@ -366,6 +374,106 @@ private static void RunExecutionTest(
366374
}
367375
}
368376

377+
// Test L: Assembly Compilation
378+
// ──────────────────────────────────────────────
379+
private static void RunAssemblyCompilationTest(
380+
IBlueprintToBlockScriptConverter reverseConverter,
381+
IBlockScriptParser parser,
382+
System.IServiceProvider sp)
383+
{
384+
try
385+
{
386+
var sourceCode = @"#ConstBlock
387+
int count = 0;
388+
int max = 3;
389+
390+
#MainBlock
391+
Set(""count"", 0);
392+
NextBlock = ""LoopBlock"";
393+
394+
#Block LoopBlock
395+
NextBlock = Loop(HelperFuncCompare(""BLT"", Get(""count""), max), ""PrintBlock"", ""EndBlock"");
396+
397+
#Block PrintBlock
398+
Print(Get(""count""));
399+
Set(""count"", HelperFuncAdd(Get(""count""), 1));
400+
NextBlock = ToLoopCond(""LoopBlock"");
401+
402+
#Block EndBlock
403+
Print(""Done"");
404+
";
405+
406+
var parseResult = parser.Parse(sourceCode);
407+
if (!parseResult.IsSuccess || parseResult.Script == null)
408+
{
409+
Console.WriteLine("[Test L] FAILED: Parse error: " + parseResult.ErrorMessage);
410+
return;
411+
}
412+
413+
parseResult.Script.HelperFunctions = GetExecutionHelpers();
414+
415+
// Test 1: Direct ScriptAssemblyCompiler test
416+
var compiler = new ScriptAssemblyCompiler();
417+
Console.WriteLine("[Test L] Compiling...");
418+
var compiled = compiler.CompileScript(parseResult.Script);
419+
Console.WriteLine($"[Test L] Assembly compilation: {(compiled != null ? "SUCCESS" : "FAILED (null)")}");
420+
421+
if (compiled != null)
422+
{
423+
// Test 2: Execute via compiled assembly
424+
var output = new List<string>();
425+
var globals = new BlockScriptExecutionGlobals(
426+
new BlockScopeManager(), output);
427+
globals.ResetRunState();
428+
429+
Console.WriteLine("[Test L] About to call Run()...");
430+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
431+
try
432+
{
433+
compiled.Run(globals, cts.Token);
434+
Console.WriteLine($"[Test L] Assembly execution: SUCCESS");
435+
Console.WriteLine($"[Test L] ExecutedBlockCount: {globals.ExecutedBlockCount}");
436+
Console.WriteLine($"[Test L] Output: [{string.Join(", ", output)}]");
437+
}
438+
catch (OperationCanceledException)
439+
{
440+
Console.WriteLine("[Test L] Assembly execution: TIMEOUT (infinite loop?)");
441+
}
442+
catch (Exception ex)
443+
{
444+
Console.WriteLine($"[Test L] Assembly execution: EXCEPTION - {ex.GetType().Name}: {ex.Message}");
445+
if (ex.InnerException != null)
446+
Console.WriteLine($" Inner: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
447+
}
448+
}
449+
450+
// Test 3: Compare via IBlockScriptExecutor (should use assembly path)
451+
var executor = sp.GetRequiredService<IBlockScriptExecutor>();
452+
using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(5));
453+
var result = executor.ExecuteAsync(parseResult.Script, cancellationToken: cts2.Token)
454+
.GetAwaiter().GetResult();
455+
Console.WriteLine($"[Test L] IBlockScriptExecutor: IsSuccess={result.IsSuccess}, BlockCount={result.ExecutedBlockCount}");
456+
Console.WriteLine($"[Test L] Executor output ({result.Output.Count} lines): [{string.Join(", ", result.Output)}]");
457+
458+
// Verify output matches expected: 0, 1, 2, Done
459+
var expected = new List<string> { "0", "1", "2", "Done" };
460+
bool match = result.IsSuccess && result.Output.Count == expected.Count;
461+
if (match)
462+
{
463+
for (int i = 0; i < expected.Count; i++)
464+
{
465+
if (result.Output[i] != expected[i]) { match = false; break; }
466+
}
467+
}
468+
469+
Console.WriteLine($"[Test L] {(match ? "PASS - Output correct!" : (result.IsSuccess ? "DIFF" : "FAILED"))}");
470+
}
471+
catch (Exception ex)
472+
{
473+
Console.WriteLine($"[Test L] FAILED: {ex.Message}\n {ex.StackTrace}");
474+
}
475+
}
476+
369477
/// <summary>
370478
/// Helper functions with actual execution bodies (for execution test).
371479
/// </summary>

KitX Clients/KitX Core/KitX.Core/Workflow/BlockScripting/BlockScriptExecutionGlobals.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public class BlockScriptExecutionGlobals
2424
/// </summary>
2525
public string? NextBlock { get; set; } = null;
2626

27+
/// <summary>
28+
/// Number of blocks executed so far in the current run.
29+
/// Incremented at the start of each block's switch case in the compiled assembly path.
30+
/// </summary>
31+
public int ExecutedBlockCount { get; set; }
32+
2733
/// <summary>
2834
/// Creates script globals
2935
/// </summary>
@@ -55,6 +61,20 @@ public dynamic Get(string name)
5561
return _scopeManager.ResolveVariable(name)!;
5662
}
5763

64+
/// <summary>
65+
/// Gets a variable value typed as <typeparamref name="T"/>.
66+
/// Used by the compiled assembly path to avoid <c>dynamic</c> binding overhead.
67+
/// Performs standard unboxing/conversion, no <c>Microsoft.CSharp.RuntimeBinder</c> needed.
68+
/// </summary>
69+
public T? Get<T>(string name)
70+
{
71+
if (name == "NextBlock")
72+
return (T?)(object?)NextBlock;
73+
if (_variables.TryGetValue(name, out var value))
74+
return (T?)value;
75+
return (T?)_scopeManager.ResolveVariable(name);
76+
}
77+
5878
/// <summary>
5979
/// Sets a variable value
6080
/// </summary>
@@ -140,7 +160,11 @@ public void Pause(int milliseconds)
140160
/// <summary>
141161
/// Resets run-level state (called when execution starts from Entry node)
142162
/// </summary>
143-
public void ResetRunState() => _flipCounter = 0;
163+
public void ResetRunState()
164+
{
165+
_flipCounter = 0;
166+
ExecutedBlockCount = 0;
167+
}
144168

145169
/// <summary>
146170
/// Flip — alternating control flow. Routes to outputA on odd calls, outputB on even calls.

KitX Clients/KitX Core/KitX.Core/Workflow/BlockScripting/BlockScriptExecutor.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public class BlockScriptExecutor : IBlockScriptExecutor
4545
private Dictionary<string, string>? _blockCodeCache;
4646
private HashSet<string>? _predeclaredVariables;
4747

48+
// Full-script assembly compilation
49+
private readonly ScriptAssemblyCompiler _assemblyCompiler = new();
50+
4851
/// <summary>
4952
/// Creates a new block script executor
5053
/// </summary>
@@ -91,6 +94,46 @@ public async Task<BlockScriptExecutionResult> ExecuteAsync(
9194

9295
try
9396
{
97+
// ── Fast path: Full-script assembly compilation ──
98+
// Try to compile the entire script into a .NET assembly and run it directly.
99+
// This eliminates all per-block CSharpScript overhead (ContinueWithAsync).
100+
// On failure, falls back to the existing CSharpScript execution path.
101+
try
102+
{
103+
var compiled = _assemblyCompiler.CompileScript(script);
104+
if (compiled != null)
105+
{
106+
Log.Debug("[BlockScriptExecutor] Using assembly-compiled execution path");
107+
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output, _pluginManager);
108+
_globals.ResetRunState();
109+
_scopeManager.InitializeGlobalScope(script);
110+
111+
// Import parameters
112+
if (parameters != null)
113+
{
114+
foreach (var p in parameters)
115+
_globals.Set(p.Key, p.Value);
116+
}
117+
118+
compiled.Run(_globals, cancellationToken);
119+
120+
_stopwatch.Stop();
121+
return new BlockScriptExecutionResult
122+
{
123+
IsSuccess = true,
124+
ExecutedBlockCount = _globals.ExecutedBlockCount,
125+
ExecutionTimeMs = _stopwatch.ElapsedMilliseconds,
126+
Output = _output
127+
};
128+
}
129+
}
130+
catch (OperationCanceledException) { throw; }
131+
catch (Exception ex)
132+
{
133+
Log.Warning(ex, "[BlockScriptExecutor] Assembly compilation failed, falling back to CSharpScript");
134+
}
135+
136+
// ── Fallback path: CSharpScript execution (per-block) ──
94137
// Store script reference for use in EvaluateExpressionAsync
95138
_currentScript = script;
96139

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Threading;
2+
using KitX.Core.Contract.Workflow;
3+
4+
namespace KitX.Core.Workflow.BlockScripting;
5+
6+
/// <summary>
7+
/// Represents a BlockScript compiled into a .NET assembly via Roslyn CSharpCompilation.
8+
/// The <c>Run</c> method executes the entire script in a single JIT-compiled method call,
9+
/// eliminating all per-block CSharpScript overhead.
10+
/// </summary>
11+
public interface ICompiledBlockScript
12+
{
13+
/// <summary>
14+
/// Executes the compiled script using the provided globals and cancellation token.
15+
/// The method contains a <c>while(true) + switch(NextBlock)</c> dispatcher that
16+
/// runs all blocks until the script naturally terminates or is cancelled.
17+
/// </summary>
18+
/// <param name="globals">Execution globals providing built-in functions and variable storage.</param>
19+
/// <param name="cancellationToken">Token for cooperative cancellation.</param>
20+
void Run(BlockScriptExecutionGlobals globals, CancellationToken cancellationToken);
21+
}

0 commit comments

Comments
 (0)