use lex_ast::Stage;
use lex_types::{check_program, TypeError};
use crate::apply::{apply, ApplyError, NewHead};
use crate::op_log::OpLog;
use crate::operation::{OpId, Operation, StageTransition};
#[derive(Debug, thiserror::Error)]
pub enum GateError {
#[error(transparent)]
Apply(#[from] ApplyError),
#[error("type errors after applying op {op_id}: {} error(s)", errors.len())]
TypeError {
op_id: OpId,
errors: Vec<TypeError>,
},
}
pub fn check_and_apply(
op_log: &OpLog,
head_op: Option<&OpId>,
op: Operation,
transition: StageTransition,
candidate: &[Stage],
) -> Result<NewHead, GateError> {
if let Err(errors) = check_program(candidate) {
return Err(GateError::TypeError {
op_id: op.op_id(),
errors,
});
}
Ok(apply(op_log, head_op, op, transition)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operation::OperationKind;
use std::collections::BTreeSet;
fn fac_op_and_transition() -> (Operation, StageTransition) {
let op = Operation::new(
OperationKind::AddFunction {
sig_id: "fac".into(),
stage_id: "s1".into(),
effects: BTreeSet::new(),
},
[],
);
let t = StageTransition::Create {
sig_id: "fac".into(),
stage_id: "s1".into(),
};
(op, t)
}
fn parse(src: &str) -> Vec<Stage> {
let prog = lex_syntax::parse_source(src).expect("parse");
lex_ast::canonicalize_program(&prog)
}
#[test]
fn clean_program_is_accepted_and_persisted() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op, t) = fac_op_and_transition();
let head = check_and_apply(&log, None, op, t, &candidate).unwrap();
assert!(log.get(&head.op_id).unwrap().is_some());
}
#[test]
fn type_error_is_rejected_and_nothing_persisted() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let candidate =
parse("fn broken(x :: Int) -> Int { not_defined(x) }\n");
let (op, t) = fac_op_and_transition();
let expected_op_id = op.op_id();
let err = check_and_apply(&log, None, op, t, &candidate)
.expect_err("expected TypeError");
match err {
GateError::TypeError { op_id, errors } => {
assert_eq!(op_id, expected_op_id);
assert!(!errors.is_empty(), "expected at least one TypeError");
}
other => panic!("expected TypeError, got {other:?}"),
}
assert!(log.get(&expected_op_id).unwrap().is_none());
}
#[test]
fn arity_mismatch_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let candidate = parse(
"fn add(x :: Int, y :: Int) -> Int { x + y }\nfn caller() -> Int { add(1) }\n",
);
let (op, t) = fac_op_and_transition();
let err = check_and_apply(&log, None, op, t, &candidate)
.expect_err("expected TypeError");
assert!(matches!(err, GateError::TypeError { .. }));
}
#[test]
fn parent_check_still_runs_when_program_is_clean() {
let tmp = tempfile::tempdir().unwrap();
let log = OpLog::open(tmp.path()).unwrap();
let candidate = parse(
"fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n",
);
let (op1, t1) = fac_op_and_transition();
let head1 = check_and_apply(&log, None, op1, t1, &candidate).unwrap();
let bogus = Operation::new(
OperationKind::ModifyBody {
sig_id: "fac".into(),
from_stage_id: "s1".into(),
to_stage_id: "s2".into(),
},
["someone-else".into()],
);
let t = StageTransition::Replace {
sig_id: "fac".into(),
from: "s1".into(),
to: "s2".into(),
};
let err = check_and_apply(&log, Some(&head1.op_id), bogus, t, &candidate)
.expect_err("expected Apply(StaleParent)");
match err {
GateError::Apply(ApplyError::StaleParent { .. }) => {}
other => panic!("expected Apply(StaleParent), got {other:?}"),
}
}
}