rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -149,6 +149,38 @@ module Rigor
|
|
|
149
149
|
next_step: "Generate signatures for the overriding subclass too — widen the `sig-gen` path set to " \
|
|
150
150
|
"cover it — and the parent's declaration becomes safe to emit."
|
|
151
151
|
),
|
|
152
|
+
"sig.skipped.overridden-bot" => Entry.new(
|
|
153
|
+
id: "sig.skipped.overridden-bot",
|
|
154
|
+
summary: "The method's body never returns, and the analysed code overrides the method.",
|
|
155
|
+
explanation: "The inferred return is `bot`, but the analysed code defines the method again, with `def` " \
|
|
156
|
+
"or `define_method`: a subclass (signed or not), a class that includes the method's module, " \
|
|
157
|
+
"a module included later or prepended, or an extending class's own singleton method. " \
|
|
158
|
+
"`bot` describes only the base body the overrides replace, and every override returns a " \
|
|
159
|
+
"value, so the declaration would contradict each of them (#3029).",
|
|
160
|
+
next_step: "Declare the return every implementation shares (often `untyped` or the overrides' common " \
|
|
161
|
+
"type) by hand in `sig/`."
|
|
162
|
+
),
|
|
163
|
+
"sig.skipped.abstract-method" => Entry.new(
|
|
164
|
+
id: "sig.skipped.abstract-method",
|
|
165
|
+
summary: "The method is abstract: its body only raises an error asking for an implementation.",
|
|
166
|
+
explanation: "The body ends in `raise NotImplementedError`, in an error class named `Abstract...` or " \
|
|
167
|
+
"`NotImplemented...`, or in a raise whose message asks for the method to be implemented, " \
|
|
168
|
+
"defined or overridden. Its inferred return is `bot`, but the method is meant to be " \
|
|
169
|
+
"replaced, and the replacements may live outside the analysed code, as in a plugin gem " \
|
|
170
|
+
"(#3029).",
|
|
171
|
+
next_step: "Declare the return the implementations must provide by hand in `sig/`."
|
|
172
|
+
),
|
|
173
|
+
"sig.skipped.template-method" => Entry.new(
|
|
174
|
+
id: "sig.skipped.template-method",
|
|
175
|
+
summary: "The method's `bot` return may come only from calling an overridden or abstract method.",
|
|
176
|
+
explanation: "The inferred return is `bot`, and the body does not end in a direct `raise`, `fail`, " \
|
|
177
|
+
"`throw`, `exit` or `abort`. It calls a method the analysed code overrides or declares " \
|
|
178
|
+
"abstract, directly or through a chain of self-dispatched calls, so the `bot` may be only " \
|
|
179
|
+
"the base body of that call, which an override replaces at run time. The call is matched " \
|
|
180
|
+
"by name, whatever its receiver (#3029).",
|
|
181
|
+
next_step: "Declare the return by hand in `sig/`, usually the return the overridden hook's " \
|
|
182
|
+
"implementations share."
|
|
183
|
+
),
|
|
152
184
|
"sig.skipped.top-level-def" => Entry.new(
|
|
153
185
|
id: "sig.skipped.top-level-def",
|
|
154
186
|
summary: "A top-level `def` defines a private `Object` method, and sig-gen writes no signature for it.",
|
|
@@ -171,6 +203,21 @@ module Rigor
|
|
|
171
203
|
"writes neither rather than guess (#1968).",
|
|
172
204
|
next_step: "Move the visibility call to the module body's own statements, or name the methods it " \
|
|
173
205
|
"should cover (`public :name`, `module_function :name`), so the reading is certain."
|
|
206
|
+
),
|
|
207
|
+
"sig.skipped.platform-guarded" => Entry.new(
|
|
208
|
+
id: "sig.skipped.platform-guarded",
|
|
209
|
+
summary: "The method is defined in a file loaded only under a platform guard, so its proposal was withheld.",
|
|
210
|
+
explanation: "The file is `require`d / `require_relative`d only inside a conditional on " \
|
|
211
|
+
"`RUBY_ENGINE`, `RUBY_PLATFORM` or `defined?(…)`, so it is not an ordinary definition " \
|
|
212
|
+
"for every platform — the shape is `if RUBY_ENGINE == 'truffleruby'` guarding a " \
|
|
213
|
+
"pure-Ruby fallback whose real methods a C extension provides elsewhere. sig-gen " \
|
|
214
|
+
"withholds the proposal when another scanned file also defines the method (the " \
|
|
215
|
+
"guarded copy would compete with or duplicate the definition this platform loads), " \
|
|
216
|
+
"or when the inferred return is `bot` — the shape a raising stub takes, and never a " \
|
|
217
|
+
"contract worth writing from a file the current engine may never read (#3035).",
|
|
218
|
+
next_step: "Declare the method for the platform the project targets — hand-write the `sig/` " \
|
|
219
|
+
"entry, or cover the file that defines it on this platform in the `sig-gen` paths — " \
|
|
220
|
+
"rather than taking the guarded file's stub as the contract."
|
|
174
221
|
)
|
|
175
222
|
}.freeze
|
|
176
223
|
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -38,6 +38,21 @@ module Rigor
|
|
|
38
38
|
:target, keyword_init: true)
|
|
39
39
|
private_constant :MergeState
|
|
40
40
|
|
|
41
|
+
# #2576 — a target the writer cannot write (a permission denial, a directory sitting where the
|
|
42
|
+
# `.rbs` belongs, a parent path that is a regular file) is a bad `--write` invocation, not a
|
|
43
|
+
# crash: the command
|
|
44
|
+
# reports it as the same usage error `rigor init --path` gives and exits 64. Carries the target
|
|
45
|
+
# and the stripped Errno reason so the CLI prints `cannot write PATH: REASON` without
|
|
46
|
+
# re-parsing the message.
|
|
47
|
+
class WriteError < StandardError
|
|
48
|
+
attr_reader :target_path
|
|
49
|
+
|
|
50
|
+
def initialize(target_path, error)
|
|
51
|
+
@target_path = target_path.to_s
|
|
52
|
+
super("cannot write #{@target_path}: #{error.message.sub(/ @ .*\z/m, '')}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
41
56
|
# The target routing this writer applies; the generator maps candidates through it to ask {#accepts_target?}.
|
|
42
57
|
attr_reader :path_mapper
|
|
43
58
|
|
|
@@ -186,6 +201,8 @@ module Rigor
|
|
|
186
201
|
end
|
|
187
202
|
|
|
188
203
|
target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
|
|
204
|
+
rescue SystemCallError => e
|
|
205
|
+
raise WriteError.new(target, e)
|
|
189
206
|
end
|
|
190
207
|
|
|
191
208
|
# Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
|
|
@@ -222,6 +239,8 @@ module Rigor
|
|
|
222
239
|
end
|
|
223
240
|
WriteResult.new(source_path: source_path, target_path: target,
|
|
224
241
|
action: :created, applied: candidates)
|
|
242
|
+
rescue SystemCallError => e
|
|
243
|
+
raise WriteError.new(target, e)
|
|
225
244
|
end
|
|
226
245
|
|
|
227
246
|
# ADR-14 gap-#3 follow-up (c): when one candidate's `class_name` is a strict prefix of another's, emit a
|
|
@@ -429,6 +448,8 @@ module Rigor
|
|
|
429
448
|
action: :updated, applied: state.applied, skipped: state.skipped,
|
|
430
449
|
left_unreadable: state.left_unreadable, replaced: state.replaced,
|
|
431
450
|
repaired_headers: state.repaired_headers)
|
|
451
|
+
rescue SystemCallError => e
|
|
452
|
+
raise WriteError.new(target, e)
|
|
432
453
|
end
|
|
433
454
|
|
|
434
455
|
# Applies every class group, then every requested shell, then normalises the layout the three steps
|
data/lib/rigor/sig_gen.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "sig_gen/classification"
|
|
|
5
5
|
require_relative "sig_gen/effect_annotation"
|
|
6
6
|
require_relative "sig_gen/inline_declarations"
|
|
7
7
|
require_relative "sig_gen/method_candidate"
|
|
8
|
+
require_relative "sig_gen/platform_guarded_files"
|
|
8
9
|
require_relative "sig_gen/observed_call"
|
|
9
10
|
require_relative "sig_gen/meta_class_shape"
|
|
10
11
|
require_relative "sig_gen/type_elaborator"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "literals"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Source
|
|
8
|
+
# Issue #2793 — the methods a `Forwardable` / `SingleForwardable` delegator macro defines, read off its syntax
|
|
9
|
+
# alone. Ruby 4.0's `forwardable.rb` is the oracle for each row:
|
|
10
|
+
#
|
|
11
|
+
# - `def_instance_delegator(acc, m, ali = m)` defines `ali` on the instance side;
|
|
12
|
+
# - `def_instance_delegators(acc, *ms)` defines each `m` but `__send__` and `__id__`, instance side (the two are
|
|
13
|
+
# recorded anyway: `BasicObject` declares both, so a recorded entry changes no answer);
|
|
14
|
+
# - `instance_delegate(ms => acc)` defines each key name (a name or an array of them), instance side;
|
|
15
|
+
# - `def_single_delegator(s)` and `single_delegate` are the same on the singleton side;
|
|
16
|
+
# - `def_delegator(s)` and `delegate` are the same on the side the `extend` decides.
|
|
17
|
+
#
|
|
18
|
+
# `def_delegator(s)` and `delegate` are `Forwardable`'s instance aliases and `SingleForwardable`'s singleton
|
|
19
|
+
# ones, so their side is {AMBIGUOUS} and the caller settles it from the `extend` it has seen. `def_delegator :@x,
|
|
20
|
+
# :last, :final` defines `final` and never `last`.
|
|
21
|
+
#
|
|
22
|
+
# A name the syntax does not spell (`def_delegators :@x, *NAMES`, `delegate names => :@x`) makes the delegation
|
|
23
|
+
# {Delegation#open?}: the macro defines methods no reader can enumerate. `delegate` takes Forwardable's
|
|
24
|
+
# single-hash form only, so ActiveSupport's `delegate :a, to: :b` (positional names before a `to:` key) is
|
|
25
|
+
# not read here.
|
|
26
|
+
module ForwardableDelegators
|
|
27
|
+
AMBIGUOUS = :ambiguous
|
|
28
|
+
|
|
29
|
+
# The names a delegator macro defines, on which side, and whether it also defines names its syntax hides.
|
|
30
|
+
Delegation = Data.define(:side, :names, :open) do
|
|
31
|
+
def open? = open
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `[side, shape]` per macro: `:alias` takes `(accessor, method, alias = method)`, `:list` takes `(accessor,
|
|
35
|
+
# *methods)`, `:hash` takes `(methods => accessor, …)`.
|
|
36
|
+
MACROS = Ractor.make_shareable(
|
|
37
|
+
{
|
|
38
|
+
def_instance_delegator: %i[instance alias], def_instance_delegators: %i[instance list],
|
|
39
|
+
instance_delegate: %i[instance hash],
|
|
40
|
+
def_single_delegator: %i[singleton alias], def_single_delegators: %i[singleton list],
|
|
41
|
+
single_delegate: %i[singleton hash],
|
|
42
|
+
def_delegator: [AMBIGUOUS, :alias], def_delegators: [AMBIGUOUS, :list], delegate: [AMBIGUOUS, :hash]
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
private_constant :MACROS
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# The {Delegation} of an implicit-self (or `self.`) delegator macro call, or nil for any other call, and for
|
|
51
|
+
# one whose arguments are not a shape the macro accepts.
|
|
52
|
+
def of(call_node)
|
|
53
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
54
|
+
return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
55
|
+
|
|
56
|
+
side, shape = MACROS[call_node.name]
|
|
57
|
+
return nil if side.nil?
|
|
58
|
+
|
|
59
|
+
arguments = call_node.arguments&.arguments || []
|
|
60
|
+
return Delegation.new(side: side, names: [].freeze, open: true) if unplaceable_splat?(shape, arguments)
|
|
61
|
+
|
|
62
|
+
names, open = case shape
|
|
63
|
+
when :alias then alias_names(arguments)
|
|
64
|
+
when :list then list_names(arguments)
|
|
65
|
+
else hash_names(arguments)
|
|
66
|
+
end
|
|
67
|
+
return nil if names.nil?
|
|
68
|
+
|
|
69
|
+
Delegation.new(side: side, names: names.freeze, open: open)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A splat (`def_delegator(*ARGS)`, `def_delegators(*ARGS)`, `delegate(*pairs)`) hides which argument is the
|
|
73
|
+
# accessor and which a name, so the macro may define any name. A splat after the accessor of the list form is
|
|
74
|
+
# only a hidden name, and {#list_names} reads the literal names beside it.
|
|
75
|
+
def unplaceable_splat?(shape, arguments)
|
|
76
|
+
splats = arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
|
|
77
|
+
return false if splats.empty?
|
|
78
|
+
|
|
79
|
+
shape != :list || splats.include?(0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def alias_names(arguments)
|
|
83
|
+
return nil unless (2..3).cover?(arguments.size)
|
|
84
|
+
|
|
85
|
+
name = Literals.symbol_or_string(arguments.last)
|
|
86
|
+
name.nil? ? [[], true] : [[name], false]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def list_names(arguments)
|
|
90
|
+
return nil if arguments.empty?
|
|
91
|
+
|
|
92
|
+
rest = arguments.drop(1)
|
|
93
|
+
names = rest.filter_map { |argument| Literals.symbol_or_string(argument) }
|
|
94
|
+
[names, names.size != rest.size]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def hash_names(arguments)
|
|
98
|
+
return nil unless arguments.size == 1
|
|
99
|
+
|
|
100
|
+
hash = arguments.first
|
|
101
|
+
return nil unless hash.is_a?(Prism::KeywordHashNode) || hash.is_a?(Prism::HashNode)
|
|
102
|
+
|
|
103
|
+
names = []
|
|
104
|
+
open = false
|
|
105
|
+
hash.elements.each do |element|
|
|
106
|
+
return [[], true] unless element.is_a?(Prism::AssocNode)
|
|
107
|
+
|
|
108
|
+
keys = key_names(element.key)
|
|
109
|
+
keys.nil? ? (open = true) : names.concat(keys)
|
|
110
|
+
end
|
|
111
|
+
[names, open]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The names a hash key spells — a literal name, or an array of them — or nil when any is computed.
|
|
115
|
+
def key_names(key)
|
|
116
|
+
elements = key.is_a?(Prism::ArrayNode) ? key.elements : [key]
|
|
117
|
+
names = elements.map { |element| Literals.symbol_or_string(element) }
|
|
118
|
+
names.include?(nil) ? nil : names
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -109,6 +109,19 @@ module Rigor
|
|
|
109
109
|
def each_child(node, &)
|
|
110
110
|
node.rigor_each_child(&) if node.is_a?(Prism::Node)
|
|
111
111
|
end
|
|
112
|
+
|
|
113
|
+
# #2455 — whether `root` nests deeper than `bound` levels, measured without recursion (the question is asked after
|
|
114
|
+
# the recursive walkers ran out of stack). Stops as soon as one path passes the bound.
|
|
115
|
+
def self.nested_deeper_than?(root, bound)
|
|
116
|
+
stack = [[root, 1]]
|
|
117
|
+
until stack.empty?
|
|
118
|
+
node, depth = stack.pop
|
|
119
|
+
return true if depth > bound
|
|
120
|
+
|
|
121
|
+
node.rigor_each_child { |child| stack << [child, depth + 1] }
|
|
122
|
+
end
|
|
123
|
+
false
|
|
124
|
+
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
end
|
|
@@ -46,6 +46,27 @@ module Rigor
|
|
|
46
46
|
@untyped
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# ADR-130 WD2 — the parity predicate. Every site that keys a leniency on `untyped` asks this instead of testing
|
|
50
|
+
# a `Dynamic`'s facet for `Top` or comparing with {untyped}, so a value whose facet parity erases
|
|
51
|
+
# ({erased_facet?}) reads as `untyped` everywhere but member dispatch. `spec/rigor/type/untyped_parity_spec.rb`
|
|
52
|
+
# flags the raw spellings it knows outside this module. True for `Dynamic[top]` and for a plugin-interface
|
|
53
|
+
# value, `Dynamic[Rigor::Plugin::_Scope]`.
|
|
54
|
+
def untyped?(type)
|
|
55
|
+
return false unless type.is_a?(Dynamic)
|
|
56
|
+
|
|
57
|
+
facet = type.static_facet
|
|
58
|
+
facet.is_a?(Top) || erased_facet?(facet)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ADR-130 WD1 — true for a `Dynamic` facet that parity reads as `top`: the `Rigor::Plugin` interface carrier
|
|
62
|
+
# ({Interface}). {untyped?} answers true for it, and {merge_erased_members} merges it with `Dynamic[top]`.
|
|
63
|
+
# `spec/rigor/type/combinator_facet_erasure_spec.rb` also drives the merge through a stub facet. An engine
|
|
64
|
+
# hook, not public API.
|
|
65
|
+
def erased_facet?(facet)
|
|
66
|
+
facet.is_a?(Interface)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :erased_facet?
|
|
69
|
+
|
|
49
70
|
# Wraps the static facet in a Dynamic[T] carrier. Idempotent on the static facet so
|
|
50
71
|
# Dynamic[Dynamic[T]] collapses to Dynamic[T] per the value-lattice algebra.
|
|
51
72
|
def dynamic(static_facet)
|
|
@@ -821,7 +842,104 @@ module Rigor
|
|
|
821
842
|
|
|
822
843
|
return [top] if flattened.any?(Top)
|
|
823
844
|
|
|
824
|
-
absorb_contained_members(unique_members(flattened))
|
|
845
|
+
absorb_contained_members(merge_erased_members(unique_members(flattened)))
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# ADR-130 WD1 — a facet parity erases ({erased_facet?}) must not give a join a shape master's dedup would
|
|
849
|
+
# not: `Dynamic[_A] | Dynamic[top]` is `Dynamic[top]`, and so are the same members anywhere inside a carrier
|
|
850
|
+
# that holds a type ({TYPE_CHILDREN}). Members equal after {erase_facets} merge into the erased form, at the
|
|
851
|
+
# first one's position; a member no other matches keeps its facet, and {absorb_contained_members} compares
|
|
852
|
+
# the erased forms too. The facet scan runs on every join and allocates nothing (no enumerator, no array),
|
|
853
|
+
# so the merge costs only where a facet is present, which is nowhere until ADR-130 slice 1b.
|
|
854
|
+
def merge_erased_members(members)
|
|
855
|
+
return members if members.size < 2
|
|
856
|
+
return members unless members.any? { |member| facet_bearing?(member) }
|
|
857
|
+
|
|
858
|
+
erased = members.map { |member| erase_facets(member) }
|
|
859
|
+
counts = Hash.new(0)
|
|
860
|
+
erased.each { |type| counts[type] += 1 }
|
|
861
|
+
merged = {}
|
|
862
|
+
result = []
|
|
863
|
+
members.each_with_index do |member, index|
|
|
864
|
+
type = erased[index]
|
|
865
|
+
if counts[type] == 1
|
|
866
|
+
result << member
|
|
867
|
+
elsif !merged.key?(type)
|
|
868
|
+
merged[type] = true
|
|
869
|
+
result << type
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
result
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def facet_bearing?(type)
|
|
876
|
+
return erased_facet?(type.static_facet) || facet_bearing?(type.static_facet) if type.is_a?(Dynamic)
|
|
877
|
+
|
|
878
|
+
readers = TYPE_CHILDREN[type.class]
|
|
879
|
+
return false if readers.nil?
|
|
880
|
+
|
|
881
|
+
readers.each { |reader| return true if child_facet_bearing?(type.public_send(reader)) }
|
|
882
|
+
false
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def child_facet_bearing?(child)
|
|
886
|
+
case child
|
|
887
|
+
when Array then child.any? { |type| facet_bearing?(type) }
|
|
888
|
+
when Hash
|
|
889
|
+
child.each_value { |type| return true if facet_bearing?(type) }
|
|
890
|
+
false
|
|
891
|
+
else facet_bearing?(child)
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# `type` with every facet {erased_facet?} accepts rewritten to `top`, through every carrier
|
|
896
|
+
# {TYPE_CHILDREN} lists. The identity, by reference, on a type that bears none. A facet-bearing `Dynamic`
|
|
897
|
+
# is never `Dynamic[top]`, so {untyped?} answers the hook for it here.
|
|
898
|
+
def erase_facets(type)
|
|
899
|
+
return type unless facet_bearing?(type)
|
|
900
|
+
|
|
901
|
+
case type
|
|
902
|
+
when Dynamic then untyped?(type) ? untyped : dynamic(erase_facets(type.static_facet))
|
|
903
|
+
when Union then union(*erase_each(type.members))
|
|
904
|
+
when Intersection then intersection(*erase_each(type.members))
|
|
905
|
+
when Tuple then tuple_of(*erase_each(type.elements))
|
|
906
|
+
when Nominal then nominal_of(type.class_name, type_args: erase_each(type.type_args))
|
|
907
|
+
when HashShape then erase_shape_facets(type)
|
|
908
|
+
else erase_wrapper_facets(type)
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def erase_wrapper_facets(type)
|
|
913
|
+
case type
|
|
914
|
+
when Difference then Difference.new(erase_facets(type.base), erase_facets(type.removed))
|
|
915
|
+
when Refined then Refined.new(erase_facets(type.base), type.predicate_id)
|
|
916
|
+
when Maybe then Maybe.new(erase_facets(type.value_type))
|
|
917
|
+
when Result then Result.new(erase_facets(type.ok_type), erase_facets(type.err_type))
|
|
918
|
+
else erase_bearer_facets(type)
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def erase_bearer_facets(type)
|
|
923
|
+
case type
|
|
924
|
+
when App then App.new(type.uri, erase_each(type.args), bound: erase_facets(type.bound))
|
|
925
|
+
when BoundMethod
|
|
926
|
+
BoundMethod.new(receiver_type: erase_facets(type.receiver_type), method_name: type.method_name)
|
|
927
|
+
when DataInstance then DataInstance.new(erase_values(type.members), type.class_name)
|
|
928
|
+
when StructInstance then StructInstance.new(erase_values(type.members), type.class_name)
|
|
929
|
+
# The facet itself, erased apart from its `Dynamic` (which erases whole, to `untyped`).
|
|
930
|
+
when Interface then Interface.new(type.name, erase_each(type.type_args))
|
|
931
|
+
else type
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def erase_each(types) = types.map { |type| erase_facets(type) }
|
|
936
|
+
|
|
937
|
+
def erase_values(pairs) = pairs.transform_values { |type| erase_facets(type) }
|
|
938
|
+
|
|
939
|
+
def erase_shape_facets(shape)
|
|
940
|
+
HashShape.new(erase_values(shape.pairs),
|
|
941
|
+
required_keys: shape.required_keys, optional_keys: shape.optional_keys,
|
|
942
|
+
read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys)
|
|
825
943
|
end
|
|
826
944
|
|
|
827
945
|
# Drops every member another member already contains, so the join after a guard names one set
|
|
@@ -835,15 +953,30 @@ module Rigor
|
|
|
835
953
|
# The `!absorbed_by?(other, member)` re-check costs one predicate call per pair that absorbed
|
|
836
954
|
# at all, and it is what makes "both arms dropped" unrepresentable should a future clause make
|
|
837
955
|
# the relation symmetric on a pair of distinct members.
|
|
956
|
+
#
|
|
957
|
+
# ADR-130 WD1 — the relation reads each member's erased form ({erase_facets}), so a facet does not keep an
|
|
958
|
+
# arm master absorbs: `[Dynamic[_A]] | [Dynamic[top] | Integer]` drops the first arm as master's
|
|
959
|
+
# `[Dynamic[top]] | [Dynamic[top] | Integer]` does. Without a facet the readings are the members themselves.
|
|
838
960
|
def absorb_contained_members(members)
|
|
839
961
|
return members unless members.any? { |m| absorbable_member?(m) }
|
|
840
962
|
|
|
841
963
|
members = join_sequence_elements(members)
|
|
842
|
-
members.
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
964
|
+
readings = members.any? { |member| facet_bearing?(member) } ? members.map { |m| erase_facets(m) } : members
|
|
965
|
+
# `each_index`, not `each_with_index`: the Enumerable form allocates an iterator object on every join.
|
|
966
|
+
kept = []
|
|
967
|
+
members.each_index do |index|
|
|
968
|
+
member = members[index]
|
|
969
|
+
kept << member unless absorbable_member?(member) && absorbed_reading?(readings, index)
|
|
970
|
+
end
|
|
971
|
+
kept
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# `any?` rather than an early `return` from an `each` block: a non-local return allocates on every absorbed
|
|
975
|
+
# member. Readings are distinct objects (the members were made unique, and an erased form is a new object).
|
|
976
|
+
def absorbed_reading?(readings, index)
|
|
977
|
+
reading = readings[index]
|
|
978
|
+
readings.any? do |wider|
|
|
979
|
+
!wider.equal?(reading) && absorbed_by?(reading, wider) && !absorbed_by?(wider, reading)
|
|
847
980
|
end
|
|
848
981
|
end
|
|
849
982
|
|
|
@@ -1040,10 +1173,18 @@ module Rigor
|
|
|
1040
1173
|
# need not render alike (`Constant[1..2]` and `Constant[1..2.0]`), so equality is not a safe key. A
|
|
1041
1174
|
# non-main Ractor may not read the module's ivar, so a pool worker renders the key every time, as every
|
|
1042
1175
|
# run did before.
|
|
1176
|
+
#
|
|
1177
|
+
# ADR-130 WD1 — a member bearing a facet sorts by its erased form's rendering, so a facet does not move it:
|
|
1178
|
+
# `Dynamic[_A] | Dynamic[String]` keeps master's `Dynamic[String] | Dynamic[top]` order. The facet scan runs
|
|
1179
|
+
# only when a key is rendered, and a member without a facet renders as it always did.
|
|
1043
1180
|
def sort_key(member)
|
|
1044
|
-
return member
|
|
1181
|
+
return render_sort_key(member) unless Ractor.main?
|
|
1045
1182
|
|
|
1046
|
-
@sort_keys[member] ||= member
|
|
1183
|
+
@sort_keys[member] ||= render_sort_key(member)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def render_sort_key(member)
|
|
1187
|
+
(facet_bearing?(member) ? erase_facets(member) : member).describe(:short)
|
|
1047
1188
|
end
|
|
1048
1189
|
end
|
|
1049
1190
|
|
|
@@ -1052,6 +1193,21 @@ module Rigor
|
|
|
1052
1193
|
# The main Ractor's {sort_key} memo, allocated here for the same reason.
|
|
1053
1194
|
@sort_keys = ObjectSpace::WeakMap.new
|
|
1054
1195
|
|
|
1196
|
+
# ADR-130 WD1 — the readers of every type a carrier holds, which facet erasure walks. A leaf lists none.
|
|
1197
|
+
# `spec/rigor/type/combinator_facet_erasure_spec.rb` fails on a `Rigor::Type` carrier missing here, and on a
|
|
1198
|
+
# listed carrier whose erasure leaves a facet in any of its type slots.
|
|
1199
|
+
TYPE_CHILDREN = Ractor.make_shareable(
|
|
1200
|
+
{
|
|
1201
|
+
Top => [], Bot => [], Constant => [], IntegerRange => [], FloatRange => [], Singleton => [],
|
|
1202
|
+
DataClass => [], StructClass => [],
|
|
1203
|
+
Dynamic => %i[static_facet], Nominal => %i[type_args], Tuple => %i[elements], HashShape => %i[pairs],
|
|
1204
|
+
Union => %i[members], Intersection => %i[members], Difference => %i[base removed], Refined => %i[base],
|
|
1205
|
+
Maybe => %i[value_type], Result => %i[ok_type err_type], App => %i[args bound],
|
|
1206
|
+
BoundMethod => %i[receiver_type], DataInstance => %i[members], StructInstance => %i[members],
|
|
1207
|
+
Interface => %i[type_args]
|
|
1208
|
+
}
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1055
1211
|
NIL_CONSTANT = Constant.new(nil)
|
|
1056
1212
|
TRUE_CONSTANT = Constant.new(true)
|
|
1057
1213
|
FALSE_CONSTANT = Constant.new(false)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../trinary"
|
|
4
|
+
require_relative "../value_semantics"
|
|
5
|
+
require_relative "acceptance_router"
|
|
6
|
+
require_relative "plain_lattice"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Type
|
|
10
|
+
# ADR-130 WD1 — an RBS interface under `Rigor::Plugin` (`_Scope`, `_Environment`, ...), as the facet of the
|
|
11
|
+
# `Dynamic` that carries a plugin-interface value: `Dynamic[Rigor::Plugin::_Scope]`. It appears only there.
|
|
12
|
+
# `Combinator.untyped?` reads such a `Dynamic` as `untyped`, and union normalisation erases it to `Dynamic[top]`
|
|
13
|
+
# wherever a join would otherwise keep an arm master does not, so the facet changes one behaviour: a call of a
|
|
14
|
+
# member the interface declares resolves through that member's RBS (`RbsDispatch`, ADR-130 WD3).
|
|
15
|
+
#
|
|
16
|
+
# `name` is spelled as `Nominal#class_name` is (`Rigor::Plugin::_Scope`), and `type_args` are the interface's
|
|
17
|
+
# type arguments, translated in the caller's context. Every other RBS interface still translates to
|
|
18
|
+
# `Dynamic[top]` (`RbsTypeTranslator`, ADR-130 WD4).
|
|
19
|
+
class Interface
|
|
20
|
+
attr_reader :name, :type_args
|
|
21
|
+
|
|
22
|
+
EMPTY_TYPE_ARGS = [].freeze
|
|
23
|
+
private_constant :EMPTY_TYPE_ARGS
|
|
24
|
+
|
|
25
|
+
def initialize(name, type_args = EMPTY_TYPE_ARGS)
|
|
26
|
+
raise ArgumentError, "name must be a String, got #{name.class}" unless name.is_a?(String)
|
|
27
|
+
|
|
28
|
+
@name = name.frozen? ? name : name.dup.freeze
|
|
29
|
+
@type_args = type_args.frozen? ? type_args : type_args.dup.freeze
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def describe(verbosity = :short)
|
|
34
|
+
return name if type_args.empty?
|
|
35
|
+
|
|
36
|
+
"#{name}[#{type_args.map { |arg| arg.describe(verbosity) }.join(', ')}]"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A facet never reaches erasure on its own (`Dynamic#erase_to_rbs` is `untyped`); spelled as RBS for a
|
|
40
|
+
# caller that erases the carrier directly.
|
|
41
|
+
def erase_to_rbs
|
|
42
|
+
return name if type_args.empty?
|
|
43
|
+
|
|
44
|
+
"#{name}[#{type_args.map(&:erase_to_rbs).join(', ')}]"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
include Rigor::Type::PlainLattice
|
|
48
|
+
|
|
49
|
+
include Rigor::Type::AcceptanceRouter
|
|
50
|
+
|
|
51
|
+
include Rigor::ValueSemantics
|
|
52
|
+
|
|
53
|
+
value_fields :name, :type_args
|
|
54
|
+
|
|
55
|
+
def inspect
|
|
56
|
+
"#<Rigor::Type::Interface #{describe(:short)}>"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/rigor/type.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "type/union"
|
|
|
28
28
|
require_relative "type/difference"
|
|
29
29
|
require_relative "type/refined"
|
|
30
30
|
require_relative "type/intersection"
|
|
31
|
+
require_relative "type/interface"
|
|
31
32
|
require_relative "type/app"
|
|
32
33
|
require_relative "type/result"
|
|
33
34
|
require_relative "type/maybe"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeOfMemo
|
|
5
|
+
# G1's stage-4 form (ADR-129 WD5), mixed into {TypeOfMemo}'s singleton: on a would-be hit with
|
|
6
|
+
# `TypeOfMemo.enable!(serve: true)`, the memo serves the entry and checks it against a recomputation. For specs
|
|
7
|
+
# and audit tools; the CLI's `RIGOR_TYPE_OF_SHADOW=1` never serves.
|
|
8
|
+
module Serving
|
|
9
|
+
EMPTY = [].freeze
|
|
10
|
+
private_constant :EMPTY
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# G1's stage-4 form (ADR-129 WD5) on one would-be hit:
|
|
15
|
+
# 1. recompute on the same scope objects with the memo suspended, in a capture and a priors window, and record
|
|
16
|
+
# the final state of every entry either side touches; a recomputation that touched a Resource is a mismatch
|
|
17
|
+
# and its answer stands;
|
|
18
|
+
# 2. undo the recomputation's Table writes and drop its journal records, which the served entry replaces;
|
|
19
|
+
# 3. serve the entry: validate its reads, merge them into the enclosing captures, replay its writes, and compare
|
|
20
|
+
# the touched entries and the type with the recorded ones. A recomputation that raises where an entry exists
|
|
21
|
+
# is a mismatch too.
|
|
22
|
+
def serve_hit(state, table, phase, scope, node, entry, &)
|
|
23
|
+
stored, reads, writes = entry.is_a?(Array) ? entry : [entry, EMPTY, EMPTY]
|
|
24
|
+
count(phase, :checked)
|
|
25
|
+
mark = state.journal_size
|
|
26
|
+
fresh, priors, refused = recompute(state, table, phase, scope, node, stored, &)
|
|
27
|
+
if refused
|
|
28
|
+
judge(state, phase, scope, node, stored, fresh, writes, priors, refused)
|
|
29
|
+
return fresh
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
serve(state, phase, scope, node, stored, fresh, [reads, writes, priors, mark])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Steps 2 and 3 of {#serve_hit}. An error in them (a replay into a store the entry outlived) is a mismatch,
|
|
36
|
+
# counted rather than raised into the typing path's rescues. The recomputation's type is answered; the tables may
|
|
37
|
+
# hold a partial replay by then, which the counted mismatch already fails.
|
|
38
|
+
def serve(state, phase, scope, node, stored, fresh, slices)
|
|
39
|
+
reads, writes, priors, mark = slices
|
|
40
|
+
live = touched(writes, priors).map { |slot, key| [slot, key, state.entry(slot, key)] }
|
|
41
|
+
state.undo(priors)
|
|
42
|
+
state.truncate_journal(mark)
|
|
43
|
+
kinds = []
|
|
44
|
+
kinds << :revalidate if state.stale_read(reads)
|
|
45
|
+
state.merge_reads(reads)
|
|
46
|
+
state.replay(writes)
|
|
47
|
+
served(state, phase, scope, node, kinds, stored, fresh, live)
|
|
48
|
+
stored
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
report(phase, scope, node, [:serve_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
|
|
51
|
+
fresh
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def recompute(state, table, phase, scope, node, stored)
|
|
55
|
+
state.open_capture
|
|
56
|
+
state.open_priors
|
|
57
|
+
table.suspended += 1
|
|
58
|
+
returned = false
|
|
59
|
+
begin
|
|
60
|
+
fresh = yield
|
|
61
|
+
returned = true
|
|
62
|
+
ensure
|
|
63
|
+
table.suspended -= 1
|
|
64
|
+
priors = state.close_priors
|
|
65
|
+
refused, = state.close_capture
|
|
66
|
+
report(phase, scope, node, [:raised], stored, nil, []) unless returned
|
|
67
|
+
end
|
|
68
|
+
[fresh, priors, refused]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Every entry the served writes or the recomputation's writes touch, each once.
|
|
72
|
+
def touched(writes, priors)
|
|
73
|
+
seen = TypingState::KeyIndex.new
|
|
74
|
+
keys = []
|
|
75
|
+
[writes, priors].each do |slice|
|
|
76
|
+
slice.each_slice(3) do |slot, key, _|
|
|
77
|
+
next if seen.key?(slot, key)
|
|
78
|
+
|
|
79
|
+
seen.store(slot, key, true)
|
|
80
|
+
keys << [slot, key]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
keys
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def served(state, phase, scope, node, kinds, stored, fresh, live) # rubocop:disable Metrics/ParameterLists
|
|
87
|
+
kinds << :type unless same?(fresh, stored)
|
|
88
|
+
diffs = live.filter_map do |slot, key, want|
|
|
89
|
+
now = state.entry(slot, key)
|
|
90
|
+
[slot, key, want, now] unless same?(want, now)
|
|
91
|
+
end
|
|
92
|
+
kinds << :table unless diffs.empty?
|
|
93
|
+
report(phase, scope, node, kinds, stored, fresh, diffs) unless kinds.empty?
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
report(phase, scope, node, [:judge_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|