rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -12,9 +12,12 @@ require_relative "method_dispatcher/constant_folding"
|
|
|
12
12
|
require_relative "method_dispatcher/literal_string_folding"
|
|
13
13
|
require_relative "method_dispatcher/shape_dispatch"
|
|
14
14
|
require_relative "method_dispatcher/data_folding"
|
|
15
|
+
require_relative "method_dispatcher/struct_folding"
|
|
15
16
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
16
17
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
18
|
+
require_relative "method_dispatcher/reduce_folding"
|
|
17
19
|
require_relative "method_dispatcher/block_folding"
|
|
20
|
+
require_relative "method_dispatcher/array_to_h_folding"
|
|
18
21
|
require_relative "method_dispatcher/file_folding"
|
|
19
22
|
require_relative "method_dispatcher/shellwords_folding"
|
|
20
23
|
require_relative "method_dispatcher/math_folding"
|
|
@@ -36,28 +39,15 @@ module Rigor
|
|
|
36
39
|
# callers (today only `ExpressionTyper`) own the fail-soft fallback
|
|
37
40
|
# and decide whether to record a `FallbackTracer` event.
|
|
38
41
|
#
|
|
39
|
-
#
|
|
42
|
+
# Tier order is documented inline in `resolve`; the precise-tier
|
|
43
|
+
# group is built from `PRECISE_TIERS_HEAD`, `STDLIB_SINGLETON_FOLDERS`,
|
|
44
|
+
# and `PRECISE_TIERS_TAIL`. `ShapeDispatch` runs above {RbsDispatch}
|
|
45
|
+
# so a precise per-position/per-key answer wins over the projected
|
|
46
|
+
# `Array#[]`/`Hash#fetch` RBS answer.
|
|
40
47
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# 2. {ShapeDispatch}: returns the precise element/value type for a
|
|
45
|
-
# curated catalogue of `Tuple`/`HashShape` element-access
|
|
46
|
-
# methods (`first`, `last`, `[]` with a static integer/key,
|
|
47
|
-
# `fetch`, `dig`, `size`/`length`/`count`). Slice 5 phase 2.
|
|
48
|
-
# 3. {RbsDispatch}: looks up the receiver's class in the RBS
|
|
49
|
-
# environment carried by the scope and translates the method's
|
|
50
|
-
# return type into a Rigor::Type. Slice 4.
|
|
51
|
-
#
|
|
52
|
-
# `ShapeDispatch` deliberately runs *above* {RbsDispatch} so the
|
|
53
|
-
# precise per-position/per-key answer wins over the projected
|
|
54
|
-
# `Array#[]`/`Hash#fetch` answer; it falls through (`nil`) when
|
|
55
|
-
# the call cannot be proved against the static shape, in which
|
|
56
|
-
# case the projection answer from {RbsDispatch} applies.
|
|
57
|
-
#
|
|
58
|
-
# The dispatcher's public signature reserves space for `block_type:`
|
|
59
|
-
# and ADR-2 plugin extensions (later slices), so call sites added
|
|
60
|
-
# now do not have to be rewritten when those tiers arrive.
|
|
48
|
+
# The `block_type:` and plugin contribution (`dynamic_return`) tiers
|
|
49
|
+
# landed in Slice 6 phase C and v0.1.1 Track 2 respectively; all
|
|
50
|
+
# call sites pass through `dispatch`/`resolve` unchanged.
|
|
61
51
|
module MethodDispatcher # rubocop:disable Metrics/ModuleLength
|
|
62
52
|
module_function
|
|
63
53
|
|
|
@@ -274,7 +264,14 @@ module Rigor
|
|
|
274
264
|
class_name, kind = discovered_method_lookup(receiver_type)
|
|
275
265
|
return nil if class_name.nil?
|
|
276
266
|
return nil unless scope.discovered_method?(class_name, method_name, kind)
|
|
267
|
+
# Decline when a re-typable body is recorded for the method, so the
|
|
268
|
+
# downstream `ExpressionTyper` inference tier can fold a precise
|
|
269
|
+
# return instead of collapsing to `Dynamic[top]` here — instance
|
|
270
|
+
# bodies via `user_def_for`, singleton bodies (`def self.x` /
|
|
271
|
+
# `module_function`) via `singleton_def_for` (module-singleton
|
|
272
|
+
# call resolution, ADR-57 follow-up).
|
|
277
273
|
return nil if kind == :instance && scope.user_def_for(class_name, method_name)
|
|
274
|
+
return nil if kind == :singleton && scope.singleton_def_for(class_name, method_name)
|
|
278
275
|
|
|
279
276
|
Type::Combinator.untyped
|
|
280
277
|
end
|
|
@@ -317,19 +314,6 @@ module Rigor
|
|
|
317
314
|
Type::Combinator.untyped
|
|
318
315
|
end
|
|
319
316
|
|
|
320
|
-
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
321
|
-
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
322
|
-
# `dynamic_return` rules, wraps the contributed types as
|
|
323
|
-
# `FlowContribution` bundles, merges them through
|
|
324
|
-
# `FlowContribution::Merger`, and returns the merged
|
|
325
|
-
# `return_type` slot (or nil when no plugin contributed a
|
|
326
|
-
# return type).
|
|
327
|
-
#
|
|
328
|
-
# Plugins whose hook raises have their contribution
|
|
329
|
-
# silently dropped for this call so the dispatch chain
|
|
330
|
-
# keeps moving — the run-level diagnostic envelope (per
|
|
331
|
-
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
332
|
-
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
333
317
|
# ADR-20 slice 3 — looks up the receiver / method pair
|
|
334
318
|
# in {Rigor::Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES}
|
|
335
319
|
# and returns the reduced HKT type. Only fires when the
|
|
@@ -404,6 +388,19 @@ module Rigor
|
|
|
404
388
|
end
|
|
405
389
|
end
|
|
406
390
|
|
|
391
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
392
|
+
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
393
|
+
# `dynamic_return` rules, wraps the contributed types as
|
|
394
|
+
# `FlowContribution` bundles, merges them through
|
|
395
|
+
# `FlowContribution::Merger`, and returns the merged
|
|
396
|
+
# `return_type` slot (or nil when no plugin contributed a
|
|
397
|
+
# return type).
|
|
398
|
+
#
|
|
399
|
+
# Plugins whose hook raises have their contribution
|
|
400
|
+
# silently dropped for this call so the dispatch chain
|
|
401
|
+
# keeps moving — the run-level diagnostic envelope (per
|
|
402
|
+
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
403
|
+
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
407
404
|
def try_plugin_contribution(call_node, scope, receiver_type)
|
|
408
405
|
return nil if call_node.nil? || scope.nil?
|
|
409
406
|
|
|
@@ -416,16 +413,6 @@ module Rigor
|
|
|
416
413
|
FlowContribution::Merger.merge(contributions).return_type
|
|
417
414
|
end
|
|
418
415
|
|
|
419
|
-
# ADR-10 slice 2b-ii. Consults the per-run
|
|
420
|
-
# `Analysis::DependencySourceInference::Index` carried by
|
|
421
|
-
# the environment for `(class_name, method_name)`
|
|
422
|
-
# observations harvested from opt-in gems' `roots:`. On a
|
|
423
|
-
# hit, returns `Combinator.untyped` so the call site
|
|
424
|
-
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
425
|
-
# "Inference contract": gem-source-inferred shapes never
|
|
426
|
-
# publish as ground-truth `T`). Returns `nil` when the
|
|
427
|
-
# environment carries no index, the index has no entry, or
|
|
428
|
-
# the receiver has no nominal class to look up.
|
|
429
416
|
# ADR-16 synthetic-method tier. Slice 2b shipped the floor —
|
|
430
417
|
# a match short-circuits at the right precedence (above
|
|
431
418
|
# dep-source / discovered / user-class-fallback; below RBS)
|
|
@@ -547,6 +534,16 @@ module Rigor
|
|
|
547
534
|
Type::Combinator.dynamic(entry.return_type)
|
|
548
535
|
end
|
|
549
536
|
|
|
537
|
+
# ADR-10 slice 2b-ii. Consults the per-run
|
|
538
|
+
# `Analysis::DependencySourceInference::Index` carried by
|
|
539
|
+
# the environment for `(class_name, method_name)`
|
|
540
|
+
# observations harvested from opt-in gems' `roots:`. On a
|
|
541
|
+
# hit, returns `Combinator.untyped` so the call site
|
|
542
|
+
# carries the `Dynamic[top]` provenance (per ADR-10's
|
|
543
|
+
# "Inference contract": gem-source-inferred shapes never
|
|
544
|
+
# publish as ground-truth `T`). Returns `nil` when the
|
|
545
|
+
# environment carries no index, the index has no entry, or
|
|
546
|
+
# the receiver has no nominal class to look up.
|
|
550
547
|
def try_dependency_source(receiver_type, method_name, environment)
|
|
551
548
|
index = environment&.dependency_source_index
|
|
552
549
|
return nil if index.nil? || index.empty?
|
|
@@ -750,10 +747,6 @@ module Rigor
|
|
|
750
747
|
# below this chain and is invoked by the outer `dispatch`
|
|
751
748
|
# method.
|
|
752
749
|
#
|
|
753
|
-
# `BlockFolding` runs last among the precision tiers because
|
|
754
|
-
# its rules apply only to block-taking calls, so the cheaper
|
|
755
|
-
# arity-based fold tiers above it filter out the common
|
|
756
|
-
# cases first. When `block_type` is nil the tier is a no-op.
|
|
757
750
|
# The precise-tier folders, consulted in order via the uniform
|
|
758
751
|
# `_DispatchTier` interface (`try_dispatch(CallContext) -> Type?`).
|
|
759
752
|
# Order is significant: ConstantFolding's exact-value folds win
|
|
@@ -792,7 +785,7 @@ module Rigor
|
|
|
792
785
|
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
793
786
|
|
|
794
787
|
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
795
|
-
KernelDispatch, MethodFolding, BlockFolding
|
|
788
|
+
KernelDispatch, MethodFolding, ReduceFolding, ArrayToHFolding, BlockFolding
|
|
796
789
|
].freeze)
|
|
797
790
|
private_constant :PRECISE_TIERS_TAIL
|
|
798
791
|
|
|
@@ -806,6 +799,14 @@ module Rigor
|
|
|
806
799
|
data_result = DataFolding.try_dispatch(context)
|
|
807
800
|
return data_result if data_result
|
|
808
801
|
|
|
802
|
+
# ADR-48 Struct follow-up — runs in the same band and for the same
|
|
803
|
+
# reason as DataFolding: `meta_new` would otherwise intercept every
|
|
804
|
+
# `Singleton[*].new` (the old `struct_new_lift` produced a bare
|
|
805
|
+
# `Singleton[Struct]`), masking a Struct class's precise instance.
|
|
806
|
+
# Only fires on Struct receivers, so it never shadows meta's lifts.
|
|
807
|
+
struct_result = StructFolding.try_dispatch(context)
|
|
808
|
+
return struct_result if struct_result
|
|
809
|
+
|
|
809
810
|
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
810
811
|
return meta_result if meta_result
|
|
811
812
|
|
|
@@ -952,18 +953,53 @@ module Rigor
|
|
|
952
953
|
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
953
954
|
return set_lift if set_lift
|
|
954
955
|
|
|
956
|
+
hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
|
|
957
|
+
return hash_lift if hash_lift
|
|
958
|
+
|
|
955
959
|
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
956
960
|
return regexp_lift if regexp_lift
|
|
957
961
|
|
|
958
962
|
date_lift = date_new_lift(receiver_type.class_name, arg_types)
|
|
959
963
|
return date_lift if date_lift
|
|
960
964
|
|
|
965
|
+
struct_new_lift = struct_new_lift(receiver_type.class_name, arg_types)
|
|
966
|
+
return struct_new_lift if struct_new_lift
|
|
967
|
+
|
|
961
968
|
class_new_lift = class_new_lift(receiver_type.class_name, arg_types)
|
|
962
969
|
return class_new_lift if class_new_lift
|
|
963
970
|
|
|
964
971
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
965
972
|
end
|
|
966
973
|
|
|
974
|
+
# `Struct.new(:a, :b)` synthesises an anonymous Struct *subclass*
|
|
975
|
+
# (a class object), not a Struct *instance* — so the chained
|
|
976
|
+
# idiom `Struct.new(:a, :b).new(1, 2)` must resolve `.new` again
|
|
977
|
+
# on a class-like carrier. The constant-bound form
|
|
978
|
+
# (`S = Struct.new(:a); S.new(1)`) already records `Singleton[S]`
|
|
979
|
+
# via `ScopeIndexer#record_meta_new_constant?`; this lift gives
|
|
980
|
+
# the *chained* (anonymous) position the same class-like carrier
|
|
981
|
+
# so the trailing `.new` dispatches instead of firing a spurious
|
|
982
|
+
# `undefined method 'new' for Struct`.
|
|
983
|
+
#
|
|
984
|
+
# The disambiguation mirrors `ScopeIndexer#struct_new_call?`: a
|
|
985
|
+
# call whose positionals are all `Constant<Symbol>` literals is a
|
|
986
|
+
# member-list class definition → `Singleton[Struct]`. The
|
|
987
|
+
# following `AnonStruct.new(1, 2)` carries non-symbol args, so it
|
|
988
|
+
# falls through this gate to `Nominal[Struct]` (a fresh instance)
|
|
989
|
+
# via the `meta_new` tail. ADR-48 deferred full Struct *value*
|
|
990
|
+
# folding (member-reader precision) on mutability grounds; this is
|
|
991
|
+
# the narrower `.new`-dispatch-only fix and contributes no member
|
|
992
|
+
# layout, so `instance.a` stays at its RBS/Dynamic type.
|
|
993
|
+
def struct_new_lift(class_name, arg_types)
|
|
994
|
+
return nil unless class_name == "Struct"
|
|
995
|
+
|
|
996
|
+
positional = arg_types.grep_v(Type::HashShape)
|
|
997
|
+
return nil if positional.empty?
|
|
998
|
+
return nil unless positional.all? { |t| t.is_a?(Type::Constant) && t.value.is_a?(Symbol) }
|
|
999
|
+
|
|
1000
|
+
Type::Combinator.singleton_of("Struct")
|
|
1001
|
+
end
|
|
1002
|
+
|
|
967
1003
|
# `Class.new` and `Class.new(Parent)` create a brand-new
|
|
968
1004
|
# anonymous class. Statically that class is representable as
|
|
969
1005
|
# the parent's singleton type — its singleton-method surface
|
|
@@ -1050,6 +1086,33 @@ module Rigor
|
|
|
1050
1086
|
type
|
|
1051
1087
|
end
|
|
1052
1088
|
|
|
1089
|
+
# `Hash.new(default)` — lifts the default value's type into the
|
|
1090
|
+
# Hash's value parameter so a subsequent `h[k]` read surfaces the
|
|
1091
|
+
# default type rather than `Dynamic[top]`. The common counter
|
|
1092
|
+
# idiom `h = Hash.new(0); h[k] += 1` then types the read as
|
|
1093
|
+
# `Integer`. The key parameter is left `untyped` (the default
|
|
1094
|
+
# carrier imposes no key constraint), so reads of any key resolve
|
|
1095
|
+
# through the value parameter. A value-pinned `Constant` default
|
|
1096
|
+
# (`0`) is widened to its nominal (`Integer`): the hash's values
|
|
1097
|
+
# mutate over its lifetime, so pinning the parameter to the
|
|
1098
|
+
# literal would be unsound for the aggregate.
|
|
1099
|
+
#
|
|
1100
|
+
# Only the single-argument default form folds. The zero-arg
|
|
1101
|
+
# (`Hash.new`) and the block form (`Hash.new { |h, k| … }`) keep
|
|
1102
|
+
# the bare `Nominal[Hash]` answer — the block's value type is not
|
|
1103
|
+
# available at this `:new` dispatch site, and conservatively
|
|
1104
|
+
# leaving the read as today's behaviour is precision-additive.
|
|
1105
|
+
def hash_new_lift(class_name, arg_types)
|
|
1106
|
+
return nil unless class_name == "Hash"
|
|
1107
|
+
return nil unless arg_types.size == 1
|
|
1108
|
+
|
|
1109
|
+
default = arg_types.first
|
|
1110
|
+
return nil if default.nil?
|
|
1111
|
+
|
|
1112
|
+
value = Type::Combinator.widen_value_pinned(default)
|
|
1113
|
+
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1053
1116
|
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to
|
|
1054
1117
|
# `Constant[Range]` when both endpoints are `Constant[Integer]`
|
|
1055
1118
|
# or both are `Constant[String]`, and the optional third argument
|
|
@@ -32,6 +32,35 @@ module Rigor
|
|
|
32
32
|
# `#singleton_method`.
|
|
33
33
|
#
|
|
34
34
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
35
|
+
# Leaf-name extraction for a destructured positional parameter
|
|
36
|
+
# (`Prism::MultiTargetNode`). Stateless; lifted out of
|
|
37
|
+
# {MethodParameterBinder} so the binder's class length stays in
|
|
38
|
+
# budget.
|
|
39
|
+
module Destructure
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Collect every leaf local name a `MultiTargetNode` binds,
|
|
43
|
+
# recursing through nested destructures (`((a, b), c)`) and the
|
|
44
|
+
# splat slot (`(a, *rest)`). Targets without a `#name` (an
|
|
45
|
+
# index/call write target, vanishingly rare in a parameter
|
|
46
|
+
# position) are skipped — there is no local to bind.
|
|
47
|
+
def target_names(multi_target)
|
|
48
|
+
entries = multi_target.lefts + [multi_target.rest, *multi_target.rights].compact
|
|
49
|
+
entries.flat_map { |entry| names_for_entry(entry) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names_for_entry(entry)
|
|
53
|
+
# A splat sub-target (`*rest` inside the destructure) wraps its
|
|
54
|
+
# real target in a `SplatNode#expression`; unwrap it.
|
|
55
|
+
entry = entry.expression if entry.is_a?(Prism::SplatNode) && entry.expression
|
|
56
|
+
return [] if entry.nil?
|
|
57
|
+
return target_names(entry) if entry.is_a?(Prism::MultiTargetNode)
|
|
58
|
+
return [entry.name] if entry.respond_to?(:name) && entry.name
|
|
59
|
+
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
35
64
|
class MethodParameterBinder
|
|
36
65
|
# @param environment [Rigor::Environment]
|
|
37
66
|
# @param class_path [String, nil] the qualified name of the class
|
|
@@ -112,14 +141,39 @@ module Rigor
|
|
|
112
141
|
|
|
113
142
|
def positional_slots(params_node)
|
|
114
143
|
slots = []
|
|
115
|
-
params_node.requireds.each_with_index
|
|
144
|
+
params_node.requireds.each_with_index do |p, i|
|
|
145
|
+
append_positional_slot(slots, :required_positional, p, i)
|
|
146
|
+
end
|
|
116
147
|
params_node.optionals.each_with_index { |p, i| slots << ParamSlot.new(:optional_positional, p.name, i) }
|
|
117
148
|
rest = params_node.rest
|
|
118
149
|
slots << ParamSlot.new(:rest_positional, rest.name, nil) if rest.respond_to?(:name) && rest&.name
|
|
119
|
-
params_node.posts.each_with_index
|
|
150
|
+
params_node.posts.each_with_index do |p, i|
|
|
151
|
+
append_positional_slot(slots, :trailing_positional, p, i)
|
|
152
|
+
end
|
|
120
153
|
slots
|
|
121
154
|
end
|
|
122
155
|
|
|
156
|
+
# A destructured positional parameter — `def f((a, b))` — is a
|
|
157
|
+
# `Prism::MultiTargetNode` in the `requireds`/`posts` list, not a
|
|
158
|
+
# `RequiredParameterNode`, so it has no `#name`. Bind each leaf
|
|
159
|
+
# sub-target local to `Dynamic[Top]` (a `:destructured_positional`
|
|
160
|
+
# slot with no RBS index) instead of crashing on a blind `.name`.
|
|
161
|
+
# Binding the names at all is what matters: it keeps the
|
|
162
|
+
# destructured locals present in the entry scope so the body's
|
|
163
|
+
# reads of them don't fall through to undefined-local noise. The
|
|
164
|
+
# element types are not cheaply available from the parameter list
|
|
165
|
+
# alone (no RBS function param maps onto a destructured slot), so
|
|
166
|
+
# `Dynamic[Top]` is the sound default.
|
|
167
|
+
def append_positional_slot(slots, kind, param, index)
|
|
168
|
+
if param.is_a?(Prism::MultiTargetNode)
|
|
169
|
+
Destructure.target_names(param).each do |name|
|
|
170
|
+
slots << ParamSlot.new(:destructured_positional, name, nil)
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
slots << ParamSlot.new(kind, param.name, index)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
123
177
|
def keyword_slots(params_node)
|
|
124
178
|
params_node.keywords.filter_map do |kw|
|
|
125
179
|
case kw
|
|
@@ -100,19 +100,62 @@ module Rigor
|
|
|
100
100
|
|
|
101
101
|
def decompose_tuple(tuple, front_count, back_count, rest_present:)
|
|
102
102
|
elements = tuple.elements
|
|
103
|
-
fronts = Array.new(front_count) { |i| elements
|
|
103
|
+
fronts = Array.new(front_count) { |i| slot_type(elements, i) }
|
|
104
104
|
if rest_present
|
|
105
105
|
middle_end = [elements.size - back_count, front_count].max
|
|
106
106
|
middle = elements[front_count...middle_end] || []
|
|
107
107
|
rest_type = Type::Combinator.tuple_of(*middle)
|
|
108
|
-
backs = Array.new(back_count) { |i| elements
|
|
108
|
+
backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
|
|
109
109
|
else
|
|
110
110
|
rest_type = nil
|
|
111
|
-
backs = Array.new(back_count) { |i| elements
|
|
111
|
+
backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
|
|
112
112
|
end
|
|
113
113
|
[fronts, rest_type, backs]
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# The per-slot type for index `i` of a tuple decomposition, FP-safely
|
|
117
|
+
# softened: a missing slot is `nil` (the runtime value of an
|
|
118
|
+
# over-destructured positional), and a PRESENT but nil-bearing slot
|
|
119
|
+
# (`X | nil`) is softened to its non-`nil` part — for a heterogeneous
|
|
120
|
+
# `Tuple` whose optional slot was made optional by flow.
|
|
121
|
+
#
|
|
122
|
+
# Rationale (ADR-57 slice 3 work-item 2): a destructure of a tuple
|
|
123
|
+
# element that flow typed as optional is almost always guarded by a
|
|
124
|
+
# CORRELATED invariant the flow engine cannot prove. The canonical case
|
|
125
|
+
# is haml's `parse_tag`, which returns `[..., last_line || @line.index
|
|
126
|
+
# + 1]` — a 9-tuple whose `last_line` slot widens to `Dynamic[top]?`
|
|
127
|
+
# through a loop-nested destructure; at the call site `..., last_line =
|
|
128
|
+
# parse_tag(text); raise(..., last_line - 1) if parse && value.empty?`
|
|
129
|
+
# the `last_line` is nil ONLY when an earlier element is too, and the
|
|
130
|
+
# guard short-circuits — but that correlation lives across slots, so
|
|
131
|
+
# per-slot flow sees `last_line` as nil-able and `last_line - 1` fires a
|
|
132
|
+
# spurious `possible nil receiver`. Manufacturing a `T?` for every
|
|
133
|
+
# destructured slot frightens working code; FP discipline (the program
|
|
134
|
+
# works) outranks the worst-case per-slot reading, so we drop the `nil`
|
|
135
|
+
# from a destructured slot and keep the non-`nil` constituent (a bare
|
|
136
|
+
# `nil` slot stays `nil` — there is nothing to soften). A pure non-
|
|
137
|
+
# optional element keeps its precise type unchanged.
|
|
138
|
+
def slot_type(elements, index)
|
|
139
|
+
element = elements[index]
|
|
140
|
+
return Type::Combinator.constant_of(nil) if element.nil?
|
|
141
|
+
|
|
142
|
+
soften_optional_slot(element)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def soften_optional_slot(element)
|
|
146
|
+
return element unless element.is_a?(Type::Union)
|
|
147
|
+
return element unless element.members.any? { |m| nil_literal?(m) }
|
|
148
|
+
|
|
149
|
+
non_nil = element.members.reject { |m| nil_literal?(m) }
|
|
150
|
+
return element if non_nil.empty? # a bare `nil` slot: nothing to soften
|
|
151
|
+
|
|
152
|
+
Type::Combinator.union(*non_nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def nil_literal?(member)
|
|
156
|
+
member.is_a?(Type::Constant) && member.value.nil?
|
|
157
|
+
end
|
|
158
|
+
|
|
116
159
|
def decompose_default(front_count, back_count, rest_present:)
|
|
117
160
|
[
|
|
118
161
|
Array.new(front_count) { Type::Combinator.untyped },
|
|
@@ -67,18 +67,12 @@ module Rigor
|
|
|
67
67
|
# ...; @warning_issued = true`). Requires tracking the
|
|
68
68
|
# first-write position; flow-sensitive but orthogonal.
|
|
69
69
|
# - **Local-variable mutation inside a block body** (e.g.
|
|
70
|
-
# `arr = []; xs.each { |x| arr << x }`)
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# an outer LOCAL does not yet flow back. **Ivar mutations
|
|
75
|
-
# inside a block ARE handled** (ivars live in the
|
|
76
|
-
# method-body scope, not the block-local scope) — the
|
|
77
|
-
# widening fires from inside the block and the new ivar
|
|
78
|
-
# binding is visible to the outer scope.
|
|
70
|
+
# `arr = []; xs.each { |x| arr << x }`) — landed as
|
|
71
|
+
# ADR-56 slice A (`widen_after_block`). **Ivar mutations
|
|
72
|
+
# inside a block ARE also handled** (ivars live in the
|
|
73
|
+
# method-body scope, not the block-local scope).
|
|
79
74
|
#
|
|
80
|
-
#
|
|
81
|
-
# `docs/CURRENT_WORK.md` and are intentionally deferred.
|
|
75
|
+
# The remaining three items above are demand-gated; see ADR-56.
|
|
82
76
|
module MutationWidening
|
|
83
77
|
# Array mutators that change either the size or the element
|
|
84
78
|
# set of a literal-shape carrier (Tuple). Receiver-mutating
|
|
@@ -280,6 +274,148 @@ module Rigor
|
|
|
280
274
|
carriers = kinds.map { |name| Type::Combinator.nominal_of(name) }
|
|
281
275
|
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
282
276
|
end
|
|
277
|
+
|
|
278
|
+
# ----------------------------------------------------------------
|
|
279
|
+
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
280
|
+
#
|
|
281
|
+
# `widen_after_block` above forgets a literal-shape carrier's arity
|
|
282
|
+
# when a captured local is content-mutated inside a block, but it
|
|
283
|
+
# keeps only the SEED's element types — an unsound under-
|
|
284
|
+
# approximation for a non-empty seed (`out = [0]; arr.each { |x|
|
|
285
|
+
# out << x }` types `Array[0]` while the runtime array is
|
|
286
|
+
# `[0, 1, 2, 3]`). Slice C joins the appended/stored element (and
|
|
287
|
+
# key/value) types INTO the continuation collection's parameter, so
|
|
288
|
+
# the result is `Array[0 | Integer]` rather than `Array[0]`.
|
|
289
|
+
#
|
|
290
|
+
# Array content-mutators that append/store ELEMENTS. The appended
|
|
291
|
+
# element type is the call's argument type(s); `[]=`'s value is its
|
|
292
|
+
# LAST argument (the keys precede it). Subset of `ARRAY_MUTATORS`:
|
|
293
|
+
# only the element-INTRODUCING methods (removers / reorderers add no
|
|
294
|
+
# new element evidence and are already covered by the arity-forget).
|
|
295
|
+
ARRAY_CONTENT_ADDERS = %i[
|
|
296
|
+
<< push append prepend unshift concat insert []= fill replace
|
|
297
|
+
].to_set.freeze
|
|
298
|
+
|
|
299
|
+
# Hash content-mutators that store a key→value pair. For `[]=` /
|
|
300
|
+
# `store` the key is the first argument and the value the last.
|
|
301
|
+
HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
|
|
302
|
+
|
|
303
|
+
# String content-mutators that append to the buffer. String carries
|
|
304
|
+
# no element parameter, so these contribute nothing to a join — they
|
|
305
|
+
# are listed so the orchestrator recognises them as content mutators
|
|
306
|
+
# (the binding already widens to `String` via normal typing); the
|
|
307
|
+
# join helpers below short-circuit on a non-collection pre-state.
|
|
308
|
+
STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
|
|
309
|
+
|
|
310
|
+
# Every method name that mutates a captured local's CONTENT — the
|
|
311
|
+
# union the orchestrator scans the block body for.
|
|
312
|
+
CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
|
|
313
|
+
|
|
314
|
+
# The element types a single content-mutator call introduces into an
|
|
315
|
+
# Array, given the per-argument types (already typed in the block
|
|
316
|
+
# body scope). `concat`/`replace` take collection arguments, so their
|
|
317
|
+
# element evidence is the arguments' OWN element types unioned; the
|
|
318
|
+
# rest append the argument values directly. Returns `[]` when no
|
|
319
|
+
# element evidence (e.g. a `<<` with no resolvable arg).
|
|
320
|
+
def array_added_elements(method_name, arg_types)
|
|
321
|
+
return [] if arg_types.empty?
|
|
322
|
+
|
|
323
|
+
case method_name
|
|
324
|
+
when :concat, :replace
|
|
325
|
+
arg_types.flat_map { |t| collection_element_types(t) }
|
|
326
|
+
when :insert
|
|
327
|
+
# `insert(index, *objs)` — first arg is the position.
|
|
328
|
+
arg_types.drop(1)
|
|
329
|
+
when :[]=
|
|
330
|
+
# `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
|
|
331
|
+
[arg_types.last]
|
|
332
|
+
when :fill
|
|
333
|
+
# `fill(value)` — only the no-block single-value form adds a
|
|
334
|
+
# concrete element; block / range forms are conservatively
|
|
335
|
+
# ignored (the arity-forget already widened the binding).
|
|
336
|
+
arg_types.size == 1 ? arg_types : []
|
|
337
|
+
else # << push append prepend unshift
|
|
338
|
+
arg_types
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Builds the continuation Array type from the pre-state binding and
|
|
343
|
+
# the appended element types. The floor is `Array[Dynamic[top]]`
|
|
344
|
+
# (the sound empty-seed behaviour) when there is no element evidence
|
|
345
|
+
# at all.
|
|
346
|
+
def join_array_content(pre_state, added_elements)
|
|
347
|
+
seed_elements = collection_element_types(pre_state)
|
|
348
|
+
added = added_elements.compact
|
|
349
|
+
# The empty-seed floor element is `Dynamic[top]` (no element
|
|
350
|
+
# evidence). When real appended evidence exists that floor carries
|
|
351
|
+
# nothing, so drop it — an empty accumulator built by `out << x*2`
|
|
352
|
+
# reads `Array[Integer]`, not `Array[Integer | Dynamic[top]]`.
|
|
353
|
+
seed_elements = drop_dynamic(seed_elements) unless added.empty?
|
|
354
|
+
elements = seed_elements + added
|
|
355
|
+
return Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped]) if elements.empty?
|
|
356
|
+
|
|
357
|
+
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.union(*elements)])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Builds the continuation Hash type from the pre-state binding and a
|
|
361
|
+
# list of `[key_type, value_type]` pairs stored by `[]=` / `store`.
|
|
362
|
+
def join_hash_content(pre_state, added_pairs)
|
|
363
|
+
seed_keys, seed_values = hash_shape_key_values(pre_state)
|
|
364
|
+
added_keys = added_pairs.map(&:first).compact
|
|
365
|
+
added_values = added_pairs.map(&:last).compact
|
|
366
|
+
seed_keys = drop_dynamic(seed_keys) unless added_keys.empty?
|
|
367
|
+
seed_values = drop_dynamic(seed_values) unless added_values.empty?
|
|
368
|
+
keys = seed_keys + added_keys
|
|
369
|
+
values = seed_values + added_values
|
|
370
|
+
key_t = keys.empty? ? Type::Combinator.untyped : Type::Combinator.union(*keys)
|
|
371
|
+
value_t = values.empty? ? Type::Combinator.untyped : Type::Combinator.union(*values)
|
|
372
|
+
Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t])
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Drops `Dynamic` (incl. `untyped`) constituents from a type list.
|
|
376
|
+
def drop_dynamic(types)
|
|
377
|
+
types.grep_v(Type::Dynamic)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Element types carried by a collection binding, regardless of which
|
|
381
|
+
# carrier holds them: a `Tuple` lists them, a `Nominal[Array, [E]]`
|
|
382
|
+
# has one element param, a bare `Array` / anything else yields none.
|
|
383
|
+
def collection_element_types(type)
|
|
384
|
+
case type
|
|
385
|
+
when Type::Tuple
|
|
386
|
+
type.elements
|
|
387
|
+
when Type::Nominal
|
|
388
|
+
type.class_name == "Array" ? type.type_args : []
|
|
389
|
+
when Type::Union
|
|
390
|
+
# A loop's single-pass join can union the widened collection with
|
|
391
|
+
# its un-widened literal seed (`Array[0] | [0]`); pull element
|
|
392
|
+
# evidence from every Array-ish member.
|
|
393
|
+
type.members.flat_map { |m| collection_element_types(m) }
|
|
394
|
+
else
|
|
395
|
+
[]
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# `[keys, values]` evidence from a Hash-ish pre-state binding —
|
|
400
|
+
# a `HashShape` (literal pairs) or a `Nominal[Hash, [K, V]]`.
|
|
401
|
+
def hash_shape_key_values(type)
|
|
402
|
+
case type
|
|
403
|
+
when Type::HashShape
|
|
404
|
+
return [[], []] if type.pairs.empty?
|
|
405
|
+
|
|
406
|
+
[[key_union_for(type.pairs.keys)], type.pairs.values]
|
|
407
|
+
when Type::Nominal
|
|
408
|
+
type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
|
|
409
|
+
when Type::Union
|
|
410
|
+
type.members.each_with_object([[], []]) do |m, (ks, vs)|
|
|
411
|
+
mk, mv = hash_shape_key_values(m)
|
|
412
|
+
ks.concat(mk)
|
|
413
|
+
vs.concat(mv)
|
|
414
|
+
end
|
|
415
|
+
else
|
|
416
|
+
[[], []]
|
|
417
|
+
end
|
|
418
|
+
end
|
|
283
419
|
end
|
|
284
420
|
end
|
|
285
421
|
end
|