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 `%arigor:v1:hkt_register` / `%arigor:v1:hkt_define` annotations Slice 1 ships.

Today’s contents:

  • json::value` — 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.



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/rigor/builtins/hkt_builtins.rb', line 278

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` for a single element, `Tuple[Singleton<Date>, Singleton<Symbol>]` for multiple, `Nominal[Array, [Singleton<…>]]` if the analyzer widened (rare for literal arrays).



305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/rigor/builtins/hkt_builtins.rb', line 305

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



58
59
60
# File 'lib/rigor/builtins/hkt_builtins.rb', line 58

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

.csv_parsed_definitionObject



90
91
92
93
94
95
96
97
98
# File 'lib/rigor/builtins/hkt_builtins.rb', line 90

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



81
82
83
84
85
86
87
88
# File 'lib/rigor/builtins/hkt_builtins.rb', line 81

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.



246
247
248
249
250
251
252
253
# File 'lib/rigor/builtins/hkt_builtins.rb', line 246

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)


261
262
263
264
265
266
267
268
269
# File 'lib/rigor/builtins/hkt_builtins.rb', line 261

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



43
44
45
# File 'lib/rigor/builtins/hkt_builtins.rb', line 43

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

.json_value_definitionObject



71
72
73
74
75
76
77
78
79
# File 'lib/rigor/builtins/hkt_builtins.rb', line 71

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



62
63
64
65
66
67
68
69
# File 'lib/rigor/builtins/hkt_builtins.rb', line 62

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`).



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/rigor/builtins/hkt_builtins.rb', line 221

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`, element types are non-Singleton, etc.).



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/rigor/builtins/hkt_builtins.rb', line 325

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.



112
113
114
115
116
117
# File 'lib/rigor/builtins/hkt_builtins.rb', line 112

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