rigortype 0.1.19 → 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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- 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 +49 -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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- 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 +14 -2
- data/lib/rigor/scope.rb +54 -11
- 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/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -11,8 +11,8 @@ module Rigor
|
|
|
11
11
|
# catalog is NOT routed through
|
|
12
12
|
# `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS`
|
|
13
13
|
# (which dispatches on the receiver's concrete class).
|
|
14
|
-
# The data is
|
|
15
|
-
#
|
|
14
|
+
# The data is wired into `MODULE_CATALOGS` in
|
|
15
|
+
# `MethodDispatcher::ConstantFolding` (ancestor-chain lookup).
|
|
16
16
|
ENUMERABLE_CATALOG = MethodCatalog.for_topic(
|
|
17
17
|
"enumerable",
|
|
18
18
|
mutating_selectors: {
|
|
@@ -27,6 +27,24 @@ module Rigor
|
|
|
27
27
|
FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
|
|
28
28
|
EMPTY_CATALOG = { "classes" => {} }.freeze
|
|
29
29
|
|
|
30
|
+
# Selectors that are classified `:leaf` by the C-body analysis
|
|
31
|
+
# (they read no global mutable state in the C sense) but whose
|
|
32
|
+
# result is NOT reproducible across Ruby processes, so they must
|
|
33
|
+
# never be folded into a `Constant`:
|
|
34
|
+
#
|
|
35
|
+
# - `hash` — every core `#hash` (`String`/`Symbol`/`Integer`/
|
|
36
|
+
# `Float`/…) is salted with a per-process SipHash seed, so
|
|
37
|
+
# `"x".hash` differs in every process. Folding bakes one
|
|
38
|
+
# process's value into the type and the on-disk cache.
|
|
39
|
+
# - `object_id` / `__id__` — identity-allocated per process.
|
|
40
|
+
#
|
|
41
|
+
# This is a UNIVERSAL block (across every catalogued class)
|
|
42
|
+
# because `hash` / `object_id` are `Object`-level and present on
|
|
43
|
+
# every receiver; a per-class blocklist would silently miss a
|
|
44
|
+
# class. The deterministic siblings (`inspect`, `to_s`) are
|
|
45
|
+
# unaffected.
|
|
46
|
+
NON_REPRODUCIBLE_SELECTORS = Set[:hash, :object_id, :__id__].freeze
|
|
47
|
+
|
|
30
48
|
# Shared root for the offline-generated catalogues. Resolving it
|
|
31
49
|
# here keeps the repo-relative `../../../../` hop in one place
|
|
32
50
|
# instead of copying it into every per-topic loader.
|
|
@@ -59,6 +77,7 @@ module Rigor
|
|
|
59
77
|
|
|
60
78
|
def safe_for_folding?(class_name, selector, kind: :instance)
|
|
61
79
|
class_name_str = class_name.to_s
|
|
80
|
+
return false if NON_REPRODUCIBLE_SELECTORS.include?(selector.to_sym)
|
|
62
81
|
return false if blocked?(class_name_str, selector)
|
|
63
82
|
|
|
64
83
|
entry = method_entry(class_name_str, selector, kind: kind)
|
|
@@ -22,7 +22,15 @@ module Rigor
|
|
|
22
22
|
:replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
|
|
23
23
|
:prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=",
|
|
24
24
|
:upto, :each_byte, :each_char, :each_codepoint,
|
|
25
|
-
:each_grapheme_cluster, :each_line, :bytesplice
|
|
25
|
+
:each_grapheme_cluster, :each_line, :bytesplice,
|
|
26
|
+
# `crypt` is not a mutator but is blocked from folding for the
|
|
27
|
+
# same "do not bake a non-pure result into a Constant" reason:
|
|
28
|
+
# `rb_str_crypt` delegates to the platform `crypt(3)`, whose
|
|
29
|
+
# output (algorithm and digest) varies by libc / OS, so
|
|
30
|
+
# `"x".crypt("ab")` is not deterministic across the platforms
|
|
31
|
+
# an analyzed project may target. The catalog classifies it
|
|
32
|
+
# `:leaf` from its C body; this entry overrides that.
|
|
33
|
+
:crypt
|
|
26
34
|
],
|
|
27
35
|
"Symbol" => Set[
|
|
28
36
|
# Symbol is immutable in Ruby; the classifier mis-flags
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../ast"
|
|
7
|
+
require_relative "../source/constant_path"
|
|
7
8
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
8
9
|
require_relative "block_parameter_binder"
|
|
9
10
|
require_relative "body_fixpoint"
|
|
@@ -14,6 +15,7 @@ require_relative "indexed_narrowing"
|
|
|
14
15
|
require_relative "macro_block_self_type"
|
|
15
16
|
require_relative "method_dispatcher"
|
|
16
17
|
require_relative "narrowing"
|
|
18
|
+
require_relative "struct_fold_safety"
|
|
17
19
|
|
|
18
20
|
module Rigor
|
|
19
21
|
module Inference
|
|
@@ -323,12 +325,7 @@ module Rigor
|
|
|
323
325
|
dynamic_top
|
|
324
326
|
end
|
|
325
327
|
|
|
326
|
-
#
|
|
327
|
-
# narrow: self, instance/class/global variable reads, block bodies.
|
|
328
|
-
# Slice 3+ refines these in place; for now we acknowledge the node
|
|
329
|
-
# class so the coverage scanner stops flagging it without recording
|
|
330
|
-
# a fail-soft event for every occurrence.
|
|
331
|
-
# Slice A-engine. `Prism::SelfNode` resolves to the scope's
|
|
328
|
+
# `Prism::SelfNode` resolves to the scope's
|
|
332
329
|
# `self_type` when one has been injected (by
|
|
333
330
|
# `StatementEvaluator` at class-body and method-body
|
|
334
331
|
# boundaries) or `Dynamic[Top]` at the top level. Class-body
|
|
@@ -412,7 +409,7 @@ module Rigor
|
|
|
412
409
|
end
|
|
413
410
|
|
|
414
411
|
def type_of_constant_path(node)
|
|
415
|
-
full_name =
|
|
412
|
+
full_name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
416
413
|
return fallback_for(node, family: :prism) if full_name.nil?
|
|
417
414
|
|
|
418
415
|
resolve_constant_name(full_name) || fallback_for(node, family: :prism)
|
|
@@ -481,24 +478,6 @@ module Rigor
|
|
|
481
478
|
end
|
|
482
479
|
end
|
|
483
480
|
|
|
484
|
-
# Builds the dotted-colon name for a `Foo`, `Foo::Bar`, or `::Foo`
|
|
485
|
-
# path. Returns nil when an inner segment is not itself a constant
|
|
486
|
-
# reference (for example `expr::Foo`), so the caller can fall back.
|
|
487
|
-
def build_constant_path_name(node)
|
|
488
|
-
case node
|
|
489
|
-
when Prism::ConstantReadNode
|
|
490
|
-
node.name.to_s
|
|
491
|
-
when Prism::ConstantPathNode
|
|
492
|
-
parent = node.parent
|
|
493
|
-
return node.name.to_s if parent.nil?
|
|
494
|
-
|
|
495
|
-
parent_name = build_constant_path_name(parent)
|
|
496
|
-
return nil if parent_name.nil?
|
|
497
|
-
|
|
498
|
-
"#{parent_name}::#{node.name}"
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
481
|
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
|
|
503
482
|
# when every entry is a static `AssocNode` whose key is a
|
|
504
483
|
# `SymbolNode` or `StringNode` with a known value (covering the
|
|
@@ -837,7 +816,7 @@ module Rigor
|
|
|
837
816
|
# Other pattern shapes (Range, Regexp, custom `===`) stay
|
|
838
817
|
# `:maybe` — the existing union fallback handles them.
|
|
839
818
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
840
|
-
class_name =
|
|
819
|
+
class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
|
|
841
820
|
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
|
|
842
821
|
|
|
843
822
|
literal = literal_pattern_value(pattern_node)
|
|
@@ -925,7 +904,8 @@ module Rigor
|
|
|
925
904
|
end
|
|
926
905
|
|
|
927
906
|
# `while` and `until` loops produce nil unless interrupted by
|
|
928
|
-
# `break VALUE
|
|
907
|
+
# `break VALUE`; the expression value of `break VALUE` is not yet
|
|
908
|
+
# modeled (scope break-path propagation landed in `eval_loop`).
|
|
929
909
|
# Returning Constant[nil] is safe and matches Ruby semantics for
|
|
930
910
|
# the common case.
|
|
931
911
|
def type_of_loop(_node)
|
|
@@ -2382,7 +2362,19 @@ module Rigor
|
|
|
2382
2362
|
environment: scope.environment,
|
|
2383
2363
|
locals: locals.freeze,
|
|
2384
2364
|
self_type: receiver,
|
|
2385
|
-
discovery: scope.discovery
|
|
2365
|
+
discovery: scope.discovery,
|
|
2366
|
+
struct_fold_safe_locals: struct_fold_safe_locals_for(def_node.body)
|
|
2367
|
+
)
|
|
2368
|
+
end
|
|
2369
|
+
|
|
2370
|
+
# ADR-48 Struct slice 3 — the fold-safe-local set for a method body
|
|
2371
|
+
# (runs only on a return-memo miss, so the per-call cost is bounded —
|
|
2372
|
+
# measured perf-neutral). Struct member layouts of constant receivers
|
|
2373
|
+
# are resolved through the discovery side-table the body scope inherits.
|
|
2374
|
+
def struct_fold_safe_locals_for(body)
|
|
2375
|
+
StructFoldSafety.fold_safe_locals(
|
|
2376
|
+
body,
|
|
2377
|
+
->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2386
2378
|
)
|
|
2387
2379
|
end
|
|
2388
2380
|
|
|
@@ -7,18 +7,15 @@ module Rigor
|
|
|
7
7
|
# piece of a Rigor-side type expression that the reducer
|
|
8
8
|
# ({HktReducer}) walks against a concrete argument list.
|
|
9
9
|
#
|
|
10
|
-
# Slice 2a ships a programmatic constructor surface
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# `body` String stays opaque and `body_tree` is the
|
|
17
|
-
# evaluable form.
|
|
10
|
+
# Slice 2a ships a programmatic constructor surface; plugin
|
|
11
|
+
# and Rigor-bundled overlay authors may build a body tree
|
|
12
|
+
# by hand using these node types. The string-grammar parser
|
|
13
|
+
# (`HktBodyParser`, Slice 2b, shipped) reads `Definition#body`
|
|
14
|
+
# (populated by Slice 1's `HktDirectives.parse_define`) into
|
|
15
|
+
# this node tree; `body_tree` is the evaluable form.
|
|
18
16
|
#
|
|
19
|
-
# The
|
|
20
|
-
#
|
|
21
|
-
# near-term adopters:
|
|
17
|
+
# The nine node types cover JSON.parse, dry-monads, and the
|
|
18
|
+
# ADR-20 § D3 conditional / membership forms (shipped):
|
|
22
19
|
#
|
|
23
20
|
# - {TypeLeaf} — wraps a fully-built `Rigor::Type`
|
|
24
21
|
# (use for atoms like `nil`, `Constant<true>`,
|
|
@@ -11,29 +11,27 @@ module Rigor
|
|
|
11
11
|
# `%a{rigor:v1:hkt_define}` payloads) into the `HktBody`
|
|
12
12
|
# node tree the Slice 2a reducer evaluates against.
|
|
13
13
|
#
|
|
14
|
-
# The
|
|
15
|
-
# union-of-atoms
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# without conditionals). The conditional / indexed-access
|
|
20
|
-
# forms (`E <: T ? A : B`, `E in [k1, k2]`) drafted in D3
|
|
21
|
-
# remain a follow-up slice — bodies that contain `?`
|
|
22
|
-
# raise `ParseError` and the calling directive parser
|
|
23
|
-
# drops the body_tree (the body String remains stored and
|
|
24
|
-
# the reducer falls back to `app.bound`).
|
|
14
|
+
# The grammar implements the full ADR-20 § D3 subset:
|
|
15
|
+
# union-of-atoms/parameterised-forms for `JSON.parse`'s
|
|
16
|
+
# `json::value` recursive sum, plus the conditional and
|
|
17
|
+
# membership forms shipped in subsequent slices. Indexed-access
|
|
18
|
+
# forms remain deferred (no concrete demand yet).
|
|
25
19
|
#
|
|
26
|
-
# ## Grammar
|
|
20
|
+
# ## Grammar
|
|
27
21
|
#
|
|
28
22
|
# body := union
|
|
29
23
|
# union := type_expr ("|" type_expr)*
|
|
30
24
|
# type_expr := atom | nominal_app | app_ref | param
|
|
25
|
+
# | conditional
|
|
31
26
|
# atom := "nil" | "true" | "false" | "bool" | "untyped"
|
|
32
27
|
# param := UCNAME (when UCNAME ∈ params)
|
|
33
28
|
# nominal_app := class_name ("[" type_expr ("," type_expr)* "]")?
|
|
34
29
|
# class_name := "::"? UCNAME ("::" UCNAME)*
|
|
35
30
|
# app_ref := "App" "[" uri "," type_expr ("," type_expr)* "]"
|
|
36
31
|
# uri := IDENT ("::" IDENT)+
|
|
32
|
+
# conditional := "(" test "?" union ":" union ")"
|
|
33
|
+
# test := type_expr ("<:" | "==") type_expr
|
|
34
|
+
# | type_expr "in" "[" type_expr ("," type_expr)* "]"
|
|
37
35
|
# UCNAME := /[A-Z]\w*/
|
|
38
36
|
# IDENT := /[a-z_]\w*/
|
|
39
37
|
#
|
|
@@ -10,12 +10,12 @@ module Rigor
|
|
|
10
10
|
# `%a{rigor:v1:hkt_define: ...}` annotations in shipped
|
|
11
11
|
# `.rbs` files.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
13
|
+
# The registry stores registration metadata (arity, variance,
|
|
14
|
+
# bound) and the definition body as both a raw String and an
|
|
15
|
+
# evaluable `HktBody` node tree. `HktReducer` (Slice 2a) and
|
|
16
|
+
# `HktBodyParser` (Slice 2b) are both shipped and reduce
|
|
17
|
+
# `Type::App` instances against the definition. The carrier
|
|
18
|
+
# never needs to read from the registry
|
|
19
19
|
# because Slice 1's `Type::App` carries its `bound` directly;
|
|
20
20
|
# the registry exists at this slice solely so the parser
|
|
21
21
|
# round-trip and downstream slices have a stable target API.
|
|
@@ -65,16 +65,15 @@ module Rigor
|
|
|
65
65
|
# definition.
|
|
66
66
|
#
|
|
67
67
|
# `body` is the raw String payload from the `%a{...}`
|
|
68
|
-
# annotation (Slice 1's parser populates it)
|
|
69
|
-
#
|
|
68
|
+
# annotation (Slice 1's parser populates it); parsed into
|
|
69
|
+
# `body_tree` by `HktBodyParser` (Slice 2b, shipped).
|
|
70
70
|
#
|
|
71
|
-
# `body_tree` is the
|
|
71
|
+
# `body_tree` is the evaluable form: a
|
|
72
72
|
# `Rigor::Inference::HktBody::*` node tree the Slice 2a
|
|
73
73
|
# reducer walks against the application's concrete
|
|
74
74
|
# arguments. Plugin and Rigor-bundled overlay authors
|
|
75
75
|
# construct it programmatically through
|
|
76
|
-
# {
|
|
77
|
-
# it from `body` once it ships. The reducer treats a
|
|
76
|
+
# {.definition_with_body_tree}. The reducer treats a
|
|
78
77
|
# `nil` `body_tree` as "definition not yet evaluable"
|
|
79
78
|
# and returns the registered bound.
|
|
80
79
|
Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
|
|
@@ -42,10 +42,7 @@ module Rigor
|
|
|
42
42
|
#
|
|
43
43
|
# This is the single place the call-context field list is
|
|
44
44
|
# enumerated — the whole point of the value object is to absorb
|
|
45
|
-
# the wide keyword list the tiers used to each redeclare.
|
|
46
|
-
# ParameterLists disable here retires the per-tier disables (the
|
|
47
|
-
# `RbsDispatch` quartet-plus signatures) rather than adding to
|
|
48
|
-
# them.
|
|
45
|
+
# the wide keyword list the tiers used to each redeclare.
|
|
49
46
|
def self.build(receiver:, method_name:, args:, # rubocop:disable Metrics/ParameterLists
|
|
50
47
|
block_type: nil, environment: nil, call_node: nil,
|
|
51
48
|
scope: nil, self_type_override: nil, public_only: false)
|
|
@@ -40,7 +40,7 @@ module Rigor
|
|
|
40
40
|
# receiver/argument combination.
|
|
41
41
|
#
|
|
42
42
|
# Anything else returns `nil`, signalling "no rule matched" so the
|
|
43
|
-
# caller (`
|
|
43
|
+
# caller (`MethodDispatcher`) falls back to `Dynamic[Top]` and records a
|
|
44
44
|
# fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
|
|
45
45
|
# behind this rule book, but the constant-folding semantics defined
|
|
46
46
|
# here MUST NOT regress: any value reachable by literal arithmetic at
|
|
@@ -51,7 +51,7 @@ module Rigor
|
|
|
51
51
|
NUMERIC_BINARY = Set[
|
|
52
52
|
:+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
|
|
53
53
|
:<, :<=, :>, :>=, :==, :!=, :<=>,
|
|
54
|
-
:gcd, :lcm, :fdiv
|
|
54
|
+
:gcd, :lcm, :fdiv, :quo, :ceildiv, :[]
|
|
55
55
|
].freeze
|
|
56
56
|
STRING_BINARY = Set[
|
|
57
57
|
:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
|
|
@@ -60,12 +60,31 @@ module Rigor
|
|
|
60
60
|
:match?, :index, :rindex, :center, :ljust, :rjust,
|
|
61
61
|
# 1-arg pure transforms/queries whose output never exceeds the
|
|
62
62
|
# input: `delete`/`squeeze` shrink the string, `count` → Integer.
|
|
63
|
-
:delete, :count, :squeeze
|
|
63
|
+
:delete, :count, :squeeze,
|
|
64
|
+
# ASCII / Unicode-case-fold comparison — deterministic, no
|
|
65
|
+
# locale read: `casecmp` → -1/0/1, `casecmp?` → bool/nil.
|
|
66
|
+
:casecmp, :casecmp?
|
|
64
67
|
].freeze
|
|
65
68
|
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
66
69
|
BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
|
|
67
70
|
NIL_BINARY = Set[:==, :!=].freeze
|
|
68
|
-
|
|
71
|
+
# Rational arithmetic / ordering are exact and pure. Division
|
|
72
|
+
# (`/`) and `**` may return a `Float`/`Complex` for some operands,
|
|
73
|
+
# all of which are foldable `Constant` value classes. `==` / `!=`
|
|
74
|
+
# are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes
|
|
75
|
+
# through `rb_funcall(:==)` on the operands — user-redefinable —
|
|
76
|
+
# so the catalog classifies it `:dispatch` and the equality stays
|
|
77
|
+
# the RBS `bool`. (The set would otherwise bypass that gate.)
|
|
78
|
+
RATIONAL_BINARY = Set[
|
|
79
|
+
:+, :-, :*, :/, :**, :<=>, :<, :<=, :>, :>=,
|
|
80
|
+
:div, :modulo, :%, :remainder, :fdiv, :quo
|
|
81
|
+
].freeze
|
|
82
|
+
# Complex arithmetic. `ops_for` gains a `Complex` branch so these
|
|
83
|
+
# reach the binary fold path (Complex was previously unary-only).
|
|
84
|
+
# `/` and `**` stay foldable (Complex result). `==` / `!=` are
|
|
85
|
+
# excluded for the same reason as Rational (`nucomp_eqeq_p`
|
|
86
|
+
# delegates to operand `==`); ordering is undefined for Complex.
|
|
87
|
+
COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
|
|
69
88
|
|
|
70
89
|
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
71
90
|
# - take zero arguments,
|
|
@@ -83,20 +102,30 @@ module Rigor
|
|
|
83
102
|
# user-defined `def is_odd(n) = n.odd?` so
|
|
84
103
|
# `Parity.new.is_odd(3)` types as `Constant[true]`
|
|
85
104
|
# rather than the RBS-widened `bool`.
|
|
105
|
+
# NOTE: `:hash` is deliberately NOT in any of these sets.
|
|
106
|
+
# `Object#hash` (and the `String`/`Symbol`/`Integer`/`Float`
|
|
107
|
+
# overrides) is salted with a per-process SipHash seed, so
|
|
108
|
+
# `"abc".hash` returns a different Integer in every Ruby
|
|
109
|
+
# process. Folding it to a `Constant` would bake one process's
|
|
110
|
+
# value into the type (and the on-disk cache), making the
|
|
111
|
+
# result non-deterministic across runs — a violation of the
|
|
112
|
+
# purity contract this catalogue rests on. A literal's `.hash`
|
|
113
|
+
# therefore stays the RBS-widened `Integer`. The deterministic
|
|
114
|
+
# siblings `:inspect` / `:to_s` remain folded.
|
|
86
115
|
INTEGER_UNARY = Set[
|
|
87
116
|
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
88
117
|
:succ, :pred, :next, :abs, :magnitude,
|
|
89
118
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
90
119
|
:floor, :ceil, :round, :truncate, :chr,
|
|
91
|
-
:inspect,
|
|
120
|
+
:inspect, :-@, :+@, :~, :to_r, :to_c
|
|
92
121
|
].freeze
|
|
93
122
|
FLOAT_UNARY = Set[
|
|
94
123
|
:zero?, :positive?, :negative?,
|
|
95
124
|
:nan?, :finite?, :infinite?,
|
|
96
125
|
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
97
126
|
:next_float, :prev_float,
|
|
98
|
-
:to_s, :to_i, :to_int, :to_f,
|
|
99
|
-
:inspect,
|
|
127
|
+
:to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
|
|
128
|
+
:inspect, :-@, :+@
|
|
100
129
|
].freeze
|
|
101
130
|
STRING_UNARY = Set[
|
|
102
131
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
@@ -104,20 +133,29 @@ module Rigor
|
|
|
104
133
|
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
|
|
105
134
|
:to_s, :to_str, :to_sym, :intern,
|
|
106
135
|
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
107
|
-
:
|
|
136
|
+
:sum, :inspect
|
|
108
137
|
].freeze
|
|
109
138
|
SYMBOL_UNARY = Set[
|
|
110
139
|
:to_s, :to_sym, :to_proc, :length, :size,
|
|
111
140
|
:empty?, :upcase, :downcase, :capitalize,
|
|
112
|
-
:swapcase, :
|
|
141
|
+
:swapcase, :succ, :next, :inspect
|
|
113
142
|
].freeze
|
|
114
|
-
BOOL_UNARY = Set[:!, :to_s, :inspect,
|
|
115
|
-
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect
|
|
143
|
+
BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
|
|
144
|
+
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect].freeze
|
|
116
145
|
RATIONAL_UNARY = Set[
|
|
117
146
|
:zero?, :integer?, :real, :abs2,
|
|
118
|
-
:conj, :conjugate, :nonzero
|
|
147
|
+
:conj, :conjugate, :nonzero?,
|
|
148
|
+
:numerator, :denominator, :abs, :magnitude,
|
|
149
|
+
:to_f, :to_i, :to_int, :to_r, :rationalize,
|
|
150
|
+
:floor, :ceil, :round, :truncate,
|
|
151
|
+
:-@, :+@
|
|
152
|
+
].freeze
|
|
153
|
+
COMPLEX_UNARY = Set[
|
|
154
|
+
:zero?, :nonzero?,
|
|
155
|
+
:abs, :magnitude, :abs2, :arg, :angle, :phase,
|
|
156
|
+
:conjugate, :conj, :real, :imaginary, :imag,
|
|
157
|
+
:to_c, :-@, :+@
|
|
119
158
|
].freeze
|
|
120
|
-
COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
|
|
121
159
|
|
|
122
160
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
123
161
|
|
|
@@ -386,9 +424,15 @@ module Rigor
|
|
|
386
424
|
# Only fires on a single-receiver Range with finite integer
|
|
387
425
|
# endpoints; mixed unions fall through so the existing
|
|
388
426
|
# union-of-Constants path keeps the rest of the arms.
|
|
389
|
-
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax
|
|
427
|
+
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
|
|
428
|
+
:sum].freeze
|
|
429
|
+
# 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
|
|
430
|
+
# `take(n)` return the first `n` elements, `last(n)` the final `n` —
|
|
431
|
+
# each lifts to a per-position `Tuple[Constant[Integer]…]`. The
|
|
432
|
+
# no-arg `first` / `last` stay on the unary path (single Integer).
|
|
433
|
+
RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take].freeze
|
|
390
434
|
RANGE_TO_A_LIMIT = 16
|
|
391
|
-
private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
|
|
435
|
+
private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
|
|
392
436
|
|
|
393
437
|
def try_fold_range_constant_unary(receiver_values, method_name)
|
|
394
438
|
return nil unless RANGE_FOLD_METHODS.include?(method_name)
|
|
@@ -408,6 +452,11 @@ module Rigor
|
|
|
408
452
|
when :last, :max then range_endpoint_constant(range, :last)
|
|
409
453
|
when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
|
|
410
454
|
when :minmax then range_minmax_tuple(range)
|
|
455
|
+
# `range.sum` is closed-form (Gauss) for an integer range, so a
|
|
456
|
+
# huge range still costs O(1) and yields a single Integer — no
|
|
457
|
+
# materialisation, no cap needed. Endless ranges are already
|
|
458
|
+
# excluded by the Integer-endpoint guard in the caller.
|
|
459
|
+
when :sum then Type::Combinator.constant_of(range.sum)
|
|
411
460
|
end
|
|
412
461
|
end
|
|
413
462
|
|
|
@@ -441,10 +490,46 @@ module Rigor
|
|
|
441
490
|
)
|
|
442
491
|
end
|
|
443
492
|
|
|
493
|
+
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
|
|
494
|
+
# tail forms. `first`/`last` already fold no-arg through the unary
|
|
495
|
+
# path; this is the n-arg sibling, mirroring the Tuple carrier's
|
|
496
|
+
# `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
|
|
497
|
+
def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
498
|
+
return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
|
|
499
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
500
|
+
|
|
501
|
+
range = receiver_values.first
|
|
502
|
+
return nil unless range.is_a?(Range)
|
|
503
|
+
return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
504
|
+
|
|
505
|
+
range_take_tuple(range, method_name, arg_values.first)
|
|
506
|
+
rescue StandardError
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def range_take_tuple(range, method_name, count)
|
|
511
|
+
return nil unless count.is_a?(Integer) && !count.negative?
|
|
512
|
+
# `first(n)`/`last(n)`/`take(n)` materialise at most `min(n, size)`
|
|
513
|
+
# elements; cap that count so a huge `n` (or range) never blows up
|
|
514
|
+
# the Constant. `Range#size` is O(1) for integer endpoints.
|
|
515
|
+
return nil if [count, range.size].min > RANGE_TO_A_LIMIT
|
|
516
|
+
|
|
517
|
+
values = method_name == :last ? range.last(count) : range.first(count)
|
|
518
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
519
|
+
|
|
520
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
521
|
+
end
|
|
522
|
+
|
|
444
523
|
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
524
|
+
range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
525
|
+
return range_lift if range_lift
|
|
526
|
+
|
|
445
527
|
string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
446
528
|
return string_lift if string_lift
|
|
447
529
|
|
|
530
|
+
integer_lift = try_fold_integer_array_binary(receiver_values, method_name, arg_values)
|
|
531
|
+
return integer_lift if integer_lift
|
|
532
|
+
|
|
448
533
|
pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
|
|
449
534
|
return pathname_lift if pathname_lift
|
|
450
535
|
|
|
@@ -456,15 +541,20 @@ module Rigor
|
|
|
456
541
|
end
|
|
457
542
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
458
543
|
end
|
|
459
|
-
# v0.0.7 — `Constant<String>#chars` / `bytes` / `
|
|
460
|
-
# `split` (no-arg) return a Ruby Array of foldable
|
|
544
|
+
# v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
|
|
545
|
+
# `lines` / `split` (no-arg) return a Ruby Array of foldable
|
|
461
546
|
# scalars; `foldable_constant_value?` rejects Array
|
|
462
547
|
# results, so the standard unary path declines. Lift the
|
|
463
548
|
# Array to a per-position `Tuple[Constant…]` directly,
|
|
464
549
|
# capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
|
|
465
|
-
# bounded for long strings.
|
|
466
|
-
|
|
467
|
-
|
|
550
|
+
# bounded for long strings. (`codepoints` yields per-character
|
|
551
|
+
# Integer codepoints, the sibling of the byte-valued `bytes`.)
|
|
552
|
+
STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :lines, :split].freeze
|
|
553
|
+
# `partition` / `rpartition` always return a fixed 3-element
|
|
554
|
+
# `[head, separator, tail]` Array whose members are substrings of
|
|
555
|
+
# the receiver (bounded by the input), so they lift to a precise
|
|
556
|
+
# 3-slot `Tuple[Constant…]`.
|
|
557
|
+
STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
|
|
468
558
|
STRING_ARRAY_LIFT_LIMIT = 32
|
|
469
559
|
private_constant :STRING_ARRAY_UNARY_METHODS,
|
|
470
560
|
:STRING_ARRAY_BINARY_METHODS,
|
|
@@ -494,6 +584,14 @@ module Rigor
|
|
|
494
584
|
INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
|
|
495
585
|
private_constant :INTEGER_ARRAY_UNARY_METHODS
|
|
496
586
|
|
|
587
|
+
# 1-arg Integer methods that return an Array of foldable
|
|
588
|
+
# Integers: `digits(base)` (base-n place values; raises on a
|
|
589
|
+
# negative receiver or base < 2 → declines) and `gcdlcm(other)`
|
|
590
|
+
# (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
|
|
591
|
+
# result lifts to a `Tuple[Constant[Integer]…]`.
|
|
592
|
+
INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
|
|
593
|
+
private_constant :INTEGER_ARRAY_BINARY_METHODS
|
|
594
|
+
|
|
497
595
|
# v0.0.7 — `Constant<Pathname>` delegates to a curated set
|
|
498
596
|
# of pure path-manipulation methods. Pathname is immutable
|
|
499
597
|
# in Ruby (per its docstring) and the catalog classifies
|
|
@@ -613,6 +711,25 @@ module Rigor
|
|
|
613
711
|
nil
|
|
614
712
|
end
|
|
615
713
|
|
|
714
|
+
# `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
|
|
715
|
+
# 1-arg Array-returning Integer methods. `digits(base)` declines
|
|
716
|
+
# on a negative receiver (the unary path's guard); other domain
|
|
717
|
+
# errors (base < 2) raise and are rescued. `gcdlcm` is total over
|
|
718
|
+
# Integer args.
|
|
719
|
+
def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
|
|
720
|
+
return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
|
|
721
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
722
|
+
|
|
723
|
+
receiver = receiver_values.first
|
|
724
|
+
arg = arg_values.first
|
|
725
|
+
return nil unless receiver.is_a?(Integer) && arg.is_a?(Integer)
|
|
726
|
+
return nil if method_name == :digits && receiver.negative?
|
|
727
|
+
|
|
728
|
+
lift_array_result(receiver.public_send(method_name, arg))
|
|
729
|
+
rescue StandardError
|
|
730
|
+
nil
|
|
731
|
+
end
|
|
732
|
+
|
|
616
733
|
# `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
|
|
617
734
|
# to `Tuple[Constant[re], Constant[im]]`. Both components are always
|
|
618
735
|
# numeric (Integer or Float for literal complexes), so they satisfy
|
|
@@ -1334,7 +1451,24 @@ module Rigor
|
|
|
1334
1451
|
private_constant :FOLDABLE_CONSTANT_CLASSES
|
|
1335
1452
|
|
|
1336
1453
|
def foldable_constant_value?(value)
|
|
1337
|
-
FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1454
|
+
return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1455
|
+
|
|
1456
|
+
# A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
|
|
1457
|
+
# or a NaN-bearing Complex) is non-reflexive under `==`, so a
|
|
1458
|
+
# `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
|
|
1459
|
+
# `build_constant_type` relies on for union dedup. Decline the
|
|
1460
|
+
# fold and let the RBS tier answer with the widened class.
|
|
1461
|
+
return false if value.is_a?(Float) && value.nan?
|
|
1462
|
+
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1463
|
+
|
|
1464
|
+
true
|
|
1465
|
+
end
|
|
1466
|
+
|
|
1467
|
+
# True when either component of a Complex is NaN.
|
|
1468
|
+
def complex_nan?(value)
|
|
1469
|
+
real = value.real
|
|
1470
|
+
imag = value.imaginary
|
|
1471
|
+
(real.is_a?(Float) && real.nan?) || (imag.is_a?(Float) && imag.nan?)
|
|
1338
1472
|
end
|
|
1339
1473
|
|
|
1340
1474
|
def safe?(receiver_value, method_name, arg_value)
|
|
@@ -1355,6 +1489,7 @@ module Rigor
|
|
|
1355
1489
|
when true, false then BOOL_BINARY
|
|
1356
1490
|
when nil then NIL_BINARY
|
|
1357
1491
|
when Rational then RATIONAL_BINARY
|
|
1492
|
+
when Complex then COMPLEX_BINARY
|
|
1358
1493
|
else Set.new
|
|
1359
1494
|
end
|
|
1360
1495
|
end
|