Class: Rigor::Effects::EnvelopeIndex

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/effects/envelope_index.rb

Overview

The envelopes a call site may import as a bound, looked up by the callee the typer named (ADR-103 WD6; #386; normative in docs/type-specification/effect-labels.md § The declared lane at call sites).

EnvelopeCheck asks "what bounds THIS method's body?" and answers it from the project's own declarations alone, because that is the stratum a contract can be checked against. This index asks the other question — "what does the thing I am calling promise?" — and so reads one stratum more: an accepted signature's annotation, from the built RBS environment. WD6's trust ladder puts both on the discharging side, and the two questions are kept in two objects because only the first one may ever produce a finding.

Four sources, nearest-first, exactly the check's precedence with the accepted stratum appended:

per-method annotation  >  class-level annotation  >  `effects.envelopes:`  >  accepted signature

The carrier is nominal: the lookup is by the receiver's static class name as the typer projected it (or, for an implicit-self call, by the unit's own class), never by walking ancestors. A structural interface erases to Dynamic[top] today and has nothing to attach a bound to (ADR-103 WD6); an inherited envelope reaches an override through effect.liskov-widened instead, which is a judgment rather than an import.

Two bounds of the slice, both deliberate:

  • An effects.envelopes: entry selected by match: does not participate here. A path glob is a fact about where a class is defined, which a per-file collection window cannot see without the whole-project class-source table; namespace: needs only the name and does participate. The entry still bounds its own classes' methods for EnvelopeCheck and the Liskov check.
  • Lookup is by the exact owner. repo.find on a receiver typed PgRepo does not import Repo#find's bound.

Values are Marshal-clean (Envelope over frozen Strings and LabelSets), because the index is built per process — the parent and each fork-pool worker build their own from the same configuration and the same signature content, so the two agree without a channel to keep in sync.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(method_envelopes: NO_ENVELOPES, class_envelopes: NO_ENVELOPES, config_entries: NO_ENTRIES, accepted: NO_ENVELOPES) ⇒ EnvelopeIndex

Returns a new instance of EnvelopeIndex.



107
108
109
110
111
112
113
114
115
# File 'lib/rigor/effects/envelope_index.rb', line 107

def initialize(method_envelopes: NO_ENVELOPES, class_envelopes: NO_ENVELOPES,
               config_entries: NO_ENTRIES, accepted: NO_ENVELOPES)
  @method_envelopes = method_envelopes.freeze
  @class_envelopes = class_envelopes.freeze
  @config_entries = config_entries.reject { |entry| entry.namespace.nil? }.freeze
  @accepted = accepted.freeze
  @config_cache = {}
  freeze
end

Class Method Details

.build(configuration:, environment: nil, plugin_facts: nil) ⇒ EnvelopeIndex

Reads every stratum this index serves, once per process.

Parameters:

  • configuration (Rigor::Configuration)
  • plugin_facts (Rigor::Effects::PluginFacts, nil) (defaults to: nil)

    the loaded plugins' effect contributions (#387); their effect_labels: join the vocabulary an annotation is read against, so a gem's %a{rigor:v1:effect rails.activejob.enqueue} resolves rather than reading as unknown.

  • environment (Rigor::Environment, nil) (defaults to: nil)

    the run's environment. Its loader supplies the rbs-inline / plugin virtual_rbs buffers and the built RBS environment the accepted stratum is read from; without one, both are simply absent (the fail-quiet direction — a missing bound costs precision, never a finding).

Returns:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/rigor/effects/envelope_index.rb', line 68

def self.build(configuration:, environment: nil, plugin_facts: nil)
  # Required here rather than at the top of the file: the reader pulls in the whole
  # `RbsExtended` surface, and this build runs only under an `effects:` block.
  require_relative "../rbs_extended/envelope_scanner"

  registry = Registry.for_configuration(configuration, plugin_facts: plugin_facts)
  loader = environment&.rbs_loader
  scan = RbsExtended::EnvelopeScanner.scan(
    sources: SignatureSources.collect(
      signature_paths: configuration.signature_paths, virtual_rbs: loader&.virtual_rbs
    ),
    registry: registry
  )
  new(
    method_envelopes: scan.method_envelopes, class_envelopes: scan.class_envelopes,
    config_entries: ConfigEnvelopes.build(entries: configuration.effects_envelopes, registry: registry),
    accepted: accepted_for(loader, registry)
  )
rescue StandardError
  empty
end

.emptyObject

The index a run with no declaration of any kind uses — and the fail-soft answer for a build that raised. Every lookup on it is one empty? read.



53
54
55
# File 'lib/rigor/effects/envelope_index.rb', line 53

def self.empty
  @empty ||= new
end

Instance Method Details

#[](owner, singleton, selector) ⇒ Envelope?

The envelope bounding owner's selector, or nil.

Parameters:

  • owner (String)

    the receiver's static class name, as the typer projected it

  • singleton (Boolean)

    whether the call is Owner.selector rather than Owner#selector

  • selector (String)

Returns:

  • (Envelope, nil)

    never a ⊤ envelope: a bound that bounds nothing is not a bound, and importing it would both add nothing and discharge a taint on the strength of a typo.



130
131
132
133
134
135
136
137
138
139
# File 'lib/rigor/effects/envelope_index.rb', line 130

def [](owner, singleton, selector)
  return nil if owner.nil? || empty?

  key = "#{owner}#{singleton ? '.' : '#'}#{selector}"
  envelope = @method_envelopes[key] || @class_envelopes[owner] || config_envelope(owner) ||
             @accepted[key]
  return nil if envelope.nil? || envelope.top?

  envelope
end

#empty?Boolean

Whether no stratum has anything to say. The scan's fast path: a project with no envelope of any kind pays one predicate per call site and nothing else.

Returns:

  • (Boolean)


119
120
121
# File 'lib/rigor/effects/envelope_index.rb', line 119

def empty?
  @method_envelopes.empty? && @class_envelopes.empty? && @config_entries.empty? && @accepted.empty?
end