rigortype 0.0.1

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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +373 -0
  3. data/README.md +152 -0
  4. data/exe/rigor +9 -0
  5. data/lib/rigor/analysis/check_rules.rb +503 -0
  6. data/lib/rigor/analysis/diagnostic.rb +35 -0
  7. data/lib/rigor/analysis/fact_store.rb +133 -0
  8. data/lib/rigor/analysis/result.rb +29 -0
  9. data/lib/rigor/analysis/runner.rb +119 -0
  10. data/lib/rigor/ast/type_node.rb +41 -0
  11. data/lib/rigor/ast.rb +22 -0
  12. data/lib/rigor/cli/type_of_command.rb +160 -0
  13. data/lib/rigor/cli/type_of_renderer.rb +88 -0
  14. data/lib/rigor/cli/type_scan_command.rb +160 -0
  15. data/lib/rigor/cli/type_scan_renderer.rb +165 -0
  16. data/lib/rigor/cli/type_scan_report.rb +32 -0
  17. data/lib/rigor/cli.rb +195 -0
  18. data/lib/rigor/configuration.rb +49 -0
  19. data/lib/rigor/environment/class_registry.rb +141 -0
  20. data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
  21. data/lib/rigor/environment/rbs_loader.rb +244 -0
  22. data/lib/rigor/environment.rb +177 -0
  23. data/lib/rigor/inference/acceptance.rb +444 -0
  24. data/lib/rigor/inference/block_parameter_binder.rb +198 -0
  25. data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
  26. data/lib/rigor/inference/coverage_scanner.rb +85 -0
  27. data/lib/rigor/inference/expression_typer.rb +831 -0
  28. data/lib/rigor/inference/fallback.rb +35 -0
  29. data/lib/rigor/inference/fallback_tracer.rb +64 -0
  30. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
  31. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
  32. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
  33. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +213 -0
  35. data/lib/rigor/inference/method_parameter_binder.rb +257 -0
  36. data/lib/rigor/inference/multi_target_binder.rb +143 -0
  37. data/lib/rigor/inference/narrowing.rb +1008 -0
  38. data/lib/rigor/inference/rbs_type_translator.rb +219 -0
  39. data/lib/rigor/inference/scope_indexer.rb +468 -0
  40. data/lib/rigor/inference/statement_evaluator.rb +1017 -0
  41. data/lib/rigor/rbs_extended.rb +98 -0
  42. data/lib/rigor/scope.rb +340 -0
  43. data/lib/rigor/source/node_locator.rb +104 -0
  44. data/lib/rigor/source/node_walker.rb +37 -0
  45. data/lib/rigor/source.rb +15 -0
  46. data/lib/rigor/testing.rb +65 -0
  47. data/lib/rigor/trinary.rb +108 -0
  48. data/lib/rigor/type/accepts_result.rb +109 -0
  49. data/lib/rigor/type/bot.rb +57 -0
  50. data/lib/rigor/type/combinator.rb +148 -0
  51. data/lib/rigor/type/constant.rb +90 -0
  52. data/lib/rigor/type/dynamic.rb +60 -0
  53. data/lib/rigor/type/hash_shape.rb +246 -0
  54. data/lib/rigor/type/nominal.rb +83 -0
  55. data/lib/rigor/type/singleton.rb +65 -0
  56. data/lib/rigor/type/top.rb +56 -0
  57. data/lib/rigor/type/tuple.rb +84 -0
  58. data/lib/rigor/type/union.rb +65 -0
  59. data/lib/rigor/type.rb +23 -0
  60. data/lib/rigor/version.rb +5 -0
  61. data/lib/rigor.rb +29 -0
  62. data/sig/rigor/analysis/fact_store.rbs +51 -0
  63. data/sig/rigor/ast.rbs +11 -0
  64. data/sig/rigor/environment.rbs +59 -0
  65. data/sig/rigor/inference.rbs +151 -0
  66. data/sig/rigor/rbs_extended.rbs +22 -0
  67. data/sig/rigor/scope.rbs +49 -0
  68. data/sig/rigor/source.rbs +20 -0
  69. data/sig/rigor/testing.rbs +9 -0
  70. data/sig/rigor/trinary.rbs +29 -0
  71. data/sig/rigor/type.rbs +171 -0
  72. data/sig/rigor.rbs +70 -0
  73. metadata +260 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Three-valued logic value object shared by capability queries, relational
