use lex_ast::{canonicalize_program, sig_id, stage_id, CExpr, CLit, NodeId, Stage};
use lex_store::{Store, DEFAULT_BRANCH};
use lex_syntax::parse_source;
use tempfile::TempDir;
fn fresh() -> (Store, TempDir) {
let tmp = TempDir::new().unwrap();
let s = Store::open(tmp.path()).unwrap();
(s, tmp)
}
fn stage_named(src: &str, name: &str) -> Stage {
let prog = parse_source(src).unwrap();
canonicalize_program(&prog)
.into_iter()
.find(|s| match s {
Stage::FnDecl(fd) => fd.name == name,
_ => false,
})
.expect("stage not found")
}
const PICK_SRC: &str = "fn pick(n :: Int) -> Int { match n { 0 => 1, _ => 2 } }\n";
fn pick_match_node() -> NodeId {
NodeId("n_0.2".into())
}
fn publish_initial_pick(store: &Store) -> (String, String) {
let s = stage_named(PICK_SRC, "pick");
let sig = sig_id(&s).unwrap();
let stg = stage_id(&s).unwrap();
store.publish(&s).unwrap();
let op = lex_vcs::Operation::new(
lex_vcs::OperationKind::AddFunction {
sig_id: sig.clone(),
stage_id: stg.clone(),
effects: Default::default(),
budget_cost: None,
},
[],
);
let t = lex_vcs::StageTransition::Create {
sig_id: sig.clone(),
stage_id: stg.clone(),
};
store.apply_operation(DEFAULT_BRANCH, op, t).unwrap();
(sig, stg)
}
#[test]
fn replace_match_arm_lands_a_typed_op_on_the_branch() {
let (store, _tmp) = fresh();
let (sig, from_stage_id) = publish_initial_pick(&store);
let new_body = CExpr::Literal { value: CLit::Int { value: 42 } };
let op_id = store
.apply_replace_match_arm(DEFAULT_BRANCH, &from_stage_id, &pick_match_node(), 0, new_body)
.expect("transform should apply cleanly on well-typed input");
let log = lex_vcs::OpLog::open(store.root()).unwrap();
let rec = log.get(&op_id).unwrap().unwrap();
let lex_vcs::OperationKind::ReplaceMatchArm {
sig_id: rec_sig,
from_stage_id: rec_from,
arm_index,
match_node,
..
} = &rec.op.kind else {
panic!("expected ReplaceMatchArm op kind, got {:?}", rec.op.kind);
};
assert_eq!(rec_sig, &sig);
assert_eq!(rec_from, &from_stage_id);
assert_eq!(*arm_index, 0);
assert_eq!(match_node, "n_0.2");
let head = store.branch_head(DEFAULT_BRANCH).unwrap();
assert_ne!(head.get(&sig), Some(&from_stage_id));
}
#[test]
fn replace_match_arm_get_ast_reconstructs_new_body() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial_pick(&store);
let new_body = CExpr::Literal { value: CLit::Int { value: 7 } };
let op_id = store
.apply_replace_match_arm(DEFAULT_BRANCH, &from_stage_id, &pick_match_node(), 0, new_body)
.unwrap();
let log = lex_vcs::OpLog::open(store.root()).unwrap();
let rec = log.get(&op_id).unwrap().unwrap();
let lex_vcs::OperationKind::ReplaceMatchArm { to_stage_id, .. } = &rec.op.kind else {
panic!();
};
let reloaded = store.get_ast(to_stage_id).unwrap();
let Stage::FnDecl(fd) = reloaded else { panic!() };
let CExpr::Match { arms, .. } = fd.body else { panic!() };
assert!(matches!(arms[0].body, CExpr::Literal { value: CLit::Int { value: 7 } }));
assert!(matches!(arms[1].body, CExpr::Literal { value: CLit::Int { value: 2 } }));
}
#[test]
fn replace_match_arm_emits_typecheck_attestation() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial_pick(&store);
let new_body = CExpr::Literal { value: CLit::Int { value: 99 } };
let op_id = store
.apply_replace_match_arm(DEFAULT_BRANCH, &from_stage_id, &pick_match_node(), 0, new_body)
.unwrap();
let log = lex_vcs::OpLog::open(store.root()).unwrap();
let rec = log.get(&op_id).unwrap().unwrap();
let lex_vcs::OperationKind::ReplaceMatchArm { to_stage_id, .. } = &rec.op.kind else {
panic!();
};
let attlog = store.attestation_log().unwrap();
let by_stage = attlog.list_for_stage(to_stage_id).unwrap();
assert!(
by_stage.iter().any(|a| matches!(a.kind, lex_vcs::AttestationKind::TypeCheck)),
"expected a TypeCheck attestation against the new stage"
);
}
#[test]
fn replace_match_arm_rejects_ill_typed_result() {
let (store, _tmp) = fresh();
let (sig, from_stage_id) = publish_initial_pick(&store);
let head_before = store.branch_head(DEFAULT_BRANCH).unwrap();
let ill_typed = CExpr::Literal { value: CLit::Str { value: "not an int".into() } };
let err = store
.apply_replace_match_arm(DEFAULT_BRANCH, &from_stage_id, &pick_match_node(), 0, ill_typed)
.unwrap_err();
assert!(matches!(err, lex_store::StoreError::TypeError(_)),
"expected TypeError, got {err:?}");
let head_after = store.branch_head(DEFAULT_BRANCH).unwrap();
assert_eq!(head_after.get(&sig), head_before.get(&sig),
"branch head must be unchanged after a rejected transform");
}
#[test]
fn replace_match_arm_surface_transform_errors() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial_pick(&store);
let err = store
.apply_replace_match_arm(
DEFAULT_BRANCH, &from_stage_id, &pick_match_node(),
99, CExpr::Literal { value: CLit::Int { value: 0 } },
)
.unwrap_err();
assert!(matches!(err, lex_store::StoreError::TransformError(_)),
"expected TransformError, got {err:?}");
}
#[test]
fn replace_match_arm_no_op_is_rejected() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial_pick(&store);
let same = CExpr::Literal { value: CLit::Int { value: 1 } };
let err = store
.apply_replace_match_arm(DEFAULT_BRANCH, &from_stage_id, &pick_match_node(), 0, same)
.unwrap_err();
assert!(matches!(err, lex_store::StoreError::InvalidTransition(_)),
"expected InvalidTransition for no-op transform, got {err:?}");
}