Rigor Type System — Quick Guide
Rigor is an inference-first static analyzer for Ruby. Its type language is a strict superset of RBS: every RBS type round-trips losslessly through Rigor’s internal representation, and every Rigor-inferred type erases conservatively back to ordinary RBS.
This file is the one-page entry point. The full normative specification lives in docs/type-specification/. Design rationale and rejected/deferred options live in docs/adr/1-types.md.
Concept
Section titled “Concept”- No inline DSL. Application Ruby code stays free of Rigor-only annotation syntax. RBS, rbs-inline, and Steep-compatible annotations are accepted as type sources.
- Lossless RBS in, conservative RBS out. Internal precision (literal sets, refinements, shapes, dynamic-origin provenance) MAY exceed what RBS can spell. On export, Rigor erases to ordinary RBS that is never narrower than what was proved.
- Three-valued certainty. Type, reflection, and member queries return
yes,no, ormaybe.maybedoes not narrow as ifyesand does not produce the opposite-edge fact as ifno. - Two relations, kept separate. Subtyping (
A <: B, value-set inclusion) and gradual consistency (consistent(A, B), dynamic-boundary compatibility) are not unified.untypedis the dynamic type, distinct fromtop. - Robustness principle (Postel’s law). Rigor-authored types are strict on returns and lenient on parameters. A precise return propagates useful facts through the inference engine; a permissive parameter prevents coercion workarounds at call sites. Hand-written RBS authorship binds — the principle directs Rigor’s defaults, not user-supplied signatures. See robustness-principle.md for the normative rule and adr/5-robustness-principle.md for the design rationale.
Carriers at a glance
Section titled “Carriers at a glance”A vanilla checker asks “what class is this?”; Rigor asks “what subset of values can this expression produce?” and records the answer in a carrier. The everyday ones, with the type Rigor infers in the trailing comment:
n = 1 + 2 # Constant<3> — a single proven valuelen = ARGV.size # int<0, max> — a bounded range (a.k.a. non-negative-int)s = id.downcase # lowercase-string — a refinement: String restricted by a predicaterow = [1, "a"] # Tuple[Constant<1>, Constant<"a">] — per-position array shapecfg = {port: 8080} # HashShape{port: Constant<8080>} — per-key hash shapetag = choose_color # Constant<:red> | Constant<:blue> — a finite unionx = gets # String | nil; Dynamic[Top] when nothing can be provedAngle brackets hold a concrete value or bound (Constant<3>, int<0, max>); square brackets hold type parameters, as in RBS (Tuple[…], Dynamic[Top]). Every carrier erases to its base RBS class at the boundary (Constant<3> → Integer), so adopting Rigor is strictly additive. The full walkthrough is handbook chapter 2 — Everyday types.
Main features
Section titled “Main features”| Feature | Where to read more |
|---|---|
Dynamic[T] algebra and gradual-typing provenance | value-lattice.md, special-types.md |
| Edge-aware control-flow narrowing inside compound conditions | control-flow-analysis.md |
| Negative facts, difference types, complement display contract | type-operators.md |
| Structural duck typing through RBS interfaces and inferred object shapes | structural-interfaces-and-object-shapes.md |
Capability roles (_RewindableStream, _ClosableStream, …) for IO-like compatibility | structural-interfaces-and-object-shapes.md |
Refinements (non-empty-string, positive-int, hash-shape extra-key policy, …) | imported-built-in-types.md, rigor-extensions.md |
RBS::Extended annotations (%a{rigor:v1:…} for predicates, assertions, conformance) | rbs-extended.md |
Lightweight HKT — defunctionalised App[F, A] type constructors (e.g. JSON.parse return discrimination) | rigor-extensions.md, rbs-extended.md, adr/20-lightweight-hkt.md |
| Inference budgets and boundary contracts for recursion / operator ambiguity | inference-budgets.md |
| Diagnostic identifier taxonomy and suppression markers | diagnostic-policy.md |
| Conservative RBS erasure and hash-shape erasure algorithm | rbs-erasure.md |
Quick reading paths
Section titled “Quick reading paths”- Just want the mental model? Read overview.md, value-lattice.md, and special-types.md in that order.
- Implementing inference? Add control-flow-analysis.md, normalization.md, inference-budgets.md, and the analyzer-internal contracts in
docs/internal-spec/— start with implementation-expectations.md and internal-type-api.md. - Writing RBS or
RBS::Extendedpayloads? Read rbs-compatible-types.md and rbs-extended.md, then rbs-erasure.md to see how they round-trip. - Reviewing or extending the diagnostic surface? Read diagnostic-policy.md alongside type-operators.md.
Specification index
Section titled “Specification index”The full reading order, conventions (RFC 2119 keywords, RBS-first compatibility hierarchy), and one-line description of each topical document live in docs/type-specification/README.md.
For analyzer-internal contracts that complement the type specification (engine-surface, type-object public API), see docs/internal-spec/README.md.
Related documents
Section titled “Related documents”README.md— project overview and CLI entry pointAGENTS.md— development workflow for this repositorydocs/adr/0-concept.md— Rigor’s high-level concept ADRdocs/adr/1-types.md— type-model ADR (design rationale, options considered, rejected/deferred items, open questions)docs/adr/2-extension-api.md— plugin extension API ADRdocs/adr/3-type-representation.md— internal type representation ADR (design rationale and open questions)docs/adr/4-type-inference-engine.md— type inference engine ADR (slice roadmap, tentative answers to ADR-3 open questions)docs/internal-spec/README.md— analyzer-internal contracts (engine surface, type-object public API, inference engine)
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.