rigortype 0.1.5 → 0.1.7
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 +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- metadata +56 -1
data/lib/rigor/environment.rb
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
require_relative "environment/class_registry"
|
|
4
4
|
require_relative "environment/rbs_loader"
|
|
5
5
|
require_relative "environment/reflection"
|
|
6
|
+
require_relative "environment/reporters"
|
|
7
|
+
require_relative "environment/hkt_registry_holder"
|
|
6
8
|
require_relative "environment/bundle_sig_discovery"
|
|
7
9
|
require_relative "environment/lockfile_resolver"
|
|
8
10
|
require_relative "environment/rbs_collection_discovery"
|
|
9
11
|
require_relative "environment/rbs_coverage_report"
|
|
10
12
|
require_relative "inference/synthetic_method_index"
|
|
13
|
+
require_relative "inference/project_patched_methods"
|
|
14
|
+
require_relative "inference/hkt_registry"
|
|
15
|
+
require_relative "builtins/hkt_builtins"
|
|
11
16
|
require_relative "type_node/name_scope"
|
|
12
17
|
require_relative "type_node/resolver_chain"
|
|
13
18
|
|
|
@@ -57,8 +62,8 @@ module Rigor
|
|
|
57
62
|
].freeze
|
|
58
63
|
|
|
59
64
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
60
|
-
:
|
|
61
|
-
:synthetic_method_index
|
|
65
|
+
:reporters, :name_scope,
|
|
66
|
+
:synthetic_method_index, :project_patched_methods
|
|
62
67
|
|
|
63
68
|
# @param class_registry [Rigor::Environment::ClassRegistry]
|
|
64
69
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
|
|
@@ -79,24 +84,105 @@ module Rigor
|
|
|
79
84
|
# sources the dispatcher consults BELOW RBS dispatch.
|
|
80
85
|
# When nil (the default), no dep-source contribution
|
|
81
86
|
# participates and the dispatcher tier is a no-op.
|
|
82
|
-
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
|
|
87
|
+
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil, # rubocop:disable Metrics/ParameterLists
|
|
83
88
|
plugin_registry: nil, dependency_source_index: nil,
|
|
84
89
|
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
85
|
-
synthetic_method_index: nil
|
|
90
|
+
synthetic_method_index: nil, project_patched_methods: nil,
|
|
91
|
+
hkt_registry: nil)
|
|
86
92
|
@class_registry = class_registry
|
|
87
93
|
@rbs_loader = rbs_loader
|
|
88
94
|
@plugin_registry = plugin_registry
|
|
89
95
|
@dependency_source_index = dependency_source_index
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
# ADR-pending — reporters live in a mutable container so
|
|
97
|
+
# long-lived integrations (LSP `ProjectContext`) can swap
|
|
98
|
+
# them per `Runner.run` without rebuilding the env. The
|
|
99
|
+
# existing `#rbs_extended_reporter` / `#boundary_cross_reporter`
|
|
100
|
+
# accessors below preserve the public lookup shape.
|
|
101
|
+
@reporters = Reporters.new(
|
|
102
|
+
rbs_extended: rbs_extended_reporter,
|
|
103
|
+
boundary_cross: boundary_cross_reporter
|
|
104
|
+
)
|
|
92
105
|
@synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
|
|
106
|
+
@project_patched_methods = project_patched_methods || Inference::ProjectPatchedMethods::EMPTY
|
|
107
|
+
# ADR-20 slice 2c + 2e — the per-env HKT registry
|
|
108
|
+
# consulted by the reducer when resolving `Type::App`
|
|
109
|
+
# carriers. Defaults to {Inference::HktRegistry::EMPTY};
|
|
110
|
+
# the {.default} / {.for_project} class methods seed it
|
|
111
|
+
# with the bundled builtins (`json::value`, …) plus any
|
|
112
|
+
# `%a{rigor:v1:hkt_register / hkt_define}` annotations
|
|
113
|
+
# the RBS loader exposes. The hkt_registry getter
|
|
114
|
+
# (defined below) MEMOIZES the result of merging the
|
|
115
|
+
# base with the RBS scan so the scan is paid at most
|
|
116
|
+
# once per Environment lifetime — and only when first
|
|
117
|
+
# consulted, leaving fast paths like `rigor check
|
|
118
|
+
# --cache-stats --no-stats` from doing the RBS env
|
|
119
|
+
# build at all.
|
|
120
|
+
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
121
|
+
@hkt_registry_holder = HktRegistryHolder.new
|
|
93
122
|
@name_scope = build_name_scope
|
|
94
123
|
freeze
|
|
95
124
|
end
|
|
96
125
|
|
|
126
|
+
# ADR-20 slices 2e + 6 — lazy HKT registry getter.
|
|
127
|
+
# Merge order on first call: builtins (base) ← plugin
|
|
128
|
+
# manifest aggregation ← RBS env scan. Last-write-wins on
|
|
129
|
+
# URI collisions so user-authored `.rbs` overlays beat
|
|
130
|
+
# plugin entries, which beat the bundled JSON_VALUE.
|
|
131
|
+
# Memoised; single-threaded use only (under the Ractor
|
|
132
|
+
# pool path each worker has its own Environment so
|
|
133
|
+
# cross-worker mutation is impossible; the LSP
|
|
134
|
+
# single-publish-at-a-time invariant serialises here).
|
|
135
|
+
def hkt_registry
|
|
136
|
+
@hkt_registry_holder.fetch do
|
|
137
|
+
with_plugin_overlay = if @plugin_registry.respond_to?(:hkt_overlay_registry)
|
|
138
|
+
@hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
|
|
139
|
+
else
|
|
140
|
+
@hkt_registry_base
|
|
141
|
+
end
|
|
142
|
+
Inference::HktRegistry.scan_rbs_loader(
|
|
143
|
+
@rbs_loader,
|
|
144
|
+
base: with_plugin_overlay,
|
|
145
|
+
reporter: rbs_extended_reporter
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Backwards-compatible reporter accessors — every existing
|
|
151
|
+
# consumer (rbs_extended, method_dispatcher) calls these. The
|
|
152
|
+
# frozen `@reporters` container is mutable for slot reassignment
|
|
153
|
+
# via {#attach_reporters!} below.
|
|
154
|
+
def rbs_extended_reporter
|
|
155
|
+
@reporters.rbs_extended
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def boundary_cross_reporter
|
|
159
|
+
@reporters.boundary_cross
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Replaces the env's per-run reporter slots. Intended for
|
|
163
|
+
# long-lived integrations (LSP `ProjectContext`) that share one
|
|
164
|
+
# Environment instance across many `Runner.run` calls: each call
|
|
165
|
+
# attaches its own fresh reporter pair so per-call diagnostic
|
|
166
|
+
# events stay scoped to that call rather than accumulating
|
|
167
|
+
# across publishes.
|
|
168
|
+
#
|
|
169
|
+
# Single-threaded use only. Concurrent publishes against one
|
|
170
|
+
# Environment must serialise — the LSP `Server` debouncer +
|
|
171
|
+
# synchronized writer already enforces this for the editor
|
|
172
|
+
# path. The Ractor pool path builds a per-worker Environment
|
|
173
|
+
# and does not reach this surface.
|
|
174
|
+
def attach_reporters!(rbs_extended_reporter:, boundary_cross_reporter:)
|
|
175
|
+
@reporters.rbs_extended = rbs_extended_reporter
|
|
176
|
+
@reporters.boundary_cross = boundary_cross_reporter
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
97
180
|
class << self
|
|
98
181
|
def default
|
|
99
|
-
@default ||= new(
|
|
182
|
+
@default ||= new(
|
|
183
|
+
rbs_loader: RbsLoader.default,
|
|
184
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
185
|
+
).freeze
|
|
100
186
|
end
|
|
101
187
|
|
|
102
188
|
# Builds an Environment that consults the project's local
|
|
@@ -127,7 +213,7 @@ module Rigor
|
|
|
127
213
|
bundler_bundle_path: nil, bundler_auto_detect: false,
|
|
128
214
|
bundler_lockfile: nil,
|
|
129
215
|
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
130
|
-
synthetic_method_index: nil)
|
|
216
|
+
synthetic_method_index: nil, project_patched_methods: nil)
|
|
131
217
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
132
218
|
# O4 MVP — append per-gem `sig/` directories discovered
|
|
133
219
|
# under the target project's bundler install root. Empty
|
|
@@ -173,13 +259,24 @@ module Rigor
|
|
|
173
259
|
signature_paths: loader_signature_paths,
|
|
174
260
|
cache_store: cache_store
|
|
175
261
|
)
|
|
262
|
+
# ADR-20 slice 2c + 2e — seed hkt_registry with the
|
|
263
|
+
# bundled builtins. The Environment's `#hkt_registry`
|
|
264
|
+
# getter then LAZILY merges in the RBS env scan on
|
|
265
|
+
# first call so fast paths that don't consult HKT
|
|
266
|
+
# (e.g. `rigor check --cache-stats --no-stats`) don't
|
|
267
|
+
# pay the eager env-build cost up front. URI
|
|
268
|
+
# collisions let the user-authored overlay win over
|
|
269
|
+
# the bundled builtin (last-write-wins per ADR-20
|
|
270
|
+
# OQ3 tentative).
|
|
176
271
|
new(
|
|
177
272
|
rbs_loader: loader,
|
|
178
273
|
plugin_registry: plugin_registry,
|
|
179
274
|
dependency_source_index: dependency_source_index,
|
|
180
275
|
rbs_extended_reporter: rbs_extended_reporter,
|
|
181
276
|
boundary_cross_reporter: boundary_cross_reporter,
|
|
182
|
-
synthetic_method_index: synthetic_method_index
|
|
277
|
+
synthetic_method_index: synthetic_method_index,
|
|
278
|
+
project_patched_methods: project_patched_methods,
|
|
279
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
183
280
|
)
|
|
184
281
|
end
|
|
185
282
|
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
@@ -257,6 +354,19 @@ module Rigor
|
|
|
257
354
|
@rbs_loader&.reflection
|
|
258
355
|
end
|
|
259
356
|
|
|
357
|
+
# Returns true when the RBS environment carries the named
|
|
358
|
+
# declaration as a Module (not a Class). Used by the
|
|
359
|
+
# `user_class_fallback_receiver` tier to detect a module-mixin
|
|
360
|
+
# receiver (e.g. `PP::ObjectMixin`) so the dispatcher can route
|
|
361
|
+
# unresolved method calls through the `Nominal[Object]`
|
|
362
|
+
# fallback — every concrete includer of M honours Kernel /
|
|
363
|
+
# Object instance methods through its own ancestor chain.
|
|
364
|
+
def rbs_module?(name)
|
|
365
|
+
return false unless rbs_loader
|
|
366
|
+
|
|
367
|
+
rbs_loader.rbs_module?(name)
|
|
368
|
+
end
|
|
369
|
+
|
|
260
370
|
# Compares two class/module names using analyzer-owned class data.
|
|
261
371
|
# Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
|
|
262
372
|
# `:unknown`. The static registry handles built-ins cheaply; the RBS
|
|
@@ -31,14 +31,20 @@ module Rigor
|
|
|
31
31
|
#
|
|
32
32
|
# ## Field set
|
|
33
33
|
#
|
|
34
|
-
# - `target_kind`: `:parameter` (call-site argument)
|
|
35
|
-
# `:
|
|
36
|
-
#
|
|
37
|
-
#
|
|
34
|
+
# - `target_kind`: `:parameter` (call-site argument), `:self`
|
|
35
|
+
# (receiver), or `:local` (a named local in the surrounding
|
|
36
|
+
# scope). v0.1.8 Pillar 2 Slice 1 added `:local` so plugins
|
|
37
|
+
# recognising bespoke call shapes (`expect(x).to be_a(T)`)
|
|
38
|
+
# can narrow a specific scope-bound local without routing
|
|
39
|
+
# through the parameter-name lookup that requires an
|
|
40
|
+
# authoritative RBS sig on the called method. Future slices
|
|
41
|
+
# may extend further (`:ivar`, `:result`). The merger is
|
|
42
|
+
# agnostic to the concrete kinds and only requires equality.
|
|
38
43
|
# - `target_name`: a `Symbol`. For `:parameter` it's the
|
|
39
44
|
# declared parameter name. For `:self` it is the literal
|
|
40
45
|
# `:self` symbol so the field stays non-nil and the merge
|
|
41
|
-
# key is well-defined.
|
|
46
|
+
# key is well-defined. For `:local` it's the local-variable
|
|
47
|
+
# name (e.g. `:x` for `expect(x).to be_a(T)`).
|
|
42
48
|
# - `type`: a `Rigor::Type::*` (Nominal, Refined,
|
|
43
49
|
# IntegerRange, Difference, …) the fact narrows the
|
|
44
50
|
# target toward (when `negative` is false) or away from
|
|
@@ -53,7 +59,7 @@ module Rigor
|
|
|
53
59
|
# value {Element#target} keys on, so two facts that narrow
|
|
54
60
|
# the same parameter from different contribution sources
|
|
55
61
|
# land in the same merge bucket.
|
|
56
|
-
FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
|
|
62
|
+
FACT_VALID_TARGET_KINDS = %i[parameter self local].freeze
|
|
57
63
|
|
|
58
64
|
class Fact < Data.define(:target_kind, :target_name, :type, :negative)
|
|
59
65
|
def initialize(target_kind:, target_name:, type:, negative: false)
|
|
@@ -72,10 +78,14 @@ module Rigor
|
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# Composite target identifier the merger keys on. `:self`
|
|
75
|
-
# for self-targeted facts; otherwise `[
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# bucket.
|
|
81
|
+
# for self-targeted facts; otherwise `[kind, name]` so two
|
|
82
|
+
# contributions that narrow the same `(kind, name)` pair —
|
|
83
|
+
# regardless of source family — land in the same merge
|
|
84
|
+
# bucket. `:local` and `:parameter` facts that name the
|
|
85
|
+
# same symbol stay in separate buckets, which is the
|
|
86
|
+
# correct semantics: a `:local` fact narrows the surrounding
|
|
87
|
+
# scope's named local, a `:parameter` fact narrows the
|
|
88
|
+
# call-site argument matching the parameter declaration.
|
|
79
89
|
def target
|
|
80
90
|
target_kind == :self ? :self : [target_kind, target_name]
|
|
81
91
|
end
|
|
@@ -446,13 +446,45 @@ module Rigor
|
|
|
446
446
|
|
|
447
447
|
def accepts_nominal_from_constant(self_type, constant, mode)
|
|
448
448
|
ruby_class = resolve_class(self_type.class_name)
|
|
449
|
-
if ruby_class
|
|
450
|
-
|
|
449
|
+
return constant_is_a_result(ruby_class, constant, self_type, mode) if ruby_class
|
|
450
|
+
|
|
451
|
+
# The host process may not have required the constant's
|
|
452
|
+
# declared self_type (e.g. `BigDecimal` since Ruby 3.4
|
|
453
|
+
# is no longer a default gem). Fall back to inspecting
|
|
454
|
+
# the value's own class ancestor chain — always loadable
|
|
455
|
+
# because the value already exists. Required for
|
|
456
|
+
# OverloadSelector to reject `Integer#+(BigDecimal) ->
|
|
457
|
+
# BigDecimal` overloads contributed by `bigdecimal`'s
|
|
458
|
+
# RBS reopening when the actual arg is a Constant<Integer>.
|
|
459
|
+
ancestor_names = constant.value.class.ancestors.map(&:name)
|
|
460
|
+
if ancestor_names.include?(self_type.class_name)
|
|
461
|
+
Type::AcceptsResult.yes(
|
|
451
462
|
mode: mode,
|
|
452
|
-
reasons: "class #{self_type.class_name}
|
|
463
|
+
reasons: "Constant value class ancestors include #{self_type.class_name}"
|
|
464
|
+
)
|
|
465
|
+
else
|
|
466
|
+
Type::AcceptsResult.no(
|
|
467
|
+
mode: mode,
|
|
468
|
+
reasons: "Constant value class ancestors exclude #{self_type.class_name}"
|
|
453
469
|
)
|
|
454
470
|
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def subtype_result_via_ancestors(actual_class, target_name, mode)
|
|
474
|
+
if actual_class.ancestors.map(&:name).include?(target_name)
|
|
475
|
+
Type::AcceptsResult.yes(
|
|
476
|
+
mode: mode,
|
|
477
|
+
reasons: "#{actual_class.name} ancestors include #{target_name}"
|
|
478
|
+
)
|
|
479
|
+
else
|
|
480
|
+
Type::AcceptsResult.no(
|
|
481
|
+
mode: mode,
|
|
482
|
+
reasons: "#{actual_class.name} ancestors exclude #{target_name} (target unloadable)"
|
|
483
|
+
)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
455
486
|
|
|
487
|
+
def constant_is_a_result(ruby_class, constant, self_type, mode)
|
|
456
488
|
if constant.value.is_a?(ruby_class)
|
|
457
489
|
Type::AcceptsResult.yes(mode: mode, reasons: "Constant value is_a?(#{self_type.class_name})")
|
|
458
490
|
else
|
|
@@ -794,6 +826,19 @@ module Rigor
|
|
|
794
826
|
|
|
795
827
|
target_class = resolve_class(target_name)
|
|
796
828
|
actual_class = resolve_class(actual_name)
|
|
829
|
+
# When only `actual` resolves, we can still rule out
|
|
830
|
+
# `actual <:= target` by inspecting `actual`'s ancestor
|
|
831
|
+
# chain. The canonical case: `target=BigDecimal` is not
|
|
832
|
+
# loadable in the host process (no `require` in rigor's
|
|
833
|
+
# own runtime), but `actual=Integer` IS, and Integer's
|
|
834
|
+
# ancestors do not include `BigDecimal`, so the subtype
|
|
835
|
+
# relation MUST be `:no` rather than the conservative
|
|
836
|
+
# `:maybe`. The reverse asymmetry (target resolves,
|
|
837
|
+
# actual doesn't) does not let us conclude anything —
|
|
838
|
+
# the unloaded `actual` could be an unrelated class or
|
|
839
|
+
# a subclass of `target` we can't see, so we still
|
|
840
|
+
# answer `:maybe` there.
|
|
841
|
+
return subtype_result_via_ancestors(actual_class, target_name, mode) if target_class.nil? && actual_class
|
|
797
842
|
if target_class.nil? || actual_class.nil?
|
|
798
843
|
return Type::AcceptsResult.maybe(
|
|
799
844
|
mode: mode,
|
|
@@ -61,6 +61,10 @@ module Rigor
|
|
|
61
61
|
Prism::RationalNode => :type_of_literal_value,
|
|
62
62
|
Prism::SymbolNode => :symbol_type_for,
|
|
63
63
|
Prism::StringNode => :string_type_for,
|
|
64
|
+
Prism::XStringNode => :type_of_xstring,
|
|
65
|
+
Prism::InterpolatedXStringNode => :type_of_xstring,
|
|
66
|
+
Prism::SourceFileNode => :type_of_source_file,
|
|
67
|
+
Prism::SourceLineNode => :type_of_source_line,
|
|
64
68
|
Prism::TrueNode => :type_of_true,
|
|
65
69
|
Prism::FalseNode => :type_of_false,
|
|
66
70
|
Prism::NilNode => :type_of_nil,
|
|
@@ -144,6 +148,9 @@ module Rigor
|
|
|
144
148
|
Prism::AliasMethodNode => :type_of_nil_value,
|
|
145
149
|
Prism::AliasGlobalVariableNode => :type_of_nil_value,
|
|
146
150
|
Prism::UndefNode => :type_of_nil_value,
|
|
151
|
+
Prism::PostExecutionNode => :type_of_nil_value,
|
|
152
|
+
Prism::ShareableConstantNode => :type_of_shareable_constant,
|
|
153
|
+
Prism::ImplicitNode => :type_of_implicit,
|
|
147
154
|
Prism::ForwardingSuperNode => :type_of_dynamic_top,
|
|
148
155
|
Prism::BlockArgumentNode => :type_of_non_value,
|
|
149
156
|
# Parameters and blocks (non-value positions)
|
|
@@ -159,6 +166,7 @@ module Rigor
|
|
|
159
166
|
Prism::ForwardingParameterNode => :type_of_non_value,
|
|
160
167
|
Prism::NoKeywordsParameterNode => :type_of_non_value,
|
|
161
168
|
Prism::ImplicitRestNode => :type_of_non_value,
|
|
169
|
+
Prism::ItParametersNode => :type_of_non_value,
|
|
162
170
|
Prism::BlockNode => :type_of_dynamic_top,
|
|
163
171
|
Prism::SplatNode => :type_of_non_value,
|
|
164
172
|
# Control flow (Slice 3 phase 1): branch types are unioned, jumps
|
|
@@ -188,8 +196,8 @@ module Rigor
|
|
|
188
196
|
Prism::UntilNode => :type_of_loop,
|
|
189
197
|
Prism::ForNode => :type_of_dynamic_top,
|
|
190
198
|
Prism::DefinedNode => :type_of_defined,
|
|
191
|
-
Prism::NumberedReferenceReadNode => :
|
|
192
|
-
Prism::BackReferenceReadNode => :
|
|
199
|
+
Prism::NumberedReferenceReadNode => :type_of_numbered_reference,
|
|
200
|
+
Prism::BackReferenceReadNode => :type_of_back_reference,
|
|
193
201
|
Prism::MatchPredicateNode => :type_of_match_predicate,
|
|
194
202
|
Prism::MatchRequiredNode => :type_of_match_required,
|
|
195
203
|
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
@@ -339,6 +347,21 @@ module Rigor
|
|
|
339
347
|
)
|
|
340
348
|
end
|
|
341
349
|
|
|
350
|
+
# `$1` / `$2` / ... — numbered match-data globals. When the
|
|
351
|
+
# narrowing tier has bound a tighter type for this number
|
|
352
|
+
# (typically `String` after a `=~`-success guard like `unless
|
|
353
|
+
# /(\d+)/ =~ s; raise; end`), prefer the scope-bound type.
|
|
354
|
+
# Falls back to the default `String | nil`.
|
|
355
|
+
def type_of_numbered_reference(node)
|
|
356
|
+
scope.global(:"$#{node.number}") || type_of_string_or_nil(node)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same
|
|
360
|
+
# narrowing model as numbered references.
|
|
361
|
+
def type_of_back_reference(node)
|
|
362
|
+
scope.global(node.name) || type_of_string_or_nil(node)
|
|
363
|
+
end
|
|
364
|
+
|
|
342
365
|
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
343
366
|
# when the pattern matches, `false` otherwise.
|
|
344
367
|
def type_of_match_predicate(_node)
|
|
@@ -888,6 +911,45 @@ module Rigor
|
|
|
888
911
|
Type::Combinator.constant_of(unescaped)
|
|
889
912
|
end
|
|
890
913
|
|
|
914
|
+
# Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a
|
|
915
|
+
# String. Even when the content is statically known, we widen to
|
|
916
|
+
# Nominal[String] because the runtime value depends on the
|
|
917
|
+
# subprocess output, not the source text.
|
|
918
|
+
def type_of_xstring(_node)
|
|
919
|
+
Type::Combinator.nominal_of(String)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# __FILE__ is the source file path. Always non-empty when
|
|
923
|
+
# parsing a real file (the path resolver gives the buffer
|
|
924
|
+
# name, which is at minimum `"(stdin)"` / `"-e"` / a real
|
|
925
|
+
# path — never the empty String). Widened to
|
|
926
|
+
# `non-empty-string` instead of `Nominal[String]` so
|
|
927
|
+
# downstream String-emptiness checks know the value cannot
|
|
928
|
+
# be `""`.
|
|
929
|
+
def type_of_source_file(_node)
|
|
930
|
+
Type::Combinator.non_empty_string
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# __LINE__ is the line of the source literal. Ruby line
|
|
934
|
+
# numbers are 1-indexed, so `__LINE__` is always at least
|
|
935
|
+
# 1 — `positive-int` (Integer in `[1, +Inf)`) is the
|
|
936
|
+
# canonical refinement.
|
|
937
|
+
def type_of_source_line(_node)
|
|
938
|
+
Type::Combinator.positive_int
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
# `# shareable_constant_value:` magic comment wraps the next
|
|
942
|
+
# constant write. Type is the wrapped write's value.
|
|
943
|
+
def type_of_shareable_constant(node)
|
|
944
|
+
type_of(node.write)
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
# `{ x: }` shorthand hash. The implicit value is the call to
|
|
948
|
+
# `x` (or a local read of `x`). Delegate.
|
|
949
|
+
def type_of_implicit(node)
|
|
950
|
+
type_of(node.value)
|
|
951
|
+
end
|
|
952
|
+
|
|
891
953
|
def local_read(node)
|
|
892
954
|
scope.local(node.name) || dynamic_top
|
|
893
955
|
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-20 Slice 2a — node types for the parsed body of a
|
|
6
|
+
# type-function `Definition`. Each node represents one
|
|
7
|
+
# piece of a Rigor-side type expression that the reducer
|
|
8
|
+
# ({HktReducer}) walks against a concrete argument list.
|
|
9
|
+
#
|
|
10
|
+
# Slice 2a ships a programmatic constructor surface only:
|
|
11
|
+
# plugin and Rigor-bundled overlay authors build a body
|
|
12
|
+
# tree by hand using these node types. The string-grammar
|
|
13
|
+
# parser that reads `Definition#body` (the raw String slot
|
|
14
|
+
# already populated by Slice 1's `HktDirectives.parse_define`)
|
|
15
|
+
# into a tree is Slice 2b's deliverable; until it ships, the
|
|
16
|
+
# `body` String stays opaque and `body_tree` is the
|
|
17
|
+
# evaluable form.
|
|
18
|
+
#
|
|
19
|
+
# The five node types cover the JSON.parse and dry-monads
|
|
20
|
+
# use cases ADR-20 § Implementation slicing names as
|
|
21
|
+
# near-term adopters:
|
|
22
|
+
#
|
|
23
|
+
# - {TypeLeaf} — wraps a fully-built `Rigor::Type`
|
|
24
|
+
# (use for atoms like `nil`, `Constant<true>`,
|
|
25
|
+
# `Nominal[Integer]`).
|
|
26
|
+
# - {Param} — reference to a formal parameter
|
|
27
|
+
# declared in the enclosing `Definition#params` list
|
|
28
|
+
# (e.g. `K` in `json::value[K]`). The reducer
|
|
29
|
+
# substitutes from the application's `args`.
|
|
30
|
+
# - {AppRef} — abstract HKT application; the reducer
|
|
31
|
+
# resolves it via the registry, or returns the `App`
|
|
32
|
+
# carrier as-is when the reference is self-recursive
|
|
33
|
+
# (lazy "tying-the-knot" handling that lets recursive
|
|
34
|
+
# sums like `json::value` reduce without infinite
|
|
35
|
+
# expansion).
|
|
36
|
+
# - {Union} — N-ary union of arms.
|
|
37
|
+
# - {NominalApp} — parameterised nominal class
|
|
38
|
+
# (`Array[X]`, `Hash[K, V]`) whose type args are
|
|
39
|
+
# themselves body nodes.
|
|
40
|
+
#
|
|
41
|
+
# Every node is a frozen `Data.define` value; structural
|
|
42
|
+
# equality is by-field.
|
|
43
|
+
module HktBody
|
|
44
|
+
# Wraps a pre-built `Rigor::Type` value. Use for atoms
|
|
45
|
+
# that need no substitution (e.g. `Nominal[Integer]`,
|
|
46
|
+
# `Constant<nil>`).
|
|
47
|
+
TypeLeaf = Data.define(:type) do
|
|
48
|
+
def initialize(type:)
|
|
49
|
+
raise ArgumentError, "type must not be nil" if type.nil?
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Reference to a formal parameter the enclosing
|
|
56
|
+
# `Definition#params` declared. The reducer substitutes
|
|
57
|
+
# this node with the matching positional arg from the
|
|
58
|
+
# `App` being reduced; an unknown name raises during
|
|
59
|
+
# reduction (the parser, when it ships, MUST reject
|
|
60
|
+
# unknown names earlier).
|
|
61
|
+
Param = Data.define(:name) do
|
|
62
|
+
def initialize(name:)
|
|
63
|
+
raise ArgumentError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
|
|
64
|
+
|
|
65
|
+
super
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Abstract HKT application — the reducer's primary
|
|
70
|
+
# recursion point. `uri` is a namespaced Symbol
|
|
71
|
+
# matching some `Registration` in the registry; `args`
|
|
72
|
+
# is an Array of body nodes (each gets substituted /
|
|
73
|
+
# resolved before being used).
|
|
74
|
+
AppRef = Data.define(:uri, :args) do
|
|
75
|
+
def initialize(uri:, args:)
|
|
76
|
+
raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
|
|
77
|
+
raise ArgumentError, "uri must be namespaced as `:a::b`, got #{uri.inspect}" unless uri.to_s.include?("::")
|
|
78
|
+
raise ArgumentError, "args must be an Array, got #{args.class}" unless args.is_a?(Array)
|
|
79
|
+
raise ArgumentError, "args must be non-empty" if args.empty?
|
|
80
|
+
|
|
81
|
+
super(uri: uri, args: args.dup.freeze)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# N-ary union. The reducer builds the result through
|
|
86
|
+
# `Type::Combinator.union(*reduced_arms)` so
|
|
87
|
+
# normalization (flattening, dedup, Bot drop) applies.
|
|
88
|
+
Union = Data.define(:arms) do
|
|
89
|
+
def initialize(arms:)
|
|
90
|
+
raise ArgumentError, "arms must be an Array, got #{arms.class}" unless arms.is_a?(Array)
|
|
91
|
+
raise ArgumentError, "arms must be non-empty" if arms.empty?
|
|
92
|
+
|
|
93
|
+
super(arms: arms.dup.freeze)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Parameterised nominal class. `class_name` is the
|
|
98
|
+
# Ruby class name (`"Array"`, `"Hash"`); `args` is an
|
|
99
|
+
# Array of body nodes for the type arguments. The
|
|
100
|
+
# reducer builds the result through
|
|
101
|
+
# `Type::Combinator.nominal_of(class_name, type_args:
|
|
102
|
+
# reduced_args)`.
|
|
103
|
+
NominalApp = Data.define(:class_name, :args) do
|
|
104
|
+
def initialize(class_name:, args:)
|
|
105
|
+
unless class_name.is_a?(String) && !class_name.empty?
|
|
106
|
+
raise ArgumentError, "class_name must be a non-empty String, got #{class_name.inspect}"
|
|
107
|
+
end
|
|
108
|
+
raise ArgumentError, "args must be an Array, got #{args.class}" unless args.is_a?(Array)
|
|
109
|
+
raise ArgumentError, "args must be non-empty (use TypeLeaf with Nominal for raw class refs)" if args.empty?
|
|
110
|
+
|
|
111
|
+
super(class_name: class_name, args: args.dup.freeze)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ADR-20 § D3 — conditional type form. `test` is a
|
|
116
|
+
# {TestSubtype} / {TestEquality} / {TestMembership}
|
|
117
|
+
# value object the reducer evaluates against the
|
|
118
|
+
# current bindings; `then_branch` / `else_branch` are
|
|
119
|
+
# body nodes. The reducer's trinary handling:
|
|
120
|
+
#
|
|
121
|
+
# - test = `yes` → return the reduced `then_branch`.
|
|
122
|
+
# - test = `no` → return the reduced `else_branch`.
|
|
123
|
+
# - test = `maybe` → widen to the union of both
|
|
124
|
+
# reduced branches (per ADR-20 WD7 / robustness
|
|
125
|
+
# principle).
|
|
126
|
+
Conditional = Data.define(:test, :then_branch, :else_branch) do
|
|
127
|
+
def initialize(test:, then_branch:, else_branch:)
|
|
128
|
+
raise ArgumentError, "test must not be nil" if test.nil?
|
|
129
|
+
raise ArgumentError, "then_branch must not be nil" if then_branch.nil?
|
|
130
|
+
raise ArgumentError, "else_branch must not be nil" if else_branch.nil?
|
|
131
|
+
|
|
132
|
+
super
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `left <: right` — subtype check. `left` is typically
|
|
137
|
+
# a {Param} reference; `right` is any body expression.
|
|
138
|
+
TestSubtype = Data.define(:left, :right) do
|
|
139
|
+
def initialize(left:, right:)
|
|
140
|
+
raise ArgumentError, "left/right must not be nil" if left.nil? || right.nil?
|
|
141
|
+
|
|
142
|
+
super
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# `left == right` — structural equality. Useful for
|
|
147
|
+
# discriminating against literal constants
|
|
148
|
+
# (`E == :symbol`).
|
|
149
|
+
TestEquality = Data.define(:left, :right) do
|
|
150
|
+
def initialize(left:, right:)
|
|
151
|
+
raise ArgumentError, "left/right must not be nil" if left.nil? || right.nil?
|
|
152
|
+
|
|
153
|
+
super
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# `left in [opt1, opt2, ...]` — set membership. Each
|
|
158
|
+
# `option` is a body node; the test passes iff `left`
|
|
159
|
+
# is structurally equal to any of the options.
|
|
160
|
+
TestMembership = Data.define(:left, :options) do
|
|
161
|
+
def initialize(left:, options:)
|
|
162
|
+
raise ArgumentError, "left must not be nil" if left.nil?
|
|
163
|
+
raise ArgumentError, "options must be an Array, got #{options.class}" unless options.is_a?(Array)
|
|
164
|
+
raise ArgumentError, "options must be non-empty" if options.empty?
|
|
165
|
+
|
|
166
|
+
super(left: left, options: options.dup.freeze)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|