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 rule_hits: _,
115 }) => {
116 payload = new_payload;
117 queue = new_queue.into_iter().collect();
118 version += 1;
119 let id = id_gen.next_id();
120 let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
121 facts.push(Fact::StateTransition {
122 id,
123 cause: current_cause,
124 new_payload: payload.clone(),
125 new_queue: new_queue_snapshot,
126 });
127 current_cause = id;
128 }
129 Ok(TransitionResult::IoRequired { io_type, params }) => {
130 let io_type = IoType::new(&io_type);
132 let req_id = id_gen.next_id();
133 pending_io.insert(req_id, instruction.clone());
135 facts.push(Fact::IoRequest {
136 id: req_id,
137 cause: current_cause,
138 io_type: io_type.clone(),
139 params,
140 });
141 let err_id = id_gen.next_id();
143 facts.push(Fact::Error {
144 id: err_id,
145 message: format!("no I/O handler for io_type={}", io_type.as_str()),
146 });
147 tracing::warn!(
148 io_type = %io_type.as_str(),
149 request_id = ?req_id,
150 "I/O required but no handler available, stopping"
151 );
152 break;
153 }
154 Ok(TransitionResult::Ignored {
155 instruction_type,
156 reason,
157 rule_hits: _,
159 }) => {
160 let err_id = id_gen.next_id();
163 let msg = format!(
164 "Instruction ignored by TCB: type={}, reason={}, instruction={:?}",
165 instruction_type, reason, instruction
166 );
167 facts.push(Fact::Error {
168 id: err_id,
169 message: msg,
170 });
171 tracing::warn!(
172 instruction_type = %instruction_type,
173 reason = %reason,
174 "TCB silently ignored instruction (no matching rule or noop effect)"
175 );
176 break;
177 }
178 Ok(TransitionResult::Halted { rule_index, reason }) => {
179 let v_id = id_gen.next_id();
182 facts.push(Fact::Violation {
183 id: v_id,
184 cause: current_cause,
185 rule_index: rule_index as u64,
186 reason: reason.clone(),
187 instruction: instruction.clone(),
188 });
189 tracing::warn!(
190 rule_index,
191 %reason,
192 "enforce primitive blocked: violating instruction rejected"
193 );
194 continue;
195 }
196 Err(e) => {
197 let err_id = id_gen.next_id();
198 let msg = format!("TCB error at step {}: {}", steps, e);
199 facts.push(Fact::Error {
200 id: err_id,
201 message: msg,
202 });
203 tracing::error!(step = steps, error = %e, "TCB execution error");
204 break;
205 }
206 }
207 }
208
209 let stable_id = id_gen.next_id();
212 facts.push(Fact::Stable {
213 id: stable_id,
214 version,
215 });
216
217 Ok((facts, payload))
218}
219
220#[cfg(test)]
221mod tests {
222 #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
223 use super::*;
224 use evorule_tcb::JsonValue;
225
226 fn noop_instruction() -> JsonValue {
228 JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
229 }
230
231 fn push_noop_rule() -> JsonValue {
233 JsonValue::object_from_pairs(&[
234 ("type", JsonValue::string("push")),
235 (
236 "params",
237 JsonValue::object_from_pairs(&[(
238 "instructions",
239 JsonValue::array(vec![noop_instruction()]),
240 )]),
241 ),
242 ])
243 }
244
245 fn io_request_rule(io_type: &str) -> JsonValue {
247 JsonValue::object_from_pairs(&[
248 ("type", JsonValue::string("io_request")),
249 (
250 "params",
251 JsonValue::object_from_pairs(&[
252 ("io_type", JsonValue::string(io_type)),
253 ("url", JsonValue::string("http://example.com")),
254 ]),
255 ),
256 ])
257 }
258
259 #[test]
260 fn test_execute_empty_core_eval_noop() {
261 let (facts, _) = execute(
264 &[],
265 JsonValue::empty_object(),
266 noop_instruction(),
267 DEFAULT_MAX_STEPS,
268 )
269 .unwrap();
270
271 assert_eq!(facts.len(), 3, "expected Command + Error(ignored) + Stable");
273 assert!(matches!(facts[0], Fact::Command { .. }));
274 match &facts[1] {
275 Fact::Error { message, .. } => {
276 assert!(
277 message.contains("ignored by TCB"),
278 "expected TCB ignored error, got: {}",
279 message
280 );
281 }
282 other => panic!("expected Error for Ignored, got {:?}", other),
283 }
284 assert!(matches!(facts[2], Fact::Stable { .. }));
285 }
286
287 #[test]
288 fn test_execute_max_steps_zero() {
289 let (facts, _) = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
291
292 assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
294 assert!(matches!(facts[0], Fact::Command { .. }));
295 match &facts[1] {
296 Fact::Error { message, .. } => {
297 assert!(message.contains("max_steps"), "message: {}", message);
298 }
299 other => panic!("expected Error, got {:?}", other),
300 }
301 assert!(matches!(facts[2], Fact::Stable { .. }));
302 }
303
304 #[test]
305 fn test_execute_max_steps_exceeded_with_push() {
306 let core_eval = vec![push_noop_rule()];
308 let (facts, _) =
309 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
310
311 assert!(
314 facts.len() >= 4,
315 "expected at least Command + StateTransitions + Error + Stable, got {}",
316 facts.len()
317 );
318 let last_idx = facts.len() - 2;
320 match &facts[last_idx] {
321 Fact::Error { message, .. } => {
322 assert!(message.contains("max_steps"), "message: {}", message);
323 }
324 other => panic!("expected Error at index {}, got {:?}", last_idx, other),
325 }
326 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
328 }
329
330 #[test]
331 fn test_execute_push_produces_nonempty_queue() {
332 let core_eval = vec![push_noop_rule()];
334 let (facts, _) =
335 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
336
337 let st = facts.iter().find_map(|f| {
339 if let Fact::StateTransition { new_queue, .. } = f {
340 Some(new_queue)
341 } else {
342 None
343 }
344 });
345 let new_queue = st.expect("should have StateTransition");
346 assert!(
347 !new_queue.is_empty(),
348 "push rule should produce non-empty new_queue"
349 );
350 }
351
352 #[test]
353 fn test_execute_io_request_produces_io_fact() {
354 let core_eval = vec![io_request_rule("call_external")];
356 let (facts, _) = execute(
357 &core_eval,
358 JsonValue::empty_object(),
359 noop_instruction(),
360 DEFAULT_MAX_STEPS,
361 )
362 .unwrap();
363
364 let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
366 assert!(has_io_request, "should have IoRequest fact");
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!(has_error, "should have Error fact about no I/O handler");
372
373 assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
375 }
376
377 #[test]
378 fn test_execute_unknown_io_type_produces_error() {
379 let core_eval = vec![io_request_rule("unknown_io_type")];
382 let (facts, _) = execute(
383 &core_eval,
384 JsonValue::empty_object(),
385 noop_instruction(),
386 DEFAULT_MAX_STEPS,
387 )
388 .unwrap();
389
390 let has_error = facts.iter().any(
391 |f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
392 );
393 assert!(
394 has_error,
395 "should have Error (no I/O handler) for unknown io_type"
396 );
397 }
398
399 #[test]
400 fn test_execute_tcb_error_produces_error() {
401 let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
403 let (facts, _) = execute(
404 &[bad_rule],
405 JsonValue::empty_object(),
406 noop_instruction(),
407 DEFAULT_MAX_STEPS,
408 )
409 .unwrap();
410
411 let has_error = facts
413 .iter()
414 .any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
415 assert!(has_error, "should have TCB error fact");
416 }
417
418 #[test]
419 fn test_execute_fact_ids_monotonic() {
420 let (facts, _) = execute(
422 &[],
423 JsonValue::empty_object(),
424 noop_instruction(),
425 DEFAULT_MAX_STEPS,
426 )
427 .unwrap();
428
429 let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
430 for i in 1..ids.len() {
431 assert!(
432 ids[i] > ids[i - 1],
433 "FactIds must be monotonically increasing: {:?}",
434 ids
435 );
436 }
437 }
438
439 #[test]
440 fn test_execute_fifo_pop_front_semantics() {
441 let push_two = JsonValue::object_from_pairs(&[
444 ("type", JsonValue::string("push")),
445 (
446 "params",
447 JsonValue::object_from_pairs(&[(
448 "instructions",
449 JsonValue::array(vec![noop_instruction(), noop_instruction()]),
450 )]),
451 ),
452 ]);
453 let core_eval = vec![push_two];
454 let (facts, _) =
456 execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
457
458 let state_transitions: Vec<_> = facts
464 .iter()
465 .filter(|f| matches!(f, Fact::StateTransition { .. }))
466 .collect();
467 assert!(
468 !state_transitions.is_empty(),
469 "should have at least one StateTransition"
470 );
471 if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
473 assert_eq!(
474 new_queue.len(),
475 2,
476 "first push should produce 2 instructions in queue"
477 );
478 }
479 }
480}