1use crate::document_ng::Document;
18use crate::schema::model as schema;
19
20use super::headless::Headless;
21use super::parse::{Line, ParseError, parse_line};
22use super::step::{Script, Step};
23
24pub struct LineEntry {
26 pub source: String,
27 pub kind: LineKind,
28}
29
30pub enum LineKind {
31 Blank,
33 Marker {
36 key: String,
37 narration: String,
38 },
39 Step(Step),
40 Invalid(ParseError),
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum StepOutcome {
47 Advanced,
48 Halted(usize),
51 AtEnd,
52 AtStart,
53}
54
55pub struct Debugger {
56 initial_kdl: String,
59 lines: Vec<LineEntry>,
60 position: usize,
62 harness: Headless,
63 runtime_halt: Option<(usize, String)>,
66}
67
68const NAME: &str = "script-debugger";
69
70impl Debugger {
71 pub fn new(doc: &Document) -> Self {
73 let initial_kdl = schema::Document::from(doc).to_kdl();
74 let harness = Headless::new(Some(&initial_kdl), NAME);
75 Self {
76 initial_kdl,
77 lines: Vec::new(),
78 position: 0,
79 harness,
80 runtime_halt: None,
81 }
82 }
83
84 pub fn init(&mut self, doc: &Document) {
87 self.initial_kdl = schema::Document::from(doc).to_kdl();
88 let _ = self.replay(0);
90 }
91
92 pub fn set_text(&mut self, text: &str) {
96 let new_lines: Vec<LineEntry> = text
97 .split('\n')
98 .map(|source| LineEntry {
99 kind: match parse_line(source) {
100 Ok(Line::Blank) => LineKind::Blank,
101 Ok(Line::Marker { key, narration }) => LineKind::Marker { key, narration },
102 Ok(Line::Step(step)) => LineKind::Step(step),
103 Err(e) => LineKind::Invalid(e),
104 },
105 source: source.to_owned(),
106 })
107 .collect();
108 let first_change = self
109 .lines
110 .iter()
111 .zip(&new_lines)
112 .position(|(old, new)| old.source != new.source)
113 .unwrap_or_else(|| self.lines.len().min(new_lines.len()));
114 self.lines = new_lines;
115 if first_change < self.position {
116 let _ = self.replay(first_change);
119 }
120 }
121
122 pub fn text(&self) -> String {
123 let sources: Vec<&str> = self.lines.iter().map(|l| l.source.as_str()).collect();
124 sources.join("\n")
125 }
126
127 pub fn lines(&self) -> &[LineEntry] {
128 &self.lines
129 }
130
131 pub fn position(&self) -> usize {
133 self.position
134 }
135
136 pub fn runtime_halt(&self) -> Option<&(usize, String)> {
139 self.runtime_halt.as_ref()
140 }
141
142 pub fn step_forward(&mut self) -> StepOutcome {
145 loop {
146 match self.lines.get(self.position).map(|l| &l.kind) {
147 None => return StepOutcome::AtEnd,
148 Some(LineKind::Blank | LineKind::Marker { .. }) => self.position += 1,
149 Some(LineKind::Invalid(_)) => return StepOutcome::Halted(self.position),
150 Some(LineKind::Step(_)) => {
151 let target = self.position + 1;
152 return match self.replay(target) {
153 Ok(()) => StepOutcome::Advanced,
154 Err(line) => StepOutcome::Halted(line),
155 };
156 }
157 }
158 }
159 }
160
161 pub fn step_back(&mut self) -> StepOutcome {
163 let previous = self.lines[..self.position]
164 .iter()
165 .rposition(|l| matches!(l.kind, LineKind::Step(_)));
166 if let Some(line) = previous {
167 let _ = self.replay(line);
168 StepOutcome::Advanced
169 } else {
170 let _ = self.replay(0);
171 StepOutcome::AtStart
172 }
173 }
174
175 pub fn reset(&mut self) {
177 let _ = self.replay(0);
178 }
179
180 pub fn document(&self) -> &Document {
182 self.harness.document()
183 }
184
185 pub fn projected(&self) -> schema::Document {
186 self.harness.projected()
187 }
188
189 fn replay(&mut self, target: usize) -> Result<(), usize> {
195 self.runtime_halt = None;
196 self.harness.reset(Some(&self.initial_kdl), NAME);
197 let step_lines: Vec<usize> = self.lines[..target]
198 .iter()
199 .enumerate()
200 .filter_map(|(i, l)| matches!(l.kind, LineKind::Step(_)).then_some(i))
201 .collect();
202 let script = Script::new(
203 step_lines
204 .iter()
205 .filter_map(|&i| match self.lines[i].kind {
206 LineKind::Step(step) => Some(step),
207 _ => None,
208 })
209 .collect(),
210 );
211 match self.harness.run_script(&script) {
212 Ok(()) => {
213 self.position = target;
214 Ok(())
215 }
216 Err(e) => {
217 let line = step_lines.get(e.step).copied().unwrap_or(target);
219 self.position = line;
220 self.runtime_halt = Some((line, format!("names a missing object: {:?}", e.target)));
221 Err(line)
222 }
223 }
224 }
225
226 pub fn first_invalid(&self) -> Option<usize> {
228 self.lines
229 .iter()
230 .position(|l| matches!(l.kind, LineKind::Invalid(_)))
231 }
232
233 pub fn emit_level(&self, meta: &LevelMeta) -> Result<String, String> {
237 if let Some(line) = self.first_invalid() {
238 return Err(format!("line {} is invalid", line + 1));
239 }
240 let has_steps = self
241 .lines
242 .iter()
243 .any(|l| matches!(l.kind, LineKind::Step(_)));
244 if !has_steps {
245 return Err("the script has no steps".into());
246 }
247 let mut scratch = Headless::new(Some(&self.initial_kdl), NAME);
250 let script = Script::new(
251 self.lines
252 .iter()
253 .filter_map(|l| match l.kind {
254 LineKind::Step(step) => Some(step),
255 _ => None,
256 })
257 .collect(),
258 );
259 scratch
260 .run_script(&script)
261 .map_err(|e| format!("step {} names a missing object", e.step))?;
262 let solution = scratch.projected();
263 Ok(emit(meta, &self.initial_kdl, &solution, &self.lines))
264 }
265}
266
267pub struct LevelMeta {
270 pub id: String,
271 pub title: String,
272 pub instructions: String,
273 pub camera: Option<(i32, i32, i32, i32)>,
274}
275
276fn emit(
277 meta: &LevelMeta,
278 initial: &str,
279 solution: &schema::Document,
280 lines: &[LineEntry],
281) -> String {
282 use std::fmt::Write;
283 let mut out = String::new();
284 let title = if meta.title.trim().is_empty() {
285 meta.id.clone()
286 } else {
287 meta.title.trim().to_owned()
288 };
289 let _ = writeln!(
290 out,
291 "level {} title={} {{",
292 kdl_quote(meta.id.trim()),
293 kdl_quote(&title)
294 );
295 let _ = writeln!(
296 out,
297 " instructions {}",
298 kdl_quote(meta.instructions.trim())
299 );
300 let (cx, cy, cw, ch) = meta.camera.unwrap_or_else(|| solution_camera(solution));
301 let _ = writeln!(out, " camera x={cx} y={cy} w={cw} h={ch}");
302 let _ = writeln!(
303 out,
304 " initial {{\n{}\n }}",
305 indent(initial.trim_end(), " ")
306 );
307 let _ = writeln!(
308 out,
309 " solution {{\n{}\n }}",
310 indent(solution.to_kdl().trim_end(), " ")
311 );
312 let mut group_open = false;
313 for entry in lines {
314 match &entry.kind {
315 LineKind::Blank | LineKind::Invalid(_) => {}
316 LineKind::Marker { key, narration } => {
317 if group_open {
318 let _ = writeln!(out, " }}");
319 }
320 let _ = writeln!(
321 out,
322 " step key={} en={} {{",
323 kdl_quote(key),
324 kdl_quote(narration)
325 );
326 group_open = true;
327 }
328 LineKind::Step(_) => {
329 if !group_open {
330 let _ = writeln!(out, " step key=\"step-1\" en=\"step-1\" {{");
333 group_open = true;
334 }
335 let _ = writeln!(out, " {}", entry.source.trim());
336 }
337 }
338 }
339 if group_open {
340 let _ = writeln!(out, " }}");
341 }
342 out.push_str("}\n");
343 out
344}
345
346fn kdl_quote(s: &str) -> String {
347 let escaped = s
348 .replace('\\', "\\\\")
349 .replace('"', "\\\"")
350 .replace('\n', "\\n");
351 format!("\"{escaped}\"")
352}
353
354fn indent(kdl: &str, by: &str) -> String {
356 kdl.lines()
357 .map(|l| {
358 if l.is_empty() {
359 String::new()
360 } else {
361 format!("{by}{l}")
362 }
363 })
364 .collect::<Vec<_>>()
365 .join("\n")
366}
367
368fn solution_camera(solution: &schema::Document) -> (i32, i32, i32, i32) {
371 if let Some(top) = solution.blocks.iter().find(|b| b.id == solution.top) {
372 let (w, h) = (
373 i32::try_from(top.w).unwrap_or(i32::MAX),
374 i32::try_from(top.h).unwrap_or(i32::MAX),
375 );
376 return (top.x, top.y, w, h);
377 }
378 let xs = || solution.blocks.iter();
379 let x0 = xs().map(|b| b.x).min().unwrap_or(0);
380 let y0 = xs().map(|b| b.y).min().unwrap_or(0);
381 let far = |b: &schema::Block| b.x.saturating_add(i32::try_from(b.w).unwrap_or(i32::MAX));
382 let low = |b: &schema::Block| b.y.saturating_add(i32::try_from(b.h).unwrap_or(i32::MAX));
383 let x1 = xs().map(far).max().unwrap_or(24);
384 let y1 = xs().map(low).max().unwrap_or(20);
385 (x0 - 2, y0 - 2, (x1 - x0) + 4, (y1 - y0) + 4)
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::widget::drawing::finalize_load;
392
393 const INITIAL: &str = r#"
394top "b0"
395
396block "b0" x=0 y=0 w=24 h=20 {
397 title "sheet"
398}
399"#;
400
401 const SCRIPT: &str = r#"step key="pick" en="Pick the New Block tool"
402highlight "tool:new-block" secs=0.3
403click "tool:new-block"
404step key="place" en="Click two corners"
405move-to "8,6" secs=0.3
406click "8,6"
407move-to "16,13" secs=0.3
408click "16,13"
409type "12,14" text="CPU" secs=0.5"#;
410
411 fn initial_doc() -> Document {
412 let mut doc = crate::document_ng::schema_convert::from_kdl(INITIAL, "test").unwrap();
413 finalize_load(&mut doc);
414 doc
415 }
416
417 fn debugger_with_script() -> Debugger {
418 let mut debugger = Debugger::new(&initial_doc());
419 debugger.set_text(SCRIPT);
420 debugger
421 }
422
423 fn titles(doc: &schema::Document) -> Vec<String> {
424 doc.blocks
425 .iter()
426 .filter_map(|b| b.title.as_ref().map(|t| t.name.clone()))
427 .collect()
428 }
429
430 fn run_to_end(debugger: &mut Debugger) {
431 while debugger.step_forward() == StepOutcome::Advanced {}
432 }
433
434 #[test]
435 fn stepping_to_the_end_builds_the_block() {
436 let mut debugger = debugger_with_script();
437 run_to_end(&mut debugger);
438 assert_eq!(debugger.step_forward(), StepOutcome::AtEnd);
439 let projected = debugger.projected();
440 assert_eq!(projected.blocks.len(), 2);
441 assert!(titles(&projected).contains(&"CPU".to_owned()));
442 }
443
444 #[test]
447 fn stepping_back_and_forward_is_deterministic() {
448 let mut debugger = debugger_with_script();
449 run_to_end(&mut debugger);
450 let at_end = debugger.projected();
451
452 assert_eq!(debugger.step_back(), StepOutcome::Advanced);
453 assert!(
454 !titles(&debugger.projected()).contains(&"CPU".to_owned()),
455 "the type line should be un-done"
456 );
457 assert_eq!(debugger.step_forward(), StepOutcome::Advanced);
458 assert_eq!(debugger.projected(), at_end);
459 }
460
461 #[test]
462 fn reset_returns_to_the_initial_state() {
463 let mut debugger = debugger_with_script();
464 let initial = debugger.projected();
465 run_to_end(&mut debugger);
466 assert_ne!(debugger.projected(), initial);
467 debugger.reset();
468 assert_eq!(debugger.position(), 0);
469 assert_eq!(debugger.projected(), initial);
470 }
471
472 #[test]
475 fn execution_halts_at_the_first_invalid_line() {
476 let mut debugger = Debugger::new(&initial_doc());
477 debugger.set_text("click \"tool:new-block\"\nwiggle \"1,1\"\nmove-to \"8,6\" secs=0.3");
478 assert_eq!(debugger.step_forward(), StepOutcome::Advanced);
479 let before = debugger.projected();
480 assert_eq!(debugger.step_forward(), StepOutcome::Halted(1));
481 assert_eq!(debugger.step_forward(), StepOutcome::Halted(1));
482 assert_eq!(debugger.position(), 1);
483 assert_eq!(debugger.projected(), before);
484 }
485
486 #[test]
488 fn a_missing_target_halts_at_runtime() {
489 let mut debugger = Debugger::new(&initial_doc());
490 debugger.set_text("drag \"block:ghost\" \"4,4\" secs=0.3");
491 assert_eq!(debugger.step_forward(), StepOutcome::Halted(0));
492 assert!(debugger.runtime_halt().is_some());
493 }
494
495 #[test]
498 fn editing_an_executed_line_rewinds_to_it() {
499 let mut debugger = debugger_with_script();
500 run_to_end(&mut debugger);
501 let mut text = debugger.text();
502 text = text.replace("click \"8,6\"", "click \"9,6\"");
503 debugger.set_text(&text);
504 assert!(debugger.position() <= 5);
505 assert!(!titles(&debugger.projected()).contains(&"CPU".to_owned()));
506 }
507
508 #[test]
511 fn emitted_level_replays_to_its_solution_by_construction() {
512 let mut debugger = debugger_with_script();
513 run_to_end(&mut debugger);
514 let text = debugger
515 .emit_level(&LevelMeta {
516 id: "authored".into(),
517 title: "Authored".into(),
518 instructions: "Do the thing.".into(),
519 camera: None,
520 })
521 .unwrap();
522 let level = crate::tutorial::level::Level::parse(Box::leak(text.into_boxed_str()))
523 .unwrap_or_else(|e| panic!("{e}"));
524 assert_eq!(level.steps.len(), 2);
525 assert_eq!(level.steps[0].key, "pick");
526 assert_eq!(level.steps[1].key, "place");
527
528 let mut harness = Headless::new(level.initial_kdl, &level.id);
529 harness.run_script(&level.script).unwrap();
530 let mut solution =
531 crate::document_ng::schema_convert::from_kdl(level.solution_kdl, &level.id).unwrap();
532 finalize_load(&mut solution);
533 assert_eq!(harness.projected(), schema::Document::from(&solution));
534 }
535
536 #[test]
537 fn emission_refuses_invalid_lines_and_empty_scripts() {
538 let meta = LevelMeta {
539 id: "x".into(),
540 title: String::new(),
541 instructions: String::new(),
542 camera: None,
543 };
544 let mut debugger = Debugger::new(&initial_doc());
545 assert!(debugger.emit_level(&meta).is_err());
546 debugger.set_text("wiggle \"1,1\"");
547 assert!(debugger.emit_level(&meta).unwrap_err().contains("line 1"));
548 }
549}