rigortype 0.1.4 → 0.1.6

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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. metadata +92 -1
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hkt_body"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-20 Slice 2a — reducer that walks a `Definition`'s
8
+ # `body_tree` against a concrete `Type::App` and returns a
9
+ # fully-typed `Rigor::Type`.
10
+ #
11
+ # Reduction is the operational interpretation of ADR-20
12
+ # § D4 ("Evaluation rules"):
13
+ #
14
+ # 1. **Resolve `F`.** Look up the registered body via
15
+ # `registry.definition(uri)`.
16
+ # 2. **Substitute arguments.** Walk the body tree, replacing
17
+ # `{HktBody::Param}` nodes with the matching positional
18
+ # arg from the application.
19
+ # 3. **Build types.** `{HktBody::TypeLeaf}` returns its
20
+ # wrapped type as-is; `{HktBody::Union}` and
21
+ # `{HktBody::NominalApp}` route their reduced children
22
+ # through `Type::Combinator.union` / `.nominal_of` so
23
+ # normalization applies.
24
+ # 4. **Recurse on `{HktBody::AppRef}` nodes.** Reduce the
25
+ # args first; if the resulting `(uri, args)` matches an
26
+ # App already on the current reduction stack, return the
27
+ # in-progress `Type::App` carrier as-is (lazy
28
+ # self-reference handling — the standard "tying the
29
+ # knot" trick for recursive type aliases like
30
+ # `json::value`). Otherwise build a fresh
31
+ # `Type::App` and recursively reduce it against the
32
+ # same registry, sharing the fuel budget.
33
+ # 5. **Fuel budget.** Each visited node consumes one unit.
34
+ # On exhaustion, reduction unwinds to `app.bound`.
35
+ #
36
+ # The reducer is **pure** with respect to its inputs (the
37
+ # registry + the App) but uses a per-call mutable state
38
+ # bag for fuel + cycle tracking. Concurrent reductions
39
+ # MUST allocate fresh reducers (or fresh `_reduce` calls)
40
+ # — the per-call state is not shared.
41
+ class HktReducer
42
+ DEFAULT_FUEL = 64
43
+
44
+ class FuelExhausted < StandardError; end
45
+
46
+ def initialize(registry)
47
+ raise ArgumentError, "registry must be an HktRegistry" unless registry.is_a?(HktRegistry)
48
+
49
+ @registry = registry
50
+ end
51
+
52
+ # Reduce `app` against the registry.
53
+ #
54
+ # @param app [Rigor::Type::App]
55
+ # @param fuel [Integer] reduction-step budget (default 64
56
+ # per ADR-20 WD3). Each visited body node costs one
57
+ # unit. On exhaustion the reduction returns `app.bound`.
58
+ # @return [Rigor::Type] the reduced type, or `app.bound`
59
+ # when reduction is impossible (URI not defined, arity
60
+ # mismatch, body_tree absent, fuel exhausted).
61
+ def reduce(app, fuel: DEFAULT_FUEL)
62
+ raise ArgumentError, "expected a Rigor::Type::App, got #{app.class}" unless app.is_a?(Type::App)
63
+
64
+ definition = @registry.definition(app.uri)
65
+ return app.bound if definition.nil? || definition.body_tree.nil?
66
+ return app.bound if definition.params.size != app.args.size
67
+
68
+ state = State.new(fuel: fuel)
69
+ begin
70
+ state.with_in_progress(app.uri, app.args, app) do
71
+ walk(definition.body_tree, bindings: bindings_for(definition, app.args), state: state) || app.bound
72
+ end
73
+ rescue FuelExhausted
74
+ app.bound
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def bindings_for(definition, args)
81
+ definition.params.zip(args).to_h
82
+ end
83
+
84
+ def walk(node, bindings:, state:)
85
+ state.consume_fuel!
86
+
87
+ case node
88
+ when HktBody::TypeLeaf
89
+ node.type
90
+ when HktBody::Param
91
+ bindings.fetch(node.name) do
92
+ raise ArgumentError, "unknown param #{node.name.inspect}; declared: #{bindings.keys}"
93
+ end
94
+ when HktBody::Union
95
+ reduced = node.arms.map { |arm| walk(arm, bindings: bindings, state: state) }
96
+ Type::Combinator.union(*reduced)
97
+ when HktBody::NominalApp
98
+ reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
99
+ Type::Combinator.nominal_of(node.class_name, type_args: reduced_args)
100
+ when HktBody::AppRef
101
+ reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
102
+ reduce_app_ref(node.uri, reduced_args, state: state)
103
+ when HktBody::Conditional
104
+ walk_conditional(node, bindings: bindings, state: state)
105
+ else
106
+ raise ArgumentError, "unknown body node: #{node.class}"
107
+ end
108
+ end
109
+
110
+ # ADR-20 § D3 conditional reduction. Resolves the test
111
+ # against the current bindings and picks a branch:
112
+ #
113
+ # - test = `yes` → return the reduced `then_branch`.
114
+ # - test = `no` → return the reduced `else_branch`.
115
+ # - test = `maybe` → widen to the union of both
116
+ # reduced branches (per ADR-20 WD7).
117
+ def walk_conditional(node, bindings:, state:)
118
+ verdict = evaluate_test(node.test, bindings: bindings, state: state)
119
+ case verdict
120
+ when :yes
121
+ walk(node.then_branch, bindings: bindings, state: state)
122
+ when :no
123
+ walk(node.else_branch, bindings: bindings, state: state)
124
+ else
125
+ # `:maybe` — widen to the union of both branches.
126
+ then_t = walk(node.then_branch, bindings: bindings, state: state)
127
+ else_t = walk(node.else_branch, bindings: bindings, state: state)
128
+ Type::Combinator.union(then_t, else_t)
129
+ end
130
+ end
131
+
132
+ def evaluate_test(test, bindings:, state:)
133
+ case test
134
+ when HktBody::TestSubtype
135
+ left = walk(test.left, bindings: bindings, state: state)
136
+ right = walk(test.right, bindings: bindings, state: state)
137
+ subtype_verdict(left, right)
138
+ when HktBody::TestEquality
139
+ left = walk(test.left, bindings: bindings, state: state)
140
+ right = walk(test.right, bindings: bindings, state: state)
141
+ equality_verdict(left, right)
142
+ when HktBody::TestMembership
143
+ left = walk(test.left, bindings: bindings, state: state)
144
+ options = test.options.map { |o| walk(o, bindings: bindings, state: state) }
145
+ membership_verdict(left, options)
146
+ else
147
+ raise ArgumentError, "unknown test node: #{test.class}"
148
+ end
149
+ end
150
+
151
+ # `left <: right`. Slice 7a verdict policy: structural
152
+ # equality is `:yes`; clearly-disjoint nominal /
153
+ # constant pairs are `:no`; everything else is `:maybe`
154
+ # (widens to the union per ADR-20 WD7 — robustness
155
+ # principle keeps us conservative on undecided tests).
156
+ def subtype_verdict(left, right)
157
+ return :yes if left == right
158
+
159
+ # Both Nominals with different class names AND
160
+ # neither carries Dynamic — `:no` (statically
161
+ # disjoint). For any other shape pair, `:maybe`.
162
+ return :no if disjoint_nominals?(left, right)
163
+ return :no if disjoint_constants?(left, right)
164
+
165
+ :maybe
166
+ end
167
+
168
+ # Structural equality verdict — same shape as
169
+ # subtype_verdict but symmetric.
170
+ def equality_verdict(left, right)
171
+ return :yes if left == right
172
+ return :no if disjoint_nominals?(left, right)
173
+ return :no if disjoint_constants?(left, right)
174
+
175
+ :maybe
176
+ end
177
+
178
+ def membership_verdict(left, options)
179
+ verdicts = options.map { |o| equality_verdict(left, o) }
180
+ return :yes if verdicts.include?(:yes)
181
+ return :no if verdicts.all? { |v| v == :no }
182
+
183
+ :maybe
184
+ end
185
+
186
+ def disjoint_nominals?(left, right)
187
+ return false unless left.is_a?(Type::Nominal) && right.is_a?(Type::Nominal)
188
+
189
+ left.class_name != right.class_name
190
+ end
191
+
192
+ def disjoint_constants?(left, right)
193
+ return false unless left.is_a?(Type::Constant) && right.is_a?(Type::Constant)
194
+
195
+ left.value != right.value
196
+ end
197
+
198
+ def reduce_app_ref(uri, reduced_args, state:)
199
+ # Cycle detection — when the same `(uri, args)` is
200
+ # already on the reduction stack, return the
201
+ # in-progress App carrier as-is so recursive type
202
+ # aliases (`Array[App[json::value, K]]` inside the
203
+ # `json::value` body) terminate.
204
+ existing = state.in_progress_for(uri, reduced_args)
205
+ return existing if existing
206
+
207
+ registration = @registry.registration(uri)
208
+ bound = registration&.bound || Type::Combinator.untyped
209
+ new_app = Type::App.new(uri, reduced_args, bound: bound)
210
+
211
+ definition = @registry.definition(uri)
212
+ return new_app if definition.nil? || definition.body_tree.nil?
213
+ return new_app if definition.params.size != reduced_args.size
214
+
215
+ state.with_in_progress(uri, reduced_args, new_app) do
216
+ walk(definition.body_tree, bindings: bindings_for(definition, reduced_args), state: state) || new_app
217
+ end
218
+ end
219
+
220
+ # Per-call mutable bag carrying the remaining fuel
221
+ # budget and the active reduction stack (for cycle
222
+ # detection). Not shared across `reduce` calls.
223
+ class State
224
+ def initialize(fuel:)
225
+ @fuel = fuel
226
+ @in_progress = {}
227
+ end
228
+
229
+ def consume_fuel!
230
+ raise FuelExhausted if @fuel <= 0
231
+
232
+ @fuel -= 1
233
+ end
234
+
235
+ def in_progress_for(uri, args)
236
+ @in_progress[[uri, args]]
237
+ end
238
+
239
+ def with_in_progress(uri, args, app)
240
+ key = [uri, args]
241
+ previous = @in_progress[key]
242
+ @in_progress[key] = app
243
+ begin
244
+ yield
245
+ ensure
246
+ if previous.nil?
247
+ @in_progress.delete(key)
248
+ else
249
+ @in_progress[key] = previous
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hkt_body"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-20 § "Decision D1 / D2" — registry of Lightweight HKT
8
+ # tag registrations + type-function bodies parsed off the
9
+ # `%a{rigor:v1:hkt_register: ...}` /
10
+ # `%a{rigor:v1:hkt_define: ...}` annotations in shipped
11
+ # `.rbs` files.
12
+ #
13
+ # Slice 1 keeps the registry **opaque**: it stores the
14
+ # registration metadata (arity, variance, bound) and the
15
+ # un-evaluated definition body (a raw String — Slice 2
16
+ # introduces the conditional / indexed-access evaluator that
17
+ # parses the body and reduces `Type::App` instances against
18
+ # it). The carrier never needs to read from the registry
19
+ # because Slice 1's `Type::App` carries its `bound` directly;
20
+ # the registry exists at this slice solely so the parser
21
+ # round-trip and downstream slices have a stable target API.
22
+ #
23
+ # The registry is immutable after construction. Callers that
24
+ # need to extend it (e.g. plugin registrations layered on top
25
+ # of stdlib registrations) MUST build a new registry via
26
+ # `merge` rather than mutating an existing one. This keeps the
27
+ # registry shareable across Ractor boundaries per ADR-15.
28
+ class HktRegistry
29
+ # Frozen value object recording one tag registration.
30
+ #
31
+ # - `uri`: namespaced Symbol per ADR-20 WD1 (must include
32
+ # `"::"`).
33
+ # - `arity`: positive Integer — the number of formal
34
+ # parameters the registered constructor takes.
35
+ # - `variance`: ordered Array of Symbols, one per
36
+ # parameter, each `:out` (covariant), `:in`
37
+ # (contravariant), or `:inv` (invariant; default).
38
+ # - `bound`: a `Rigor::Type` to erase to when an `App`
39
+ # referring to this URI cannot be reduced. Defaults to
40
+ # `Dynamic[Top]` (the parser fills in the default when
41
+ # the annotation omits `bound:`).
42
+ Registration = Data.define(:uri, :arity, :variance, :bound) do
43
+ def initialize(uri:, arity:, variance:, bound:)
44
+ raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
45
+ raise ArgumentError, "uri must be namespaced as `:a::b`, got #{uri.inspect}" unless uri.to_s.include?("::")
46
+ unless arity.is_a?(Integer) && arity.positive?
47
+ raise ArgumentError,
48
+ "arity must be a positive Integer, got #{arity.inspect}"
49
+ end
50
+ raise ArgumentError, "variance must be an Array, got #{variance.class}" unless variance.is_a?(Array)
51
+ raise ArgumentError, "variance must have #{arity} entries, got #{variance.size}" unless variance.size == arity
52
+
53
+ variance.each do |v|
54
+ unless %i[out in inv].include?(v)
55
+ raise ArgumentError, "variance entries must be :out, :in, or :inv, got #{v.inspect}"
56
+ end
57
+ end
58
+ raise ArgumentError, "bound must not be nil" if bound.nil?
59
+
60
+ super(uri: uri, arity: arity, variance: variance.dup.freeze, bound: bound)
61
+ end
62
+ end
63
+
64
+ # Frozen value object recording one type-function
65
+ # definition.
66
+ #
67
+ # `body` is the raw String payload from the `%a{...}`
68
+ # annotation (Slice 1's parser populates it). It stays
69
+ # opaque until Slice 2b's body-string parser lands.
70
+ #
71
+ # `body_tree` is the optional evaluable form: a
72
+ # `Rigor::Inference::HktBody::*` node tree the Slice 2a
73
+ # reducer walks against the application's concrete
74
+ # arguments. Plugin and Rigor-bundled overlay authors
75
+ # construct it programmatically through
76
+ # {with_body_tree}; the Slice 2b string parser will set
77
+ # it from `body` once it ships. The reducer treats a
78
+ # `nil` `body_tree` as "definition not yet evaluable"
79
+ # and returns the registered bound.
80
+ Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
81
+ def initialize(uri:, params:, body:, body_tree: nil, source_path: nil, source_line: nil)
82
+ raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
83
+ raise ArgumentError, "params must be an Array, got #{params.class}" unless params.is_a?(Array)
84
+
85
+ params.each do |p|
86
+ raise ArgumentError, "params entries must be Symbols, got #{p.inspect}" unless p.is_a?(Symbol)
87
+ end
88
+ raise ArgumentError, "body must be a String, got #{body.class}" unless body.is_a?(String)
89
+
90
+ super(
91
+ uri: uri,
92
+ params: params.dup.freeze,
93
+ body: body,
94
+ body_tree: body_tree,
95
+ source_path: source_path,
96
+ source_line: source_line
97
+ )
98
+ end
99
+ end
100
+
101
+ # Convenience constructor for callers that have a body
102
+ # tree but no raw String — typically Rigor-bundled HKT
103
+ # overlays that build the body programmatically. The
104
+ # raw `body` slot is filled with an empty placeholder
105
+ # so existing consumers keep their type contract.
106
+ def self.definition_with_body_tree(uri:, params:, body_tree:, source_path: nil, source_line: nil)
107
+ Definition.new(
108
+ uri: uri,
109
+ params: params,
110
+ body: "",
111
+ body_tree: body_tree,
112
+ source_path: source_path,
113
+ source_line: source_line
114
+ )
115
+ end
116
+
117
+ attr_reader :registrations, :definitions
118
+
119
+ # @param registrations [Array<Registration>]
120
+ # @param definitions [Array<Definition>]
121
+ def initialize(registrations: [], definitions: [])
122
+ @registrations = registrations.to_h { |r| [r.uri, r] }.freeze
123
+ @definitions = definitions.to_h { |d| [d.uri, d] }.freeze
124
+ freeze
125
+ end
126
+
127
+ def registered?(uri)
128
+ @registrations.key?(uri)
129
+ end
130
+
131
+ def defined?(uri)
132
+ @definitions.key?(uri)
133
+ end
134
+
135
+ def registration(uri)
136
+ @registrations[uri]
137
+ end
138
+
139
+ def definition(uri)
140
+ @definitions[uri]
141
+ end
142
+
143
+ # @return [HktRegistry] a new registry whose entries are
144
+ # the union of this registry's and `other`'s. On URI
145
+ # collisions `other`'s entries win (last-write-wins; OQ3
146
+ # tentative).
147
+ def merge(other)
148
+ raise ArgumentError, "merge target must be an HktRegistry, got #{other.class}" unless other.is_a?(HktRegistry)
149
+
150
+ self.class.new(
151
+ registrations: @registrations.merge(other.registrations).values,
152
+ definitions: @definitions.merge(other.definitions).values
153
+ )
154
+ end
155
+
156
+ def empty?
157
+ @registrations.empty? && @definitions.empty?
158
+ end
159
+
160
+ # ADR-20 Slice 2a — reduce an `App` against this
161
+ # registry. Convenience wrapper around `HktReducer.new(self).reduce`.
162
+ # Each call allocates a fresh reducer; concurrent
163
+ # reductions are safe.
164
+ def reduce(app, fuel: HktReducer::DEFAULT_FUEL)
165
+ HktReducer.new(self).reduce(app, fuel: fuel)
166
+ end
167
+
168
+ # ADR-20 slice 2e — scan a Rigor RbsLoader for
169
+ # `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
170
+ # annotations attached to class- or module-level
171
+ # declarations in the loaded RBS env, parse them via
172
+ # {Rigor::RbsExtended::HktDirectives}, and return a new
173
+ # registry that is the union of `base` and every parsed
174
+ # entry. Last-write-wins on URI collisions per
175
+ # {#merge}'s contract. Fail-soft on per-annotation parse
176
+ # errors (the reporter records an `:info` entry; the
177
+ # other annotations still apply).
178
+ #
179
+ # @param rbs_loader [Rigor::Environment::RbsLoader]
180
+ # @param base [HktRegistry] starting registry (typically
181
+ # the bundled `Rigor::Builtins::HktBuiltins.registry`).
182
+ # @param name_scope [Rigor::Environment::NameScope, nil]
183
+ # threaded through to the bound resolver for class-name
184
+ # lookups; safe to omit during scanning since hkt
185
+ # bounds are typically `untyped` or stdlib classes.
186
+ # @param reporter [#record, nil] same fail-soft reporter
187
+ # contract the other RBS-extended parsers use.
188
+ def self.scan_rbs_loader(rbs_loader, base: EMPTY, name_scope: nil, reporter: nil)
189
+ return base if rbs_loader.nil?
190
+
191
+ # Required lazily here to avoid a hard circular
192
+ # require between hkt_registry / hkt_directives;
193
+ # HktDirectives requires HktRegistry to construct its
194
+ # value objects.
195
+ require_relative "../rbs_extended/hkt_directives"
196
+
197
+ registrations = []
198
+ definitions = []
199
+
200
+ rbs_loader.each_class_decl_annotation do |annotation_string, source_location|
201
+ reg = Rigor::RbsExtended::HktDirectives.parse_register(
202
+ annotation_string, name_scope: name_scope, reporter: reporter, source_location: source_location
203
+ )
204
+ registrations << reg if reg
205
+
206
+ defn = Rigor::RbsExtended::HktDirectives.parse_define(
207
+ annotation_string, reporter: reporter, source_location: source_location
208
+ )
209
+ definitions << defn if defn
210
+ end
211
+
212
+ return base if registrations.empty? && definitions.empty?
213
+
214
+ overlay = new(registrations: registrations, definitions: definitions)
215
+ base.merge(overlay)
216
+ end
217
+
218
+ EMPTY = new.freeze
219
+ end
220
+ end
221
+ end
222
+
223
+ require_relative "hkt_reducer"
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-16 Tier A — engine hook. Consults every registered
8
+ # plugin manifest's `block_as_methods` entries to decide
9
+ # whether a block call site qualifies for `Scope#self_type`
10
+ # narrowing.
11
+ #
12
+ # The match contract for a class-level DSL like Sinatra's
13
+ # `class MyApp < Sinatra::Base; get '/foo' do ... end; end`:
14
+ #
15
+ # - the call's lexical receiver type is `Singleton[X]`
16
+ # (the implicit-self in a class body, or an explicit
17
+ # `MyApp.get(...)` call);
18
+ # - the underlying class `X` equals or inherits from the
19
+ # entry's `receiver_constraint`;
20
+ # - the call's method name is in the entry's `verbs`.
21
+ #
22
+ # On a match the helper returns the **instance** type of
23
+ # the receiver class (`Nominal[X]`) — the narrowed
24
+ # `self_type` for the block body, matching Sinatra's
25
+ # runtime semantics where `Sinatra::Base#generate_method`
26
+ # turns the block into an instance method of the user's
27
+ # app class.
28
+ #
29
+ # Slice 1b ships the floor only (per ADR-16 § WD13):
30
+ # bare-identifier method lookups inside the block resolve
31
+ # through the inference engine's normal `self_type`-driven
32
+ # path, so methods declared on `Sinatra::Base` (RBS or
33
+ # otherwise) become visible. Precision additions —
34
+ # parameter-typed block params, declared per-verb argument
35
+ # contracts — are ceiling concerns for later slices.
36
+ module MacroBlockSelfType
37
+ module_function
38
+
39
+ # @param scope [Rigor::Scope]
40
+ # @param call_node [Prism::CallNode]
41
+ # @param receiver_type [Rigor::Type, nil]
42
+ # @return [Rigor::Type, nil] the narrowed self-type, or
43
+ # `nil` when no registered entry matches the call shape.
44
+ def narrow_self_type_for(scope:, call_node:, receiver_type:)
45
+ return nil if receiver_type.nil?
46
+
47
+ environment = scope&.environment
48
+ registry = environment&.plugin_registry
49
+ return nil if registry.nil? || registry.empty?
50
+
51
+ receiver_class_name = singleton_receiver_class_name(receiver_type)
52
+ return nil if receiver_class_name.nil?
53
+
54
+ verb = call_node.name
55
+ registry.plugins.each do |plugin|
56
+ plugin.manifest.block_as_methods.each do |entry| # rigor:disable undefined-method
57
+ return instance_type_for(receiver_class_name, environment) if matches?(entry, verb, receiver_class_name,
58
+ environment)
59
+ end
60
+ end
61
+ nil
62
+ end
63
+
64
+ # Tier A's match contract is intentionally narrow:
65
+ # class-level DSL calls (receiver is `Singleton[X]`) only.
66
+ # Instance-receiver calls and DSL forms whose block body
67
+ # binds a different `self` (Concern's `included do`,
68
+ # `instance_eval { ... }`) are handled by later slices
69
+ # (Concern walker, Tier D, etc.) — not Tier A.
70
+ def singleton_receiver_class_name(receiver_type)
71
+ return nil unless receiver_type.is_a?(Type::Singleton)
72
+
73
+ receiver_type.class_name
74
+ end
75
+
76
+ def matches?(entry, verb, receiver_class_name, environment)
77
+ return false unless entry.verbs.include?(verb)
78
+
79
+ receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
80
+ end
81
+
82
+ def receiver_class_inherits_from?(class_name, constraint, environment)
83
+ return true if class_name == constraint
84
+
85
+ ordering = environment.class_ordering(class_name, constraint)
86
+ %i[equal subclass].include?(ordering)
87
+ rescue StandardError
88
+ false
89
+ end
90
+
91
+ def instance_type_for(class_name, environment)
92
+ environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1028,10 +1028,10 @@ module Rigor
1028
1028
  # class's ancestor chain at lookup time; the catalog
