Skip to content

Commit bd4593f

Browse files
🧩💾 Refactor, Feat(Blueprint, BlockScript): 引入 CFG 控制流图重构 BS↔BP 转换管线,实现块级预编译执行优化
BP→BS 反向转换改用 CFG 四阶段管线(CFGBuilder→ConditionDuplicator→ScriptGenerator→Serializer), 替代旧的 ReversePipeline 逐块拼接方式,保证正反转换一致性;修复 VariableNode 丢失与 HelperFunctions 未传递的往返问题。新增 BlockCompiler 将每个块预编译为单段 CSharpScript 代码,遇到流程控制语句截断 死代码并自动补全隐式 NextBlock,执行器从逐条 ContinueWithAsync 改为按块单次调用,性能提升约 2-10x(取决于单块内语句数量)。
1 parent 8f02dfc commit bd4593f

23 files changed

Lines changed: 2956 additions & 1155 deletions

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

Lines changed: 132 additions & 65 deletions
Large diffs are not rendered by default.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text;
4+
using KitX.Core.Contract.Workflow;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Serilog;
7+
8+
namespace KitX.Core.Workflow.BlockScripting;
9+
10+
/// <summary>
11+
/// Compiles a <see cref="BlockDefinition"/> into a single CSharpScript code string,
12+
/// enabling block-level precompilation instead of per-statement evaluation.
13+
///
14+
/// <para>Key design principles based on BlockScript grammar:</para>
15+
/// <list type="bullet">
16+
/// <item>Flow control statements (Branch/Loop/ToLoopCond/Break/Return) always terminate
17+
/// a block — any code after them is dead code and will be truncated with a warning.</item>
18+
/// <item><c>NextBlock = "BlockName"</c> is executable code that sets
19+
/// <see cref="BlockScriptExecutionGlobals.NextBlock"/> at runtime, not just declarative
20+
/// metadata. It is preserved in compiled output.</item>
21+
/// <item>When a block has no explicit <c>NextBlock</c> assignment and no flow control
22+
/// terminator, the compiler auto-completes <c>NextBlock = "nextBlockName";</c> from
23+
/// <see cref="BlockDefinition.NextBlockName"/>.</item>
24+
/// </list>
25+
/// </summary>
26+
internal class BlockCompiler
27+
{
28+
/// <summary>
29+
/// Compiles a <see cref="BlockDefinition"/> into a single CSharpScript code string.
30+
/// </summary>
31+
/// <param name="block">The block to compile.</param>
32+
/// <param name="predeclaredVariables">
33+
/// Variables already declared in the initialization script (e.g. ConstBlock/PubVarBlock variables).
34+
/// These are omitted from re-declaration — the block only emits assignments for them.
35+
/// </param>
36+
/// <returns>
37+
/// The compiled CSharpScript code string, or <c>null</c> if the block is empty
38+
/// and has no NextBlock to auto-complete.
39+
/// </returns>
40+
public string? CompileBlock(BlockDefinition block, HashSet<string> predeclaredVariables)
41+
{
42+
var sb = new StringBuilder();
43+
44+
foreach (var statement in block.Statements)
45+
{
46+
switch (statement)
47+
{
48+
case ExpressionStatement exprStmt:
49+
AppendExpression(sb, exprStmt, predeclaredVariables);
50+
break;
51+
52+
case VariableDeclarationStatement varStmt:
53+
AppendVariableDeclaration(sb, varStmt, predeclaredVariables);
54+
break;
55+
56+
case FlowControlStatement flowStmt:
57+
// Flow control terminates the block — append it and stop
58+
AppendFlowControl(sb, flowStmt);
59+
Log.Debug("[BlockCompiler] Block '{BlockName}': flow control ({ControlType}) " +
60+
"terminates block at statement index, truncating any dead code after it",
61+
block.Name, flowStmt.ControlType);
62+
return Finalize(sb, block);
63+
64+
default:
65+
Log.Warning("[BlockCompiler] Block '{BlockName}': unknown statement type {Type} at line {Line}, skipping",
66+
block.Name, statement.GetType().Name, statement.LineNumber);
67+
break;
68+
}
69+
}
70+
71+
// No flow control statement found — check if we need auto-completed NextBlock
72+
return Finalize(sb, block);
73+
}
74+
75+
/// <summary>
76+
/// Appends an expression statement to the code builder.
77+
/// </summary>
78+
private static void AppendExpression(StringBuilder sb, ExpressionStatement exprStmt,
79+
HashSet<string> predeclaredVariables)
80+
{
81+
var expr = exprStmt.Expression;
82+
if (string.IsNullOrWhiteSpace(expr))
83+
return;
84+
85+
// Handle NextBlock = "BlockName" — it's executable, keep it
86+
// (the parser already extracts NextBlockName from plain string assignments,
87+
// but the SourceCode preserves the original assignment expression for execution)
88+
sb.AppendLine(expr + ";");
89+
}
90+
91+
/// <summary>
92+
/// Appends a variable declaration to the code builder.
93+
/// For variables already declared in the init script, only the assignment is emitted.
94+
/// </summary>
95+
private static void AppendVariableDeclaration(StringBuilder sb, VariableDeclarationStatement varStmt,
96+
HashSet<string> predeclaredVariables)
97+
{
98+
var decl = varStmt.Declaration;
99+
100+
if (predeclaredVariables.Contains(decl.Name))
101+
{
102+
// Variable already declared in init script — just assign
103+
if (!string.IsNullOrEmpty(decl.InitialValueExpression))
104+
{
105+
sb.AppendLine($"{decl.Name} = {decl.InitialValueExpression};");
106+
}
107+
else if (decl.DefaultValue != null)
108+
{
109+
var literal = FormatLiteral(decl.Type, decl.DefaultValue);
110+
sb.AppendLine($"{decl.Name} = {literal};");
111+
}
112+
// else: uninitialized predeclared variable — nothing to emit (already declared as dynamic)
113+
}
114+
else
115+
{
116+
// Not predeclared — need a declaration
117+
if (!string.IsNullOrEmpty(decl.InitialValueExpression))
118+
{
119+
sb.AppendLine($"var {decl.Name} = {decl.InitialValueExpression};");
120+
}
121+
else if (decl.DefaultValue != null)
122+
{
123+
var literal = FormatLiteral(decl.Type, decl.DefaultValue);
124+
sb.AppendLine($"var {decl.Name} = {literal};");
125+
}
126+
else
127+
{
128+
// Uninitialized — declare with explicit type
129+
sb.AppendLine($"{decl.Type} {decl.Name};");
130+
}
131+
}
132+
}
133+
134+
/// <summary>
135+
/// Appends a flow control statement to the code builder.
136+
/// The SourceCode already contains the executable expression like
137+
/// <c>NextBlock = Loop(cond, "trueBlock", "falseBlock");</c>
138+
/// </summary>
139+
private static void AppendFlowControl(StringBuilder sb, FlowControlStatement flowStmt)
140+
{
141+
var source = flowStmt.SourceCode;
142+
if (string.IsNullOrWhiteSpace(source))
143+
{
144+
// Regenerate from fields if SourceCode is empty
145+
flowStmt.RegenerateSourceCode();
146+
source = flowStmt.SourceCode;
147+
}
148+
149+
// Ensure it ends with semicolon
150+
if (!source.TrimEnd().EndsWith(";"))
151+
source = source.TrimEnd() + ";";
152+
153+
sb.AppendLine(source);
154+
}
155+
156+
/// <summary>
157+
/// Finalizes the compiled block code: applies plugin call preprocessing
158+
/// and auto-completes NextBlock if needed.
159+
/// </summary>
160+
private string? Finalize(StringBuilder sb, BlockDefinition block)
161+
{
162+
var code = sb.ToString();
163+
if (string.IsNullOrWhiteSpace(code) && string.IsNullOrEmpty(block.NextBlockName))
164+
{
165+
// Empty block with no NextBlock — nothing to execute
166+
return null;
167+
}
168+
169+
// Auto-complete NextBlock if the block has no explicit NextBlock assignment
170+
// and no flow control terminator
171+
if (!string.IsNullOrEmpty(block.NextBlockName) && !HasNextBlockAssignment(code))
172+
{
173+
sb.AppendLine($"NextBlock = \"{block.NextBlockName}\";");
174+
code = sb.ToString();
175+
}
176+
177+
// Apply plugin call preprocessing once on the entire block
178+
code = PreProcessPluginCalls(code);
179+
180+
return code;
181+
}
182+
183+
/// <summary>
184+
/// Checks if the compiled code already contains a NextBlock assignment.
185+
/// This includes both explicit assignments (<c>NextBlock = "...";</c>) and
186+
/// flow control calls (<c>NextBlock = Loop/Branch/Flip/ToLoopCond(...)</c>).
187+
/// </summary>
188+
private static bool HasNextBlockAssignment(string code)
189+
{
190+
// Match: NextBlock = ...; (covers both string assignments and flow control)
191+
// Using simple string search since the code is already well-formed C#
192+
return code.Contains("NextBlock = ");
193+
}
194+
195+
/// <summary>
196+
/// Formats a literal value for C# source code emission.
197+
/// </summary>
198+
private static string FormatLiteral(string type, object value)
199+
{
200+
return type switch
201+
{
202+
"string" => $"\"{value}\"",
203+
"char" => $"'{value}'",
204+
_ => value?.ToString() ?? "null"
205+
};
206+
}
207+
208+
/// <summary>
209+
/// Preprocesses an entire block of code to rewrite dotted plugin calls
210+
/// into PluginCall() invocations. Uses the same Roslyn SyntaxRewriter
211+
/// as the per-statement version but operates on the whole block at once.
212+
/// </summary>
213+
private static string PreProcessPluginCalls(string code)
214+
{
215+
try
216+
{
217+
var syntaxTree = CSharpSyntaxTree.ParseText(code);
218+
var root = syntaxTree.GetCompilationUnitRoot();
219+
var rewriter = new BlockScriptExecutor.PluginCallRewriter();
220+
var rewritten = rewriter.Visit(root);
221+
222+
var result = rewritten.ToString();
223+
if (result != code)
224+
{
225+
Log.Debug("[BlockCompiler] PreProcessPluginCalls: rewrote plugin calls in block code");
226+
}
227+
return result;
228+
}
229+
catch (System.Exception ex)
230+
{
231+
Log.Warning(ex, "[BlockCompiler] PreProcessPluginCalls: exception, returning original code");
232+
return code;
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)