Hmbown/CodeWhale · error

finite points

Error message

finite points

What it means

Panic from `.expect("finite points")` on `serde_json::to_string(&points)` inside the pet-watch QuickJS worker (crates/tui/src/tui/pet_watch/worker.rs:129). The points are parsed from the bundled `whale-points.tsv` as `f64`; `serde_json::to_string` errors when a value is NaN or infinite (JSON has no representation for them). This is a build-data invariant: the shipped TSV must contain only finite floats.

Solutions

  1. Audit `crates/tui/src/tui/ambient_life/whale-points.tsv` for NaN/Inf-producing values and regenerate it with finite numbers only.
  2. Add a build-time check that parses the TSV and asserts every cell is finite, so bad data fails the build instead of the worker.
  3. If the parser must tolerate malformed lines, drop them (`.filter(|v| v.is_finite())`) rather than substituting non-finite defaults.
  4. Replace the bare `.expect("finite points")` with an expect that names the offending cell/line for faster diagnosis.

Example fix

// before
serde_json::to_string(&points).expect("finite points")
// after
serde_json::to_string(&points)
    .expect("whale-points.tsv must contain only finite floats (json serialize)")
Defensive patterns

Strategy: validation

Validate before calling

let points: Vec<Vec<f64>> = /* parsed TSV */;
assert!(points.iter().flatten().all(|v| v.is_finite()), "non-finite point in whale-points.tsv");

Type guard

fn all_finite(points: &[Vec<f64>]) -> bool { points.iter().flatten().all(|v| v.is_finite()) }

Try / catch

let json = serde_json::to_string(&points)
    .expect("finite points (check whale-points.tsv for NaN/Inf)");

Prevention

When it happens

Trigger: `whale-points.tsv` (or any replacement points file) containing a token that parses to NaN/inf, or a code change altering the parser to emit non-finite defaults (e.g. `parse().unwrap_or(f64::NAN)`); the panic fires at worker `start()`/`run()` when the JS pet engine is initialized.

Common situations: Editing or regenerating the ambient-life points dataset with an export script that emits `NaN`/`Inf`; fuzzing or replacing the TSV in tests; hand-editing the resource with locale-formatted decimals (`1,5`) that a filter_map silently drops, followed by a later change substituting non-finite fallbacks.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/9998f68e2fa894eb. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/pet_watch/worker.rs:129

    let deadline = Arc::new(Mutex::new(Instant::now() + Duration::from_secs(2)));
    let check = Arc::clone(&deadline);
    runtime.set_interrupt_handler(Some(Box::new(move || {
        check.lock().map_or(true, |d| Instant::now() > *d)
    })));
    let context = Context::full(&runtime).map_err(|_| ())?;
    context
        .with(|ctx| -> rquickjs::Result<()> {
            let points: Vec<Vec<f64>> = include_str!("../ambient_life/whale-points.tsv")
                .lines()
                .map(|line| {
                    line.split_whitespace()
                        .filter_map(|s| s.parse().ok())
                        .collect()
                })
                .collect();
            ctx.globals().set(
                "points",
                serde_json::to_string(&points).expect("finite points"),
            )?;
            ctx.eval::<(), _>(include_bytes!("pet-native.js").as_slice())?;
            ctx.eval::<(), _>("globalThis.pet = new PetNative(points, '', '[]', true)")?;
            Ok(())
        })
        .map_err(|_| ())?;
    let mut store = None;
    let mut offset_ms = 0.0;
    if let Some(session) = session {
        // Hydrating a bounded recording validates its whole accepted history.
        // It runs off the UI thread and can take longer than a frame command.
        *deadline.lock().map_err(|_| ())? = Instant::now() + Duration::from_secs(10);
        let loaded = (|| -> Result<Store, ()> {
            let mut files = Store::open(session).map_err(|_| ())?;
            if let Some(saved) = files.load().map_err(|_| ())? {
                offset_ms = context
                    .with(|ctx| -> rquickjs::Result<f64> {
                        ctx.globals().set("savedHabitat", saved)?;

View on GitHub (pinned to 73e0f67d83)