1029
1029
  # corresponds to the module-mode YAML at
1030
1030
  # `data/builtins/ruby_core/<topic>.yml`.
1031
- MODULE_CATALOGS = [
1032
- [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1033
- [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1034
- ].freeze
1031
+ MODULE_CATALOGS = Ractor.make_shareable([
1032
+ [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1033
+ [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1034
+ ])
1035
1035
  private_constant :MODULE_CATALOGS
1036
1036
 
1037
1037
  # Returns the `(catalog, class_name)` pairs for every
@@ -1057,31 +1057,31 @@ module Rigor
1057
1057
  # Otherwise a `DateTime` receiver would match the `Date`
1058
1058
  # arm first and the catalog would consult the Date entry
1059
1059
  # in `DATE_CATALOG` for the wrong class.
1060
- CATALOG_BY_CLASS = [
1061
- [Integer, [Builtins::NumericCatalog, "Integer"]],
1062
- [Float, [Builtins::NumericCatalog, "Float"]],
1063
- [String, [Builtins::STRING_CATALOG, "String"]],
1064
- [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1065
- [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1066
- [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1067
- [Range, [Builtins::RANGE_CATALOG, "Range"]],
1068
- [::Set, [Builtins::SET_CATALOG, "Set"]],
1069
- [Time, [Builtins::TIME_CATALOG, "Time"]],
1070
- [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1071
- [Date, [Builtins::DATE_CATALOG, "Date"]],
1072
- [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1073
- [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1074
- [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1075
- [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1076
- [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1077
- [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1078
- [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1079
- [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1080
- [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1081
- [Method, [Builtins::PROC_CATALOG, "Method"]],
1082
- [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1083
- [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1084
- ].freeze
1060
+ CATALOG_BY_CLASS = Ractor.make_shareable([
1061
+ [Integer, [Builtins::NumericCatalog, "Integer"]],
1062
+ [Float, [Builtins::NumericCatalog, "Float"]],
1063
+ [String, [Builtins::STRING_CATALOG, "String"]],
1064
+ [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1065
+ [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1066
+ [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1067
+ [Range, [Builtins::RANGE_CATALOG, "Range"]],
1068
+ [::Set, [Builtins::SET_CATALOG, "Set"]],
1069
+ [Time, [Builtins::TIME_CATALOG, "Time"]],
1070
+ [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1071
+ [Date, [Builtins::DATE_CATALOG, "Date"]],
1072
+ [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1073
+ [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1074
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1075
+ [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1076
+ [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1077
+ [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1078
+ [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1079
+ [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1080
+ [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1081
+ [Method, [Builtins::PROC_CATALOG, "Method"]],
1082
+ [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1083
+ [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1084
+ ])
1085
1085
  private_constant :CATALOG_BY_CLASS
1086
1086
 
1087
1087
  # Returns `[catalog, class_name]` for receivers we have a
@@ -36,10 +36,10 @@ module Rigor
36
36
  # the result into a `Constant<Rational>` / `Constant<Complex>`.
37
37
  # The factory accepts the same shapes as Ruby:
38
38
  # `Rational(a)`, `Rational(a, b)`, `Complex(a)`, `Complex(a, b)`.
39
- NUMERIC_CONSTRUCTORS = {
40
- Rational: ->(*args) { Rational(*args) },
41
- Complex: ->(*args) { Complex(*args) }
42
- }.freeze
39
+ NUMERIC_CONSTRUCTORS = Ractor.make_shareable({
40
+ Rational: Ractor.make_shareable(->(*args) { Rational(*args) }),
41
+ Complex: Ractor.make_shareable(->(*args) { Complex(*args) })
42
+ })
43
43
  private_constant :NUMERIC_CONSTRUCTORS
44
44
 
45
45
  # `Kernel#Integer(s)` predicate-aware refinement set