Skip to content

Commit efb4172

Browse files
💾 🧩 Feat, Refactor(Workflow): : Add StringConcat function and enhance KCS compile test with round-trip validation
- Implemented StringConcat builtin function for concatenating multiple strings. - Created runtime method for StringConcat to handle concatenation logic. - Enhanced KCS compile test to include optional round-trip validation for converter regressions. - Updated Program.cs to support new --roundtrip flag for KCS compile tests. - Improved error handling and diagnostics during round-trip conversion in KcsCompileTest. - Added support for preserving assigned variables in BlockStatementExtractor. - Modified various builtin functions to support output variable assignment. - Enhanced BP2CFGConverter and BS2CFGConverter to handle string concatenation and expression expansion.
1 parent 228cb82 commit efb4172

22 files changed

Lines changed: 617 additions & 45 deletions

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

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
34
using System.Text.Json;
45
using Microsoft.Extensions.DependencyInjection;
56
using KitX.Core.DI;
67
using KitX.Core.Contract.Workflow;
78
using KitX.Workflow.BlockScripting;
89
using KitX.Workflow.Contract;
910
using KitX.Workflow.Contract.Models;
11+
using KitX.Workflow.Blueprint;
12+
using KitX.Workflow.CFG;
13+
using KitX.Workflow.Conversion;
1014

1115
namespace KitX.Core.BluePrint.Test;
1216

