From e893621d76ac8266e2eb4395683f2eeda8e7723d Mon Sep 17 00:00:00 2001 From: lloyd Date: Sat, 10 Oct 2026 09:03:33 +0200 Subject: [PATCH] Web app: committing a cell no longer traps on the clock Every edit goes through cmd::commit, which read std::time::Instant::now() up front for GRIDCRAFT_PROFILE timing, whether profiling was on or not. wasm32-unknown-unknown has no clock: Instant::now panics there, and with panic=abort the browser reports `RuntimeError: unreachable`. So in the web app, typing 1 into A1 and pressing Enter trapped (#118). The native apps and the CLI have a clock and were unaffected. commit now reads the clock only when GRIDCRAFT_PROFILE is set, and never on wasm. The other clock reads (NOW(), the current year for typed dates, the UI's frame timer) already skip wasm. Native tests can't see this trap, so clippy.toml now disallows Instant::now and SystemTime::now. A clock read has to be gated off wasm and allowed at the call, and `cargo clippy -D warnings` fails on the old commit code. The existing, already-gated reads and the native-only perf example allow it. Checked by building the engine for wasm32-unknown-unknown into a small cdylib that runs cell.set A1=1 on a new workbook, and running it in Node: before the fix it traps with `RuntimeError: unreachable`, after it returns normally. Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 8 ++++++++ crates/calc/src/recalc.rs | 2 ++ crates/core/src/parse.rs | 1 + crates/engine/examples/perf.rs | 2 ++ crates/engine/src/cmd/mod.rs | 12 +++++++----- crates/ui-egui/src/lib.rs | 1 + 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/clippy.toml b/clippy.toml index 4b6c79f..8b8bc5a 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,11 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-panic-in-tests = true allow-indexing-slicing-in-tests = true + +# The web app (wasm32-unknown-unknown) has no clock: these trap (`RuntimeError: unreachable`) +# instead of returning. Read the clock only off wasm (`cfg(not(target_arch = "wasm32"))` or +# `cfg!`) and allow the lint at that call. +disallowed-methods = [ + { path = "std::time::Instant::now", reason = "traps on wasm; read the clock only off wasm" }, + { path = "std::time::SystemTime::now", reason = "traps on wasm; read the clock only off wasm" }, +] diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 13b47c4..a15bec9 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -553,6 +553,7 @@ impl Calc { } /// Local date-time as a serial (1900 system). Wasm without a clock returns a fixed date. +#[allow(clippy::disallowed_methods)] // the clock is read only off wasm pub fn now_serial() -> f64 { #[cfg(not(target_arch = "wasm32"))] { @@ -592,6 +593,7 @@ pub fn evaluate_expr(wb: &Workbook, sheet: usize, at: CellRef, expr: &Expr) -> V ev.value(expr) } +#[allow(clippy::disallowed_methods)] // the clock is read only off wasm fn prof_now() -> f64 { #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/core/src/parse.rs b/crates/core/src/parse.rs index b08cf2a..217c45b 100644 --- a/crates/core/src/parse.rs +++ b/crates/core/src/parse.rs @@ -275,6 +275,7 @@ fn valid_ymd(sys: DateSystem, y: i64, m: i64, d: i64) -> Option { /// The year used for dates typed without one. Fixed per process from the system clock when /// available (wasm without clock falls back to 2026). +#[allow(clippy::disallowed_methods)] // the clock is read only off wasm pub fn current_year() -> i64 { #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/engine/examples/perf.rs b/crates/engine/examples/perf.rs index 2a533a2..61ecf56 100644 --- a/crates/engine/examples/perf.rs +++ b/crates/engine/examples/perf.rs @@ -1,6 +1,8 @@ //! Performance scenarios on large workbooks (release build): //! `cargo run --release -p gridcraft-engine --example perf -- [rows]` +#![allow(clippy::disallowed_methods)] // a native-only benchmark: wasm never runs it + use std::time::Instant; use gridcraft_engine::Session; diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 397a05c..530bc3a 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -217,8 +217,10 @@ pub(crate) fn edit(s: &mut Session, f: impl FnOnce(&mut Ctx) -> Result) -> } pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) -> Result { - let prof = std::env::var_os("GRIDCRAFT_PROFILE").is_some(); - let t0 = std::time::Instant::now(); + // `GRIDCRAFT_PROFILE` timing. Read the clock only when profiling, and never on wasm: there + // `Instant::now` traps (`RuntimeError: unreachable`), which broke every edit in the web app. + #[allow(clippy::disallowed_methods)] + let t0 = if cfg!(target_arch = "wasm32") { None } else { std::env::var_os("GRIDCRAFT_PROFILE").map(|_| std::time::Instant::now()) }; let mut sel = d.selection.clone(); let mut ctx = Ctx { wb: (*d.wb).clone(), changed: Vec::new(), structural: false, sel: &mut sel, fit_rows: Vec::new() }; let r = f(&mut ctx)?; @@ -226,7 +228,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) if changed.len() <= 200_000 { fit_rows.extend(changed.iter().map(|(s, c)| (*s, c.row))); } - if prof { + if let Some(t0) = t0 { eprintln!("commit: edit {:?}, {} changed", t0.elapsed(), changed.len()); } if structural { @@ -234,7 +236,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) } else if !changed.is_empty() { d.calc.cells_changed(&mut wb, &changed); } - if prof { + if let Some(t0) = t0 { eprintln!("commit: + recalc {:?}", t0.elapsed()); } fit_rows.sort_unstable(); @@ -242,7 +244,7 @@ pub(crate) fn commit(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result) for (si, row) in fit_rows { auto_row_height(&mut wb, si, row); } - if prof { + if let Some(t0) = t0 { eprintln!("commit: + rows {:?}", t0.elapsed()); } d.wb = std::sync::Arc::new(wb); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8eb1eca..530097b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -486,6 +486,7 @@ pub fn clean_error(e: &str) -> String { e.to_string() } +#[allow(clippy::disallowed_methods)] // the clock is read only off wasm pub fn now_ms() -> f64 { #[cfg(not(target_arch = "wasm32"))] {