Module: Rigor::RbsExtended
- Defined in:
- lib/rigor/rbs_extended.rb
Overview
Slice 7 phase 15 — first-preview reader for the ‘RBS::Extended` annotation surface described in `docs/type-specification/rbs-extended.md`.
This module reads ‘%a<payload>` annotations off RBS method definitions and returns well-typed effect objects the inference engine can consume. v0.0.2 recognises:
-
‘rigor:v1:predicate-if-true <target> is <ClassName>`
-
‘rigor:v1:predicate-if-false <target> is <ClassName>`
-
‘rigor:v1:assert <target> is <ClassName>`
-
‘rigor:v1:assert-if-true <target> is <ClassName>`
-
‘rigor:v1:assert-if-false <target> is <ClassName>`
‘predicate-if-*` fires when the call is used as an `if` / `unless` condition; `assert` fires unconditionally at the call’s post-scope; ‘assert-if-true` / `assert-if-false` fire at the post-scope only when the call’s return value can be observed as truthy / falsey (currently: when the call is the predicate of a subsequent ‘if` / `unless`). Other directives in the spec (`param`, `return`, `conforms-to`, negation `~T`, `target: self` narrowing, …) remain on the v0.0.x roadmap. Annotations whose key is in the `rigor:v1:` namespace but whose directive is unrecognised are silently ignored at first-preview quality (a future slice MAY surface them as diagnostics-on-Rigor-itself per the spec’s “unsupported metadata” guidance).
The parser is minimal: it accepts a strict shape ‘<target> is <ClassName>` where `<target>` is a Ruby identifier (parameter name) or `self`, and `<ClassName>` is a single non-namespaced class identifier or a `::Foo::Bar` style constant path. Negative refinements (`~T`), intersections, and unions are deferred to the next iteration.
Defined Under Namespace
Classes: AssertEffect, ParamOverride, PredicateEffect
Constant Summary collapse
- DIRECTIVE_PREFIX =
rubocop:disable Metrics/ModuleLength
"rigor:v1:"- RBS_EXTENDED_PROVENANCE =
The shared FlowContribution::Provenance for every bundle this module produces. ‘source_family: :rbs_extended` so consumers (today the documentation surface; v0.1.0 the plugin contribution merger) can attribute facts back to the RBS::Extended layer.
FlowContribution::Provenance.new( source_family: :rbs_extended, plugin_id: nil, node: nil, descriptor: nil ).freeze
Class Method Summary collapse
-
.build_flow_contribution(predicate_effects, assert_effects, return_override) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity.
- .nilable_slot(facts) ⇒ Object
-
.param_type_override_map(method_def) ⇒ Object
Convenience reader for call sites that want to look up a single override by parameter name.
- .parse_assert_annotation(string) ⇒ Object
- .parse_param_annotation(string) ⇒ Object
- .parse_predicate_annotation(string) ⇒ Object
- .parse_return_type_override(string) ⇒ Object
-
.read_assert_effects(method_def) ⇒ Object
Reads RBS::Extended assertion effects (‘assert`, `assert-if-true`, `assert-if-false`) off `RBS::Definition::Method#annotations`.
-
.read_flow_contribution(method_def) ⇒ Object
Rolls up every recognised RBS::Extended directive on ‘method_def` into a single FlowContribution with the canonical FlowContribution::Fact payload (see ADR-7 § “Slice 4-A”):.
-
.read_param_type_overrides(method_def) ⇒ Object
Reads every ‘rigor:v1:param: <name> <refinement>` directive off `RBS::Definition::Method#annotations` and returns the resolved `ParamOverride` list.
-
.read_predicate_effects(method_def) ⇒ Object
Reads RBS::Extended predicate effects off ‘RBS::Definition::Method#annotations`.
-
.read_return_type_override(method_def) ⇒ Object
Reads the ‘rigor:v1:return: <kebab-name>` directive off `RBS::Definition::Method#annotations`.
-
.resolve_directive_rhs(match) ⇒ Object
Resolves the ‘class_name` / `refinement` alternation in the assert / predicate directive patterns.
- .target_fields(target) ⇒ Object
Class Method Details
.build_flow_contribution(predicate_effects, assert_effects, return_override) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/rigor/rbs_extended.rb', line 491 def build_flow_contribution(predicate_effects, assert_effects, return_override) # rubocop:disable Metrics/CyclomaticComplexity truthy = predicate_effects.select(&:truthy_only?).map(&:to_fact) falsey = predicate_effects.select(&:falsey_only?).map(&:to_fact) post_return = [] assert_effects.each do |effect| case effect.condition when :if_truthy_return then truthy << effect.to_fact when :if_falsey_return then falsey << effect.to_fact else post_return << effect.to_fact end end FlowContribution.new( return_type: return_override, truthy_facts: nilable_slot(truthy), falsey_facts: nilable_slot(falsey), post_return_facts: nilable_slot(post_return), provenance: RBS_EXTENDED_PROVENANCE ) end |
.nilable_slot(facts) ⇒ Object
513 514 515 |
# File 'lib/rigor/rbs_extended.rb', line 513 def nilable_slot(facts) facts.empty? ? nil : facts end |
.param_type_override_map(method_def) ⇒ Object
Convenience reader for call sites that want to look up a single override by parameter name. Returns a frozen Hash<Symbol, Rigor::Type>; missing keys mean “use the RBS-declared type”. Callers MUST treat the hash as read-only.
414 415 416 |
# File 'lib/rigor/rbs_extended.rb', line 414 def param_type_override_map(method_def) read_param_type_overrides(method_def).to_h { |o| [o.param_name, o.type] }.freeze end |
.parse_assert_annotation(string) ⇒ Object
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/rigor/rbs_extended.rb', line 235 def parse_assert_annotation(string) match = ASSERT_DIRECTIVE_PATTERN.match(string) return nil if match.nil? directive = match[:directive].to_s condition = ASSERT_CONDITIONS[directive] return nil if condition.nil? target = match[:target].to_s target_kind, target_name = target_fields(target) class_name, refinement_type, negative = resolve_directive_rhs(match) return nil if class_name.nil? && refinement_type.nil? AssertEffect.new( condition: condition, target_kind: target_kind, target_name: target_name, class_name: class_name, negative: negative, refinement_type: refinement_type ) end |
.parse_param_annotation(string) ⇒ Object
437 438 439 440 441 442 443 444 445 |
# File 'lib/rigor/rbs_extended.rb', line 437 def parse_param_annotation(string) match = PARAM_DIRECTIVE_PATTERN.match(string) return nil if match.nil? type = Builtins::ImportedRefinements.parse(match[:payload]) return nil if type.nil? ParamOverride.new(param_name: match[:param].to_sym, type: type) end |
.parse_predicate_annotation(string) ⇒ Object
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/rigor/rbs_extended.rb', line 171 def parse_predicate_annotation(string) match = PREDICATE_DIRECTIVE_PATTERN.match(string) return nil if match.nil? directive = match[:directive].to_s target = match[:target].to_s edge = directive == "predicate-if-true" ? :truthy_only : :falsey_only target_kind, target_name = target_fields(target) class_name, refinement_type, negative = resolve_directive_rhs(match) return nil if class_name.nil? && refinement_type.nil? PredicateEffect.new( edge: edge, target_kind: target_kind, target_name: target_name, class_name: class_name, negative: negative, refinement_type: refinement_type ) end |
.parse_return_type_override(string) ⇒ Object
363 364 365 366 367 368 |
# File 'lib/rigor/rbs_extended.rb', line 363 def parse_return_type_override(string) match = RETURN_DIRECTIVE_PATTERN.match(string) return nil if match.nil? Builtins::ImportedRefinements.parse(match[:payload]) end |
.read_assert_effects(method_def) ⇒ Object
Reads RBS::Extended assertion effects (‘assert`, `assert-if-true`, `assert-if-false`) off `RBS::Definition::Method#annotations`. Returns an empty array when no recognised assertion directives are attached to the method.
197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/rigor/rbs_extended.rb', line 197 def read_assert_effects(method_def) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? effects = [] annotations.each do |annotation| effect = parse_assert_annotation(annotation.string) effects << effect if effect end effects.uniq end |
.read_flow_contribution(method_def) ⇒ Object
Rolls up every recognised RBS::Extended directive on ‘method_def` into a single FlowContribution with the canonical FlowContribution::Fact payload (see ADR-7 § “Slice 4-A”):
-
‘predicate-if-true` → `truthy_facts`
-
‘predicate-if-false` → `falsey_facts`
-
‘assert` → `post_return_facts`
-
‘assert-if-true` → `truthy_facts`
-
‘assert-if-false` → `falsey_facts`
-
‘return:` override → `return_type` (`Rigor::Type`)
Param overrides are intentionally NOT included — they refine the call’s signature contract rather than its flow facts and do not fit ADR-2 § “Flow Contribution Bundle” slot semantics. Callers that care about parameter contracts keep using read_param_type_overrides / param_type_override_map.
Returns ‘nil` when the method carries no recognised contribution directives (callers can skip the merge step without iterating an empty bundle).
480 481 482 483 484 485 486 487 488 489 |
# File 'lib/rigor/rbs_extended.rb', line 480 def read_flow_contribution(method_def) return nil if method_def.nil? predicate_effects = read_predicate_effects(method_def) assert_effects = read_assert_effects(method_def) return_override = read_return_type_override(method_def) return nil if predicate_effects.empty? && assert_effects.empty? && return_override.nil? build_flow_contribution(predicate_effects, assert_effects, return_override) end |
.read_param_type_overrides(method_def) ⇒ Object
Reads every ‘rigor:v1:param: <name> <refinement>` directive off `RBS::Definition::Method#annotations` and returns the resolved `ParamOverride` list. Annotations the parser cannot resolve (typo, unknown refinement, no `param:` directive at all) are silently dropped — the call site keeps the RBS-declared parameter type for those parameters. The reader accepts a nil method definition so call sites can pass through optional method lookups without a guard.
Example annotation in an RBS file:
class Slug
%a{rigor:v1:param: id is non-empty-string}
def normalise: (::String id) -> String
end
The RBS-declared type of ‘id` is `String`. The override tightens it to `non-empty-string` for argument-check purposes; passing a too-wide `Nominal` argument is flagged as an argument-type mismatch at the call site.
400 401 402 403 404 405 406 407 |
# File 'lib/rigor/rbs_extended.rb', line 400 def read_param_type_overrides(method_def) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? annotations.filter_map { |annotation| parse_param_annotation(annotation.string) } end |
.read_predicate_effects(method_def) ⇒ Object
Reads RBS::Extended predicate effects off ‘RBS::Definition::Method#annotations`. Returns the effects in source order; duplicates and unrecognised `rigor:v1:` directives are dropped. Returns an empty array (NEVER `nil`) for a method with no recognised annotations so callers can iterate unconditionally.
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/rigor/rbs_extended.rb', line 132 def read_predicate_effects(method_def) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? effects = [] annotations.each do |annotation| effect = parse_predicate_annotation(annotation.string) effects << effect if effect end effects.uniq end |
.read_return_type_override(method_def) ⇒ Object
Reads the ‘rigor:v1:return: <kebab-name>` directive off `RBS::Definition::Method#annotations`. The directive overrides a method’s RBS-declared return type with one of the imported-built-in refinements registered in ‘Rigor::Builtins::ImportedRefinements`. The override is the primary integration path for refinement carriers (`non-empty-string`, `positive-int`, `non-empty-array`, …) in v0.0 — annotation-driven, opt-in per method, and never silently rewrites a hand-authored RBS signature outside the annotation.
Example annotation in an RBS file:
class User
%a{rigor:v1:return: non-empty-string}
def name: () -> String
end
The RBS-declared return is ‘String`. The override tightens it to `non-empty-string` (i.e. `Difference[String, “”]`) for callers; RBS erasure of the tightened return goes back to `String` so the round-trip to ordinary RBS is unaffected.
Returns the resolved ‘Rigor::Type` value, or `nil` when:
-
the method has no annotations,
-
none of the annotations match the ‘rigor:v1:return:` directive,
-
the directive’s payload names a refinement not registered in ‘Rigor::Builtins::ImportedRefinements` (the analyzer prefers a silent miss over crashing on a typo; future slices MAY surface the miss as a `:warning` self-diagnostic).
331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/rigor/rbs_extended.rb', line 331 def read_return_type_override(method_def) return nil if method_def.nil? annotations = method_def.annotations return nil if annotations.nil? || annotations.empty? annotations.each do |annotation| type = parse_return_type_override(annotation.string) return type if type end nil end |
.resolve_directive_rhs(match) ⇒ Object
Resolves the ‘class_name` / `refinement` alternation in the assert / predicate directive patterns. Returns `[class_name, refinement_type, negative]`:
-
Class-name arm matched: ‘class_name` is the resolved string (leading `::` stripped), `refinement_type` is nil, `negative` reflects the optional `~` prefix.
-
Refinement arm matched: ‘class_name` is nil, `refinement_type` is the resolved `Rigor::Type`, `negative` reflects the `~` prefix. v0.0.5 supports refinement-form negation for the `Difference[base, Constant]` shape (the narrowing tier computes the complement decomposition); other refinement carriers under negation fall back to the conservative “current_type unchanged” answer.
-
Refinement payload unparseable: returns ‘[nil, nil, false]` so callers can drop the directive silently (fail-soft policy).
276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/rigor/rbs_extended.rb', line 276 def resolve_directive_rhs(match) negative = match[:negation].to_s == "~" class_capture = match[:class_name] return [class_capture.to_s.sub(/\A::/, ""), nil, negative] if class_capture refinement_capture = match[:refinement] return [nil, nil, false] if refinement_capture.nil? type = Builtins::ImportedRefinements.parse(refinement_capture) return [nil, nil, false] if type.nil? [nil, type, negative] end |
.target_fields(target) ⇒ Object
290 291 292 293 294 295 296 |
# File 'lib/rigor/rbs_extended.rb', line 290 def target_fields(target) if target == "self" %i[self self] else [:parameter, target.to_sym] end end |