rigortype 0.2.0 → 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 +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- 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 +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -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/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- 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 +120 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# Appendix — Coming from Rust
|
|
2
|
+
|
|
3
|
+
If your mental model of "types" was set by Rust, this appendix
|
|
4
|
+
maps Rigor's vocabulary onto the concepts you already know.
|
|
5
|
+
Rust and Rigor sit at opposite ends of one axis: Rust is
|
|
6
|
+
ahead-of-time, sound, and refuses to compile anything it cannot
|
|
7
|
+
prove safe; Rigor analyses Ruby that already runs and stays
|
|
8
|
+
silent on anything it cannot prove *wrong*. But they meet
|
|
9
|
+
often on the others: sum types, exhaustive
|
|
10
|
+
matching, the absence of a billion-dollar null.
|
|
11
|
+
|
|
12
|
+
This is a translation table plus a discussion of the places
|
|
13
|
+
where the two systems make genuinely different choices. Those
|
|
14
|
+
are where your Rust reflexes will mislead you. The biggest one:
|
|
15
|
+
Rust's type checker is a gate the program must pass before it
|
|
16
|
+
exists; Rigor's is an advisor over a program that already runs.
|
|
17
|
+
There is no borrow checker, no ownership, and no "it does not
|
|
18
|
+
compile." The Ruby ran, and Rigor tells you where it can prove
|
|
19
|
+
a type goes wrong.
|
|
20
|
+
|
|
21
|
+
## The five-second pitch
|
|
22
|
+
|
|
23
|
+
| Question | Rust | Rigor |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| When does the checker run? | At compile time; nothing runs until it passes | After the fact, on code that already runs |
|
|
26
|
+
| Soundness stance | Sound — a type error is a hard stop | No-false-positives — silent unless it can prove the error |
|
|
27
|
+
| Where do annotations live? | In source; locals inferred, signatures explicit | In `.rbs` files; whole bodies inferred |
|
|
28
|
+
| The "I don't know" type | (none — Rust has no escape hatch) | `Dynamic[top]` — silent at the boundary |
|
|
29
|
+
| Null | Does not exist (`Option<T>` instead) | `nil` exists; narrowed away like Rust narrows `Option` |
|
|
30
|
+
| Identity of types | Nominal, with trait coherence | Nominal + structural facets |
|
|
31
|
+
|
|
32
|
+
Rust earns its guarantees by refusing to run until every value
|
|
33
|
+
is accounted for. Rigor takes the opposite bet: the program
|
|
34
|
+
runs, most of it is fine, and the analyzer should only speak
|
|
35
|
+
when it can *prove* a problem — never frighten working code over
|
|
36
|
+
a worst-case it cannot rule out. If "the compiler is always
|
|
37
|
+
right and I obey it" is your reflex, the one to retrain is that
|
|
38
|
+
Rigor is an advisor, not a gate.
|
|
39
|
+
|
|
40
|
+
## Type vocabulary mapping
|
|
41
|
+
|
|
42
|
+
| Rust | Rigor | Notes |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `i8` / `i32` / `i64` / `u32` / `usize` | `Integer` | Ruby integers are arbitrary-precision; no width or signedness in the type. |
|
|
45
|
+
| `f32` / `f64` | `Float` | `Numeric` is the common supertype. |
|
|
46
|
+
| `bool` | `bool` (`Constant<true> \| Constant<false>`) | Structurally a union of two constants. |
|
|
47
|
+
| `char` / `&str` / `String` | `String` | Ruby has one string type; no borrow distinction. |
|
|
48
|
+
| `()` (unit) | `nil` / `void` | `void` when the caller must ignore the result; `nil` as a value. |
|
|
49
|
+
| `!` (never) | `Bot` | Empty type — unreachable branches, `raise`-only bodies. |
|
|
50
|
+
| `Option<T>` | `T?` (i.e. `T \| nil`) | See [Option and Result](#option-and-result). |
|
|
51
|
+
| `Result<T, E>` | (no single carrier — Ruby raises) | See [Option and Result](#option-and-result). |
|
|
52
|
+
| `Vec<T>` / `&[T]` | `Array[T]` | |
|
|
53
|
+
| `HashMap<K, V>` | `Hash[K, V]` | |
|
|
54
|
+
| `HashSet<T>` | `Set[T]` | |
|
|
55
|
+
| `(i32, String)` (tuple) | `Tuple[Integer, String]` | Same per-position model. |
|
|
56
|
+
| `struct Point { x: i32, y: i32 }` | `Point = Data.define(:x, :y)` | See [Structs ↔ Data.define](#structs--datadefine). |
|
|
57
|
+
| `enum E { A, B(i32) }` (sum type) | union of the variants | See [Sum types & exhaustiveness](#sum-types-and-exhaustiveness). |
|
|
58
|
+
| `trait T { … }` | RBS `interface` (structural) | Nominal in Rust, structural in Rigor — see below. |
|
|
59
|
+
| `<T: Trait>` / `where T: Trait` | RBS `[T < Bound]` bounded parameter | |
|
|
60
|
+
| `dyn Trait` | a structural interface type | Dynamic dispatch over the method set. |
|
|
61
|
+
| `Box<dyn Any>` | `Dynamic[top]` | The closest Rust has to "silence the checker"; Rigor reaches for it routinely, Rust almost never. |
|
|
62
|
+
| (no literal types) | `Constant<42>` / `Constant<"hi">` | Rust has no literal types (const generics aside); a Rigor novelty. |
|
|
63
|
+
|
|
64
|
+
## Option and Result
|
|
65
|
+
|
|
66
|
+
Rust's two famous enums split cleanly: one maps almost exactly
|
|
67
|
+
onto Rigor, the other does not, because Ruby chose a different
|
|
68
|
+
error-handling model.
|
|
69
|
+
|
|
70
|
+
**`Option<T>` ↔ `T?`.** This is a near-perfect match.
|
|
71
|
+
`Option<T>` is "a `T` or nothing"; Rigor's `T?` is `T | nil`,
|
|
72
|
+
and the narrowing mirrors `match` / `if let`:
|
|
73
|
+
|
|
74
|
+
```rust
|
|
75
|
+
fn length(s: Option<String>) -> usize {
|
|
76
|
+
match s {
|
|
77
|
+
Some(v) => v.len(),
|
|
78
|
+
None => 0,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
def length(s) # s : String? (RBS-declared)
|
|
85
|
+
return 0 if s.nil?
|
|
86
|
+
s.length # s : String — nil stripped by the guard
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The reflex to drop is `unwrap()`. In Rust you reach for
|
|
91
|
+
`.unwrap()` / `.expect()` when you *know* it is `Some`. Rigor
|
|
92
|
+
has no in-source assertion that lies to the checker; the
|
|
93
|
+
equivalents are a `nil?` guard (checked, not asserted) or
|
|
94
|
+
`T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet/)
|
|
95
|
+
plugin (see [Chapter 10](10-sorbet.md)).
|
|
96
|
+
|
|
97
|
+
**`Result<T, E>` ↔ exceptions.** Here the models diverge. Ruby
|
|
98
|
+
signals failure by *raising*, not by returning a tagged value,
|
|
99
|
+
so there is no single `Result` carrier. A Rust function
|
|
100
|
+
returning `Result<T, E>` becomes a Ruby method that returns `T`
|
|
101
|
+
and raises on the error path:
|
|
102
|
+
|
|
103
|
+
```rust
|
|
104
|
+
fn parse(s: &str) -> Result<i32, ParseIntError> { s.parse() }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
def parse(s) # returns Integer, raises on bad input
|
|
109
|
+
Integer(s)
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Rigor does not track the exception type as part of the
|
|
114
|
+
signature — there is no typed `throws` and no `?` operator. If
|
|
115
|
+
you want to keep Rust's value-returning style, you *can* return
|
|
116
|
+
a tagged tuple (`[:ok, value]` / `[:error, reason]`) and pattern-
|
|
117
|
+
match it with `case`/`in`, and Rigor will type the `Tuple` and
|
|
118
|
+
the union precisely. That is a deliberate port of the Rust
|
|
119
|
+
idiom, not the idiomatic Ruby.
|
|
120
|
+
|
|
121
|
+
## Narrowing — `match` / `if let`
|
|
122
|
+
|
|
123
|
+
Rust narrows through `match`, `if let`, and pattern binding.
|
|
124
|
+
Rigor has direct analogues; the behaviour matches even though
|
|
125
|
+
the surface differs.
|
|
126
|
+
|
|
127
|
+
| Rust | Rigor |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `match x { … }` | `case x; in …` |
|
|
130
|
+
| `if let Some(v) = x` | `if x` (strips `nil`), or `case x; in val` |
|
|
131
|
+
| `if let Pat = x { … }` (binding) | `case x; in Pat => v` |
|
|
132
|
+
| `x as i64` (numeric cast) | `x.to_i` / `Integer(x)` — a real conversion, not a type cast |
|
|
133
|
+
| `x.unwrap()` | (no in-source assertion) — `nil?` guard, or `T.must` via `rigor-sorbet` |
|
|
134
|
+
| `matches!(x, Pat)` | `case x; in Pat then true; else false; end`, or an `is_a?` predicate |
|
|
135
|
+
| guard `Pat if cond =>` | `in Pat if cond` (pattern guard) |
|
|
136
|
+
|
|
137
|
+
The structural-pattern part of `match` maps onto `case`/`in`
|
|
138
|
+
one-to-one: `in Circle => c` narrows `x` to `Circle` along that
|
|
139
|
+
clause exactly as `Circle(c) =>` does in Rust.
|
|
140
|
+
|
|
141
|
+
## Sum types and exhaustiveness
|
|
142
|
+
|
|
143
|
+
Rust's `enum` is an algebraic sum type, and `match` over it is
|
|
144
|
+
*compiler-enforced* exhaustive — miss a variant and it does not
|
|
145
|
+
compile. Rigor models the data the same way but approaches
|
|
146
|
+
exhaustiveness from the dual side.
|
|
147
|
+
|
|
148
|
+
A Rust `enum` whose variants carry data becomes, in Ruby, one
|
|
149
|
+
`Data.define` per variant plus a union of them:
|
|
150
|
+
|
|
151
|
+
```rust
|
|
152
|
+
enum Shape {
|
|
153
|
+
Circle { radius: f64 },
|
|
154
|
+
Rectangle { w: f64, h: f64 },
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn area(s: Shape) -> f64 {
|
|
158
|
+
match s {
|
|
159
|
+
Shape::Circle { radius } => PI * radius * radius,
|
|
160
|
+
Shape::Rectangle { w, h } => w * h,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
Circle = Data.define(:radius)
|
|
167
|
+
Rectangle = Data.define(:w, :h)
|
|
168
|
+
|
|
169
|
+
def area(s)
|
|
170
|
+
case s
|
|
171
|
+
in Circle then Math::PI * s.radius * s.radius
|
|
172
|
+
in Rectangle then s.w * s.h
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The crucial difference is *direction*.
|
|
178
|
+
[ADR-47](../adr/47-narrowing-driven-clause-reachability.md)'s
|
|
179
|
+
`flow.unreachable-clause` rule fires when a clause is provably
|
|
180
|
+
*dead* — its subject already narrowed to `Bot` by prior clauses
|
|
181
|
+
or prior exhaustion:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
case shape
|
|
185
|
+
in Circle then shape.radius
|
|
186
|
+
in Rectangle then shape.width * shape.height
|
|
187
|
+
in Circle then "…" # flow.unreachable-clause — Circle already covered
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Rust **requires** you to cover every variant and rejects a
|
|
192
|
+
non-exhaustive `match`. Rigor does the dual — it reports clauses
|
|
193
|
+
that can never run, but it does **not** force you to handle every
|
|
194
|
+
variant. A `case` that omits a branch is not an error; an
|
|
195
|
+
*unreachable* clause is. This follows the no-false-positives
|
|
196
|
+
stance: an omitted branch may be intentional (that variant
|
|
197
|
+
cannot reach this point), and Rigor will not frighten working
|
|
198
|
+
code over it. If you want the missing-arm safety back, a
|
|
199
|
+
trailing `else raise` makes the omission explicit — the analogue
|
|
200
|
+
of Rust's `_ => unreachable!()`.
|
|
201
|
+
|
|
202
|
+
## Structs ↔ `Data.define`
|
|
203
|
+
|
|
204
|
+
A Rust `struct` maps onto Ruby's `Data.define` — an immutable,
|
|
205
|
+
value-equal, member-shaped aggregate. Rigor models it natively
|
|
206
|
+
([ADR-48](../adr/48-data-struct-value-folding.md)).
|
|
207
|
+
|
|
208
|
+
```rust
|
|
209
|
+
struct Point { x: i32, y: i32 }
|
|
210
|
+
let p = Point { x: 1, y: 2 };
|
|
211
|
+
let a = p.x; // a : i32
|
|
212
|
+
let q = Point { x: 9, ..p }; // functional update
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Point = Data.define(:x, :y)
|
|
217
|
+
p = Point.new(1, 2)
|
|
218
|
+
assert_type("1", p.x) # member value is folded, not just Integer
|
|
219
|
+
q = p.with(x: 9) # Data#with ↔ Rust's ..p update
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Two things go beyond Rust here:
|
|
223
|
+
|
|
224
|
+
- **Member values fold.** `Point.new(1, 2).x` is `1`,
|
|
225
|
+
not merely `Integer`. Rust erases the literal at
|
|
226
|
+
construction; Rigor keeps it (subject to the folding budget).
|
|
227
|
+
- **`with` is first-class.** `Data#with` is the analogue of
|
|
228
|
+
Rust's `..p` functional-update syntax, and Rigor types the
|
|
229
|
+
result with the overridden member folded in.
|
|
230
|
+
|
|
231
|
+
Ruby's mutable `Struct` is deliberately not folded the same way
|
|
232
|
+
yet; its mutability breaks the value-folding soundness story.
|
|
233
|
+
See [Chapter 6](06-classes.md).
|
|
234
|
+
|
|
235
|
+
## Traits ↔ RBS interfaces
|
|
236
|
+
|
|
237
|
+
Rust traits and Rigor's structural interfaces both describe "a
|
|
238
|
+
type that has these methods," but they differ on *how
|
|
239
|
+
membership is decided*, and the difference is the same one
|
|
240
|
+
Go programmers feel from the other side.
|
|
241
|
+
|
|
242
|
+
A Rust trait is **nominal with coherence**: a type has the trait
|
|
243
|
+
only if there is an explicit `impl Trait for Type`, and the
|
|
244
|
+
orphan rule governs where that `impl` may live. Rigor's RBS
|
|
245
|
+
`interface` is **structural**: any object with the right methods
|
|
246
|
+
satisfies it, no declaration of intent, no coherence rule. This
|
|
247
|
+
is Go's `interface`, not Rust's `trait`.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# An RBS structural interface
|
|
251
|
+
interface _Drawable
|
|
252
|
+
def draw: () -> String
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Any Ruby object that responds to `draw` returning a `String`
|
|
257
|
+
satisfies `_Drawable` — you never write `impl _Drawable for …`.
|
|
258
|
+
Rigor also infers anonymous object *shapes* and capability roles
|
|
259
|
+
without any interface declared at all. The
|
|
260
|
+
[structural-typing appendix](appendix-protocols-and-structural-typing.md)
|
|
261
|
+
is the canonical explainer; the short version is "traits you do
|
|
262
|
+
not have to implement on purpose."
|
|
263
|
+
|
|
264
|
+
## Refinements vs the newtype pattern
|
|
265
|
+
|
|
266
|
+
When a Rust invariant outruns the type system — "a string that
|
|
267
|
+
is non-empty," "an integer in 1..=9" — you reach for the newtype
|
|
268
|
+
pattern: a `struct NonEmptyString(String)` with a validating
|
|
269
|
+
constructor. Rigor has first-class refinement carriers instead;
|
|
270
|
+
the invariant rides on the ordinary type, produced automatically
|
|
271
|
+
by narrowing.
|
|
272
|
+
|
|
273
|
+
| Rigor refinement | Rust idiom | Comment |
|
|
274
|
+
| --- | --- | --- |
|
|
275
|
+
| `non-empty-string` | `struct NonEmptyString(String)` newtype | Rigor produces it from `unless s.empty?`, no wrapper. |
|
|
276
|
+
| `positive-int` | `struct PositiveInt(u32)` newtype | Rigor narrows from `n > 0`. |
|
|
277
|
+
| `int<1, 9>` | newtype + range check, or const generics gymnastics | Rigor's range carrier handles arbitrary bounds directly. |
|
|
278
|
+
| `numeric-string` | newtype wrapping validated parse | No type-level analogue. |
|
|
279
|
+
| `non-empty-array[T]` | newtype over `Vec<T>` | Rigor produces it from `unless arr.empty?`. |
|
|
280
|
+
|
|
281
|
+
If you have ever written a newtype purely to encode an invariant
|
|
282
|
+
the compiler could not express on the base type, this is the
|
|
283
|
+
part of Rigor that earns its keep: no wrapper allocation, the
|
|
284
|
+
invariant narrowed from a plain `if`.
|
|
285
|
+
|
|
286
|
+
## Generics
|
|
287
|
+
|
|
288
|
+
RBS generics are what Rigor reads; they are more conservative
|
|
289
|
+
than Rust's. RBS supports class- and method-level type
|
|
290
|
+
parameters with bounds, but does not infer call-site
|
|
291
|
+
instantiation as eagerly, and has nothing like trait-bound
|
|
292
|
+
dispatch resolution.
|
|
293
|
+
|
|
294
|
+
| Rust | Rigor (via RBS) |
|
|
295
|
+
| --- | --- |
|
|
296
|
+
| `fn id<T>(x: T) -> T` | `def id: [T] (T) -> T` |
|
|
297
|
+
| `Vec<T>` | `Array[T]` |
|
|
298
|
+
| `HashMap<K, V>` | `Hash[K, V]` |
|
|
299
|
+
| `fn f<T: Ord>(x: T)` | `def f: [T < Comparable[T]] (T) -> void` |
|
|
300
|
+
| `impl Trait` return | a structural interface return type |
|
|
301
|
+
|
|
302
|
+
Rust's associated types, higher-ranked trait bounds, and const
|
|
303
|
+
generics have no RBS analogue. Rigor's generics are deliberately
|
|
304
|
+
modest so the analyzer stays fast on real Ruby.
|
|
305
|
+
|
|
306
|
+
## Severity, suppression, and "strict mode"
|
|
307
|
+
|
|
308
|
+
| Rust | Rigor |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| `#![deny(warnings)]` / lint levels | `severity_profile: lenient` / `balanced` / `strict` |
|
|
311
|
+
| `#[allow(lint_name)]` | `# rigor:disable <rule>` |
|
|
312
|
+
| crate-level `#![allow(…)]` | `# rigor:disable-file all` |
|
|
313
|
+
| `cargo check` (the gate) | `rigor check lib` (the advisor) |
|
|
314
|
+
|
|
315
|
+
The mental shift: `cargo check` is part of the build, so code does
|
|
316
|
+
not ship until it passes. `rigor check` is not a gate the
|
|
317
|
+
program must clear; it is a diagnostic surface you tune with
|
|
318
|
+
severity profiles and adopt incrementally via baselines. The
|
|
319
|
+
program already runs.
|
|
320
|
+
|
|
321
|
+
## What Rust has and Rigor does not
|
|
322
|
+
|
|
323
|
+
Be honest about what you give up:
|
|
324
|
+
|
|
325
|
+
- **Ownership and borrowing.** Rust's defining feature has no
|
|
326
|
+
Rigor analogue — Ruby is garbage-collected and aliases freely.
|
|
327
|
+
Rigor does not model lifetimes, moves, or `&mut` exclusivity.
|
|
328
|
+
(This is not a gap Rigor is trying to fill; it is a different
|
|
329
|
+
language's contract.)
|
|
330
|
+
- **Enforced exhaustiveness.** A non-exhaustive `match` is a
|
|
331
|
+
Rust compile error. Rigor reports *unreachable* clauses, not
|
|
332
|
+
*missing* ones — by design (see above).
|
|
333
|
+
- **No-null guarantee.** Rust *eliminates* null; `Option<T>` is
|
|
334
|
+
the only absence. Rigor's `nil` still exists — it narrows it
|
|
335
|
+
away, but it cannot promise a value is never `nil` the way
|
|
336
|
+
Rust's type system can.
|
|
337
|
+
- **`Result` / the `?` operator.** Typed, value-level error
|
|
338
|
+
propagation has no Rigor analogue; Ruby raises.
|
|
339
|
+
- **Trait coherence and associated types.** The `impl`-based,
|
|
340
|
+
orphan-ruled trait machinery is outside RBS's structural model.
|
|
341
|
+
- **Const generics, zero-cost guarantees, `unsafe`.** All
|
|
342
|
+
compile-model concepts with no place in a runtime advisor.
|
|
343
|
+
|
|
344
|
+
## What Rigor has and Rust does not
|
|
345
|
+
|
|
346
|
+
The other direction:
|
|
347
|
+
|
|
348
|
+
- **Literal / constant types.** `Constant<42>`, `Constant<:ok>`,
|
|
349
|
+
`Constant<"FOO">`. Rust has no literal types; the nearest is a
|
|
350
|
+
unit-variant `enum`, hand-declared. Rigor infers them from
|
|
351
|
+
ordinary values.
|
|
352
|
+
- **Constant folding through method calls.** `"foo".upcase` is
|
|
353
|
+
`Constant<"FOO">`, not `String`. Rigor catalogues which
|
|
354
|
+
built-in methods are pure and folds through them.
|
|
355
|
+
- **Refinements without a newtype.** Invariants on the ordinary
|
|
356
|
+
type, no wrapper struct and no validating constructor.
|
|
357
|
+
- **Structural facets without an `impl`.** A Ruby object that has
|
|
358
|
+
the right methods satisfies an RBS `interface` with no
|
|
359
|
+
declaration of intent — and Rigor infers anonymous shapes and
|
|
360
|
+
capability roles besides.
|
|
361
|
+
- **No-false-positives stance.** Rigor stays silent on
|
|
362
|
+
`Dynamic[top]` receivers rather than complaining. You will
|
|
363
|
+
never see a diagnostic whose honest answer is "well, the
|
|
364
|
+
checker cannot know."
|
|
365
|
+
- **No annotation tax.** `rigor check` on a Ruby project with
|
|
366
|
+
zero `.rbs` files still yields useful diagnostics from
|
|
367
|
+
inference. Adding `.rbs` is incremental — every file you skip
|
|
368
|
+
is `Dynamic[top]` at the boundary, not an error.
|
|
369
|
+
|
|
370
|
+
## A migration vignette
|
|
371
|
+
|
|
372
|
+
You are porting a Rust module — a sum type with an exhaustive
|
|
373
|
+
`match` and an `Option`-returning lookup — to Ruby. The
|
|
374
|
+
original:
|
|
375
|
+
|
|
376
|
+
```rust
|
|
377
|
+
enum Shape {
|
|
378
|
+
Circle { radius: f64 },
|
|
379
|
+
Rectangle { w: f64, h: f64 },
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fn area(s: &Shape) -> f64 {
|
|
383
|
+
match s {
|
|
384
|
+
Shape::Circle { radius } => PI * radius * radius,
|
|
385
|
+
Shape::Rectangle { w, h } => w * h,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn first_circle(shapes: &[Shape]) -> Option<&Shape> {
|
|
390
|
+
shapes.iter().find(|s| matches!(s, Shape::Circle { .. }))
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The Rigor approach — `Data.define` for the variants, `case`/`in`
|
|
395
|
+
for the dispatch, `T?` for the optional, no annotations:
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# lib/shape.rb
|
|
399
|
+
Circle = Data.define(:radius)
|
|
400
|
+
Rectangle = Data.define(:w, :h)
|
|
401
|
+
|
|
402
|
+
def area(s)
|
|
403
|
+
case s
|
|
404
|
+
in Circle then Math::PI * s.radius * s.radius
|
|
405
|
+
in Rectangle then s.w * s.h
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def first_circle(shapes)
|
|
410
|
+
shapes.find { |s| s.is_a?(Circle) } # returns Circle? (Circle | nil)
|
|
411
|
+
end
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
What carries over and what changes:
|
|
415
|
+
|
|
416
|
+
- The `enum` variants become `Data.define` — immutable, value-
|
|
417
|
+
equal, member-shaped, and Rigor folds their member reads.
|
|
418
|
+
- The `match` becomes `case`/`in`; `in Circle` narrows `s`
|
|
419
|
+
exactly as `Shape::Circle { .. }` does.
|
|
420
|
+
- `Option<&Shape>` becomes a plain `Circle?` return —
|
|
421
|
+
`Array#find` yields the element or `nil`, and a `nil?` guard
|
|
422
|
+
at the call site narrows it, the way `if let Some(c)` would.
|
|
423
|
+
- The exhaustive `match` loses its enforced totality. Rigor will
|
|
424
|
+
not demand the third variant you have not written; it will
|
|
425
|
+
only tell you if a clause you *did* write can never run. Add a
|
|
426
|
+
trailing `else raise` if you want Rust's `_ => unreachable!()`.
|
|
427
|
+
|
|
428
|
+
## What's next
|
|
429
|
+
|
|
430
|
+
You probably do not need to read the rest of the handbook
|
|
431
|
+
sequentially. Useful pointers:
|
|
432
|
+
|
|
433
|
+
- [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
|
|
434
|
+
the direct analogue to `match` / `if let` narrowing.
|
|
435
|
+
- [Chapter 6 — Classes](06-classes.md) for `Data.define`, the
|
|
436
|
+
`struct` analogue, and its value-folding.
|
|
437
|
+
- [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
|
|
438
|
+
for the directive grammar — `predicate-if-true` is the
|
|
439
|
+
user-defined narrowing analogue.
|
|
440
|
+
|
|
441
|
+
If you want to compare against another tool, the sibling
|
|
442
|
+
appendix pages cover [TypeScript](appendix-typescript.md),
|
|
443
|
+
[PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
|
|
444
|
+
[Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
|
|
445
|
+
[Java / C#](appendix-java-csharp.md), [Go](appendix-go.md), and
|
|
446
|
+
[Elixir](appendix-elixir.md).
|