rigortype 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +215 -117
- data/data/builtins/ruby_core/comparable.yml +87 -0
- data/data/builtins/ruby_core/complex.yml +505 -0
- data/data/builtins/ruby_core/date.yml +1737 -0
- data/data/builtins/ruby_core/enumerable.yml +557 -0
- data/data/builtins/ruby_core/file.yml +9 -0
- data/data/builtins/ruby_core/hash.yml +936 -0
- data/data/builtins/ruby_core/range.yml +389 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/set.yml +594 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +752 -0
- data/lib/rigor/analysis/check_rules.rb +11 -3
- data/lib/rigor/builtins/imported_refinements.rb +192 -10
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +181 -12
- data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
- data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/hash_catalog.rb +40 -0
- data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
- data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +145 -11
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +202 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +135 -6
- data/lib/rigor/inference/method_dispatcher.rb +3 -1
- data/lib/rigor/inference/method_parameter_binder.rb +29 -4
- data/lib/rigor/inference/narrowing.rb +211 -0
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +6 -0
- data/lib/rigor/rbs_extended.rb +170 -14
- data/lib/rigor/type/combinator.rb +90 -0
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/type/intersection.rb +135 -0
- data/lib/rigor/type/refined.rb +174 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/rbs_extended.rbs +11 -0
- data/sig/rigor/type.rbs +75 -35
- metadata +22 -1
|
@@ -241,6 +241,55 @@ module Rigor
|
|
|
241
241
|
narrow_class_dispatch(type, class_name, context)
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
+
# Negation pair for `assert_value is ~refinement` /
|
|
245
|
+
# `predicate-if-* … is ~refinement` directives. Computes
|
|
246
|
+
# the complement of `refinement` within the current
|
|
247
|
+
# local's domain `current_type`.
|
|
248
|
+
#
|
|
249
|
+
# Carrier-by-carrier rules:
|
|
250
|
+
#
|
|
251
|
+
# - `Difference[base, Constant[v]]`. Complement of
|
|
252
|
+
# `base \ {v}` within `current_type`. Walk the current
|
|
253
|
+
# type's union members, keep each part disjoint from
|
|
254
|
+
# `base`, and add the removed-value Constant once when
|
|
255
|
+
# any current member covers it. `assert s is
|
|
256
|
+
# ~non-empty-string` over `s: String | nil` narrows to
|
|
257
|
+
# `Constant[""] | NilClass`.
|
|
258
|
+
# - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is
|
|
259
|
+
# the two open halves `int<min, a-1>` and
|
|
260
|
+
# `int<b+1, max>`, each intersected with the
|
|
261
|
+
# integer-domain parts of `current_type`. Non-integer
|
|
262
|
+
# parts (nil, String, …) of a Union receiver survive
|
|
263
|
+
# unchanged. `assert n is ~int<5, 10>` over `n:
|
|
264
|
+
# Integer | nil` narrows to `int<min, 4> | int<11,
|
|
265
|
+
# max> | NilClass`.
|
|
266
|
+
# - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De
|
|
267
|
+
# Morgan: `D \ (M1 ∩ M2) = (D \ M1) ∪ (D \ M2)`. Each
|
|
268
|
+
# member's complement is computed independently within
|
|
269
|
+
# `current_type` and the results are unioned. Members
|
|
270
|
+
# the algebra cannot complement (Refined, non-Constant
|
|
271
|
+
# Difference, …) contribute `current_type` itself, so
|
|
272
|
+
# the union widens the answer to `current_type` —
|
|
273
|
+
# sound but imprecise.
|
|
274
|
+
# - `Refined[base, predicate]`. Predicate complements are
|
|
275
|
+
# not reducible to a finite carrier without a richer
|
|
276
|
+
# shape (e.g. `~lowercase-string` is "uppercase OR
|
|
277
|
+
# mixed-case"); `current_type` is returned unchanged.
|
|
278
|
+
def narrow_not_refinement(current_type, refinement_type)
|
|
279
|
+
case refinement_type
|
|
280
|
+
when Type::Difference
|
|
281
|
+
return current_type unless refinement_type.removed.is_a?(Type::Constant)
|
|
282
|
+
|
|
283
|
+
complement_difference(current_type, refinement_type)
|
|
284
|
+
when Type::IntegerRange
|
|
285
|
+
complement_integer_range(current_type, refinement_type)
|
|
286
|
+
when Type::Intersection
|
|
287
|
+
complement_intersection(current_type, refinement_type)
|
|
288
|
+
else
|
|
289
|
+
current_type
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
244
293
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
|
|
245
294
|
# always; when no narrowing rule matches the predicate node both
|
|
246
295
|
# entries are the receiver scope unchanged.
|
|
@@ -321,6 +370,162 @@ module Rigor
|
|
|
321
370
|
class << self
|
|
322
371
|
private
|
|
323
372
|
|
|
373
|
+
# Complement of `Difference[base, Constant[v]]` within
|
|
374
|
+
# `current_type`. Walks the current type's union members,
|
|
375
|
+
# keeps each member disjoint from `base` (those values
|
|
376
|
+
# were never in the refinement to begin with), and adds
|
|
377
|
+
# the removed-value `Constant[v]` exactly once when any
|
|
378
|
+
# current member covers it. Members that are fully
|
|
379
|
+
# contained in the refinement (i.e. inside `base` and
|
|
380
|
+
# NOT equal to the removed value) are dropped — they are
|
|
381
|
+
# exactly the values the negation excludes.
|
|
382
|
+
def complement_difference(current_type, difference)
|
|
383
|
+
base = difference.base
|
|
384
|
+
removed = difference.removed
|
|
385
|
+
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
386
|
+
|
|
387
|
+
survivors = []
|
|
388
|
+
add_removed = false
|
|
389
|
+
parts.each do |part|
|
|
390
|
+
if base_disjoint?(base, part)
|
|
391
|
+
survivors << part
|
|
392
|
+
elsif part_covers_constant?(part, removed)
|
|
393
|
+
add_removed = true
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
survivors << removed if add_removed
|
|
397
|
+
|
|
398
|
+
return current_type if survivors.empty?
|
|
399
|
+
|
|
400
|
+
Type::Combinator.union(*survivors)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def base_disjoint?(base, part)
|
|
404
|
+
base.accepts(part, mode: :gradual).no?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def part_covers_constant?(part, constant)
|
|
408
|
+
result = part.accepts(constant, mode: :gradual)
|
|
409
|
+
result.yes? || result.maybe?
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Complement of an `IntegerRange[a, b]` within
|
|
413
|
+
# `current_type`. Splits the range complement into the
|
|
414
|
+
# two open halves `int<min, a-1>` and `int<b+1, max>`
|
|
415
|
+
# (skipping a half when its bound is infinity), then
|
|
416
|
+
# intersects each half with the integer-domain parts of
|
|
417
|
+
# `current_type`. Non-integer parts of a Union receiver
|
|
418
|
+
# (nil, String, …) survive unchanged.
|
|
419
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
420
|
+
def complement_integer_range(current_type, range)
|
|
421
|
+
halves = integer_range_complement_halves(range)
|
|
422
|
+
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
423
|
+
|
|
424
|
+
survivors = []
|
|
425
|
+
parts.each do |part|
|
|
426
|
+
if integer_member?(part)
|
|
427
|
+
halves.each do |half|
|
|
428
|
+
meet = intersect_integer_part(part, half)
|
|
429
|
+
survivors << meet unless meet.nil? || meet.is_a?(Type::Bot)
|
|
430
|
+
end
|
|
431
|
+
else
|
|
432
|
+
survivors << part
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
return current_type if survivors.empty?
|
|
437
|
+
|
|
438
|
+
Type::Combinator.union(*survivors)
|
|
439
|
+
end
|
|
440
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
441
|
+
|
|
442
|
+
# Returns the two open halves of an IntegerRange's
|
|
443
|
+
# complement: the left half `int<-∞, a-1>` (when `a` is
|
|
444
|
+
# finite) and the right half `int<b+1, ∞>` (when `b` is
|
|
445
|
+
# finite). Universal ranges (both bounds infinite) yield
|
|
446
|
+
# an empty array — the complement is empty.
|
|
447
|
+
def integer_range_complement_halves(range)
|
|
448
|
+
halves = []
|
|
449
|
+
left_max = range.min
|
|
450
|
+
right_min = range.max
|
|
451
|
+
|
|
452
|
+
if left_max.is_a?(Integer)
|
|
453
|
+
halves << Type::Combinator.integer_range(Type::IntegerRange::NEG_INFINITY, left_max - 1)
|
|
454
|
+
end
|
|
455
|
+
if right_min.is_a?(Integer)
|
|
456
|
+
halves << Type::Combinator.integer_range(right_min + 1, Type::IntegerRange::POS_INFINITY)
|
|
457
|
+
end
|
|
458
|
+
halves
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def integer_member?(part)
|
|
462
|
+
case part
|
|
463
|
+
when Type::Constant then part.value.is_a?(Integer)
|
|
464
|
+
when Type::IntegerRange then true
|
|
465
|
+
when Type::Nominal then part.class_name == "Integer"
|
|
466
|
+
else false
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Intersect an integer-domain part with a complement
|
|
471
|
+
# half-range. For a Nominal[Integer] receiver the meet
|
|
472
|
+
# is the half itself; for an existing IntegerRange the
|
|
473
|
+
# meet narrows both bounds; for a Constant[Integer] the
|
|
474
|
+
# meet is the constant when the half covers it,
|
|
475
|
+
# otherwise nil.
|
|
476
|
+
def intersect_integer_part(part, half)
|
|
477
|
+
case part
|
|
478
|
+
when Type::Nominal
|
|
479
|
+
half
|
|
480
|
+
when Type::IntegerRange
|
|
481
|
+
integer_range_meet(part, half)
|
|
482
|
+
when Type::Constant
|
|
483
|
+
half.covers?(part.value) ? part : nil
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def integer_range_meet(left, right)
|
|
488
|
+
low = numeric_to_bound([integer_bound_value(left.min), integer_bound_value(right.min)].max)
|
|
489
|
+
high = numeric_to_bound([integer_bound_value(left.max), integer_bound_value(right.max)].min)
|
|
490
|
+
return nil if integer_range_disjoint?(low, high)
|
|
491
|
+
|
|
492
|
+
Type::Combinator.integer_range(low, high)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def integer_bound_value(bound)
|
|
496
|
+
return -Float::INFINITY if bound == Type::IntegerRange::NEG_INFINITY
|
|
497
|
+
return Float::INFINITY if bound == Type::IntegerRange::POS_INFINITY
|
|
498
|
+
|
|
499
|
+
bound
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def numeric_to_bound(value)
|
|
503
|
+
return Type::IntegerRange::NEG_INFINITY if value == -Float::INFINITY
|
|
504
|
+
return Type::IntegerRange::POS_INFINITY if value == Float::INFINITY
|
|
505
|
+
|
|
506
|
+
value.to_i
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def integer_range_disjoint?(low, high)
|
|
510
|
+
return false if low == Type::IntegerRange::NEG_INFINITY
|
|
511
|
+
return false if high == Type::IntegerRange::POS_INFINITY
|
|
512
|
+
|
|
513
|
+
low > high
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪
|
|
517
|
+
# …`. Each member's complement is computed independently
|
|
518
|
+
# within `current_type` and the results are unioned.
|
|
519
|
+
# Members the algebra cannot complement contribute
|
|
520
|
+
# `current_type` itself, so the union widens to
|
|
521
|
+
# `current_type` overall — sound but imprecise.
|
|
522
|
+
def complement_intersection(current_type, intersection)
|
|
523
|
+
per_member = intersection.members.map do |member|
|
|
524
|
+
Narrowing.narrow_not_refinement(current_type, member)
|
|
525
|
+
end
|
|
526
|
+
Type::Combinator.union(*per_member)
|
|
527
|
+
end
|
|
528
|
+
|
|
324
529
|
def falsey_value?(value)
|
|
325
530
|
value.nil? || value == false
|
|
326
531
|
end
|
|
@@ -1029,6 +1234,12 @@ module Rigor
|
|
|
1029
1234
|
# the effect's `negative?` flag. Shared between
|
|
1030
1235
|
# predicate-if-* and assert-if-* application paths.
|
|
1031
1236
|
def narrow_for_effect(current, effect, environment)
|
|
1237
|
+
if effect.respond_to?(:refinement?) && effect.refinement?
|
|
1238
|
+
return narrow_not_refinement(current, effect.refinement_type) if effect.negative?
|
|
1239
|
+
|
|
1240
|
+
return effect.refinement_type
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1032
1243
|
if effect.negative?
|
|
1033
1244
|
narrow_not_class(current, effect.class_name, exact: false, environment: environment)
|
|
1034
1245
|
else
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../scope"
|
|
6
6
|
require_relative "../type"
|
|
7
|
+
require_relative "narrowing"
|
|
7
8
|
require_relative "statement_evaluator"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
@@ -475,16 +476,9 @@ module Rigor
|
|
|
475
476
|
|
|
476
477
|
case node
|
|
477
478
|
when Prism::ModuleNode, Prism::ClassNode
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
singleton = Type::Combinator.singleton_of(full)
|
|
482
|
-
identity_table[node.constant_path] = singleton
|
|
483
|
-
discovered[full] = singleton
|
|
484
|
-
child_prefix = qualified_prefix + [name]
|
|
485
|
-
record_declarations(node.body, child_prefix, identity_table, discovered) if node.body
|
|
486
|
-
return
|
|
487
|
-
end
|
|
479
|
+
return if record_class_or_module?(node, qualified_prefix, identity_table, discovered)
|
|
480
|
+
when Prism::ConstantWriteNode
|
|
481
|
+
return if record_data_define_constant?(node, qualified_prefix, identity_table, discovered)
|
|
488
482
|
end
|
|
489
483
|
|
|
490
484
|
node.compact_child_nodes.each do |child|
|
|
@@ -492,6 +486,60 @@ module Rigor
|
|
|
492
486
|
end
|
|
493
487
|
end
|
|
494
488
|
|
|
489
|
+
def record_class_or_module?(node, qualified_prefix, identity_table, discovered)
|
|
490
|
+
name = qualified_name_for(node.constant_path)
|
|
491
|
+
return false unless name
|
|
492
|
+
|
|
493
|
+
full = (qualified_prefix + [name]).join("::")
|
|
494
|
+
singleton = Type::Combinator.singleton_of(full)
|
|
495
|
+
identity_table[node.constant_path] = singleton
|
|
496
|
+
discovered[full] = singleton
|
|
497
|
+
child_prefix = qualified_prefix + [name]
|
|
498
|
+
record_declarations(node.body, child_prefix, identity_table, discovered) if node.body
|
|
499
|
+
true
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Recognises `Const = Data.define(*Symbol) [do ... end]` and registers
|
|
503
|
+
# `Const` (qualified by the surrounding class/module path) as a
|
|
504
|
+
# discovered class. `Const.new(...)` then resolves to a fresh
|
|
505
|
+
# `Nominal[Const]` via `meta_new`, instead of the un-narrowed
|
|
506
|
+
# `Dynamic[top]` returned by the default `Class#new` envelope.
|
|
507
|
+
#
|
|
508
|
+
# The Data.define block body, if present, is recursed into so any
|
|
509
|
+
# nested class/module declarations in the override block (rare but
|
|
510
|
+
# legal) still feed the discovered table.
|
|
511
|
+
def record_data_define_constant?(node, qualified_prefix, identity_table, discovered)
|
|
512
|
+
return false unless data_define_call?(node.value)
|
|
513
|
+
|
|
514
|
+
full = (qualified_prefix + [node.name.to_s]).join("::")
|
|
515
|
+
discovered[full] = Type::Combinator.singleton_of(full)
|
|
516
|
+
record_declarations(node.value, qualified_prefix, identity_table, discovered)
|
|
517
|
+
true
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end`
|
|
521
|
+
# at constant-write rvalue position. The receiver MUST be the bare
|
|
522
|
+
# `Data` constant (or `::Data`); other receivers (a local variable, a
|
|
523
|
+
# method call return) are rejected because their identity is not
|
|
524
|
+
# statically known.
|
|
525
|
+
def data_define_call?(node)
|
|
526
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
527
|
+
return false unless node.name == :define
|
|
528
|
+
return false unless data_constant_receiver?(node.receiver)
|
|
529
|
+
|
|
530
|
+
args = node.arguments&.arguments || []
|
|
531
|
+
args.all?(Prism::SymbolNode)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def data_constant_receiver?(node)
|
|
535
|
+
case node
|
|
536
|
+
when Prism::ConstantReadNode
|
|
537
|
+
node.name == :Data
|
|
538
|
+
when Prism::ConstantPathNode
|
|
539
|
+
node.parent.nil? && node.name == :Data
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
495
543
|
def qualified_name_for(constant_path_node)
|
|
496
544
|
case constant_path_node
|
|
497
545
|
when Prism::ConstantReadNode
|
|
@@ -515,6 +563,13 @@ module Rigor
|
|
|
515
563
|
# Prism node the StatementEvaluator did not visit (i.e. expression-
|
|
516
564
|
# interior nodes like the receiver/args of a CallNode). Those
|
|
517
565
|
# nodes inherit their nearest recorded ancestor's scope.
|
|
566
|
+
#
|
|
567
|
+
# `IfNode` / `UnlessNode` are special-cased: the truthy and falsey
|
|
568
|
+
# branches each get their predicate's narrowed scope before
|
|
569
|
+
# recursing. This handles expression-position conditionals
|
|
570
|
+
# (e.g. `cache[k] = if cond; t; else; e; end` and conditionals
|
|
571
|
+
# nested as call arguments) which are typed by ExpressionTyper
|
|
572
|
+
# without going through `eval_if`'s narrowing path.
|
|
518
573
|
def propagate(node, table, parent_scope)
|
|
519
574
|
return unless node.is_a?(Prism::Node)
|
|
520
575
|
|
|
@@ -526,7 +581,28 @@ module Rigor
|
|
|
526
581
|
parent_scope
|
|
527
582
|
end
|
|
528
583
|
|
|
529
|
-
node
|
|
584
|
+
case node
|
|
585
|
+
when Prism::IfNode
|
|
586
|
+
propagate_if_branches(node, table, current_scope)
|
|
587
|
+
when Prism::UnlessNode
|
|
588
|
+
propagate_unless_branches(node, table, current_scope)
|
|
589
|
+
else
|
|
590
|
+
node.compact_child_nodes.each { |child| propagate(child, table, current_scope) }
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def propagate_if_branches(node, table, current_scope)
|
|
595
|
+
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, current_scope)
|
|
596
|
+
propagate(node.predicate, table, current_scope) if node.predicate
|
|
597
|
+
propagate(node.statements, table, truthy_scope) if node.statements
|
|
598
|
+
propagate(node.subsequent, table, falsey_scope) if node.subsequent
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def propagate_unless_branches(node, table, current_scope)
|
|
602
|
+
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, current_scope)
|
|
603
|
+
propagate(node.predicate, table, current_scope) if node.predicate
|
|
604
|
+
propagate(node.statements, table, falsey_scope) if node.statements
|
|
605
|
+
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
530
606
|
end
|
|
531
607
|
end
|
|
532
608
|
# rubocop:enable Metrics/ModuleLength
|
|
@@ -894,6 +894,12 @@ module Rigor
|
|
|
894
894
|
end
|
|
895
895
|
|
|
896
896
|
def narrow_for_assert_effect(current_type, effect, environment)
|
|
897
|
+
if effect.refinement?
|
|
898
|
+
return Narrowing.narrow_not_refinement(current_type, effect.refinement_type) if effect.negative?
|
|
899
|
+
|
|
900
|
+
return effect.refinement_type
|
|
901
|
+
end
|
|
902
|
+
|
|
897
903
|
if effect.negative?
|
|
898
904
|
Narrowing.narrow_not_class(current_type, effect.class_name, exact: false, environment: environment)
|
|
899
905
|
else
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -50,10 +50,19 @@ module Rigor
|
|
|
50
50
|
# when the directive uses the `~ClassName` form, in
|
|
51
51
|
# which case the engine narrows AWAY from `class_name`
|
|
52
52
|
# (`Narrowing.narrow_not_class`) instead of toward it.
|
|
53
|
-
|
|
53
|
+
#
|
|
54
|
+
# `refinement_type` is non-nil when the right-hand side is
|
|
55
|
+
# a kebab-case refinement name (`non-empty-string`,
|
|
56
|
+
# `lowercase-string`, …) instead of a Capitalised class
|
|
57
|
+
# name. The narrowing tier substitutes the carrier for the
|
|
58
|
+
# current local type; `class_name` is then nil and
|
|
59
|
+
# `negative` is false (refinement-form directives do not
|
|
60
|
+
# support `~T` negation in v0.0.4).
|
|
61
|
+
PredicateEffect = Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type) do
|
|
54
62
|
def truthy_only? = edge == :truthy_only
|
|
55
63
|
def falsey_only? = edge == :falsey_only
|
|
56
64
|
def negative? = negative == true
|
|
65
|
+
def refinement? = !refinement_type.nil?
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
# Returned for `assert` / `assert-if-true` /
|
|
@@ -71,11 +80,12 @@ module Rigor
|
|
|
71
80
|
#
|
|
72
81
|
# `negative` mirrors `PredicateEffect`: true when the
|
|
73
82
|
# directive uses `~ClassName` syntax.
|
|
74
|
-
AssertEffect = Data.define(:condition, :target_kind, :target_name, :class_name, :negative) do
|
|
83
|
+
AssertEffect = Data.define(:condition, :target_kind, :target_name, :class_name, :negative, :refinement_type) do
|
|
75
84
|
def always? = condition == :always
|
|
76
85
|
def if_truthy_return? = condition == :if_truthy_return
|
|
77
86
|
def if_falsey_return? = condition == :if_falsey_return
|
|
78
87
|
def negative? = negative == true
|
|
88
|
+
def refinement? = !refinement_type.nil?
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
module_function
|
|
@@ -100,6 +110,14 @@ module Rigor
|
|
|
100
110
|
effects.uniq
|
|
101
111
|
end
|
|
102
112
|
|
|
113
|
+
# The right-hand side accepts either a Capitalised class
|
|
114
|
+
# name (with optional `~` negation, optional `::` prefix,
|
|
115
|
+
# qualified names) OR a kebab-case refinement payload
|
|
116
|
+
# routed through `Builtins::ImportedRefinements::Parser`
|
|
117
|
+
# (bare names, `name[T]`, `name<min, max>`). The two arms
|
|
118
|
+
# share the same overall directive shape; the parser
|
|
119
|
+
# detects which form matched by looking at the `class_name`
|
|
120
|
+
# vs `refinement` capture groups.
|
|
103
121
|
PREDICATE_DIRECTIVE_PATTERN = /
|
|
104
122
|
\A
|
|
105
123
|
rigor:v1:(?<directive>predicate-if-(?:true|false))
|
|
@@ -107,7 +125,11 @@ module Rigor
|
|
|
107
125
|
(?<target>self|[a-z_][a-zA-Z0-9_]*)
|
|
108
126
|
\s+is\s+
|
|
109
127
|
(?<negation>~?)
|
|
110
|
-
(
|
|
128
|
+
(?:
|
|
129
|
+
(?<class_name>(?:::)?[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)
|
|
130
|
+
|
|
|
131
|
+
(?<refinement>[a-z][a-z0-9-]*(?:[\[<][^\]>]*[\]>])?)
|
|
132
|
+
)
|
|
111
133
|
\s*
|
|
112
134
|
\z
|
|
113
135
|
/x
|
|
@@ -119,16 +141,18 @@ module Rigor
|
|
|
119
141
|
|
|
120
142
|
directive = match[:directive].to_s
|
|
121
143
|
target = match[:target].to_s
|
|
122
|
-
class_name = match[:class_name].to_s.sub(/\A::/, "")
|
|
123
144
|
edge = directive == "predicate-if-true" ? :truthy_only : :falsey_only
|
|
124
|
-
target_kind = target
|
|
125
|
-
|
|
145
|
+
target_kind, target_name = target_fields(target)
|
|
146
|
+
class_name, refinement_type, negative = resolve_directive_rhs(match)
|
|
147
|
+
return nil if class_name.nil? && refinement_type.nil?
|
|
148
|
+
|
|
126
149
|
PredicateEffect.new(
|
|
127
150
|
edge: edge,
|
|
128
151
|
target_kind: target_kind,
|
|
129
152
|
target_name: target_name,
|
|
130
153
|
class_name: class_name,
|
|
131
|
-
negative:
|
|
154
|
+
negative: negative,
|
|
155
|
+
refinement_type: refinement_type
|
|
132
156
|
)
|
|
133
157
|
end
|
|
134
158
|
|
|
@@ -158,7 +182,11 @@ module Rigor
|
|
|
158
182
|
(?<target>self|[a-z_][a-zA-Z0-9_]*)
|
|
159
183
|
\s+is\s+
|
|
160
184
|
(?<negation>~?)
|
|
161
|
-
(
|
|
185
|
+
(?:
|
|
186
|
+
(?<class_name>(?:::)?[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)
|
|
187
|
+
|
|
|
188
|
+
(?<refinement>[a-z][a-z0-9-]*(?:[\[<][^\]>]*[\]>])?)
|
|
189
|
+
)
|
|
162
190
|
\s*
|
|
163
191
|
\z
|
|
164
192
|
/x
|
|
@@ -180,18 +208,60 @@ module Rigor
|
|
|
180
208
|
return nil if condition.nil?
|
|
181
209
|
|
|
182
210
|
target = match[:target].to_s
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
211
|
+
target_kind, target_name = target_fields(target)
|
|
212
|
+
class_name, refinement_type, negative = resolve_directive_rhs(match)
|
|
213
|
+
return nil if class_name.nil? && refinement_type.nil?
|
|
214
|
+
|
|
186
215
|
AssertEffect.new(
|
|
187
216
|
condition: condition,
|
|
188
217
|
target_kind: target_kind,
|
|
189
218
|
target_name: target_name,
|
|
190
219
|
class_name: class_name,
|
|
191
|
-
negative:
|
|
220
|
+
negative: negative,
|
|
221
|
+
refinement_type: refinement_type
|
|
192
222
|
)
|
|
193
223
|
end
|
|
194
224
|
|
|
225
|
+
# Resolves the `class_name` / `refinement` alternation in
|
|
226
|
+
# the assert / predicate directive patterns. Returns
|
|
227
|
+
# `[class_name, refinement_type, negative]`:
|
|
228
|
+
#
|
|
229
|
+
# - Class-name arm matched: `class_name` is the resolved
|
|
230
|
+
# string (leading `::` stripped), `refinement_type` is
|
|
231
|
+
# nil, `negative` reflects the optional `~` prefix.
|
|
232
|
+
# - Refinement arm matched: `class_name` is nil,
|
|
233
|
+
# `refinement_type` is the resolved `Rigor::Type`,
|
|
234
|
+
# `negative` reflects the `~` prefix. v0.0.5 supports
|
|
235
|
+
# refinement-form negation for the `Difference[base,
|
|
236
|
+
# Constant]` shape (the narrowing tier computes the
|
|
237
|
+
# complement decomposition); other refinement carriers
|
|
238
|
+
# under negation fall back to the conservative
|
|
239
|
+
# "current_type unchanged" answer.
|
|
240
|
+
# - Refinement payload unparseable: returns
|
|
241
|
+
# `[nil, nil, false]` so callers can drop the directive
|
|
242
|
+
# silently (fail-soft policy).
|
|
243
|
+
def resolve_directive_rhs(match)
|
|
244
|
+
negative = match[:negation].to_s == "~"
|
|
245
|
+
class_capture = match[:class_name]
|
|
246
|
+
return [class_capture.to_s.sub(/\A::/, ""), nil, negative] if class_capture
|
|
247
|
+
|
|
248
|
+
refinement_capture = match[:refinement]
|
|
249
|
+
return [nil, nil, false] if refinement_capture.nil?
|
|
250
|
+
|
|
251
|
+
type = Builtins::ImportedRefinements.parse(refinement_capture)
|
|
252
|
+
return [nil, nil, false] if type.nil?
|
|
253
|
+
|
|
254
|
+
[nil, type, negative]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def target_fields(target)
|
|
258
|
+
if target == "self"
|
|
259
|
+
%i[self self]
|
|
260
|
+
else
|
|
261
|
+
[:parameter, target.to_sym]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
195
265
|
# Reads the `rigor:v1:return: <kebab-name>` directive off
|
|
196
266
|
# `RBS::Definition::Method#annotations`. The directive
|
|
197
267
|
# overrides a method's RBS-declared return type with one of
|
|
@@ -238,11 +308,20 @@ module Rigor
|
|
|
238
308
|
nil
|
|
239
309
|
end
|
|
240
310
|
|
|
311
|
+
# The trailing payload supports the full refinement
|
|
312
|
+
# grammar in `Builtins::ImportedRefinements::Parser` —
|
|
313
|
+
# bare kebab-case names plus parameterised forms like
|
|
314
|
+
# `non-empty-array[Integer]`, `non-empty-hash[Symbol,
|
|
315
|
+
# Integer]`, and `int<5, 10>`. The directive head is
|
|
316
|
+
# consumed by the regex; the rest is forwarded to the
|
|
317
|
+
# refinement parser. Anything the parser cannot resolve
|
|
318
|
+
# falls back to nil so the call site keeps the
|
|
319
|
+
# RBS-declared return type.
|
|
241
320
|
RETURN_DIRECTIVE_PATTERN = /
|
|
242
321
|
\A
|
|
243
322
|
rigor:v1:return:
|
|
244
323
|
\s+
|
|
245
|
-
(?<
|
|
324
|
+
(?<payload>\S(?:.*\S)?)
|
|
246
325
|
\s*
|
|
247
326
|
\z
|
|
248
327
|
/x
|
|
@@ -252,7 +331,84 @@ module Rigor
|
|
|
252
331
|
match = RETURN_DIRECTIVE_PATTERN.match(string)
|
|
253
332
|
return nil if match.nil?
|
|
254
333
|
|
|
255
|
-
Builtins::ImportedRefinements.
|
|
334
|
+
Builtins::ImportedRefinements.parse(match[:payload])
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Returned for `rigor:v1:param: <name> <refinement>`. The
|
|
338
|
+
# parameter name is a Ruby identifier (Symbol); the type
|
|
339
|
+
# is any `Rigor::Type` the refinement parser resolves
|
|
340
|
+
# (bare kebab-case name, parameterised form, or `int<...>`
|
|
341
|
+
# range — the same grammar the `return:` directive
|
|
342
|
+
# accepts).
|
|
343
|
+
ParamOverride = Data.define(:param_name, :type)
|
|
344
|
+
|
|
345
|
+
# Reads every `rigor:v1:param: <name> <refinement>`
|
|
346
|
+
# directive off `RBS::Definition::Method#annotations` and
|
|
347
|
+
# returns the resolved `ParamOverride` list. Annotations
|
|
348
|
+
# the parser cannot resolve (typo, unknown refinement, no
|
|
349
|
+
# `param:` directive at all) are silently dropped — the
|
|
350
|
+
# call site keeps the RBS-declared parameter type for
|
|
351
|
+
# those parameters. The reader accepts a nil method
|
|
352
|
+
# definition so call sites can pass through optional
|
|
353
|
+
# method lookups without a guard.
|
|
354
|
+
#
|
|
355
|
+
# Example annotation in an RBS file:
|
|
356
|
+
#
|
|
357
|
+
# class Slug
|
|
358
|
+
# %a{rigor:v1:param: id is non-empty-string}
|
|
359
|
+
# def normalise: (::String id) -> String
|
|
360
|
+
# end
|
|
361
|
+
#
|
|
362
|
+
# The RBS-declared type of `id` is `String`. The override
|
|
363
|
+
# tightens it to `non-empty-string` for argument-check
|
|
364
|
+
# purposes; passing a too-wide `Nominal[String]` argument
|
|
365
|
+
# is flagged as an argument-type mismatch at the call
|
|
366
|
+
# site.
|
|
367
|
+
def read_param_type_overrides(method_def)
|
|
368
|
+
return [] if method_def.nil?
|
|
369
|
+
|
|
370
|
+
annotations = method_def.annotations
|
|
371
|
+
return [] if annotations.nil? || annotations.empty?
|
|
372
|
+
|
|
373
|
+
annotations.filter_map { |annotation| parse_param_annotation(annotation.string) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Convenience reader for call sites that want to look up
|
|
377
|
+
# a single override by parameter name. Returns a frozen
|
|
378
|
+
# Hash<Symbol, Rigor::Type>; missing keys mean "use the
|
|
379
|
+
# RBS-declared type". Callers MUST treat the hash as
|
|
380
|
+
# read-only.
|
|
381
|
+
def param_type_override_map(method_def)
|
|
382
|
+
read_param_type_overrides(method_def).to_h { |o| [o.param_name, o.type] }.freeze
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The `is` glue word is optional so authors can write
|
|
386
|
+
# either `param: id is non-empty-string` (consistent with
|
|
387
|
+
# the existing `assert` / `predicate-if-*` directives) or
|
|
388
|
+
# the terser `param: id non-empty-string`. The trailing
|
|
389
|
+
# payload accepts the full refinement grammar in
|
|
390
|
+
# `Builtins::ImportedRefinements::Parser`.
|
|
391
|
+
PARAM_DIRECTIVE_PATTERN = /
|
|
392
|
+
\A
|
|
393
|
+
rigor:v1:param:
|
|
394
|
+
\s+
|
|
395
|
+
(?<param>[a-z_][a-zA-Z0-9_]*)
|
|
396
|
+
\s+
|
|
397
|
+
(?:is\s+)?
|
|
398
|
+
(?<payload>\S(?:.*\S)?)
|
|
399
|
+
\s*
|
|
400
|
+
\z
|
|
401
|
+
/x
|
|
402
|
+
private_constant :PARAM_DIRECTIVE_PATTERN
|
|
403
|
+
|
|
404
|
+
def parse_param_annotation(string)
|
|
405
|
+
match = PARAM_DIRECTIVE_PATTERN.match(string)
|
|
406
|
+
return nil if match.nil?
|
|
407
|
+
|
|
408
|
+
type = Builtins::ImportedRefinements.parse(match[:payload])
|
|
409
|
+
return nil if type.nil?
|
|
410
|
+
|
|
411
|
+
ParamOverride.new(param_name: match[:param].to_sym, type: type)
|
|
256
412
|
end
|
|
257
413
|
end
|
|
258
414
|
end
|