Module: Rigor::RbsExtended
- Defined in:
- lib/rigor/rbs_extended.rb,
lib/rigor/rbs_extended/reporter.rb,
lib/rigor/rbs_extended/hkt_directives.rb,
lib/rigor/rbs_extended/conformance_checker.rb,
sig/rigor/rbs_extended.rbs
Overview
Reader for the RBS::Extended annotation surface described in docs/type-specification/rbs-extended.md.
Reads %a{rigor:v1:<directive> <payload>} annotations off RBS method definitions and returns well-typed
effect objects the inference engine can consume. Implemented directives:
rigor:v1:predicate-if-true <target> is <ClassName|refinement>rigor:v1:predicate-if-false <target> is <ClassName|refinement>rigor:v1:assert <target> is <ClassName|refinement>rigor:v1:assert-if-true <target> is <ClassName|refinement>rigor:v1:assert-if-false <target> is <ClassName|refinement>rigor:v1:param <name> <type-expr>— per-call param narrowingrigor:v1:return <type-expr>— per-call return overriderigor:v1:conforms-to <InterfaceName>— structural conformance
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. Negation (~T) is supported for both class-name and
refinement right-hand sides. Parameterised refinements (non-empty-array[T]) are also recognised. Annotations
whose directive is unrecognised are silently ignored per the spec's "unsupported metadata" guidance.
Defined Under Namespace
Modules: ConformanceChecker, HktDirectives Classes: AssertEffect, ParamOverride, PredicateEffect, Reporter
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_extendedso 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
- .nilable_slot(facts) ⇒ Object
-
.param_type_override_map(method_def, environment: nil) ⇒ Object
Convenience reader for call sites that want to look up a single override by parameter name.
-
.parse_app_payload(payload, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) ⇒ Object
ADR-20 slice 2d.
- .parse_assert_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
-
.parse_conforms_to_annotation(string) ⇒ Object
Returns the interface name (leading
::stripped) for arigor:v1:conforms-to <Interface>annotation, ornilwhen the string is not a conforms-to directive (so callers can walk an annotation list without pre-filtering). - .parse_param_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
- .parse_predicate_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
- .parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) ⇒ Object
-
.read_assert_effects(method_def, environment: nil) ⇒ Object
Reads RBS::Extended assertion effects (
assert,assert-if-true,assert-if-false) offRBS::Definition::Method#annotations. -
.read_flow_contribution(method_def, environment: nil) ⇒ Object
Rolls up every recognised RBS::Extended directive on
method_definto a single FlowContribution with the canonical FlowContribution::Fact payload (see ADR-7 § "Slice 4-A"):. -
.read_param_type_overrides(method_def, environment: nil) ⇒ Object
Reads every
rigor:v1:param: <name> <refinement>directive offRBS::Definition::Method#annotationsand returns the resolvedParamOverridelist. -
.read_predicate_effects(method_def, environment: nil) ⇒ Object
Reads RBS::Extended predicate effects off
RBS::Definition::Method#annotations. -
.read_return_type_override(method_def, environment: nil) ⇒ Object
Reads the
rigor:v1:return: <kebab-name>directive offRBS::Definition::Method#annotations. -
.record_unresolved(reporter, payload, source_location) ⇒ Object
ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events uniformly without nil-checking each time.
- .resolve_app_arg(class_name, name_scope: nil) ⇒ Object
-
.resolve_directive_rhs(match, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
Resolves the
class_name/refinementalternation in the assert / predicate directive patterns. - .target_fields(target) ⇒ Object
Instance Method Summary collapse
- #self?.param_type_override_map ⇒ Hash[Symbol, Type::t]
- #self?.parse_assert_annotation ⇒ AssertEffect?
- #self?.parse_conforms_to_annotation ⇒ String?
- #self?.parse_param_annotation ⇒ ParamOverride?
- #self?.parse_predicate_annotation ⇒ PredicateEffect?
- #self?.parse_return_type_override ⇒ Type::t?
- #self?.read_assert_effects ⇒ Array[AssertEffect]
- #self?.read_flow_contribution ⇒ Object?
- #self?.read_param_type_overrides ⇒ Array[ParamOverride]
- #self?.read_predicate_effects ⇒ Array[PredicateEffect]
- #self?.read_return_type_override ⇒ Type::t?
Class Method Details
.build_flow_contribution(predicate_effects, assert_effects, return_override) ⇒ Object
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 |
# File 'lib/rigor/rbs_extended.rb', line 598 def build_flow_contribution(predicate_effects, assert_effects, return_override) 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
620 621 622 |
# File 'lib/rigor/rbs_extended.rb', line 620 def nilable_slot(facts) facts.empty? ? nil : facts end |
.param_type_override_map(method_def, environment: nil) ⇒ 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.
488 489 490 491 492 |
# File 'lib/rigor/rbs_extended.rb', line 488 def param_type_override_map(method_def, environment: nil) read_param_type_overrides(method_def, environment: environment) .to_h { |o| [o.param_name, o.type] } .freeze end |
.parse_app_payload(payload, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) ⇒ Object
ADR-20 slice 2d. Parses App[<uri>, <ClassName>, ...] syntax into a Rigor::Type::App. When
hkt_registry is supplied and the URI is registered with a body_tree, the App is reduced eagerly via
Inference::HktRegistry#reduce so call sites observe the unfolded form (e.g.
Union[nil, true, false, ..., Array[App[json::value, String]], Hash[String, App[json::value, String]]])
rather than the opaque carrier. When the registry is absent or the URI is unregistered, the carrier with
its registry-supplied bound (or untyped as a last-resort fallback) is returned as-is.
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 |
# File 'lib/rigor/rbs_extended.rb', line 409 def parse_app_payload(payload, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) match = APP_PAYLOAD_PATTERN.match(payload) return nil if match.nil? uri = match[:uri].to_sym arg_classes = match[:args].split(",").map(&:strip) args = arg_classes.map { |name| resolve_app_arg(name, name_scope: name_scope) } if args.any?(&:nil?) record_unresolved(reporter, "App payload `#{payload}`: unresolved arg class name", source_location) return nil end registration = hkt_registry&.registration(uri) bound = registration&.bound || Type::Combinator.untyped app = Type::App.new(uri, args, bound: bound) return app if hkt_registry.nil? || !hkt_registry.defined?(uri) reduced = hkt_registry.reduce(app) reduced || app end |
.parse_assert_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/rigor/rbs_extended.rb', line 226 def parse_assert_annotation(string, name_scope: nil, reporter: nil, source_location: nil) 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, name_scope: name_scope, reporter: reporter, source_location: source_location ) if class_name.nil? && refinement_type.nil? record_unresolved(reporter, string, source_location) return nil end AssertEffect.new( condition: condition, target_kind: target_kind, target_name: target_name, class_name: class_name, negative: negative, refinement_type: refinement_type ) end |
.parse_conforms_to_annotation(string) ⇒ Object
Returns the interface name (leading :: stripped) for a rigor:v1:conforms-to <Interface> annotation, or
nil when the string is not a conforms-to directive (so callers can walk an annotation list without
pre-filtering). The name is returned verbatim otherwise — namespace resolution happens at the loader
boundary when the interface is built.
550 551 552 553 554 555 556 557 |
# File 'lib/rigor/rbs_extended.rb', line 550 def parse_conforms_to_annotation(string) return nil if string.nil? match = CONFORMS_TO_DIRECTIVE_PATTERN.match(string) return nil if match.nil? match[:interface].to_s.sub(/\A::/, "") end |
.parse_param_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/rigor/rbs_extended.rb', line 510 def parse_param_annotation(string, name_scope: nil, reporter: nil, source_location: nil) match = PARAM_DIRECTIVE_PATTERN.match(string) return nil if match.nil? type = Builtins::ImportedRefinements.parse( match[:payload], name_scope: name_scope, reporter: reporter, source_location: source_location ) if type.nil? record_unresolved(reporter, string, source_location) return nil end ParamOverride.new(param_name: match[:param].to_sym, type: type) end |
.parse_predicate_annotation(string, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/rigor/rbs_extended.rb', line 146 def parse_predicate_annotation(string, name_scope: nil, reporter: nil, source_location: nil) 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, name_scope: name_scope, reporter: reporter, source_location: source_location ) if class_name.nil? && refinement_type.nil? record_unresolved(reporter, string, source_location) return nil end 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, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/rigor/rbs_extended.rb', line 380 def parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil) match = RETURN_DIRECTIVE_PATTERN.match(string) return nil if match.nil? app_type = parse_app_payload( match[:payload], name_scope: name_scope, reporter: reporter, source_location: source_location, hkt_registry: hkt_registry ) return app_type if app_type type = Builtins::ImportedRefinements.parse( match[:payload], name_scope: name_scope, reporter: reporter, source_location: source_location ) record_unresolved(reporter, string, source_location) if type.nil? type end |
.read_assert_effects(method_def, environment: nil) ⇒ 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.
See read_predicate_effects for the environment: keyword contract.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/rigor/rbs_extended.rb', line 180 def read_assert_effects(method_def, environment: nil) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? name_scope = environment&.name_scope reporter = environment&.rbs_extended_reporter effects = [] annotations.each do |annotation| effect = parse_assert_annotation( annotation.string, name_scope: name_scope, reporter: reporter, source_location: annotation.location ) effects << effect if effect end effects.uniq end |
.read_flow_contribution(method_def, environment: nil) ⇒ 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_factspredicate-if-false→falsey_factsassert→post_return_factsassert-if-true→truthy_factsassert-if-false→falsey_factsreturn: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).
See read_predicate_effects for the environment: keyword contract.
587 588 589 590 591 592 593 594 595 596 |
# File 'lib/rigor/rbs_extended.rb', line 587 def read_flow_contribution(method_def, environment: nil) return nil if method_def.nil? predicate_effects = read_predicate_effects(method_def, environment: environment) assert_effects = read_assert_effects(method_def, environment: environment) return_override = read_return_type_override(method_def, environment: environment) 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, environment: nil) ⇒ 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[String] argument is flagged as an argument-type
mismatch at the call site.
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 |
# File 'lib/rigor/rbs_extended.rb', line 466 def read_param_type_overrides(method_def, environment: nil) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? name_scope = environment&.name_scope reporter = environment&.rbs_extended_reporter annotations.filter_map do |annotation| parse_param_annotation( annotation.string, name_scope: name_scope, reporter: reporter, source_location: annotation.location ) end end |
.read_predicate_effects(method_def, environment: nil) ⇒ 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.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/rigor/rbs_extended.rb', line 102 def read_predicate_effects(method_def, environment: nil) return [] if method_def.nil? annotations = method_def.annotations return [] if annotations.nil? || annotations.empty? name_scope = environment&.name_scope reporter = environment&.rbs_extended_reporter effects = [] annotations.each do |annotation| effect = parse_predicate_annotation( annotation.string, name_scope: name_scope, reporter: reporter, source_location: annotation.location ) effects << effect if effect end effects.uniq end |
.read_return_type_override(method_def, environment: nil) ⇒ 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; ADR-13 slice 3b surfaces the miss as adynamic.rbs-extended.unresolved:infodiagnostic when anenvironment:is supplied).
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/rigor/rbs_extended.rb', line 325 def read_return_type_override(method_def, environment: nil) return nil if method_def.nil? annotations = method_def.annotations return nil if annotations.nil? || annotations.empty? name_scope = environment&.name_scope reporter = environment&.rbs_extended_reporter hkt_registry = environment&.hkt_registry annotations.each do |annotation| type = parse_return_type_override( annotation.string, name_scope: name_scope, reporter: reporter, source_location: annotation.location, hkt_registry: hkt_registry ) return type if type end nil end |
.record_unresolved(reporter, payload, source_location) ⇒ Object
ADR-13 slice 3b — guards every reporter call so the in-RbsExtended-module call sites can record events
uniformly without nil-checking each time. When the reporter is nil (the v0.1.0 → v0.1.3 default for call
sites that do not yet thread environment:), the call is a no-op and the parser stays fail-soft.
627 628 629 630 631 |
# File 'lib/rigor/rbs_extended.rb', line 627 def record_unresolved(reporter, payload, source_location) return if reporter.nil? reporter.record_unresolved(payload: payload, source_location: source_location) end |
.resolve_app_arg(class_name, name_scope: nil) ⇒ Object
432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/rigor/rbs_extended.rb', line 432 def resolve_app_arg(class_name, name_scope: nil) return nil unless /\A(?:::)?(?:[A-Z]\w*)(?:::[A-Z]\w*)*\z/.match?(class_name) normalized = class_name.sub(/\A::/, "") return Type::Nominal.new(normalized) if name_scope.nil? if name_scope.respond_to?(:nominal_for_name) resolved = name_scope.nominal_for_name(normalized) return resolved if resolved end Type::Nominal.new(normalized) end |
.resolve_directive_rhs(match, name_scope: nil, reporter: nil, source_location: nil) ⇒ Object
Resolves the class_name / refinement alternation in
the assert / predicate directive patterns. Returns
[class_name, refinement_type, negative]:
- Class-name arm matched:
class_nameis the resolved string (leading::stripped),refinement_typeis nil,negativereflects the optional~prefix. - Refinement arm matched:
class_nameis nil,refinement_typeis the resolvedRigor::Type,negativereflects the~prefix. v0.0.5 supports refinement-form negation for theDifference[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).
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/rigor/rbs_extended.rb', line 275 def resolve_directive_rhs(match, name_scope: nil, reporter: nil, source_location: nil) 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, name_scope: name_scope, reporter: reporter, source_location: source_location ) return [nil, nil, false] if type.nil? [nil, type, negative] end |
.target_fields(target) ⇒ Object
294 295 296 297 298 299 300 |
# File 'lib/rigor/rbs_extended.rb', line 294 def target_fields(target) if target == "self" %i[self self] else [:parameter, target.to_sym] end end |
Instance Method Details
#self?.param_type_override_map ⇒ Hash[Symbol, Type::t]
58 |
# File 'sig/rigor/rbs_extended.rbs', line 58
def self?.param_type_override_map: (untyped method_def) -> Hash[Symbol, Type::t]
|
#self?.parse_assert_annotation ⇒ AssertEffect?
41 |
# File 'sig/rigor/rbs_extended.rbs', line 41
def self?.parse_assert_annotation: (String string) -> AssertEffect?
|
#self?.parse_conforms_to_annotation ⇒ String?
48 |
# File 'sig/rigor/rbs_extended.rbs', line 48
def self?.parse_conforms_to_annotation: (String? string) -> String?
|
#self?.parse_param_annotation ⇒ ParamOverride?
59 |
# File 'sig/rigor/rbs_extended.rbs', line 59
def self?.parse_param_annotation: (String string) -> ParamOverride?
|
#self?.parse_predicate_annotation ⇒ PredicateEffect?
22 |
# File 'sig/rigor/rbs_extended.rbs', line 22
def self?.parse_predicate_annotation: (String string) -> PredicateEffect?
|
#self?.parse_return_type_override ⇒ Type::t?
44 |
# File 'sig/rigor/rbs_extended.rbs', line 44
def self?.parse_return_type_override: (String string) -> Type::t?
|
#self?.read_assert_effects ⇒ Array[AssertEffect]
40 |
# File 'sig/rigor/rbs_extended.rbs', line 40
def self?.read_assert_effects: (untyped method_def) -> Array[AssertEffect]
|
#self?.read_flow_contribution ⇒ Object?
46 |
# File 'sig/rigor/rbs_extended.rbs', line 46
def self?.read_flow_contribution: (untyped method_def) -> untyped?
|
#self?.read_param_type_overrides ⇒ Array[ParamOverride]
57 |
# File 'sig/rigor/rbs_extended.rbs', line 57
def self?.read_param_type_overrides: (untyped method_def) -> Array[ParamOverride]
|
#self?.read_predicate_effects ⇒ Array[PredicateEffect]
21 |
# File 'sig/rigor/rbs_extended.rbs', line 21
def self?.read_predicate_effects: (untyped method_def) -> Array[PredicateEffect]
|
#self?.read_return_type_override ⇒ Type::t?
43 |
# File 'sig/rigor/rbs_extended.rbs', line 43
def self?.read_return_type_override: (untyped method_def) -> Type::t?
|