rigortype 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -14
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +90 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Lightweight HKT (JSON.parse and friends)
|
|
2
|
+
|
|
3
|
+
`JSON.parse(str)` returns "some JSON value": `nil`, a bool, a
|
|
4
|
+
number, a string, an array of JSON values, or a hash of JSON
|
|
5
|
+
values. RBS describes that as `untyped` because there is no way
|
|
6
|
+
to spell a recursive sum type without quantifying over a type
|
|
7
|
+
constructor. Most type checkers shrug and let `JSON.parse(str)`
|
|
8
|
+
fade into `Dynamic[top]`.
|
|
9
|
+
|
|
10
|
+
Rigor models it precisely:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
parsed = JSON.parse('{"name": "Alice"}')
|
|
14
|
+
assert_type(
|
|
15
|
+
"Array[json::value[String]] | Float | " \
|
|
16
|
+
"Hash[String, json::value[String]] | Integer | " \
|
|
17
|
+
"String | false | nil | true",
|
|
18
|
+
parsed)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The mechanism behind this — and the one that lets you wire the
|
|
22
|
+
same shape for your own DSL or stdlib method — is **Lightweight
|
|
23
|
+
HKT** ([ADR-20](../adr/20-lightweight-hkt.md)), Rigor's
|
|
24
|
+
defunctionalised encoding of higher-kinded types in the
|
|
25
|
+
[Yallop & White 2014](https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf) /
|
|
26
|
+
[fp-ts `URItoKind`](https://github.com/gcanti/fp-ts/blob/master/src/HKT.ts)
|
|
27
|
+
style. This chapter walks through what it does, when to reach
|
|
28
|
+
for it, and how to author your own overlay.
|
|
29
|
+
|
|
30
|
+
This is the most advanced chapter in the handbook. Most
|
|
31
|
+
readers only need the first two sections — what the carrier
|
|
32
|
+
looks like and which stdlib methods are wired out of the box.
|
|
33
|
+
Everything after "Authoring your own overlay" is for the rare
|
|
34
|
+
case where you want to model a recursive sum type of your own.
|
|
35
|
+
|
|
36
|
+
## The five-second pitch
|
|
37
|
+
|
|
38
|
+
| Concept | Rigor spelling | Where you see it |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Type-constructor "tag" | Namespaced Symbol URI (`:json::value`, `:dry_monads::result`) | `%a{rigor:v1:hkt_register: uri=…}` directive |
|
|
41
|
+
| Abstract application `F<A>` | `Type::App[uri, args]` | Carrier in dispatcher output |
|
|
42
|
+
| Type-level definition | `%a{rigor:v1:hkt_define: uri=… params=… body=…}` directive | `.rbs` overlay file |
|
|
43
|
+
| Reducing `App[F, A]` to a real type | `env.hkt_registry.reduce(app)` (or `app.reduce(registry)`) | Called eagerly by the dispatcher tier for known stdlib methods |
|
|
44
|
+
| Hooking it to a method | `Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES` table | Plugin / Rigor-bundled wiring |
|
|
45
|
+
|
|
46
|
+
The next sections show each of these in action.
|
|
47
|
+
|
|
48
|
+
## What's bundled today
|
|
49
|
+
|
|
50
|
+
Rigor ships two HKT registrations out of the box. The main
|
|
51
|
+
one is **`json::value[K]`**, the recursive JSON-value sum (the
|
|
52
|
+
second, `csv::parsed[K]`, is covered at the end of this
|
|
53
|
+
section). `json::value` has two parts:
|
|
54
|
+
|
|
55
|
+
```rbs
|
|
56
|
+
# Registration — names the tag, declares its arity, variance,
|
|
57
|
+
# and erasure bound. The bound is what Rigor's RBS round-trip
|
|
58
|
+
# falls back to when reduction is blocked.
|
|
59
|
+
uri=json::value arity=1 variance=out bound=untyped
|
|
60
|
+
|
|
61
|
+
# Definition — the actual body, parameterised on K (the hash
|
|
62
|
+
# key type). Note the self-referential `App[json::value, K]`
|
|
63
|
+
# arms — Rigor's reducer handles recursion with lazy "tying-
|
|
64
|
+
# the-knot" semantics.
|
|
65
|
+
params=K body=
|
|
66
|
+
nil | true | false | Integer | Float | String
|
|
67
|
+
| Array[App[json::value, K]]
|
|
68
|
+
| Hash[K, App[json::value, K]]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Nine stdlib methods route through this:
|
|
72
|
+
|
|
73
|
+
- `JSON.parse` / `JSON.parse!` / `JSON.load` / `JSON.load_file` / `JSON.load_file!`
|
|
74
|
+
- `YAML.safe_load` / `YAML.safe_load_file`
|
|
75
|
+
- `Psych.safe_load` / `Psych.safe_load_file`
|
|
76
|
+
|
|
77
|
+
The HKT-builtin dispatcher tier sits ABOVE the standard RBS
|
|
78
|
+
dispatch, so even though upstream RBS declares
|
|
79
|
+
`JSON.parse: (string, ?options) -> untyped`, Rigor's answer is
|
|
80
|
+
the reduced Union. `YAML.load` / `YAML.unsafe_load` deliberately
|
|
81
|
+
stay out — they can return any Ruby object and have no useful
|
|
82
|
+
HKT envelope.
|
|
83
|
+
|
|
84
|
+
The second bundled registration, **`csv::parsed[K]`**, models
|
|
85
|
+
`CSV.parse` / `CSV.read` as `Array[Array[K | nil]]` — the
|
|
86
|
+
no-headers shape. Calls passing `headers: true` (which return
|
|
87
|
+
a `CSV::Table`) and `CSV.foreach` (which yields rather than
|
|
88
|
+
returns) fall through to the upstream RBS type.
|
|
89
|
+
|
|
90
|
+
## Two kinds of call-site discrimination
|
|
91
|
+
|
|
92
|
+
The bundled overrides are not just `(receiver, method) → fixed
|
|
93
|
+
type`. Two **discriminators** look at the call's actual
|
|
94
|
+
arguments:
|
|
95
|
+
|
|
96
|
+
### `symbolize_names: true` swaps K
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
JSON.parse(str)
|
|
100
|
+
# parsed: ... | Hash[String, json::value[String]] | ...
|
|
101
|
+
|
|
102
|
+
JSON.parse(str, symbolize_names: true)
|
|
103
|
+
# parsed: ... | Hash[Symbol, json::value[Symbol]] | ...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `:json_symbolize_names` discriminator inspects the call's
|
|
107
|
+
second-argument `HashShape` for a literal `symbolize_names: true`
|
|
108
|
+
entry. Match swaps `K = String` for `K = Symbol` before the
|
|
109
|
+
reducer runs. Non-literal `symbolize_names: x` (a variable, a
|
|
110
|
+
non-`Constant<true>` value) stays on the default `String`
|
|
111
|
+
branch.
|
|
112
|
+
|
|
113
|
+
### `permitted_classes:` unions extra arms
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
require "date"
|
|
117
|
+
parsed = YAML.safe_load(str, permitted_classes: [Date])
|
|
118
|
+
# parsed: ... | Date | ...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The `:yaml_permitted_classes` **post-reduce hook** runs after the
|
|
122
|
+
reducer and augments the result. It walks the second-argument
|
|
123
|
+
`HashShape` for a `permitted_classes:` key whose value is a
|
|
124
|
+
literal `Tuple` or `Array` of Singleton classes, maps each to a
|
|
125
|
+
`Nominal`, and unions them with the base `json::value` Union.
|
|
126
|
+
`[Date, Symbol]` adds both arms.
|
|
127
|
+
|
|
128
|
+
Non-literal `permitted_classes:` values (a variable, a `Dynamic`,
|
|
129
|
+
a non-Singleton element) silently no-op so Rigor never invents
|
|
130
|
+
classes it can't statically see.
|
|
131
|
+
|
|
132
|
+
## Authoring your own overlay
|
|
133
|
+
|
|
134
|
+
You can register your own HKT URIs in a `.rbs` file under your
|
|
135
|
+
`signature_paths:`. The annotations attach to a class or module
|
|
136
|
+
declaration (RBS's annotation grammar requires that):
|
|
137
|
+
|
|
138
|
+
```rbs
|
|
139
|
+
%a{rigor:v1:hkt_register: uri=my_app::box arity=1 variance=out bound=untyped}
|
|
140
|
+
%a{rigor:v1:hkt_define: uri=my_app::box params=K body=K | nil}
|
|
141
|
+
class MyAppBoxOverlay
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A few rules:
|
|
146
|
+
|
|
147
|
+
- **URIs MUST be namespaced** (`<author>::<name>`). The `::`
|
|
148
|
+
separator prevents cross-plugin collisions per ADR-20 WD1.
|
|
149
|
+
- **The payload format is space-separated `key=value` pairs.**
|
|
150
|
+
RBS's `%a{...}` annotation grammar rejects quotes, so JSON
|
|
151
|
+
payload won't work — the kv-form is what RBS will deliver.
|
|
152
|
+
- **`body=` is special-cased to gobble everything to the end** of
|
|
153
|
+
the payload, so the body string can contain spaces, `|`, `[]`
|
|
154
|
+
etc. without escaping.
|
|
155
|
+
- **`params=` is a comma-separated list** of UCName identifiers
|
|
156
|
+
(`params=K` or `params=T,E`).
|
|
157
|
+
- **`bound=` accepts `untyped` (default) or a bare class name**.
|
|
158
|
+
Richer bound forms (parameterised generics, unions,
|
|
159
|
+
refinements) wait for a follow-up slice's expression parser.
|
|
160
|
+
|
|
161
|
+
When `Environment.for_project` builds the env, it scans the
|
|
162
|
+
loaded RBS for these annotations and merges them into
|
|
163
|
+
`env.hkt_registry` on top of the bundled builtins. Last-write-
|
|
164
|
+
wins on URI collisions so an overlay can override `json::value`
|
|
165
|
+
if you want to.
|
|
166
|
+
|
|
167
|
+
## The body grammar
|
|
168
|
+
|
|
169
|
+
`body=` is parsed by `HktBodyParser` into a tree the reducer
|
|
170
|
+
walks. The grammar covers ADR-20 § D3 in full:
|
|
171
|
+
|
|
172
|
+
| Form | Example | Meaning |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| Atom | `nil` / `true` / `false` / `bool` / `untyped` | Constants and the `Dynamic[top]` carrier |
|
|
175
|
+
| Nominal class | `Integer` / `String` / `Foo::Bar` / `::String` | `Nominal[class_name]` |
|
|
176
|
+
| Param reference | `K`, `T`, `E` (when in `params`) | Substituted at reduction time |
|
|
177
|
+
| Parameterised nominal | `Array[K]`, `Hash[K, V]` | `Nominal[..., type_args: [...]]` |
|
|
178
|
+
| Lightweight HKT application | `App[json::value, K]` | Another `Type::App` carrier, reduced lazily |
|
|
179
|
+
| Union | `A \| B \| C` | `Type::Union` (normalised) |
|
|
180
|
+
| **Conditional** | `(K <: String ? Integer : Float)` | Branches on a test verdict |
|
|
181
|
+
|
|
182
|
+
Disambiguation: a UCName matching one of `params` becomes a
|
|
183
|
+
`Param` node, **unless** it's followed by `::` (qualified class
|
|
184
|
+
continuation) or `[` (parameterised app), in which case it's
|
|
185
|
+
treated as a nominal. So `K` is a param ref, `K[X]` is the
|
|
186
|
+
class `K` applied to `X`.
|
|
187
|
+
|
|
188
|
+
### Conditional types (§ D3)
|
|
189
|
+
|
|
190
|
+
Conditional types let the body branch on the bound type — useful
|
|
191
|
+
for shape-driven discriminators inside a single registration:
|
|
192
|
+
|
|
193
|
+
```rbs
|
|
194
|
+
%a{rigor:v1:hkt_define: uri=my_app::result params=K body=
|
|
195
|
+
(K <: String ? Integer : Float)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Three test operators:
|
|
200
|
+
|
|
201
|
+
| Test | Example | Meaning |
|
|
202
|
+
| --- | --- | --- |
|
|
203
|
+
| `<:` (subtype) | `K <: String` | True when `K`'s reduced type is a subtype of `String` |
|
|
204
|
+
| `==` (structural equality) | `K == :symbol` | True when `K`'s reduced type structurally equals the right side |
|
|
205
|
+
| `in [...]` (membership) | `K in [String, Symbol]` | True when `K`'s reduced type structurally equals any option |
|
|
206
|
+
|
|
207
|
+
The reducer's verdict policy is **trinary**:
|
|
208
|
+
|
|
209
|
+
- `:yes` → reduce the `then_branch`.
|
|
210
|
+
- `:no` → reduce the `else_branch`.
|
|
211
|
+
- `:maybe` (undecided — e.g. `Dynamic[T]` on either side) → widen
|
|
212
|
+
to the union of both reduced branches (per ADR-20 WD7 /
|
|
213
|
+
robustness principle — Rigor stays conservative when it can't
|
|
214
|
+
prove which arm fires).
|
|
215
|
+
|
|
216
|
+
Verdict policy at the current slice: structural equality → `:yes`;
|
|
217
|
+
disjoint nominals (different `class_name`) or disjoint constants
|
|
218
|
+
(different `value`) → `:no`; everything else → `:maybe`.
|
|
219
|
+
|
|
220
|
+
Branches accept unions and nested conditionals:
|
|
221
|
+
|
|
222
|
+
```rbs
|
|
223
|
+
%a{rigor:v1:hkt_define: uri=my_app::numeric params=E body=
|
|
224
|
+
(E <: Integer ? Integer
|
|
225
|
+
: (E <: Float ? Float
|
|
226
|
+
: (E <: String ? Integer | Float | nil
|
|
227
|
+
: untyped)))
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Test sides themselves are single arms (no union directly on a
|
|
232
|
+
test side — wrap in `App[my_union, ...]` if you need a union
|
|
233
|
+
there).
|
|
234
|
+
|
|
235
|
+
## Reduction semantics — lazy "tying-the-knot"
|
|
236
|
+
|
|
237
|
+
The interesting part: `json::value`'s body contains
|
|
238
|
+
`Array[App[json::value, K]]` — a SELF-REFERENCE. A naive
|
|
239
|
+
recursive reducer would infinite-loop.
|
|
240
|
+
|
|
241
|
+
Rigor's reducer carries an **in-progress stack** keyed on
|
|
242
|
+
`(uri, reduced_args)`. When evaluating an `AppRef` whose
|
|
243
|
+
`(uri, args)` matches something already on the stack, it
|
|
244
|
+
returns the in-progress `Type::App` carrier as-is — lazily,
|
|
245
|
+
without unfolding. The standard fix-point trick for recursive
|
|
246
|
+
type aliases.
|
|
247
|
+
|
|
248
|
+
So reducing `App[json::value, [String]]` produces:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
Union[ nil, true, false, Integer, Float, String,
|
|
252
|
+
Array[ Type::App[json::value, [String]] ], ← carrier left intact
|
|
253
|
+
Hash[ String, Type::App[json::value, [String]] ] ]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The nested `Type::App` is a normal Rigor type; downstream
|
|
257
|
+
consumers (acceptance, narrowing, dispatch) handle it by
|
|
258
|
+
delegating to its `bound` (default `Dynamic[top]`). If they
|
|
259
|
+
need one more level of unfolding, they call
|
|
260
|
+
`app.reduce(env.hkt_registry)` again — but the typical
|
|
261
|
+
consumer doesn't need to.
|
|
262
|
+
|
|
263
|
+
A **fuel budget** (default 64 reduction steps per call-site
|
|
264
|
+
evaluation) bounds runaway expansion. Exhaustion unwinds to
|
|
265
|
+
`app.bound`.
|
|
266
|
+
|
|
267
|
+
## What it doesn't do (yet)
|
|
268
|
+
|
|
269
|
+
Lightweight HKT is, well, lightweight. Conscious non-goals:
|
|
270
|
+
|
|
271
|
+
- **Pattern-matching with binder extraction**
|
|
272
|
+
(`E <: [:if, _, A, B] ? lisp_type[A] | lisp_type[B] : ...`).
|
|
273
|
+
The conditional grammar described above tests yes/no/maybe
|
|
274
|
+
but does not bind new type variables out of the pattern.
|
|
275
|
+
`rigor-lisp-eval` needs binder extraction for full
|
|
276
|
+
AST-shape discrimination; it stays on the diagnostic-emitter
|
|
277
|
+
path until pattern bindings land.
|
|
278
|
+
- **Multi-arg HKTs for non-recursive containers**
|
|
279
|
+
(`Result[T, E]` / `Maybe[T]`) — the registry supports
|
|
280
|
+
multi-arg URIs, but Rigor's existing carriers don't have
|
|
281
|
+
the sealed-union shape `Result` needs (ADR-3 amendment is
|
|
282
|
+
the gating piece).
|
|
283
|
+
- **Sugar syntax**. The explicit `%a{rigor:v1:hkt_register /
|
|
284
|
+
hkt_define}` pair is the canonical form. A recursive
|
|
285
|
+
`type alias` shorthand is a future option, gated on user
|
|
286
|
+
feedback that the explicit form is too verbose.
|
|
287
|
+
- **Plugin-side resolver hookup**. Plugins can't yet register
|
|
288
|
+
HKT URIs through their manifests; today only Rigor-bundled
|
|
289
|
+
registrations and user `.rbs` overlays populate the
|
|
290
|
+
registry.
|
|
291
|
+
|
|
292
|
+
If you hit one of these, ADR-20's § Implementation slicing
|
|
293
|
+
menu names the slice that addresses it.
|
|
294
|
+
|
|
295
|
+
## Where to look in the code
|
|
296
|
+
|
|
297
|
+
| Layer | Location |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| Carrier | [`lib/rigor/type/app.rb`](../../lib/rigor/type/app.rb) |
|
|
300
|
+
| Registry value objects | [`lib/rigor/inference/hkt_registry.rb`](../../lib/rigor/inference/hkt_registry.rb) |
|
|
301
|
+
| Body tree node types | [`lib/rigor/inference/hkt_body.rb`](../../lib/rigor/inference/hkt_body.rb) |
|
|
302
|
+
| Reducer (lazy self-ref + fuel) | [`lib/rigor/inference/hkt_reducer.rb`](../../lib/rigor/inference/hkt_reducer.rb) |
|
|
303
|
+
| Body-string grammar parser | [`lib/rigor/inference/hkt_body_parser.rb`](../../lib/rigor/inference/hkt_body_parser.rb) |
|
|
304
|
+
| Directive parser (`hkt_register` / `hkt_define`) | [`lib/rigor/rbs_extended/hkt_directives.rb`](../../lib/rigor/rbs_extended/hkt_directives.rb) |
|
|
305
|
+
| Bundled `json::value` + `METHOD_RETURN_OVERRIDES` | [`lib/rigor/builtins/hkt_builtins.rb`](../../lib/rigor/builtins/hkt_builtins.rb) |
|
|
306
|
+
| Dispatcher tier | [`lib/rigor/inference/method_dispatcher.rb`](../../lib/rigor/inference/method_dispatcher.rb) (`try_hkt_builtin_return`) |
|
|
307
|
+
| Environment integration | [`lib/rigor/environment.rb`](../../lib/rigor/environment.rb) (`#hkt_registry` + `HktRegistryHolder`) |
|
|
308
|
+
| RBS scan | [`lib/rigor/environment/rbs_loader.rb`](../../lib/rigor/environment/rbs_loader.rb) (`each_class_decl_annotation`) |
|
|
309
|
+
|
|
310
|
+
## What's next
|
|
311
|
+
|
|
312
|
+
If you came here from a "where does JSON.parse get its type
|
|
313
|
+
from?" question, the rest of the handbook covers the surrounding
|
|
314
|
+
machinery:
|
|
315
|
+
|
|
316
|
+
- [Chapter 2 — Everyday types](02-everyday-types.md) for the
|
|
317
|
+
carrier zoo the reducer outputs.
|
|
318
|
+
- [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
|
|
319
|
+
for the broader annotation grammar (`%a{rigor:v1:return:}`,
|
|
320
|
+
`%a{rigor:v1:predicate-if-true}`, …) the HKT directives
|
|
321
|
+
sit alongside.
|
|
322
|
+
- [Appendix — Connections to type theory](appendix-type-theory.md)
|
|
323
|
+
§ "What Rigor does NOT model" for the formal-type-theory
|
|
324
|
+
context that explains why Rigor adopted the lightweight
|
|
325
|
+
encoding rather than real HKT.
|
|
326
|
+
|
|
327
|
+
If you want to author your own overlay end-to-end, the
|
|
328
|
+
worked example in
|
|
329
|
+
[`spec/rigor/environment_spec.rb`](../../spec/rigor/environment_spec.rb)
|
|
330
|
+
("ADR-20 HKT registry scan" context) is the smallest viable
|
|
331
|
+
reference — a fixture `.rbs` file with the directive pair, a
|
|
332
|
+
class declaration to anchor them on, and an `Environment.for_project`
|
|
333
|
+
call that surfaces the registration through `env.hkt_registry`.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# The Rigor Handbook
|
|
2
|
+
|
|
3
|
+
A walkthrough of Rigor's type model written for Ruby
|
|
4
|
+
programmers — no prior static-typing background assumed. Read
|
|
5
|
+
top to bottom for the first pass; come back to individual
|
|
6
|
+
chapters for reference once you know what you are looking for.
|
|
7
|
+
|
|
8
|
+
## Who this is for
|
|
9
|
+
|
|
10
|
+
You write Ruby for a living, you have run into a `NoMethodError`
|
|
11
|
+
on `nil` more than once, and you want to know:
|
|
12
|
+
|
|
13
|
+
- What does `rigor check` actually look at?
|
|
14
|
+
- Why did it flag this expression — or, more often, why
|
|
15
|
+
didn't it flag the one I expected it to?
|
|
16
|
+
- When inference falls short, how do I push it further
|
|
17
|
+
without writing annotations all over my `.rb` files?
|
|
18
|
+
|
|
19
|
+
The handbook answers those questions. It does **not** try to
|
|
20
|
+
replace the [normative type
|
|
21
|
+
specification](../type-specification/README.md) — that lives
|
|
22
|
+
in `docs/type-specification/` and is the binding source when
|
|
23
|
+
this handbook disagrees.
|
|
24
|
+
|
|
25
|
+
Operational topics — installation, the CLI command reference,
|
|
26
|
+
configuration keys, baselines, CI — live in the
|
|
27
|
+
[User Manual](../manual/README.md). Reach for this handbook to
|
|
28
|
+
understand what a type *means*; reach for the manual to look
|
|
29
|
+
up the flag, key, or command that *acts* on it.
|
|
30
|
+
|
|
31
|
+
## Table of contents
|
|
32
|
+
|
|
33
|
+
1. [**Getting started**](01-getting-started.md) — running
|
|
34
|
+
`rigor check`, reading diagnostics, the "no annotations
|
|
35
|
+
needed" stance.
|
|
36
|
+
2. [**Everyday types**](02-everyday-types.md) — the carrier
|
|
37
|
+
zoo. Constants, integer ranges, refinements, unions,
|
|
38
|
+
`Dynamic[top]`. The shortest path to "now I see what
|
|
39
|
+
Rigor sees."
|
|
40
|
+
3. [**Narrowing**](03-narrowing.md) — how `if`, `case`, and
|
|
41
|
+
predicate methods sharpen a variable's type along the
|
|
42
|
+
branch.
|
|
43
|
+
4. [**Tuples and hash shapes**](04-tuples-and-shapes.md) — the
|
|
44
|
+
structural carriers Ruby's `[a, b, c]` literals and
|
|
45
|
+
`{key: value}` hashes get when Rigor can prove their layout.
|
|
46
|
+
Includes the **shape-projection functions** (`pick_of` /
|
|
47
|
+
`omit_of` / `partial_of` / `required_of` / `readonly_of`)
|
|
48
|
+
that mirror TypeScript's `Pick` / `Omit` / `Partial` /
|
|
49
|
+
`Required` / `Readonly` utility types.
|
|
50
|
+
5. [**Methods and blocks**](05-methods-and-blocks.md) — argument
|
|
51
|
+
typing, return-type inference, block parameters, arity.
|
|
52
|
+
6. [**Classes**](06-classes.md) — instance-side vs class-side,
|
|
53
|
+
`self`, `attr_accessor`, `Data.define`.
|
|
54
|
+
7. [**RBS and RBS::Extended**](07-rbs-and-extended.md) — when
|
|
55
|
+
inference cannot prove what the runtime actually returns,
|
|
56
|
+
how to nudge it through `.rbs` files and `%a{rigor:v1:…}`
|
|
57
|
+
directives.
|
|
58
|
+
8. [**Understanding errors**](08-understanding-errors.md) —
|
|
59
|
+
the rule catalogue (`call.undefined-method`,
|
|
60
|
+
`call.argument-type-mismatch`, `flow.always-raises`, …),
|
|
61
|
+
severity profiles, and `# rigor:disable` suppression.
|
|
62
|
+
9. [**Plugins**](09-plugins.md) — when to author one,
|
|
63
|
+
pointer to the [examples/](../../examples/README.md)
|
|
64
|
+
landing page.
|
|
65
|
+
10. [**Coexisting with Sorbet**](10-sorbet.md) — for users
|
|
66
|
+
arriving from a Sorbet-using project: the
|
|
67
|
+
[`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter
|
|
68
|
+
reads `sig { ... }` blocks, RBI files, and
|
|
69
|
+
`T.let` / `T.cast` / `T.must` / `T.unsafe` assertions
|
|
70
|
+
as type sources without rewriting in RBS.
|
|
71
|
+
11. [**Generating RBS with rigor sig-gen**](11-sig-gen.md)
|
|
72
|
+
— emitting RBS from Rigor's inference results, the
|
|
73
|
+
`new-file` / `new-method` / `tighter-return`
|
|
74
|
+
classification model, the `--print` / `--diff` /
|
|
75
|
+
`--write` modes, the `--params` policy and ADR-5
|
|
76
|
+
trade-off, RSpec-aware observations.
|
|
77
|
+
12. [**Lightweight HKT (JSON.parse and friends)**](12-lightweight-hkt.md)
|
|
78
|
+
— Rigor's defunctionalised higher-kinded type encoding
|
|
79
|
+
([ADR-20](../adr/20-lightweight-hkt.md), Yallop & White
|
|
80
|
+
2014 / fp-ts shape). Covers the bundled `json::value`
|
|
81
|
+
registration backing `JSON.parse` / `YAML.safe_load`,
|
|
82
|
+
the `symbolize_names: true` + `permitted_classes: [...]`
|
|
83
|
+
call-site discriminators, how to author your own URI
|
|
84
|
+
overlay in `.rbs`, the body grammar, the reducer's
|
|
85
|
+
lazy "tying-the-knot" handling for recursive sums, and
|
|
86
|
+
the conscious non-goals (no conditional bodies, no
|
|
87
|
+
multi-arg containers yet, no plugin manifest hookup).
|
|
88
|
+
|
|
89
|
+
### Appendix — Coming from another type checker
|
|
90
|
+
|
|
91
|
+
A short cross-language reference for readers whose mental
|
|
92
|
+
model of "static type checker" was set by another tool.
|
|
93
|
+
Each page maps Rigor's vocabulary onto the concepts you
|
|
94
|
+
already know — type carriers, narrowing primitives,
|
|
95
|
+
configuration shape, severity model, suppression — and
|
|
96
|
+
calls out the places where the two systems make different
|
|
97
|
+
choices.
|
|
98
|
+
|
|
99
|
+
- [**Coming from TypeScript**](appendix-typescript.md) —
|
|
100
|
+
the structural-vs-nominal-with-refinements split, `unknown`
|
|
101
|
+
/ `any` / `never` ↔ `Top` / `Dynamic[top]` / `Bot`,
|
|
102
|
+
type guards ↔ `predicate-if-true` directives, what
|
|
103
|
+
conditional / mapped types do not have a Rigor analogue.
|
|
104
|
+
- [**Coming from PHPStan**](appendix-phpstan.md) — the
|
|
105
|
+
closest peer in spirit. Identical refinement vocabulary
|
|
106
|
+
(`non-empty-string`, `int<min, max>`, `numeric-string`,
|
|
107
|
+
`literal-string`), `@phpstan-assert*` ↔ `RBS::Extended`,
|
|
108
|
+
Type-Specifying Extensions ↔ plugins, baseline diffing.
|
|
109
|
+
- [**Coming from mypy / Pyright**](appendix-mypy.md) — gradual
|
|
110
|
+
typing parallels, `Literal` ↔ `Constant`, `TypeGuard` /
|
|
111
|
+
`TypeIs` ↔ `predicate-if-true` / `predicate-if-false`,
|
|
112
|
+
`Protocol` ↔ RBS `interface`, `LiteralString` ↔
|
|
113
|
+
`literal-string`.
|
|
114
|
+
- [**Coming from Steep**](appendix-steep.md) — Ruby's other
|
|
115
|
+
RBS-driven static checker. Both consume the same `.rbs`
|
|
116
|
+
files; this page covers the layer each tool adds on top
|
|
117
|
+
and the coexistence pattern for projects that want to run
|
|
118
|
+
both.
|
|
119
|
+
- [**Coming from TypeProf**](appendix-typeprof.md) — Ruby's
|
|
120
|
+
official type *inference* tool. Both infer without
|
|
121
|
+
annotations; this page covers the whole-program-vs-local
|
|
122
|
+
analysis trade, why `rigor sig-gen` is the direct analogue
|
|
123
|
+
to the `typeprof` CLI, and the diagnostics-vs-RBS-output
|
|
124
|
+
split.
|
|
125
|
+
- [**Coming from Java or C#**](appendix-java-csharp.md) — the
|
|
126
|
+
two nominal, statically-typed languages share enough reflexes
|
|
127
|
+
(annotate-everything, generics, records, sealed
|
|
128
|
+
hierarchies, pattern-matching `switch`) to share one page.
|
|
129
|
+
Covers the inference-first vs annotate-first inversion,
|
|
130
|
+
records ↔ `Data.define`, C#'s `string?` / Java's
|
|
131
|
+
`Optional<T>` ↔ `T?`, declaration-site (C#) vs use-site
|
|
132
|
+
(Java) variance ↔ RBS, `dynamic` ↔ `Dynamic[top]`, and why
|
|
133
|
+
Rigor reports *unreachable* `case` clauses rather than
|
|
134
|
+
enforcing sealed-type exhaustiveness.
|
|
135
|
+
- [**Coming from Rust**](appendix-rust.md) — the sum-type and
|
|
136
|
+
exhaustive-`match` peer. `Option<T>` ↔ `T?`, `Result<T, E>`
|
|
137
|
+
↔ Ruby's raising model, `enum` variants ↔ a union of
|
|
138
|
+
`Data.define`, `match` ↔ `case`/`in` with the
|
|
139
|
+
enforce-totality vs report-unreachable inversion, traits
|
|
140
|
+
(nominal, coherent) ↔ RBS structural interfaces, and
|
|
141
|
+
refinements ↔ the newtype pattern. Ownership is a noted
|
|
142
|
+
non-goal.
|
|
143
|
+
- [**Coming from Go**](appendix-go.md) — the structural-typing
|
|
144
|
+
cousin. Go's implicitly satisfied `interface` *is* Rigor's
|
|
145
|
+
RBS interface, so the feature Java/C# readers stumble on is
|
|
146
|
+
the one a Go reader already has by reflex. Covers
|
|
147
|
+
`interface{}` / `any` ↔ `Dynamic[top]`, type switch ↔
|
|
148
|
+
`case`/`in`, errors-as-values ↔ raising, and the unions /
|
|
149
|
+
literal types / refinements Go does not have.
|
|
150
|
+
- [**Coming from Elixir**](appendix-elixir.md) — the closest
|
|
151
|
+
*philosophical* match: dynamic origin, gradual typing, and a
|
|
152
|
+
success-typing / no-false-positives stance shared with
|
|
153
|
+
Dialyzer. Covers pattern matching + guards ↔ narrowing (the
|
|
154
|
+
`flow.unreachable-clause` rule was modelled on Elixir's
|
|
155
|
+
clause-reachability work, [ADR-47](../adr/47-narrowing-driven-clause-reachability.md)),
|
|
156
|
+
set-theoretic types ↔ Rigor's unions + `~T` / `T - U`
|
|
157
|
+
operators, atoms ↔ symbols, and protocols / behaviours ↔
|
|
158
|
+
structural interfaces.
|
|
159
|
+
|
|
160
|
+
### Appendix — Protocols and structural typing
|
|
161
|
+
|
|
162
|
+
A standalone concept page for the question this handbook gets
|
|
163
|
+
from Python and Swift readers alike: *"where is Rigor's
|
|
164
|
+
`Protocol`?"* It untangles the one word that means two
|
|
165
|
+
unrelated things in Rigor — the **interface** (RBS structural
|
|
166
|
+
type, the Python `typing.Protocol` analogue) and the
|
|
167
|
+
**protocol contract** (ADR-28's path-scoped behavioural
|
|
168
|
+
contract) — so you reach for the right one.
|
|
169
|
+
|
|
170
|
+
- [**Protocols, interfaces, and structural typing**](appendix-protocols-and-structural-typing.md)
|
|
171
|
+
— RBS `interface` ↔ `typing.Protocol`, inferred object
|
|
172
|
+
shapes and capability roles, and how all of that differs
|
|
173
|
+
from the plugin-declared, path-scoped protocol contracts of
|
|
174
|
+
[ADR-28](../adr/28-path-scoped-protocol-contracts.md).
|
|
175
|
+
Includes the side-by-side "interface vs protocol contract"
|
|
176
|
+
table and a "which one do I want?" guide.
|
|
177
|
+
|
|
178
|
+
### Appendix — Connections to type theory
|
|
179
|
+
|
|
180
|
+
A short bridge between Rigor's vocabulary and the formal
|
|
181
|
+
type-theoretic concepts you may have seen in a programming-
|
|
182
|
+
languages textbook or in another type checker's documentation.
|
|
183
|
+
Read this if you came in from a "where does Rigor sit in the
|
|
184
|
+
type-theory landscape" question; the handbook proper stays
|
|
185
|
+
deliberately short on theory.
|
|
186
|
+
|
|
187
|
+
- [**Connections to type theory**](appendix-type-theory.md) —
|
|
188
|
+
the type lattice, subtyping vs gradual consistency, nominal
|
|
189
|
+
vs structural, the polymorphism family (parametric / subtype
|
|
190
|
+
/ ad-hoc), variance, refinement / predicate subtyping,
|
|
191
|
+
occurrence typing, gradual typing, effect systems, the
|
|
192
|
+
soundness vs completeness trade-off, and a short list of
|
|
193
|
+
features Rigor deliberately does not model (HKT,
|
|
194
|
+
higher-rank, full dependent types, …) — each with the
|
|
195
|
+
matching Rigor surface and a pointer into the spec corpus.
|
|
196
|
+
- [**The Liskov Substitution Principle**](appendix-liskov.md) —
|
|
197
|
+
why LSP is a *behavioural* discipline that applies to Ruby
|
|
198
|
+
*more* than to a statically-checked language (not less — the
|
|
199
|
+
"Ruby isn't statically typed so LSP is optional" claim gets
|
|
200
|
+
the principle backwards), how Rigor's robustness principle
|
|
201
|
+
(strict returns, lenient parameters) re-derives the LSP
|
|
202
|
+
signature rule (covariant returns, contravariant parameters)
|
|
203
|
+
from Ruby-adoption ergonomics rather than substitutability
|
|
204
|
+
proofs, why that convergence means Rigor's defaults do not
|
|
205
|
+
fight duck typing or the "L" of SOLID, and which
|
|
206
|
+
behavioral-subtyping obligations (cross-hierarchy override
|
|
207
|
+
compatibility, exception rules, Design-by-Contract, the
|
|
208
|
+
history constraint) Rigor does *not* statically enforce. On
|
|
209
|
+
this page only, "LSP" means Liskov — not the Language Server.
|
|
210
|
+
|
|
211
|
+
## How to read this handbook
|
|
212
|
+
|
|
213
|
+
Each chapter is short on theory and long on examples. Every
|
|
214
|
+
example is real Ruby that runs under MRI as written; the
|
|
215
|
+
prose around it is what `rigor check` would say about that
|
|
216
|
+
code.
|
|
217
|
+
|
|
218
|
+
When you see an `assert_type(...)` line in a snippet, that is
|
|
219
|
+
Rigor's introspection helper, not a runtime check — it pins
|
|
220
|
+
the inferred type at that program point so you can compare
|
|
221
|
+
the prose to the actual analyzer output. `dump_type(...)` is
|
|
222
|
+
the same idea but emits a notice instead of failing on
|
|
223
|
+
mismatch.
|
|
224
|
+
|
|
225
|
+
Snippet conventions:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
n = 1 + 2
|
|
229
|
+
assert_type("3", n) # Rigor folds the literal sum
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
means: at the `assert_type` call, Rigor's inference for `n` is
|
|
233
|
+
`3` — the folded literal value.
|
|
234
|
+
|
|
235
|
+
Wording convention — **"interface"**: Ruby has no `interface`
|
|
236
|
+
keyword, so most readers import the word's meaning from another
|
|
237
|
+
language, and the Java / PHP *nominal* sense (a class declares
|
|
238
|
+
`implements`) dominates. Rigor's RBS `interface` is the opposite —
|
|
239
|
+
*structural*, like Go's `interface` or Python's `Protocol`, satisfied
|
|
240
|
+
by having the methods with no declaration. To avoid the misread,
|
|
241
|
+
qualify the word on first use in any chapter as **"structural
|
|
242
|
+
interface"** or **"RBS interface,"** never the bare "interface." The
|
|
243
|
+
[Protocols and structural typing appendix](appendix-protocols-and-structural-typing.md)
|
|
244
|
+
is the canonical explainer.
|
|
245
|
+
|
|
246
|
+
When a chapter references a more formal document, the link
|
|
247
|
+
takes you out of the handbook into the binding spec corpus or
|
|
248
|
+
ADRs:
|
|
249
|
+
|
|
250
|
+
- [`docs/types.md`](../types.md) — one-page mental model.
|
|
251
|
+
- [`docs/type-specification/`](../type-specification/README.md)
|
|
252
|
+
— normative spec corpus.
|
|
253
|
+
- [`docs/internal-spec/`](../internal-spec/README.md) —
|
|
254
|
+
analyzer-internal contracts (engine surface, type-object
|
|
255
|
+
public API).
|
|
256
|
+
- [`docs/adr/`](../adr/) — architecture decision records.
|
|
257
|
+
|
|
258
|
+
## Non-goals
|
|
259
|
+
|
|
260
|
+
The handbook is meant to be readable cover-to-cover in a few
|
|
261
|
+
hours. To keep it short:
|
|
262
|
+
|
|
263
|
+
- It does **not** introduce Ruby itself. `def`, `class`,
|
|
264
|
+
blocks, modules, `attr_*`, regex, RBS basics — all assumed.
|
|
265
|
+
- It does **not** cover every edge case. Edge cases live in
|
|
266
|
+
the spec corpus.
|
|
267
|
+
- It does **not** discuss internal contracts (engine surface,
|
|
268
|
+
type-object public API). Those live in
|
|
269
|
+
[`docs/internal-spec/`](../internal-spec/README.md).
|
|
270
|
+
- It does **not** cover plugin **authoring** — that is the
|
|
271
|
+
job of [examples/](../../examples/README.md). Chapter 9 is
|
|
272
|
+
a one-page pointer.
|
|
273
|
+
|
|
274
|
+
If a topic comes up that the handbook does not explain, the
|
|
275
|
+
relevant spec document is one click away.
|