use std::collections::{HashMap, VecDeque};
use evorule_reactor::{Fact, FactId, FactIdGenerator, IoType};
use evorule_tcb::{execute_transition, JsonValue, TransitionResult};
use crate::error::CliError;
pub const DEFAULT_MAX_STEPS: usize = 10000;
pub fn execute(
core_eval: &[JsonValue],
initial_payload: JsonValue,
initial_instruction: JsonValue,
max_steps: usize,
) -> Result<Vec<Fact>, CliError> {
let mut facts: Vec<Fact> = Vec::new();
let mut id_gen = FactIdGenerator::new();
let mut queue: VecDeque<JsonValue> = VecDeque::new();
queue.push_back(initial_instruction);
let mut payload = initial_payload;
let mut steps = 0;
let mut pending_io: HashMap<FactId, JsonValue> = HashMap::new();
let cmd_id = id_gen.next_id();
let mut current_cause: FactId = cmd_id;
let cmd_instruction = queue.front().cloned().unwrap_or(JsonValue::Null);
facts.push(Fact::Command {
id: cmd_id,
instruction: cmd_instruction,
});
while !queue.is_empty() {
if steps >= max_steps {
let err_id = id_gen.next_id();
facts.push(Fact::Error {
id: err_id,
message: format!("max_steps exceeded: {}", steps),
});
tracing::warn!(steps, max_steps, "max_steps exceeded");
break;
}
let instruction = match queue.pop_front() {
Some(i) => i,
None => break, };
steps += 1;
let queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
let result = execute_transition(core_eval, &instruction, &payload, &queue_snapshot);
match result {
Ok(TransitionResult::State {
new_payload,
new_queue,
}) => {
payload = new_payload;
queue = new_queue.into_iter().collect();
let id = id_gen.next_id();
let new_queue_snapshot: Vec<JsonValue> = queue.iter().cloned().collect();
facts.push(Fact::StateTransition {
id,
cause: current_cause,
new_payload: payload.clone(),
new_queue: new_queue_snapshot,
});
current_cause = id;
}
Ok(TransitionResult::IoRequired { io_type, params }) => {
let io_type = IoType::new(&io_type);
let req_id = id_gen.next_id();
pending_io.insert(req_id, instruction.clone());
facts.push(Fact::IoRequest {
id: req_id,
cause: current_cause,
io_type: io_type.clone(),
params,
});
let err_id = id_gen.next_id();
facts.push(Fact::Error {
id: err_id,
message: format!("no I/O handler for io_type={}", io_type.as_str()),
});
tracing::warn!(
io_type = %io_type.as_str(),
request_id = ?req_id,
"I/O required but no handler available, stopping"
);
break;
}
Err(e) => {
let err_id = id_gen.next_id();
let msg = format!("TCB error at step {}: {}", steps, e);
facts.push(Fact::Error {
id: err_id,
message: msg,
});
tracing::error!(step = steps, error = %e, "TCB execution error");
break;
}
}
}
let stable_id = id_gen.next_id();
facts.push(Fact::Stable {
id: stable_id,
final_snapshot: payload,
});
Ok(facts)
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
use super::*;
use evorule_tcb::JsonValue;
fn noop_instruction() -> JsonValue {
JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))])
}
fn push_noop_rule() -> JsonValue {
JsonValue::object_from_pairs(&[
("type", JsonValue::string("push")),
(
"params",
JsonValue::object_from_pairs(&[(
"instructions",
JsonValue::array(vec![noop_instruction()]),
)]),
),
])
}
fn io_request_rule(io_type: &str) -> JsonValue {
JsonValue::object_from_pairs(&[
("type", JsonValue::string("io_request")),
(
"params",
JsonValue::object_from_pairs(&[
("io_type", JsonValue::string(io_type)),
("url", JsonValue::string("http://example.com")),
]),
),
])
}
#[test]
fn test_execute_empty_core_eval_noop() {
let facts = execute(
&[],
JsonValue::empty_object(),
noop_instruction(),
DEFAULT_MAX_STEPS,
)
.unwrap();
assert_eq!(
facts.len(),
3,
"expected Command + StateTransition + Stable"
);
assert!(matches!(facts[0], Fact::Command { .. }));
assert!(matches!(facts[1], Fact::StateTransition { .. }));
assert!(matches!(facts[2], Fact::Stable { .. }));
}
#[test]
fn test_execute_max_steps_zero() {
let facts = execute(&[], JsonValue::empty_object(), noop_instruction(), 0).unwrap();
assert_eq!(facts.len(), 3, "expected Command + Error + Stable");
assert!(matches!(facts[0], Fact::Command { .. }));
match &facts[1] {
Fact::Error { message, .. } => {
assert!(message.contains("max_steps"), "message: {}", message);
}
other => panic!("expected Error, got {:?}", other),
}
assert!(matches!(facts[2], Fact::Stable { .. }));
}
#[test]
fn test_execute_max_steps_exceeded_with_push() {
let core_eval = vec![push_noop_rule()];
let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
assert!(
facts.len() >= 4,
"expected at least Command + StateTransitions + Error + Stable, got {}",
facts.len()
);
let last_idx = facts.len() - 2;
match &facts[last_idx] {
Fact::Error { message, .. } => {
assert!(message.contains("max_steps"), "message: {}", message);
}
other => panic!("expected Error at index {}, got {:?}", last_idx, other),
}
assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
}
#[test]
fn test_execute_push_produces_nonempty_queue() {
let core_eval = vec![push_noop_rule()];
let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 1).unwrap();
let st = facts.iter().find_map(|f| {
if let Fact::StateTransition { new_queue, .. } = f {
Some(new_queue)
} else {
None
}
});
let new_queue = st.expect("should have StateTransition");
assert!(
!new_queue.is_empty(),
"push rule should produce non-empty new_queue"
);
}
#[test]
fn test_execute_io_request_produces_io_fact() {
let core_eval = vec![io_request_rule("call_external")];
let facts = execute(
&core_eval,
JsonValue::empty_object(),
noop_instruction(),
DEFAULT_MAX_STEPS,
)
.unwrap();
let has_io_request = facts.iter().any(|f| matches!(f, Fact::IoRequest { .. }));
assert!(has_io_request, "should have IoRequest fact");
let has_error = facts.iter().any(
|f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
);
assert!(has_error, "should have Error fact about no I/O handler");
assert!(matches!(facts[facts.len() - 1], Fact::Stable { .. }));
}
#[test]
fn test_execute_unknown_io_type_produces_error() {
let core_eval = vec![io_request_rule("unknown_io_type")];
let facts = execute(
&core_eval,
JsonValue::empty_object(),
noop_instruction(),
DEFAULT_MAX_STEPS,
)
.unwrap();
let has_error = facts.iter().any(
|f| matches!(f, Fact::Error { ref message, .. } if message.contains("no I/O handler")),
);
assert!(
has_error,
"should have Error (no I/O handler) for unknown io_type"
);
}
#[test]
fn test_execute_tcb_error_produces_error() {
let bad_rule = JsonValue::object_from_pairs(&[("type", JsonValue::string("set"))]);
let facts = execute(
&[bad_rule],
JsonValue::empty_object(),
noop_instruction(),
DEFAULT_MAX_STEPS,
)
.unwrap();
let has_error = facts
.iter()
.any(|f| matches!(f, Fact::Error { ref message, .. } if message.contains("TCB error")));
assert!(has_error, "should have TCB error fact");
}
#[test]
fn test_execute_fact_ids_monotonic() {
let facts = execute(
&[],
JsonValue::empty_object(),
noop_instruction(),
DEFAULT_MAX_STEPS,
)
.unwrap();
let ids: Vec<u64> = facts.iter().map(|f| f.id().0).collect();
for i in 1..ids.len() {
assert!(
ids[i] > ids[i - 1],
"FactIds must be monotonically increasing: {:?}",
ids
);
}
}
#[test]
fn test_execute_fifo_pop_front_semantics() {
let push_two = JsonValue::object_from_pairs(&[
("type", JsonValue::string("push")),
(
"params",
JsonValue::object_from_pairs(&[(
"instructions",
JsonValue::array(vec![noop_instruction(), noop_instruction()]),
)]),
),
]);
let core_eval = vec![push_two];
let facts = execute(&core_eval, JsonValue::empty_object(), noop_instruction(), 3).unwrap();
let state_transitions: Vec<_> = facts
.iter()
.filter(|f| matches!(f, Fact::StateTransition { .. }))
.collect();
assert!(
!state_transitions.is_empty(),
"should have at least one StateTransition"
);
if let Fact::StateTransition { new_queue, .. } = state_transitions[0] {
assert_eq!(
new_queue.len(),
2,
"first push should produce 2 instructions in queue"
);
}
}
}