Overview
Core principle
Section titled “Core principle”Rigor’s type language is a strict superset of RBS.
The RBS→Rigor direction is lossless: every RBS type has a representation in Rigor that round-trips back to the same RBS type. Internal precision-bearing wrappers such as Dynamic[T] (see special-types.md) are reversible at the boundary so the round-trip is exact rather than approximate.
The Rigor→RBS direction is not lossless. Every Rigor-inferred type MUST have an RBS erasure so Rigor can export an approximation as ordinary RBS, but erasure MAY collapse refinements, literal unions, shapes, and dynamic-origin provenance. Erasure MUST NOT produce a narrower type than Rigor proved; it MAY produce a wider one.
Rigor uses RBS as the interoperability surface and a richer internal type model for inference, control-flow analysis, and diagnostics.
Source-of-truth boundaries
Section titled “Source-of-truth boundaries”Rigor borrows ideas from PHPStan, TypeScript, and Python’s typing specification, but the borrowed ideas remain Ruby-shaped:
- RBS classes and modules stay nominal.
- RBS interfaces and Rigor object shapes provide structural duck typing (see structural-interfaces-and-object-shapes.md).
- Ruby truthiness means only
falseandnilare falsey. - Ruby equality, case equality,
respond_to?,method_missing, singleton methods, and module inclusion are runtime behaviors that MUST be modeled through Ruby semantics, RBS signatures, or plugin facts rather than copied from another language. - Application Ruby code stays free of Rigor-only annotation syntax. Existing RBS-, rbs-inline-, and Steep-compatible annotations are accepted as type sources, not treated as Rigor-specific syntax.
Design priorities
Section titled “Design priorities”This specification is organized around the ideal type model, not the first implementation milestone. The priorities, in order, are:
- Preserve every RBS type and every RBS export rule.
- Keep Ruby runtime behavior as the source of truth for narrowing and member availability.
- Make gradual loss of precision explicit through
untypedprovenance. - Treat control-flow facts as scope transitions at expression edges, not only as block-level branch labels.
- Support Ruby duck typing through structural interfaces and object shapes without making all class compatibility structural.
- Let plugins and
RBS::Extendedcontribute facts, effects, and dynamic reflection while the analyzer keeps ownership of scope application and normalization. - Apply the robustness principle (Postel’s law) to every Rigor-authored type — strict on returns, lenient on parameters — so precision propagates downstream and call sites avoid coercion noise.
- Minimise false positives on working code. A program that runs is primary evidence of its own correctness; a diagnostic MUST NOT alarm the author over a worst case the runtime does not reach, nor induce defensive code beyond what the logic genuinely needs. See False-positive discipline.
False-positive discipline
Section titled “False-positive discipline”Design priority 8 is a first-class value — on the same footing as RBS preservation and the robustness principle, not a tuning knob.
-
The working program is the most important fact. When static analysis derives a worst-case-sound reading — a
T | nil, a possibly-missing method — that the running, test-covered program never actually reaches, the runtime evidence outranks the static worst case. Rigor reports such a site cautiously, and a release-quality codebase is expected to surface few or no diagnostics from it. -
Reasonable defensive programming is welcome, and Rigor stays quiet about it. Guarding a genuine external-input boundary, handling a value that is honestly nilable, validating at a trust boundary — these are good Ruby. Rigor MUST NOT flag them as redundant or push back on them.
-
What Rigor MUST NOT induce is excessive defensiveness. A diagnostic that pushes the author toward a guard the type information already proves unnecessary — a
nilcheck on a value narrowing has established as non-nil, anis_a?on a value whose class is already known — or toward belt-and-suspenders coding beyond what the logic needs, is a design failure. The author must not be made more conservative or more verbose than the program’s own logic warrants. The line is necessity: the guard the logic genuinely needs is welcome; the guard the types render moot is noise. -
Consequence for engine and rule work. A precision change or a new rule that increases diagnostics on working code is suspect even when it also catches more — the false-positive cost is weighed heavily, never treated as free. Declining (narrowing to
Dynamic, emitting nothing) is preferred over a speculative diagnostic. This is the diagnostic-emission expression of the same intent the robustness principle carries on the type-authorship side (clause 2’s workaround-multiplication anti-pattern) and the baseline mechanism (ADR-22) carries on the adoption side.
Release scope
Section titled “Release scope”This specification describes the long-term type model. The first user-visible release (v1) ships a deliberately scoped slice. The boundary is:
- The full specification — fact-stability buckets, capability-role catalog, mutation summary set,
Dynamic[T]algebra, type operators, theRBS::Extendedschema — is normative. Internal data structures MAY be present in v1 even when the user-visible narrowing surface does not yet exploit them. - The v1 narrowing surface is the subset of derivation rules that are turned on for end users in the first release.
- Later releases expand the narrowing surface using rules the data structures already support. Across the
0.1.xpreview line much of this has already shipped (intra-procedural fact propagation, plugin-supplied flow contributions, thedef.override-*family); each addition lands incrementally so previously shipped behavior is preserved.
The detailed shipped-versus-deferred boundaries appear in the relevant topical sections, including control-flow-analysis.md, structural-interfaces-and-object-shapes.md, and rbs-extended.md.
Compatibility hierarchy
Section titled “Compatibility hierarchy”The compatibility hierarchy is:
- RBS and rbs-inline are first-order norms for type syntax and inline annotation compatibility.
- Steep 2.0 behavior is the second-order norm for how existing annotations are interpreted when prose specifications leave behavior open.
- TypeScript, PHPStan, and Python typing are design references, not syntax compatibility targets.
When the three sources differ, Rigor follows the resolution order documented in README.md.
Inline annotation handling: Rigor MUST be 100% compatible with RBS and rbs-inline syntax, and SHOULD follow Steep 2.0 behavior for inline annotation interpretation and precedence. Existing rbs-inline and Steep-compatible annotations are official type sources. Rigor MUST NOT rewrite them, MUST NOT warn solely because they are complex, and MUST NOT require # rbs_inline: enabled to begin parsing them. Only the rbs-inline configuration directives such as # rbs_inline: enabled and # rbs_inline: disabled are interpreted; the rbs-inline annotation comments themselves (for example #: String, # @rbs, parameter annotations) are always parsed and used whenever present.
Standalone .rbs files and generated stubs remain the preferred place for complete type definitions. Inline annotations are nevertheless real contracts when present. They are not merely hints. Implementation-side checking is independent of where the contract came from: a return type written as #: void, a method type written with # @rbs, parameter types written in rbs-inline style, a generated stub, and an external .rbs declaration all constrain the implementation in the same way.
Style guidance for inline annotations
Section titled “Style guidance for inline annotations”Style guidance is only about whether authors should write a type in .rb source. It does not change validity:
#: voidand#: botare strongly recommended when they express intent and create useful inference boundaries.- Short returns such as
#: bool,#: String, or#: Userare neutral; authors MAY write them when they make intent clearer. - Complex inline types — unions, generics, records, and nested method types — are valid RBS/rbs-inline input and MUST be accepted. Rigor’s style guidance prefers moving them to
.rbsor generated stubs, but Rigor MUST NOT report diagnostics merely for using them.
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.