Skip to content

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.)


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.


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.


The heart of the type checker we’re about to build is just two functions. We grow these two across the whole book.

  • type_offind (synthesize) a type from an expression. 1 + 2Integer, "a""a". If it can’t tell, it just returns untyped — 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

▼ 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:

  1. ① Type theory — one concept you meet in the chapter(and where『しくみ』covers it).
  2. ② In Ruby / RBS — how it looks in Ruby, or how it fails to show up.
  3. ③ 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 addKeywords
1Represent types as data; find types from expressionsConst · type_of · check · annotate
2Type method sendsdispatch table · unknown is Dynamic
3Handle variables and statementsimmutable Scope · threading statements
4Types branch (Union)Union
5Narrow types by casenarrowing · two laws
6Give hashes and arrays typesHashShape · Tuple
7Judge “does it fit” with three valuesaccepts · :yes / :no / :maybe
8Pull types from RBS; infer and show return typesRbs.load · synthesizing a def’s sig
9Close with the philosophy of gradual typinguntyped 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.


Anything with Ruby 3.4+ (Prism bundled) will run it. No test framework, either.

Terminal window
$ ruby exe/chibirigor check path/to/file.rb # type diagnostics
$ ruby exe/chibirigor annotate path/to/file.rb # show inferred types

Let’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.