use std::collections::{HashMap, HashSet, VecDeque};
pub type CellId = u64;
#[derive(Debug, Clone, Default)]
pub struct CellOutput {
pub stdout: String,
pub stderr: String,
pub value: Option<String>,
pub exec_time_ms: u64,
}
#[derive(Debug, Clone)]
pub struct Cell {
pub id: CellId,
pub source: String,
pub output: CellOutput,
pub dependencies: HashSet<String>,
pub definitions: HashSet<String>,
pub dirty: bool,
pub execution_order: u64,
}
impl Cell {
#[must_use]
pub fn new(id: CellId, source: impl Into<String>) -> Self {
let source = source.into();
let (dependencies, definitions) = Self::analyze_dependencies(&source);
Self {
id,
source,
output: CellOutput::default(),
dependencies,
definitions,
dirty: true,
execution_order: 0,
}
}
pub fn update_source(&mut self, source: impl Into<String>) {
self.source = source.into();
let (deps, defs) = Self::analyze_dependencies(&self.source);
self.dependencies = deps;
self.definitions = defs;
self.dirty = true;
}
fn analyze_dependencies(source: &str) -> (HashSet<String>, HashSet<String>) {
let mut dependencies = HashSet::new();
let mut definitions = HashSet::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("let ") {
if let Some(eq_pos) = rest.find('=') {
let name = rest[..eq_pos].trim().to_string();
definitions.insert(name);
let rhs = &rest[eq_pos + 1..];
for word in rhs.split(|c: char| !c.is_alphanumeric() && c != '_') {
let word = word.trim();
if !word.is_empty() && word.chars().next().is_some_and(char::is_alphabetic)
{
dependencies.insert(word.to_string());
}
}
}
} else {
for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
let word = word.trim();
if !word.is_empty() && word.chars().next().is_some_and(char::is_alphabetic) {
dependencies.insert(word.to_string());
}
}
}
}
for def in &definitions {
dependencies.remove(def);
}
for kw in &[
"let", "if", "else", "for", "while", "fun", "return", "true", "false",
] {
dependencies.remove(*kw);
}
(dependencies, definitions)
}
}
#[derive(Debug, Default)]
pub struct CellGraph {
cells: HashMap<CellId, Cell>,
var_to_cell: HashMap<String, CellId>,
next_id: CellId,
}
impl CellGraph {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add_cell(&mut self, source: impl Into<String>) -> CellId {
let id = self.next_id;
self.next_id += 1;
let cell = Cell::new(id, source);
for var in &cell.definitions {
self.var_to_cell.insert(var.clone(), id);
}
self.cells.insert(id, cell);
id
}
pub fn update_cell(&mut self, id: CellId, source: impl Into<String>) -> HashSet<CellId> {
let mut dirty = HashSet::new();
if let Some(cell) = self.cells.get_mut(&id) {
for var in &cell.definitions {
self.var_to_cell.remove(var);
}
cell.update_source(source);
for var in &cell.definitions {
self.var_to_cell.insert(var.clone(), id);
}
dirty.insert(id);
}
self.propagate_dirty(id, &mut dirty);
dirty
}
pub fn remove_cell(&mut self, id: CellId) -> Option<Cell> {
if let Some(cell) = self.cells.remove(&id) {
for var in &cell.definitions {
if self.var_to_cell.get(var) == Some(&id) {
self.var_to_cell.remove(var);
}
}
Some(cell)
} else {
None
}
}
#[must_use]
pub fn get_cell(&self, id: CellId) -> Option<&Cell> {
self.cells.get(&id)
}
pub fn get_cell_mut(&mut self, id: CellId) -> Option<&mut Cell> {
self.cells.get_mut(&id)
}
pub fn cells(&self) -> impl Iterator<Item = &Cell> {
self.cells.values()
}
#[must_use]
pub fn len(&self) -> usize {
self.cells.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
fn propagate_dirty(&mut self, from: CellId, dirty: &mut HashSet<CellId>) {
let definitions: HashSet<String> = self
.cells
.get(&from)
.map(|c| c.definitions.clone())
.unwrap_or_default();
for (id, cell) in &mut self.cells {
if *id == from {
continue;
}
if cell.dependencies.iter().any(|d| definitions.contains(d)) {
cell.dirty = true;
dirty.insert(*id);
}
}
}
#[must_use]
pub fn topological_order(&self) -> Option<Vec<CellId>> {
let mut in_degree: HashMap<CellId, usize> = HashMap::new();
let mut edges: HashMap<CellId, Vec<CellId>> = HashMap::new();
for id in self.cells.keys() {
in_degree.insert(*id, 0);
edges.insert(*id, Vec::new());
}
for (id, cell) in &self.cells {
for dep in &cell.dependencies {
if let Some(&def_cell) = self.var_to_cell.get(dep) {
if def_cell != *id {
edges.entry(def_cell).or_default().push(*id);
*in_degree.entry(*id).or_default() += 1;
}
}
}
}
let mut queue: VecDeque<CellId> = in_degree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(&id, _)| id)
.collect();
let mut result = Vec::with_capacity(self.cells.len());
while let Some(id) = queue.pop_front() {
result.push(id);
if let Some(neighbors) = edges.get(&id) {
for &neighbor in neighbors {
if let Some(deg) = in_degree.get_mut(&neighbor) {
*deg -= 1;
if *deg == 0 {
queue.push_back(neighbor);
}
}
}
}
}
if result.len() == self.cells.len() {
Some(result)
} else {
None }
}
#[must_use]
pub fn cells_to_execute(&self) -> Vec<CellId> {
self.topological_order()
.unwrap_or_default()
.into_iter()
.filter(|id| self.cells.get(id).is_some_and(|c| c.dirty))
.collect()
}
}
#[derive(Debug, Default)]
pub struct NotebookRuntime {
graph: CellGraph,
execution_counter: u64,
namespace: HashMap<String, String>,
}
impl NotebookRuntime {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add_cell(&mut self, source: impl Into<String>) -> CellId {
self.graph.add_cell(source)
}
pub fn update_cell(&mut self, id: CellId, source: impl Into<String>) -> HashSet<CellId> {
self.graph.update_cell(id, source)
}
pub fn remove_cell(&mut self, id: CellId) -> Option<Cell> {
self.graph.remove_cell(id)
}
#[must_use]
pub fn get_cell(&self, id: CellId) -> Option<&Cell> {
self.graph.get_cell(id)
}
pub fn cells(&self) -> impl Iterator<Item = &Cell> {
self.graph.cells()
}
#[must_use]
pub fn cells_in_order(&self) -> Vec<CellId> {
self.graph.topological_order().unwrap_or_default()
}
#[must_use]
pub fn dirty_cells(&self) -> Vec<CellId> {
self.graph.cells_to_execute()
}
pub fn execute_cell(&mut self, id: CellId) -> Option<&CellOutput> {
self.execution_counter += 1;
let counter = self.execution_counter;
if let Some(cell) = self.graph.get_cell_mut(id) {
cell.dirty = false;
cell.execution_order = counter;
cell.output = CellOutput {
stdout: String::new(),
stderr: String::new(),
value: Some(format!("/* executed cell {} */", id)),
exec_time_ms: 0,
};
for var in &cell.definitions {
self.namespace.insert(var.clone(), "null".to_string());
}
return Some(&cell.output);
}
None
}
pub fn execute_dirty(&mut self) -> Vec<(CellId, CellOutput)> {
let to_execute = self.dirty_cells();
let mut results = Vec::with_capacity(to_execute.len());
for id in to_execute {
if let Some(output) = self.execute_cell(id) {
results.push((id, output.clone()));
}
}
results
}
pub fn execute_all(&mut self) -> Vec<(CellId, CellOutput)> {
for cell in self.graph.cells.values_mut() {
cell.dirty = true;
}
self.execute_dirty()
}
#[must_use]
pub fn namespace(&self) -> &HashMap<String, String> {
&self.namespace
}
pub fn clear_namespace(&mut self) {
self.namespace.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.graph.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.graph.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_creation() {
let cell = Cell::new(1, "let x = 10");
assert_eq!(cell.id, 1);
assert!(cell.definitions.contains("x"));
assert!(!cell.dependencies.contains("x")); }
#[test]
fn test_cell_dependencies() {
let cell = Cell::new(1, "let y = x * 2");
assert!(cell.definitions.contains("y"));
assert!(cell.dependencies.contains("x"));
}
#[test]
fn test_cell_graph_add() {
let mut graph = CellGraph::new();
let id = graph.add_cell("let x = 10");
assert_eq!(graph.len(), 1);
assert!(graph.get_cell(id).is_some());
}
#[test]
fn test_cell_graph_topological_order() {
let mut graph = CellGraph::new();
let a = graph.add_cell("let x = 10");
let b = graph.add_cell("let y = x * 2");
let c = graph.add_cell("let z = x + y");
let order = graph.topological_order().unwrap();
let pos_a = order.iter().position(|&id| id == a).unwrap();
let pos_b = order.iter().position(|&id| id == b).unwrap();
let pos_c = order.iter().position(|&id| id == c).unwrap();
assert!(pos_a < pos_b);
assert!(pos_a < pos_c);
assert!(pos_b < pos_c); }
#[test]
fn test_cell_graph_update_propagates_dirty() {
let mut graph = CellGraph::new();
let a = graph.add_cell("let x = 10");
let b = graph.add_cell("let y = x * 2");
graph.get_cell_mut(a).unwrap().dirty = false;
graph.get_cell_mut(b).unwrap().dirty = false;
let dirty = graph.update_cell(a, "let x = 20");
assert!(dirty.contains(&a));
assert!(dirty.contains(&b)); }
#[test]
fn test_notebook_runtime_basic() {
let mut notebook = NotebookRuntime::new();
let a = notebook.add_cell("let x = 10");
let b = notebook.add_cell("let y = x * 2");
assert_eq!(notebook.len(), 2);
let results = notebook.execute_all();
assert_eq!(results.len(), 2);
assert!(!notebook.get_cell(a).unwrap().dirty);
assert!(!notebook.get_cell(b).unwrap().dirty);
}
#[test]
fn test_notebook_runtime_reactive_update() {
let mut notebook = NotebookRuntime::new();
let a = notebook.add_cell("let x = 10");
let b = notebook.add_cell("let y = x * 2");
notebook.execute_all();
let dirty = notebook.update_cell(a, "let x = 20");
assert!(dirty.contains(&a));
assert!(dirty.contains(&b));
let results = notebook.execute_dirty();
assert_eq!(results.len(), 2);
}
#[test]
fn test_notebook_remove_cell() {
let mut notebook = NotebookRuntime::new();
let a = notebook.add_cell("let x = 10");
assert_eq!(notebook.len(), 1);
let removed = notebook.remove_cell(a);
assert!(removed.is_some());
assert_eq!(notebook.len(), 0);
}
#[test]
fn test_cell_graph_cycle_detection() {
let mut graph = CellGraph::new();
let _ = graph.add_cell("let x = 10");
let _ = graph.add_cell("let y = x * 2");
assert!(graph.topological_order().is_some());
}
#[test]
fn test_cell_expression_dependencies() {
let cell = Cell::new(1, "x + y + z");
assert!(cell.dependencies.contains("x"));
assert!(cell.dependencies.contains("y"));
assert!(cell.dependencies.contains("z"));
assert!(cell.definitions.is_empty());
}
}