1use std::collections::HashMap;
6use std::io::Read;
7use std::path::PathBuf;
8
9use crate::proofs::obligations::ProverObligation;
10use crate::proofs::plans::{ObligationPlan, build_plan_from_obligations};
11use crate::proofs::tactics::{IsTactic, Tactic};
12use crate::prover::{ProverReport, ProverReportNode, TaskOutcome, run_prover_plan};
13use serde::{Deserialize, Serialize};
14use xlsynth_prover::prover::SolverChoice;
15
16pub type Selector = Vec<String>;
17
18fn root_selector() -> Selector {
19 vec!["root".to_string()]
20}
21fn join_selector(parent: &Selector, seg: &str) -> Selector {
22 let mut v = parent.clone();
23 v.push(seg.to_string());
24 v
25}
26fn selector_to_path(sel: &Selector) -> String {
27 sel.join("/")
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum NodeStatus {
32 Open,
33 Pending,
34 Solved,
35 Failed,
36}
37
38#[derive(Debug)]
39pub enum NodeKind {
40 Leaf(ProverObligation),
41 Internal {
42 #[allow(dead_code)]
43 tactic: Tactic,
44 children: Vec<OblNode>,
45 original: ProverObligation,
46 },
47}
48
49#[derive(Debug)]
50pub struct OblNode {
51 #[allow(dead_code)]
52 pub segment: String,
53 pub selector: Selector,
54 pub kind: NodeKind,
55 pub status: NodeStatus,
56 pub solve: bool,
57}
58
59impl OblNode {
60 pub fn leaf(seg: String, parent: &Selector, ob: ProverObligation) -> Self {
61 Self {
62 segment: seg.clone(),
63 selector: join_selector(parent, &seg),
64 kind: NodeKind::Leaf(ob),
65 status: NodeStatus::Open,
66 solve: false,
67 }
68 }
69}
70
71#[derive(Debug)]
72pub struct OblTreeConfig {
73 pub dslx_stdlib_path: Option<PathBuf>,
74 pub dslx_paths: Vec<PathBuf>,
75 pub solver: Option<SolverChoice>,
76 pub timeout_ms: Option<u64>,
77}
78
79#[derive(Debug)]
80pub struct OblTree {
81 pub root: OblNode,
82 pub config: OblTreeConfig,
83}
84
85impl OblTree {
86 pub fn new(mut root_obligation: ProverObligation, config: OblTreeConfig) -> Self {
87 root_obligation.selector_segment = "root".to_string();
89 let root = OblNode {
90 segment: "root".to_string(),
91 selector: root_selector(),
92 kind: NodeKind::Leaf(root_obligation),
93 status: NodeStatus::Open,
94 solve: false,
95 };
96 Self { root, config }
97 }
98
99 fn find_mut<'a>(node: &'a mut OblNode, sel: &Selector) -> Option<&'a mut OblNode> {
100 if &node.selector == sel {
101 return Some(node);
102 }
103 if let NodeKind::Internal { children, .. } = &mut node.kind {
104 for c in children.iter_mut() {
105 if let Some(found) = Self::find_mut(c, sel) {
106 return Some(found);
107 }
108 }
109 }
110 None
111 }
112
113 }
116
117#[derive(Clone, Debug, Serialize, Deserialize)]
118pub enum Command {
119 Apply(Tactic),
120 Solve,
121}
122
123#[derive(Clone, Debug, Serialize, Deserialize)]
124pub struct ScriptStep {
125 pub selector: Selector,
126 pub command: Command,
127}
128
129fn apply_tactic(
130 node: &mut OblNode,
131 parent_selector: &Selector,
132 tactic: Tactic,
133) -> Result<(), String> {
134 let base = match &node.kind {
135 NodeKind::Leaf(ob) => ob.clone(),
136 _ => return Err("tactic requires a leaf".to_string()),
137 };
138 let children_obs = tactic.apply(&base)?;
139 let mut children: Vec<OblNode> = Vec::new();
140 for (idx, ob) in children_obs.into_iter().enumerate() {
141 if ob.selector_segment.is_empty() {
142 return Err(format!(
143 "tactic produced child {} with empty selector segment",
144 idx + 1
145 ));
146 }
147 let seg = ob.selector_segment.clone();
148 children.push(OblNode::leaf(seg, parent_selector, ob));
149 }
150 node.kind = NodeKind::Internal {
151 tactic,
152 children,
153 original: base,
154 };
155 Ok(())
156}
157
158fn collect_solve_targets(node: &OblNode, out: &mut Vec<(Selector, ProverObligation)>) {
159 match &node.kind {
160 NodeKind::Leaf(ob) => {
161 if node.solve {
162 out.push((node.selector.clone(), ob.clone()));
163 }
164 }
165 NodeKind::Internal { children, .. } => {
166 for c in children.iter() {
167 collect_solve_targets(c, out);
168 }
169 }
170 }
171}
172
173#[derive(Clone, Debug)]
174enum TaskSimple {
175 Success,
176 Failure,
177 Indefinite,
178}
179
180fn collect_outcomes(report: &ProverReportNode) -> HashMap<String, TaskSimple> {
181 let mut out: HashMap<String, TaskSimple> = HashMap::new();
182 match report {
183 ProverReportNode::Task {
184 task_id, outcome, ..
185 } => {
186 if let (Some(tid), Some(outcome_ref)) = (task_id, outcome) {
187 let simple = match outcome_ref {
188 TaskOutcome::Normal { success: true } => TaskSimple::Success,
189 TaskOutcome::Normal { success: false } => TaskSimple::Failure,
190 TaskOutcome::Indefinite { .. } => TaskSimple::Indefinite,
191 };
192 out.insert(tid.clone(), simple);
193 }
194 }
195 ProverReportNode::Group { tasks, .. } => {
196 for t in tasks {
197 out.extend(collect_outcomes(t));
198 }
199 }
200 }
201 out
202}
203
204fn update_from_outcome(
205 tree: &mut OblTree,
206 report: &ProverReport,
207 sel: Selector,
208 path: &str,
209 status: Option<TaskSimple>,
210 solved: &mut Vec<Selector>,
211 failed_logs: &mut Vec<TaskLogEntry>,
212 indefinite_logs: &mut Vec<TaskLogEntry>,
213 rolled_back: &mut Vec<RollbackEntry>,
214) {
215 match status {
216 Some(TaskSimple::Success) => {
217 if let Some(n) = OblTree::find_mut(&mut tree.root, &sel) {
218 n.status = NodeStatus::Solved;
219 }
220 let _ = report.plan.find_task_node(path);
221 solved.push(sel);
222 }
223 Some(TaskSimple::Failure) => {
224 if let Some(n) = OblTree::find_mut(&mut tree.root, &sel) {
225 n.status = NodeStatus::Failed;
226 }
227 if let Some(task_node) = report.plan.find_task_node(path) {
228 if let ProverReportNode::Task {
229 task_id,
230 cmdline,
231 stdout,
232 stderr,
233 ..
234 } = task_node
235 {
236 println!(
237 "FAILED TASK id={:?}\ncmd={}\nstdout=\n{}\nstderr=\n{}\n",
238 task_id,
239 cmdline.as_deref().unwrap_or(""),
240 stdout.as_deref().unwrap_or(""),
241 stderr.as_deref().unwrap_or("")
242 );
243 failed_logs.push(TaskLogEntry {
244 selector: sel.clone(),
245 stdout: stdout.clone(),
246 stderr: stderr.clone(),
247 });
248 }
249 }
250 if sel.len() > 1 {
251 let parent_sel: Selector = sel[..sel.len() - 1].to_vec();
252 if let Some(parent) = OblTree::find_mut(&mut tree.root, &parent_sel) {
253 if let NodeKind::Internal { original, .. } = &parent.kind {
254 let original_ob = original.clone();
255 parent.kind = NodeKind::Leaf(original_ob);
256 parent.status = NodeStatus::Open;
257 parent.solve = false;
258 }
259 rolled_back.push(RollbackEntry {
260 selector: parent_sel,
261 reason_selector: sel,
262 });
263 }
264 }
265 }
266 Some(TaskSimple::Indefinite) => {
267 if let Some(n) = OblTree::find_mut(&mut tree.root, &sel) {
268 n.status = NodeStatus::Pending;
269 }
270 if let Some(task_node) = report.plan.find_task_node(path) {
271 if let ProverReportNode::Task { stdout, stderr, .. } = task_node {
272 indefinite_logs.push(TaskLogEntry {
273 selector: sel,
274 stdout: stdout.clone(),
275 stderr: stderr.clone(),
276 });
277 }
278 }
279 }
280 None => {
281 if let Some(n) = OblTree::find_mut(&mut tree.root, &sel) {
282 n.status = NodeStatus::Pending;
283 }
284 }
285 }
286}
287
288#[derive(Clone, Debug, Serialize, Deserialize)]
289pub struct TaskLogEntry {
290 pub selector: Selector,
291 pub stdout: Option<String>,
292 pub stderr: Option<String>,
293}
294
295#[derive(Clone, Debug, Serialize, Deserialize)]
296pub struct RollbackEntry {
297 pub selector: Selector,
298 pub reason_selector: Selector,
299}
300
301#[derive(Clone, Debug, Serialize, Deserialize)]
302pub struct ScriptReport {
303 pub solved: Vec<Selector>,
304 pub indefinite: Vec<TaskLogEntry>,
305 pub failed: Vec<TaskLogEntry>,
306 pub rolled_back: Vec<RollbackEntry>,
307}
308
309pub fn parse_script_steps_from_json_str(input: &str) -> Result<Vec<ScriptStep>, String> {
311 serde_json::from_str::<Vec<ScriptStep>>(input)
312 .map_err(|e| format!("tactic script: expected JSON array of ScriptStep: {}", e))
313}
314
315pub fn parse_script_steps_from_jsonl_str(input: &str) -> Result<Vec<ScriptStep>, String> {
318 let mut steps: Vec<ScriptStep> = Vec::new();
319 for (lineno, line) in input.lines().enumerate() {
320 let line = line.trim();
321 if line.is_empty() {
322 continue;
323 }
324 if line.starts_with('#') {
325 continue;
326 }
327 match serde_json::from_str::<ScriptStep>(line) {
328 Ok(step) => steps.push(step),
329 Err(e) => {
330 return Err(format!(
331 "tactic script (jsonl) parse error on line {}: {}",
332 lineno + 1,
333 e
334 ));
335 }
336 }
337 }
338 Ok(steps)
339}
340
341fn read_script_text_from_path(path: &str, kind: &str) -> Result<String, String> {
344 if path == "-" {
345 let mut buf = String::new();
346 std::io::stdin()
347 .read_to_string(&mut buf)
348 .map_err(|e| format!("failed to read tactic script ({} ) from stdin: {}", kind, e))?;
349 Ok(buf)
350 } else {
351 std::fs::read_to_string(path)
352 .map_err(|e| format!("failed to read tactic script ({}) {}: {}", kind, path, e))
353 }
354}
355
356pub fn read_script_steps_from_json_path(path: &str) -> Result<Vec<ScriptStep>, String> {
358 let text = read_script_text_from_path(path, "json")?;
359 parse_script_steps_from_json_str(&text)
360}
361
362pub fn read_script_steps_from_jsonl_path(path: &str) -> Result<Vec<ScriptStep>, String> {
364 let text = read_script_text_from_path(path, "jsonl")?;
365 parse_script_steps_from_jsonl_str(&text)
366}
367
368pub fn execute_script(tree: &mut OblTree, steps: &[ScriptStep]) -> Result<ScriptReport, String> {
369 for step in steps {
371 match &step.command {
372 Command::Solve => {
373 let node = OblTree::find_mut(&mut tree.root, &step.selector).ok_or_else(|| {
374 format!("selector not found: {}", selector_to_path(&step.selector))
375 })?;
376 if !matches!(node.kind, NodeKind::Leaf(_)) {
377 return Err("solve requires a leaf".to_string());
378 }
379 node.solve = true;
380 }
381 Command::Apply(tactic) => {
382 let node = OblTree::find_mut(&mut tree.root, &step.selector).ok_or_else(|| {
383 format!("selector not found: {}", selector_to_path(&step.selector))
384 })?;
385 apply_tactic(node, &step.selector, tactic.clone())?;
386 }
387 }
388 }
389
390 let mut targets: Vec<(Selector, ProverObligation)> = Vec::new();
392 collect_solve_targets(&tree.root, &mut targets);
393 let mut obligations_with_ids: Vec<(String, ProverObligation)> = Vec::new();
394 for (sel, ob) in targets.iter() {
395 let path = selector_to_path(sel);
396 obligations_with_ids.push((path, ob.clone()));
397 }
398
399 log::debug!("obligations_to_solve={}", obligations_with_ids.len());
400
401 if obligations_with_ids.is_empty() {
402 return Ok(ScriptReport {
403 solved: vec![],
404 indefinite: vec![],
405 failed: vec![],
406 rolled_back: vec![],
407 });
408 }
409
410 let stdlib = tree.config.dslx_stdlib_path.as_deref();
414 let paths: Vec<&std::path::Path> = tree.config.dslx_paths.iter().map(|p| p.as_path()).collect();
415 let plan_bundle: ObligationPlan = build_plan_from_obligations(
416 &obligations_with_ids,
417 stdlib,
418 &paths,
419 tree.config.solver.clone(),
420 tree.config.timeout_ms,
421 )?;
422 let (plan, tempdir) = plan_bundle.into_parts();
423 let _keep_tempdir = tempdir;
424 let cores = std::thread::available_parallelism()
425 .map(|n| n.get())
426 .unwrap_or(1);
427 let report: ProverReport = run_prover_plan(plan, cores).map_err(|e| e.to_string())?;
428
429 let outcomes: HashMap<String, TaskSimple> = collect_outcomes(&report.plan);
431
432 let mut solved: Vec<Selector> = Vec::new();
434 let mut failed_logs: Vec<TaskLogEntry> = Vec::new();
435 let mut indefinite_logs: Vec<TaskLogEntry> = Vec::new();
436 let mut rolled_back: Vec<RollbackEntry> = Vec::new();
437
438 for (sel, _) in targets.into_iter() {
439 let path = selector_to_path(&sel);
440 let status = outcomes.get(&path).cloned();
441 update_from_outcome(
442 tree,
443 &report,
444 sel,
445 &path,
446 status,
447 &mut solved,
448 &mut failed_logs,
449 &mut indefinite_logs,
450 &mut rolled_back,
451 );
452 }
453
454 Ok(ScriptReport {
455 solved,
456 indefinite: indefinite_logs,
457 failed: failed_logs,
458 rolled_back,
459 })
460}
461
462#[cfg(test)]
463#[cfg(feature = "has-bitwuzla")]
464mod tests {
465 use super::*;
466 use crate::proofs::obligations::{
467 FileWithHistory, LecObligation, ObligationPayload, ProverObligation, SourceFile,
468 };
469 use crate::proofs::tactics::cosliced::{CoslicedTactic, NamedSlice};
470
471 fn sel(parts: &[&str]) -> Selector {
472 parts.iter().map(|s| s.to_string()).collect()
473 }
474
475 fn base_obligation() -> ProverObligation {
476 use crate::proofs::obligations::LecSide;
477 let dslx = "pub fn f(x: u8) -> u8 { x + u8:3 }".to_string();
478 let lhs = LecSide {
479 top_func: "f".to_string(),
480 uf_map: std::collections::BTreeMap::new(),
481 file: FileWithHistory {
482 base_source: SourceFile::Text(dslx.clone()),
483 edits: Vec::new(),
484 text: dslx.clone(),
485 },
486 };
487 let rhs = LecSide {
488 top_func: "f".to_string(),
489 uf_map: std::collections::BTreeMap::new(),
490 file: FileWithHistory {
491 base_source: SourceFile::Text(dslx.clone()),
492 edits: Vec::new(),
493 text: dslx,
494 },
495 };
496 ProverObligation {
497 selector_segment: "root".to_string(),
498 description: None,
499 payload: ObligationPayload::Lec(LecObligation { lhs, rhs }),
500 }
501 }
502
503 fn base_config() -> OblTreeConfig {
504 let mut cfg = OblTreeConfig {
505 dslx_stdlib_path: None,
506 dslx_paths: vec![],
507 solver: None,
508 timeout_ms: None,
509 };
510 cfg.solver = Some(SolverChoice::Bitwuzla);
511 cfg
512 }
513
514 #[test]
515 fn cosliced_then_solve_all_succeeds() {
516 let mut tree = OblTree::new(base_obligation(), base_config());
517 let s1 = vec![
518 NamedSlice {
519 func_name: "f_slice_1".to_string(),
520 code: SourceFile::Text("pub fn f_slice_1(x: u8) -> u8 { x + u8:1 }".to_string()),
521 },
522 NamedSlice {
523 func_name: "f_slice_2".to_string(),
524 code: SourceFile::Text("pub fn f_slice_2(x: u8) -> u8 { x + u8:2 }".to_string()),
525 },
526 ];
527 let s2 = vec![
528 NamedSlice {
529 func_name: "f_slice_1".to_string(),
530 code: SourceFile::Text("pub fn f_slice_1(x: u8) -> u8 { x + u8:1 }".to_string()),
531 },
532 NamedSlice {
533 func_name: "f_slice_2".to_string(),
534 code: SourceFile::Text("pub fn f_slice_2(x: u8) -> u8 { x + u8:2 }".to_string()),
535 },
536 ];
537 let c1 = NamedSlice {
538 func_name: "f_composed".to_string(),
539 code: SourceFile::Text(
540 "pub fn f_composed(x: u8) -> u8 { f_slice_2(f_slice_1(x)) }".to_string(),
541 ),
542 };
543 let c2 = NamedSlice {
544 func_name: "f_composed".to_string(),
545 code: SourceFile::Text(
546 "pub fn f_composed(x: u8) -> u8 { f_slice_2(f_slice_1(x)) }".to_string(),
547 ),
548 };
549 let mut steps = Vec::new();
550 steps.push(ScriptStep {
551 selector: root_selector(),
552 command: Command::Apply(Tactic::Cosliced(CoslicedTactic {
553 lhs_slices: s1,
554 rhs_slices: s2,
555 lhs_composed: c1,
556 rhs_composed: c2,
557 })),
558 });
559 for seg in ["lhs_self", "rhs_self", "slice_1", "slice_2", "skeleton"] {
561 steps.push(ScriptStep {
562 selector: sel(&["root", seg]).clone(),
563 command: Command::Solve,
564 });
565 }
566
567 execute_script(&mut tree, &steps).expect("execute_script");
568 fn walk(
570 n: &OblNode,
571 solved: &mut Vec<Selector>,
572 failed: &mut Vec<Selector>,
573 pending: &mut Vec<Selector>,
574 ) {
575 match n.status {
576 NodeStatus::Solved => solved.push(n.selector.clone()),
577 NodeStatus::Failed => failed.push(n.selector.clone()),
578 NodeStatus::Pending => pending.push(n.selector.clone()),
579 _ => {}
580 }
581 if let NodeKind::Internal { children, .. } = &n.kind {
582 for c in children {
583 walk(c, solved, failed, pending);
584 }
585 }
586 }
587 let mut solved = Vec::new();
588 let mut failed = Vec::new();
589 let mut pending = Vec::new();
590 walk(&tree.root, &mut solved, &mut failed, &mut pending);
591 assert!(failed.is_empty(), "some tasks failed: {:?}", failed);
592 assert!(pending.is_empty(), "some tasks pending: {:?}", pending);
593 let solved_paths: std::collections::HashSet<String> =
594 solved.iter().map(|s| selector_to_path(s)).collect();
595 for seg in ["lhs_self", "rhs_self", "slice_1", "slice_2", "skeleton"] {
596 if !solved_paths.contains(&format!("root/{}", seg)) {
597 println!("missing solved {}", seg);
598 }
599 assert!(
600 solved_paths.contains(&format!("root/{}", seg)),
601 "missing solved {}",
602 seg
603 );
604 }
605 }
606}