Skip to main content

evorule_cli/
executor.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! 同步反应器循环 —— CLI 执行核心
5//!
6//! # P0 修复
7//! 1. **FIFO 队列**:用 `VecDeque::pop_front()`,不能用 `Vec::pop()`。
8//!    tier0 `exec_push` 用 `new_queue.append(queue)` 把新指令前置(插队语义),
9//!    必须从前端取才能保证 push 的指令先执行。
10//! 2. **max_steps 上界**:先检后 pop(对齐 evorule-reactor reactor.rs BUG-3 修复)。
11//!    默认 10000,可 `--max-steps` 覆盖。超限发 `Fact::Error` + break。
12//! 3. **I/O 两阶段架构**:`pending_io: HashMap<FactId, JsonValue>` 缓存 orig 指令。
13//!    0.2.0 无 handler 时发 `Fact::Error` 退出,但架构正确——0.3.0 加 handler 时
14//!    只需在 IoRequest 分支注入 IoResponse + push_front(orig) 即可,循环主体不变。
15//!
16//! # 不引入 tokio runtime
17//! `execute_transition` 是同步纯函数,整个循环无 await。tokio 仅作为 evorule-reactor
18//! 编译依赖存在,CLI 不创建 runtime。
19//!
20//! # Fact 序列
21//! 执行产生 `Vec<Fact>`:
22//! 1. `Command`(初始指令)
23//! 2. 若干 `StateTransition`(每步执行)
24//! 3. 可选 `IoRequest` + `Error`(I/O 请求但无 handler)
25//! 4. 可选 `Error`(TCB 错误或 max_steps 超限)
26//! 5. `Stable`(最终快照,始终发射)
27
28use std::collections::{HashMap, VecDeque};
29
30use evorule_reactor::{Fact, FactId, FactIdGenerator, IoType};
31use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
32
33use crate::error::CliError;
34
35/// 默认 max_steps(与 evorule-reactor 默认 max_rounds 一致量级)
36pub const DEFAULT_MAX_STEPS: usize = 10000;
37
38/// 执行规则,产生 Fact 序列
39///
40/// # 参数
41/// - `core_eval`:transform 规则列表(由 `io_util::load_rules` 加载)
42/// - `initial_payload`:初始 payload
43/// - `initial_instruction`:初始指令(通常是 `{"type":"noop"}` 触发 transform 链)
44/// - `max_steps`:最大执行步数上界(先检后 pop)
45///
46/// # 返回
47/// `Vec<Fact>`:包含 Command、若干 StateTransition、可选 Error、结尾 Stable
48///
49/// # 不变量
50/// - FIFO 队列:`VecDeque::pop_front`,不能用 `Vec::pop`
51/// - max_steps 先检后 pop:超限发 Error + break
52/// - I/O 两阶段:IoRequest 时缓存 orig 指令到 pending_io,0.2.0 无 handler 发 Error
53// 108 行: CLI 主循环 + I/O 两阶段 + max_steps 门禁 + 错误处理必须单函数原子语义
54// 拆函数会让 4 阶段 (loop / dispatch / pending_io / break) 状态传递出错
55#[allow(clippy::too_many_lines)]
56pub fn execute(
57    core_eval: &[JsonValue],
58    initial_payload: JsonValue,
59    initial_instruction: JsonValue,
60    max_steps: usize,
61) -> Result<Vec<Fact>, CliError> {
62    let mut facts: Vec<Fact> = Vec::new();
63    let mut id_gen = FactIdGenerator::new();
64    let mut queue: VecDeque<JsonValue> = VecDeque::new();
65    queue.push_back(initial_instruction);
66    let mut payload = initial_payload;
67    let mut steps = 0;
68    // 0.2.0 无 I/O handler,pending_io 仅缓存不消费(为 0.3.0 铺路)
69    let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
70
71    // 发射初始 Command fact
72    let cmd_id = id_gen.next_id();
73    let mut current_cause: FactId = cmd_id;
74    let cmd_instruction = queue.front().cloned().unwrap_or(JsonValue::Null);
75    facts.push(Fact::Command {
76        id: cmd_id,
77        instruction: cmd_instruction,
78    });
79
80    while !queue.is_empty() {
81        // max_steps 先检后 pop(对齐 evorule-reactor BUG-3 修复)
82        if steps >= max_steps {
83            let err_id = id_gen.next_id();
84            facts.push(Fact::Error {
85                id: err_id,
86                message: format!("max_steps exceeded: {}", steps),
87            });
88            tracing::warn!(steps, max_steps, "max_steps exceeded");
89            break;
90        }
91
92        // FIFO:pop_front(不能用 Vec::pop,那是 LIFO)
93        let instruction = match queue.pop_front() {
94            Some(i) => i,
95            None => break, // 逻辑不可达(while 条件已检查),防御性
96        };
97        steps += 1;
98
99        // 传当前 queue 快照给 execute_transition(供 core_eval 规则引用 __exec__.queue)
100        let queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
101        let result = execute_transition(core_eval, &instruction, &payload, &queue_snapshot);
102
103        match result {
104            Ok(TransitionResult::State {
105                new_payload,
106                new_queue,
107            }) => {
108                payload = new_payload;
109                queue = new_queue.into_iter().collect();
110                let id = id_gen.next_id();
111                let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
112                facts.push(Fact::StateTransition {
113                    id,
114                    cause: current_cause,
115                    new_payload: payload.clone(),
116                    new_queue: new_queue_snapshot,
117                });
118                current_cause = id;
119            }
120            Ok(TransitionResult::IoRequired { io_type, params }) => {
121                // v0.2.0:io_type 透传不校验(parse 已 deprecated,无条件接受)
122                let io_type = IoType::new(&io_type);
123                let req_id = id_gen.next_id();
124                // 缓存 orig 指令(0.3.0 加 handler 时用于 push_front 重执行)
125                pending_io.insert(req_id, instruction.clone());
126                facts.push(Fact::IoRequest {
127                    id: req_id,
128                    cause: current_cause,
129                    io_type: io_type.clone(),
130                    params,
131                });
132                // 0.2.0 无 I/O handler,发 Error 退出
133                let err_id = id_gen.next_id();
134                facts.push(Fact::Error {
135                    id: err_id,
136                    message: format!("no I/O handler for io_type={}", io_type.as_str()),
137                });
138                tracing::warn!(
139                    io_type = %io_type.as_str(),
140                    request_id = ?req_id,
141                    "I/O required but no handler available, stopping"
142                );
143                break;
144            }
145            Ok(TransitionResult::Ignored {
146                instruction_type,
147                reason,
148            }) => {
149                // v0.3.1:指令被静默忽略(无匹配 transform 规则或规则产生 noop 效果)。
150                // 与 reactor 行为一致:产生 Error 事实使系统显式感知此问题
151                let err_id = id_gen.next_id();
152                let msg = format!(
153                    "Instruction ignored by TCB: type={}, reason={}, instruction={:?}",
154                    instruction_type, reason, instruction
155                );
156                facts.push(Fact::Error {
157                    id: err_id,
158                    message: msg,
159                });
160                tracing::warn!(
161                    instruction_type = %instruction_type,
162                    reason = %reason,
163                    "TCB 静默忽略指令(无匹配规则或 noop 效果)"
164                );
165                break;
166            }
167            Err(e) => {
168                let err_id = id_gen.next_id();
169                let msg = format!("TCB error at step {}: {}", steps, e);
170                facts.push(Fact::Error {
171                    id: err_id,
172                    message: msg,
173                });
174                tracing::error!(step = steps, error = %e, "TCB execution error");
175                break;
176            }
177        }
178    }
179
180    // 始终发射 Stable(即使是 Error 退出,也记录最终 payload 快照)
181    let stable_id = id_gen.next_id();
182    facts.push(Fact::Stable {
183        id: stable_id,
184        final_snapshot: payload,
185    });
186
187    Ok(facts)
188}
189
190#[cfg(test)]
191mod tests {
192    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
193    use super::*;
194    use evorule_tcb::JsonValue;
195
196    /// 构造 noop 指令
197    fn noop_instruction() -> JsonValue {
198        JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
199    }
200
201    /// 构造无条件 push 规则:push 一条 noop 到队列前端
202    fn push_noop_rule() -> JsonValue {
203        JsonValue::object_from_pairs(&[
204            ("type", JsonValue::string("push")),
205            (
206                "params",
207                JsonValue::object_from_pairs(&[(
208                    "instructions",
209                    JsonValue::array(vec![noop_instruction()]),
210                )]),
211            ),
212        ])
213    }
214
215    /// 构造无条件 io_request 规则
216    fn io_request_rule(io_type: &str) -> JsonValue {
217        JsonValue::object_from_pairs(&[
218            ("type", JsonValue::string("io_request")),
219            (
220                "params",
221                JsonValue::object_from_pairs(&[
222                    ("io_type", JsonValue::string(io_type)),
223                    ("url", JsonValue::string("http://example.com")),
224                ]),
225            ),
226        ])
227    }
228
229    #[test]
230    fn test_execute_empty_core_eval_noop() {
231        // v0.3.1:空 core_eval + noop 指令 → TCB 显式返回 `Ignored`(无匹配 transform 规则)
232        // cli executor 按 reactor 一致行为产生 `Error` 事实(不再静默失败)
233        let facts = execute(
234            &[],
235            JsonValue::empty_object(),
236            noop_instruction(),
237            DEFAULT_MAX_STEPS,
238        )
239        .unwrap();
240
241        // 应产生:Command + Error(ignored by TCB) + Stable
242        assert_eq!(facts.len(), 3, "expected Command + Error(ignored) + Stable");
243        assert!(matches!(facts[0], Fact::Command { .. }));
244        match &facts[1] {
245            Fact::Error { message, .. } => {
246                assert!(
247                    message.contains("ignored by TCB"),
248                    "expected TCB ignored error, got: {}",
249                    message
250                );
251            }
252            other => panic!("expected Error for Ignored, got {:?}", other),
253        }
254        assert!(matches!(facts[2], Fact::Stable { .. }));
255    }
256
257    #[test]
258    fn test_execute_max_steps_zero() {
259        // max_steps=0:立即发 Error,不执行任何指令
260        let facts = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
261
262        // 应产生:Command + Error + Stable
263        assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
264        assert!(matches!(facts[0], Fact::Command { .. }));
265        match &facts[1] {
266            Fact::Error { message, .. } => {
267                assert!(message.contains("max_steps"), "message: {}", message);
268            }
269            other => panic!("expected Error, got {:?}", other),
270        }
271        assert!(matches!(facts[2], Fact::Stable { .. }));
272    }
273
274    #[test]
275    fn test_execute_max_steps_exceeded_with_push() {
276        // core_eval 含 push 规则(无限循环),max_steps=3 限制
277        let core_eval = vec![push_noop_rule()];
278        let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
279
280        // 应产生:Command + 3×StateTransition + Error + Stable = 6
281        // (steps 0,1,2 各产生 StateTransition,step 3 触发 max_steps)
282        assert!(
283            facts.len() >= 4,
284            "expected at least Command + StateTransitions + Error + Stable, got {}",
285            facts.len()
286        );
287        // 最后第二个应为 Error
288        let last_idx = facts.len() - 2;
289        match &facts[last_idx] {
290            Fact::Error { message, .. } => {
291                assert!(message.contains("max_steps"), "message: {}", message);
292            }
293            other => panic!("expected Error at index {}, got {:?}", last_idx, other),
294        }
295        // 最后应为 Stable
296        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
297    }
298
299    #[test]
300    fn test_execute_push_produces_nonempty_queue() {
301        // core_eval 含 push 规则,max_steps=1 只执行一步
302        let core_eval = vec![push_noop_rule()];
303        let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
304
305        // 第一个 StateTransition 的 new_queue 应非空(push 生效)
306        let st = facts.iter().find_map(|f| {
307            if let Fact::StateTransition { new_queue, .. } = f {
308                Some(new_queue)
309            } else {
310                None
311            }
312        });
313        let new_queue = st.expect("should have StateTransition");
314        assert!(
315            !new_queue.is_empty(),
316            "push rule should produce non-empty new_queue"
317        );
318    }
319
320    #[test]
321    fn test_execute_io_request_produces_io_fact() {
322        // core_eval 含 io_request 规则
323        let core_eval = vec![io_request_rule("call_external")];
324        let facts = execute(
325            &core_eval,
326            JsonValue::empty_object(),
327            noop_instruction(),
328            DEFAULT_MAX_STEPS,
329        )
330        .unwrap();
331
332        // 应产生:Command + IoRequest + Error + Stable
333        let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
334        assert!(has_io_request, "should have IoRequest fact");
335
336        let has_error = facts.iter().any(
337            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
338        );
339        assert!(has_error, "should have Error fact about no I/O handler");
340
341        // 最后应为 Stable
342        assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
343    }
344
345    #[test]
346    fn test_execute_unknown_io_type_produces_error() {
347        // v0.2.0:io_type 透传不校验,"unknown_io_type" 不再被 parse 拒绝,
348        // 而是透传后由 cli(无 handler)发 "no I/O handler for io_type=unknown_io_type" Error
349        let core_eval = vec![io_request_rule("unknown_io_type")];
350        let facts = execute(
351            &core_eval,
352            JsonValue::empty_object(),
353            noop_instruction(),
354            DEFAULT_MAX_STEPS,
355        )
356        .unwrap();
357
358        let has_error = facts.iter().any(
359            |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
360        );
361        assert!(
362            has_error,
363            "should have Error (no I/O handler) for unknown io_type"
364        );
365    }
366
367    #[test]
368    fn test_execute_tcb_error_produces_error() {
369        // 构造会触发 TCB 错误的场景:core_eval 含非法规则(缺 params)
370        let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
371        let facts = execute(
372            &[bad_rule],
373            JsonValue::empty_object(),
374            noop_instruction(),
375            DEFAULT_MAX_STEPS,
376        )
377        .unwrap();
378
379        // 应产生 Error(TCB error)
380        let has_error = facts
381            .iter()
382            .any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
383        assert!(has_error, "should have TCB error fact");
384    }
385
386    #[test]
387    fn test_execute_fact_ids_monotonic() {
388        // 验证 FactId 单调递增
389        let facts = execute(
390            &[],
391            JsonValue::empty_object(),
392            noop_instruction(),
393            DEFAULT_MAX_STEPS,
394        )
395        .unwrap();
396
397        let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
398        for i in 1..ids.len() {
399            assert!(
400                ids[i] > ids[i - 1],
401                "FactIds must be monotonically increasing: {:?}",
402                ids
403            );
404        }
405    }
406
407    #[test]
408    fn test_execute_fifo_pop_front_semantics() {
409        // 验证 FIFO:push 两条指令后,按顺序执行
410        // core_eval: push [noop, noop](两条指令)
411        let push_two = JsonValue::object_from_pairs(&[
412            ("type", JsonValue::string("push")),
413            (
414                "params",
415                JsonValue::object_from_pairs(&[(
416                    "instructions",
417                    JsonValue::array(vec![noop_instruction(), noop_instruction()]),
418                )]),
419            ),
420        ]);
421        let core_eval = vec![push_two];
422        // max_steps=3:初始指令执行 push(step1),然后执行 push 的两条 noop(step2,3)
423        let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
424
425        // 验证:每次执行都通过 pop_front 取指令(FIFO)
426        // step1: pop_front 初始 noop → push [noop, noop] → queue=[noop, noop]
427        // step2: pop_front noop(第1条) → push [noop, noop] → queue=[noop, noop, noop]
428        // step3: pop_front noop → push → queue=[noop, noop, noop]
429        // max_steps=3 触发 Error
430        let state_transitions: Vec<_> = facts
431            .iter()
432            .filter(|f| matches!(f, Fact::StateTransition { .. }))
433            .collect();
434        assert!(
435            !state_transitions.is_empty(),
436            "should have at least one StateTransition"
437        );
438        // 第一个 StateTransition 的 new_queue 应有 2 条指令(push 的结果)
439        if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
440            assert_eq!(
441                new_queue.len(),
442                2,
443                "first push should produce 2 instructions in queue"
444            );
445        }
446    }
447}