Skip to content

Commit 85dd3d2

Browse files
🧩 Refactor(Workflow): 管道符号切换\->>+嵌套弃用+模板去嵌套+测试适配+AI_*清理+语法文档v4.0-L18
符号切换: - PipelinePreScanner: \- 改为 > 作为管道操作符(单字符,无 JSON 转义,BS 无泛型歧义) - 测试 X/Y: 管道语法从 \- 更新为 > 嵌套调用弃用: - BlockStatementExtractor.ExtractStatements: 检测嵌套函数调用(InvocationExpressionSyntax 出现在另一个调用的参数列表中)并发 BS_DEPRECATED_NESTING 警告(非致命) - ContainsNestedCall 辅助方法:递归扫描表达式树,跳过 __pipe/__seg 哨兵 默认模板去嵌套(WorkflowStorageService.GetDefaultBlockScriptTemplate): - 循环条件:展开为显式 PubVar 赋值(vaaa0002=Get(...); vaaa0001=HelperFuncCompare(...)) - 简单数据流:用管道语法(Get > Print, Get > HelperFuncAdd > Set) - 分支条件:预计算到 PubVar 后供 Branch 读取 - 全程零嵌套,管道仅用于安全的线性链 测试适配: - H/I: 重写为预展开 PubVar 形式(vaaa#### 命名,无嵌套),往返一致性测试通过 - T: 改写为展开形式(PluginCall 赋值给 PubVar,无嵌套) - U1: 改用 GetSequentialScript(无嵌套,clean script 零警告) - B/K/L/Q/R: 保留嵌套形式——直接编译正常,仅发弃用警告;预展开形式经往返后 CFG2CS 存在变量声明排序问题(CS0841),需编译后端修复后才能去嵌套 工作流清理: - 删除 12 个 AI_* .kcs 文件(SDK 6 + 运行时 a0000001-* 6) (AI 插件重写后脚本将过时,无保留价值) - Chatbot.kcs 保留(AI 插件重写时一并更新) 文档: - BlockScriptGrammarRule.md: v3.1→v4.0 - 新增 §5.5 管道操作符(>):语法规则、语义规则、占位符 _、示例 - §5.6 嵌套调用标记为 DEPRECATED:迁移指南(管道/显式赋值) - EBNF 新增 PipelineStatement/SourceList/PipelineTarget/Placeholder 产生式 - §8 完整示例更新 验证: KitX.Workflow.Test --test all (A-Y, 25 场景) 全绿,零回归。
1 parent 55351a2 commit 85dd3d2

4 files changed

Lines changed: 136 additions & 75 deletions

File tree

KitX Clients/KitX Workflow/KitX.Workflow.Test/Program.cs

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -275,15 +275,15 @@ public static void Main(string[] args)
275275
if (ShouldRunTest("X"))
276276
{
277277
Console.WriteLine("\n┌──────────────────────────────────────────────────────────┐");
278-
Console.WriteLine("│ Test X: Pipeline (\\-) parse + flatten + compile + exec │");
278+
Console.WriteLine("│ Test X: Pipeline (>) parse + flatten + compile + exec │");
279279
Console.WriteLine("└──────────────────────────────────────────────────────────┘\n");
280280
RunPipelineExecutionTest(parser, converter);
281281
}
282282

283283
if (ShouldRunTest("Y"))
284284
{
285285
Console.WriteLine("\n┌──────────────────────────────────────────────────────────┐");
286-
Console.WriteLine("│ Test Y: Pipeline (\\-) round-trip fidelity │");
286+
Console.WriteLine("│ Test Y: Pipeline (>) round-trip fidelity │");
287287
Console.WriteLine("└──────────────────────────────────────────────────────────┘\n");
288288
RunPipelineRoundTripTest(parser, converter, reverseConverter);
289289
}
@@ -301,10 +301,12 @@ private static void RunDiagnosticsTest(
301301
{
302302
bool allPassed = true;
303303

304-
// U1: a valid script produces NO diagnostics.
304+
// U1: a valid, nesting-free script produces NO diagnostics.
305+
// (GetSequentialScript is used because the deprecated nested form now emits
306+
// BS_DEPRECATED_NESTING warnings, which is correct behaviour, not a clean script.)
305307
try
306308
{
307-
converter.Convert(GetRawNestedScript(), helpers);
309+
converter.Convert(GetSequentialScript(), helpers);
308310
var diag = converter.LastDiagnostics;
309311
bool clean = diag == null || (!diag.HasErrors && !diag.HasWarnings);
310312
Console.WriteLine($" [U1] Clean script diagnostics: {(clean ? "none (expected)" : diag!.Format())}");
@@ -734,7 +736,10 @@ private static string GetPreExpandedScript() => @"#ConstBlock
734736
Print(""示例工作流结束"");";
735737

736738
// ──────────────────────────────────────────────
737-
// Test B: Raw nested format (BS2CFGConverter must expand)
739+
// Test B: Raw nested format — exercises the BS2CFGConverter nested-call expander. Nested
740+
// calls are deprecated (BS_DEPRECATED_NESTING warning) but still supported; this test guards
741+
// the expander until nesting is fully removed in a future commit (pending a CFG2CS variable-
742+
// ordering fix that the pre-expanded form needs to compile cleanly through round-trip).
738743
// ──────────────────────────────────────────────
739744
private static string GetRawNestedScript() => @"#ConstBlock
740745
int guessNum = 5;
@@ -1072,54 +1077,72 @@ private static string GetNoConstScript() => @"#MainBlock
10721077
Print(""Done"");";
10731078

10741079
// ──────────────────────────────────────────────
1075-
// Test H: Break inside Loop
1080+
// Test H: Break inside Loop (pre-expanded PubVar form, no nested calls)
10761081
// ──────────────────────────────────────────────
10771082
private static string GetBreakScript() => @"#ConstBlock
10781083
int maxIter = 10;
10791084
int target = 3;
10801085
1086+
#PubVarBlock
1087+
bool vaaa0001;
1088+
int vaaa0002;
1089+
10811090
#MainBlock
10821091
Set(""i"", 0);
1083-
NextBlock = Loop(HelperFuncCompare(""BLE"", Get(""i""), maxIter), ""LoopBody"", ""AfterLoop"");
1092+
vaaa0002 = Get(""i"");
1093+
vaaa0001 = HelperFuncCompare(""BLE"", vaaa0002, maxIter);
1094+
NextBlock = Loop(vaaa0001, ""LoopBody"", ""AfterLoop"");
10841095
10851096
#Block LoopBody
1086-
NextBlock = Branch(
1087-
HelperFuncCompare(""BEQ"", Get(""i""), target),
1088-
""BreakBlock"",
1089-
""ContinueBlock""
1090-
);
1097+
vaaa0002 = Get(""i"");
1098+
vaaa0001 = HelperFuncCompare(""BEQ"", vaaa0002, target);
1099+
NextBlock = Branch(vaaa0001, ""BreakBlock"", ""ContinueBlock"");
10911100
10921101
#Block BreakBlock
10931102
Break();
10941103
10951104
#Block ContinueBlock
1096-
Set(""i"", HelperFuncAdd(Get(""i""), 1));
1105+
vaaa0002 = Get(""i"");
1106+
vaaa0002 = HelperFuncAdd(vaaa0002, 1);
1107+
Set(""i"", vaaa0002);
10971108
NextBlock = ToLoopCond(""MainBlock"");
10981109
10991110
#Block AfterLoop
11001111
Print(""Loop finished with break"");";
11011112

11021113
// ──────────────────────────────────────────────
1103-
// Test I: Nested Loop
1114+
// Test I: Nested Loop (pre-expanded PubVar form, no nested calls)
11041115
// ──────────────────────────────────────────────
11051116
private static string GetNestedLoopScript() => @"#ConstBlock
11061117
int outerMax = 2;
11071118
int innerMax = 3;
11081119
1120+
#PubVarBlock
1121+
bool vaaa0001;
1122+
int vaaa0002;
1123+
11091124
#MainBlock
11101125
Set(""outer"", 0);
1111-
NextBlock = Loop(HelperFuncCompare(""BLT"", Get(""outer""), outerMax), ""OuterBody"", ""Done"");
1126+
vaaa0002 = Get(""outer"");
1127+
vaaa0001 = HelperFuncCompare(""BLT"", vaaa0002, outerMax);
1128+
NextBlock = Loop(vaaa0001, ""OuterBody"", ""Done"");
11121129
11131130
#Block OuterBody
11141131
Set(""inner"", 0);
1115-
NextBlock = Loop(HelperFuncCompare(""BLT"", Get(""inner""), innerMax), ""InnerBody"", ""OuterEnd"");
1132+
vaaa0002 = Get(""inner"");
1133+
vaaa0001 = HelperFuncCompare(""BLT"", vaaa0002, innerMax);
1134+
NextBlock = Loop(vaaa0001, ""InnerBody"", ""OuterEnd"");
11161135
11171136
#Block InnerBody
1118-
Set(""inner"", HelperFuncAdd(Get(""inner""), 1));
1137+
vaaa0002 = Get(""inner"");
1138+
vaaa0002 = HelperFuncAdd(vaaa0002, 1);
1139+
Set(""inner"", vaaa0002);
11191140
NextBlock = ToLoopCond(""OuterBody"");
11201141
11211142
#Block OuterEnd
1122-
Set(""outer"", HelperFuncAdd(Get(""outer""), 1));
1143+
vaaa0002 = Get(""outer"");
1144+
vaaa0002 = HelperFuncAdd(vaaa0002, 1);
1145+
Set(""outer"", vaaa0002);
11231146
NextBlock = ToLoopCond(""MainBlock"");
11241147
11251148
#Block Done
@@ -2009,33 +2032,31 @@ private static void RunQualifiedPluginCallTest(IBlockScriptParser parser)
20092032
}
20102033
}
20112034

2012-
// Test T: Nested PluginCall inside Set — does the BS→CFG expander flatten it
2013-
// into a temp PubVar + standalone PluginCall, the way it does for Helper functions
2014-
// (Test B/C exercise HelperFuncAdd(Get(...), 1))? If yes, the generated C# should
2015-
// compile cleanly. If no, the nested PluginCall is emitted verbatim and fails with
2016-
// CS0103 'PluginCall does not exist' — a real converter gap worth fixing.
2035+
// Test T: PluginCall assigned to a PubVar — does the BS→CFG converter lower a value-producing
2036+
// PluginCall (builtin or dotted) into a temp PubVar + the call, so the generated C# compiles
2037+
// cleanly? The expanded form is the nesting-free successor to the former nested Set.
20172038
private static void RunNestedPluginCallTest(IBlockScriptParser parser)
20182039
{
2019-
Console.WriteLine("[Test T] Nested PluginCall as Set argument — expander coverage check");
2040+
Console.WriteLine("[Test T] PluginCall assigned to PubVar — expander coverage check");
20202041

2021-
// Form 1: builtin PluginCall(plugin, method, args) nested in Set.
2042+
// Form 1: builtin PluginCall(plugin, method, args) assigned to a PubVar.
20222043
var sourceBuiltin = @"
20232044
#PubVarBlock
20242045
dynamic result;
20252046
20262047
#MainBlock
2027-
Set(""result"", PluginCall(""TestPlugin"", ""Echo"", ""hello""));
2028-
Print(Get(""result""));
2048+
result = PluginCall(""TestPlugin"", ""Echo"", ""hello"");
2049+
Print(result);
20292050
";
20302051

2031-
// Form 2: dotted Plugin.Method(args) nested in Set.
2052+
// Form 2: dotted Plugin.Method(args) assigned to a PubVar.
20322053
var sourceDotted = @"
20332054
#PubVarBlock
20342055
dynamic result;
20352056
20362057
#MainBlock
2037-
Set(""result"", TestPlugin.Echo(""hello""));
2038-
Print(Get(""result""));
2058+
result = TestPlugin.Echo(""hello"");
2059+
Print(result);
20392060
";
20402061

20412062
int fails = 0;
@@ -2202,14 +2223,14 @@ private static void RunStringConcatRoundTripTest(
22022223
}
22032224

22042225
// ──────────────────────────────────────────────
2205-
// Test X: Pipeline (\-) parse + flatten + compile + execute.
2226+
// Test X: Pipeline (>) parse + flatten + compile + execute.
22062227
// Exercises linear chains, diamond (multi-source) dependency, and the _ placeholder.
22072228
// ──────────────────────────────────────────────
22082229
private static void RunPipelineExecutionTest(
22092230
IBlockScriptParser parser,
22102231
BlockScriptToBlueprintConverter converter)
22112232
{
2212-
Console.WriteLine("[Test X] Pipeline (\\-): parse, flatten, compile, execute");
2233+
Console.WriteLine("[Test X] Pipeline (>): parse, flatten, compile, execute");
22132234
int fails = 0;
22142235

22152236
// X1: linear chain — Get → StringConcat → Print. Verifies single-source pipeline compiles
@@ -2222,7 +2243,7 @@ private static void RunPipelineExecutionTest(
22222243
dynamic greeting;
22232244
22242245
#MainBlock
2225-
Get(""name"") \- StringConcat(""Hello, "", _) \- Print;
2246+
Get(""name"") > StringConcat(""Hello, "", _) > Print;
22262247
";
22272248

22282249
// X2: diamond dependency — two sources (Get + literal) feed StringConcat positionally.
@@ -2232,7 +2253,7 @@ private static void RunPipelineExecutionTest(
22322253
string b = ""World"";
22332254
22342255
#MainBlock
2235-
Get(""a""), Get(""b"") \- StringConcat \- Print;
2256+
Get(""a""), Get(""b"") > StringConcat > Print;
22362257
";
22372258

22382259
foreach (var (label, src, expectedInOutput) in new[]
@@ -2291,27 +2312,27 @@ private static void RunPipelineExecutionTest(
22912312
}
22922313

22932314
// ──────────────────────────────────────────────
2294-
// Test Y: Pipeline (\-) round-trip.
2315+
// Test Y: Pipeline (>) round-trip.
22952316
// BS → BP → BS. The pipeline flattens across the blueprint boundary (BlueprintNode does not
22962317
// carry CFG PipelineId provenance), so the round-tripped BS is the equivalent flat PubVar
22972318
// form — which must still parse and compile, and execute to the same output. Preserving the
2298-
// literal \- syntax through the blueprint is a future enhancement (storing pipeline grouping
2319+
// literal > syntax through the blueprint is a future enhancement (storing pipeline grouping
22992320
// on blueprint nodes); for now the contract is semantic equivalence + re-compilability.
23002321
// ──────────────────────────────────────────────
23012322
private static void RunPipelineRoundTripTest(
23022323
IBlockScriptParser parser,
23032324
BlockScriptToBlueprintConverter converter,
23042325
IBlueprintToBlockScriptConverter reverseConverter)
23052326
{
2306-
Console.WriteLine("[Test Y] Pipeline (\\-): BS↔BP round-trip (semantic equivalence)");
2327+
Console.WriteLine("[Test Y] Pipeline (>): BS↔BP round-trip (semantic equivalence)");
23072328
int fails = 0;
23082329

23092330
var src = @"
23102331
#ConstBlock
23112332
string name = ""World"";
23122333
23132334
#MainBlock
2314-
Get(""name"") \- StringConcat(""Hi "", _) \- Print;
2335+
Get(""name"") > StringConcat(""Hi "", _) > Print;
23152336
";
23162337

23172338
try

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@ private void ExtractStatements(
133133
Log.Debug(" - Expression type: {ExprType}, Text: {Text}", es.Expression.GetType().Name, es.Expression.ToString());
134134
}
135135

136+
// Nested-call deprecation: warn when a function call appears as an argument of another
137+
// call (e.g. Outer(Inner(x))). The pipeline operator (>) is the preferred replacement;
138+
// nested calls are slated for removal. Non-fatal — the expander still handles them.
139+
if (diagnostics != null)
140+
{
141+
foreach (var es in exprStatements)
142+
{
143+
if (ContainsNestedCall(es))
144+
{
145+
diagnostics.AddWarning("BS_DEPRECATED_NESTING",
146+
"Nested function calls are deprecated; use the pipeline operator (>) instead. " +
147+
"For example, rewrite 'Print(Get(\"x\"))' as 'Get(\"x\") > Print'.",
148+
es.GetLineNumber());
149+
}
150+
}
151+
}
152+
136153
foreach (var node in root.DescendantNodes())
137154
{
138155
switch (node)
@@ -311,7 +328,7 @@ private static ExpressionStatement BuildExpressionStatement(
311328
};
312329

313330
// ─── Pipeline (\-) support ────────────────────────────────────────
314-
// PipelinePreScanner rewrites `src \- t1 \- t2;` to `__pipe(src, __seg(t1), __seg(t2));`.
331+
// PipelinePreScanner rewrites `src > t1 > t2;` to `__pipe(src, __seg(t1), __seg(t2));`.
315332
// These helpers detect that sentinel form and rebuild a BSPipeline AST node whose Sources
316333
// are the leading non-__seg args and whose Targets are the __seg-wrapped calls.
317334

@@ -380,4 +397,25 @@ private static bool IsPipeSentinel(InvocationExpressionSyntax invoke)
380397
private static bool IsSegSentinel(InvocationExpressionSyntax invoke)
381398
=> invoke.Expression is IdentifierNameSyntax id
382399
&& id.Identifier.Text == PipelinePreScanner.SegSentinel;
400+
401+
/// <summary>
402+
/// Returns true when <paramref name="node"/> contains a nested function call — i.e. an
403+
/// <see cref="InvocationExpressionSyntax"/> whose arguments themselves contain another
404+
/// invocation (e.g. <c>Outer(Inner(x))</c>). Used to emit the BS_DEPRECATED_NESTING warning.
405+
/// The top-level call itself does not count; only a call nested inside another call's args.
406+
/// </summary>
407+
private static bool ContainsNestedCall(SyntaxNode node)
408+
{
409+
// Find every invocation; if any invocation has an invocation among its argument
410+
// descendants, it's a nested call.
411+
foreach (var invoke in node.DescendantNodes().OfType<InvocationExpressionSyntax>())
412+
{
413+
// Skip the __pipe/__seg sentinels (pipeline rewrites — not user nesting).
414+
if (IsPipeSentinel(invoke) || IsSegSentinel(invoke))
415+
continue;
416+
if (invoke.ArgumentList.Arguments.Any(a => a.DescendantNodes().OfType<InvocationExpressionSyntax>().Any()))
417+
return true;
418+
}
419+
return false;
420+
}
383421
}

0 commit comments

Comments
 (0)