5
+ # queries, and any analyzer surface that distinguishes "proven yes",
6
+ # "proven no", and "cannot prove either".
7
+ #
8
+ # See docs/type-specification/relations-and-certainty.md for semantics and
9
+ # docs/internal-spec/internal-type-api.md for the contract.
10
+ class Trinary
11
+ VALUES = %i[yes no maybe].freeze
12
+
13
+ class << self
14
+ def yes
15
+ @yes ||= new(:yes).freeze
16
+ end
17
+
18
+ def no
19
+ @no ||= new(:no).freeze
20
+ end
21
+
22
+ def maybe
23
+ @maybe ||= new(:maybe).freeze
24
+ end
25
+
26
+ def from_symbol(symbol)
27
+ case symbol
28
+ when :yes then yes
29
+ when :no then no
30
+ when :maybe then maybe
31
+ else
32
+ raise ArgumentError, "unknown trinary value: #{symbol.inspect}"
33
+ end
34
+ end
35
+ end
36
+
37
+ attr_reader :value
38
+
39
+ def initialize(value)
40
+ raise ArgumentError, "unknown trinary value: #{value.inspect}" unless VALUES.include?(value)
41
+
42
+ @value = value
43
+ end
44
+
45
+ def yes?
46
+ value == :yes
47
+ end
48
+
49
+ def no?
50
+ value == :no
51
+ end
52
+
53
+ def maybe?
54
+ value == :maybe
55
+ end
56
+
57
+ def negate
58
+ case value
59
+ when :yes then self.class.no
60
+ when :no then self.class.yes
61
+ when :maybe then self.class.maybe
62
+ end
63
+ end
64
+
65
+ # Conjunction. yes & yes = yes, no with anything = no, otherwise maybe.
66
+ def and(other)
67
+ coerced = coerce(other)
68
+ return self.class.no if no? || coerced.no?
69
+ return self.class.yes if yes? && coerced.yes?
70
+
71
+ self.class.maybe
72
+ end
73
+
74
+ # Disjunction. yes with anything = yes, no & no = no, otherwise maybe.
75
+ def or(other)
76
+ coerced = coerce(other)
77
+ return self.class.yes if yes? || coerced.yes?
78
+ return self.class.no if no? && coerced.no?
79
+
80
+ self.class.maybe
81
+ end
82
+
83
+ def ==(other)
84
+ other.is_a?(Trinary) && value == other.value
85
+ end
86
+ alias eql? ==
87
+
88
+ def hash
89
+ value.hash
90
+ end
91
+
92
+ def to_s
93
+ value.to_s
94
+ end
95
+
96
+ def inspect
97
+ "#<Rigor::Trinary #{value}>"
98
+ end
99
+
100
+ private
101
+
102
+ def coerce(other)
103
+ return other if other.is_a?(Trinary)
104
+
105
+ raise TypeError, "expected Rigor::Trinary, got #{other.class}"
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # Immutable value object returned by `Rigor::Type#accepts(other, mode:)`.
8
+ # Carries the three-valued answer alongside the boundary mode the answer
9
+ # was computed under and an ordered list of textual reasons describing
10
+ # which rules fired.
11
+ #
12
+ # AcceptsResult is the dual of `SubtypeResult` (Slice 5+). Acceptance
13
+ # answers "is `other` passable to `self` at a method-parameter or
14
+ # assignment boundary?", consulting the gradual-typing rules in
15
+ # docs/type-specification/value-lattice.md when `mode` is `:gradual`,
16
+ # and the strict subset relation when `mode` is `:strict`. Phase 2c
17
+ # ships full `:gradual` semantics; `:strict` is reserved for later
18
+ # slices and currently raises ArgumentError.
19
+ #
20
+ # Reasons are stored as plain strings for now. Slice 5+ MAY upgrade
21
+ # them to structured records (rule id, supporting facts, dynamic
22
+ # provenance); callers MUST treat the reasons array as opaque except
23
+ # for human-readable logging.
24
+ #
25
+ # See docs/internal-spec/internal-type-api.md ("Result Value Objects").
26
+ class AcceptsResult
27
+ MODES = %i[gradual strict].freeze
28
+ private_constant :MODES
29
+
30
+ attr_reader :trinary, :mode, :reasons
31
+
32
+ # @param trinary [Rigor::Trinary]
33
+ # @param mode [Symbol] currently `:gradual` (default) or `:strict`.
34
+ # @param reasons [Array<String>, String, nil] textual reasons; a
35
+ # single string is wrapped, `nil` becomes an empty array.
36
+ def initialize(trinary, mode: :gradual, reasons: nil)
37
+ raise ArgumentError, "trinary must be Rigor::Trinary, got #{trinary.class}" unless trinary.is_a?(Trinary)
38
+ raise ArgumentError, "mode must be one of #{MODES.inspect}, got #{mode.inspect}" unless MODES.include?(mode)
39
+
40
+ @trinary = trinary
41
+ @mode = mode
42
+ @reasons = normalize_reasons(reasons).freeze
43
+ freeze
44
+ end
45
+
46
+ class << self
47
+ def yes(mode: :gradual, reasons: nil)
48
+ new(Trinary.yes, mode: mode, reasons: reasons)
49
+ end
50
+
51
+ def no(mode: :gradual, reasons: nil)
52
+ new(Trinary.no, mode: mode, reasons: reasons)
53
+ end
54
+
55
+ def maybe(mode: :gradual, reasons: nil)
56
+ new(Trinary.maybe, mode: mode, reasons: reasons)
57
+ end
58
+ end
59
+
60
+ def yes?
61
+ trinary.yes?
62
+ end
63
+
64
+ def no?
65
+ trinary.no?
66
+ end
67
+
68
+ def maybe?
69
+ trinary.maybe?
70
+ end
71
+
72
+ # Returns a new AcceptsResult whose reasons list is `self.reasons`
73
+ # with `reason` appended. Used by combinator-style routing in
74
+ # {Rigor::Inference::Acceptance} to thread context through nested
75
+ # acceptance checks without mutating any object.
76
+ def with_reason(reason)
77
+ return self if reason.nil? || reason.empty?
78
+
79
+ self.class.new(trinary, mode: mode, reasons: reasons + [reason])
80
+ end
81
+
82
+ def ==(other)
83
+ other.is_a?(AcceptsResult) &&
84
+ trinary == other.trinary &&
85
+ mode == other.mode &&
86
+ reasons == other.reasons
87
+ end
88
+ alias eql? ==
89
+
90
+ def hash
91
+ [AcceptsResult, trinary, mode, reasons].hash
92
+ end
93
+
94
+ def inspect
95
+ "#<Rigor::Type::AcceptsResult #{trinary.inspect} mode=#{mode}>"
96
+ end
97
+
98
+ private
99
+
100
+ def normalize_reasons(reasons)
101
+ case reasons
102
+ when nil then []
103
+ when Array then reasons.dup
104
+ else [reasons.to_s]
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # The bottom of the value lattice: contains no values. The result type of
8
+ # expressions that cannot terminate normally. See
9
+ # docs/type-specification/special-types.md.
10
+ class Bot
11
+ class << self
12
+ def instance
13
+ @instance ||= new.freeze
14
+ end
15
+
16
+ private :new
17
+ end
18
+
19
+ def describe(_verbosity = :short)
20
+ "bot"
21
+ end
22
+
23
+ def erase_to_rbs
24
+ "bot"
25
+ end
26
+
27
+ def top
28
+ Trinary.no
29
+ end
30
+
31
+ def bot
32
+ Trinary.yes
33
+ end
34
+
35
+ def dynamic
36
+ Trinary.no
37
+ end
38
+
39
+ def accepts(other, mode: :gradual)
40
+ Inference::Acceptance.accepts(self, other, mode: mode)
41
+ end
42
+
43
+ def ==(other)
44
+ other.is_a?(Bot)
45
+ end
46
+ alias eql? ==
47
+
48
+ def hash
49
+ Bot.hash
50
+ end
51
+
52
+ def inspect
53
+ "#<Rigor::Type::Bot>"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "top"
4
+ require_relative "bot"
5
+ require_relative "dynamic"
6
+ require_relative "nominal"
7
+ require_relative "singleton"
8
+ require_relative "constant"
9
+ require_relative "tuple"
10
+ require_relative "hash_shape"
11
+ require_relative "union"
12
+
13
+ module Rigor
14
+ module Type
15
+ # Factory entry point that routes every public construction through the
16
+ # deterministic normalization rules. Production code paths MUST go
17
+ # through Rigor::Type::Combinator. Direct constructor calls are an
18
+ # internal escape hatch for tests and for combinator's own
19
+ # implementation.
20
+ #
21
+ # See docs/internal-spec/internal-type-api.md and
22
+ # docs/type-specification/normalization.md.
23
+ module Combinator
24
+ module_function
25
+
26
+ def top
27
+ Top.instance
28
+ end
29
+
30
+ def bot
31
+ Bot.instance
32
+ end
33
+
34
+ def untyped
35
+ @untyped ||= Dynamic.new(top)
36
+ end
37
+
38
+ # Wraps the static facet in a Dynamic[T] carrier. Idempotent on the
39
+ # static facet so Dynamic[Dynamic[T]] collapses to Dynamic[T] per the
40
+ # value-lattice algebra.
41
+ def dynamic(static_facet)
42
+ return untyped if static_facet.equal?(top)
43
+
44
+ facet = static_facet.is_a?(Dynamic) ? static_facet.static_facet : static_facet
45
+ return untyped if facet.is_a?(Top)
46
+
47
+ Dynamic.new(facet)
48
+ end
49
+
50
+ # Constructs a Nominal type. Slice 4 phase 2d accepts an optional
51
+ # `type_args:` array, an ordered list of Rigor::Type values that
52
+ # carry the receiver's generic instantiation (`Array[Integer]` is
53
+ # `Nominal["Array", [Nominal["Integer"]]]`). Omitting the keyword
54
+ # produces the raw form `Nominal["Array"]`, which is structurally
55
+ # distinct from any applied form.
56
+ def nominal_of(class_name_or_object, type_args: [])
57
+ Nominal.new(resolve_class_name(class_name_or_object), type_args)
58
+ end
59
+
60
+ def singleton_of(class_name_or_object)
61
+ Singleton.new(resolve_class_name(class_name_or_object))
62
+ end
63
+
64
+ def constant_of(value)
65
+ Constant.new(value)
66
+ end
67
+
68
+ # Constructs a heterogeneous, fixed-arity Tuple from positional
69
+ # element types. `tuple_of()` produces the empty tuple `Tuple[]`,
70
+ # which is structurally distinct from the raw `Nominal[Array]`.
71
+ def tuple_of(*elements)
72
+ Tuple.new(elements)
73
+ end
74
+
75
+ # Constructs a HashShape from an ordered (Symbol|String) -> type
76
+ # map. The argument is duped and frozen by the carrier; callers
77
+ # MUST NOT rely on later mutation.
78
+ def hash_shape_of(pairs = nil, **options)
79
+ if pairs.nil?
80
+ pairs = options
81
+ options = {}
82
+ end
83
+
84
+ HashShape.new(pairs, **options)
85
+ end
86
+
87
+ # Normalized union. Flattens nested Unions, deduplicates structurally
88
+ # equal members, drops Bot, and collapses 0/1-member results.
89
+ def union(*types)
90
+ collapse_union(normalized_union_members(types))
91
+ end
92
+
93
+ class << self
94
+ private
95
+
96
+ def normalized_union_members(types)
97
+ flattened = []
98
+ types.each { |t| flatten_into(flattened, t) }
99
+ flattened.reject! { |t| t.is_a?(Bot) }
100
+
101
+ return [top] if flattened.any?(Top)
102
+
103
+ unique_members(flattened)
104
+ end
105
+
106
+ def unique_members(types)
107
+ types.each_with_object([]) do |type, unique|
108
+ unique << type unless unique.any? { |member| member == type }
109
+ end
110
+ end
111
+
112
+ def collapse_union(types)
113
+ case types.size
114
+ when 0 then bot
115
+ when 1 then types.first
116
+ else Union.new(sort_members(types))
117
+ end
118
+ end
119
+
120
+ def resolve_class_name(class_name_or_object)
121
+ name =
122
+ case class_name_or_object
123
+ when Module then class_name_or_object.name
124
+ when String then class_name_or_object
125
+ else
126
+ raise ArgumentError, "expected Class/Module or String, got #{class_name_or_object.class}"
127
+ end
128
+
129
+ raise ArgumentError, "anonymous class has no name" if name.nil? || name.empty?
130
+
131
+ name
132
+ end
133
+
134
+ def flatten_into(acc, type)
135
+ if type.is_a?(Union)
136
+ type.members.each { |m| flatten_into(acc, m) }
137
+ else
138
+ acc << type
139
+ end
140
+ end
141
+
142
+ def sort_members(members)
143
+ members.sort_by { |m| m.describe(:short) }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # A literal carrier under ADR-3 OQ1 Option C (Hybrid). Wraps a Ruby
8
+ # literal value of one of the supported immutable-ish classes. Compound
9
+ # literal shapes (Tuple, HashShape, Record) get dedicated classes in
10
+ # later slices; Range is carried only when both static endpoints are
11
+ # known enough for tuple slicing.
12
+ #
13
+ # See docs/adr/4-type-inference-engine.md for the tentative answer to
14
+ # the open question and docs/type-specification/rigor-extensions.md for
15
+ # the refinement neighbourhood this carrier lives in.
16
+ class Constant
17
+ SCALAR_CLASSES = [
18
+ Integer,
19
+ Float,
20
+ String,
21
+ Symbol,
22
+ Range,
23
+ Rational,
24
+ Complex,
25
+ TrueClass,
26
+ FalseClass,
27
+ NilClass
28
+ ].freeze
29
+
30
+ RBS_LITERAL_CLASSES = {
31
+ TrueClass => "true",
32
+ FalseClass => "false",
33
+ NilClass => "nil"
34
+ }.freeze
35
+
36
+ attr_reader :value
37
+
38
+ def initialize(value)
39
+ unless SCALAR_CLASSES.any? { |klass| value.is_a?(klass) }
40
+ raise ArgumentError, "Rigor::Type::Constant only carries scalar literals; got #{value.class}"
41
+ end
42
+
43
+ @value = value.is_a?(String) ? value.dup.freeze : value
44
+ freeze
45
+ end
46
+
47
+ def describe(_verbosity = :short)
48
+ value.inspect
49
+ end
50
+
51
+ def erase_to_rbs
52
+ case value
53
+ when true then "true"
54
+ when false then "false"
55
+ when nil then "nil"
56
+ else value.class.name
57
+ end
58
+ end
59
+
60
+ def top
61
+ Trinary.no
62
+ end
63
+
64
+ def bot
65
+ Trinary.no
66
+ end
67
+
68
+ def dynamic
69
+ Trinary.no
70
+ end
71
+
72
+ def accepts(other, mode: :gradual)
73
+ Inference::Acceptance.accepts(self, other, mode: mode)
74
+ end
75
+
76
+ def ==(other)
77
+ other.is_a?(Constant) && value.class == other.value.class && value == other.value
78
+ end
79
+ alias eql? ==
80
+
81
+ def hash
82
+ [Constant, value.class, value].hash
83
+ end
84
+
85
+ def inspect
86
+ "#<Rigor::Type::Constant #{describe(:short)}>"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # The dynamic-origin wrapper: marks values whose type came from an
8
+ # unchecked source. Carries a static facet that records the analyzer's
9
+ # best static knowledge. See docs/type-specification/value-lattice.md
10
+ # for the algebra and docs/type-specification/special-types.md for the
11
+ # untyped/Dynamic[T] relationship.
12
+ #
13
+ # Construct via Rigor::Type::Combinator.dynamic(static_facet).
14
+ class Dynamic
15
+ attr_reader :static_facet
16
+
17
+ def initialize(static_facet)
18
+ @static_facet = static_facet
19
+ freeze
20
+ end
21
+
22
+ def describe(verbosity = :short)
23
+ "Dynamic[#{static_facet.describe(verbosity)}]"
24
+ end
25
+
26
+ def erase_to_rbs
27
+ "untyped"
28
+ end
29
+
30
+ def top
31
+ Trinary.no
32
+ end
33
+
34
+ def bot
35
+ Trinary.no
36
+ end
37
+
38
+ def dynamic
39
+ Trinary.yes
40
+ end
41
+
42
+ def accepts(other, mode: :gradual)
43
+ Inference::Acceptance.accepts(self, other, mode: mode)
44
+ end
45
+
46
+ def ==(other)
47
+ other.is_a?(Dynamic) && static_facet == other.static_facet
48
+ end
49
+ alias eql? ==
50
+
51
+ def hash
52
+ [Dynamic, static_facet].hash
53
+ end
54
+
55
+ def inspect
56
+ "#<Rigor::Type::Dynamic #{describe(:short)}>"
57
+ end
58
+ end
59
+ end
60
+ end