Hedronite · Synthesis Lesson · Rust × Pair γ · Thu 2026-06-04

Rust's Newtype Pattern and Smart Constructors

Compile-time sizing guards, saturating arithmetic, and the pre-trade gate verdict type.

Lesson Class: Dev Synthesis
Language: Rust (Mon+Thu W3 per runbook §II.D; first Rust × γ in supercycle)
Week / Cycle: Week 3 of Language Cycle / Cycle 1
Word Count: ~2,510
Paired Ops: Pre-Trade Risk Gates and Position Sizing for Adversarial Markets
Paired Cert: Pre-Tool Risk Gates for Agentic AI (AWS AIP-C01 + GH-600)
Discipline: ROD v3 (universal-application)

§ IFrame

Today's Ops lesson named three numbers and one verdict. The numbers are a regime probability in zero-to-one, a catalyst discount in zero-to-one, and a capital-at-risk notional in non-negative base-currency units. The verdict is one of three terminal codes. A production gate written in any language can compute these numbers; a production gate written in Rust can make most of the failure modes of getting them wrong compile errors.

This lesson works through the small stack of Rust type-system features that gives the gate its compile-time invariants: the newtype pattern that wraps primitives in tuple structs with private fields, the smart constructor that validates the invariant at construction time and returns a Result, the saturating arithmetic that prevents silent overflow, and the enum-with-variants pattern that gives the verdict a closed return shape. None of the features is exotic; all are everyday Rust. The discipline is in choosing to use them where a less typed-up gate would let primitives flow as raw f64 and u64 values that the compiler permits to be added in any direction.

§ IILanguage Idiom — Newtypes, Smart Constructors, Saturating Arithmetic, and Closed Enums

A newtype in Rust is a tuple struct that wraps another type but does not transparently inherit its methods. The wrapper carries its own identity through the type system: a RegimeScore(f64) is not assignable to a CatalystDiscount(f64) even though both contain an f64 of the same range. The compiler treats them as distinct nominal types. The wrapper's inner field is conventionally kept private so that all construction routes through a public constructor function that can validate the invariant.

A smart constructor is the constructor function that performs the validation. Its signature returns Result<Self, ValidationError> rather than Self directly. The Result forces every caller to handle the failure branch — there is no way to ignore an invalid construction except by writing .unwrap(), which is visible in code review and rare in production gate code. The smart constructor is the choke point: every regime score, every catalyst discount, every capital-at-risk amount in the gate's runtime came through one of these constructors and was either valid at that moment or never instantiated at all.

Saturating arithmetic is Rust's family of checked_*, saturating_*, wrapping_*, and overflowing_* integer operations. The four families give the operator a choice that primitive + does not: checked_add returns Option<T> and forces the caller to handle the None branch where overflow would have happened; saturating_add clamps to T::MAX or T::MIN rather than wrapping; wrapping_add is the textbook two's-complement wrap; overflowing_add returns the value plus a boolean indicating whether overflow occurred. For capital-at-risk arithmetic — where a wrap from u64::MAX to 0 would silently authorize an order the operator never intended — the choice between these families is the discipline.

A closed enum in Rust is an enum declared in the current crate without #[non_exhaustive]. The compiler enforces, at every match site, that every variant is handled or that an explicit _ arm catches the residual. The verdict type closes the universe of return shapes. A function that adds a fourth variant to the enum is a compile-break at every match site downstream; the gate's consumers cannot silently miss the new case.

The four features compose. The newtype gives identity. The smart constructor gives validation. The saturating arithmetic gives overflow safety. The closed enum gives exhaustive consumption. Together they constitute the compile-time gate — a gate whose invariants are checked before the binary is shipped, not at the moment of an intent at the worst possible hour of the worst possible day.

§ IIICode Worked Example — The Pre-Trade Gate in Rust

The three numbers and one verdict, rendered as Rust types and a single gate function.

use thiserror::Error;

