rigortype 0.4.1 → 0.4.3
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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -31,6 +31,7 @@ require_relative "match_rebinding"
|
|
|
31
31
|
require_relative "element_read_widening"
|
|
32
32
|
require_relative "hash_lookup_mutation"
|
|
33
33
|
require_relative "indexed_narrowing"
|
|
34
|
+
require_relative "key_presence_guard"
|
|
34
35
|
require_relative "index_write_widening"
|
|
35
36
|
require_relative "method_dispatcher"
|
|
36
37
|
require_relative "method_parameter_binder"
|
|
@@ -41,8 +42,10 @@ require_relative "operand_effects"
|
|
|
41
42
|
require_relative "operand_walk"
|
|
42
43
|
require_relative "optimistic_origin"
|
|
43
44
|
require_relative "return_barrier"
|
|
45
|
+
require_relative "repeated_or_writes"
|
|
44
46
|
require_relative "rewrite_mutation"
|
|
45
47
|
require_relative "unknown_store_widening"
|
|
48
|
+
require_relative "untyped_rejoin"
|
|
46
49
|
require_relative "version_guard"
|
|
47
50
|
|
|
48
51
|
module Rigor
|
|
@@ -277,11 +280,15 @@ module Rigor
|
|
|
277
280
|
# a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
|
|
278
281
|
# being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
|
|
279
282
|
# the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
|
|
283
|
+
# @param widen_clauses — issue #1231: when true (and a recorder is installed), a rescue or `ensure` clause
|
|
284
|
+
# records its per-node entry scopes under {#widen_clause_entry}'s widening. The check path sets it; every
|
|
285
|
+
# other reader of the index (parameter inference, sig-gen, the scanners) keeps the clause's plain entry.
|
|
280
286
|
def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
|
|
281
287
|
lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
|
|
282
288
|
operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
|
|
283
|
-
opaque_class: false)
|
|
289
|
+
opaque_class: false, widen_clauses: false)
|
|
284
290
|
@scope = scope
|
|
291
|
+
@widen_clauses = widen_clauses
|
|
285
292
|
@opaque_class = opaque_class
|
|
286
293
|
@tracer = tracer
|
|
287
294
|
@on_enter = on_enter
|
|
@@ -352,7 +359,7 @@ module Rigor
|
|
|
352
359
|
@on_enter&.call(node, @scope)
|
|
353
360
|
|
|
354
361
|
handler = HANDLERS[node.class]
|
|
355
|
-
return forget_implicit_call_guards(node, send(handler, node)) if handler
|
|
362
|
+
return forget_aliased_key_guards(node, forget_implicit_call_guards(node, send(handler, node))) if handler
|
|
356
363
|
|
|
357
364
|
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
358
365
|
# observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
|
|
@@ -373,6 +380,16 @@ module Rigor
|
|
|
373
380
|
[type, after.forget_guard_narrowings]
|
|
374
381
|
end
|
|
375
382
|
|
|
383
|
+
# Issue #1703 — a write that stores a `key?` guard's receiver or key root elsewhere (`g = h`) aliases it, so
|
|
384
|
+
# the guard ends ({KeyPresenceGuard.invalidate_after_write}).
|
|
385
|
+
def forget_aliased_key_guards(node, result)
|
|
386
|
+
type, after = result
|
|
387
|
+
return result unless after.is_a?(Scope)
|
|
388
|
+
|
|
389
|
+
guarded = KeyPresenceGuard.invalidate_after_write(node, after)
|
|
390
|
+
guarded.equal?(after) ? result : [type, guarded]
|
|
391
|
+
end
|
|
392
|
+
|
|
376
393
|
# Issue #1359 — the nodes whose handler runs their parts in order, each from the scope the parts before it
|
|
377
394
|
# left, so a `$_` reader among them forgets it where it runs ({LastLine}) and a read before it keeps the
|
|
378
395
|
# narrowing.
|
|
@@ -473,9 +490,10 @@ module Rigor
|
|
|
473
490
|
# `opts[k] ||= raise KeyError` is a guard, never `bot`, and `@flags[n] ||= false` is `true` after an
|
|
474
491
|
# `@flags[n] = true` elsewhere, never provably `false`.
|
|
475
492
|
#
|
|
476
|
-
# Nor is a site
|
|
477
|
-
#
|
|
478
|
-
# stored rather than an
|
|
493
|
+
# Nor is a marked site (`Scope#repeated_or_write?`, {RepeatedOrWrites}): a block-return pass, and this
|
|
494
|
+
# evaluator's statement and write-back passes ({#with_repeated_or_write_marks}), type every run of a repeating
|
|
495
|
+
# body from one entry scope, so its slot's gradual type may be what an EARLIER run stored rather than an
|
|
496
|
+
# absence of evidence.
|
|
479
497
|
def index_compound_write_value(node)
|
|
480
498
|
return index_write_stored_type(node, scope) unless node.is_a?(Prism::IndexOrWriteNode)
|
|
481
499
|
|
|
@@ -1169,7 +1187,8 @@ module Rigor
|
|
|
1169
1187
|
|
|
1170
1188
|
[
|
|
1171
1189
|
Type::Combinator.union(then_type, else_type),
|
|
1172
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1190
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1191
|
+
[node.statements, node.subsequent])
|
|
1173
1192
|
]
|
|
1174
1193
|
end
|
|
1175
1194
|
|
|
@@ -1204,7 +1223,8 @@ module Rigor
|
|
|
1204
1223
|
|
|
1205
1224
|
[
|
|
1206
1225
|
Type::Combinator.union(then_type, else_type),
|
|
1207
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1226
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1227
|
+
[node.statements, node.else_clause])
|
|
1208
1228
|
]
|
|
1209
1229
|
end
|
|
1210
1230
|
|
|
@@ -1275,7 +1295,7 @@ module Rigor
|
|
|
1275
1295
|
subject_type, post_pred = node.predicate ? sub_eval(node.predicate, scope) : [nil, scope]
|
|
1276
1296
|
branch_results, falsey_scope = eval_case_when_branches(subject_type, node.predicate, node.conditions, post_pred)
|
|
1277
1297
|
if pattern_case_matches_every_path?(node, branch_results)
|
|
1278
|
-
return unmatched_pattern_result(branch_results, node.conditions)
|
|
1298
|
+
return unmatched_pattern_result(branch_results, node.conditions, post_pred)
|
|
1279
1299
|
end
|
|
1280
1300
|
|
|
1281
1301
|
else_result = eval_case_else(node.else_clause, falsey_scope)
|
|
@@ -1284,7 +1304,7 @@ module Rigor
|
|
|
1284
1304
|
branch_nodes = [*node.conditions, node.else_clause]
|
|
1285
1305
|
[
|
|
1286
1306
|
Type::Combinator.union(*all_results.map(&:first)),
|
|
1287
|
-
join_case_branch_scopes(all_results, branch_nodes)
|
|
1307
|
+
join_case_branch_scopes(all_results, branch_nodes, post_pred)
|
|
1288
1308
|
]
|
|
1289
1309
|
end
|
|
1290
1310
|
|
|
@@ -1299,8 +1319,9 @@ module Rigor
|
|
|
1299
1319
|
node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil? && !branch_results.empty?
|
|
1300
1320
|
end
|
|
1301
1321
|
|
|
1302
|
-
def unmatched_pattern_result(branch_results, branch_nodes)
|
|
1303
|
-
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1322
|
+
def unmatched_pattern_result(branch_results, branch_nodes, entry)
|
|
1323
|
+
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1324
|
+
join_case_branch_scopes(branch_results, branch_nodes, entry)]
|
|
1304
1325
|
end
|
|
1305
1326
|
|
|
1306
1327
|
# Joins the post-scopes of every `when`/`in`/`else` branch, dropping the scope of any branch that terminates
|
|
@@ -1309,15 +1330,16 @@ module Rigor
|
|
|
1309
1330
|
# `branch_terminates?` rule `eval_if`/`eval_unless` already apply to the if/else merge: e.g. `case x; when 1 then
|
|
1310
1331
|
# v="a"; when 2 then v="b"; else raise; end` keeps `v: "a" | "b"` instead of `... | nil`. When every branch
|
|
1311
1332
|
# terminates the merge is itself unreachable; fall back to the full join so the continuation scope stays
|
|
1312
|
-
# well-formed.
|
|
1313
|
-
|
|
1333
|
+
# well-formed. A local the clauses only narrowed from `Dynamic[top]` re-joins to it ({UntypedRejoin}, issue
|
|
1334
|
+
# #1840), read against the post-predicate `entry`.
|
|
1335
|
+
def join_case_branch_scopes(results, nodes, entry)
|
|
1314
1336
|
live = []
|
|
1315
1337
|
results.each_with_index do |(type, branch_scope), i|
|
|
1316
1338
|
live << branch_scope unless branch_terminates?(nodes[i], type)
|
|
1317
1339
|
end
|
|
1318
1340
|
|
|
1319
1341
|
live = results.map(&:last) if live.empty?
|
|
1320
|
-
reduce_scopes_with_nil_injection(live)
|
|
1342
|
+
UntypedRejoin.apply(reduce_scopes_with_nil_injection(live), entry, nodes)
|
|
1321
1343
|
end
|
|
1322
1344
|
|
|
1323
1345
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
@@ -1466,7 +1488,8 @@ module Rigor
|
|
|
1466
1488
|
# prefix of the body, and so reads it forgotten whenever the body may set it.
|
|
1467
1489
|
entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
|
|
1468
1490
|
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge)
|
|
1469
|
-
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
|
|
1491
|
+
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
|
|
1492
|
+
edge: edge, writes: rescue_writes(node, edge))
|
|
1470
1493
|
|
|
1471
1494
|
# B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
|
|
1472
1495
|
# primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
|
|
@@ -1485,13 +1508,132 @@ module Rigor
|
|
|
1485
1508
|
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
|
|
1486
1509
|
end
|
|
1487
1510
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1511
|
+
[exit_type, node.ensure_clause ? eval_begin_ensure(node, exit_scope, jump_marks) : exit_scope]
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1515
|
+
# `jump_marks` carried through it.
|
|
1516
|
+
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1517
|
+
carry_jumps_through_ensure(node.ensure_clause, jump_marks)
|
|
1518
|
+
eval_widened_clause(node.ensure_clause, node.ensure_clause.statements, exit_scope, ensure_writes(node)).last
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
1522
|
+
# body, its `else` or a rescue clause raised as well as after they finished, so a clause reads each local or
|
|
1523
|
+
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1524
|
+
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1525
|
+
# raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
|
|
1526
|
+
# deliberately imprecisely. Each name the clause mentions ({#clause_mentions}) that its entry binds and those
|
|
1527
|
+
# clauses write, outside a nested `def` or class body, reads as `Dynamic[top]`, which reports nothing. A plain
|
|
1528
|
+
# join would read `x` as `1?` in `x = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a
|
|
1529
|
+
# `Dynamic` that keeps the join as its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member
|
|
1530
|
+
# for the next call to report. A name written inside a nested `ensure` clause, which also runs on the way out of
|
|
1531
|
+
# a raise, is covered by the same floor.
|
|
1532
|
+
#
|
|
1533
|
+
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1534
|
+
def rescue_writes(node, edge)
|
|
1535
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil? && widening_recorded?
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1539
|
+
def ensure_writes(node)
|
|
1540
|
+
return nil unless widening_recorded?
|
|
1541
|
+
|
|
1542
|
+
sources = [node.statements, node.else_clause]
|
|
1543
|
+
current = node.rescue_clause
|
|
1544
|
+
while current
|
|
1545
|
+
sources << current
|
|
1546
|
+
current = current.subsequent
|
|
1492
1547
|
end
|
|
1548
|
+
clause_writes(sources)
|
|
1549
|
+
end
|
|
1493
1550
|
|
|
1494
|
-
|
|
1551
|
+
# The local and instance variable names `sources` write, a nested block's included (it may write an outer local),
|
|
1552
|
+
# or nil for none. A `def` or class body writes nothing here.
|
|
1553
|
+
def clause_writes(sources)
|
|
1554
|
+
writes = nil
|
|
1555
|
+
sources.each { |source| writes = collect_clause_names(source, CLAUSE_WRITE_NODES, nil, writes) if source }
|
|
1556
|
+
writes
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
CLAUSE_WRITE_NODES = Set[
|
|
1560
|
+
*CapturedLocals::LOCAL_WRITE_NODES,
|
|
1561
|
+
Prism::InstanceVariableWriteNode,
|
|
1562
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
1563
|
+
Prism::InstanceVariableOrWriteNode,
|
|
1564
|
+
Prism::InstanceVariableAndWriteNode,
|
|
1565
|
+
Prism::InstanceVariableTargetNode
|
|
1566
|
+
].freeze
|
|
1567
|
+
private_constant :CLAUSE_WRITE_NODES
|
|
1568
|
+
|
|
1569
|
+
CLAUSE_MENTION_NODES = Set[
|
|
1570
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *CLAUSE_WRITE_NODES
|
|
1571
|
+
].freeze
|
|
1572
|
+
private_constant :CLAUSE_MENTION_NODES
|
|
1573
|
+
|
|
1574
|
+
# The names of the `node_classes` nodes under `node` (restricted to `among` when given) added to `found`, outside
|
|
1575
|
+
# a nested `def` or class body.
|
|
1576
|
+
def collect_clause_names(node, node_classes, among, found)
|
|
1577
|
+
return found if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
1578
|
+
|
|
1579
|
+
if node_classes.include?(node.class) && (among.nil? || among.include?(node.name))
|
|
1580
|
+
(found ||= Set.new) << node.name
|
|
1581
|
+
end
|
|
1582
|
+
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1583
|
+
found
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
# `clause_scope` with each name of `writes` that `statements` mentions, and `clause_scope` binds, rebound to
|
|
1587
|
+
# `Dynamic[top]`, or nil when none changes.
|
|
1588
|
+
def widen_clause_entry(clause_scope, statements, writes)
|
|
1589
|
+
return nil if writes.nil? || statements.nil? || !widening_recorded?
|
|
1590
|
+
|
|
1591
|
+
names = collect_clause_names(statements, CLAUSE_MENTION_NODES, writes, nil)
|
|
1592
|
+
return nil if names.nil?
|
|
1593
|
+
|
|
1594
|
+
untyped = Type::Combinator.untyped
|
|
1595
|
+
widened = names.reduce(clause_scope) do |acc, name|
|
|
1596
|
+
kind = name.start_with?("@") ? :ivar : :local
|
|
1597
|
+
current = clause_scope.public_send(VAR_KIND_GETTERS.fetch(kind), name)
|
|
1598
|
+
current.nil? || current == untyped ? acc : rebind_variable(acc, kind, name, untyped)
|
|
1599
|
+
end
|
|
1600
|
+
widened.equal?(clause_scope) ? nil : widened
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1603
|
+
# Whether a widened pass would record anything: only the check path asks for one ({#initialize}'s
|
|
1604
|
+
# `widen_clauses`), and a pass nothing records is skipped, so a nested clause inside an unrecorded pass is
|
|
1605
|
+
# evaluated once.
|
|
1606
|
+
def widening_recorded?
|
|
1607
|
+
@widen_clauses && !(@on_enter.nil? && @operand_recorder.nil?)
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
# `[type, scope]` of a rescue or `ensure` clause entered with `clause_scope`. When {#widen_clause_entry} widens
|
|
1611
|
+
# it, the clause is evaluated twice: under the widened entry for the per-node index, which is what the check
|
|
1612
|
+
# rules read, and under `clause_scope` without recording, for the value and the scope the code after the
|
|
1613
|
+
# `begin` continues from. The widened pass collects no `return`, `next`, `break` or retry-edge scope, so the
|
|
1614
|
+
# widening reaches neither an inferred return nor a loop: it can only take diagnostics away inside the clause.
|
|
1615
|
+
def eval_widened_clause(node, statements, clause_scope, writes)
|
|
1616
|
+
widened = widen_clause_entry(clause_scope, statements, writes)
|
|
1617
|
+
return sub_eval(node, clause_scope) unless widened
|
|
1618
|
+
|
|
1619
|
+
eval_widened_pass(node, widened)
|
|
1620
|
+
sub_eval(node, clause_scope, **UNRECORDED)
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
# The recorded pass over a widened clause: it reaches no `return`, `next`, `break` or retry-frame sink.
|
|
1624
|
+
def eval_widened_pass(node, widened)
|
|
1625
|
+
return if node.nil?
|
|
1626
|
+
|
|
1627
|
+
without_jump_sinks { sub_eval(node, widened, next_scope_sink: nil) }
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
1631
|
+
def without_jump_sinks
|
|
1632
|
+
previous = CLAUSE_SINK_KEYS.map { |key| Thread.current[key] }
|
|
1633
|
+
CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
|
|
1634
|
+
yield
|
|
1635
|
+
ensure
|
|
1636
|
+
CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
|
|
1495
1637
|
end
|
|
1496
1638
|
|
|
1497
1639
|
# The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
|
|
@@ -1589,6 +1731,10 @@ module Rigor
|
|
|
1589
1731
|
RETRY_FRAMES_KEY = :rigor_retry_frames
|
|
1590
1732
|
private_constant :RETRY_FRAMES_KEY
|
|
1591
1733
|
|
|
1734
|
+
# The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
|
|
1735
|
+
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1736
|
+
private_constant :CLAUSE_SINK_KEYS
|
|
1737
|
+
|
|
1592
1738
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1593
1739
|
# no `retry` targets.
|
|
1594
1740
|
def retry_edge_for(node)
|
|
@@ -1865,13 +2011,23 @@ module Rigor
|
|
|
1865
2011
|
# each arm holding one. The latter still carries a rebind the `retry` itself does not see: a write an `ensure`
|
|
1866
2012
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
1867
2013
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
1868
|
-
|
|
2014
|
+
#
|
|
2015
|
+
# Issue #1231 — `writes` widens the scope each clause enters with, as {#eval_widened_clause} does, before its
|
|
2016
|
+
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2017
|
+
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2018
|
+
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
1869
2019
|
on_enter = edge ? retry_scope_recorder(edge) : @on_enter
|
|
1870
2020
|
results = []
|
|
1871
2021
|
current = rescue_node
|
|
1872
2022
|
while current
|
|
2023
|
+
widened = widen_clause_entry(entry_scope, current.statements, writes)
|
|
2024
|
+
eval_widened_pass(current.statements, bind_rescue_reference(current, widened)) if widened
|
|
1873
2025
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
1874
|
-
arm =
|
|
2026
|
+
arm = if widened
|
|
2027
|
+
sub_eval(current.statements, rescue_scope, **UNRECORDED)
|
|
2028
|
+
else
|
|
2029
|
+
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2030
|
+
end
|
|
1875
2031
|
edge.retry_scopes << arm.last if edge&.retrying_arms&.include?(current)
|
|
1876
2032
|
results << [arm, current]
|
|
1877
2033
|
current = current.subsequent
|
|
@@ -1941,7 +2097,7 @@ module Rigor
|
|
|
1941
2097
|
# the single-pass join before returning.
|
|
1942
2098
|
if names.empty?
|
|
1943
2099
|
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
1944
|
-
return [Type::Combinator.constant_of(nil),
|
|
2100
|
+
return [Type::Combinator.constant_of(nil), loop_exit(node, post_pred, fast)]
|
|
1945
2101
|
end
|
|
1946
2102
|
|
|
1947
2103
|
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
@@ -1950,7 +2106,7 @@ module Rigor
|
|
|
1950
2106
|
# :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
|
|
1951
2107
|
# un-narrowed post-predicate scope, so its arms stand in for that first iteration.
|
|
1952
2108
|
post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
|
|
1953
|
-
post_loop =
|
|
2109
|
+
post_loop = loop_exit(node, post_pred, post_loop)
|
|
1954
2110
|
[Type::Combinator.constant_of(nil), post_loop]
|
|
1955
2111
|
end
|
|
1956
2112
|
|
|
@@ -2037,6 +2193,12 @@ module Rigor
|
|
|
2037
2193
|
join_break_scopes(post_loop, arms, names)
|
|
2038
2194
|
end
|
|
2039
2195
|
|
|
2196
|
+
# A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
|
|
2197
|
+
# ({UntypedRejoin}, issue #1840), then the exit edge narrows.
|
|
2198
|
+
def loop_exit(node, post_pred, post_loop)
|
|
2199
|
+
narrow_loop_exit_edge(node, UntypedRejoin.apply(post_loop, post_pred, [node.predicate, node.statements]))
|
|
2200
|
+
end
|
|
2201
|
+
|
|
2040
2202
|
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
|
|
2041
2203
|
# exit edge: `while` exits when `pred` is falsey, `until` when `pred` is truthy. So after the loop the
|
|
2042
2204
|
# predicate-assignment target carries the exit polarity — `until line = io.gets; …; end; line.foo` reads `line`
|
|
@@ -2368,6 +2530,12 @@ module Rigor
|
|
|
2368
2530
|
stores
|
|
2369
2531
|
end
|
|
2370
2532
|
|
|
2533
|
+
# A `for` loop's continuation, with a local the body only narrowed from `Dynamic[top]` re-joined to it
|
|
2534
|
+
# ({UntypedRejoin}, issue #1840). The index is written on every iteration.
|
|
2535
|
+
def for_exit(node, post_coll, continuation)
|
|
2536
|
+
UntypedRejoin.apply(continuation, post_coll, [node.index, node.statements])
|
|
2537
|
+
end
|
|
2538
|
+
|
|
2371
2539
|
# `for index in collection; body; end`. Unlike `each {}` blocks, `for` does NOT create a new variable scope: the
|
|
2372
2540
|
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
2373
2541
|
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
@@ -2394,7 +2562,7 @@ module Rigor
|
|
|
2394
2562
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
2395
2563
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
2396
2564
|
continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
|
|
2397
|
-
[Type::Combinator.constant_of(nil), continuation]
|
|
2565
|
+
[Type::Combinator.constant_of(nil), for_exit(node, post_coll, continuation)]
|
|
2398
2566
|
end
|
|
2399
2567
|
|
|
2400
2568
|
# Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
|
|
@@ -2554,7 +2722,7 @@ module Rigor
|
|
|
2554
2722
|
|
|
2555
2723
|
# A dead RHS is still evaluated and its scope still joins, so a write inside it nil-injects exactly as
|
|
2556
2724
|
# before; only its value is dropped, because it cannot be the value of the expression.
|
|
2557
|
-
joined_scope = join_with_nil_injection(left_scope, right_scope)
|
|
2725
|
+
joined_scope = UntypedRejoin.apply(join_with_nil_injection(left_scope, right_scope), left_scope, [node.right])
|
|
2558
2726
|
type = skipped_type
|
|
2559
2727
|
type = Type::Combinator.union(skipped_type, right_type) unless right_operand_dead?(node, left_type, left_scope)
|
|
2560
2728
|
return [type, joined_scope] unless edges
|
|
@@ -2994,13 +3162,15 @@ module Rigor
|
|
|
2994
3162
|
# outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
|
|
2995
3163
|
# calls against the root variable invalidate the chain.
|
|
2996
3164
|
post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
|
|
3165
|
+
# Issue #1703 — a non-literal-key `key?` guard the call may break ({KeyPresenceGuard.breaks?}).
|
|
3166
|
+
post_scope = KeyPresenceGuard.invalidate_after_call(node, post_scope)
|
|
2997
3167
|
# B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
|
|
2998
3168
|
# of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
|
|
2999
3169
|
# binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
|
|
3000
3170
|
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
|
|
3001
3171
|
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
3002
3172
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
|
|
3003
|
-
# C1 — regex match-data globals (`$~`, `$1..$
|
|
3173
|
+
# C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
|
|
3004
3174
|
# later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
|
|
3005
3175
|
# only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
|
|
3006
3176
|
# known receiver — `$3.to_i`, `year < 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)`
|
|
@@ -3529,7 +3699,9 @@ module Rigor
|
|
|
3529
3699
|
end
|
|
3530
3700
|
|
|
3531
3701
|
def enter_call_block(node, block)
|
|
3532
|
-
block_entry = narrow_define_method_block_self(
|
|
3702
|
+
block_entry = narrow_define_method_block_self(
|
|
3703
|
+
node, with_repeated_or_write_marks(node, block, repeating_block_entry(node, block))
|
|
3704
|
+
)
|
|
3533
3705
|
# #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
|
|
3534
3706
|
# semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
|
|
3535
3707
|
# and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
|
|
@@ -3559,15 +3731,19 @@ module Rigor
|
|
|
3559
3731
|
# does not type as a class object (a gem class with no RBS) keeps the name as written: the body is still some
|
|
3560
3732
|
# class's body, and leaving it on the enclosing `self` made a `refine` at the file's top level report every
|
|
3561
3733
|
# implicit-self call in it as `call.unresolved-toplevel`.
|
|
3734
|
+
#
|
|
3735
|
+
# Issue #1689 — a `refine` call the file's {InEffectRefinements} places in a class body is the class's own
|
|
3736
|
+
# method, not `Module#refine`, so its block is entered as any other block.
|
|
3562
3737
|
def refined_class_context(node)
|
|
3563
3738
|
target = ScopeIndexer.refine_target(node)
|
|
3564
3739
|
return nil if target.nil?
|
|
3565
3740
|
|
|
3566
|
-
|
|
3567
|
-
|
|
3741
|
+
return nil if InEffectRefinements.class_body_refine?(scope, node)
|
|
3742
|
+
|
|
3743
|
+
name = ScopeIndexer.refined_class_name(target, scope)
|
|
3568
3744
|
return nil if name.nil?
|
|
3569
3745
|
|
|
3570
|
-
[ClassFrame.new(name: name
|
|
3746
|
+
[ClassFrame.new(name: name, singleton: false, refinement: true)]
|
|
3571
3747
|
end
|
|
3572
3748
|
|
|
3573
3749
|
# The block calls whose body `return` leaves only the block ({ReturnBarrier.block_call?}). Like a `->` body
|
|
@@ -3601,7 +3777,9 @@ module Rigor
|
|
|
3601
3777
|
narrowed = DefineMethodBlockSelf.narrow_self_type_for(
|
|
3602
3778
|
scope: scope, call_node: call_node
|
|
3603
3779
|
)
|
|
3604
|
-
|
|
3780
|
+
return block_entry unless narrowed
|
|
3781
|
+
|
|
3782
|
+
block_entry.with_block_self_type(narrowed, keeps_unknown: scope.block_self_narrowing_unknown?(call_node))
|
|
3605
3783
|
end
|
|
3606
3784
|
|
|
3607
3785
|
# Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
|
|
@@ -3616,8 +3794,17 @@ module Rigor
|
|
|
3616
3794
|
# record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
|
|
3617
3795
|
# frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
|
|
3618
3796
|
# divergence that makes the chain a separate record rather than a view of the frame stack.
|
|
3797
|
+
#
|
|
3798
|
+
# Issue #1759 — a `refine X do … end` block runs on the refinement module, not on X, which Rigor does not
|
|
3799
|
+
# model: the body keeps X's frame and singleton so its `def`s and implicit-self calls resolve as before, but
|
|
3800
|
+
# carries the unknown-`self` mark ({Scope#block_self_unknown?}), so an explicit `self.import_methods(M)` is
|
|
3801
|
+
# not checked against `singleton(X)`. The indexer leaves a value-position refine block unknown the same way.
|
|
3619
3802
|
def enter_meta_class_body(block, block_entry, class_context)
|
|
3620
|
-
|
|
3803
|
+
# Issue #1717 — the body runs on the new class whatever encloses it, so its `self` is known.
|
|
3804
|
+
entry = block_entry.with_block_self_type(self_type_for_class_body(class_context),
|
|
3805
|
+
keeps_unknown: class_context.last.refinement &&
|
|
3806
|
+
Scope::REFINEMENT_BLOCK_SELF)
|
|
3807
|
+
.with_singleton_class_body(false)
|
|
3621
3808
|
sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
|
|
3622
3809
|
end
|
|
3623
3810
|
|
|
@@ -4114,7 +4301,7 @@ module Rigor
|
|
|
4114
4301
|
# The `break` scopes of one unrecorded pass from `converged` — for a capped fixpoint, whose widened binding no
|
|
4115
4302
|
# pass has run from.
|
|
4116
4303
|
def converged_break_arms(call_node, block, converged, targets)
|
|
4117
|
-
entry = block_pass_entry(call_node, block, converged)
|
|
4304
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, converged))
|
|
4118
4305
|
sink, = collect_break_scopes { sub_eval(block, entry, **UNRECORDED) }
|
|
4119
4306
|
targeted_scopes(sink, targets)
|
|
4120
4307
|
end
|
|
@@ -4914,7 +5101,8 @@ module Rigor
|
|
|
4914
5101
|
# params / `;`-locals re-bound as usual) and returns the per-name exit binding for `names`. Used as the
|
|
4915
5102
|
# `BodyFixpoint` body-evaluator.
|
|
4916
5103
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
4917
|
-
|
|
5104
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5105
|
+
_type, exit_scope = sub_eval(block, entry)
|
|
4918
5106
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
4919
5107
|
end
|
|
4920
5108
|
|
|
@@ -4930,6 +5118,38 @@ module Rigor
|
|
|
4930
5118
|
end
|
|
4931
5119
|
end
|
|
4932
5120
|
|
|
5121
|
+
# Issue #1336 — `entry` with the index `||=` sites of a repeating block marked as the generic block-return pass
|
|
5122
|
+
# marks them ({RepeatedOrWrites}, `Scope#with_repeated_or_writes`). The statement pass and every ADR-56
|
|
5123
|
+
# write-back pass type the body from one entry scope just as that pass does, so a slot an earlier run stored
|
|
5124
|
+
# into reads as a lone `Dynamic` there too, and the memoizing reading ({#index_compound_write_value}) answered
|
|
5125
|
+
# one site's rvalue: `last = (cache[:k] ||= :f).itself` beside a `cache[:k] ||= :e` wrote `:f` alone back
|
|
5126
|
+
# into `last`, and `last == :e` folded always-falsey. A call that runs its block at most once marks nothing.
|
|
5127
|
+
def with_repeated_or_write_marks(call_node, block, entry)
|
|
5128
|
+
sites = repeated_or_write_sites(call_node, block)
|
|
5129
|
+
sites.empty? ? entry : entry.with_repeated_or_writes(sites)
|
|
5130
|
+
end
|
|
5131
|
+
|
|
5132
|
+
# The sites {#with_repeated_or_write_marks} lays, once per block: a body that spells no `||=` — nearly every
|
|
5133
|
+
# one — answers none without the repetition question.
|
|
5134
|
+
def repeated_or_write_sites(call_node, block)
|
|
5135
|
+
memo = (@repeated_or_write_sites ||= {}.compare_by_identity)
|
|
5136
|
+
return memo[block] if memo.key?(block)
|
|
5137
|
+
|
|
5138
|
+
memo[block] = compute_repeated_or_write_sites(call_node, block)
|
|
5139
|
+
end
|
|
5140
|
+
|
|
5141
|
+
def compute_repeated_or_write_sites(call_node, block)
|
|
5142
|
+
body = block.body
|
|
5143
|
+
return NO_REPEATED_OR_WRITES if body.nil? || !RepeatedOrWrites.may_hold_or_write?(body)
|
|
5144
|
+
return NO_REPEATED_OR_WRITES if repeating_block_class(call_node).nil?
|
|
5145
|
+
|
|
5146
|
+
stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
|
|
5147
|
+
RepeatedOrWrites.sites(block, stores, scope).at(nil)
|
|
5148
|
+
end
|
|
5149
|
+
|
|
5150
|
+
NO_REPEATED_OR_WRITES = [].freeze
|
|
5151
|
+
private_constant :NO_REPEATED_OR_WRITES
|
|
5152
|
+
|
|
4933
5153
|
NO_CAPTURE_BINDINGS = {}.freeze
|
|
4934
5154
|
private_constant :NO_CAPTURE_BINDINGS
|
|
4935
5155
|
|
|
@@ -5140,27 +5360,31 @@ module Rigor
|
|
|
5140
5360
|
# ({ExpressionTyper#block_body_self_narrowing}) already applies the same contract to block-return
|
|
5141
5361
|
# typing; without it here the recorded per-node scopes — what `dump_type`/`assert_type` and the
|
|
5142
5362
|
# survey read — keep the enclosing `self_type` and every DSL call inside stays `Dynamic[top]`.
|
|
5143
|
-
|
|
5144
|
-
|
|
5363
|
+
# Issue #1667 — the entry's `refinements:` are put in effect in the body alongside.
|
|
5364
|
+
match = call_node && macro_block_match(call_node)
|
|
5365
|
+
if match
|
|
5366
|
+
keeps_unknown = scope.block_self_narrowing_unknown?(call_node)
|
|
5367
|
+
scope_with_params = MacroBlockSelfType.apply(scope_with_params, match, keeps_unknown: keeps_unknown)
|
|
5368
|
+
end
|
|
5145
5369
|
block_local_names(block_node).reduce(scope_with_params) do |acc, name|
|
|
5146
5370
|
acc.with_local(name, Type::Combinator.constant_of(nil))
|
|
5147
5371
|
end
|
|
5148
5372
|
end
|
|
5149
5373
|
|
|
5150
|
-
# The
|
|
5374
|
+
# The ADR-16 `block_as_methods:` match for `call_node`, keyed on the explicit receiver's type, or on the
|
|
5151
5375
|
# current `self_type` for an implicit-self DSL call (the `params do` / `namespace do` shapes, whose
|
|
5152
5376
|
# receiver is the enclosing `Singleton[X]`). A miss leaves the entry scope as built — the false-
|
|
5153
5377
|
# positive-safe direction.
|
|
5154
|
-
def
|
|
5378
|
+
def macro_block_match(call_node)
|
|
5155
5379
|
receiver_type =
|
|
5156
5380
|
if call_node.receiver
|
|
5157
5381
|
explicit_receiver_type(call_node)
|
|
5158
5382
|
else
|
|
5159
|
-
scope
|
|
5383
|
+
MacroBlockSelfType.implicit_receiver_type(scope)
|
|
5160
5384
|
end
|
|
5161
5385
|
return nil if receiver_type.nil?
|
|
5162
5386
|
|
|
5163
|
-
MacroBlockSelfType.
|
|
5387
|
+
MacroBlockSelfType.match_for(
|
|
5164
5388
|
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
5165
5389
|
)
|
|
5166
5390
|
rescue StandardError
|
|
@@ -5193,7 +5417,8 @@ module Rigor
|
|
|
5193
5417
|
method_name: call_node.name,
|
|
5194
5418
|
arg_types: arg_types,
|
|
5195
5419
|
environment: scope.environment,
|
|
5196
|
-
scope: scope
|
|
5420
|
+
scope: scope,
|
|
5421
|
+
call_node: call_node
|
|
5197
5422
|
)
|
|
5198
5423
|
rescue StandardError
|
|
5199
5424
|
[]
|
|
@@ -5218,13 +5443,19 @@ module Rigor
|
|
|
5218
5443
|
fresh = build_fresh_body_scope
|
|
5219
5444
|
# Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
|
|
5220
5445
|
# singleton of a class nothing names, and its empty frame stack is not the top level.
|
|
5221
|
-
|
|
5222
|
-
|
|
5446
|
+
singleton_body = node.is_a?(Prism::SingletonClassNode)
|
|
5447
|
+
opaque_self ||= singleton_body && @opaque_class && new_context.empty?
|
|
5448
|
+
# Issue #1763 — a `class << self` in a `refine X` block (the refinement frame {#singleton_context_for} built)
|
|
5449
|
+
# opens the refinement module's singleton class, which Rigor does not model, as the `def`s in it already read
|
|
5450
|
+
# ({#self_type_for_method_body}); `Singleton[X]` there checked `self.m` and a local holding `self` against X's
|
|
5451
|
+
# class methods. A `class << X` names X's real singleton class and takes a frame without the flag.
|
|
5452
|
+
untyped_self = opaque_self || (singleton_body && new_context.last&.refinement)
|
|
5453
|
+
body_self = untyped_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
|
|
5223
5454
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
5224
5455
|
# Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
|
|
5225
5456
|
# carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
|
|
5226
5457
|
# `def` reached from this body clears it by starting from a fresh scope.
|
|
5227
|
-
fresh = fresh.with_singleton_class_body(
|
|
5458
|
+
fresh = fresh.with_singleton_class_body(singleton_body)
|
|
5228
5459
|
fresh = fresh.with_match_frame(node.body)
|
|
5229
5460
|
fresh = stamp_nesting(fresh, new_nesting)
|
|
5230
5461
|
sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
|
|
@@ -5254,7 +5485,7 @@ module Rigor
|
|
|
5254
5485
|
# then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
|
|
5255
5486
|
# class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
|
|
5256
5487
|
fresh = build_fresh_body_scope
|
|
5257
|
-
body_self = self_type_for_method_body(singleton: singleton)
|
|
5488
|
+
body_self = self_type_for_method_body(singleton: singleton, receiver: def_node.receiver)
|
|
5258
5489
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
5259
5490
|
# A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
|
|
5260
5491
|
# declaration that encloses it, which is what the evaluator is already carrying (#652).
|
|
@@ -5389,12 +5620,16 @@ module Rigor
|
|
|
5389
5620
|
# chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
|
|
5390
5621
|
# discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
|
|
5391
5622
|
# be dropped here by a missed per-field copy.
|
|
5623
|
+
#
|
|
5624
|
+
# Issue #1667 — a plugin-declared refined block's modules carry into a `def` or class body written in it: CRuby
|
|
5625
|
+
# defines the method with the block's cref, and a class body's cref inherits the outer one's refinements.
|
|
5392
5626
|
Scope.new(
|
|
5393
5627
|
environment: scope.environment,
|
|
5394
5628
|
locals: {}.freeze,
|
|
5395
5629
|
source_path: scope.source_path,
|
|
5396
5630
|
discovery: scope.discovery,
|
|
5397
|
-
dynamic_origins: scope.dynamic_origins
|
|
5631
|
+
dynamic_origins: scope.dynamic_origins,
|
|
5632
|
+
declared_refinements: scope.declared_refinements
|
|
5398
5633
|
)
|
|
5399
5634
|
end
|
|
5400
5635
|
|
|
@@ -5444,11 +5679,15 @@ module Rigor
|
|
|
5444
5679
|
# `Nominal[Foo]`.
|
|
5445
5680
|
#
|
|
5446
5681
|
# Returns nil for top-level defs that have no enclosing class.
|
|
5447
|
-
def self_type_for_method_body(singleton:)
|
|
5682
|
+
def self_type_for_method_body(singleton:, receiver: nil)
|
|
5448
5683
|
path = current_class_path
|
|
5449
5684
|
# Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
|
|
5450
5685
|
# read its body as the top level and its implicit-self calls as unresolved top-level calls.
|
|
5451
5686
|
return (Type::Combinator.untyped if @opaque_class) if path.nil?
|
|
5687
|
+
# Issue #1759 — a `def self.m` in a `refine X` block (or a `def m` in a `class << self` there) is a method
|
|
5688
|
+
# of the refinement module, which Rigor does not model; reading it as `Singleton[X]` checked its
|
|
5689
|
+
# explicit-self calls against X's class methods. A `def X.m` there does define X's class method.
|
|
5690
|
+
return Type::Combinator.untyped if singleton && @class_context.last.refinement && refinement_self_def?(receiver)
|
|
5452
5691
|
|
|
5453
5692
|
if singleton
|
|
5454
5693
|
Type::Combinator.singleton_of(path)
|
|
@@ -5457,6 +5696,10 @@ module Rigor
|
|
|
5457
5696
|
end
|
|
5458
5697
|
end
|
|
5459
5698
|
|
|
5699
|
+
# Issue #1759 — whether a singleton `def` under a refinement frame defines a method on the refinement module
|
|
5700
|
+
# (`def self.m`, or `def m` in a `class << self`) rather than on an explicitly named receiver (`def X.m`).
|
|
5701
|
+
def refinement_self_def?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
5702
|
+
|
|
5460
5703
|
def singleton_context_for(node)
|
|
5461
5704
|
case node.expression
|
|
5462
5705
|
when Prism::SelfNode
|
|
@@ -5464,7 +5707,7 @@ module Rigor
|
|
|
5464
5707
|
|
|
5465
5708
|
outer = @class_context[0..-2]
|
|
5466
5709
|
last = @class_context.last
|
|
5467
|
-
outer + [ClassFrame.new(name: last.name, singleton: true)]
|
|
5710
|
+
outer + [ClassFrame.new(name: last.name, singleton: true, refinement: last.refinement)]
|
|
5468
5711
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
5469
5712
|
target = singleton_constant_target(node.expression)
|
|
5470
5713
|
return @class_context unless target
|
|
@@ -5611,6 +5854,7 @@ module Rigor
|
|
|
5611
5854
|
class_context: class_context,
|
|
5612
5855
|
lexical_nesting: lexical_nesting,
|
|
5613
5856
|
converged_loop_recording: @converged_loop_recording,
|
|
5857
|
+
widen_clauses: @widen_clauses,
|
|
5614
5858
|
next_scope_sink: next_scope_sink,
|
|
5615
5859
|
operand_scope: operand_scope,
|
|
5616
5860
|
in_operand: in_operand,
|
|
@@ -5905,8 +6149,7 @@ module Rigor
|
|
|
5905
6149
|
# it is frozen, and the dispatcher already projects it (see `DataFolding`).
|
|
5906
6150
|
def struct_instance_projection(subject_type, method_name, scope, subject_node)
|
|
5907
6151
|
return nil unless subject_type.is_a?(Type::StructInstance)
|
|
5908
|
-
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node,
|
|
5909
|
-
scope)
|
|
6152
|
+
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node, scope)
|
|
5910
6153
|
|
|
5911
6154
|
case method_name
|
|
5912
6155
|
when :deconstruct then Type::Combinator.tuple_of(*subject_type.members.values)
|