use lex_ast::{
canonicalize_program, sig_id, stage_id, CExpr, ExtractFnSpec,
NodeId, Param, Stage, TypeExpr,
};
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 COMBINE_SRC: &str = "fn combine(n :: Int, m :: Int) -> Int { (n * 2) + m }\n";
fn publish_initial(store: &Store) -> (String, String) {
let s = stage_named(COMBINE_SRC, "combine");
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)
}
fn double_n_spec() -> ExtractFnSpec {
ExtractFnSpec {
name: "double_n".into(),
type_params: Vec::new(),
params: vec![Param {
name: "n".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}],
effects: Vec::new(),
return_type: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
}
}
#[test]
fn extract_function_emits_two_linked_ops() {
let (store, _tmp) = fresh();
let (source_sig, from_stage_id) = publish_initial(&store);
let (add_op, modify_op) = store
.apply_extract_function(
DEFAULT_BRANCH,
&from_stage_id,
&NodeId("n_0.3.0".into()),
double_n_spec(),
)
.expect("extract should succeed on well-typed input");
let log = lex_vcs::OpLog::open(store.root()).unwrap();
let add_rec = log.get(&add_op).unwrap().unwrap();
let mod_rec = log.get(&modify_op).unwrap().unwrap();
let lex_vcs::OperationKind::AddFunction { sig_id: add_sig, .. } = &add_rec.op.kind else {
panic!("expected AddFunction, got {:?}", add_rec.op.kind);
};
assert_ne!(add_sig, &source_sig, "new fn has its own sig");
let lex_vcs::OperationKind::ModifyBody { sig_id: mod_sig, .. } = &mod_rec.op.kind else {
panic!("expected ModifyBody, got {:?}", mod_rec.op.kind);
};
assert_eq!(mod_sig, &source_sig);
let intent_a = add_rec.op.intent_id.as_deref().expect("add op has intent");
let intent_b = mod_rec.op.intent_id.as_deref().expect("modify op has intent");
assert_eq!(intent_a, intent_b, "both ops share the synthetic Intent");
}
#[test]
fn extract_function_intent_carries_structured_prompt() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial(&store);
let (add_op, _modify_op) = store
.apply_extract_function(
DEFAULT_BRANCH,
&from_stage_id,
&NodeId("n_0.3.0".into()),
double_n_spec(),
)
.unwrap();
let log = lex_vcs::OpLog::open(store.root()).unwrap();
let rec = log.get(&add_op).unwrap().unwrap();
let intent_id = rec.op.intent_id.as_deref().unwrap().to_string();
let intent_log = lex_vcs::IntentLog::open(store.root()).unwrap();
let intent = intent_log.get(&intent_id).unwrap().unwrap();
assert!(intent.prompt.starts_with("[lex.transform.extract_function]"));
assert!(intent.prompt.contains("new_fn=double_n"));
assert!(intent.prompt.contains("expr_node=n_0.3.0"));
}
#[test]
fn extract_function_branch_state_is_consistent_after_both_ops() {
let (store, _tmp) = fresh();
let (source_sig, _from_stage_id) = publish_initial(&store);
let (_add, _modify) = store
.apply_extract_function(
DEFAULT_BRANCH,
&_from_stage_id,
&NodeId("n_0.3.0".into()),
double_n_spec(),
)
.unwrap();
let head = store.branch_head(DEFAULT_BRANCH).unwrap();
assert_eq!(head.len(), 2, "two sigs on branch head, got {head:?}");
assert!(head.contains_key(&source_sig));
assert!(head.keys().any(|k| k != &source_sig), "new sig present");
let modified_source_id = head.get(&source_sig).unwrap();
let ast = store.get_ast(modified_source_id).unwrap();
let Stage::FnDecl(fd) = ast else { panic!() };
let CExpr::BinOp { lhs, .. } = fd.body else { panic!() };
let CExpr::Call { callee, args } = *lhs else { panic!() };
assert!(matches!(*callee, CExpr::Var { name: ref n } if n == "double_n"));
assert_eq!(args.len(), 1);
}
#[test]
fn extract_function_surfaces_transform_errors() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial(&store);
let err = store
.apply_extract_function(
DEFAULT_BRANCH,
&from_stage_id,
&NodeId("n_0.99".into()),
double_n_spec(),
)
.unwrap_err();
assert!(matches!(err, lex_store::StoreError::TransformError(_)),
"got {err:?}");
}
#[test]
fn extract_function_surfaces_param_mismatch() {
let (store, _tmp) = fresh();
let (_sig, from_stage_id) = publish_initial(&store);
let mut spec = double_n_spec();
spec.params.push(Param {
name: "z".into(),
ty: TypeExpr::Named { name: "Int".into(), args: Vec::new() },
});
let err = store
.apply_extract_function(
DEFAULT_BRANCH,
&from_stage_id,
&NodeId("n_0.3.0".into()),
spec,
)
.unwrap_err();
assert!(matches!(err, lex_store::StoreError::TransformError(
lex_ast::TransformError::ExtractFnRefused { .. }
)), "got {err:?}");
}