1317
/// <summary>
1418
/// Compiles a .kcs workflow file end-to-end (parse + BS→CFG→CS→assembly) and reports
15-
/// diagnostics. Invoked via <c>--kcs &lt;path&gt;</c>. Useful for validating real workflow
16-
/// scripts without running the Dashboard.
19+
/// diagnostics. Invoked via <c>--kcs &lt;path&gt;</c>. With the additional
20+
/// <c>--roundtrip</c> flag, also runs an optional BS→BP→BS→Compile round-trip to
21+
/// surface converter regressions that a one-way compile would hide. Useful for
22+
/// validating real workflow scripts without running the Dashboard.
1723
/// </summary>
1824
public partial class Program
1925
{
20-
public static void RunKcsCompileTest(string kcsPath)
26+
public static void RunKcsCompileTest(string kcsPath, bool withRoundTrip = false)
2127
{
22-
Console.WriteLine($"=== KCS Compile Test: {kcsPath} ===");
28+
Console.WriteLine($"=== KCS Compile Test: {kcsPath} ===" +
29+
(withRoundTrip ? " (with round-trip)" : ""));
2330

2431
if (!File.Exists(kcsPath))
2532
{
@@ -80,7 +87,90 @@ public static void RunKcsCompileTest(string kcsPath)
8087
foreach (var e in errors)
8188
Console.WriteLine(" ERR: " + e);
8289

83-
if (result != null && errors.Count == 0)
90+
bool compileOk = result != null && errors.Count == 0;
91+
92+
// Optional round-trip phase — surfaces converter bugs that a single compile hides.
93+
// Mirrors the BS→BP→BS→Compile pattern from Test V but driven by a real .kcs file.
94+
bool roundTripOk = true;
95+
if (withRoundTrip)
96+
{
97+
Console.WriteLine();
98+
Console.WriteLine("--- Round-trip phase (BS → BP → BS → Compile) ---");
99+
try
100+
{
101+
var nodeRegistry = sp.GetRequiredService<INodeRegistry>();
102+
var layoutService = sp.GetRequiredService<ILayoutService>();
103+
var reverseConverter = sp.GetRequiredService<IBlueprintToBlockScriptConverter>();
104+
var funcRegistry = sp.GetRequiredService<BuiltinFunctionRegistry>();
105+
var converter = new BlockScriptToBlueprintConverter(parser, nodeRegistry, layoutService, funcRegistry);
106+
107+
var helpers = kcs.HelperFunctions ?? new System.Collections.Generic.List<HelperFunction>();
108+
109+
// BS → BP
110+
var bp = converter.Convert(sourceCode, helpers);
111+
if (bp == null)
112+
{
113+
Console.WriteLine(" FAIL: BS→BP conversion returned null");
114+
if (converter.LastDiagnostics != null)
115+
Console.WriteLine($" Diagnostics: {converter.LastDiagnostics.Format()}");
116+
roundTripOk = false;
117+
}
118+
else
119+
{
120+
Console.WriteLine(" BS→BP: OK");
121+
122+
// BP → BS
123+
var bsResult = reverseConverter.Convert(bp);
124+
if (string.IsNullOrEmpty(bsResult))
125+
{
126+
Console.WriteLine(" FAIL: BP→BS conversion returned empty");
127+
roundTripOk = false;
128+
}
129+
else
130+
{
131+
Console.WriteLine(" BP→BS: OK");
132+
Console.WriteLine(" --- Round-tripped BlockScript ---");
133+
Console.WriteLine(bsResult);
134+
Console.WriteLine(" --- end ---");
135+
136+
// Parse + compile the round-tripped source
137+
var pr2 = parser.Parse(bsResult);
138+
if (!pr2.IsSuccess || pr2.Script == null)
139+
{
140+
Console.WriteLine($" FAIL: round-tripped BS parse error: {pr2.ErrorMessage}");
141+
roundTripOk = false;
142+
}
143+
else
144+
{
145+
pr2.Script.HelperFunctions = helpers;
146+
var compiled2 = new CSCompiler().CompileScript(pr2.Script, workflowId: kcs.Id, out var errors2);
147+
if (compiled2 == null)
148+
{
149+
Console.WriteLine($" FAIL: round-tripped BS compile null, {errors2.Count} error(s)");
150+
foreach (var e in errors2.Take(10)) Console.WriteLine($" {e}");
151+
roundTripOk = false;
152+
}
153+
else
154+
{
155+
Console.WriteLine(" Round-tripped BS compile: OK");
156+
}
157+
}
158+
}
159+
}
160+
}
161+
catch (Exception ex)
162+
{
163+
Console.WriteLine($" FAIL: round-trip exception: {ex.Message}");
164+
roundTripOk = false;
165+
}
166+
}
167+
168+
// Final verdict combines the one-way compile and (if requested) the round-trip.
169+
bool overall = compileOk && roundTripOk;
170+
Console.WriteLine();
171+
if (withRoundTrip)
172+
Console.WriteLine($"RESULT: {(overall ? "PASS" : "FAIL")} (compile={compileOk}, roundtrip={roundTripOk})");
173+
else if (compileOk)
84174
Console.WriteLine("\nRESULT: PASS");
85175
else
86176
Console.WriteLine("\nRESULT: FAIL");

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

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ public static void Main(string[] args)
2626

2727
// --kcs <path>: compile a .kcs workflow file and report diagnostics.
2828
// Useful for validating real workflow scripts without running the Dashboard.
29+
// --roundtrip (alongside --kcs): additionally run a BS→BP→BS→Compile round-trip
30+
// to surface converter regressions a one-way compile would hide.
2931
var kcsIdx = Array.IndexOf(args, "--kcs");
3032
if (kcsIdx >= 0 && kcsIdx + 1 < args.Length)
3133
{
32-
RunKcsCompileTest(args[kcsIdx + 1]);
34+
bool withRoundTrip = Array.IndexOf(args, "--roundtrip") >= 0
35+
|| Array.IndexOf(args, "-r") >= 0;
36+
RunKcsCompileTest(args[kcsIdx + 1], withRoundTrip);
3337
return;
3438
}
3539

@@ -248,6 +252,14 @@ public static void Main(string[] args)
248252
Console.WriteLine("└──────────────────────────────────────────────┘\n");
249253
RunDiagnosticsTest(converter, helpers, "Test U");
250254
}
255+
256+
if (ShouldRunTest("V"))
257+
{
258+
Console.WriteLine("\n┌──────────────────────────────────────────────┐");
259+
Console.WriteLine("│ Test V: StringConcat Round-Trip │");
260+
Console.WriteLine("└──────────────────────────────────────────────┘\n");
261+
RunStringConcatRoundTripTest(parser, converter, reverseConverter, new List<HelperFunction>());
262+
}
251263
}
252264

253265
// ──────────────────────────────────────────────
@@ -1977,4 +1989,128 @@ private static void RunNestedPluginCallTest(IBlockScriptParser parser)
19771989

19781990
Console.WriteLine($"[Test T] {(fails == 0 ? "PASS" : "FAIL - see above")}");
19791991
}
1992+
1993+
// Test V: StringConcat round-trip — verifies that:
1994+
// 1. BS source with "+" string concatenation compiles to assembly.
1995+
// 2. BS→BP→BS round-trip preserves the semantics (StringConcat nodes appear).
1996+
// 3. The round-tripped BS also compiles.
1997+
// 4. Explicit StringConcat(a, b, c) in BS also compiles and round-trips.
1998+
private static void RunStringConcatRoundTripTest(
1999+
IBlockScriptParser parser,
2000+
BlockScriptToBlueprintConverter converter,
2001+
IBlueprintToBlockScriptConverter reverseConverter,
2002+
List<HelperFunction> helpers)
2003+
{
2004+
Console.WriteLine("[Test V] StringConcat: + expansion, compile, BS↔BP round-trip");
2005+
2006+
int fails = 0;
2007+
2008+
// --- Sub-test 1: "+" concatenation compiles ---
2009+
var srcPlus = @"
2010+
#ConstBlock
2011+
string a = ""Hello"";
2012+
string b = ""World"";
2013+
2014+
#PubVarBlock
2015+
dynamic v;
2016+
2017+
#MainBlock
2018+
v = a + "", "" + b + ""!"";
2019+
Print(v);
2020+
";
2021+
2022+
// --- Sub-test 2: explicit StringConcat compiles ---
2023+
var srcExplicit = @"
2024+
#ConstBlock
2025+
string a = ""Hello"";
2026+
string b = ""World"";
2027+
2028+
#PubVarBlock
2029+
dynamic v;
2030+
2031+
#MainBlock
2032+
v = StringConcat(a, "", "", b, ""!"");
2033+
Print(v);
2034+
";
2035+
2036+
foreach (var (label, src) in new[] { ("+ syntax", srcPlus), ("StringConcat explicit", srcExplicit) })
2037+
{
2038+
try
2039+
{
2040+
// Phase 1: parse + compile
2041+
var pr = parser.Parse(src);
2042+
if (!pr.IsSuccess || pr.Script == null)
2043+
{
2044+
Console.WriteLine($" [{label}] FAIL: parse error: {pr.ErrorMessage}");
2045+
fails++;
2046+
continue;
2047+
}
2048+
pr.Script.HelperFunctions = helpers;
2049+
2050+
var compiler = new CSCompiler();
2051+
var compiled = compiler.CompileScript(pr.Script, workflowId: null, out var errors);
2052+
if (compiled == null)
2053+
{
2054+
Console.WriteLine($" [{label}] FAIL: compile null, {errors.Count} error(s)");
2055+
foreach (var e in errors.Take(3)) Console.WriteLine($" {e}");
2056+
fails++;
2057+
continue;
2058+
}
2059+
Console.WriteLine($" [{label}] Compile: OK");
2060+
2061+
// Phase 2: BS→BP→BS round-trip
2062+
var bpResult = converter.Convert(src, helpers);
2063+
if (bpResult == null)
2064+
{
2065+
Console.WriteLine($" [{label}] FAIL: BS→BP conversion returned null");
2066+
fails++;
2067+
continue;
2068+
}
2069+
2070+
var bsResult = reverseConverter.Convert(bpResult);
2071+
if (string.IsNullOrEmpty(bsResult))
2072+
{
2073+
Console.WriteLine($" [{label}] FAIL: BP→BS conversion returned empty");
2074+
fails++;
2075+
continue;
2076+
}
2077+
2078+
// Phase 3: round-tripped BS should contain StringConcat
2079+
if (!bsResult.Contains("StringConcat"))
2080+
{
2081+
Console.WriteLine($" [{label}] FAIL: round-tripped BS does not contain 'StringConcat'");
2082+
Console.WriteLine($" Round-tripped source:\n{bsResult}");
2083+
fails++;
2084+
continue;
2085+
}
2086+
2087+
// Phase 4: round-tripped BS compiles
2088+
var pr2 = parser.Parse(bsResult);
2089+
if (!pr2.IsSuccess || pr2.Script == null)
2090+
{
2091+
Console.WriteLine($" [{label}] FAIL: round-tripped BS parse error: {pr2.ErrorMessage}");
2092+
fails++;
2093+
continue;
2094+
}
2095+
pr2.Script.HelperFunctions = helpers;
2096+
var compiled2 = new CSCompiler().CompileScript(pr2.Script, workflowId: null, out var errors2);
2097+
if (compiled2 == null)
2098+
{
2099+
Console.WriteLine($" [{label}] FAIL: round-tripped BS compile null, {errors2.Count} error(s)");
2100+
foreach (var e in errors2.Take(3)) Console.WriteLine($" {e}");
2101+
fails++;
2102+
continue;
2103+
}
2104+
2105+
Console.WriteLine($" [{label}] Round-trip: OK (StringConcat present, re-compiles)");
2106+
}
2107+
catch (Exception ex)
2108+
{
2109+
Console.WriteLine($" [{label}] FAIL: {ex.Message}");
2110+
fails++;
2111+
}
2112+
}
2113+
2114+
Console.WriteLine($"[Test V] {(fails == 0 ? "PASS" : "FAIL - see above")}");
2115+
}
19802116
}

