1use std::collections::{HashMap, VecDeque};
32
33use evorule_reactor::{Fact, FactId, FactIdGenerator, IoType};
34use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
35
36use crate::error::CliError;
37
38pub const DEFAULT_MAX_STEPS: usize = 10000;
40
41#[allow(clippy::too_many_lines)]
60pub fn execute(
61 core_eval: &[JsonValue],
62 initial_payload: JsonValue,
63 initial_instruction: JsonValue,
64 max_steps: usize,
65) -> Result<(Vec<Fact>, JsonValue), CliError> {
66 let mut facts: Vec<Fact> = Vec::new();
67 let mut id_gen = FactIdGenerator::new();
68 let mut queue: VecDeque<JsonValue> = VecDeque::new();
69 queue.push_back(initial_instruction);
70 let mut payload = initial_payload;
71 let mut steps = 0;
72 let mut version: u64 = 0;
74 let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
76
77 let cmd_id = id_gen.next_id();
79 let mut current_cause: FactId = cmd_id;
80 let cmd_instruction = queue.front().cloned().unwrap_or(JsonValue::Null);
81 facts.push(Fact::Command {
82 id: cmd_id,
83 instruction: cmd_instruction,
84 });
85
86 while !queue.is_empty() {
87 if steps >= max_steps {
89 let err_id = id_gen.next_id();
90 facts.push(Fact::Error {
91 id: err_id,
92 message: format!("max_steps exceeded: {}", steps),
93 });
94 tracing::warn!(steps, max_steps, "max_steps exceeded");
95 break;
96 }
97
98 let instruction = match queue.pop_front() {
100 Some(i) => i,
101 None => break, };
103 steps += 1;
104
105 let queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
107 let result = execute_transition(core_eval, &instruction, &payload, &queue_snapshot);
108
109 match result {
110 Ok(TransitionResult::State {
111 new_payload,
112 new_queue,
113 }) => {
114 payload = new_payload;
115 queue = new_queue.into_iter().collect();
116 version += 1;
117 let id = id_gen.next_id();
118 let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
119 facts.push(Fact::StateTransition {
120 id,
121 cause: current_cause,
122 new_payload: payload.clone(),
123 new_queue: new_queue_snapshot,
124 });
125 current_cause = id;
126 }
127 Ok(TransitionResult::IoRequired { io_type, params }) => {
128 let io_type = IoType::new(&io_type);
130 let req_id = id_gen.next_id();
131 pending_io.insert(req_id, instruction.clone());
133 facts.push(Fact::IoRequest {
134 id: req_id,
135 cause: current_cause,
136 io_type: io_type.clone(),
137 params,
138 });
139 let err_id = id_gen.next_id();
141 facts.push(Fact::Error {
142 id: err_id,
143 message: format!("no I/O handler for io_type={}", io_type.as_str()),
144 });
145 tracing::warn!(
146 io_type = %io_type.as_str(),
147 request_id = ?req_id,
148 "I/O required but no handler available, stopping"
149 );
150 break;
151 }
152 Ok(TransitionResult::Ignored {
153 instruction_type,
154 reason,
155 }) => {
156 let err_id = id_gen.next_id();
159 let msg = format!(
160 "Instruction ignored by TCB: type={}, reason={}, instruction={:?}",
161 instruction_type, reason, instruction
162 );
163 facts.push(Fact::Error {
164 id: err_id,
165 message: msg,
166 });
167 tracing::warn!(
168 instruction_type = %instruction_type,
169 reason = %reason,
170 "TCB 静默忽略指令(无匹配规则或 noop 效果)"
171 );
172 break;
173 }
174 Err(e) => {
175 let err_id = id_gen.next_id();
176 let msg = format!("TCB error at step {}: {}", steps, e);
177 facts.push(Fact::Error {
178 id: err_id,
179 message: msg,
180 });
181 tracing::error!(step = steps, error = %e, "TCB execution error");
182 break;
183 }
184 }
185 }
186
187 let stable_id = id_gen.next_id();
190 facts.push(Fact::Stable {
191 id: stable_id,
192 version,
193 });
194
195 Ok((facts, payload))
196}
197
198#[cfg(test)]
199mod tests {
200 #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
201 use super::*;
202 use evorule_tcb::JsonValue;
203
204 fn noop_instruction() -> JsonValue {
206 JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
207 }
208
209 fn push_noop_rule() -> JsonValue {
211 JsonValue::object_from_pairs(&[
212 ("type", JsonValue::string("push")),
213 (
214 "params",
215 JsonValue::object_from_pairs(&[(
216 "instructions",
217 JsonValue::array(vec![noop_instruction()]),
218 )]),
219 ),
220 ])
221 }
222
223 fn io_request_rule(io_type: &str) -> JsonValue {
225 JsonValue::object_from_pairs(&[
226 ("type", JsonValue::string("io_request")),
227 (
228 "params",
229 JsonValue::object_from_pairs(&[
230 ("io_type", JsonValue::string(io_type)),
231 ("url", JsonValue::string("http://example.com")),
232 ]),
233 ),
234 ])
235 }
236
237 #[test]
238 fn test_execute_empty_core_eval_noop() {
239 let (facts, _) = execute(
242 &[],
243 JsonValue::empty_object(),
244 noop_instruction(),
245 DEFAULT_MAX_STEPS,
246 )
247 .unwrap();
248
249 assert_eq!(facts.len(), 3, "expected Command + Error(ignored) + Stable");
251 assert!(matches!(facts[0], Fact::Command { .. }));
252 match &facts[1] {
253 Fact::Error { message, .. } => {
254 assert!(
255 message.contains("ignored by TCB"),
256 "expected TCB ignored error, got: {}",
257 message
258 );
259 }
260 other => panic!("expected Error for Ignored, got {:?}", other),
261 }
262 assert!(matches!(facts[2], Fact::Stable { .. }));
263 }
264
265 #[test]
266 fn test_execute_max_steps_zero() {
267 let (facts, _) = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
269
270 assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
272 assert!(matches!(facts[0], Fact::Command { .. }));
273 match &facts[1] {
274 Fact::Error { message, .. } => {
275 assert!(message.contains("max_steps"), "message: {}", message);
276 }
277 other => panic!("expected Error, got {:?}", other),
278 }
279 assert!(matches!(facts[2], Fact::Stable { .. }));
280 }
281
282 #[test]
283 fn test_execute_max_steps_exceeded_with_push() {
284 let core_eval = vec![push_noop_rule()];
286 let (facts, _) =
287 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
288
289 assert!(
292 facts.len() >= 4,
293 "expected at least Command + StateTransitions + Error + Stable, got {}",
294 facts.len()
295 );
296 let last_idx = facts.len() - 2;
298 match &facts[last_idx] {
299 Fact::Error { message, .. } => {
300 assert!(message.contains("max_steps"), "message: {}", message);
301 }
302 other => panic!("expected Error at index {}, got {:?}", last_idx, other),
303 }
304 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
306 }
307
308 #[test]
309 fn test_execute_push_produces_nonempty_queue() {
310 let core_eval = vec![push_noop_rule()];
312 let (facts, _) =
313 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
314
315 let st = facts.iter().find_map(|f| {
317 if let Fact::StateTransition { new_queue, .. } = f {
318 Some(new_queue)
319 } else {
320 None
321 }
322 });
323 let new_queue = st.expect("should have StateTransition");
324 assert!(
325 !new_queue.is_empty(),
326 "push rule should produce non-empty new_queue"
327 );
328 }
329
330 #[test]
331 fn test_execute_io_request_produces_io_fact() {
332 let core_eval = vec![io_request_rule("call_external")];
334 let (facts, _) = execute(
335 &core_eval,
336 JsonValue::empty_object(),
337 noop_instruction(),
338 DEFAULT_MAX_STEPS,
339 )
340 .unwrap();
341
342 let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
344 assert!(has_io_request, "should have IoRequest fact");
345
346 let has_error = facts.iter().any(
347 |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
348 );
349 assert!(has_error, "should have Error fact about no I/O handler");
350
351 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
353 }
354
355 #[test]
356 fn test_execute_unknown_io_type_produces_error() {
357 let core_eval = vec![io_request_rule("unknown_io_type")];
360 let (facts, _) = execute(
361 &core_eval,
362 JsonValue::empty_object(),
363 noop_instruction(),
364 DEFAULT_MAX_STEPS,
365 )
366 .unwrap();
367
368 let has_error = facts.iter().any(
369 |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
370 );
371 assert!(
372 has_error,
373 "should have Error (no I/O handler) for unknown io_type"
374 );
375 }
376
377 #[test]
378 fn test_execute_tcb_error_produces_error() {
379 let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
381 let (facts, _) = execute(
382 &[bad_rule],
383 JsonValue::empty_object(),
384 noop_instruction(),
385 DEFAULT_MAX_STEPS,
386 )
387 .unwrap();
388
389 let has_error = facts
391 .iter()
392 .any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
393 assert!(has_error, "should have TCB error fact");
394 }
395
396 #[test]
397 fn test_execute_fact_ids_monotonic() {
398 let (facts, _) = execute(
400 &[],
401 JsonValue::empty_object(),
402 noop_instruction(),
403 DEFAULT_MAX_STEPS,
404 )
405 .unwrap();
406
407 let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
408 for i in 1..ids.len() {
409 assert!(
410 ids[i] > ids[i - 1],
411 "FactIds must be monotonically increasing: {:?}",
412 ids
413 );
414 }
415 }
416
417 #[test]
418 fn test_execute_fifo_pop_front_semantics() {
419 let push_two = JsonValue::object_from_pairs(&[
422 ("type", JsonValue::string("push")),
423 (
424 "params",
425 JsonValue::object_from_pairs(&[(
426 "instructions",
427 JsonValue::array(vec![noop_instruction(), noop_instruction()]),
428 )]),
429 ),
430 ]);
431 let core_eval = vec![push_two];
432 let (facts, _) =
434 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
435
436 let state_transitions: Vec<_> = facts
442 .iter()
443 .filter(|f| matches!(f, Fact::StateTransition { .. }))
444 .collect();
445 assert!(
446 !state_transitions.is_empty(),
447 "should have at least one StateTransition"
448 );
449 if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
451 assert_eq!(
452 new_queue.len(),
453 2,
454 "first push should produce 2 instructions in queue"
455 );
456 }
457 }
458}