1use 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
35pub const DEFAULT_MAX_STEPS: usize = 10000;
37
38#[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 let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
70
71 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 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 let instruction = match queue.pop_front() {
94 Some(i) => i,
95 None => break, };
97 steps += 1;
98
99 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 let io_type = IoType::new(&io_type);
123 let req_id = id_gen.next_id();
124 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 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 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 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 fn noop_instruction() -> JsonValue {
198 JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
199 }
200
201 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 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 let facts = execute(
234 &[],
235 JsonValue::empty_object(),
236 noop_instruction(),
237 DEFAULT_MAX_STEPS,
238 )
239 .unwrap();
240
241 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 let facts = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
261
262 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 let core_eval = vec![push_noop_rule()];
278 let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
279
280 assert!(
283 facts.len() >= 4,
284 "expected at least Command + StateTransitions + Error + Stable, got {}",
285 facts.len()
286 );
287 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 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
297 }
298
299 #[test]
300 fn test_execute_push_produces_nonempty_queue() {
301 let core_eval = vec![push_noop_rule()];
303 let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
304
305 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 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 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 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
343 }
344
345 #[test]
346 fn test_execute_unknown_io_type_produces_error() {
347 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 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 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 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 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 let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
424
425 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 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}