KitX Clients/KitX Workflow/KitX.Workflow/BlockScripting/BlockStatementExtractor.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,15 @@ private void ExtractStatements(
264264
else
265265
{
266266
Log.Debug("[BlockStatementExtractor] assignment.Right is NOT InvocationExpressionSyntax, type = {Type}", assignment.Right.GetType().Name);
267+
// Preserve AssignedVariable so BS2CFGConverter can handle
268+
// non-invocation RHS (e.g. v = a + b + c where RHS is BinaryExpression).
267269
block.Statements.Add(new ExpressionStatement
268270
{
269271
LineNumber = exprStmt.GetLineNumber(),
270272
SourceCode = exprText,
271-
Expression = exprText
273+
Expression = exprText,
274+
AssignedVariable = assignment.Left.ToString(),
275+
ParsedInvocation = assignment.Right as InvocationExpressionSyntax
272276
});
273277
}
274278
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/CreateWorkflowFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ public BlueprintNode ConfigureNode(BlueprintNode node, CFGStatement stmt)
4141
{
4242
var name = helper.GetInputValue(node, "Name");
4343
var source = helper.GetInputValue(node, "Source");
44+
var expr = $"{FunctionName}({name}, {source})";
45+
var pubVar = helper.GetOutputPubVar(node, "Return");
4446
return new ExpressionStatement
4547
{
46-
Expression = $"{FunctionName}({name}, {source})",
47-
SourceCode = $"{FunctionName}({name}, {source});",
48+
Expression = expr,
49+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
4850
LineNumber = 1
4951
};
5052
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/GetPluginInfoByNameFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ public BlueprintNode ConfigureNode(BlueprintNode node, CFGStatement stmt)
4343
var pluginName = node is BuiltinFunctionNode bfn
4444
? bfn.Properties.GetValueOrDefault("PluginName", "") ?? ""
4545
: "";
46+
var expr = $"{FunctionName}(\"{pluginName}\")";
47+
var pubVar = helper.GetOutputPubVar(node, "Return");
4648
return new ExpressionStatement
4749
{
48-
Expression = $"{FunctionName}(\"{pluginName}\")",
49-
SourceCode = $"{FunctionName}(\"{pluginName}\");",
50+
Expression = expr,
51+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
5052
LineNumber = 1
5153
};
5254
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/InstallPluginFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ public class InstallPluginFunction : IBuiltinFunctionDefinition
3535
public BlockStatement? ToStatement(BlueprintNode node, INodeExportHelper helper)
3636
{
3737
var value = helper.GetInputValue(node, "KxpPath");
38+
var expr = $"{FunctionName}({value})";
39+
var pubVar = helper.GetOutputPubVar(node, "Return");
3840
return new ExpressionStatement
3941
{
40-
Expression = $"{FunctionName}({value})",
41-
SourceCode = $"{FunctionName}({value});",
42+
Expression = expr,
43+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
4244
LineNumber = 1
4345
};
4446
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/JsonGetFieldFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ public BlueprintNode ConfigureNode(BlueprintNode node, CFGStatement stmt)
4444
var fieldPath = node is BuiltinFunctionNode bfn
4545
? bfn.Properties.GetValueOrDefault("FieldPath", "") ?? ""
4646
: "";
47+
var expr = $"{FunctionName}({jsonValue}, \"{fieldPath}\")";
48+
var pubVar = helper.GetOutputPubVar(node, "Return");
4749
return new ExpressionStatement
4850
{
49-
Expression = $"{FunctionName}({jsonValue}, \"{fieldPath}\")",
50-
SourceCode = $"{FunctionName}({jsonValue}, \"{fieldPath}\");",
51+
Expression = expr,
52+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
5153
LineNumber = 1
5254
};
5355
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/ListPluginNamesFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ public class ListPluginNamesFunction : IBuiltinFunctionDefinition
3434

3535
public BlockStatement? ToStatement(BlueprintNode node, INodeExportHelper helper)
3636
{
37+
var expr = $"{FunctionName}()";
38+
var pubVar = helper.GetOutputPubVar(node, "Return");
3739
return new ExpressionStatement
3840
{
39-
Expression = $"{FunctionName}()",
40-
SourceCode = $"{FunctionName}();",
41+
Expression = expr,
42+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
4143
LineNumber = 1
4244
};
4345
}

KitX Clients/KitX Workflow/KitX.Workflow/BuiltinFunctions/ListWorkflowsFunction.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ public class ListWorkflowsFunction : IBuiltinFunctionDefinition
3333

3434
public BlockStatement? ToStatement(BlueprintNode node, INodeExportHelper helper)
3535
{
36+
var expr = $"{FunctionName}()";
37+
var pubVar = helper.GetOutputPubVar(node, "Return");
3638
return new ExpressionStatement
3739
{
38-
Expression = $"{FunctionName}()",
39-
SourceCode = $"{FunctionName}();",
40+
Expression = expr,
41+
SourceCode = string.IsNullOrEmpty(pubVar) ? $"{expr};" : $"{pubVar} = {expr};",
4042
LineNumber = 1
4143
};
4244
}

0 commit comments

Comments
 (0)