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,370 @@
|
|
|
1
|
+
# Appendix — Coming from Elixir
|
|
2
|
+
|
|
3
|
+
If your mental model of "types" was set by Elixir — Dialyzer's
|
|
4
|
+
success typing, `@spec` / `@type`, and the set-theoretic gradual
|
|
5
|
+
types now rolling into the language — this appendix maps Rigor's
|
|
6
|
+
vocabulary onto it. Of every page in this series, this is the
|
|
7
|
+
one whose *philosophy* matches Rigor most closely: both are
|
|
8
|
+
dynamic languages at heart, both add types gradually, both
|
|
9
|
+
refuse to cry wolf. Elixir's type checker only flags what it can
|
|
10
|
+
prove will fail; so does Rigor. That instinct (never frighten
|
|
11
|
+
working code) is shared DNA, not coincidence.
|
|
12
|
+
|
|
13
|
+
There is even a direct lineage: Rigor's clause-reachability rule
|
|
14
|
+
([ADR-47](../adr/47-narrowing-driven-clause-reachability.md)) was
|
|
15
|
+
modelled on Elixir's own work on detecting impossible `case`
|
|
16
|
+
clauses. You are coming from one of Rigor's design influences.
|
|
17
|
+
|
|
18
|
+
This is a translation table plus a discussion of where the two
|
|
19
|
+
diverge (Elixir is functional, immutable, and process-oriented;
|
|
20
|
+
Rigor analyses an object-oriented, mutable language) and where
|
|
21
|
+
they line up better than you would guess.
|
|
22
|
+
|
|
23
|
+
## The five-second pitch
|
|
24
|
+
|
|
25
|
+
| Question | Elixir | Rigor |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Origin | Dynamic; types added gradually | Dynamic; types added gradually |
|
|
28
|
+
| Soundness stance | Success typing — flag only provable failures | No-false-positives — silent unless it can prove the error |
|
|
29
|
+
| Where do annotations live? | `@spec` / `@type` in source | `.rbs` files alongside `.rb` |
|
|
30
|
+
| The "I don't know" type | `dynamic()` / `term()` | `Dynamic[top]` / `Top` |
|
|
31
|
+
| Narrowing engine | Pattern matching + guards | Pattern matching + guards + predicate methods |
|
|
32
|
+
| Type algebra | Set-theoretic (union / intersection / negation) | Union + type operators (`~T`, `T - U`) |
|
|
33
|
+
|
|
34
|
+
Elixir and Rigor agree on the thing that matters most: a type
|
|
35
|
+
checker for a dynamic language should be an advisor that earns
|
|
36
|
+
trust by never false-alarming, not a gate that rejects programs.
|
|
37
|
+
Dialyzer's "I will only tell you about a path that *cannot*
|
|
38
|
+
succeed" is the same contract as Rigor's "I stay silent unless I
|
|
39
|
+
can prove the error." If that stance is why you trust Dialyzer,
|
|
40
|
+
you will recognise Rigor immediately.
|
|
41
|
+
|
|
42
|
+
## Type vocabulary mapping
|
|
43
|
+
|
|
44
|
+
| Elixir | Rigor | Notes |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `integer()` | `Integer` | Both arbitrary-precision. |
|
|
47
|
+
| `float()` | `Float` | `Numeric` is the common supertype. |
|
|
48
|
+
| `boolean()` (`true \| false`) | `bool` (`Constant<true> \| Constant<false>`) | Structurally a union of two constants in both systems. |
|
|
49
|
+
| `:foo` (atom) | `Constant<:foo>` (Symbol) | Atoms ↔ symbols — a direct match. See [Atoms ↔ symbols](#atoms--symbols). |
|
|
50
|
+
| `nil` | `nil` (`Constant<nil>`) | Elixir's `nil` is the atom `nil`; Ruby's is its own singleton. Both falsy. |
|
|
51
|
+
| `binary()` / `String.t()` | `String` | |
|
|
52
|
+
| `term()` / `any()` | `Top` / `Dynamic[top]` | `any()` for "anything"; `dynamic()` for the gradual escape hatch. |
|
|
53
|
+
| `none()` | `Bot` | Empty type — no inhabitants. |
|
|
54
|
+
| `[t]` (list) | `Array[T]` | |
|
|
55
|
+
| `{a, b}` (tuple) | `Tuple[A, B]` | Same per-position model. |
|
|
56
|
+
| `%{optional(k) => v}` (map) | `Hash[K, V]` | |
|
|
57
|
+
| `%{a: t}` (map with known keys) | `HashShape{a: T}` | Closed shape with known keys. |
|
|
58
|
+
| `%User{}` (struct) | `User = Data.define(...)` | A named, member-shaped value. |
|
|
59
|
+
| `[key: t]` (keyword list) | `Hash[Symbol, T]` or `Array[Tuple]` | Ruby's keyword-ish data is a `Hash`. |
|
|
60
|
+
| `t \| u` (set-theoretic union) | `T \| U` | Same display; same idea. |
|
|
61
|
+
| `dynamic()` | `Dynamic[top]` | The "be silent here" gradual carrier. |
|
|
62
|
+
| `(integer() -> binary())` | `^(Integer) -> String` (RBS proc syntax) | |
|
|
63
|
+
|
|
64
|
+
## Pattern matching & guards ↔ narrowing
|
|
65
|
+
|
|
66
|
+
This is the section where an Elixir programmer feels at home,
|
|
67
|
+
because Ruby borrowed pattern matching in a shape close to
|
|
68
|
+
Elixir's — including the pin operator `^` — and Rigor's
|
|
69
|
+
narrowing engine is built around it.
|
|
70
|
+
|
|
71
|
+
| Elixir | Rigor |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `case x do {:ok, v} -> … end` | `case x; in [:ok, v] then …` |
|
|
74
|
+
| function-head match `def f(%Circle{} = c)` | `case x; in Circle => c` (Ruby has no multi-clause heads) |
|
|
75
|
+
| guard `when is_integer(x)` | `if x.is_a?(Integer)`, or `in Integer` |
|
|
76
|
+
| guard `when x > 0` | `n > 0` narrows to `positive-int` — see [Refinements](#refinements--guards) |
|
|
77
|
+
| `^pinned` in a pattern | `^pinned` pin in `case`/`in` (same operator, same meaning) |
|
|
78
|
+
| `with {:ok, a} <- step1(), … do` | chained `case`/`in`, or guard clauses with early return |
|
|
79
|
+
| multi-clause function + guards | `case`/`in` with `if` guards in one method |
|
|
80
|
+
|
|
81
|
+
The one structural difference: Elixir dispatches by writing
|
|
82
|
+
*multiple function heads* with patterns and guards, and the
|
|
83
|
+
runtime picks the first that matches. Ruby has no multi-clause
|
|
84
|
+
`def`; you fold the clauses into a single method body with
|
|
85
|
+
`case`/`in`. The narrowing Rigor performs along each `in` clause
|
|
86
|
+
is the analogue of the type Elixir's compiler now infers for
|
|
87
|
+
each function head.
|
|
88
|
+
|
|
89
|
+
And the lineage runs the other way too: Rigor's
|
|
90
|
+
`flow.unreachable-clause` rule — which flags a `case`/`in` clause
|
|
91
|
+
that can never match because earlier clauses already covered its
|
|
92
|
+
type — was modelled directly on Elixir's clause-reachability
|
|
93
|
+
work ([ADR-47](../adr/47-narrowing-driven-clause-reachability.md)).
|
|
94
|
+
It is the feature you may know from Elixir warning you about an
|
|
95
|
+
impossible `case` clause, brought to Ruby.
|
|
96
|
+
|
|
97
|
+
## Set-theoretic types & gradual `dynamic()`
|
|
98
|
+
|
|
99
|
+
Elixir's type system is *set-theoretic*: types are sets of
|
|
100
|
+
values, and you compose them with union, intersection, and
|
|
101
|
+
negation, with a `dynamic()` type marking the gradual boundary.
|
|
102
|
+
Rigor is built on the same union-of-values intuition (the
|
|
103
|
+
[value lattice](../type-specification/value-lattice.md)) and has
|
|
104
|
+
the operator vocabulary to match the common cases:
|
|
105
|
+
|
|
106
|
+
| Elixir | Rigor |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `t \| u` (union) | `T \| U` |
|
|
109
|
+
| intersection `t and u` | `Intersection[T, U]` |
|
|
110
|
+
| negation `not t` | `~T` (complement) |
|
|
111
|
+
| difference (set minus) | `T - U` (type difference) |
|
|
112
|
+
| `dynamic()` | `Dynamic[top]` |
|
|
113
|
+
|
|
114
|
+
The gradual story is nearly identical in spirit: a value typed
|
|
115
|
+
`dynamic()` in Elixir, like a `Dynamic[top]` receiver in Rigor,
|
|
116
|
+
is the point where the checker steps back and stays silent
|
|
117
|
+
rather than guess. Both systems lean on this to stay practical
|
|
118
|
+
on real code, and both treat reaching for it as ordinary.
|
|
119
|
+
|
|
120
|
+
Where Elixir goes further: it lets you *author* intersections
|
|
121
|
+
and negations as ordinary `@type` expressions, and its inference
|
|
122
|
+
reasons about them throughout. Rigor uses `~T` and `T - U`
|
|
123
|
+
internally and in some directives, but does not yet expose a
|
|
124
|
+
full set-theoretic authoring surface in `.rbs`. The everyday
|
|
125
|
+
overlap — unions and the gradual `dynamic()` boundary — is where
|
|
126
|
+
the two feel the same.
|
|
127
|
+
|
|
128
|
+
## Tagged tuples ↔ `case`/`in`
|
|
129
|
+
|
|
130
|
+
Elixir's signature idiom — `{:ok, value}` / `{:error, reason}`
|
|
131
|
+
returned and matched — translates almost verbatim, because Ruby
|
|
132
|
+
expresses the same shape with an array and `case`/`in`:
|
|
133
|
+
|
|
134
|
+
```elixir
|
|
135
|
+
case Integer.parse(s) do
|
|
136
|
+
{n, ""} -> {:ok, n}
|
|
137
|
+
_ -> {:error, :invalid}
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
def parse(s)
|
|
143
|
+
n = Integer(s, exception: false)
|
|
144
|
+
n ? [:ok, n] : [:error, :invalid]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
case parse(s)
|
|
148
|
+
in [:ok, n] then n
|
|
149
|
+
in [:error, why] then handle(why)
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Rigor types the `Tuple` per position and the union of the two
|
|
154
|
+
tagged shapes precisely, and `in [:ok, n]` narrows along that
|
|
155
|
+
clause exactly as the Elixir pattern does. (Ruby idiom does also
|
|
156
|
+
reach for raising on the error path; the tagged-tuple style
|
|
157
|
+
ports cleanly when you want to keep it.)
|
|
158
|
+
|
|
159
|
+
## Atoms ↔ symbols
|
|
160
|
+
|
|
161
|
+
Elixir atoms and Ruby symbols are the same idea — interned,
|
|
162
|
+
identity-compared name constants — and Rigor folds a symbol to
|
|
163
|
+
`Constant<:foo>`, the precise singleton type, not merely
|
|
164
|
+
`Symbol`:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
status = :ok
|
|
168
|
+
assert_type(":ok", status)
|
|
169
|
+
|
|
170
|
+
# a discriminated union over atoms/symbols:
|
|
171
|
+
def describe(s) # s : Constant<:ok> | Constant<:error>
|
|
172
|
+
case s
|
|
173
|
+
in :ok then "fine"
|
|
174
|
+
in :error then "broken"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is the same modelling Elixir's set-theoretic types give an
|
|
180
|
+
atom — a singleton type for the specific atom — and it powers
|
|
181
|
+
the same discriminated-union dispatch you write with atom keys
|
|
182
|
+
in Elixir.
|
|
183
|
+
|
|
184
|
+
## Protocols & behaviours
|
|
185
|
+
|
|
186
|
+
Two Elixir concepts map onto Rigor's structural typing, with one
|
|
187
|
+
twist of difference each.
|
|
188
|
+
|
|
189
|
+
- **Behaviours** (`@callback` in a behaviour module, `@behaviour`
|
|
190
|
+
on the implementer) describe a set of functions a module must
|
|
191
|
+
provide. Rigor's nearest analogue is an RBS `interface` — a
|
|
192
|
+
named set of methods — but with a key difference: an RBS
|
|
193
|
+
interface is satisfied **structurally** (have the methods,
|
|
194
|
+
satisfy it), whereas an Elixir `@behaviour` is declared.
|
|
195
|
+
- **Protocols** (`defprotocol` / `defimpl`) dispatch on the data
|
|
196
|
+
type, and a protocol is satisfied by an *explicit* `defimpl`
|
|
197
|
+
for that type — nominal, like Rust traits. Rigor has no
|
|
198
|
+
per-type `defimpl`; an object satisfies a structural interface
|
|
199
|
+
by responding to the methods, full stop.
|
|
200
|
+
|
|
201
|
+
So both of Elixir's "abstract over implementations" mechanisms
|
|
202
|
+
become Rigor's one structural-interface mechanism, which is
|
|
203
|
+
closer to Go's implicit interfaces than to either Elixir
|
|
204
|
+
construct. The
|
|
205
|
+
[structural-typing appendix](appendix-protocols-and-structural-typing.md)
|
|
206
|
+
is the canonical explainer.
|
|
207
|
+
|
|
208
|
+
## Refinements ↔ guards
|
|
209
|
+
|
|
210
|
+
An Elixir guard like `when x > 0` constrains a value at the
|
|
211
|
+
clause boundary, and the new type system reasons about some such
|
|
212
|
+
guards. Rigor turns the same guard into a named **refinement
|
|
213
|
+
carrier** that rides on the ordinary type:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
def reciprocal(n)
|
|
217
|
+
return nil unless n > 0
|
|
218
|
+
# n is positive-int here when typed as Integer; untyped params stay Dynamic[top]
|
|
219
|
+
1.0 / n
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Rigor refinement | Elixir guard / idiom | Comment |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| `positive-int` | `when n > 0` | Rigor names and carries the result. |
|
|
226
|
+
| `non-empty-string` | `when s != ""` / `byte_size(s) > 0` | Rigor produces it from `unless s.empty?`. |
|
|
227
|
+
| `int<1, 9>` | `when x in 1..9` | Rigor's range carrier handles arbitrary bounds. |
|
|
228
|
+
| `non-empty-array[T]` | `when xs != []` | Rigor produces it from `unless arr.empty?`. |
|
|
229
|
+
| `numeric-string` | `Integer.parse/1` + match | No direct Elixir analogue. |
|
|
230
|
+
|
|
231
|
+
The conceptual match is strong: both systems use a *runtime
|
|
232
|
+
guard* as the place where a more precise type becomes known.
|
|
233
|
+
Rigor's addition is naming that precise type so it flows onward.
|
|
234
|
+
|
|
235
|
+
## Severity, suppression, and "strict mode"
|
|
236
|
+
|
|
237
|
+
| Elixir | Rigor |
|
|
238
|
+
| --- | --- |
|
|
239
|
+
| Dialyzer warning selection | `severity_profile: lenient` / `balanced` / `strict` |
|
|
240
|
+
| `@dialyzer {:nowarn_function, …}` | `# rigor:disable <rule>` |
|
|
241
|
+
| module-level Dialyzer skip | `# rigor:disable-file all` |
|
|
242
|
+
| `mix dialyzer` (advisory) | `rigor check lib` (advisory) |
|
|
243
|
+
|
|
244
|
+
Both are advisory by nature — neither blocks the program from
|
|
245
|
+
running. The difference is when they run: Dialyzer over compiled
|
|
246
|
+
BEAM bytecode, Rigor over Ruby source. The adoption shape —
|
|
247
|
+
tune severity, suppress narrowly, baseline the rest — is the
|
|
248
|
+
same.
|
|
249
|
+
|
|
250
|
+
## What Elixir has and Rigor does not
|
|
251
|
+
|
|
252
|
+
Be honest about what differs:
|
|
253
|
+
|
|
254
|
+
- **Authored intersections and negations.** Elixir lets you
|
|
255
|
+
write intersection and negation types as ordinary `@type`
|
|
256
|
+
expressions and reasons about them throughout. Rigor uses
|
|
257
|
+
`~T` / `T - U` internally but does not expose a full
|
|
258
|
+
set-theoretic authoring surface.
|
|
259
|
+
- **Multi-clause function heads.** Pattern-matching dispatch
|
|
260
|
+
across separate function heads, with the runtime selecting the
|
|
261
|
+
match, has no Ruby `def`-level analogue; you fold it into
|
|
262
|
+
`case`/`in`.
|
|
263
|
+
- **Pattern matching as pervasive assignment.** Elixir's `=` is
|
|
264
|
+
a match operator everywhere; Ruby's pattern matching is scoped
|
|
265
|
+
to `case`/`in` (and `=>` / `in` one-liners).
|
|
266
|
+
- **Process and concurrency types.** The BEAM's process model
|
|
267
|
+
has no Rigor analogue.
|
|
268
|
+
- **Immutability by default.** Elixir data is immutable; Ruby is
|
|
269
|
+
mutable, and Rigor has to reason about mutation effects that
|
|
270
|
+
simply do not arise in Elixir.
|
|
271
|
+
|
|
272
|
+
## What Rigor has and Elixir does not
|
|
273
|
+
|
|
274
|
+
The other direction:
|
|
275
|
+
|
|
276
|
+
- **Shipping today, for Ruby.** Elixir's set-theoretic types are
|
|
277
|
+
rolling into the language incrementally; Rigor's analyzer is
|
|
278
|
+
here now for Ruby, with the no-false-positives stance already
|
|
279
|
+
in force.
|
|
280
|
+
- **Constant folding through method calls.** `"foo".upcase` is
|
|
281
|
+
`Constant<"FOO">`, not `String`. Rigor catalogues which
|
|
282
|
+
built-in methods are pure and folds through them — Dialyzer
|
|
283
|
+
and Elixir's types do not fold call results to singleton
|
|
284
|
+
types this way.
|
|
285
|
+
- **Named refinement carriers.** `non-empty-string`,
|
|
286
|
+
`positive-int`, `int<1, 9>`, `numeric-string` — first-class,
|
|
287
|
+
named, and flowed onward from a guard.
|
|
288
|
+
- **Inferred object shapes and capability roles.** Beyond
|
|
289
|
+
behaviours and protocols, Rigor infers anonymous structural
|
|
290
|
+
shapes from how a value is used.
|
|
291
|
+
- **No annotation tax.** `rigor check` on a Ruby project with
|
|
292
|
+
zero `.rbs` files yields useful diagnostics from inference
|
|
293
|
+
alone; adding `.rbs` is incremental.
|
|
294
|
+
|
|
295
|
+
## A migration vignette
|
|
296
|
+
|
|
297
|
+
You are porting an Elixir module — a couple of pattern-matched
|
|
298
|
+
function heads over a struct, plus a tagged-tuple parser — to
|
|
299
|
+
Ruby. The original:
|
|
300
|
+
|
|
301
|
+
```elixir
|
|
302
|
+
defmodule Shape do
|
|
303
|
+
def area(%{kind: :circle, radius: r}), do: :math.pi() * r * r
|
|
304
|
+
def area(%{kind: :rectangle, w: w, h: h}), do: w * h
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def parse_radius(s) do
|
|
308
|
+
case Float.parse(s) do
|
|
309
|
+
{r, ""} -> {:ok, r}
|
|
310
|
+
_ -> {:error, :invalid}
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The Rigor approach — `case`/`in` with hash patterns folding the
|
|
316
|
+
clauses into one method, and a tagged-tuple parser that ports
|
|
317
|
+
verbatim:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# lib/shape.rb
|
|
321
|
+
def area(shape)
|
|
322
|
+
case shape
|
|
323
|
+
in {kind: :circle, radius:} then Math::PI * radius * radius
|
|
324
|
+
in {kind: :rectangle, w:, h:} then w * h
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def parse_radius(s)
|
|
329
|
+
r = Float(s, exception: false)
|
|
330
|
+
r ? [:ok, r] : [:error, :invalid]
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
What carries over and what changes:
|
|
335
|
+
|
|
336
|
+
- The multiple function heads fold into one `case`/`in`. Ruby's
|
|
337
|
+
hash patterns with key binding (`in {radius:}`) mirror
|
|
338
|
+
Elixir's map patterns (`%{radius: r}`) almost exactly.
|
|
339
|
+
- The atom keys (`:circle`, `:ok`) become symbols, folded to
|
|
340
|
+
`Constant<:circle>` / `Constant<:ok>` — the same singleton
|
|
341
|
+
typing Elixir's atoms get.
|
|
342
|
+
- The `{:ok, r}` / `{:error, _}` tagged tuples become `[:ok, r]`
|
|
343
|
+
/ `[:error, _]` arrays, typed as a precise `Tuple` union and
|
|
344
|
+
matched with `case`/`in`.
|
|
345
|
+
- If you add an `in` clause that can never match — say a second
|
|
346
|
+
`:circle` arm — Rigor's `flow.unreachable-clause` flags it,
|
|
347
|
+
the same warning Elixir would give you about an impossible
|
|
348
|
+
clause.
|
|
349
|
+
|
|
350
|
+
## What's next
|
|
351
|
+
|
|
352
|
+
You probably do not need to read the rest of the handbook
|
|
353
|
+
sequentially. Useful pointers:
|
|
354
|
+
|
|
355
|
+
- [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
|
|
356
|
+
the analogue to guards and pattern-match narrowing.
|
|
357
|
+
- [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
|
|
358
|
+
for the directive grammar — `predicate-if-true` is the
|
|
359
|
+
user-defined guard analogue, the closest thing to teaching
|
|
360
|
+
Rigor a custom `is_*` guard.
|
|
361
|
+
- [Protocols and structural typing](appendix-protocols-and-structural-typing.md)
|
|
362
|
+
for how behaviours and protocols both map onto Rigor's single
|
|
363
|
+
structural-interface mechanism.
|
|
364
|
+
|
|
365
|
+
If you want to compare against another tool, the sibling
|
|
366
|
+
appendix pages cover [TypeScript](appendix-typescript.md),
|
|
367
|
+
[PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
|
|
368
|
+
[Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
|
|
369
|
+
[Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
|
|
370
|
+
and [Go](appendix-go.md).
|