Skip to content

Commit 0d1fc02

Browse files
💾🧩 Feat, Refactor(CFG, Debug, Blueprint): Enhance BlockScript compilation and conversion diagnostics
- Updated ScriptCompilationBackend to return human-readable diagnostics on compilation failure. - Refactored BranchFunction, FlipFunction, LoopFunction, and ToLoopCondFunction to utilize new string literal extraction method. - Introduced ConversionDiagnostics model to collect and format user-facing diagnostics during parsing and conversion. - Enhanced BS2CFGConverter and BlockScriptToBlueprintConverter to surface diagnostics from parsing and conversion processes. - Added diagnostics tracking in PipelineContext and ConversionContext for better error reporting. - Updated solution files to include new AI and FloatAssist plugins.
1 parent a1f5e9e commit 0d1fc02

24 files changed

Lines changed: 633 additions & 67 deletions

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,77 @@ public static void Main(string[] args)
223223
Console.WriteLine("└──────────────────────────────────────────┘\n");
224224
RunQualifiedPluginCallTest(parser);
225225
}
226+
227+
if (ShouldRunTest("T"))
228+
{
229+
Console.WriteLine("\n┌──────────────────────────────────────────────┐");
230+
Console.WriteLine("│ Test T: Nested PluginCall in Set │");
231+
Console.WriteLine("└──────────────────────────────────────────────┘\n");
232+
RunNestedPluginCallTest(parser);
233+
}
234+
235+
if (ShouldRunTest("U"))
236+
{
237+
Console.WriteLine("\n┌──────────────────────────────────────────────┐");
238+
Console.WriteLine("│ Test U: Conversion Diagnostics │");
239+
Console.WriteLine("└──────────────────────────────────────────────┘\n");
240+
RunDiagnosticsTest(converter, helpers, "Test U");
241+
}
242+
}
243+
244+
// ──────────────────────────────────────────────
245+
// Test U: Conversion diagnostics surface user errors instead of silently dropping them.
246+
// Guards the P0 diagnostics channel: a clean script yields no diagnostics, and dead code
247+
// after a flow-control statement (BlockScript §6) yields a BS_DEAD_CODE warning.
248+
// ──────────────────────────────────────────────
249+
private static void RunDiagnosticsTest(
250+
BlockScriptToBlueprintConverter converter,
251+
List<HelperFunction> helpers,
252+
string label)
253+
{
254+
bool allPassed = true;
255+
256+
// U1: a valid script produces NO diagnostics.
257+
try
258+
{
259+
converter.Convert(GetRawNestedScript(), helpers);
260+
var diag = converter.LastDiagnostics;
261+
bool clean = diag == null || (!diag.HasErrors && !diag.HasWarnings);
262+
Console.WriteLine($" [U1] Clean script diagnostics: {(clean ? "none (expected)" : diag!.Format())}");
263+
if (!clean) allPassed = false;
264+
}
265+
catch (Exception ex)
266+
{
267+
Console.WriteLine($" [U1] FAILED: {ex.Message}");
268+
allPassed = false;
269+
}
270+
271+
// U2: dead code after a flow-control statement → BS_DEAD_CODE warning (non-fatal).
272+
try
273+
{
274+
var src = @"#MainBlock
275+
Print(""before"");
276+
NextBlock = Branch(true, ""T"", ""F"");
277+
Print(""dead"");
278+
279+
#Block T
280+
Print(""t"");
281+
282+
#Block F
283+
Print(""f"");";
284+
converter.Convert(src, helpers);
285+
var diag = converter.LastDiagnostics;
286+
bool hasDeadCode = diag != null && diag.Items.Any(d => d.Code == "BS_DEAD_CODE");
287+
Console.WriteLine($" [U2] Dead-code warning emitted: {hasDeadCode}");
288+
if (!hasDeadCode) allPassed = false;
289+
}
290+
catch (Exception ex)
291+
{
292+
Console.WriteLine($" [U2] FAILED: {ex.Message}");
293+
allPassed = false;
294+
}
295+
296+
Console.WriteLine($"\n[{label}] {(allPassed ? "PASS - diagnostics channel works" : "FAIL - see above")}");
226297
}
227298