#[derive(Error, Debug, Clone, PartialEq)]
pub enum InvariantError {
    #[error("regime score must be in [0.0, 1.0]; got {0}")]
    RegimeScoreOutOfRange(f64),
    #[error("catalyst discount must be in [0.0, 1.0]; got {0}")]
    CatalystDiscountOutOfRange(f64),
    #[error("notional must be non-negative; got {0}")]
    NegativeNotional(i128),
    #[error("notional overflow on add: {0} + {1}")]
    NotionalOverflow(i128, i128),
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RegimeScore(f64);

impl RegimeScore {
    pub fn new(v: f64) -> Result<Self, InvariantError> {
        if v.is_finite() && (0.0..=1.0).contains(&v) {
            Ok(Self(v))
        } else {
            Err(InvariantError::RegimeScoreOutOfRange(v))
        }
    }
    pub fn get(self) -> f64 { self.0 }
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct CatalystDiscount(f64);

impl CatalystDiscount {
    pub fn new(v: f64) -> Result<Self, InvariantError> {
        if v.is_finite() && (0.0..=1.0).contains(&v) {
            Ok(Self(v))
        } else {
            Err(InvariantError::CatalystDiscountOutOfRange(v))
        }
    }
    pub fn fresh(self) -> f64 { 1.0 - self.0 }
}

Each newtype has a single private field; the field is reachable only through the get() or fresh() accessor. A caller who has a RegimeScore value in hand has guaranteed it came through new() and was in range at construction. A caller cannot do RegimeScore(2.0) from outside the module; the tuple-struct field's privacy refuses it.

The notional type uses an integer representation with explicit minor-unit precision rather than f64, mirroring Tuesday's Python decimal-quantization discipline. Saturating arithmetic guards the addition.

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
pub struct Notional(i128);

impl Notional {
    pub fn from_minor_units(v: i128) -> Result<Self, InvariantError> {
        if v >= 0 { Ok(Self(v)) } else { Err(InvariantError::NegativeNotional(v)) }
    }
    pub fn saturating_add(self, other: Self) -> Self {
        Self(self.0.saturating_add(other.0))
    }
    pub fn checked_add(self, other: Self) -> Result<Self, InvariantError> {
        self.0.checked_add(other.0)
            .map(Self)
            .ok_or(InvariantError::NotionalOverflow(self.0, other.0))
    }
    pub fn as_minor_units(self) -> i128 { self.0 }
}

The Notional type forbids subtraction by simply not implementing it; the gate has no path to construct a negative notional through arithmetic. Two notionals can be added with saturating semantics for capacity calculations or checked semantics for total-position tracking where overflow must surface to the operator.

The verdict is the closed enum.

#[derive(Debug, Clone, PartialEq)]
pub enum RefusalReason {
    RegimeBelowThreshold { score: f64, threshold: f64 },
    CatalystAboveThreshold { discount: f64, threshold: f64 },
    SizeBeyondCeiling { requested: i128, ceiling: i128 },
    SessionBudgetExhausted { remaining: i128 },
}

#[derive(Debug, Clone, PartialEq)]
pub struct Intent {
    pub strategy_id: String,
    pub instrument: String,
    pub side: Side,
    pub requested_notional: Notional,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Side { Long, Short }

#[derive(Debug, Clone, PartialEq)]
pub enum GateVerdict {
    ProceedAtRequestedSize { intent: Intent },
    ProceedAtClippedSize { intent: Intent, clipped_notional: Notional },
    Refuse { intent: Intent, reasons: Vec<RefusalReason> },
}

Three variants close the universe. A match v { ... } on a GateVerdict must handle all three or the compiler refuses to build. Adding a fourth variant breaks every downstream match — a feature, since the gate's consumers must be told a new terminal code exists. Each variant carries the intent forward so the consumer has full attribution; the refuse variant also carries the structured reasons.

The gate function itself is shaped around these types.

pub struct GateConfig {
    pub regime_threshold: f64,
    pub catalyst_threshold: f64,
    pub session_budget: Notional,
    pub edge_bp: f64,
    pub variance_bp_sq: f64,
}

pub fn evaluate(
    intent: Intent,
    regime: RegimeScore,
    catalyst: CatalystDiscount,
    consumed_this_session: Notional,
    cfg: &GateConfig,
) -> GateVerdict {
    let mut reasons: Vec<RefusalReason> = Vec::new();

    if regime.get() < cfg.regime_threshold {
        reasons.push(RefusalReason::RegimeBelowThreshold {
            score: regime.get(),
            threshold: cfg.regime_threshold,
        });
    }
    if (1.0 - catalyst.fresh()) > cfg.catalyst_threshold {
        reasons.push(RefusalReason::CatalystAboveThreshold {
            discount: 1.0 - catalyst.fresh(),
            threshold: cfg.catalyst_threshold,
        });
    }

    let kelly_fraction =
        (cfg.edge_bp * regime.get() * catalyst.fresh()) / cfg.variance_bp_sq;
    let session_remaining = cfg
        .session_budget
        .as_minor_units()
        .saturating_sub(consumed_this_session.as_minor_units());
    let kelly_ceiling = (kelly_fraction * session_remaining as f64) as i128;
    let kelly_ceiling = Notional::from_minor_units(kelly_ceiling.max(0))
        .expect("non-negative by clamp");

    if intent.requested_notional > kelly_ceiling
        && kelly_ceiling.as_minor_units() == 0
    {
        reasons.push(RefusalReason::SessionBudgetExhausted {
            remaining: session_remaining,
        });
    }

    if !reasons.is_empty() {
        return GateVerdict::Refuse { intent, reasons };
    }

    if intent.requested_notional <= kelly_ceiling {
        GateVerdict::ProceedAtRequestedSize { intent }
    } else {
        GateVerdict::ProceedAtClippedSize {
            intent,
            clipped_notional: kelly_ceiling,
        }
    }
}

Every primitive that enters the gate has come through a smart constructor. The arithmetic that produces the Kelly ceiling uses saturating subtraction for the session-remaining computation. The return is a closed enum every consumer must match exhaustively. The function compiles to a binary in which an invalid regime score, an invalid catalyst discount, a negative notional, or an unhandled verdict variant is, respectively, impossible to construct, impossible to construct, impossible to construct, and a build error at the consumer site.

A consumer of the verdict.

pub fn forward_to_executor(verdict: GateVerdict) {
    match verdict {
        GateVerdict::ProceedAtRequestedSize { intent } => {
            executor::send(intent.strategy_id, intent.instrument, intent.side,
                          intent.requested_notional);
        }
        GateVerdict::ProceedAtClippedSize { intent, clipped_notional } => {
            executor::send(intent.strategy_id, intent.instrument, intent.side,
                          clipped_notional);
        }
        GateVerdict::Refuse { intent, reasons } => {
            audit::log_refusal(intent, reasons);
        }
    }
}

The match is exhaustive. If a fifth variant were added to the verdict tomorrow — say ProceedWithCircuitBreaker — the consumer would fail to compile until the operator wrote the handling arm. The type system is doing the architecture review the operator might otherwise have skipped.

Four Type Features (Canonical for Rust × γ Gate) Newtype gives nominal identity so a regime score is not assignable to a catalyst discount. Smart constructor returns Result and validates the invariant at construction. Saturating arithmetic forces overflow to surface or clamp rather than wrap. Closed enum makes every consumer handle every variant or fail to compile.

§ IVConnection to Today's Ops Lesson

The Ops lesson named the three numbers and the verdict; the Rust lesson encodes them into types that prevent the most common implementation failures. The regime score's zero-to-one bound is enforced at construction by the smart constructor; the catalyst discount's bound is enforced symmetrically; the capital-at-risk notional's non-negativity is enforced at construction; the verdict's closed-enum shape forces every consumer to handle every case. A bug that would otherwise have shipped — a stray regime = 2.0 from a misread feature vector, a catalyst = -0.3 from a corrupted ledger, a notional = u64::MAX from a wrap-around bug, a silently dropped refuse-with-reasons variant in the executor's forwarding code — is, in the typed version, a Result::Err at the relevant input boundary or a compile error at the relevant consumer. The Rust type system absorbs the failure modes the Ops discipline names; the Ops discipline names the failure modes the Rust type system would otherwise have no idea to look for.

§ VPrior-Lesson Reach

This lesson is the first Rust × γ crossing in the 12-week supercycle. The pattern stack — newtype with smart constructor, saturating arithmetic, closed enum — has surfaced piece-by-piece in prior Rust lessons. The 2026-05-20 mTLS lesson introduced the type-state pattern for connection lifecycles; today's lesson uses the same pattern's cousin — closed enums — for verdict shapes. The 2026-05-26 secret-handling lesson introduced the newtype-with-Drop discipline for secret material; today's lesson uses newtype without Drop for arithmetic invariants. The 2026-05-29 order-lifecycle lesson used sealed traits to bound the state machine; today's verdict enum is sealed implicitly by being declared in the crate. The 2026-06-01 trait-objects-and-async-streams lesson named the pivot from compile-time-closed type-state to runtime-open trait objects for composable scorers; today's lesson pivots back to compile-time-closed enums because the gate's verdict shape is not meant to be runtime-extensible — the operator must approve every new terminal code.

The five-lesson Rust arc through γ + β + α now spans type-state (mTLS) → ownership-with-Drop (secrets) → type-state + sealed traits (orders) → trait-objects + streams (eval) → newtype + smart-constructor + saturating + closed-enum (today's gate). Each language idiom maps to a different invariant class. The operator who reads all five sees the same pattern: encode the invariant the operator must not violate into a type the compiler will refuse to compile if violated.

§ VIClosing

A pre-trade gate written without these types compiles, passes its unit tests, and still ships a bug that costs capital the operator was sure could not be at risk. A pre-trade gate written with them ships fewer bugs because the bugs that would have shipped do not compile. The cost of writing the types is a small amount of additional ceremony at the boundary; the cost of not writing them is a long-tail of operator-confidence losses every time an arithmetic shape silently exceeds its intended range.

The discipline is not Rust-specific in principle — any language with sum types and validated constructors can approximate it — but Rust's combination of mandatory pattern matching, non-transparent newtypes, and explicit overflow semantics makes the discipline the language's natural shape. The operator who writes the gate in Rust is choosing to let the compiler be the first reviewer.

Examine well. Reflect on this.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate of Imperium Luminaura
Authored 2026-06-04 Fajr cron-fire — first Rust × γ crossing in the 12-week supercycle; pivots Rust pattern-stack from type-state → trait-objects → newtype + smart-constructor + saturating + closed-enum; ROD v3 discipline held.