Module: Rigor::Builtins::HktBuiltins

Defined in:
lib/rigor/builtins/hkt_builtins.rb

Overview

ADR-20 slices 2c + 3 — Rigor-bundled Lightweight HKT registrations that ship with every analyzer instance. The set is intentionally small at v0.1.x: only the URIs whose payoff justifies hardcoded definitions. Plugin authors register more URIs through their manifests; user .rbs overlays register through the %a{rigor:v1:hkt_register} / %a{rigor:v1:hkt_define} annotations Slice 1 ships.

Today's contents:

  • json::value[K] — the recursive sum stdlib's JSON.parse returns. Body:

    nil | true | false | Integer | Float | String
    | Array[App[json::value, K]]
    | Hash[K, App[json::value, K]]
    

    The reducer handles the self-recursive App nodes via lazy "tying-the-knot" (see HktReducer). K = String matches stdlib's default key handling; K = Symbol matches symbolize_names: true.

Constant Summary collapse

METHOD_RETURN_OVERRIDES =
{
  # JSON — stdlib's `json` library. Upstream rbs declares
  # `(string, ?options) -> untyped`; the HKT-builtin tier
  # tightens to the recursive `json::value[K]` union.
  # `load_file` / `load_file!` share the `?options` slot
  # so the `symbolize_names: true` discriminator applies
  # to them too (just like `parse` / `load`).
  ["JSON", :parse,      :singleton] => JSON_VALUE_SPEC,
  ["JSON", :parse!,     :singleton] => JSON_VALUE_SPEC,
  ["JSON", :load,       :singleton] => JSON_VALUE_SPEC,
  ["JSON", :load_file,  :singleton] => JSON_VALUE_SPEC,
  ["JSON", :load_file!, :singleton] => JSON_VALUE_SPEC,
  # YAML.safe_load / Psych.safe_load — default
  # `permitted_classes: []` admits exactly the JSON
  # vocabulary (nil / true / false / Integer / Float /
  # String / Array / Hash), so the json::value tree
  # also describes them. When the call passes a literal
  # `permitted_classes: [Date, Symbol, ...]` Array, the
  # `:yaml_permitted_classes` post_reduce unions each
  # named class into the result. Non-literal options
  # (a variable, a constant reference, a `+ classes`
  # concat) silently no-op and the caller observes the
  # base json::value envelope only. YAML.load /
  # YAML.unsafe_load deliberately stay out of the
  # override table — they can return ANY Ruby object
  # and have no useful HKT envelope.
  ["YAML",  :safe_load,      :singleton] => YAML_SAFE_VALUE_SPEC,
  ["YAML",  :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
  ["Psych", :safe_load,      :singleton] => YAML_SAFE_VALUE_SPEC,
  ["Psych", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
  # CSV.parse / CSV.read — no-headers shape only.
  # Upstream rbs declares broader return shapes but
  # the common case is `Array[Array[String?]]` which
  # the `csv::parsed[String]` URI matches. The
  # `headers: true` shape (`CSV::Table` of `CSV::Row`)
  # is NOT covered — calls passing the option fall
  # through to the upstream RBS type. CSV.foreach also
  # falls through (it yields rows rather than
  # returning a typed structure).
  ["CSV", :parse, :singleton] => CSV_PARSED_SPEC,
  ["CSV", :read,  :singleton] => CSV_PARSED_SPEC
}.freeze

Class Method Summary collapse

Class Method Details

.apply_post_reduce(kind, reduced, arg_types) ⇒ Object

Slice 2c-bis — post-reduce hook. Receives the already- reduced Type and the call-site's arg_types; returns a (possibly augmented) Type. kind = nil is the identity (passes the reduced type through unchanged). Only :yaml_permitted_classes is implemented today; plugin / Rigor-bundled callers wanting their own post-reduce hooks add a branch here.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/rigor/builtins/hkt_builtins.rb', line 271

def apply_post_reduce(kind, reduced, arg_types)
  case kind
  when :yaml_permitted_classes
    augment_with_yaml_permitted_classes(reduced, arg_types)
  else
    # `nil` (no post-reduce declared) and any future
    # unrecognised kind both pass the reduced type
    # through unchanged. Unknown kinds are silently
    # tolerated rather than raised because adding a
    # new kind on a Rigor upgrade should not crash a
    # stale METHOD_RETURN_OVERRIDES entry on the
    # caller side.
    reduced
  end
end

.augment_with_yaml_permitted_classes(reduced, arg_types) ⇒ Object

Inspects arg_types for a permitted_classes: [<Class>, ...] literal Array in the options Hash and unions each named class into the reduced result. Non-literal permitted_classes: values (a variable, a constant reference, a concat) silently no-op and the caller observes the base json::value envelope only. Defensive against the various ways Ruby literal arrays surface as Rigor types: Tuple[Singleton<Date>] for a single element, Tuple[Singleton<Date>, Singleton<Symbol>] for multiple, Nominal[Array, [Singleton<...>]] if the analyzer widened (rare for literal arrays).



298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/rigor/builtins/hkt_builtins.rb', line 298

def augment_with_yaml_permitted_classes(reduced, arg_types)
  return reduced unless arg_types.is_a?(Array) && arg_types.size >= 2

  opts = arg_types[1]
  return reduced unless opts.is_a?(Rigor::Type::HashShape)

  value = opts.pairs[:permitted_classes] || opts.pairs["permitted_classes"]
  return reduced if value.nil?

  extras = permitted_class_nominals(value)
  return reduced if extras.empty?

  Rigor::Type::Combinator.union(reduced, *extras)
end

.csv_parsed_body_treeObject



51
52
53
# File 'lib/rigor/builtins/hkt_builtins.rb', line 51

def csv_parsed_body_tree
  Rigor::Inference::HktBodyParser.parse(CSV_PARSED_BODY, params: [:K])
end

.csv_parsed_definitionObject



83
84
85
86
87
88
89
90
91
# File 'lib/rigor/builtins/hkt_builtins.rb', line 83

def csv_parsed_definition
  Rigor::Inference::HktRegistry.definition_with_body_tree(
    uri: :"csv::parsed",
    params: [:K],
    body_tree: csv_parsed_body_tree,
    source_path: __FILE__,
    source_line: __LINE__ - 5
  )
end

.csv_parsed_registrationObject



74
75
76
77
78
79
80
81
# File 'lib/rigor/builtins/hkt_builtins.rb', line 74

def csv_parsed_registration
  Rigor::Inference::HktRegistry::Registration.new(
    uri: :"csv::parsed",
    arity: 1,
    variance: [:out],
    bound: Rigor::Type::Combinator.untyped
  )
end

.discriminated_args(spec, arg_types) ⇒ Object

Per-spec discriminator dispatch. Slice 3 ships one built-in discriminator (json_symbolize_names) that observes the optional 2nd argument's HashShape for a literal symbolize_names: true entry. Plugin / Rigor- bundled callers wanting their own discriminators add a branch here.



239
240
241
242
243
244
245
246
# File 'lib/rigor/builtins/hkt_builtins.rb', line 239

def discriminated_args(spec, arg_types)
  default_args = spec[:args].map { |n| Rigor::Type::Nominal.new(n) }
  return default_args if arg_types.nil?
  return default_args unless spec[:discriminator] == :json_symbolize_names
  return default_args unless json_symbolize_names?(arg_types)

  [Rigor::Type::Nominal.new("Symbol")]
end

.json_symbolize_names?(arg_types) ⇒ Boolean

Returns true iff the call-site's 2nd argument is a Type::HashShape carrying a literal symbolize_names: true entry. Anything else (no second arg, non-HashShape, missing key, non-literal true) returns false so the default K = String branch wins.

Returns:

  • (Boolean)


254
255
256
257
258
259
260
261
262
# File 'lib/rigor/builtins/hkt_builtins.rb', line 254

def json_symbolize_names?(arg_types)
  return false unless arg_types.is_a?(Array) && arg_types.size >= 2

  opts = arg_types[1]
  return false unless opts.is_a?(Rigor::Type::HashShape)

  value = opts.pairs[:symbolize_names] || opts.pairs["symbolize_names"]
  value.is_a?(Rigor::Type::Constant) && value.value == true
end

.json_value_body_treeObject



36
37
38
# File 'lib/rigor/builtins/hkt_builtins.rb', line 36

def json_value_body_tree
  Rigor::Inference::HktBodyParser.parse(JSON_VALUE_BODY, params: [:K])
end

.json_value_definitionObject



64
65
66
67
68
69
70
71
72
# File 'lib/rigor/builtins/hkt_builtins.rb', line 64

def json_value_definition
  Rigor::Inference::HktRegistry.definition_with_body_tree(
    uri: :"json::value",
    params: [:K],
    body_tree: json_value_body_tree,
    source_path: __FILE__,
    source_line: __LINE__ - 5
  )
end

.json_value_registrationObject



55
56
57
58
59
60
61
62
# File 'lib/rigor/builtins/hkt_builtins.rb', line 55

def json_value_registration
  Rigor::Inference::HktRegistry::Registration.new(
    uri: :"json::value",
    arity: 1,
    variance: [:out],
    bound: Rigor::Type::Combinator.untyped
  )
end

.method_return_override(class_name:, method_name:, kind:, arg_types: nil, hkt_registry: nil) ⇒ Rigor::Type?

Returns the reduced HKT type for the given (class_name, method_name, kind) triple, or nil when no built-in override is registered. When arg_types is supplied AND the entry carries a :discriminator symbol, the discriminator may swap the spec's default args for an alternate (e.g. JSON.parse(str, symbolize_names: true) discriminates K = Symbol instead of the default K = String).

Returns:

  • (Rigor::Type, nil)

    the reduced HKT type for the given (class_name, method_name, kind) triple, or nil when no built-in override is registered. When arg_types is supplied AND the entry carries a :discriminator symbol, the discriminator may swap the spec's default args for an alternate (e.g. JSON.parse(str, symbolize_names: true) discriminates K = Symbol instead of the default K = String).



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/rigor/builtins/hkt_builtins.rb', line 214

def method_return_override(class_name:, method_name:, kind:, arg_types: nil, hkt_registry: nil)
  spec = METHOD_RETURN_OVERRIDES[[class_name, method_name.to_sym, kind]]
  return nil unless spec

  args = discriminated_args(spec, arg_types)
  registration = hkt_registry&.registration(spec[:uri])
  bound = registration&.bound || Rigor::Type::Combinator.untyped
  app = Rigor::Type::App.new(spec[:uri], args, bound: bound)

  reduced =
    if hkt_registry.nil? || !hkt_registry.defined?(spec[:uri])
      app
    else
      hkt_registry.reduce(app) || app
    end

  apply_post_reduce(spec[:post_reduce], reduced, arg_types)
end

.permitted_class_nominals(value) ⇒ Object

Extract Singleton-class elements from a Tuple or Array-shape carrier, mapping each to its Nominal counterpart. Returns an empty array when no static Singletons are reachable (e.g. value is Dynamic[T], element types are non-Singleton, etc.).



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/rigor/builtins/hkt_builtins.rb', line 318

def permitted_class_nominals(value)
  candidates =
    if value.is_a?(Rigor::Type::Tuple)
      value.elements
    elsif value.is_a?(Rigor::Type::Nominal) && value.class_name == "Array" && value.type_args.size == 1
      element = value.type_args.first
      element.is_a?(Rigor::Type::Union) ? element.members : [element]
    else
      []
    end

  candidates.filter_map do |c|
    c.is_a?(Rigor::Type::Singleton) ? Rigor::Type::Nominal.new(c.class_name) : nil
  end
end

.registryRigor::Inference::HktRegistry

Returns frozen registry pre-seeded with all bundled HKT registrations + bodies. Allocated fresh each call rather than memoised — memoisation through a module-level @registry ivar surfaces a Ractor::IsolationError in pool workers (the ivar's contents include HktBody::AppRef Symbol-keyed structures that the current Ractor shareability audit hasn't yet been walked through). The registry is small enough that per-Environment construction is acceptable; an eager-frozen constant is a future optimisation once ADR-15 phase 4b.x covers the dependency graph.

Returns:

  • (Rigor::Inference::HktRegistry)

    frozen registry pre-seeded with all bundled HKT registrations + bodies. Allocated fresh each call rather than memoised — memoisation through a module-level @registry ivar surfaces a Ractor::IsolationError in pool workers (the ivar's contents include HktBody::AppRef Symbol-keyed structures that the current Ractor shareability audit hasn't yet been walked through). The registry is small enough that per-Environment construction is acceptable; an eager-frozen constant is a future optimisation once ADR-15 phase 4b.x covers the dependency graph.



105
106
107
108
109
110
# File 'lib/rigor/builtins/hkt_builtins.rb', line 105

def registry
  Rigor::Inference::HktRegistry.new(
    registrations: [json_value_registration, csv_parsed_registration],
    definitions: [json_value_definition, csv_parsed_definition]
  )
end