1use std::collections::{BTreeSet, HashMap};
17
18use serde::Serialize;
19
20use crate::vm::{disassemble, CompiledProgram, DebugVmState, DisasmLine, Vm, VmStep};
21
22const STEP_LIMIT: usize = 5_000_000;
25
26#[derive(Clone, Serialize)]
29pub struct DebugReg {
30 pub index: u16,
31 pub name: Option<String>,
32 pub kind: String,
34 pub value: String,
35 pub changed: bool,
37}
38
39#[derive(Clone, Serialize)]
42pub struct DebugFrame {
43 pub function: Option<String>,
44 pub base: usize,
45 pub registers: Vec<DebugReg>,
46}
47
48#[derive(Clone, Serialize, Debug)]
53pub struct HeapObject {
54 pub id: String,
56 pub kind: String,
57 pub summary: String,
58 pub storage: String,
61 pub rc: usize,
63 pub referenced_by: Vec<String>,
64 pub shared: bool,
66}
67
68#[derive(Clone, Serialize)]
70pub struct DebugSnapshot {
71 pub pc: usize,
73 pub op_text: String,
75 pub narration: String,
78 pub fol: String,
82 pub socratic: String,
87 pub op_reads: Vec<u16>,
89 pub op_writes: Option<u16>,
90 pub state: String,
92 pub error: Option<String>,
93 pub step: usize,
95 pub total_steps: usize,
97 pub total_ops: usize,
99 pub frames: Vec<DebugFrame>,
101 pub heap: Vec<HeapObject>,
103 pub globals: Vec<(String, String)>,
105 pub output: Vec<String>,
107 pub at_breakpoint: bool,
109}
110
111#[derive(Clone, Serialize)]
115pub struct VarTrace {
116 pub reg: u16,
118 pub name: String,
119 pub kind: String,
121 pub points: Vec<TimelinePoint>,
122}
123
124#[derive(Clone, Serialize)]
127pub struct TimelinePoint {
128 pub value: String,
129 pub present: bool,
132 pub changed: bool,
134}
135
136#[derive(Clone, Serialize)]
140pub struct VarTimeline {
141 pub start: usize,
143 pub steps: usize,
145 pub cursor: usize,
147 pub truncated: bool,
149 pub vars: Vec<VarTrace>,
150}
151
152#[derive(Clone, Serialize)]
156pub struct VarInsight {
157 pub name: String,
158 pub kind: String,
159 pub facts: Vec<String>,
160}
161
162#[derive(Clone, Serialize, Default)]
167pub struct ProvenFacts {
168 pub scalar: Option<String>,
169 pub int_range: Option<(i64, i64)>,
170 pub nonneg: bool,
171}
172
173impl From<crate::optimize::VarProvenFacts> for ProvenFacts {
174 fn from(v: crate::optimize::VarProvenFacts) -> Self {
175 ProvenFacts {
176 scalar: v.scalar.map(|s| format!("{s:?}")),
177 int_range: v.int_range,
178 nonneg: v.nonneg,
179 }
180 }
181}
182
183impl ProvenFacts {
184 fn labels(&self) -> Vec<String> {
187 let mut out = Vec::new();
188 match self.int_range {
189 Some((lo, hi)) => out.push(format!("\u{2208} [{lo}, {hi}]")),
190 None if self.nonneg => out.push("\u{2265} 0".to_string()),
191 None => {}
192 }
193 if let Some(s) = &self.scalar {
194 out.push(format!("type {s}"));
195 }
196 out
197 }
198}
199
200#[derive(Clone, Serialize)]
202pub struct ProvenInsight {
203 pub name: String,
204 pub facts: Vec<String>,
205}
206
207#[derive(Clone, Serialize, PartialEq, Debug)]
211pub enum ProofVerdict {
212 ProvenTrue,
213 ProvenFalse,
214 Unknown,
215}
216
217#[derive(Clone, Serialize)]
223pub struct AssertionResult {
224 pub query: String,
225 pub parsed: bool,
227 pub now: Option<bool>,
229 pub now_detail: String,
230 pub verdict: ProofVerdict,
232 pub verdict_detail: String,
233}
234
235enum Operand {
237 Var(String),
238 Int(i64),
239}
240
241#[derive(Clone, Copy)]
242enum Cmp {
243 Lt,
244 Le,
245 Gt,
246 Ge,
247 Eq,
248 Ne,
249}
250
251#[derive(Clone, Serialize)]
256pub struct CausalNode {
257 pub step: usize,
259 pub pc: usize,
261 pub op_text: String,
262 pub narration: String,
263 pub reg: u16,
265 pub name: Option<String>,
266 pub kind: String,
267 pub value: String,
268 pub inputs: Vec<CausalNode>,
270}
271
272const TIMELINE_MAX_STEPS: usize = 512;
275
276const PROVENANCE_MAX_DEPTH: usize = 24;
279const PROVENANCE_MAX_NODES: usize = 96;
280
281#[derive(Clone)]
285enum Outcome {
286 Running,
287 Done,
288 Blocked,
289 Error(String),
290}
291
292struct Frame {
294 state: DebugVmState,
295 outcome: Outcome,
296}
297
298pub struct Debugger {
306 program: CompiledProgram,
307 disasm: Vec<DisasmLine>,
308 history: Vec<Frame>,
309 cursor: usize,
310 breakpoints: BTreeSet<usize>,
311 proven: HashMap<String, ProvenFacts>,
315}
316
317impl Debugger {
318 pub fn from_source(src: &str) -> Result<Debugger, String> {
322 let (program, proven) = compile_source(src)?;
323 let disasm = disassemble(&program);
324 let initial = Vm::new(&program).save_debug_state();
325 Ok(Debugger {
326 program,
327 disasm,
328 history: vec![Frame { state: initial, outcome: Outcome::Running }],
329 cursor: 0,
330 breakpoints: BTreeSet::new(),
331 proven,
332 })
333 }
334
335 pub fn step(&mut self) {
337 self.run_one();
338 }
339
340 pub fn step_over(&mut self) {
342 let start = self.current_depth();
343 if !self.run_one() {
344 return;
345 }
346 let mut budget = STEP_LIMIT;
347 while self.is_paused() && self.current_depth() > start {
348 if self.at_breakpoint() {
349 break;
350 }
351 if !self.run_one() {
352 break;
353 }
354 budget -= 1;
355 if budget == 0 {
356 break;
357 }
358 }
359 }
360
361 pub fn step_out(&mut self) {
363 let start = self.current_depth();
364 let mut budget = STEP_LIMIT;
365 loop {
366 if !self.is_paused() || self.current_depth() < start {
367 break;
368 }
369 if !self.run_one() {
370 break;
371 }
372 if self.at_breakpoint() {
373 break;
374 }
375 budget -= 1;
376 if budget == 0 {
377 break;
378 }
379 }
380 }
381
382 pub fn resume(&mut self) {
384 let mut budget = STEP_LIMIT;
385 loop {
386 if !self.is_paused() {
387 break;
388 }
389 if !self.run_one() {
390 break;
391 }
392 if self.at_breakpoint() {
393 break;
394 }
395 budget -= 1;
396 if budget == 0 {
397 break;
398 }
399 }
400 }
401
402 pub fn reverse_resume(&mut self) {
406 while self.cursor > 0 {
407 self.cursor -= 1;
408 if self.breakpoints.contains(&self.current().pc()) {
409 break;
410 }
411 }
412 }
413
414 pub fn step_back(&mut self) {
416 self.cursor = self.cursor.saturating_sub(1);
417 }
418
419 pub fn seek(&mut self, step: usize) {
421 self.cursor = step.min(self.history.len().saturating_sub(1));
422 }
423
424 pub fn restart(&mut self) {
427 self.cursor = 0;
428 }
429
430 pub fn toggle_breakpoint(&mut self, pc: usize) {
432 if !self.breakpoints.remove(&pc) {
433 self.breakpoints.insert(pc);
434 }
435 }
436
437 pub fn set_breakpoint(&mut self, pc: usize) {
438 self.breakpoints.insert(pc);
439 }
440
441 pub fn clear_breakpoint(&mut self, pc: usize) {
442 self.breakpoints.remove(&pc);
443 }
444
445 pub fn breakpoints(&self) -> Vec<usize> {
446 self.breakpoints.iter().copied().collect()
447 }
448
449 pub fn disassembly(&self) -> &[DisasmLine] {
451 &self.disasm
452 }
453
454 pub fn is_running(&self) -> bool {
457 self.is_paused()
458 }
459
460 pub fn snapshot(&self) -> DebugSnapshot {
462 let (view, heap_raw) = self.view_and_heap(self.current());
463 let prev_inner: HashMap<u16, String> = if self.cursor >= 1 {
468 let pv = self.view_of(&self.history[self.cursor - 1].state);
469 if pv.frames.len() == view.frames.len() {
470 pv.frames
471 .last()
472 .map(|f| f.registers.iter().map(|(idx, _kind, val)| (*idx, val.clone())).collect())
473 .unwrap_or_default()
474 } else {
475 HashMap::new()
476 }
477 } else {
478 HashMap::new()
479 };
480 let main_names: HashMap<u16, String> =
482 self.program.reg_names.iter().cloned().collect();
483 let n = view.frames.len();
484 let frames: Vec<DebugFrame> = view
485 .frames
486 .iter()
487 .enumerate()
488 .map(|(fi, f)| {
489 let inner = fi + 1 == n;
490 DebugFrame {
491 function: f.func.map(|i| format!("fn#{i}")),
492 base: f.base,
493 registers: f
494 .registers
495 .iter()
496 .map(|(idx, kind, val)| DebugReg {
497 index: *idx,
498 name: if f.func.is_none() {
499 main_names.get(idx).cloned()
500 } else {
501 None
502 },
503 kind: kind.clone(),
504 value: val.clone(),
505 changed: inner
506 && prev_inner.get(idx).map(|p| p != val).unwrap_or(false),
507 })
508 .collect(),
509 }
510 })
511 .collect();
512 let (state, error) = match &self.cur().outcome {
513 Outcome::Running => ("paused", None),
514 Outcome::Done => ("done", None),
515 Outcome::Blocked => ("blocked", None),
516 Outcome::Error(e) => ("error", Some(e.clone())),
517 };
518 let op_text = self.disasm.get(view.pc).map(|d| d.text.clone()).unwrap_or_default();
519 let inner_regs: HashMap<u16, (Option<String>, String)> = frames
521 .last()
522 .map(|f: &DebugFrame| {
523 f.registers.iter().map(|r| (r.index, (r.name.clone(), r.value.clone()))).collect()
524 })
525 .unwrap_or_default();
526 let cur_op = self.program.code.get(view.pc).copied();
527 let (narration, op_reads, op_writes) = match (&self.cur().outcome, cur_op) {
528 (Outcome::Error(e), _) => (format!("error: {e}"), Vec::new(), None),
529 (Outcome::Done, _) => ("the program has finished".to_string(), Vec::new(), None),
530 (Outcome::Blocked, _) => ("waiting on a concurrency operation".to_string(), Vec::new(), None),
531 (Outcome::Running, Some(op)) => {
532 let io = crate::vm::op_io(&op);
533 (narrate(&op, &inner_regs, &self.program), io.reads, io.writes)
534 }
535 _ => (String::new(), Vec::new(), None),
536 };
537 let fol = match (&self.cur().outcome, cur_op) {
538 (Outcome::Running, Some(op)) => fol_of_op(&op, &inner_regs, &self.program),
539 _ => String::new(),
540 };
541 let socratic = match (&self.cur().outcome, cur_op) {
542 (Outcome::Running, Some(op)) => socratic_of_op(&op, &inner_regs),
543 (Outcome::Done, _) => "The program has finished — did the result match what you expected?".to_string(),
544 _ => String::new(),
545 };
546 let heap: Vec<HeapObject> = heap_raw
547 .iter()
548 .enumerate()
549 .map(|(i, o)| HeapObject {
550 id: format!("#{}", i + 1),
551 kind: o.kind.clone(),
552 summary: o.summary.clone(),
553 storage: o.storage.clone(),
554 rc: o.rc,
555 referenced_by: o.referenced_by.clone(),
556 shared: o.referenced_by.len() > 1,
557 })
558 .collect();
559 DebugSnapshot {
560 pc: view.pc,
561 op_text,
562 narration,
563 fol,
564 socratic,
565 op_reads,
566 op_writes,
567 state: state.to_string(),
568 error,
569 step: self.cursor,
570 total_steps: self.history.len().saturating_sub(1),
571 total_ops: self.disasm.len(),
572 frames,
573 heap,
574 globals: view.globals.clone(),
575 output: view.output.clone(),
576 at_breakpoint: self.at_breakpoint(),
577 }
578 }
579
580 pub fn variable_timeline(&self) -> VarTimeline {
585 let names = self.main_names();
586 let total = self.history.len();
587 let start = total.saturating_sub(TIMELINE_MAX_STEPS);
588 let truncated = start > 0;
589 let window = &self.history[start..];
590 let n = window.len();
591
592 let mut order: Vec<u16> = names.keys().copied().collect();
593 order.sort_unstable();
594 let mut series: HashMap<u16, Vec<Option<(String, String)>>> =
596 order.iter().map(|r| (*r, vec![None; n])).collect();
597 for (col, frame) in window.iter().enumerate() {
598 let view = self.view_of(&frame.state);
599 if let Some(main) = view.frames.iter().find(|f| f.func.is_none()) {
600 for (idx, kind, val) in &main.registers {
601 if let Some(slot) = series.get_mut(idx) {
602 slot[col] = Some((kind.clone(), val.clone()));
603 }
604 }
605 }
606 }
607
608 let vars = order
609 .iter()
610 .map(|reg| {
611 let name = names.get(reg).cloned().unwrap_or_else(|| format!("R{reg}"));
612 let raw = &series[reg];
613 let mut kind = String::new();
614 let mut points = Vec::with_capacity(n);
615 let mut prev: Option<String> = None;
616 for cell in raw {
617 match cell {
618 Some((k, v)) => {
619 if k != "Nothing" || kind.is_empty() {
622 kind = k.clone();
623 }
624 let changed = matches!(&prev, Some(p) if p != v);
627 points.push(TimelinePoint { value: v.clone(), present: true, changed });
628 prev = Some(v.clone());
629 }
630 None => {
631 points.push(TimelinePoint {
632 value: String::new(),
633 present: false,
634 changed: false,
635 });
636 prev = None;
637 }
638 }
639 }
640 VarTrace { reg: *reg, name, kind, points }
641 })
642 .filter(|t| t.points.iter().any(|p| p.present))
644 .collect();
645
646 VarTimeline { start, steps: n, cursor: self.cursor, truncated, vars }
647 }
648
649 pub fn observed_invariants(&self) -> Vec<VarInsight> {
654 let tl = self.variable_timeline();
655 tl.vars
656 .iter()
657 .filter_map(|v| {
658 let first = v.points.iter().position(|p| p.present && p.changed)?;
661 let vals: Vec<&str> =
662 v.points[first..].iter().filter(|p| p.present).map(|p| p.value.as_str()).collect();
663 if vals.is_empty() {
664 return None;
665 }
666 let distinct: BTreeSet<&str> = vals.iter().copied().collect();
667 let nums: Option<Vec<f64>> = vals.iter().map(|s| s.parse::<f64>().ok()).collect();
668 let mut facts = Vec::new();
669 if distinct.len() == 1 {
670 facts.push(format!("constant {}", vals[0]));
671 } else {
672 if let Some(ns) = &nums {
673 let min = ns.iter().cloned().fold(f64::INFINITY, f64::min);
674 let max = ns.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
675 facts.push(format!("range [{}, {}]", fmt_num(min), fmt_num(max)));
676 if ns.windows(2).all(|w| w[1] >= w[0]) {
677 facts.push("only increases".to_string());
678 } else if ns.windows(2).all(|w| w[1] <= w[0]) {
679 facts.push("only decreases".to_string());
680 }
681 }
682 facts.push(format!("{} distinct values", distinct.len()));
683 }
684 Some(VarInsight { name: v.name.clone(), kind: v.kind.clone(), facts })
685 })
686 .collect()
687 }
688
689 pub fn proven_invariants(&self) -> Vec<ProvenInsight> {
694 let mut out: Vec<ProvenInsight> = self
695 .proven
696 .iter()
697 .filter_map(|(name, pf)| {
698 let facts = pf.labels();
699 if facts.is_empty() {
700 None
701 } else {
702 Some(ProvenInsight { name: name.clone(), facts })
703 }
704 })
705 .collect();
706 out.sort_by(|a, b| a.name.cmp(&b.name));
707 out
708 }
709
710 pub fn assert_at_cursor(&self, predicate: &str) -> AssertionResult {
717 let query = predicate.trim().to_string();
718 let Some((lhs, cmp, rhs)) = parse_comparison(&query) else {
719 return AssertionResult {
720 query,
721 parsed: false,
722 now: None,
723 now_detail: "couldn't parse \u{2014} try a comparison like `x < y` or `x >= 0`".to_string(),
724 verdict: ProofVerdict::Unknown,
725 verdict_detail: String::new(),
726 };
727 };
728
729 let frame = self.snapshot();
731 let live = |o: &Operand| -> Option<i64> {
732 match o {
733 Operand::Int(c) => Some(*c),
734 Operand::Var(name) => frame
735 .frames
736 .last()
737 .and_then(|f| f.registers.iter().find(|r| r.name.as_deref() == Some(name.as_str())))
738 .and_then(|r| r.value.parse::<i64>().ok()),
739 }
740 };
741 let now = match (live(&lhs), live(&rhs)) {
742 (Some(a), Some(b)) => Some(apply_cmp(a, cmp, b)),
743 _ => None,
744 };
745 let now_detail = {
746 let mut parts = Vec::new();
747 for o in [&lhs, &rhs] {
748 if let Operand::Var(name) = o {
749 match live(o) {
750 Some(v) => parts.push(format!("{name} = {v}")),
751 None => parts.push(format!("{name} = ?")),
752 }
753 }
754 }
755 parts.join(", ")
756 };
757
758 let prange = |o: &Operand| -> Option<(i64, i64)> {
760 match o {
761 Operand::Int(c) => Some((*c, *c)),
762 Operand::Var(name) => self.proven.get(name).and_then(|pf| pf.int_range),
763 }
764 };
765 let (verdict, verdict_detail) = match (prange(&lhs), prange(&rhs)) {
766 (Some(a), Some(b)) => {
767 let v = entail(a, cmp, b);
768 let mut srcs = Vec::new();
769 if let Operand::Var(n) = &lhs {
770 srcs.push(format!("{n} \u{2208} [{}, {}]", a.0, a.1));
771 }
772 if let Operand::Var(n) = &rhs {
773 srcs.push(format!("{n} \u{2208} [{}, {}]", b.0, b.1));
774 }
775 let detail = match v {
776 ProofVerdict::Unknown => "the proven ranges don't decide it".to_string(),
777 _ => format!("from {}", srcs.join(", ")),
778 };
779 (v, detail)
780 }
781 _ => (ProofVerdict::Unknown, "no proven range for one of the terms".to_string()),
782 };
783
784 AssertionResult { query, parsed: true, now, now_detail, verdict, verdict_detail }
785 }
786
787 pub fn provenance(&self, reg: u16) -> Option<CausalNode> {
792 let mut budget = PROVENANCE_MAX_NODES;
793 self.trace_value(reg, self.cursor, PROVENANCE_MAX_DEPTH, &mut budget)
794 }
795
796 fn trace_value(&self, reg: u16, at_step: usize, depth: usize, budget: &mut usize) -> Option<CausalNode> {
800 let (name, kind, value) = self.reg_value_at(at_step, reg)?;
801 if *budget == 0 || depth == 0 {
802 return Some(CausalNode {
803 step: 0,
804 pc: 0,
805 op_text: String::new(),
806 narration: String::new(),
807 reg,
808 name,
809 kind,
810 value,
811 inputs: Vec::new(),
812 });
813 }
814 *budget -= 1;
815
816 for i in (1..=at_step).rev() {
819 let producer_pc = self.history[i - 1].state.pc();
820 let Some(op) = self.program.code.get(producer_pc).copied() else { continue };
821 let io = crate::vm::op_io(&op);
822 if io.writes != Some(reg) {
823 continue;
824 }
825 if let crate::vm::Op::Move { src, .. } = op {
830 if let Some(mut child) = self.trace_value(src, i - 1, depth, budget) {
831 if let Some((nm, k, v)) = self.reg_value_at(i, reg) {
832 child.reg = reg;
833 child.name = nm;
834 child.kind = k;
835 child.value = v;
836 }
837 return Some(child);
838 }
839 }
840 let (pname, pkind, pvalue) = self
843 .reg_value_at(i, reg)
844 .unwrap_or_else(|| (name.clone(), kind.clone(), value.clone()));
845 let inner = self.input_regs(i - 1);
846 let narration = narrate(&op, &inner, &self.program);
847 let op_text = self.disasm.get(producer_pc).map(|d| d.text.clone()).unwrap_or_default();
848 let inputs = io
849 .reads
850 .iter()
851 .filter_map(|r| self.trace_value(*r, i - 1, depth - 1, budget))
852 .collect();
853 return Some(CausalNode {
854 step: i,
855 pc: producer_pc,
856 op_text,
857 narration,
858 reg,
859 name: pname,
860 kind: pkind,
861 value: pvalue,
862 inputs,
863 });
864 }
865
866 Some(CausalNode {
868 step: 0,
869 pc: 0,
870 op_text: String::new(),
871 narration: String::new(),
872 reg,
873 name,
874 kind,
875 value,
876 inputs: Vec::new(),
877 })
878 }
879
880 fn main_names(&self) -> HashMap<u16, String> {
884 self.program.reg_names.iter().cloned().collect()
885 }
886
887 fn reg_value_at(&self, step: usize, reg: u16) -> Option<(Option<String>, String, String)> {
890 let frame = self.history.get(step)?;
891 let view = self.view_of(&frame.state);
892 let f = view.frames.last()?;
893 let is_main = f.func.is_none();
894 f.registers.iter().find(|(idx, _, _)| *idx == reg).map(|(idx, kind, val)| {
895 let name = if is_main { self.main_names().get(idx).cloned() } else { None };
896 (name, kind.clone(), val.clone())
897 })
898 }
899
900 fn input_regs(&self, step: usize) -> HashMap<u16, (Option<String>, String)> {
903 let names = self.main_names();
904 let Some(frame) = self.history.get(step) else { return HashMap::new() };
905 let view = self.view_of(&frame.state);
906 let Some(f) = view.frames.last() else { return HashMap::new() };
907 let is_main = f.func.is_none();
908 f.registers
909 .iter()
910 .map(|(idx, _, val)| {
911 let name = if is_main { names.get(idx).cloned() } else { None };
912 (*idx, (name, val.clone()))
913 })
914 .collect()
915 }
916
917 fn cur(&self) -> &Frame {
918 &self.history[self.cursor]
919 }
920
921 fn current(&self) -> &DebugVmState {
922 &self.cur().state
923 }
924
925 fn is_paused(&self) -> bool {
926 matches!(self.cur().outcome, Outcome::Running)
927 }
928
929 fn current_depth(&self) -> usize {
930 self.current().call_depth()
931 }
932
933 fn at_breakpoint(&self) -> bool {
934 self.is_paused() && self.breakpoints.contains(&self.current().pc())
935 }
936
937 fn view_of(&self, st: &DebugVmState) -> crate::vm::DebugView {
939 let mut vm = Vm::new(&self.program);
940 vm.restore_debug_state(st.clone());
941 vm.debug_view()
942 }
943
944 fn view_and_heap(
947 &self,
948 st: &DebugVmState,
949 ) -> (crate::vm::DebugView, Vec<crate::vm::HeapObjView>) {
950 let mut vm = Vm::new(&self.program);
951 vm.restore_debug_state(st.clone());
952 (vm.debug_view(), vm.debug_heap())
953 }
954
955 fn run_one(&mut self) -> bool {
960 if !self.is_paused() {
961 return false;
962 }
963 if self.cursor + 1 < self.history.len() {
964 self.cursor += 1;
965 return self.is_paused();
966 }
967 let cur_state = self.current().clone();
968 let mut vm = Vm::new(&self.program);
969 vm.restore_debug_state(cur_state.clone());
970 let frame = match vm.run_steps(1) {
971 Ok(VmStep::Paused) => Frame { state: vm.save_debug_state(), outcome: Outcome::Running },
972 Ok(VmStep::Done(_)) => Frame { state: vm.save_debug_state(), outcome: Outcome::Done },
973 Ok(VmStep::Blocked) => Frame { state: vm.save_debug_state(), outcome: Outcome::Blocked },
974 Err(e) => Frame { state: cur_state, outcome: Outcome::Error(e) },
976 };
977 self.history.push(frame);
978 self.cursor += 1;
979 self.is_paused()
980 }
981}
982
983fn narrate(
987 op: &crate::vm::Op,
988 regs: &HashMap<u16, (Option<String>, String)>,
989 prog: &CompiledProgram,
990) -> String {
991 use crate::vm::Op;
992 let name = |r: u16| regs.get(&r).and_then(|(n, _)| n.clone()).unwrap_or_else(|| format!("R{r}"));
993 let operand = |r: u16| match regs.get(&r) {
994 Some((_, v)) if !v.is_empty() => format!("{}({})", name(r), v),
995 _ => name(r),
996 };
997 match *op {
998 Op::LoadConst { dst, idx } => {
999 format!("load {} into {}", crate::vm::format_constant(prog, idx), name(dst))
1000 }
1001 Op::Move { dst, src } => format!("copy {} into {}", operand(src), name(dst)),
1002 Op::Add { dst, lhs, rhs } => format!("add {} + {} \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1003 Op::Sub { dst, lhs, rhs } => format!("subtract {} - {} \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1004 Op::Mul { dst, lhs, rhs } => format!("multiply {} \u{00d7} {} \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1005 Op::Div { dst, lhs, rhs } | Op::ExactDiv { dst, lhs, rhs } => {
1006 format!("divide {} \u{00f7} {} \u{2192} {}", operand(lhs), operand(rhs), name(dst))
1007 }
1008 Op::Mod { dst, lhs, rhs } => format!("{} mod {} \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1009 Op::Lt { dst, lhs, rhs } => format!("is {} < {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1010 Op::Gt { dst, lhs, rhs } => format!("is {} > {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1011 Op::LtEq { dst, lhs, rhs } => format!("is {} <= {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1012 Op::GtEq { dst, lhs, rhs } => format!("is {} >= {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1013 Op::Eq { dst, lhs, rhs } => format!("is {} == {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1014 Op::NotEq { dst, lhs, rhs } => format!("is {} != {}? \u{2192} {}", operand(lhs), operand(rhs), name(dst)),
1015 Op::AddAssign { dst, src } => format!("append {} onto {}", operand(src), name(dst)),
1016 Op::Not { dst, src } => format!("negate {} \u{2192} {}", operand(src), name(dst)),
1017 Op::Show { src } => format!("print {}", operand(src)),
1018 Op::Return { src } => format!("return {}", operand(src)),
1019 Op::ReturnNothing => "return".to_string(),
1020 Op::Jump { target } => format!("jump to step {target}"),
1021 Op::JumpIfFalse { cond, target } => format!("if {} is false, jump to {target}", operand(cond)),
1022 Op::JumpIfTrue { cond, target } => format!("if {} is true, jump to {target}", operand(cond)),
1023 Op::Index { dst, collection, index } | Op::IndexUnchecked { dst, collection, index } => {
1024 format!("read {}[{}] \u{2192} {}", name(collection), operand(index), name(dst))
1025 }
1026 Op::SetIndex { collection, index, value } | Op::SetIndexUnchecked { collection, index, value } => {
1027 format!("set {}[{}] = {}", name(collection), operand(index), operand(value))
1028 }
1029 Op::Length { dst, collection } => format!("count {} \u{2192} {}", name(collection), name(dst)),
1030 Op::ListPush { list, value } => format!("push {} onto {}", operand(value), name(list)),
1031 Op::NewEmptyList { dst } | Op::NewEmptyListI32 { dst } => format!("make an empty list \u{2192} {}", name(dst)),
1032 Op::Call { .. } => "call a function".to_string(),
1033 Op::Halt => "halt \u{2014} the program is done".to_string(),
1034 _ => String::new(),
1035 }
1036}
1037
1038fn fol_of_op(
1044 op: &crate::vm::Op,
1045 regs: &HashMap<u16, (Option<String>, String)>,
1046 prog: &CompiledProgram,
1047) -> String {
1048 use crate::vm::Op;
1049 let name = |r: u16| regs.get(&r).and_then(|(n, _)| n.clone()).unwrap_or_else(|| format!("R{r}"));
1050 match *op {
1051 Op::LoadConst { dst, idx } => format!("{} = {}", name(dst), crate::vm::format_constant(prog, idx)),
1052 Op::Move { dst, src } => format!("{} = {}", name(dst), name(src)),
1053 Op::Add { dst, lhs, rhs } => format!("{} = {} + {}", name(dst), name(lhs), name(rhs)),
1054 Op::Sub { dst, lhs, rhs } => format!("{} = {} \u{2212} {}", name(dst), name(lhs), name(rhs)),
1055 Op::Mul { dst, lhs, rhs } => format!("{} = {} \u{00d7} {}", name(dst), name(lhs), name(rhs)),
1056 Op::Div { dst, lhs, rhs } | Op::ExactDiv { dst, lhs, rhs } => {
1057 format!("{} = {} \u{00f7} {}", name(dst), name(lhs), name(rhs))
1058 }
1059 Op::Mod { dst, lhs, rhs } => format!("{} = {} mod {}", name(dst), name(lhs), name(rhs)),
1060 Op::Lt { dst, lhs, rhs } => format!("{} \u{27fa} ({} < {})", name(dst), name(lhs), name(rhs)),
1061 Op::Gt { dst, lhs, rhs } => format!("{} \u{27fa} ({} > {})", name(dst), name(lhs), name(rhs)),
1062 Op::LtEq { dst, lhs, rhs } => format!("{} \u{27fa} ({} \u{2264} {})", name(dst), name(lhs), name(rhs)),
1063 Op::GtEq { dst, lhs, rhs } => format!("{} \u{27fa} ({} \u{2265} {})", name(dst), name(lhs), name(rhs)),
1064 Op::Eq { dst, lhs, rhs } => format!("{} \u{27fa} ({} = {})", name(dst), name(lhs), name(rhs)),
1065 Op::NotEq { dst, lhs, rhs } => format!("{} \u{27fa} ({} \u{2260} {})", name(dst), name(lhs), name(rhs)),
1066 Op::Not { dst, src } => format!("{} \u{27fa} \u{00ac}{}", name(dst), name(src)),
1067 Op::AddAssign { dst, src } => format!("{} \u{2254} {} \u{29fa} {}", name(dst), name(dst), name(src)),
1068 Op::Index { dst, collection, index } | Op::IndexUnchecked { dst, collection, index } => {
1069 format!("{} = {}[{}]", name(dst), name(collection), name(index))
1070 }
1071 Op::SetIndex { collection, index, value } | Op::SetIndexUnchecked { collection, index, value } => {
1072 format!("{}[{}] \u{2254} {}", name(collection), name(index), name(value))
1073 }
1074 Op::Length { dst, collection } => format!("{} = |{}|", name(dst), name(collection)),
1075 Op::ListPush { list, value } => format!("{} \u{2254} {} \u{2295} {}", name(list), name(list), name(value)),
1076 Op::NewEmptyList { dst } | Op::NewEmptyListI32 { dst } => format!("{} = \u{2205}", name(dst)),
1077 Op::Return { src } => format!("result = {}", name(src)),
1078 Op::Jump { target } => format!("goto {target}"),
1079 Op::JumpIfFalse { cond, target } => format!("\u{00ac}{} \u{2192} goto {target}", name(cond)),
1080 Op::JumpIfTrue { cond, target } => format!("{} \u{2192} goto {target}", name(cond)),
1081 _ => String::new(),
1082 }
1083}
1084
1085fn socratic_of_op(op: &crate::vm::Op, regs: &HashMap<u16, (Option<String>, String)>) -> String {
1091 use crate::vm::Op;
1092 let name = |r: u16| regs.get(&r).and_then(|(n, _)| n.clone()).unwrap_or_else(|| format!("R{r}"));
1093 let nv = |r: u16| match regs.get(&r) {
1095 Some((_, v)) if !v.is_empty() => format!("{} ({})", name(r), v),
1096 _ => name(r),
1097 };
1098 match *op {
1099 Op::Add { lhs, rhs, .. } => format!("{} and {} \u{2014} what is their sum?", nv(lhs), nv(rhs)),
1100 Op::Sub { lhs, rhs, .. } => format!("{} minus {} \u{2014} what's left?", nv(lhs), nv(rhs)),
1101 Op::Mul { lhs, rhs, .. } => format!("{} times {} \u{2014} what do you get?", nv(lhs), nv(rhs)),
1102 Op::Div { lhs, rhs, .. } | Op::ExactDiv { lhs, rhs, .. } => {
1103 format!("{} divided by {} \u{2014} what is the quotient?", nv(lhs), nv(rhs))
1104 }
1105 Op::Mod { lhs, rhs, .. } => format!("What remains when {} is divided by {}?", nv(lhs), nv(rhs)),
1106 Op::Lt { lhs, rhs, .. } => format!("Is {} less than {}?", nv(lhs), nv(rhs)),
1107 Op::Gt { lhs, rhs, .. } => format!("Is {} greater than {}?", nv(lhs), nv(rhs)),
1108 Op::LtEq { lhs, rhs, .. } => format!("Is {} at most {}?", nv(lhs), nv(rhs)),
1109 Op::GtEq { lhs, rhs, .. } => format!("Is {} at least {}?", nv(lhs), nv(rhs)),
1110 Op::Eq { lhs, rhs, .. } => format!("Does {} equal {}?", nv(lhs), nv(rhs)),
1111 Op::NotEq { lhs, rhs, .. } => format!("Are {} and {} different?", nv(lhs), nv(rhs)),
1112 Op::Not { src, .. } => format!("{} \u{2014} what is its negation?", nv(src)),
1113 Op::JumpIfFalse { cond, .. } => {
1114 format!("{} \u{2014} will the program take this branch, or fall through?", nv(cond))
1115 }
1116 Op::JumpIfTrue { cond, .. } => {
1117 format!("{} \u{2014} is the condition met, so the program jumps?", nv(cond))
1118 }
1119 Op::Index { collection, index, .. } | Op::IndexUnchecked { collection, index, .. } => {
1120 format!("What sits at position {} of {}?", nv(index), name(collection))
1121 }
1122 Op::Length { collection, .. } => format!("How many items does {} hold?", name(collection)),
1123 Op::Return { src } => {
1124 format!("The result is about to be {} \u{2014} is that what you predicted?", nv(src))
1125 }
1126 _ => String::new(),
1127 }
1128}
1129
1130fn parse_comparison(s: &str) -> Option<(Operand, Cmp, Operand)> {
1134 for (sym, cmp) in [("<=", Cmp::Le), (">=", Cmp::Ge), ("==", Cmp::Eq), ("!=", Cmp::Ne)] {
1135 if let Some(i) = s.find(sym) {
1136 return build_cmp(&s[..i], &s[i + sym.len()..], cmp);
1137 }
1138 }
1139 for (sym, cmp) in [('<', Cmp::Lt), ('>', Cmp::Gt), ('=', Cmp::Eq)] {
1140 if let Some(i) = s.find(sym) {
1141 return build_cmp(&s[..i], &s[i + 1..], cmp);
1142 }
1143 }
1144 None
1145}
1146
1147fn build_cmp(l: &str, r: &str, cmp: Cmp) -> Option<(Operand, Cmp, Operand)> {
1148 Some((operand_of(l)?, cmp, operand_of(r)?))
1149}
1150
1151fn operand_of(s: &str) -> Option<Operand> {
1152 let t = s.trim();
1153 if t.is_empty() {
1154 return None;
1155 }
1156 Some(match t.parse::<i64>() {
1157 Ok(n) => Operand::Int(n),
1158 Err(_) => Operand::Var(t.to_string()),
1159 })
1160}
1161
1162fn apply_cmp(a: i64, cmp: Cmp, b: i64) -> bool {
1163 match cmp {
1164 Cmp::Lt => a < b,
1165 Cmp::Le => a <= b,
1166 Cmp::Gt => a > b,
1167 Cmp::Ge => a >= b,
1168 Cmp::Eq => a == b,
1169 Cmp::Ne => a != b,
1170 }
1171}
1172
1173fn entail((al, ah): (i64, i64), cmp: Cmp, (bl, bh): (i64, i64)) -> ProofVerdict {
1177 use ProofVerdict::{ProvenFalse, ProvenTrue, Unknown};
1178 let singleton_eq = al == ah && bl == bh && al == bl;
1179 let disjoint = ah < bl || bh < al;
1180 match cmp {
1181 Cmp::Lt => {
1182 if ah < bl {
1183 ProvenTrue
1184 } else if al >= bh {
1185 ProvenFalse
1186 } else {
1187 Unknown
1188 }
1189 }
1190 Cmp::Le => {
1191 if ah <= bl {
1192 ProvenTrue
1193 } else if al > bh {
1194 ProvenFalse
1195 } else {
1196 Unknown
1197 }
1198 }
1199 Cmp::Gt => {
1200 if al > bh {
1201 ProvenTrue
1202 } else if ah <= bl {
1203 ProvenFalse
1204 } else {
1205 Unknown
1206 }
1207 }
1208 Cmp::Ge => {
1209 if al >= bh {
1210 ProvenTrue
1211 } else if ah < bl {
1212 ProvenFalse
1213 } else {
1214 Unknown
1215 }
1216 }
1217 Cmp::Eq => {
1218 if singleton_eq {
1219 ProvenTrue
1220 } else if disjoint {
1221 ProvenFalse
1222 } else {
1223 Unknown
1224 }
1225 }
1226 Cmp::Ne => {
1227 if disjoint {
1228 ProvenTrue
1229 } else if singleton_eq {
1230 ProvenFalse
1231 } else {
1232 Unknown
1233 }
1234 }
1235 }
1236}
1237
1238fn fmt_num(n: f64) -> String {
1241 if n.fract() == 0.0 && n.abs() < 9.007e15 {
1242 format!("{}", n as i64)
1243 } else {
1244 format!("{n}")
1245 }
1246}
1247
1248fn compile_source(src: &str) -> Result<(CompiledProgram, HashMap<String, ProvenFacts>), String> {
1259 crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
1260 let (stmts, types, _policies) = parsed?;
1261 let program = crate::vm::Compiler::compile_for_debug(stmts, interner, Some(types))?;
1262 let facts = crate::optimize::oracle_analyze_with(stmts, interner);
1263 let proven = facts
1264 .summarize_variables(stmts)
1265 .into_iter()
1266 .map(|(sym, vf)| (interner.resolve(sym).to_string(), ProvenFacts::from(vf)))
1267 .collect();
1268 Ok((program, proven))
1269 })
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275
1276 const PROG: &str = "## Main\n\nLet x be 6.\nLet y be 7.\nShow x + y.";
1277
1278 fn run_to_done(dbg: &mut Debugger) {
1279 for _ in 0..10_000 {
1280 if dbg.snapshot().state != "paused" {
1281 break;
1282 }
1283 dbg.step();
1284 }
1285 }
1286
1287 fn pc_of(dbg: &Debugger, prefix: &str) -> usize {
1290 dbg.disassembly()
1291 .iter()
1292 .find(|l| l.text.starts_with(prefix))
1293 .unwrap_or_else(|| panic!("no `{prefix}` op in the disassembly"))
1294 .pc
1295 }
1296
1297 #[test]
1298 fn arms_at_entry() {
1299 let dbg = Debugger::from_source(PROG).expect("compiles");
1300 let s = dbg.snapshot();
1301 assert_eq!(s.step, 0, "history cursor at the start");
1302 assert_eq!(s.pc, 0, "stopped before the first op");
1303 assert_eq!(s.state, "paused");
1304 assert!(s.output.is_empty());
1305 assert!(s.total_ops > 0, "the program has instructions");
1306 assert!(!s.frames.is_empty(), "at least the Main frame");
1307 }
1308
1309 #[test]
1310 fn stepping_to_done_matches_a_normal_run() {
1311 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1312 run_to_done(&mut dbg);
1313 let s = dbg.snapshot();
1314 assert_eq!(s.state, "done", "reaches completion");
1315 let interp = crate::ui_bridge::interpret_for_ui_sync_with_args(PROG, &[]);
1317 assert_eq!(interp.error, None);
1318 assert_eq!(s.output, interp.lines, "stepped output == single-shot run output");
1319 }
1320
1321 #[test]
1322 fn disassembly_is_faithful_to_the_source() {
1323 let dbg = Debugger::from_source(PROG).expect("compiles");
1324 let texts: Vec<&str> = dbg.disassembly().iter().map(|l| l.text.as_str()).collect();
1325 assert!(texts[0].starts_with("LoadConst"), "first op loads a literal");
1326 assert_eq!(texts.last(), Some(&"Halt"), "program ends in Halt");
1327 assert!(texts.iter().any(|t| t.starts_with("Add")), "the `x + y` add is present");
1328 assert!(texts.iter().any(|t| t.starts_with("Show")), "the `Show` is present");
1329 }
1330
1331 #[test]
1332 fn step_into_advances_one_op() {
1333 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1334 assert!(!dbg.snapshot().op_text.is_empty(), "paused on a real op");
1335 dbg.step();
1336 let s = dbg.snapshot();
1337 assert_eq!(s.step, 1, "history cursor advanced one op");
1338 assert_eq!(s.pc, 1, "straight-line pc advanced");
1339 }
1340
1341 #[test]
1342 fn registers_carry_their_values() {
1343 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1344 let show = pc_of(&dbg, "Show");
1346 dbg.set_breakpoint(show);
1347 dbg.resume();
1348 let s = dbg.snapshot();
1349 assert_eq!(s.pc, show);
1350 let main = &s.frames[0];
1351 let values: Vec<&str> = main.registers.iter().map(|r| r.value.as_str()).collect();
1352 assert!(values.contains(&"6"), "x's value is live in a register: {values:?}");
1353 assert!(values.contains(&"7"), "y's value is live in a register: {values:?}");
1354 assert!(values.contains(&"13"), "x + y was computed: {values:?}");
1355 assert!(
1357 main.registers.iter().any(|r| r.changed),
1358 "the most recent write is flagged changed"
1359 );
1360 }
1361
1362 #[test]
1363 fn registers_carry_their_type() {
1364 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1367 let show = pc_of(&dbg, "Show");
1368 dbg.set_breakpoint(show);
1369 dbg.resume();
1370 let s = dbg.snapshot();
1371 let typed: Vec<(&str, &str)> = s.frames[0]
1372 .registers
1373 .iter()
1374 .map(|r| (r.kind.as_str(), r.value.as_str()))
1375 .collect();
1376 assert!(typed.contains(&("Int", "6")), "x:6 is typed Int: {typed:?}");
1377 assert!(typed.contains(&("Int", "13")), "x+y:13 is typed Int: {typed:?}");
1378 assert!(
1379 s.frames[0].registers.iter().all(|r| !r.kind.is_empty()),
1380 "no live register is left untyped: {typed:?}"
1381 );
1382 }
1383
1384 #[test]
1385 fn variable_timeline_tracks_each_variable_over_time() {
1386 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1387 dbg.resume();
1388 let tl = dbg.variable_timeline();
1389 let names: Vec<&str> = tl.vars.iter().map(|v| v.name.as_str()).collect();
1390 assert!(names.contains(&"x"), "x is a traced variable: {names:?}");
1391 assert!(names.contains(&"y"), "y is a traced variable: {names:?}");
1392
1393 for v in &tl.vars {
1395 assert_eq!(v.points.len(), tl.steps, "trace {} spans the timeline", v.name);
1396 }
1397 let x = tl.vars.iter().find(|v| v.name == "x").unwrap();
1398 assert_eq!(x.kind, "Int", "x is typed on its trace");
1399 assert!(x.points.iter().any(|p| p.value == "6" && p.changed), "x has a 6-edge");
1401 assert_eq!(x.points.last().unwrap().value, "6", "x ends at 6");
1402 assert_eq!(tl.cursor, dbg.snapshot().step, "playhead is at the cursor");
1403 }
1404
1405 #[test]
1406 fn snapshot_carries_fol_semantics() {
1407 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1409 let mut fols = Vec::new();
1410 while dbg.is_running() {
1411 let s = dbg.snapshot();
1412 if !s.fol.is_empty() {
1413 fols.push(s.fol.clone());
1414 }
1415 dbg.step();
1416 }
1417 assert!(fols.iter().any(|f| f == "R0 = 6"), "literal load reads as R0 = 6: {fols:?}");
1418 assert!(fols.iter().any(|f| f == "x = R0"), "the copy into x reads as x = R0: {fols:?}");
1419 assert!(fols.iter().any(|f| f.contains("x + y")), "the addition reads over named vars: {fols:?}");
1420 }
1421
1422 #[test]
1423 fn fol_renders_a_comparison_as_a_biconditional() {
1424 let src = "## Main\n\nLet x be 6.\nLet y be 7.\nLet t be x < y.\nShow t.";
1426 let mut dbg = Debugger::from_source(src).expect("compiles");
1427 let mut fols = Vec::new();
1428 while dbg.is_running() {
1429 let s = dbg.snapshot();
1430 if !s.fol.is_empty() {
1431 fols.push(s.fol.clone());
1432 }
1433 dbg.step();
1434 }
1435 assert!(
1436 fols.iter().any(|f| f.contains("\u{27fa}") && f.contains("x < y")),
1437 "the comparison reads as a biconditional: {fols:?}"
1438 );
1439 }
1440
1441 #[test]
1442 fn socratic_prompt_asks_the_learner_to_predict() {
1443 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1444 let mut qs = Vec::new();
1445 while dbg.is_running() {
1446 let s = dbg.snapshot();
1447 if !s.socratic.is_empty() {
1448 qs.push(s.socratic.clone());
1449 }
1450 dbg.step();
1451 }
1452 assert!(
1453 qs.iter().any(|q| q.ends_with('?') && q.contains("sum") && q.contains("(6)") && q.contains("(7)")),
1454 "the addition asks the learner to predict the sum from the live operands: {qs:?}"
1455 );
1456 assert!(qs.iter().all(|q| q.contains('?')), "every Socratic prompt is a question: {qs:?}");
1457 }
1458
1459 #[test]
1460 fn socratic_poses_a_yes_no_question_for_a_comparison() {
1461 let src = "## Main\n\nLet x be 6.\nLet y be 7.\nLet t be x < y.\nShow t.";
1462 let mut dbg = Debugger::from_source(src).expect("compiles");
1463 let mut qs = Vec::new();
1464 while dbg.is_running() {
1465 let s = dbg.snapshot();
1466 if !s.socratic.is_empty() {
1467 qs.push(s.socratic.clone());
1468 }
1469 dbg.step();
1470 }
1471 assert!(
1472 qs.iter().any(|q| q.starts_with("Is ") && q.contains("less than") && q.ends_with('?')),
1473 "the comparison is posed as a yes/no question: {qs:?}"
1474 );
1475 }
1476
1477 #[test]
1478 fn socratic_done_state_invites_reflection() {
1479 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1480 dbg.resume();
1481 let s = dbg.snapshot();
1482 assert!(s.socratic.contains("expected"), "the finished prompt invites reflection: {}", s.socratic);
1483 }
1484
1485 #[test]
1486 fn live_proof_is_true_now_and_proven_for_every_run() {
1487 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1488 dbg.resume();
1489 let r = dbg.assert_at_cursor("x < y");
1490 assert!(r.parsed, "parsed the comparison");
1491 assert_eq!(r.now, Some(true), "x=6 < y=7 holds now: {}", r.now_detail);
1492 assert_eq!(r.verdict, ProofVerdict::ProvenTrue, "proven for every run: {}", r.verdict_detail);
1493 assert!(r.verdict_detail.contains("[6, 6]"), "cites the proven ranges: {}", r.verdict_detail);
1494 }
1495
1496 #[test]
1497 fn live_proof_statically_refutes_a_false_predicate() {
1498 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1499 dbg.resume();
1500 let r = dbg.assert_at_cursor("x > y");
1501 assert_eq!(r.now, Some(false), "x=6 > y=7 is false now");
1502 assert_eq!(r.verdict, ProofVerdict::ProvenFalse, "refuted for every run: {}", r.verdict_detail);
1503 }
1504
1505 #[test]
1506 fn live_proof_proves_a_constant_equality_and_a_bound() {
1507 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1508 dbg.resume();
1509 assert_eq!(dbg.assert_at_cursor("x == 6").verdict, ProofVerdict::ProvenTrue, "x is provably 6");
1510 assert_eq!(dbg.assert_at_cursor("x >= 0").verdict, ProofVerdict::ProvenTrue, "x is provably non-negative");
1511 assert_eq!(dbg.assert_at_cursor("y <= 100").verdict, ProofVerdict::ProvenTrue, "y is provably under 100");
1512 }
1513
1514 #[test]
1515 fn live_proof_rejects_unparseable_input() {
1516 let dbg = Debugger::from_source(PROG).expect("compiles");
1517 let r = dbg.assert_at_cursor("hello world");
1518 assert!(!r.parsed, "garbage is not a comparison");
1519 assert_eq!(r.verdict, ProofVerdict::Unknown);
1520 }
1521
1522 #[test]
1523 fn proven_invariants_prove_constant_values_and_types() {
1524 let dbg = Debugger::from_source(PROG).expect("compiles");
1527 let proven = dbg.proven_invariants();
1528 let names: Vec<&str> = proven.iter().map(|p| p.name.as_str()).collect();
1529 assert!(names.contains(&"x"), "x has proven facts: {names:?}");
1530 let x = proven.iter().find(|p| p.name == "x").unwrap();
1531 assert!(x.facts.iter().any(|f| f.contains("[6, 6]")), "x proven \u{2208} [6,6]: {:?}", x.facts);
1532 assert!(x.facts.iter().any(|f| f.contains("Int")), "x proven Int: {:?}", x.facts);
1533 let y = proven.iter().find(|p| p.name == "y").unwrap();
1534 assert!(y.facts.iter().any(|f| f.contains("[7, 7]")), "y proven \u{2208} [7,7]: {:?}", y.facts);
1535 }
1536
1537 #[test]
1538 fn proven_facts_are_available_without_stepping() {
1539 let dbg = Debugger::from_source(PROG).expect("compiles");
1541 assert_eq!(dbg.snapshot().step, 0, "fresh debugger, nothing executed");
1542 assert!(!dbg.proven_invariants().is_empty(), "proven facts ready before any step");
1543 }
1544
1545 #[test]
1546 fn observed_invariants_report_a_constant() {
1547 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1548 dbg.resume();
1549 let ins = dbg.observed_invariants();
1550 let x = ins.iter().find(|i| i.name == "x").expect("x has insights");
1551 assert!(
1552 x.facts.iter().any(|f| f.contains("constant") && f.contains('6')),
1553 "x is observed constant 6: {:?}",
1554 x.facts
1555 );
1556 }
1557
1558 #[test]
1559 fn observed_invariants_detect_a_monotonic_range() {
1560 let src = "## Main\n\nLet n be 1.\nSet n to 2.\nSet n to 3.\nShow n.";
1561 let mut dbg = Debugger::from_source(src).expect("compiles");
1562 dbg.resume();
1563 let ins = dbg.observed_invariants();
1564 let n = ins.iter().find(|i| i.name == "n").expect("n has insights");
1565 assert!(
1566 n.facts.iter().any(|f| f.contains("range") && f.contains('1') && f.contains('3')),
1567 "n ranges over [1,3]: {:?}",
1568 n.facts
1569 );
1570 assert!(n.facts.iter().any(|f| f.contains("increase")), "n only increases: {:?}", n.facts);
1571 }
1572
1573 #[test]
1574 fn provenance_explains_why_a_value_exists() {
1575 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1576 dbg.resume();
1577 let snap = dbg.snapshot();
1578 let sum = snap.frames.last().unwrap().registers.iter().find(|r| r.value == "13").unwrap().index;
1580 let node = dbg.provenance(sum).expect("13 has a provenance");
1581 assert_eq!(node.value, "13");
1582 assert!(
1583 node.narration.contains("add") || node.op_text.to_lowercase().contains("add"),
1584 "the sum was produced by an add: {} / {}",
1585 node.op_text,
1586 node.narration
1587 );
1588 let input_vals: Vec<&str> = node.inputs.iter().map(|n| n.value.as_str()).collect();
1590 assert!(input_vals.contains(&"6"), "one input is 6: {input_vals:?}");
1591 assert!(input_vals.contains(&"7"), "one input is 7: {input_vals:?}");
1592 }
1593
1594 #[test]
1595 fn provenance_bottoms_out_at_a_constant_load() {
1596 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1597 dbg.resume();
1598 let snap = dbg.snapshot();
1599 let x = snap
1600 .frames
1601 .last()
1602 .unwrap()
1603 .registers
1604 .iter()
1605 .find(|r| r.name.as_deref() == Some("x"))
1606 .unwrap()
1607 .index;
1608 let node = dbg.provenance(x).expect("x has a provenance");
1609 assert_eq!(node.value, "6");
1610 assert!(node.inputs.is_empty(), "a literal load consumes no registers");
1611 assert!(
1612 node.narration.contains("load") || node.op_text.to_lowercase().contains("load"),
1613 "x came from a load: {} / {}",
1614 node.op_text,
1615 node.narration
1616 );
1617 }
1618
1619 #[test]
1620 fn provenance_chains_through_a_dependent_assignment() {
1621 let src = "## Main\n\nLet x be 6.\nLet y be 7.\nLet z be x + y.\nLet w be z + x.\nShow w.";
1623 let mut dbg = Debugger::from_source(src).expect("compiles");
1624 dbg.resume();
1625 let snap = dbg.snapshot();
1626 let w = snap
1627 .frames
1628 .last()
1629 .unwrap()
1630 .registers
1631 .iter()
1632 .find(|r| r.name.as_deref() == Some("w"))
1633 .unwrap()
1634 .index;
1635 let node = dbg.provenance(w).expect("w has a provenance");
1636 assert_eq!(node.value, "19", "w = (6+7) + 6");
1637 let z = node.inputs.iter().find(|n| n.value == "13").expect("w reads z=13");
1639 let z_inputs: Vec<&str> = z.inputs.iter().map(|n| n.value.as_str()).collect();
1640 assert!(z_inputs.contains(&"6") && z_inputs.contains(&"7"), "z traces to 6 and 7: {z_inputs:?}");
1641 }
1642
1643 #[test]
1644 fn variable_names_are_resolved() {
1645 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1646 let show = pc_of(&dbg, "Show");
1647 dbg.set_breakpoint(show);
1648 dbg.resume();
1649 let s = dbg.snapshot();
1650 let named: Vec<(&str, &str)> = s.frames[0]
1651 .registers
1652 .iter()
1653 .filter_map(|r| r.name.as_deref().map(|n| (n, r.value.as_str())))
1654 .collect();
1655 assert!(named.contains(&("x", "6")), "x = 6 shown by name: {named:?}");
1656 assert!(named.contains(&("y", "7")), "y = 7 shown by name: {named:?}");
1657 }
1658
1659 #[test]
1660 fn production_compile_carries_no_debug_names() {
1661 let (dbg_prog, _proven) = compile_source(PROG).expect("debug compile");
1663 assert!(!dbg_prog.reg_names.is_empty(), "debug path records reg_names");
1664 let prod_prog = crate::ui_bridge::with_parsed_program(PROG, |parsed, interner| {
1667 let (stmts, types, _policies) = parsed?;
1668 crate::vm::Compiler::compile_with_types(stmts, interner, Some(types))
1669 })
1670 .expect("production compile");
1671 assert!(prod_prog.reg_names.is_empty(), "production path captures no debug names");
1672 }
1673
1674 #[test]
1675 fn breakpoint_halts_continue() {
1676 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1677 let show = pc_of(&dbg, "Show");
1678 dbg.set_breakpoint(show);
1679 dbg.resume();
1680 let s = dbg.snapshot();
1681 assert_eq!(s.pc, show, "Continue stopped at the breakpoint");
1682 assert!(s.at_breakpoint);
1683 assert_eq!(s.state, "paused");
1684 assert!(s.output.is_empty(), "the Show has not run yet");
1685 dbg.step();
1687 assert!(!dbg.snapshot().output.is_empty(), "stepping the Show emits a line");
1688 }
1689
1690 #[test]
1691 fn time_travel_steps_backwards() {
1692 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1693 dbg.step();
1694 dbg.step();
1695 dbg.step();
1696 let three = dbg.snapshot();
1697 assert_eq!(three.step, 3);
1698 let pc_at_three = three.pc;
1699 dbg.step_back();
1700 assert_eq!(dbg.snapshot().step, 2, "rewound one op");
1701 dbg.step();
1703 let again = dbg.snapshot();
1704 assert_eq!(again.step, 3);
1705 assert_eq!(again.pc, pc_at_three, "replay is deterministic");
1706 }
1707
1708 #[test]
1709 fn restart_rewinds_to_entry() {
1710 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1711 run_to_done(&mut dbg);
1712 assert_eq!(dbg.snapshot().state, "done");
1713 dbg.restart();
1714 let s = dbg.snapshot();
1715 assert_eq!(s.step, 0);
1716 assert_eq!(s.pc, 0);
1717 assert_eq!(s.state, "paused");
1718 assert!(s.output.is_empty());
1719 }
1720
1721 const FUNC: &str =
1724 "## To double (x: Int) -> Int:\n Return x + x.\n\n## Main\nLet result be double(5).\nShow result.";
1725 const WHILE: &str = "## Main\nLet mutable sum be 0.\nLet mutable i be 1.\nWhile i is at most 5:\n Set sum to sum + i.\n Set i to i + 1.\nShow sum.";
1726
1727 fn drive_to_end(src: &str) -> DebugSnapshot {
1729 let mut dbg = Debugger::from_source(src)
1730 .unwrap_or_else(|e| panic!("compile failed: {e}\n--- src ---\n{src}"));
1731 let mut guard = 0;
1732 while dbg.is_running() && guard < 500_000 {
1733 dbg.step();
1734 guard += 1;
1735 }
1736 dbg.snapshot()
1737 }
1738
1739 #[test]
1744 fn corpus_stepped_output_matches_interpreter() {
1745 let corpus: &[(&str, &str)] = &[
1746 ("while_loop", WHILE),
1747 ("repeat_range", "## Main\nLet mutable total be 0.\nRepeat for i from 1 to 5:\n Set total to total + i.\nShow total."),
1748 ("for_in", "## Main\nLet mutable sum be 0.\nRepeat for x in [10, 20, 30]:\n Set sum to sum + x.\nShow sum."),
1749 ("conditional", "## Main\nLet x be 3.\nIf x is greater than 5:\n Show \"big\".\nOtherwise:\n Show \"small\"."),
1750 ("function", FUNC),
1751 ("recursion", "## To fib (n: Int) -> Int:\n If n is less than 2:\n Return n.\n Return fib(n - 1) + fib(n - 2).\n\n## Main\nShow fib(6)."),
1752 ("list", "## Main\nLet xs be [1, 2, 3].\nPush 4 to xs.\nShow length of xs."),
1753 ];
1754 for (name, src) in corpus {
1755 let oracle = crate::ui_bridge::interpret_for_ui_sync_with_args(src, &[]);
1756 assert_eq!(oracle.error, None, "{name}: the interpreter itself errored: {:?}", oracle.error);
1757 let snap = drive_to_end(src);
1758 assert_eq!(
1759 snap.state, "done",
1760 "{name}: debugger did not finish (state={}, out={:?})", snap.state, snap.output
1761 );
1762 assert_eq!(snap.output, oracle.lines, "{name}: stepped output diverged from the interpreter");
1763 }
1764 }
1765
1766 #[test]
1767 fn stepping_a_while_loop_runs_all_iterations() {
1768 let snap = drive_to_end(WHILE);
1769 assert_eq!(snap.state, "done");
1770 assert_eq!(snap.output, vec!["15".to_string()], "op-by-op stepping summed 1..=5");
1771 }
1772
1773 #[test]
1774 fn call_stack_descends_into_the_function() {
1775 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
1776 let mut entered = false;
1777 let mut guard = 0;
1778 while dbg.is_running() && guard < 10_000 {
1779 let s = dbg.snapshot();
1780 if s.frames.len() >= 2 {
1781 entered = true;
1782 assert!(s.frames.last().unwrap().function.is_some(), "inner frame is a function");
1783 break;
1784 }
1785 dbg.step();
1786 guard += 1;
1787 }
1788 assert!(entered, "stepping descends into the called function");
1789 }
1790
1791 #[test]
1792 fn step_over_runs_the_call_without_descending() {
1793 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
1794 let call_pc = pc_of(&dbg, "Call");
1795 let mut guard = 0;
1796 while dbg.snapshot().pc != call_pc && dbg.is_running() && guard < 1000 {
1797 dbg.step();
1798 guard += 1;
1799 }
1800 assert_eq!(dbg.snapshot().pc, call_pc, "reached the Call op");
1801 let depth = dbg.snapshot().frames.len();
1802 dbg.step_over();
1803 let s = dbg.snapshot();
1804 assert_eq!(s.frames.len(), depth, "step-over did not leave us inside the callee");
1805 assert!(s.pc > call_pc || s.state == "done", "advanced past the call");
1806 }
1807
1808 #[test]
1809 fn step_out_returns_to_the_caller() {
1810 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
1811 let mut guard = 0;
1812 while dbg.is_running() && dbg.snapshot().frames.len() < 2 && guard < 1000 {
1813 dbg.step();
1814 guard += 1;
1815 }
1816 assert_eq!(dbg.snapshot().frames.len(), 2, "inside the function");
1817 dbg.step_out();
1818 assert!(dbg.snapshot().frames.len() <= 1, "step-out returns to the caller");
1819 }
1820
1821 #[test]
1822 fn runtime_error_surfaces_without_panicking() {
1823 let mut dbg = Debugger::from_source("## Main\nLet x be 1 / 0.\nShow x.").expect("compiles");
1824 let mut guard = 0;
1825 while dbg.is_running() && guard < 100 {
1826 dbg.step();
1827 guard += 1;
1828 }
1829 let s = dbg.snapshot();
1830 assert_eq!(s.state, "error", "division by zero surfaces as an error, not a panic");
1831 assert!(s.error.is_some(), "the error carries a message");
1832 }
1833
1834 #[test]
1835 fn entering_a_function_does_not_flag_spurious_changes() {
1836 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
1837 let mut guard = 0;
1838 while dbg.is_running() && dbg.snapshot().frames.len() < 2 && guard < 1000 {
1839 dbg.step();
1840 guard += 1;
1841 }
1842 let s = dbg.snapshot();
1843 assert_eq!(s.frames.len(), 2, "entered the function");
1844 assert!(
1845 s.frames.last().unwrap().registers.iter().all(|r| !r.changed),
1846 "crossing into a new frame must not flag stale registers as changed"
1847 );
1848 }
1849
1850 #[test]
1851 fn time_travel_across_a_call_restores_the_frame() {
1852 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
1853 let mut guard = 0;
1854 while dbg.is_running() && dbg.snapshot().frames.len() < 2 && guard < 1000 {
1855 dbg.step();
1856 guard += 1;
1857 }
1858 let inside = dbg.snapshot();
1859 assert_eq!(inside.frames.len(), 2, "inside the function");
1860 let (pc, step) = (inside.pc, inside.step);
1861 dbg.step();
1862 dbg.step_back();
1863 let back = dbg.snapshot();
1864 assert_eq!(back.step, step, "rewound to the in-function step");
1865 assert_eq!(back.pc, pc, "pc restored exactly");
1866 assert_eq!(back.frames.len(), 2, "still inside the function after the rewind");
1867 }
1868
1869 #[test]
1870 fn narration_explains_each_step_in_english() {
1871 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1872 let mut narrations = Vec::new();
1873 let mut guard = 0;
1874 while dbg.is_running() && guard < 1000 {
1875 let s = dbg.snapshot();
1876 if !s.narration.is_empty() {
1877 narrations.push(s.narration.clone());
1878 }
1879 dbg.step();
1880 guard += 1;
1881 }
1882 let all = narrations.join(" | ");
1883 assert!(all.contains("add"), "an add step is narrated in English: {all}");
1884 assert!(all.contains("print"), "the print is narrated: {all}");
1885 assert!(
1886 narrations.iter().any(|n| n.contains("x(6)") && n.contains("y(7)")),
1887 "the add narration names the variables and their live values: {narrations:?}"
1888 );
1889 }
1890
1891 #[test]
1892 fn op_io_in_snapshot_targets_the_operands() {
1893 let mut dbg = Debugger::from_source(PROG).expect("compiles");
1894 let add = pc_of(&dbg, "Add");
1895 let mut guard = 0;
1896 while dbg.snapshot().pc != add && dbg.is_running() && guard < 100 {
1897 dbg.step();
1898 guard += 1;
1899 }
1900 let s = dbg.snapshot();
1901 assert_eq!(s.pc, add);
1902 assert!(s.op_writes.is_some(), "the Add writes a destination register");
1903 assert_eq!(s.op_reads.len(), 2, "the Add reads its two operands (for the datapath)");
1904 }
1905
1906 #[test]
1907 fn seek_scrubs_anywhere_in_history() {
1908 let mut dbg = Debugger::from_source(WHILE).expect("compiles");
1909 while dbg.is_running() {
1910 dbg.step();
1911 }
1912 let end = dbg.snapshot();
1913 assert_eq!(end.state, "done");
1914 let total = end.total_steps;
1915 assert!(total > 3, "the loop took several ops");
1916 dbg.seek(0);
1918 let s0 = dbg.snapshot();
1919 assert_eq!(s0.step, 0);
1920 assert_eq!(s0.state, "paused");
1921 assert!(s0.output.is_empty(), "no output has happened at the entry");
1922 dbg.seek(total / 2);
1924 assert_eq!(dbg.snapshot().step, total / 2);
1925 assert_eq!(dbg.snapshot().state, "paused");
1926 dbg.seek(total);
1928 let again = dbg.snapshot();
1929 assert_eq!(again.step, total);
1930 assert_eq!(again.state, "done");
1931 assert_eq!(again.output, end.output, "scrubbing to the end restores the final output");
1932 }
1933
1934 #[test]
1935 fn reverse_continue_runs_back_to_a_breakpoint() {
1936 let mut dbg = Debugger::from_source(WHILE).expect("compiles");
1937 let show = pc_of(&dbg, "Show");
1938 while dbg.is_running() {
1939 dbg.step();
1940 }
1941 assert_eq!(dbg.snapshot().state, "done");
1942 dbg.set_breakpoint(show);
1943 dbg.reverse_resume();
1944 let s = dbg.snapshot();
1945 assert_eq!(s.pc, show, "reverse-continue landed on the breakpoint");
1946 assert!(s.at_breakpoint);
1947 assert!(s.output.is_empty(), "rewound to the moment before the Show ran");
1948 }
1949
1950 #[test]
1951 fn restart_rewinds_but_keeps_explored_history() {
1952 let mut dbg = Debugger::from_source(WHILE).expect("compiles");
1953 while dbg.is_running() {
1954 dbg.step();
1955 }
1956 let total = dbg.snapshot().total_steps;
1957 dbg.restart();
1958 let s = dbg.snapshot();
1959 assert_eq!(s.step, 0, "back at the entry");
1960 assert_eq!(s.state, "paused");
1961 assert_eq!(s.total_steps, total, "explored history is retained, so re-stepping is instant");
1962 }
1963
1964 #[test]
1965 fn heap_view_lists_a_distinct_object() {
1966 let src = "## Main\nLet xs be [1, 2, 3].\nShow length of xs.";
1967 let mut dbg = Debugger::from_source(src).expect("compiles");
1968 while dbg.is_running() {
1969 dbg.step();
1970 }
1971 let s = dbg.snapshot();
1972 assert!(
1973 s.heap.iter().any(|o| o.kind == "list" && o.referenced_by.contains(&"xs".to_string())),
1974 "the list `xs` shows up as a heap object: {:?}", s.heap
1975 );
1976 }
1977
1978 #[test]
1979 fn heap_view_shows_storage_layout() {
1980 let src = "## Main\nLet xs be [1, 2, 3].\nShow length of xs.";
1983 let mut dbg = Debugger::from_source(src).expect("compiles");
1984 while dbg.is_running() {
1985 dbg.step();
1986 }
1987 let s = dbg.snapshot();
1988 let list = s.heap.iter().find(|o| o.kind == "list").expect("a list on the heap");
1989 assert_eq!(list.storage, "packed Vec<i64>", "an int list is densely packed: {list:?}");
1990 }
1991
1992 #[test]
1993 fn heap_view_reveals_aliasing() {
1994 let src = "## Main\nLet a be [1, 2, 3].\nLet b be a.\nShow a.";
1997 let mut dbg = Debugger::from_source(src).expect("compiles");
1998 while dbg.is_running() {
1999 dbg.step();
2000 }
2001 let s = dbg.snapshot();
2002 let lists: Vec<&HeapObject> = s.heap.iter().filter(|o| o.kind == "list").collect();
2003 assert_eq!(lists.len(), 1, "a and b share ONE list allocation, not two: {:?}", s.heap);
2004 let list = lists[0];
2005 assert!(list.referenced_by.contains(&"a".to_string()), "`a` references it: {list:?}");
2006 assert!(list.referenced_by.contains(&"b".to_string()), "`b` references it: {list:?}");
2007 assert!(list.shared, "aliasing is flagged");
2008 }
2009
2010 #[test]
2011 fn stack_frames_carry_their_base_address() {
2012 let mut dbg = Debugger::from_source(FUNC).expect("compiles");
2013 let mut guard = 0;
2014 while dbg.is_running() && dbg.snapshot().frames.len() < 2 && guard < 1000 {
2015 dbg.step();
2016 guard += 1;
2017 }
2018 let s = dbg.snapshot();
2019 assert_eq!(s.frames.len(), 2, "inside the function");
2020 assert_eq!(s.frames[0].base, 0, "the Main frame starts at stack address 0");
2021 assert!(s.frames[1].base > 0, "the callee frame is stacked above Main (higher address)");
2022 }
2023}