Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/wasm/notebook.rs
Line
Count
Source
1
//! WebAssembly Notebook support for Ruchy
2
//!
3
//! Provides Jupyter-style notebook functionality in the browser.
4
5
use std::collections::HashMap;
6
7
#[cfg(target_arch = "wasm32")]
8
use wasm_bindgen::prelude::*;
9
10
#[cfg(not(target_arch = "wasm32"))]
11
type JsValue = String;
12
13
#[cfg(not(target_arch = "wasm32"))]
14
use serde::{Serialize, Deserialize};
15
16
// ============================================================================
17
// Notebook Types
18
// ============================================================================
19
20
#[derive(Debug, Clone)]
21
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
22
pub struct NotebookCell {
23
    pub id: String,
24
    pub cell_type: CellType,
25
    pub source: String,
26
    pub outputs: Vec<CellOutput>,
27
    pub execution_count: Option<usize>,
28
    pub metadata: CellMetadata,
29
}
30
31
#[derive(Debug, Clone)]
32
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
33
pub enum CellType {
34
    Code,
35
    Markdown,
36
}
37
38
#[derive(Debug, Clone)]
39
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
40
pub enum CellOutput {
41
    Text(String),
42
    Html(String),
43
    Image { data: String, mime_type: String },
44
    DataFrame(DataFrameOutput),
45
    Error { message: String, traceback: Vec<String> },
46
}
47
48
#[derive(Debug, Clone)]
49
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
50
pub struct DataFrameOutput {
51
    pub columns: Vec<String>,
52
    pub rows: Vec<Vec<String>>,
53
    pub shape: (usize, usize),
54
}
55
56
#[derive(Debug, Clone, Default)]
57
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
58
pub struct CellMetadata {
59
    pub collapsed: bool,
60
    pub execution_time_ms: Option<f64>,
61
    pub tags: Vec<String>,
62
}
63
64
// ============================================================================
65
// Notebook Document
66
// ============================================================================
67
68
#[derive(Debug, Clone)]
69
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
70
pub struct Notebook {
71
    pub version: String,
72
    pub metadata: NotebookMetadata,
73
    pub cells: Vec<NotebookCell>,
74
}
75
76
#[derive(Debug, Clone)]
77
#[cfg_attr(not(target_arch = "wasm32"), derive(Serialize, Deserialize))]
78
pub struct NotebookMetadata {
79
    pub kernel: String,
80
    pub language: String,
81
    pub created: String,
82
    pub modified: String,
83
    pub ruchy_version: String,
84
}
85
86
// ============================================================================
87
// Notebook Runtime
88
// ============================================================================
89
90
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
91
pub struct NotebookRuntime {
92
    notebook: Notebook,
93
    repl: crate::wasm::repl::WasmRepl,
94
    execution_count: usize,
95
    variables: HashMap<String, String>,
96
}
97
98
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
99
impl NotebookRuntime {
100
    /// Create a new notebook runtime
101
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
102
2
    pub fn new() -> Result<NotebookRuntime, JsValue> {
103
        #[cfg(target_arch = "wasm32")]
104
        let repl = crate::wasm::repl::WasmRepl::new()?;
105
        #[cfg(not(target_arch = "wasm32"))]
106
2
        let repl = crate::wasm::repl::WasmRepl::new().map_err(|_| 
"Error creating REPL"0
.
to_string0
())
?0
;
107
2
        Ok(NotebookRuntime {
108
2
            notebook: Notebook {
109
2
                version: "1.0.0".to_string(),
110
2
                metadata: NotebookMetadata {
111
2
                    kernel: "wasm".to_string(),
112
2
                    language: "ruchy".to_string(),
113
2
                    created: current_timestamp(),
114
2
                    modified: current_timestamp(),
115
2
                    ruchy_version: env!("CARGO_PKG_VERSION").to_string(),
116
2
                },
117
2
                cells: Vec::new(),
118
2
            },
119
2
            repl,
120
2
            execution_count: 0,
121
2
            variables: HashMap::new(),
122
2
        })
123
2
    }
124
    
125
    /// Add a new cell to the notebook
126
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
127
1
    pub fn add_cell(&mut self, cell_type: &str, source: &str) -> String {
128
1
        let id = generate_cell_id();
129
1
        let cell = NotebookCell {
130
1
            id: id.clone(),
131
1
            cell_type: match cell_type {
132
1
                "markdown" => 
CellType::Markdown0
,
133
1
                _ => CellType::Code,
134
            },
135
1
            source: source.to_string(),
136
1
            outputs: Vec::new(),
137
1
            execution_count: None,
138
1
            metadata: CellMetadata::default(),
139
        };
140
        
141
1
        self.notebook.cells.push(cell);
142
1
        self.notebook.metadata.modified = current_timestamp();
143
        
144
1
        id
145
1
    }
146
    
147
    /// Execute a cell by ID
148
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
149
0
    pub fn execute_cell(&mut self, cell_id: &str) -> Result<String, JsValue> {
150
0
        let cell = self.notebook.cells.iter_mut()
151
0
            .find(|c| c.id == cell_id)
152
0
            .ok_or_else(|| {
153
                #[cfg(target_arch = "wasm32")]
154
                return JsValue::from_str("Cell not found");
155
                #[cfg(not(target_arch = "wasm32"))]
156
0
                return "Cell not found".to_string();
157
0
            })?;
158
        
159
0
        match cell.cell_type {
160
            CellType::Code => {
161
0
                let start = get_timestamp();
162
                
163
                // Execute the code
164
0
                let result = self.repl.eval(&cell.source).map_err(|e| {
165
                    #[cfg(target_arch = "wasm32")]
166
                    return e;
167
                    #[cfg(not(target_arch = "wasm32"))]
168
0
                    return format!("Eval error: {e:?}");
169
0
                })?;
170
                
171
                // Update execution count
172
0
                self.execution_count += 1;
173
0
                cell.execution_count = Some(self.execution_count);
174
                
175
                // Parse result and create output
176
0
                let output = if result.contains("error") {
177
0
                    CellOutput::Error {
178
0
                        message: result,
179
0
                        traceback: vec![],
180
0
                    }
181
                } else {
182
0
                    CellOutput::Text(result)
183
                };
184
                
185
0
                cell.outputs = vec![output];
186
0
                cell.metadata.execution_time_ms = Some(get_timestamp() - start);
187
                
188
0
                Ok(serde_json::to_string(&cell).unwrap_or_else(|_| "Error".to_string()))
189
            }
190
            CellType::Markdown => {
191
                // Markdown cells don't execute
192
0
                Ok(String::new())
193
            }
194
        }
195
0
    }
196
    
197
    /// Get all cells as JSON
198
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
199
0
    pub fn get_cells(&self) -> String {
200
0
        serde_json::to_string(&self.notebook.cells)
201
0
            .unwrap_or_else(|_| "[]".to_string())
202
0
    }
203
    
204
    /// Save notebook to JSON
205
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
206
0
    pub fn to_json(&self) -> String {
207
0
        serde_json::to_string(&self.notebook)
208
0
            .unwrap_or_else(|_| "{}".to_string())
209
0
    }
210
    
211
    /// Load notebook from JSON
212
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
213
0
    pub fn from_json(&mut self, json: &str) -> Result<(), JsValue> {
214
0
        let notebook: Notebook = serde_json::from_str(json)
215
0
            .map_err(|e| {
216
                #[cfg(target_arch = "wasm32")]
217
                return JsValue::from_str(&format!("Parse error: {}", e));
218
                #[cfg(not(target_arch = "wasm32"))]
219
0
                return format!("Parse error: {e}");
220
0
            })?;
221
0
        self.notebook = notebook;
222
0
        Ok(())
223
0
    }
224
}
225
226
impl Default for NotebookRuntime {
227
0
    fn default() -> Self {
228
0
        Self::new().unwrap()
229
0
    }
230
}
231
232
// ============================================================================
233
// Helper Functions
234
// ============================================================================
235
236
1
fn generate_cell_id() -> String {
237
1
    format!("cell-{}", get_timestamp().abs() as u64)
238
1
}
239
240
5
fn current_timestamp() -> String {
241
    #[cfg(target_arch = "wasm32")]
242
    {
243
        js_sys::Date::new_0().to_iso_string().as_string().unwrap()
244
    }
245
    #[cfg(not(target_arch = "wasm32"))]
246
    {
247
5
        chrono::Utc::now().to_rfc3339()
248
    }
249
5
}
250
251
1
fn get_timestamp() -> f64 {
252
    #[cfg(target_arch = "wasm32")]
253
    {
254
        js_sys::Date::now()
255
    }
256
    #[cfg(not(target_arch = "wasm32"))]
257
    {
258
1
        std::time::SystemTime::now()
259
1
            .duration_since(std::time::UNIX_EPOCH)
260
1
            .unwrap()
261
1
            .as_millis() as f64
262
    }
263
1
}
264
265
// ============================================================================
266
// WASM/Native Feature Parity
267
// ============================================================================
268
269
/// Feature availability in WASM vs Native
270
#[derive(Debug, Clone)]
271
pub struct FeatureParity {
272
    pub feature: String,
273
    pub native_support: bool,
274
    pub wasm_support: bool,
275
    pub notes: String,
276
}
277
278
impl FeatureParity {
279
1
    pub fn check_all() -> Vec<FeatureParity> {
280
1
        vec![
281
1
            FeatureParity {
282
1
                feature: "Basic Evaluation".to_string(),
283
1
                native_support: true,
284
1
                wasm_support: true,
285
1
                notes: "Full support".to_string(),
286
1
            },
287
1
            FeatureParity {
288
1
                feature: "File I/O".to_string(),
289
1
                native_support: true,
290
1
                wasm_support: false,
291
1
                notes: "WASM uses OPFS or IndexedDB".to_string(),
292
1
            },
293
1
            FeatureParity {
294
1
                feature: "Threading".to_string(),
295
1
                native_support: true,
296
1
                wasm_support: false,
297
1
                notes: "WASM uses Web Workers".to_string(),
298
1
            },
299
1
            FeatureParity {
300
1
                feature: "Networking".to_string(),
301
1
                native_support: true,
302
1
                wasm_support: false,
303
1
                notes: "WASM limited to Fetch API".to_string(),
304
1
            },
305
1
            FeatureParity {
306
1
                feature: "DataFrames".to_string(),
307
1
                native_support: true,
308
1
                wasm_support: true,
309
1
                notes: "Limited size in WASM".to_string(),
310
1
            },
311
        ]
312
1
    }
313
}
314
315
#[cfg(test)]
316
mod tests {
317
    use super::*;
318
    
319
    #[test]
320
1
    fn test_notebook_creation() {
321
1
        let runtime = NotebookRuntime::new();
322
1
        assert!(runtime.is_ok());
323
1
    }
324
    
325
    #[test]
326
1
    fn test_add_cell() {
327
1
        let mut runtime = NotebookRuntime::new().unwrap();
328
1
        let id = runtime.add_cell("code", "let x = 42");
329
1
        assert!(id.starts_with("cell-"));
330
1
        assert_eq!(runtime.notebook.cells.len(), 1);
331
1
    }
332
    
333
    #[test]
334
1
    fn test_feature_parity() {
335
1
        let features = FeatureParity::check_all();
336
1
        assert!(!features.is_empty());
337
        
338
        // Check that basic evaluation is supported in both
339
1
        let basic = features.iter()
340
1
            .find(|f| f.feature == "Basic Evaluation")
341
1
            .unwrap();
342
1
        assert!(basic.native_support);
343
1
        assert!(basic.wasm_support);
344
1
    }
345
}