Part 0 — Introduction: an inference-driven type checker
The doorway to the book. Before we write any code, this chapter is about grasping what chibirigor is — and isn’t. We’ll share the implementation’s premises (Prism,
check,annotate) and the two promises that run through the whole book up front.
chibirigor is the smallest Ruby type checker — one that infers types for itself. Step by
step, with our own hands, we’ll build a miniature version of the architecture of the real
Rigor, a gradual type checker for Ruby. Just as
chibivue teaches Vue by rebuilding it small, we
learn Rigor by rebuilding it small. This book is built the same way chibivue is — chibivue
leaves each round’s milestone under book/impls, and from the end of each chapter you can jump
to the staged snapshot at impls/dist/partN (the working minimal version, built up to that
chapter).
What we build, at the center of the book, is just two commands:
check— read Ruby code and report type contradictions (diagnostics).annotate— display the inferred types.
(Later, an appendix adds a small extension — opt-in --explain / --unreachable flags to
check — but the spine of the book stays these two throughout.)
0.1 An inference-driven type checker
Section titled “0.1 An inference-driven type checker”First, let’s pin down what kind of tool chibirigor is.
Most type checkers out in the world assume type annotations written into the program and decide whether those annotations contradict one another. Without annotations, they have no work to do.
- A type annotation means writing a type into the code, as in “this variable is an
Integer.” In Ruby you don’t normally write them — which is why the first time you write one yourself is much later, in Part 8 (RBS); until then they don’t appear.
But Ruby code usually has no type annotations. So before it leans on annotations, chibirigor
first derives types from the expressions themselves. See 1, it’s an integer; see
"a".upcase, it’s a string. Using the types it gets that way, it finds contradictions
(check) and shows types (annotate). In one phrase, chibirigor is “an annotation-free type
checker, built on a foundation of inference.” Inference isn’t a separate pre-pass set apart
from checking — it’s the foundation that makes checking possible.
The “inference” we mean here is building a type upward from an expression (synthesis) — “1
is an integer,” “"a".upcase is a string” — stacking up types from what’s written. The
reverse — working out a method’s parameter types backward from how callers use it — we do
not do (in the Little volume). If we can’t tell a parameter’s type, we collapse it to
untyped (just remember: when in doubt, fall back to untyped). The catalog of special types,
untyped first among them, is appendix a1 — no need to read
it through; it’s a reference to consult after you’ve read up to Part 9.
“Infer first, then check on the result.” Inference is the foundation; check and annotate
consume its output. Fix that order in your head from the start.
0.2 chibirigor doesn’t reject its input
Section titled “0.2 chibirigor doesn’t reject its input”The other promise. chibirigor accepts any code Ruby doesn’t reject as a syntax error. It never says “this has no types, so I won’t analyze it.”
- The parser is Ruby’s standard Prism. Prism partially parses even somewhat broken syntax, so the range it accepts is, if anything, wider than the Ruby runtime’s.
- But “chibirigor passed it” does not guarantee “it runs.” All we return is inferred types and diagnostics.
This is a decisive difference in stance from『しくみ』. That book’s checker throws an exception and stops on a type error. chibirigor always takes the input, and its output is diagnostics and inferred types. It doesn’t stop; where it can’t tell, it stays quiet and moves on.
0.3 The heart is two functions
Section titled “0.3 The heart is two functions”The heart of the type checker we’re about to build is just two functions. We grow these two across the whole book.
type_of— find (synthesize) a type from an expression.1 + 2→Integer,"a"→"a". If it can’t tell, it just returnsuntyped— it never fails.accepts— given a type where another is expected, check whether it fits.
The overall flow looks like this (type_of is the lead; check / annotate only consume its
output):
┌─ accepts at an expected type → on mismatch ─→ check (diagnostics)source ─Prism→ AST ─type_of→ type ┤ └─ show it as is ────────────────────────────→ annotate (inferred types)▼ Figure 0-1 — chibirigor’s data flow
type_of builds a type, check verifies “does it fit the expected type” and emits
diagnostics, and annotate shows the built type as is — inference is the foundation, and
both check and annotate consume its output.
And now, before anything else, let’s put the core principle that runs through the whole book into words — no jargon needed:
This is the true form of Rigor’s motto “never frighten working code.” chibirigor, too, cares far more about not producing false positives (not painting working code red) than about soundness (catching every last bug). Why choose that side — that settles in chapter by chapter, as we run up against the reality of Ruby.
0.4 The three perspectives in every chapter
Section titled “0.4 The three perspectives in every chapter”Each chapter is written from a small set of three perspectives:
- ① Type theory — one concept you meet in the chapter(and where『しくみ』covers it).
- ② In Ruby / RBS — how it looks in Ruby, or how it fails to show up.
- ③ Rigor’s implementation problem — why the naive implementation breaks against Ruby’s reality, and how it was reconciled.
“Understanding Rigor” means watching the trouble in ③ arise necessarily from ② (Ruby’s reality), and settle gently under the concept in ①. The hard material (the formalization of bidirectional typing, variance, recursive types, real type inference…) all goes to the Seasoned volume, The Seasoned chibirigor. The Little volume concentrates on building a working minimal version, and finishing it with satisfaction.
What we stack across these nine chapters (the whole picture)
Section titled “What we stack across these nine chapters (the whole picture)”Each chapter builds on the one before. What grows in a chapter is just “one hard thing”:
| Ch. | What we add | Keywords |
|---|---|---|
| 1 | Represent types as data; find types from expressions | Const · type_of · check · annotate |
| 2 | Type method sends | dispatch table · unknown is Dynamic |
| 3 | Handle variables and statements | immutable Scope · threading statements |
| 4 | Types branch (Union) | Union |
| 5 | Narrow types by case | narrowing · two laws |
| 6 | Give hashes and arrays types | HashShape · Tuple |
| 7 | Judge “does it fit” with three values | accepts · :yes / :no / :maybe |
| 8 | Pull types from RBS; infer and show return types | Rbs.load · synthesizing a def’s sig |
| 9 | Close with the philosophy of gradual typing | untyped propagation · baseline |
By the time you reach Part 9, you’re left with a type checker where check and annotate both
work, end to end.
0.5 Setup
Section titled “0.5 Setup”Anything with Ruby 3.4+ (Prism bundled) will run it. No test framework, either.
$ ruby exe/chibirigor check path/to/file.rb # type diagnostics$ ruby exe/chibirigor annotate path/to/file.rb # show inferred typesLet’s start by parse-ing and peeking at the syntax tree (the AST). Once you can see what kind
of tree Prism turns a program into, you’re ready for type_of to walk that tree and build up a
type.
Next chapter (Part 1): we write the first type_of. A type checker of just a few dozen
lines — literals and arithmetic only — already starts doing the job of “giving things types.”
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.