Skip to main content

aprender_present_lib/browser/
notebook.rs

1//! WASM Notebook Runtime with Reactive Cell Execution.
2//!
3//! Implements GitHub issue #6 - Notebook runtime for Ruchy integration.
4//!
5//! # Design
6//!
7//! - Dependency-aware cell execution order (topological sort)
8//! - Automatic re-execution on upstream changes
9//! - Cell state persistence
10//!
11//! # Example
12//!
13//! ```ignore
14//! let mut notebook = NotebookRuntime::new();
15//!
16//! // Add cells
17//! let a = notebook.add_cell("let x = 10");
18//! let b = notebook.add_cell("let y = x * 2");  // depends on a
19//! let c = notebook.add_cell("x + y");          // depends on a, b
20//!
21//! // Execute all cells in dependency order
22//! notebook.execute_all();
23//!
24//! // Update cell a - cells b and c re-execute automatically
25//! notebook.update_cell(a, "let x = 20");
26//! ```
27
28use std::collections::{HashMap, HashSet, VecDeque};
29
30/// Unique identifier for a notebook cell.
31pub type CellId = u64;
32
33/// Output from a cell execution.
34#[derive(Debug, Clone, Default)]
35pub struct CellOutput {
36    /// Standard output text
37    pub stdout: String,
38    /// Error output text (if any)
39    pub stderr: String,
40    /// Return value (serialized)
41    pub value: Option<String>,
42    /// Execution time in milliseconds
43    pub exec_time_ms: u64,
44}
45
46/// A single notebook cell with source code and execution state.
47#[derive(Debug, Clone)]
48pub struct Cell {
49    /// Unique identifier
50    pub id: CellId,
51    /// Source code
52    pub source: String,
53    /// Last execution output
54    pub output: CellOutput,
55    /// Variables this cell depends on
56    pub dependencies: HashSet<String>,
57    /// Variables this cell defines
58    pub definitions: HashSet<String>,
59    /// Whether cell needs re-execution
60    pub dirty: bool,
61    /// Cell execution order (0 = not executed)
62    pub execution_order: u64,
63}
64
65impl Cell {
66    /// Create a new cell with the given source code.
67    #[must_use]
68    pub fn new(id: CellId, source: impl Into<String>) -> Self {
69        let source = source.into();
70        let (dependencies, definitions) = Self::analyze_dependencies(&source);
71
72        Self {
73            id,
74            source,
75            output: CellOutput::default(),
76            dependencies,
77            definitions,
78            dirty: true,
79            execution_order: 0,
80        }
81    }
82
83    /// Update the cell's source code and re-analyze dependencies.
84    pub fn update_source(&mut self, source: impl Into<String>) {
85        self.source = source.into();
86        let (deps, defs) = Self::analyze_dependencies(&self.source);
87        self.dependencies = deps;
88        self.definitions = defs;
89        self.dirty = true;
90    }
91
92    /// Analyze source code to extract variable dependencies and definitions.
93    ///
94    /// This is a simplified analysis - a real implementation would use the Ruchy parser.
95    fn analyze_dependencies(source: &str) -> (HashSet<String>, HashSet<String>) {
96        let mut dependencies = HashSet::new();
97        let mut definitions = HashSet::new();
98
99        for line in source.lines() {
100            let trimmed = line.trim();
101
102            // Simple pattern: "let x = ..." or "var x = ..."
103            if let Some(rest) = trimmed.strip_prefix("let ") {
104                if let Some(eq_pos) = rest.find('=') {
105                    let name = rest[..eq_pos].trim().to_string();
106                    definitions.insert(name);
107
108                    // Simple: everything after '=' are potential dependencies
109                    let rhs = &rest[eq_pos + 1..];
110                    for word in rhs.split(|c: char| !c.is_alphanumeric() && c != '_') {
111                        let word = word.trim();
112                        if !word.is_empty() && word.chars().next().is_some_and(char::is_alphabetic)
113                        {
114                            dependencies.insert(word.to_string());
115                        }
116                    }
117                }
118            } else {
119                // Expression - all identifiers are dependencies
120                for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
121                    let word = word.trim();
122                    if !word.is_empty() && word.chars().next().is_some_and(char::is_alphabetic) {
123                        dependencies.insert(word.to_string());
124                    }
125                }
126            }
127        }
128
129        // Remove self-definitions from dependencies
130        for def in &definitions {
131            dependencies.remove(def);
132        }
133
134        // Remove keywords
135        for kw in &[
136            "let", "if", "else", "for", "while", "fun", "return", "true", "false",
137        ] {
138            dependencies.remove(*kw);
139        }
140
141        (dependencies, definitions)
142    }
143}
144
145/// Directed acyclic graph tracking cell dependencies.
146#[derive(Debug, Default)]
147pub struct CellGraph {
148    /// All cells in the notebook
149    cells: HashMap<CellId, Cell>,
150    /// Variable name -> cell that defines it
151    var_to_cell: HashMap<String, CellId>,
152    /// Next cell ID
153    next_id: CellId,
154}
155
156impl CellGraph {
157    /// Create a new empty cell graph.
158    #[must_use]
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Add a new cell to the graph.
164    #[must_use]
165    pub fn add_cell(&mut self, source: impl Into<String>) -> CellId {
166        let id = self.next_id;
167        self.next_id += 1;
168
169        let cell = Cell::new(id, source);
170
171        // Register variable definitions
172        for var in &cell.definitions {
173            self.var_to_cell.insert(var.clone(), id);
174        }
175
176        self.cells.insert(id, cell);
177        id
178    }
179
180    /// Update a cell's source code.
181    ///
182    /// Returns the set of cells that need re-execution.
183    pub fn update_cell(&mut self, id: CellId, source: impl Into<String>) -> HashSet<CellId> {
184        let mut dirty = HashSet::new();
185
186        if let Some(cell) = self.cells.get_mut(&id) {
187            // Remove old definitions
188            for var in &cell.definitions {
189                self.var_to_cell.remove(var);
190            }
191
192            // Update source
193            cell.update_source(source);
194
195            // Register new definitions
196            for var in &cell.definitions {
197                self.var_to_cell.insert(var.clone(), id);
198            }
199
200            dirty.insert(id);
201        }
202
203        // Mark all downstream cells as dirty
204        self.propagate_dirty(id, &mut dirty);
205
206        dirty
207    }
208
209    /// Remove a cell from the graph.
210    pub fn remove_cell(&mut self, id: CellId) -> Option<Cell> {
211        if let Some(cell) = self.cells.remove(&id) {
212            // Remove variable definitions
213            for var in &cell.definitions {
214                if self.var_to_cell.get(var) == Some(&id) {
215                    self.var_to_cell.remove(var);
216                }
217            }
218            Some(cell)
219        } else {
220            None
221        }
222    }
223
224    /// Get a cell by ID.
225    #[must_use]
226    pub fn get_cell(&self, id: CellId) -> Option<&Cell> {
227        self.cells.get(&id)
228    }
229
230    /// Get a mutable reference to a cell.
231    pub fn get_cell_mut(&mut self, id: CellId) -> Option<&mut Cell> {
232        self.cells.get_mut(&id)
233    }
234
235    /// Get all cells.
236    pub fn cells(&self) -> impl Iterator<Item = &Cell> {
237        self.cells.values()
238    }
239
240    /// Get the number of cells.
241    #[must_use]
242    pub fn len(&self) -> usize {
243        self.cells.len()
244    }
245
246    /// Check if the graph is empty.
247    #[must_use]
248    pub fn is_empty(&self) -> bool {
249        self.cells.is_empty()
250    }
251
252    /// Propagate dirty flag to all downstream cells.
253    fn propagate_dirty(&mut self, from: CellId, dirty: &mut HashSet<CellId>) {
254        // Get definitions from the source cell
255        let definitions: HashSet<String> = self
256            .cells
257            .get(&from)
258            .map(|c| c.definitions.clone())
259            .unwrap_or_default();
260
261        // Find all cells that depend on these definitions
262        for (id, cell) in &mut self.cells {
263            if *id == from {
264                continue;
265            }
266
267            // Check if this cell depends on any of the definitions
268            if cell.dependencies.iter().any(|d| definitions.contains(d)) {
269                cell.dirty = true;
270                dirty.insert(*id);
271            }
272        }
273    }
274
275    /// Get cells in topological order based on dependencies.
276    ///
277    /// Returns `None` if there's a cycle.
278    #[must_use]
279    pub fn topological_order(&self) -> Option<Vec<CellId>> {
280        let mut in_degree: HashMap<CellId, usize> = HashMap::new();
281        let mut edges: HashMap<CellId, Vec<CellId>> = HashMap::new();
282
283        // Initialize in-degrees
284        for id in self.cells.keys() {
285            in_degree.insert(*id, 0);
286            edges.insert(*id, Vec::new());
287        }
288
289        // Build edges: if cell B depends on variable defined in cell A, add edge A -> B
290        for (id, cell) in &self.cells {
291            for dep in &cell.dependencies {
292                if let Some(&def_cell) = self.var_to_cell.get(dep) {
293                    if def_cell != *id {
294                        edges.entry(def_cell).or_default().push(*id);
295                        *in_degree.entry(*id).or_default() += 1;
296                    }
297                }
298            }
299        }
300
301        // Kahn's algorithm
302        let mut queue: VecDeque<CellId> = in_degree
303            .iter()
304            .filter(|(_, &deg)| deg == 0)
305            .map(|(&id, _)| id)
306            .collect();
307
308        let mut result = Vec::with_capacity(self.cells.len());
309
310        while let Some(id) = queue.pop_front() {
311            result.push(id);
312
313            if let Some(neighbors) = edges.get(&id) {
314                for &neighbor in neighbors {
315                    if let Some(deg) = in_degree.get_mut(&neighbor) {
316                        *deg -= 1;
317                        if *deg == 0 {
318                            queue.push_back(neighbor);
319                        }
320                    }
321                }
322            }
323        }
324
325        if result.len() == self.cells.len() {
326            Some(result)
327        } else {
328            None // Cycle detected
329        }
330    }
331
332    /// Get cells that need execution (dirty cells in topological order).
333    #[must_use]
334    pub fn cells_to_execute(&self) -> Vec<CellId> {
335        self.topological_order()
336            .unwrap_or_default()
337            .into_iter()
338            .filter(|id| self.cells.get(id).is_some_and(|c| c.dirty))
339            .collect()
340    }
341}
342
343/// Notebook runtime with reactive cell execution.
344#[derive(Debug, Default)]
345pub struct NotebookRuntime {
346    /// Cell dependency graph
347    graph: CellGraph,
348    /// Global execution counter
349    execution_counter: u64,
350    /// Shared variable namespace (name -> value as JSON)
351    namespace: HashMap<String, String>,
352}
353
354impl NotebookRuntime {
355    /// Create a new notebook runtime.
356    #[must_use]
357    pub fn new() -> Self {
358        Self::default()
359    }
360
361    /// Add a new cell to the notebook.
362    #[must_use]
363    pub fn add_cell(&mut self, source: impl Into<String>) -> CellId {
364        self.graph.add_cell(source)
365    }
366
367    /// Update a cell's source code.
368    ///
369    /// Returns the set of cells that need re-execution.
370    pub fn update_cell(&mut self, id: CellId, source: impl Into<String>) -> HashSet<CellId> {
371        self.graph.update_cell(id, source)
372    }
373
374    /// Remove a cell from the notebook.
375    pub fn remove_cell(&mut self, id: CellId) -> Option<Cell> {
376        self.graph.remove_cell(id)
377    }
378
379    /// Get a cell by ID.
380    #[must_use]
381    pub fn get_cell(&self, id: CellId) -> Option<&Cell> {
382        self.graph.get_cell(id)
383    }
384
385    /// Get all cells.
386    pub fn cells(&self) -> impl Iterator<Item = &Cell> {
387        self.graph.cells()
388    }
389
390    /// Get cells in execution order.
391    #[must_use]
392    pub fn cells_in_order(&self) -> Vec<CellId> {
393        self.graph.topological_order().unwrap_or_default()
394    }
395
396    /// Get cells that need execution.
397    #[must_use]
398    pub fn dirty_cells(&self) -> Vec<CellId> {
399        self.graph.cells_to_execute()
400    }
401
402    /// Execute a single cell.
403    ///
404    /// This is a stub - the actual execution would be done by the Ruchy runtime.
405    pub fn execute_cell(&mut self, id: CellId) -> Option<&CellOutput> {
406        self.execution_counter += 1;
407        let counter = self.execution_counter;
408
409        if let Some(cell) = self.graph.get_cell_mut(id) {
410            // Placeholder: Ruchy runtime integration pending
411            // Currently marks cell as executed without actual evaluation
412            cell.dirty = false;
413            cell.execution_order = counter;
414            cell.output = CellOutput {
415                stdout: String::new(),
416                stderr: String::new(),
417                value: Some(format!("/* executed cell {} */", id)),
418                exec_time_ms: 0,
419            };
420
421            // Register defined variables
422            for var in &cell.definitions {
423                self.namespace.insert(var.clone(), "null".to_string());
424            }
425
426            return Some(&cell.output);
427        }
428        None
429    }
430
431    /// Execute all dirty cells in dependency order.
432    pub fn execute_dirty(&mut self) -> Vec<(CellId, CellOutput)> {
433        let to_execute = self.dirty_cells();
434        let mut results = Vec::with_capacity(to_execute.len());
435
436        for id in to_execute {
437            if let Some(output) = self.execute_cell(id) {
438                results.push((id, output.clone()));
439            }
440        }
441
442        results
443    }
444
445    /// Execute all cells in dependency order.
446    pub fn execute_all(&mut self) -> Vec<(CellId, CellOutput)> {
447        // Mark all cells as dirty
448        for cell in self.graph.cells.values_mut() {
449            cell.dirty = true;
450        }
451
452        self.execute_dirty()
453    }
454
455    /// Get the shared namespace.
456    #[must_use]
457    pub fn namespace(&self) -> &HashMap<String, String> {
458        &self.namespace
459    }
460
461    /// Clear the namespace.
462    pub fn clear_namespace(&mut self) {
463        self.namespace.clear();
464    }
465
466    /// Get the number of cells.
467    #[must_use]
468    pub fn len(&self) -> usize {
469        self.graph.len()
470    }
471
472    /// Check if the notebook is empty.
473    #[must_use]
474    pub fn is_empty(&self) -> bool {
475        self.graph.is_empty()
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_cell_creation() {
485        let cell = Cell::new(1, "let x = 10");
486        assert_eq!(cell.id, 1);
487        assert!(cell.definitions.contains("x"));
488        assert!(!cell.dependencies.contains("x")); // x is defined, not a dependency
489    }
490
491    #[test]
492    fn test_cell_dependencies() {
493        let cell = Cell::new(1, "let y = x * 2");
494        assert!(cell.definitions.contains("y"));
495        assert!(cell.dependencies.contains("x"));
496    }
497
498    #[test]
499    fn test_cell_graph_add() {
500        let mut graph = CellGraph::new();
501        let id = graph.add_cell("let x = 10");
502        assert_eq!(graph.len(), 1);
503        assert!(graph.get_cell(id).is_some());
504    }
505
506    #[test]
507    fn test_cell_graph_topological_order() {
508        let mut graph = CellGraph::new();
509        let a = graph.add_cell("let x = 10");
510        let b = graph.add_cell("let y = x * 2");
511        let c = graph.add_cell("let z = x + y");
512
513        let order = graph.topological_order().unwrap();
514
515        // a must come before b and c
516        let pos_a = order.iter().position(|&id| id == a).unwrap();
517        let pos_b = order.iter().position(|&id| id == b).unwrap();
518        let pos_c = order.iter().position(|&id| id == c).unwrap();
519
520        assert!(pos_a < pos_b);
521        assert!(pos_a < pos_c);
522        assert!(pos_b < pos_c); // b defines y which c depends on
523    }
524
525    #[test]
526    fn test_cell_graph_update_propagates_dirty() {
527        let mut graph = CellGraph::new();
528        let a = graph.add_cell("let x = 10");
529        let b = graph.add_cell("let y = x * 2");
530
531        // Clear dirty flags
532        graph.get_cell_mut(a).unwrap().dirty = false;
533        graph.get_cell_mut(b).unwrap().dirty = false;
534
535        // Update cell a
536        let dirty = graph.update_cell(a, "let x = 20");
537
538        assert!(dirty.contains(&a));
539        assert!(dirty.contains(&b)); // b depends on x, should be marked dirty
540    }
541
542    #[test]
543    fn test_notebook_runtime_basic() {
544        let mut notebook = NotebookRuntime::new();
545        let a = notebook.add_cell("let x = 10");
546        let b = notebook.add_cell("let y = x * 2");
547
548        assert_eq!(notebook.len(), 2);
549
550        // Execute all
551        let results = notebook.execute_all();
552        assert_eq!(results.len(), 2);
553
554        // Cells should be marked as not dirty
555        assert!(!notebook.get_cell(a).unwrap().dirty);
556        assert!(!notebook.get_cell(b).unwrap().dirty);
557    }
558
559    #[test]
560    fn test_notebook_runtime_reactive_update() {
561        let mut notebook = NotebookRuntime::new();
562        let a = notebook.add_cell("let x = 10");
563        let b = notebook.add_cell("let y = x * 2");
564
565        // Execute all
566        notebook.execute_all();
567
568        // Update cell a
569        let dirty = notebook.update_cell(a, "let x = 20");
570
571        // Both cells should be dirty
572        assert!(dirty.contains(&a));
573        assert!(dirty.contains(&b));
574
575        // Execute dirty cells
576        let results = notebook.execute_dirty();
577        assert_eq!(results.len(), 2);
578    }
579
580    #[test]
581    fn test_notebook_remove_cell() {
582        let mut notebook = NotebookRuntime::new();
583        let a = notebook.add_cell("let x = 10");
584
585        assert_eq!(notebook.len(), 1);
586
587        let removed = notebook.remove_cell(a);
588        assert!(removed.is_some());
589        assert_eq!(notebook.len(), 0);
590    }
591
592    #[test]
593    fn test_cell_graph_cycle_detection() {
594        let mut graph = CellGraph::new();
595
596        // Create a simple graph without cycles
597        let _ = graph.add_cell("let x = 10");
598        let _ = graph.add_cell("let y = x * 2");
599
600        // Should have valid topological order
601        assert!(graph.topological_order().is_some());
602    }
603
604    #[test]
605    fn test_cell_expression_dependencies() {
606        let cell = Cell::new(1, "x + y + z");
607        assert!(cell.dependencies.contains("x"));
608        assert!(cell.dependencies.contains("y"));
609        assert!(cell.dependencies.contains("z"));
610        assert!(cell.definitions.is_empty());
611    }
612}