228299
private static void ParseArgs(string[] args)
@@ -1829,4 +1900,72 @@ private static void RunQualifiedPluginCallTest(IBlockScriptParser parser)
18291900
Console.WriteLine($"[Test S] FAILED: {ex.Message}\n {ex.StackTrace}");
18301901
}
18311902
}
1903+
1904+
// Test T: Nested PluginCall inside Set — does the BS→CFG expander flatten it
1905+
// into a temp PubVar + standalone PluginCall, the way it does for Helper functions
1906+
// (Test B/C exercise HelperFuncAdd(Get(...), 1))? If yes, the generated C# should
1907+
// compile cleanly. If no, the nested PluginCall is emitted verbatim and fails with
1908+
// CS0103 'PluginCall does not exist' — a real converter gap worth fixing.
1909+
private static void RunNestedPluginCallTest(IBlockScriptParser parser)
1910+
{
1911+
Console.WriteLine("[Test T] Nested PluginCall as Set argument — expander coverage check");
1912+
1913+
// Form 1: builtin PluginCall(plugin, method, args) nested in Set.
1914+
var sourceBuiltin = @"
1915+
#PubVarBlock
1916+
dynamic result;
1917+
1918+
#MainBlock
1919+
Set(""result"", PluginCall(""TestPlugin"", ""Echo"", ""hello""));
1920+
Print(Get(""result""));
1921+
";
1922+
1923+
// Form 2: dotted Plugin.Method(args) nested in Set.
1924+
var sourceDotted = @"
1925+
#PubVarBlock
1926+
dynamic result;
1927+
1928+
#MainBlock
1929+
Set(""result"", TestPlugin.Echo(""hello""));
1930+
Print(Get(""result""));
1931+
";
1932+
1933+
int fails = 0;
1934+
1935+
foreach (var (label, src) in new[] { ("builtin", sourceBuiltin), ("dotted", sourceDotted) })
1936+
{
1937+
try
1938+
{
1939+
var pr = parser.Parse(src);
1940+
if (!pr.IsSuccess || pr.Script == null)
1941+
{
1942+
Console.WriteLine($" [{label}] FAIL: parse error: {pr.ErrorMessage}");
1943+
fails++;
1944+
continue;
1945+
}
1946+
pr.Script.HelperFunctions = new List<HelperFunction>();
1947+
1948+
var compiler = new CSCompiler();
1949+
var compiled = compiler.CompileScript(pr.Script, workflowId: null, out var errors);
1950+
if (compiled != null)
1951+
{
1952+
Console.WriteLine($" [{label}] PASS: nested PluginCall expanded + compiled");
1953+
}
1954+
else
1955+
{
1956+
Console.WriteLine($" [{label}] FAIL: compiled null, {errors.Count} error(s):");
1957+
foreach (var e in errors.Take(3))
1958+
Console.WriteLine($" {e}");
1959+
fails++;
1960+
}
1961+
}
1962+
catch (Exception ex)
1963+
{
1964+
Console.WriteLine($" [{label}] FAIL: {ex.Message}");
1965+
fails++;
1966+
}
1967+
}
1968+
1969+
Console.WriteLine($"[Test T] {(fails == 0 ? "PASS" : "FAIL - see above")}");
1970+
}
18321971
}

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,14 @@ public async Task<BlockScriptExecutionResult> ExecuteAsync(
111111
CFG2CSGenerator.IsDebugMode = _debugger != null;
112112

113113
// Full-script assembly compilation
114-
var compiled = _assemblyCompiler.CompileScript(script, _workflowId);
114+
var compiled = _assemblyCompiler.CompileScript(script, _workflowId, out var compileErrors);
115115
if (compiled == null)
116116
{
117117
_stopwatch.Stop();
118118
return new BlockScriptExecutionResult
119119
{
120120
IsSuccess = false,
121-
ErrorMessage = "Script compilation failed",
121+
ErrorMessage = FormatCompileErrors(compileErrors),
122122
ExecutionTimeMs = _stopwatch.ElapsedMilliseconds,
123123
Output = _output
124124
};
@@ -184,14 +184,14 @@ internal async Task<BlockScriptExecutionResult> ExecuteFromCFGAsync(
184184
{
185185
CFG2CSGenerator.IsDebugMode = _debugger != null;
186186

187-
var compiled = _assemblyCompiler.CompileFromCFG(cfg, script, _workflowId);
187+
var compiled = _assemblyCompiler.CompileFromCFG(cfg, script, _workflowId, out var compileErrors);
188188
if (compiled == null)
189189
{
190190
_stopwatch.Stop();
191191
return new BlockScriptExecutionResult
192192
{
193193
IsSuccess = false,
194-
ErrorMessage = "Script compilation failed",
194+
ErrorMessage = FormatCompileErrors(compileErrors),
195195
ExecutionTimeMs = _stopwatch.ElapsedMilliseconds,
196196
Output = _output
197197
};
@@ -316,4 +316,37 @@ public BlockScriptValidationResult Validate(BlockScript script)
316316

317317
return result;
318318
}
319+
320+
/// <summary>
321+
/// Formats Roslyn compilation diagnostics into a single multi-line string suitable
322+
/// for display in the workflow editor's output panel. Capped at 10 entries so a flood
323+
/// of cascading errors (e.g. one missing type producing dozens) stays readable.
324+
/// </summary>
325+
private static string FormatCompileErrors(IReadOnlyList<string>? errors)
326+
{
327+
if (errors is null || errors.Count == 0)
328+
return "Script compilation failed (no diagnostic details were captured).";
329+
330+
const int maxShown = 10;
331+
var sb = new System.Text.StringBuilder();
332+
sb.Append("Script compilation failed with ");
333+
sb.Append(errors.Count);
334+
sb.Append(" error");
335+
if (errors.Count != 1) sb.Append('s');
336+
sb.Append(':');
337+
sb.AppendLine();
338+
var shown = Math.Min(maxShown, errors.Count);
339+
for (var i = 0; i < shown; i++)
340+
{
341+
sb.Append(" • ");
342+
sb.AppendLine(errors[i]);
343+
}
344+
if (errors.Count > maxShown)
345+
{
346+
sb.Append(" • …and ");
347+
sb.Append(errors.Count - maxShown);
348+
sb.AppendLine(" more (see Log/ for the full list).");
349+
}
350+
return sb.ToString().TrimEnd();
351+
}
319352
}

KitX Clients/KitX Workflow/KitX.Workflow/BlockScripting/BlockScriptParser.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public BlockScriptParseResult Parse(string sourceCode)
3737
};
3838
}
3939

40+
// Declared outside the try so the catch block can still attach whatever diagnostics
41+
// were collected before the exception.
42+
var diagnostics = new ConversionDiagnostics();
43+
4044
try
4145
{
4246
Log.Debug("[BlockScriptParser] Parse called with {LineCount} lines of code", sourceCode.Split('\n').Length);
@@ -75,12 +79,13 @@ public BlockScriptParseResult Parse(string sourceCode)
7579
{
7680
IsSuccess = false,
7781
ErrorMessage = validationResult.ErrorMessage,
78-
ErrorLine = recognized.StartLine + validationResult.ErrorLine
82+
ErrorLine = recognized.StartLine + validationResult.ErrorLine,
83+
Diagnostics = diagnostics
7984
};
8085
}
8186

8287
// Phase 3: Extract statements and create BlockDefinition
83-
var blockDef = _extractor.CreateBlockDefinition(recognized, validationResult);
88+
var blockDef = _extractor.CreateBlockDefinition(recognized, validationResult, diagnostics);
8489

8590
// Add to appropriate slot in script
8691
switch (recognized.BlockType)
@@ -119,7 +124,8 @@ public BlockScriptParseResult Parse(string sourceCode)
119124
return new BlockScriptParseResult
120125
{
121126
IsSuccess = true,
122-
Script = script
127+
Script = script,
128+
Diagnostics = diagnostics
123129
};
124130
}
125131
catch (Exception ex)
@@ -129,7 +135,8 @@ public BlockScriptParseResult Parse(string sourceCode)
129135
{
130136
IsSuccess = false,
131137
ErrorMessage = $"Parse error: {ex.Message}",
132-
ErrorLine = 0
138+
ErrorLine = 0,
139+
Diagnostics = diagnostics
133140
};
134141
}
135142
}

0 commit comments

Comments
 (0)