## Coverage Report

#### Created: 2025-09-08 21: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
    0
        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
    0
            let repl = crate::wasm::repl::WasmRepl::new().map_err(|_| "Error creating REPL".to_string())?;
    107
    0
            Ok(NotebookRuntime {
    108
    0
                notebook: Notebook {
    109
    0
                    version: "1.0.0".to_string(),
    110
    0
                    metadata: NotebookMetadata {
    111
    0
                        kernel: "wasm".to_string(),
    112
    0
                        language: "ruchy".to_string(),
    113
    0
                        created: current_timestamp(),
    114
    0
                        modified: current_timestamp(),
    115
    0
                        ruchy_version: env!("CARGO_PKG_VERSION").to_string(),
    116
    0
                    },
    117
    0
                    cells: Vec::new(),
    118
    0
                },
    119
    0
                repl,
    120
    0
                execution_count: 0,
    121
    0
                variables: HashMap::new(),
    122
    0
            })
    123
    0
        }
    124
    125
        /// Add a new cell to the notebook
    126
        #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    127
    0
        pub fn add_cell(&mut self, cell_type: &str, source: &str) -> String {
    128
    0
            let id = generate_cell_id();
    129
    0
            let cell = NotebookCell {
    130
    0
                id: id.clone(),
    131
    0
                cell_type: match cell_type {
    132
    0
                    "markdown" => CellType::Markdown,
    133
    0
                    _ => CellType::Code,
    134
                },
    135
    0
                source: source.to_string(),
    136
    0
                outputs: Vec::new(),
    137
    0
                execution_count: None,
    138
    0
                metadata: CellMetadata::default(),
    139
            };
    140
    141
    0
            self.notebook.cells.push(cell);
    142
    0
            self.notebook.metadata.modified = current_timestamp();
    143
    144
    0
            id
    145
    0
        }
    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
    0
    fn generate_cell_id() -> String {
    237
    0
        format!("cell-{}", get_timestamp().abs() as u64)
    238
    0
    }
    239
    240
    0
    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
    0
            chrono::Utc::now().to_rfc3339()
    248
        }
    249
    0
    }
    250
    251
    0
    fn get_timestamp() -> f64 {
    252
        #[cfg(target_arch = "wasm32")]
    253
        {
    254
            js_sys::Date::now()
    255
        }
    256
        #[cfg(not(target_arch = "wasm32"))]
    257
        {
    258
    0
            std::time::SystemTime::now()
    259
    0
                .duration_since(std::time::UNIX_EPOCH)
    260
    0
                .unwrap()
    261
    0
                .as_millis() as f64
    262
        }
    263
    0
    }
    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
    0
        pub fn check_all() -> Vec<FeatureParity> {
    280
    0
            vec![
    281
    0
                FeatureParity {
    282
    0
                    feature: "Basic Evaluation".to_string(),
    283
    0
                    native_support: true,
    284
    0
                    wasm_support: true,
    285
    0
                    notes: "Full support".to_string(),
    286
    0
                },
    287
    0
                FeatureParity {
    288
    0
                    feature: "File I/O".to_string(),
    289
    0
                    native_support: true,
    290
    0
                    wasm_support: false,
    291
    0
                    notes: "WASM uses OPFS or IndexedDB".to_string(),
    292
    0
                },
    293
    0
                FeatureParity {
    294
    0
                    feature: "Threading".to_string(),
    295
    0
                    native_support: true,
    296
    0
                    wasm_support: false,
    297
    0
                    notes: "WASM uses Web Workers".to_string(),
    298
    0
                },
    299
    0
                FeatureParity {
    300
    0
                    feature: "Networking".to_string(),
    301
    0
                    native_support: true,
    302
    0
                    wasm_support: false,
    303
    0
                    notes: "WASM limited to Fetch API".to_string(),
    304
    0
                },
    305
    0
                FeatureParity {
    306
    0
                    feature: "DataFrames".to_string(),
    307
    0
                    native_support: true,
    308
    0
                    wasm_support: true,
    309
    0
                    notes: "Limited size in WASM".to_string(),
    310
    0
                },
    311
            ]
    312
    0
        }
    313
    }
    314
    315
    #[cfg(test)]
    316
    mod tests {
    317
        use super::*;
    318
    319
        #[test]
    320
        fn test_notebook_creation() {
    321
            let runtime = NotebookRuntime::new();
    322
            assert!(runtime.is_ok());
    323
        }
    324
    325
        #[test]
    326
        fn test_add_cell() {
    327
            let mut runtime = NotebookRuntime::new().unwrap();
    328
            let id = runtime.add_cell("code", "let x = 42");
    329
            assert!(id.starts_with("cell-"));
    330
            assert_eq!(runtime.notebook.cells.len(), 1);
    331
        }
    332
    333
        #[test]
    334
        fn test_feature_parity() {
    335
            let features = FeatureParity::check_all();
    336
            assert!(!features.is_empty());
    337
    338
            // Check that basic evaluation is supported in both
    339
            let basic = features.iter()
    340
                .find(|f| f.feature == "Basic Evaluation")
    341
                .unwrap();
    342
            assert!(basic.native_support);
    343
            assert!(basic.wasm_support);
    344
        }
    345
    }