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