rigortype 0.3.2 → 0.3.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/docs/manual/04-diagnostics.md +8 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +56 -13
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/scope_indexer.rb +119 -28
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor.rbs +4 -0
- metadata +5 -1
|
@@ -44,8 +44,11 @@ module Rigor
|
|
|
44
44
|
# * `block_type:` is ignored; method types that constrain the block return type are not yet honored.
|
|
45
45
|
# * Keyword arguments are not threaded through call_arg_types, so overloads with required keywords
|
|
46
46
|
# are skipped (they cannot match the empty kwargs we send).
|
|
47
|
-
# * Method-level type parameters
|
|
48
|
-
#
|
|
47
|
+
# * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
|
|
48
|
+
# and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
|
|
49
|
+
# `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
|
|
50
|
+
# such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
|
|
51
|
+
# a rest positional (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
|
|
49
52
|
#
|
|
50
53
|
# See docs/adr/4-type-inference-engine.md for the broader plan.
|
|
51
54
|
# rubocop:disable Metrics/ModuleLength
|
|
@@ -61,6 +64,14 @@ module Rigor
|
|
|
61
64
|
# objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
|
|
62
65
|
ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
|
|
63
66
|
|
|
67
|
+
# Shared empty returns for the argument-position type-variable binding (issue #303). The
|
|
68
|
+
# no-candidate answer is by far the common case — every non-generic overload takes it — so it must
|
|
69
|
+
# not allocate.
|
|
70
|
+
EMPTY_TYPE_VARS = {}.freeze
|
|
71
|
+
private_constant :EMPTY_TYPE_VARS
|
|
72
|
+
EMPTY_TYPE_PARAM_NAMES = [].freeze
|
|
73
|
+
private_constant :EMPTY_TYPE_PARAM_NAMES
|
|
74
|
+
|
|
64
75
|
# @param receiver [Rigor::Type]
|
|
65
76
|
# @param method_name [Symbol]
|
|
66
77
|
# @param args [Array<Rigor::Type>]
|
|
@@ -355,21 +366,9 @@ module Rigor
|
|
|
355
366
|
)
|
|
356
367
|
return nil unless method_type
|
|
357
368
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
# call node, so the `static.value-use.void` check rule can fire when this `top` is used in value
|
|
362
|
-
# context. Recording is gated on `scope` && `call_node` being present, which naturally scopes it
|
|
363
|
-
# to the *direct*-RBS dispatch (the receiver's own resolvable class): the user-class / Object
|
|
364
|
-
# ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
365
|
-
record_void_recovery(method_type, scope, call_node, [class_name, method_name, kind])
|
|
366
|
-
# Issue #286 — the same call site is the one place that still knows the selected overload spelled
|
|
367
|
-
# its miss as `%a{implicitly-returns-nil}` rather than as `?`; the translator below reads the
|
|
368
|
-
# return type only, by the deliberate choice the spec records. Mark the result so a certainty
|
|
369
|
-
# judgment downstream can tell "nil-free because of its class" from "nil-free because we bet".
|
|
370
|
-
record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
371
|
-
|
|
372
|
-
full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
369
|
+
call_site = [class_name, method_name, kind]
|
|
370
|
+
record_dispatch_provenance(method_definition, method_type, scope, call_node, call_site)
|
|
371
|
+
full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
373
372
|
|
|
374
373
|
RbsTypeTranslator.translate(
|
|
375
374
|
method_type.type.return_type,
|
|
@@ -379,6 +378,34 @@ module Rigor
|
|
|
379
378
|
)
|
|
380
379
|
end
|
|
381
380
|
|
|
381
|
+
# The two provenance side-tables the return-typing tier is the last place able to populate, recorded
|
|
382
|
+
# together because they share one gate (`scope` && `call_node`) and one call site.
|
|
383
|
+
#
|
|
384
|
+
# ADR-100 WD2/WD3 — the return-typing tier is where `void → top` widens, so it is the one place that
|
|
385
|
+
# still knows the RBS return was an author-declared `-> void` before the translator erases it to a
|
|
386
|
+
# plain `top`. The recovery lands on the scope's `void_origins` side-table, keyed by the call node,
|
|
387
|
+
# so the `static.value-use.void` check rule can fire when this `top` is used in value context. The
|
|
388
|
+
# gate naturally scopes it to the *direct*-RBS dispatch (the receiver's own resolvable class): the
|
|
389
|
+
# user-class / Object ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
390
|
+
#
|
|
391
|
+
# Issue #286 — the same call site is the one place that still knows the selected overload spelled
|
|
392
|
+
# its miss as `%a{implicitly-returns-nil}` rather than as `?`; the translator reads the return type
|
|
393
|
+
# only, by the deliberate choice the spec records. Mark the result so a certainty judgment
|
|
394
|
+
# downstream can tell "nil-free because of its class" from "nil-free because we bet".
|
|
395
|
+
def record_dispatch_provenance(method_definition, method_type, scope, call_node, call_site)
|
|
396
|
+
record_void_recovery(method_type, scope, call_node, call_site)
|
|
397
|
+
record_optimistic_nil_free(method_definition, method_type, scope, call_node)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# The two method-level type-parameter binding positions, layered in precedence order over the
|
|
401
|
+
# receiver-derived `type_vars`: the block return type first, then the argument positions (which
|
|
402
|
+
# never displace an existing key). See {#compose_arg_type_vars} for the argument envelope.
|
|
403
|
+
def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
|
|
404
|
+
vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
405
|
+
compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
|
|
406
|
+
call_site: call_site)
|
|
407
|
+
end
|
|
408
|
+
|
|
382
409
|
# Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
|
|
383
410
|
# `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
|
|
384
411
|
# kind]` triple the {VoidOrigin} carries.
|
|
@@ -434,6 +461,109 @@ module Rigor
|
|
|
434
461
|
type_vars.merge(block_var_name => block_type)
|
|
435
462
|
end
|
|
436
463
|
|
|
464
|
+
# Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
|
|
465
|
+
# receiver-derived and block-derived `type_vars` exactly as {#compose_block_type_vars} does, so
|
|
466
|
+
# `Ractor.make_shareable("x")` (`[T] (T) -> T`) answers `"x"` instead of `Dynamic[top]`.
|
|
467
|
+
#
|
|
468
|
+
# Envelope, deliberately the narrowest sound shape:
|
|
469
|
+
#
|
|
470
|
+
# * only a positional parameter (required or optional) whose declared type is EXACTLY
|
|
471
|
+
# `RBS::Types::Variable` — no container walk, so `(Array[T]) -> T` still degrades;
|
|
472
|
+
# * only names the SELECTED overload declares in its own `type_params` (a class-level variable
|
|
473
|
+
# keeps its receiver-derived binding);
|
|
474
|
+
# * an existing key wins, so the receiver and the block return type both outrank an argument;
|
|
475
|
+
# * a `Dynamic[Top]` argument carries no evidence and contributes nothing;
|
|
476
|
+
# * repeated occurrences of one variable union their arguments.
|
|
477
|
+
#
|
|
478
|
+
# See {#arg_binding_permitted?} for why the contribution is gated on the call site.
|
|
479
|
+
def compose_arg_type_vars(method_type, type_vars, args, scope:, call_node:, call_site:)
|
|
480
|
+
bindings = arg_type_var_bindings(method_type, type_vars, args)
|
|
481
|
+
return type_vars if bindings.empty?
|
|
482
|
+
return type_vars unless arg_binding_permitted?(scope, call_node, call_site)
|
|
483
|
+
|
|
484
|
+
type_vars.merge(bindings)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# The candidate bindings, computed before the guard so a non-generic overload (the overwhelming
|
|
488
|
+
# majority) never pays for the `scope` probes.
|
|
489
|
+
def arg_type_var_bindings(method_type, type_vars, args)
|
|
490
|
+
declared = declared_type_param_names(method_type)
|
|
491
|
+
return EMPTY_TYPE_VARS if declared.empty? || args.empty?
|
|
492
|
+
|
|
493
|
+
fun = method_type.type
|
|
494
|
+
return EMPTY_TYPE_VARS unless fun.respond_to?(:required_positionals)
|
|
495
|
+
|
|
496
|
+
positionals = fun.required_positionals + fun.optional_positionals
|
|
497
|
+
positionals.zip(args).each_with_object({}) do |(param, arg), bindings|
|
|
498
|
+
next if arg.nil?
|
|
499
|
+
|
|
500
|
+
name = variable_param_name(param, declared, type_vars)
|
|
501
|
+
next if name.nil?
|
|
502
|
+
next if no_static_evidence?(arg)
|
|
503
|
+
|
|
504
|
+
bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], arg) : arg
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def declared_type_param_names(method_type)
|
|
509
|
+
params = method_type.respond_to?(:type_params) ? method_type.type_params : nil
|
|
510
|
+
return EMPTY_TYPE_PARAM_NAMES if params.nil? || params.empty?
|
|
511
|
+
|
|
512
|
+
params.map(&:name)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# The parameter's binding name when it is spelled as a bare method-level type variable that is
|
|
516
|
+
# still unbound; nil for every other shape.
|
|
517
|
+
def variable_param_name(param, declared, type_vars)
|
|
518
|
+
declared_type = param.type
|
|
519
|
+
return nil unless declared_type.is_a?(RBS::Types::Variable)
|
|
520
|
+
|
|
521
|
+
name = declared_type.name
|
|
522
|
+
return nil unless declared.include?(name)
|
|
523
|
+
return nil if type_vars.key?(name)
|
|
524
|
+
|
|
525
|
+
name
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# `Dynamic[top]` is the engine's "we could not tell" answer, so binding a variable to it would
|
|
529
|
+
# dress up an absence of evidence as an inference. The variable stays unbound and degrades as it
|
|
530
|
+
# did before, which is the same value anyway.
|
|
531
|
+
def no_static_evidence?(arg)
|
|
532
|
+
arg.is_a?(Type::Dynamic) && arg.static_facet.is_a?(Type::Top)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Issue #303 — the FP guard on the argument-position binding.
|
|
536
|
+
#
|
|
537
|
+
# Binding an argument makes the RESULT precise, which turns a benign mis-resolution into a
|
|
538
|
+
# confidently wrong type. The shape that matters is a user method shadowing the RBS method this
|
|
539
|
+
# dispatch resolved: `spec/integration/fixtures/kernel_functions.rb`'s `def p(node)` self-send
|
|
540
|
+
# resolves through `Kernel#p: [T] (T) -> T`, and binding `T` would type `p(1)` as `1` when the
|
|
541
|
+
# real method returns a String.
|
|
542
|
+
#
|
|
543
|
+
# Two gates, cheapest first.
|
|
544
|
+
#
|
|
545
|
+
# 1. `scope` && `call_node` present, the same evidence surface {#record_void_recovery} reads.
|
|
546
|
+
# DIAGNOSED, not assumed: that fixture's `p(1)` reaches here through
|
|
547
|
+
# `MethodDispatcher#try_user_class_fallback`, which dispatches with `scope: nil, call_node:
|
|
548
|
+
# nil` — so the presence gate alone already declines every call routed through the Object /
|
|
549
|
+
# Kernel ancestor fallback, the path a class with no RBS of its own always takes.
|
|
550
|
+
# 2. The explicit redefinition probe, in the shape `KernelDispatch#user_redefined?` established.
|
|
551
|
+
# The presence gate does NOT cover the direct-dispatch spelling of the same hazard — a
|
|
552
|
+
# top-level `def p(x)` called from the top level resolves `Nominal[Object]` DIRECTLY, with
|
|
553
|
+
# scope and call node both live — so a discovered top-level def, or a discovered method on the
|
|
554
|
+
# resolved class itself, declines too. Over-wide by construction (a project class that also
|
|
555
|
+
# has RBS declines for its own self-sends, losing precision it could have kept), and
|
|
556
|
+
# deliberately so: the cost is a `Dynamic[top]` that was already there.
|
|
557
|
+
def arg_binding_permitted?(scope, call_node, call_site)
|
|
558
|
+
return false if scope.nil? || call_node.nil?
|
|
559
|
+
|
|
560
|
+
class_name, method_name, kind = call_site
|
|
561
|
+
return false if method_name.nil?
|
|
562
|
+
return false if scope.top_level_def_for(method_name)
|
|
563
|
+
|
|
564
|
+
!scope.discovered_method?(class_name, method_name, kind)
|
|
565
|
+
end
|
|
566
|
+
|
|
437
567
|
def method_type_block_return_variable(method_type)
|
|
438
568
|
return_variable = block_return_variable(method_type)
|
|
439
569
|
return nil if return_variable.nil?
|
|
@@ -77,6 +77,14 @@ module Rigor
|
|
|
77
77
|
fetch: :tuple_index,
|
|
78
78
|
dig: :tuple_dig,
|
|
79
79
|
values_at: :tuple_values_at,
|
|
80
|
+
# `concat` / `<<` are deliberately ABSENT from this table: both mutate the receiver in place
|
|
81
|
+
# (`ARRAY_MUTATORS` in `MutationWidening`), and a fold tier has no way to write the folded
|
|
82
|
+
# result back into the caller's binding — it can only return a value for the call expression.
|
|
83
|
+
# `tuple.concat(other)` would fold correctly as a VALUE (same shape as `tuple + other`) but the
|
|
84
|
+
# receiver local/ivar would still carry the pre-concat Tuple afterward, which is unsound (#121).
|
|
85
|
+
# `MutationWidening.widen_after_call` is the actual precision mechanism for a mutator call site:
|
|
86
|
+
# it forgets the literal-arity carrier so a stale `size`/`empty?` fold cannot survive the
|
|
87
|
+
# mutation, rather than trying to fold the mutator itself.
|
|
80
88
|
:+ => :tuple_concat,
|
|
81
89
|
compact: :tuple_compact,
|
|
82
90
|
take: :tuple_take,
|
|
@@ -98,14 +106,19 @@ module Rigor
|
|
|
98
106
|
rindex: :tuple_rindex,
|
|
99
107
|
flatten: :tuple_flatten,
|
|
100
108
|
join: :tuple_join,
|
|
109
|
+
# `inspect` / `to_s` (an exact alias for Array — `Array#to_s` IS `Array#inspect`) — see
|
|
110
|
+
# {#tuple_inspect}, below the join helpers it shares a byte cap with.
|
|
111
|
+
inspect: :tuple_inspect,
|
|
112
|
+
to_s: :tuple_inspect,
|
|
113
|
+
:* => :tuple_star,
|
|
101
114
|
freeze: :shape_self,
|
|
102
115
|
dup: :shape_self,
|
|
103
116
|
clone: :shape_self,
|
|
104
117
|
itself: :shape_self
|
|
105
118
|
}.freeze
|
|
106
119
|
|
|
107
|
-
# Byte cap on a folded `tuple.join` result — a huge tuple
|
|
108
|
-
# materialise an unbounded `Constant`.
|
|
120
|
+
# Byte cap on a folded `tuple.join` / `tuple.inspect` / `hash.inspect` result — a huge tuple or
|
|
121
|
+
# shape must not materialise an unbounded `Constant`.
|
|
109
122
|
TUPLE_JOIN_BYTE_LIMIT = 4096
|
|
110
123
|
private_constant :TUPLE_JOIN_BYTE_LIMIT
|
|
111
124
|
|
|
@@ -126,6 +139,13 @@ module Rigor
|
|
|
126
139
|
entries: :hash_to_a,
|
|
127
140
|
to_h: :hash_to_h,
|
|
128
141
|
to_hash: :hash_to_h,
|
|
142
|
+
# `inspect` / `to_s` (an exact alias for Hash — `Hash#to_s` IS `Hash#inspect`) — see
|
|
143
|
+
# {#hash_inspect}. Gated CLOSED-no-optional-key, same predicate `URIFolding#hash_form_pairs`
|
|
144
|
+
# uses for `URI.encode_www_form`'s HashShape argument: an open or partially-optional shape
|
|
145
|
+
# describes a hash whose real membership this fold cannot see, so its `inspect` string is not
|
|
146
|
+
# deterministic.
|
|
147
|
+
inspect: :hash_inspect,
|
|
148
|
+
to_s: :hash_inspect,
|
|
129
149
|
deconstruct_keys: :hash_deconstruct_keys,
|
|
130
150
|
invert: :hash_invert,
|
|
131
151
|
merge: :hash_merge,
|
|
@@ -823,6 +843,59 @@ module Rigor
|
|
|
823
843
|
arg.value
|
|
824
844
|
end
|
|
825
845
|
|
|
846
|
+
# `tuple.inspect` / `tuple.to_s` (`Array#to_s` is an exact alias of `Array#inspect`) — folds when
|
|
847
|
+
# every element is a `Constant`, by reconstructing the Ruby Array and calling the REAL `inspect`
|
|
848
|
+
# so the analyzer's Ruby (4.0.x's `Array#inspect` format) is matched rather than re-derived.
|
|
849
|
+
# Capped at `TUPLE_JOIN_BYTE_LIMIT` like `#join`, since a large tuple's `inspect` string is
|
|
850
|
+
# similarly unbounded.
|
|
851
|
+
def tuple_inspect(tuple, _method_name, args)
|
|
852
|
+
return nil unless args.empty?
|
|
853
|
+
|
|
854
|
+
values = constant_values(tuple.elements)
|
|
855
|
+
return nil if values.nil?
|
|
856
|
+
|
|
857
|
+
result = values.inspect
|
|
858
|
+
return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
|
|
859
|
+
|
|
860
|
+
Type::Combinator.constant_of(result)
|
|
861
|
+
rescue StandardError
|
|
862
|
+
nil
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# `tuple * n` — a `Constant[String]` argument is `Array#*`'s join-alias form (identical semantics
|
|
866
|
+
# to `#join`); a `Constant[Integer]` argument is repetition. Any other argument shape (2+ args,
|
|
867
|
+
# `Dynamic`, `Float`, no args) declines so the RBS / alias-pass tier answers — for the Integer
|
|
868
|
+
# case that tier already gives a decent union-of-elements `Array[...]` answer, so a decline here
|
|
869
|
+
# is not a regression.
|
|
870
|
+
def tuple_star(tuple, _method_name, args)
|
|
871
|
+
return nil unless args.size == 1
|
|
872
|
+
|
|
873
|
+
arg = args.first
|
|
874
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
875
|
+
|
|
876
|
+
case arg.value
|
|
877
|
+
when String then tuple_join(tuple, :join, args)
|
|
878
|
+
when Integer then tuple_repeat(tuple, arg.value)
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# `tuple * n` repetition. `n == 0` folds to the empty Tuple (any array repeated 0 times is `[]`,
|
|
883
|
+
# even for an unbounded/empty receiver). A negative `n` declines — `Array#*` raises
|
|
884
|
+
# `ArgumentError` at runtime, so no fold should invent a value for it (same fail-soft discipline
|
|
885
|
+
# every other handler in this catalogue follows). The result element count is capped at
|
|
886
|
+
# `MAX_SET_OPERATION_SIZE` (64) — the same element-count-cap convention as
|
|
887
|
+
# `ShellwordsFolding::SHELLWORDS_SPLIT_LIMIT` / `RegexpFolding`'s `Regexp.union` cap / the Tuple
|
|
888
|
+
# set operations above, reused rather than minting a second constant with the same value.
|
|
889
|
+
def tuple_repeat(tuple, count)
|
|
890
|
+
return nil if count.negative?
|
|
891
|
+
return Type::Combinator.tuple_of if count.zero?
|
|
892
|
+
|
|
893
|
+
total = tuple.elements.size * count
|
|
894
|
+
return nil if total > MAX_SET_OPERATION_SIZE
|
|
895
|
+
|
|
896
|
+
Type::Combinator.tuple_of(*(tuple.elements * count))
|
|
897
|
+
end
|
|
898
|
+
|
|
826
899
|
# `tuple.min` / `tuple.max` — fold when every element is a `Constant` whose values share a
|
|
827
900
|
# Ruby-comparable domain. Empty tuples fold to `Constant[nil]`. The 1-arg `min(n)` / `max(n)`
|
|
828
901
|
# form folds to a `Tuple` of the n edge-most values in Ruby's order (`min(n)` ascending, `max(n)`
|
|
@@ -1596,6 +1669,28 @@ module Rigor
|
|
|
1596
1669
|
shape
|
|
1597
1670
|
end
|
|
1598
1671
|
|
|
1672
|
+
# `shape.inspect` / `shape.to_s` (`Hash#to_s` is an exact alias of `Hash#inspect`) — folds only
|
|
1673
|
+
# on a CLOSED shape with no optional keys (an open or partially-optional shape's real membership
|
|
1674
|
+
# is not fully known, so its `inspect` string is not deterministic — the same gate
|
|
1675
|
+
# `URIFolding#hash_form_pairs` uses for `URI.encode_www_form`'s HashShape argument) and only when
|
|
1676
|
+
# every value is a `Constant` ({#constant_pairs}). Reconstructs the Ruby Hash and calls the REAL
|
|
1677
|
+
# `inspect` so the analyzer matches Ruby 4.0.x's `{a: 1}` hash-inspect format rather than
|
|
1678
|
+
# re-deriving it. Capped at `TUPLE_JOIN_BYTE_LIMIT` like the Tuple sibling.
|
|
1679
|
+
def hash_inspect(shape, _method_name, args)
|
|
1680
|
+
return nil unless args.empty?
|
|
1681
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
1682
|
+
|
|
1683
|
+
pairs = constant_pairs(shape)
|
|
1684
|
+
return nil if pairs.nil?
|
|
1685
|
+
|
|
1686
|
+
result = pairs.inspect
|
|
1687
|
+
return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
|
|
1688
|
+
|
|
1689
|
+
Type::Combinator.constant_of(result)
|
|
1690
|
+
rescue StandardError
|
|
1691
|
+
nil
|
|
1692
|
+
end
|
|
1693
|
+
|
|
1599
1694
|
# `shape.invert` — swaps keys and values. Folds when every value is a `Constant` whose value is a
|
|
1600
1695
|
# scalar the carrier accepts as a key (Symbol / String / Integer / Float / true / false / nil;
|
|
1601
1696
|
# see {#static_shape_key?}). Duplicate values
|
|
@@ -6,6 +6,7 @@ require_relative "../flow_contribution"
|
|
|
6
6
|
require_relative "../flow_contribution/merger"
|
|
7
7
|
require_relative "../builtins/hkt_builtins"
|
|
8
8
|
require_relative "../builtins/static_return_refinements"
|
|
9
|
+
require_relative "anonymous_meta_class"
|
|
9
10
|
require_relative "dynamic_origin"
|
|
10
11
|
require_relative "flow_tracer"
|
|
11
12
|
require_relative "void_tail_summary"
|
|
@@ -760,7 +761,7 @@ module Rigor
|
|
|
760
761
|
struct_result = StructFolding.try_dispatch(context)
|
|
761
762
|
return struct_result if struct_result
|
|
762
763
|
|
|
763
|
-
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
764
|
+
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args, context)
|
|
764
765
|
return meta_result if meta_result
|
|
765
766
|
|
|
766
767
|
PRECISE_TIERS_HEAD.each do |tier|
|
|
@@ -868,10 +869,10 @@ module Rigor
|
|
|
868
869
|
# `Foo.class` as `Singleton[Class]` (deliberate; calling `.class` on a class object yields
|
|
869
870
|
# `Class`, the metaclass). We also special-case `is_a?`-adjacent calls and the trivial
|
|
870
871
|
# `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.
|
|
871
|
-
def try_meta_introspection(receiver_type, method_name, arg_types = [])
|
|
872
|
+
def try_meta_introspection(receiver_type, method_name, arg_types = [], context = nil)
|
|
872
873
|
case method_name
|
|
873
874
|
when :class then meta_class(receiver_type)
|
|
874
|
-
when :new then meta_new(receiver_type, arg_types)
|
|
875
|
+
when :new then meta_new(receiver_type, arg_types, context)
|
|
875
876
|
end
|
|
876
877
|
end
|
|
877
878
|
|
|
@@ -891,13 +892,13 @@ module Rigor
|
|
|
891
892
|
# `Type::Constant::SCALAR_CLASSES` accepts (today: `Pathname`), `.new(Constant<…>)` lifts
|
|
892
893
|
# to a `Constant<…>` carrier so downstream method calls fold through the standard catalog
|
|
893
894
|
# tier.
|
|
894
|
-
def meta_new(receiver_type, arg_types = [])
|
|
895
|
+
def meta_new(receiver_type, arg_types = [], context = nil)
|
|
895
896
|
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
896
897
|
|
|
897
898
|
constant_lift = constant_constructor_lift(receiver_type.class_name, arg_types)
|
|
898
899
|
return constant_lift if constant_lift
|
|
899
900
|
|
|
900
|
-
array_lift = array_new_lift(receiver_type.class_name, arg_types)
|
|
901
|
+
array_lift = array_new_lift(receiver_type.class_name, arg_types, context&.block_type)
|
|
901
902
|
return array_lift if array_lift
|
|
902
903
|
|
|
903
904
|
range_lift = range_new_lift(receiver_type.class_name, arg_types)
|
|
@@ -918,7 +919,7 @@ module Rigor
|
|
|
918
919
|
struct_new_lift = struct_new_lift(receiver_type.class_name, arg_types)
|
|
919
920
|
return struct_new_lift if struct_new_lift
|
|
920
921
|
|
|
921
|
-
class_new_lift = class_new_lift(receiver_type.class_name, arg_types)
|
|
922
|
+
class_new_lift = class_new_lift(receiver_type.class_name, arg_types, context)
|
|
922
923
|
return class_new_lift if class_new_lift
|
|
923
924
|
|
|
924
925
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
@@ -955,9 +956,22 @@ module Rigor
|
|
|
955
956
|
# so `Singleton[Parent]` lets downstream `klass.some_class_method` resolve. No parent →
|
|
956
957
|
# `singleton(Object)`. Anything else (dynamic parent, more than one positional, …) falls
|
|
957
958
|
# back to `Nominal[Class]` via the surrounding `meta_new` tail.
|
|
958
|
-
|
|
959
|
+
# #319 — a `Class.new do ... end` with no parent is NOT `Object`: the block body is a class body, so the
|
|
960
|
+
# class owns whatever that body defines. Answering `Singleton[Object]` handed the call site `Object`'s
|
|
961
|
+
# zero-arity `new` and `Object`'s method surface, so an `initialize(bucket)` written right there produced
|
|
962
|
+
# `wrong number of arguments ... (given 1, expected 0)` on code Ruby runs happily. `ScopeIndexer` registers
|
|
963
|
+
# the body's methods under the call site's anonymous name; return that name so the lookups reach them.
|
|
964
|
+
#
|
|
965
|
+
# Scoped to the parentless form on purpose. `Class.new(Parent) { ... }` keeps `Singleton[Parent]`: the
|
|
966
|
+
# anonymous name would have to carry the full ancestor chain for `Parent`'s own surface to stay reachable,
|
|
967
|
+
# and the parentless case is where the false positive lives.
|
|
968
|
+
def class_new_lift(class_name, arg_types, context = nil)
|
|
959
969
|
return nil unless class_name == "Class"
|
|
960
|
-
|
|
970
|
+
|
|
971
|
+
if arg_types.empty?
|
|
972
|
+
anonymous = anonymous_class_new_name(context)
|
|
973
|
+
return Type::Combinator.singleton_of(anonymous || "Object")
|
|
974
|
+
end
|
|
961
975
|
return nil unless arg_types.size == 1
|
|
962
976
|
|
|
963
977
|
parent = arg_types.first
|
|
@@ -966,6 +980,16 @@ module Rigor
|
|
|
966
980
|
nil
|
|
967
981
|
end
|
|
968
982
|
|
|
983
|
+
# The synthetic name `ScopeIndexer` keyed this call site's block body by, or nil when the context carries no
|
|
984
|
+
# call node (internal dispatcher callers) or the call has no block. The source path is passed through
|
|
985
|
+
# verbatim — including when it is nil, which is exactly what `ScopeIndexer` saw for the same file — so the
|
|
986
|
+
# two passes derive the same name whether or not the caller supplied a path.
|
|
987
|
+
def anonymous_class_new_name(context)
|
|
988
|
+
return nil if context.nil?
|
|
989
|
+
|
|
990
|
+
AnonymousMetaClass.name_for(context.call_node, context.scope&.source_path)
|
|
991
|
+
end
|
|
992
|
+
|
|
969
993
|
# ADR-15 Phase 4b.x — `Ractor.make_shareable` on both the outer Hash and each lambda value.
|
|
970
994
|
# A plain `.freeze` leaves the Procs unshareable; reading `CONSTANT_CONSTRUCTORS[class]`
|
|
971
995
|
# from a worker Ractor would raise `Ractor::IsolationError`, which the `rescue
|
|
@@ -1000,17 +1024,24 @@ module Rigor
|
|
|
1000
1024
|
# `Tuple[…]` when `n` is a small `Constant<Integer>`. Cap at `ARRAY_NEW_TUPLE_LIMIT` (16)
|
|
1001
1025
|
# so a `Array.new(1_000_000)` does not balloon the carrier; oversize calls fall back to
|
|
1002
1026
|
# `Nominal[Array]`.
|
|
1027
|
+
#
|
|
1028
|
+
# #317 — `Array.new(n) { |i| ... }` fills every slot from the BLOCK's return type instead
|
|
1029
|
+
# of the two-arg `(n, default_value)` overload's `nil` fill. The block and the trailing
|
|
1030
|
+
# `default_value` positional are mutually exclusive per `Array#initialize`'s RBS overload
|
|
1031
|
+
# set (`(int size, ?E default_value) -> void` vs `(int size) { (Integer index) -> E } ->
|
|
1032
|
+
# void`), so a block present at the call site always wins over any (illegal, but tolerated
|
|
1033
|
+
# rather than rejected here) second positional.
|
|
1003
1034
|
ARRAY_NEW_TUPLE_LIMIT = 16
|
|
1004
1035
|
private_constant :ARRAY_NEW_TUPLE_LIMIT
|
|
1005
1036
|
|
|
1006
|
-
def array_new_lift(class_name, arg_types)
|
|
1037
|
+
def array_new_lift(class_name, arg_types, block_type = nil)
|
|
1007
1038
|
return nil unless class_name == "Array"
|
|
1008
1039
|
return nil if arg_types.empty? || arg_types.size > 2
|
|
1009
1040
|
|
|
1010
1041
|
size = array_new_size(arg_types.first)
|
|
1011
1042
|
return nil if size.nil? || size.negative? || size > ARRAY_NEW_TUPLE_LIMIT
|
|
1012
1043
|
|
|
1013
|
-
fill = array_new_fill(arg_types[1])
|
|
1044
|
+
fill = block_type || array_new_fill(arg_types[1])
|
|
1014
1045
|
Type::Combinator.tuple_of(*Array.new(size, fill))
|
|
1015
1046
|
end
|
|
1016
1047
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Inference
|
|
5
7
|
# Marks a value whose *nil-freeness* rests on Rigor's deliberate choice to ignore core RBS's
|
|
@@ -15,9 +17,16 @@ module Rigor
|
|
|
15
17
|
# attaches to — the value here is *not* `Dynamic`; it is an ordinary `Union` / `Constant` / `Nominal`
|
|
16
18
|
# that happens to have been produced optimistically.
|
|
17
19
|
#
|
|
18
|
-
# Issue #286: the `if` / `unless` branch elision is
|
|
19
|
-
#
|
|
20
|
-
# passage above. This channel is what lets a certainty judgment tell the two apart.
|
|
20
|
+
# Issue #286: the `if` / `unless` branch elision is one consumer of `Narrowing.predicate_certainty`; the
|
|
21
|
+
# other two are `flow.always-truthy-condition` and the `&&` / `||` `constant_value_polarity` gate, and the
|
|
22
|
+
# spec passage above binds all three. This channel is what lets a certainty judgment tell the two apart.
|
|
23
|
+
#
|
|
24
|
+
# Issue #313: the mark is attached to a *value* — the call node that produced it, or the local / ivar it
|
|
25
|
+
# was bound to — but every one of those consumers reads a *predicate expression*, and a predicate is
|
|
26
|
+
# rarely the bare carrier. `x.nil?` collapses the carrier's nil-freeness into a `Constant[false]` of its
|
|
27
|
+
# own, `!x.nil?` inverts it, and `x.nil? || y.nil?` composes two of them; each step produced an unmarked
|
|
28
|
+
# `Constant` that the gates then read as proof. {.resolve} therefore derives the mark through exactly
|
|
29
|
+
# those shapes, so the exclusion survives composition instead of stopping at the read.
|
|
21
30
|
module OptimisticOrigin
|
|
22
31
|
# The core-RBS annotation `RbsDispatch` reads the return type past.
|
|
23
32
|
ANNOTATION = "implicitly-returns-nil"
|
|
@@ -26,8 +35,64 @@ module Rigor
|
|
|
26
35
|
# distinguish further optimistic families without changing the table's shape.
|
|
27
36
|
IMPLICITLY_RETURNS_NIL = :implicitly_returns_nil
|
|
28
37
|
|
|
38
|
+
# The argument-free unary predicates whose folded result is a statement about the receiver's
|
|
39
|
+
# *nil-freeness* and nothing else, which is what makes the derivation sound rather than a general taint:
|
|
40
|
+
# `nil?` answers the exact question the optimism is a bet on, and `!` (which Prism spells as a `CallNode`
|
|
41
|
+
# named `:!`, covering both `!x` and `not x`) inverts whatever it is applied to. Value predicates —
|
|
42
|
+
# `empty?`, `zero?`, `any?` — are deliberately absent: they fold from the carrier's *value*, and marking
|
|
43
|
+
# them would widen this channel into a taint that silences genuine diagnostics.
|
|
44
|
+
NIL_COLLAPSING_PREDICATES = %i[nil? !].freeze
|
|
45
|
+
|
|
29
46
|
module_function
|
|
30
47
|
|
|
48
|
+
# The effective optimistic-nil-free cause of an expression under `scope`, or nil when its nil-freeness is
|
|
49
|
+
# a property of the value rather than a bet. The single owner of the judgment: `ExpressionTyper`,
|
|
50
|
+
# `StatementEvaluator` and `AlwaysTruthyConditionCollector` all route here, so the three consumers the
|
|
51
|
+
# spec binds cannot drift apart.
|
|
52
|
+
#
|
|
53
|
+
# Resolution order — the mark recorded on the node itself, then the binding a bare local / ivar read (or
|
|
54
|
+
# a write in value position, `if (x = MAP[k])`) resolves through, then the predicate-fold derivation
|
|
55
|
+
# issue #313 added.
|
|
56
|
+
#
|
|
57
|
+
# @param node [Prism::Node, nil]
|
|
58
|
+
# @param scope [Rigor::Scope, nil]
|
|
59
|
+
# @return [Symbol, nil]
|
|
60
|
+
def resolve(node, scope)
|
|
61
|
+
return nil if node.nil? || scope.nil?
|
|
62
|
+
|
|
63
|
+
recorded = scope.optimistic_origins[node]
|
|
64
|
+
return recorded if recorded
|
|
65
|
+
|
|
66
|
+
case node
|
|
67
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.optimistic_local(node.name)
|
|
68
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.optimistic_ivar(node.name)
|
|
69
|
+
when Prism::AndNode, Prism::OrNode then resolve(node.left, scope) || resolve(node.right, scope)
|
|
70
|
+
when Prism::CallNode then resolve_through_predicate(node, scope)
|
|
71
|
+
when Prism::ParenthesesNode then resolve_through_parentheses(node, scope)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `recv.nil?` / `!recv` — the fold is a statement about `recv`, so it is exactly as optimistic as `recv`
|
|
76
|
+
# is. A block or any argument means this is not the unary predicate it looks like (`x.!(y)` is a
|
|
77
|
+
# user-defined operator), and the derivation declines.
|
|
78
|
+
def resolve_through_predicate(node, scope)
|
|
79
|
+
return nil unless NIL_COLLAPSING_PREDICATES.include?(node.name)
|
|
80
|
+
return nil unless node.block.nil?
|
|
81
|
+
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
82
|
+
|
|
83
|
+
resolve(node.receiver, scope)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `(x.nil?)` — a single-statement parenthesised body is its own value, and authors do parenthesise a
|
|
87
|
+
# composed guard. A multi-statement body's value is its last statement, but the earlier statements can
|
|
88
|
+
# rebind, so only the single-statement form is derived.
|
|
89
|
+
def resolve_through_parentheses(node, scope)
|
|
90
|
+
body = node.body
|
|
91
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
92
|
+
|
|
93
|
+
resolve(body.body.first, scope)
|
|
94
|
+
end
|
|
95
|
+
|
|
31
96
|
# Whether the overload the selector actually picked carries the ignored annotation. The judgment is
|
|
32
97
|
# per-overload, which is what makes it precise: `Array#first` is optimistic while `Array#first(3)` is
|
|
33
98
|
# not, and `String#[]` / `Enumerable#find` are honest because they already spell the miss as `?`.
|