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.
@@ -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 (e.g., `def foo[T]: (T) -> T`) are not bound; their variables remain
48
- # `Dynamic[Top]` after substitution.
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
- # ADR-100 WD2/WD3 — the return-typing tier is where `void → top` widens, so it is the one place
359
- # that still knows the RBS return was an author-declared `-> void` before the translator erases
360
- # it to a plain `top`. Record the recovery on the scope's `void_origins` side-table, keyed by the
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 times a long separator must not
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
- def class_new_lift(class_name, arg_types)
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
- return Type::Combinator.singleton_of("Object") if arg_types.empty?
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 a third consumer of `Narrowing.predicate_certainty`,
19
- # and unlike `flow.always-truthy-condition` and the `&&` / `||` gate it is not constrained by the spec
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 `?`.