rigortype 0.1.3 → 0.1.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 +154 -33
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +26 -6
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +498 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +364 -55
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +39 -6
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +61 -3
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +109 -6
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +77 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
- data/lib/rigor/inference/method_dispatcher.rb +274 -5
- data/lib/rigor/inference/method_parameter_binder.rb +22 -14
- data/lib/rigor/inference/narrowing.rb +129 -12
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +5 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +102 -10
- data/lib/rigor/plugin/registry.rb +43 -2
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +2 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +207 -3
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +68 -0
- data/lib/rigor/type_node/identifier.rb +38 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +32 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +8 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +16 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- data/sig/rigor.rbs +35 -2
- metadata +90 -1
|
@@ -13,6 +13,7 @@ require_relative "union"
|
|
|
13
13
|
require_relative "difference"
|
|
14
14
|
require_relative "refined"
|
|
15
15
|
require_relative "intersection"
|
|
16
|
+
require_relative "bound_method"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
module Type
|
|
@@ -35,8 +36,14 @@ module Rigor
|
|
|
35
36
|
Bot.instance
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# ADR-15 Phase 4b.x — read the eagerly-allocated
|
|
40
|
+
# `@untyped` ivar instead of `||=`. The singleton-class
|
|
41
|
+
# `@untyped = Dynamic.new(top)` initializer runs at module
|
|
42
|
+
# body (below) on the main Ractor at load time. Workers
|
|
43
|
+
# READ the populated ivar without performing the lazy
|
|
44
|
+
# write that non-main Ractors are forbidden from doing.
|
|
38
45
|
def untyped
|
|
39
|
-
@untyped
|
|
46
|
+
@untyped
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
# Wraps the static facet in a Dynamic[T] carrier. Idempotent on the
|
|
@@ -69,6 +76,14 @@ module Rigor
|
|
|
69
76
|
Constant.new(value)
|
|
70
77
|
end
|
|
71
78
|
|
|
79
|
+
# `Object#method(:name)` carrier. Stores the bound
|
|
80
|
+
# `(receiver, method_name)` pair so the dispatcher can
|
|
81
|
+
# substitute the original dispatch at `.call` / `.()` /
|
|
82
|
+
# `[]` time. See {Type::BoundMethod}.
|
|
83
|
+
def bound_method_of(receiver_type, method_name)
|
|
84
|
+
BoundMethod.new(receiver_type: receiver_type, method_name: method_name)
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
# Bounded-integer carrier. Each bound is either an `Integer` or
|
|
73
88
|
# one of `:neg_infinity` / `:pos_infinity` (sentinels exposed as
|
|
74
89
|
# `IntegerRange::NEG_INFINITY` / `POS_INFINITY`).
|
|
@@ -347,7 +362,6 @@ module Rigor
|
|
|
347
362
|
INT_MASK_UNION_LIMIT = 16
|
|
348
363
|
private_constant :INT_MASK_FLAG_LIMIT, :INT_MASK_UNION_LIMIT
|
|
349
364
|
|
|
350
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
351
365
|
def int_mask(flags)
|
|
352
366
|
return nil unless flags.is_a?(Array) && flags.all?(Integer)
|
|
353
367
|
return nil if flags.any?(&:negative?)
|
|
@@ -362,7 +376,6 @@ module Rigor
|
|
|
362
376
|
integer_range(values.min, values.max)
|
|
363
377
|
end
|
|
364
378
|
end
|
|
365
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
366
379
|
|
|
367
380
|
# `int_mask_of[T]` — derives the int_mask closure from
|
|
368
381
|
# a finite integer-literal type:
|
|
@@ -401,6 +414,99 @@ module Rigor
|
|
|
401
414
|
end
|
|
402
415
|
end
|
|
403
416
|
|
|
417
|
+
# `pick_of[T, K]` shape-projection — keeps only the entries
|
|
418
|
+
# of `T` whose key is in the literal-key set extracted from
|
|
419
|
+
# `K`. ADR-13 § "Shape projection / Restriction and removal".
|
|
420
|
+
#
|
|
421
|
+
# Phase A handles `Type::HashShape` (literal-key K).
|
|
422
|
+
# Phase B (slice 5) extends to `Type::Tuple` (integer-index
|
|
423
|
+
# K) — `pick_of[Tuple[A, B, C], 0 | 2]` evaluates to
|
|
424
|
+
# `Tuple[A, C]`. Non-shape inputs (`Type::Nominal`, etc.)
|
|
425
|
+
# return `type` unchanged ("lossy degradation"; the
|
|
426
|
+
# `dynamic.shape.lossy-projection` diagnostic that flags
|
|
427
|
+
# the boundary lands when caller-side diagnostic threading
|
|
428
|
+
# arrives).
|
|
429
|
+
def pick_of(type, keys)
|
|
430
|
+
case type
|
|
431
|
+
when HashShape then hash_shape_pick(type, keys)
|
|
432
|
+
when Tuple then tuple_pick(type, keys)
|
|
433
|
+
else type
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# `omit_of[T, K]` shape-projection — dual of {pick_of}.
|
|
438
|
+
# Drops the entries / positions whose key (or index, for a
|
|
439
|
+
# `Tuple`) is in the literal-key set extracted from `K`.
|
|
440
|
+
def omit_of(type, keys)
|
|
441
|
+
case type
|
|
442
|
+
when HashShape then hash_shape_omit(type, keys)
|
|
443
|
+
when Tuple then tuple_omit(type, keys)
|
|
444
|
+
else type
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# `partial_of[T]` shape-projection — flips every required
|
|
449
|
+
# entry of `T` to optional. ADR-13 § "Required-ness flips".
|
|
450
|
+
# Does NOT add `nil` to value types — Rigor's HashShape
|
|
451
|
+
# distinguishes "key absent" from "key present with nil
|
|
452
|
+
# value", so flipping required-ness is sufficient.
|
|
453
|
+
def partial_of(type)
|
|
454
|
+
return type unless type.is_a?(HashShape)
|
|
455
|
+
|
|
456
|
+
HashShape.new(
|
|
457
|
+
type.pairs,
|
|
458
|
+
required_keys: [],
|
|
459
|
+
optional_keys: type.pairs.keys,
|
|
460
|
+
read_only_keys: type.read_only_keys,
|
|
461
|
+
extra_keys: type.extra_keys
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# `required_of[T]` shape-projection — inverse of
|
|
466
|
+
# {partial_of}; flips every optional entry to required.
|
|
467
|
+
def required_of(type)
|
|
468
|
+
return type unless type.is_a?(HashShape)
|
|
469
|
+
|
|
470
|
+
HashShape.new(
|
|
471
|
+
type.pairs,
|
|
472
|
+
required_keys: type.pairs.keys,
|
|
473
|
+
optional_keys: [],
|
|
474
|
+
read_only_keys: type.read_only_keys,
|
|
475
|
+
extra_keys: type.extra_keys
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# `readonly_of[T]` shape-projection — marks every entry of
|
|
480
|
+
# `T` as read-only in the current view. View-level only —
|
|
481
|
+
# does NOT prove the underlying Ruby Hash is frozen.
|
|
482
|
+
def readonly_of(type)
|
|
483
|
+
return type unless type.is_a?(HashShape)
|
|
484
|
+
|
|
485
|
+
HashShape.new(
|
|
486
|
+
type.pairs,
|
|
487
|
+
required_keys: type.required_keys,
|
|
488
|
+
optional_keys: type.optional_keys,
|
|
489
|
+
read_only_keys: type.pairs.keys,
|
|
490
|
+
extra_keys: type.extra_keys
|
|
491
|
+
)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Predicate that a shape-projection (`pick_of`, `omit_of`,
|
|
495
|
+
# `partial_of`, `required_of`, `readonly_of`) would degrade
|
|
496
|
+
# to "input unchanged" on this carrier. Callers consult
|
|
497
|
+
# this BEFORE invoking the projection so they can emit a
|
|
498
|
+
# `dynamic.shape.lossy-projection` diagnostic at the site
|
|
499
|
+
# where the projection was authored.
|
|
500
|
+
#
|
|
501
|
+
# `HashShape` and `Tuple` carry shape-level information
|
|
502
|
+
# the projections honour; every other carrier is lossy.
|
|
503
|
+
# Slice 5b wires diagnostic emission through `RbsExtended`
|
|
504
|
+
# / parser callers; this predicate stands alone in slice 5
|
|
505
|
+
# for unit-test coverage and future composition.
|
|
506
|
+
def shape_projection_lossy?(type)
|
|
507
|
+
!type.is_a?(HashShape) && !type.is_a?(Tuple)
|
|
508
|
+
end
|
|
509
|
+
|
|
404
510
|
class << self # rubocop:disable Metrics/ClassLength
|
|
405
511
|
private
|
|
406
512
|
|
|
@@ -480,6 +586,99 @@ module Rigor
|
|
|
480
586
|
end
|
|
481
587
|
end
|
|
482
588
|
|
|
589
|
+
# Literal-key set extraction for {pick_of} / {omit_of}.
|
|
590
|
+
# Accepts `Constant<Symbol|String>` or `Union[Constant…]`
|
|
591
|
+
# where every member is such a Constant. Returns `nil`
|
|
592
|
+
# when the shape can't be reduced to a finite key set
|
|
593
|
+
# (untyped, Top, Difference, Refined, mixed-kind union,
|
|
594
|
+
# etc.) — callers degrade to "input unchanged" per
|
|
595
|
+
# ADR-13's lossy-projection rule.
|
|
596
|
+
def extract_constant_key_set(type)
|
|
597
|
+
case type
|
|
598
|
+
when Constant then constant_key_set(type)
|
|
599
|
+
when Union then union_key_set(type)
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def constant_key_set(type)
|
|
604
|
+
literal_key?(type.value) ? [type.value] : nil
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def union_key_set(type)
|
|
608
|
+
return nil unless type.members.all?(Constant)
|
|
609
|
+
|
|
610
|
+
values = type.members.map(&:value)
|
|
611
|
+
values.all? { |v| literal_key?(v) } ? values : nil
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def literal_key?(value)
|
|
615
|
+
value.is_a?(Symbol) || value.is_a?(String)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Rebuild a {HashShape} from the subset of `keys` the
|
|
619
|
+
# caller decided to keep. Preserves required / optional /
|
|
620
|
+
# read-only classification AND the extra-keys policy of
|
|
621
|
+
# the source shape; entries dropped from `pairs` also
|
|
622
|
+
# drop from each policy list. Used by both {pick_of}
|
|
623
|
+
# (intersection with K) and {omit_of} (set difference).
|
|
624
|
+
def rebuild_hash_shape_with_keys(shape, kept_keys)
|
|
625
|
+
HashShape.new(
|
|
626
|
+
shape.pairs.slice(*kept_keys),
|
|
627
|
+
required_keys: shape.required_keys.select { |k| kept_keys.include?(k) },
|
|
628
|
+
optional_keys: shape.optional_keys.select { |k| kept_keys.include?(k) },
|
|
629
|
+
read_only_keys: shape.read_only_keys.select { |k| kept_keys.include?(k) },
|
|
630
|
+
extra_keys: shape.extra_keys
|
|
631
|
+
)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def hash_shape_pick(type, keys)
|
|
635
|
+
key_set = extract_constant_key_set(keys)
|
|
636
|
+
return type if key_set.nil?
|
|
637
|
+
|
|
638
|
+
rebuild_hash_shape_with_keys(type, type.pairs.keys & key_set)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def hash_shape_omit(type, keys)
|
|
642
|
+
key_set = extract_constant_key_set(keys)
|
|
643
|
+
return type if key_set.nil?
|
|
644
|
+
|
|
645
|
+
rebuild_hash_shape_with_keys(type, type.pairs.keys - key_set)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# ADR-13 slice 5 — Tuple support. `K` MUST be a
|
|
649
|
+
# `Constant<Integer>` or `Union[Constant<Integer>, …]`;
|
|
650
|
+
# other K shapes (or non-integer Constants in a Union)
|
|
651
|
+
# return the input unchanged. Negative or out-of-range
|
|
652
|
+
# indices are dropped silently per slice 5's permissive
|
|
653
|
+
# take — surface diagnostics are slice 5b material.
|
|
654
|
+
def tuple_pick(type, keys)
|
|
655
|
+
index_set = extract_tuple_index_set(keys, type.elements.size)
|
|
656
|
+
return type if index_set.nil?
|
|
657
|
+
|
|
658
|
+
Tuple.new(index_set.map { |i| type.elements[i] })
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def tuple_omit(type, keys)
|
|
662
|
+
index_set = extract_tuple_index_set(keys, type.elements.size)
|
|
663
|
+
return type if index_set.nil?
|
|
664
|
+
|
|
665
|
+
dropped = index_set.to_a
|
|
666
|
+
Tuple.new(type.elements.each_with_index.reject { |_, i| dropped.include?(i) }.map(&:first))
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Extracts a sorted, deduplicated set of in-range integer
|
|
670
|
+
# indices from a `Constant<Integer>` / `Union[Constant<Integer>, …]`
|
|
671
|
+
# carrier. Out-of-range indices are dropped silently; the
|
|
672
|
+
# caller decides whether an empty result still means
|
|
673
|
+
# "lossy projection" (current pick / omit just produce an
|
|
674
|
+
# empty Tuple).
|
|
675
|
+
def extract_tuple_index_set(type, size)
|
|
676
|
+
flags = extract_constant_int_set(type)
|
|
677
|
+
return nil if flags.nil?
|
|
678
|
+
|
|
679
|
+
flags.uniq.select { |i| i >= 0 && i < size }.sort
|
|
680
|
+
end
|
|
681
|
+
|
|
483
682
|
def tuple_indexed_access(tuple, key)
|
|
484
683
|
return top unless key.is_a?(Constant) && key.value.is_a?(Integer)
|
|
485
684
|
|
|
@@ -616,6 +815,11 @@ module Rigor
|
|
|
616
815
|
members.sort_by { |m| m.describe(:short) }
|
|
617
816
|
end
|
|
618
817
|
end
|
|
818
|
+
|
|
819
|
+
# ADR-15 Phase 4b.x — eager-allocate the singleton
|
|
820
|
+
# `Dynamic[Top]` carrier on the main Ractor at load time.
|
|
821
|
+
# The `untyped` reader above just returns this ivar.
|
|
822
|
+
@untyped = Dynamic.new(Top.instance)
|
|
619
823
|
end
|
|
620
824
|
end
|
|
621
825
|
end
|
data/lib/rigor/type/constant.rb
CHANGED
|
@@ -50,11 +50,24 @@ module Rigor
|
|
|
50
50
|
value.inspect
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# RBS supports `Literal` types for booleans, nil, integer
|
|
54
|
+
# literals (positive and negative), symbol literals, and
|
|
55
|
+
# string literals. Erasing to these preserves the
|
|
56
|
+
# carrier's precision at the RBS boundary — `Constant<64>`
|
|
57
|
+
# round-trips as `64`, not as `Integer` — and
|
|
58
|
+
# `RbsTypeTranslator#translate_literal` already maps the
|
|
59
|
+
# parsed RBS Literal back to `Constant`. Scalar carriers
|
|
60
|
+
# without RBS Literal support (Float, Range, Rational,
|
|
61
|
+
# Complex, Regexp, Pathname) keep their pre-existing
|
|
62
|
+
# widen-to-class-name behaviour because RBS rejects their
|
|
63
|
+
# literal spellings as syntax errors.
|
|
53
64
|
def erase_to_rbs
|
|
54
65
|
case value
|
|
55
66
|
when true then "true"
|
|
56
67
|
when false then "false"
|
|
57
68
|
when nil then "nil"
|
|
69
|
+
when Integer then value.to_s
|
|
70
|
+
when Symbol, String then value.inspect
|
|
58
71
|
else value.class.name
|
|
59
72
|
end
|
|
60
73
|
end
|
|
@@ -22,7 +22,6 @@ module Rigor
|
|
|
22
22
|
#
|
|
23
23
|
# See docs/type-specification/rbs-compatible-types.md (records) and
|
|
24
24
|
# docs/type-specification/rigor-extensions.md (hash shape).
|
|
25
|
-
# rubocop:disable Metrics/ClassLength
|
|
26
25
|
class HashShape
|
|
27
26
|
ALLOWED_KEY_CLASSES = [Symbol, String].freeze
|
|
28
27
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
@@ -241,6 +240,5 @@ module Rigor
|
|
|
241
240
|
Type::Combinator.union(*key_types)
|
|
242
241
|
end
|
|
243
242
|
end
|
|
244
|
-
# rubocop:enable Metrics/ClassLength
|
|
245
243
|
end
|
|
246
244
|
end
|
|
@@ -78,13 +78,13 @@ module Rigor
|
|
|
78
78
|
Float::INFINITY
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
ALIAS_NAMES = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
ALIAS_NAMES = Ractor.make_shareable({
|
|
82
|
+
[NEG_INFINITY, POS_INFINITY] => "int",
|
|
83
|
+
[1, POS_INFINITY] => "positive-int",
|
|
84
|
+
[0, POS_INFINITY] => "non-negative-int",
|
|
85
|
+
[NEG_INFINITY, -1] => "negative-int",
|
|
86
|
+
[NEG_INFINITY, 0] => "non-positive-int"
|
|
87
|
+
})
|
|
88
88
|
|
|
89
89
|
def describe(_verbosity = :short)
|
|
90
90
|
ALIAS_NAMES[[min, max]] || generic_description
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -165,18 +165,24 @@ module Rigor
|
|
|
165
165
|
# kebab-case canonical name. Registered shapes print
|
|
166
166
|
# through `describe`; unregistered combinations fall back
|
|
167
167
|
# to the operator form.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
168
|
+
#
|
|
169
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
|
|
170
|
+
# because the keys are nested two-element Arrays. Plain
|
|
171
|
+
# `.freeze` would leave the inner arrays mutable, so a
|
|
172
|
+
# worker Ractor reading `CANONICAL_NAMES[[base, predicate]]`
|
|
173
|
+
# would trip `Ractor::IsolationError`.
|
|
174
|
+
CANONICAL_NAMES = Ractor.make_shareable({
|
|
175
|
+
["String", :lowercase] => "lowercase-string",
|
|
176
|
+
["String", :not_lowercase] => "non-lowercase-string",
|
|
177
|
+
["String", :uppercase] => "uppercase-string",
|
|
178
|
+
["String", :not_uppercase] => "non-uppercase-string",
|
|
179
|
+
["String", :numeric] => "numeric-string",
|
|
180
|
+
["String", :not_numeric] => "non-numeric-string",
|
|
181
|
+
["String", :decimal_int] => "decimal-int-string",
|
|
182
|
+
["String", :octal_int] => "octal-int-string",
|
|
183
|
+
["String", :hex_int] => "hex-int-string",
|
|
184
|
+
["String", :literal_string] => "literal-string"
|
|
185
|
+
})
|
|
180
186
|
private_constant :CANONICAL_NAMES
|
|
181
187
|
|
|
182
188
|
# Bidirectional `predicate_id ↔ complement_predicate_id`
|
data/lib/rigor/type/top.rb
CHANGED
|
@@ -7,10 +7,11 @@ module Rigor
|
|
|
7
7
|
# The top of the value lattice: contains every value, including untyped
|
|
8
8
|
# boundaries. See docs/type-specification/special-types.md.
|
|
9
9
|
class Top
|
|
10
|
+
# ADR-15 Phase 4b.x — eager singleton (see Bot.rb).
|
|
11
|
+
@instance = new.freeze
|
|
12
|
+
|
|
10
13
|
class << self
|
|
11
|
-
|
|
12
|
-
@instance ||= new.freeze
|
|
13
|
-
end
|
|
14
|
+
attr_reader :instance
|
|
14
15
|
|
|
15
16
|
private :new
|
|
16
17
|
end
|
data/lib/rigor/type/union.rb
CHANGED
|
@@ -28,8 +28,27 @@ module Rigor
|
|
|
28
28
|
members.map { |m| m.describe(verbosity) }.join(" | ")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# ADR-1 § "RBS round-trip is lossless" + the value-lattice
|
|
32
|
+
# rule `untyped | T = untyped` (every `T` is gradually
|
|
33
|
+
# consistent with `untyped`). When any union member erases
|
|
34
|
+
# to `"untyped"`, the whole union erases to `"untyped"` —
|
|
35
|
+
# the RBS surface has no carrier for "Dynamic-origin
|
|
36
|
+
# alongside a static facet", and the gradual-consistency
|
|
37
|
+
# contract guarantees the substitution is sound at every
|
|
38
|
+
# call site.
|
|
39
|
+
#
|
|
40
|
+
# Post-erasure dedupe removes `String | String` artefacts
|
|
41
|
+
# that arise when two structurally-distinct `Constant`
|
|
42
|
+
# carriers (e.g. `Constant<"Alice">` / `Constant<"Bob">`)
|
|
43
|
+
# share an RBS-erased envelope. The members themselves
|
|
44
|
+
# are already structurally deduped at construction by
|
|
45
|
+
# `Type::Combinator.union`, but the post-erase strings
|
|
46
|
+
# can collide.
|
|
31
47
|
def erase_to_rbs
|
|
32
|
-
members.map(&:erase_to_rbs)
|
|
48
|
+
erased = members.map(&:erase_to_rbs)
|
|
49
|
+
return "untyped" if erased.include?("untyped")
|
|
50
|
+
|
|
51
|
+
erased.uniq.join(" | ")
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
def top
|
data/lib/rigor/type.rb
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# A parameterised named-type reference (`Pick<T, K>`,
|
|
6
|
+
# `non-empty-array[Integer]`, `pick_of[T, "name" | "email"]`,
|
|
7
|
+
# …) in an RBS::Extended payload. The `head` is the parser-
|
|
8
|
+
# observed name (no bracket type); `args` is the ordered
|
|
9
|
+
# sequence of type-argument nodes already produced by the
|
|
10
|
+
# parser at one level of depth.
|
|
11
|
+
#
|
|
12
|
+
# Args are themselves {TypeNode::Identifier} or
|
|
13
|
+
# {TypeNode::Generic}. Nested generics ride the same shape:
|
|
14
|
+
# `Pick<Address, "name" | "surname">` reaches the resolver as
|
|
15
|
+
# `Generic("Pick", [Identifier("Address"), Generic("Union", [...])])`
|
|
16
|
+
# — actually the union spelling depends on the parser's
|
|
17
|
+
# eventual convention (slice 3 pins it); for now the field
|
|
18
|
+
# set is the only public commitment.
|
|
19
|
+
#
|
|
20
|
+
# The carrier is intentionally permissive about `args.size`.
|
|
21
|
+
# The grammar-level rule "no brackets ⇒ Identifier; brackets ⇒
|
|
22
|
+
# Generic" lives on the parser side; nothing here forbids a
|
|
23
|
+
# zero-arg Generic so plugins can synthesise nodes for
|
|
24
|
+
# diagnostic or testing purposes without the parser fighting
|
|
25
|
+
# back.
|
|
26
|
+
class Generic < Data.define(:head, :args)
|
|
27
|
+
def initialize(head:, args:)
|
|
28
|
+
unless head.is_a?(String) && !head.empty?
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"TypeNode::Generic head must be a non-empty String, " \
|
|
31
|
+
"got #{head.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless args.is_a?(Array) && args.all? { |a| valid_arg?(a) }
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"TypeNode::Generic args must be an Array of " \
|
|
37
|
+
"TypeNode::Identifier / TypeNode::Generic / " \
|
|
38
|
+
"TypeNode::IntegerLiteral, got #{args.inspect}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Freeze the String head + Array args so the Data
|
|
42
|
+
# object is `Ractor.shareable?`. Each `a` is already a
|
|
43
|
+
# shareable TypeNode value object (checked above), so
|
|
44
|
+
# freezing the wrapping Array is sufficient.
|
|
45
|
+
frozen_head = head.frozen? ? head : head.dup.freeze
|
|
46
|
+
frozen_args = args.frozen? ? args : args.dup.freeze
|
|
47
|
+
super(head: frozen_head, args: frozen_args)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# ADR-13 slice 3 expanded the accepted set to include
|
|
53
|
+
# {IntegerLiteral} so the parser can emit a uniform AST for
|
|
54
|
+
# `int<5, 10>` (angle bounds) and `int_mask[1, 2, 4]`
|
|
55
|
+
# (square-bracketed bitflag union). The follow-up further
|
|
56
|
+
# admits {SymbolLiteral} / {StringLiteral} / {IndexedAccess}
|
|
57
|
+
# / {Union} so `Pick[T, :a | "b"]` carries through to the
|
|
58
|
+
# resolver as a uniform AST. Slice 1 originally accepted
|
|
59
|
+
# only `Identifier` / `Generic`; every later addition stays
|
|
60
|
+
# additive — every slice-1-shape Generic remains valid.
|
|
61
|
+
def valid_arg?(arg)
|
|
62
|
+
arg.is_a?(Identifier) || arg.is_a?(Generic) || arg.is_a?(IntegerLiteral) ||
|
|
63
|
+
arg.is_a?(SymbolLiteral) || arg.is_a?(StringLiteral) ||
|
|
64
|
+
arg.is_a?(IndexedAccess) || arg.is_a?(Union)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# A bare named-type reference in an RBS::Extended payload. The
|
|
6
|
+
# `name` is the head as the parser saw it — kebab-case for
|
|
7
|
+
# built-in refinement names (`"non-empty-string"`),
|
|
8
|
+
# PascalCase for class-like names (`"String"`, `"Pick"`),
|
|
9
|
+
# `lower_snake` for type-function-shaped names without
|
|
10
|
+
# arguments (rare).
|
|
11
|
+
#
|
|
12
|
+
# The resolver dispatch path treats an `Identifier` as the
|
|
13
|
+
# no-arg form: if a plugin recognises `Pick` as a TS-utility
|
|
14
|
+
# name, it MAY still return `Dynamic[top]` for the bare
|
|
15
|
+
# `Identifier("Pick")` since TypeScript's `Pick` is only
|
|
16
|
+
# meaningful with two type arguments. The `Generic` carrier
|
|
17
|
+
# is what plugin resolvers normally key on.
|
|
18
|
+
class Identifier < Data.define(:name)
|
|
19
|
+
def initialize(name:)
|
|
20
|
+
unless name.is_a?(String) && !name.empty?
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"TypeNode::Identifier name must be a non-empty String, " \
|
|
23
|
+
"got #{name.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Freeze the String field so the resulting Data object
|
|
27
|
+
# is `Ractor.shareable?` regardless of whether the
|
|
28
|
+
# caller passed a `# frozen_string_literal: true`
|
|
29
|
+
# constant or a dynamically built String. The same
|
|
30
|
+
# discipline applies to every other TypeNode value
|
|
31
|
+
# object — they live in the parser's hot path and are
|
|
32
|
+
# the natural carriers to flow through future Ractor
|
|
33
|
+
# boundaries (see CURRENT_WORK Open Items #8).
|
|
34
|
+
super(name: name.frozen? ? name : name.dup.freeze)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# AST wrapper for the trailing `T[K]` indexed-access projection
|
|
6
|
+
# chain. The parser emits a left-associative chain by wrapping
|
|
7
|
+
# the receiver AST in successive `IndexedAccess` nodes
|
|
8
|
+
# (`Tuple[A, B][1][0]` parses to
|
|
9
|
+
# `IndexedAccess(IndexedAccess(Generic(Tuple, [A, B]), 1), 0)`).
|
|
10
|
+
#
|
|
11
|
+
# `receiver` and `key` are themselves any AST node — the
|
|
12
|
+
# indexed-access chain is applied at resolution time, after the
|
|
13
|
+
# receiver has been resolved to a {Rigor::Type} carrier and the
|
|
14
|
+
# key has been resolved (typically to a `Constant<Integer>` or
|
|
15
|
+
# a constant String/Symbol singleton).
|
|
16
|
+
class IndexedAccess < Data.define(:receiver, :key)
|
|
17
|
+
def initialize(receiver:, key:)
|
|
18
|
+
unless valid_node?(receiver)
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"TypeNode::IndexedAccess receiver must be a TypeNode " \
|
|
21
|
+
"node, got #{receiver.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless valid_node?(key)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"TypeNode::IndexedAccess key must be a TypeNode " \
|
|
27
|
+
"node, got #{key.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def valid_node?(node)
|
|
36
|
+
node.is_a?(Identifier) || node.is_a?(Generic) ||
|
|
37
|
+
node.is_a?(IntegerLiteral) || node.is_a?(IndexedAccess)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Integer-literal AST node. Used as a {Generic#args} entry for
|
|
6
|
+
# parametric forms whose arguments are bare integers — namely
|
|
7
|
+
# `int<5, 10>` (angle-bracketed integer bounds for
|
|
8
|
+
# {Type::IntegerRange}) and `int_mask[1, 2, 4]` (square-
|
|
9
|
+
# bracketed bitflag union for {Type::Combinator.int_mask}).
|
|
10
|
+
#
|
|
11
|
+
# ADR-13 slice 3 introduces this node so the parser can emit a
|
|
12
|
+
# uniform AST regardless of bracket flavour: the resolver pass
|
|
13
|
+
# then dispatches to the appropriate built-in builder by head
|
|
14
|
+
# name. Plugin resolvers receive the same shape and MAY treat
|
|
15
|
+
# integer literals as input to custom carriers (e.g. an
|
|
16
|
+
# opinionated `port_number<8000>` plugin).
|
|
17
|
+
class IntegerLiteral < Data.define(:value)
|
|
18
|
+
def initialize(value:)
|
|
19
|
+
unless value.is_a?(Integer)
|
|
20
|
+
raise ArgumentError,
|
|
21
|
+
"TypeNode::IntegerLiteral value must be an Integer, " \
|
|
22
|
+
"got #{value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Companion context handed to every {Rigor::Plugin::TypeNodeResolver}
|
|
6
|
+
# invocation. ADR-13 § "`Plugin::TypeNodeResolver` shape".
|
|
7
|
+
#
|
|
8
|
+
# Three slots:
|
|
9
|
+
#
|
|
10
|
+
# - `resolver`: re-entry point so a plugin can recursively
|
|
11
|
+
# resolve its own arguments. Any object responding to
|
|
12
|
+
# `#resolve(node, scope)`. Slice 3 uses {ResolverChain} as
|
|
13
|
+
# the concrete implementation; tests may pass a stub that
|
|
14
|
+
# answers `resolve` directly.
|
|
15
|
+
# - `class_context`: the surrounding class / module name, if
|
|
16
|
+
# any (`String` or `nil`). Plugins use this to resolve
|
|
17
|
+
# `self`-relative type references or to scope nominal-name
|
|
18
|
+
# lookups.
|
|
19
|
+
# - `type_alias_table`: a frozen read-only view of the
|
|
20
|
+
# project's RBS type aliases for forward references. Slice
|
|
21
|
+
# 3 lands the slot with a default empty Hash; the
|
|
22
|
+
# populated table is wired from {Rigor::Environment} in a
|
|
23
|
+
# later slice once plugin authors ask for it.
|
|
24
|
+
class NameScope < Data.define(:resolver, :class_context, :type_alias_table)
|
|
25
|
+
def initialize(resolver:, class_context: nil, type_alias_table: {})
|
|
26
|
+
unless resolver.respond_to?(:resolve)
|
|
27
|
+
raise ArgumentError,
|
|
28
|
+
"TypeNode::NameScope resolver must respond to #resolve(node, scope), " \
|
|
29
|
+
"got #{resolver.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unless class_context.nil? || class_context.is_a?(String)
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"TypeNode::NameScope class_context must be nil or a String, " \
|
|
35
|
+
"got #{class_context.inspect}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless type_alias_table.is_a?(Hash)
|
|
39
|
+
raise ArgumentError,
|
|
40
|
+
"TypeNode::NameScope type_alias_table must be a Hash, " \
|
|
41
|
+
"got #{type_alias_table.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
super(
|
|
45
|
+
resolver: resolver,
|
|
46
|
+
class_context: class_context.nil? ? nil : class_context.dup.freeze,
|
|
47
|
+
type_alias_table: type_alias_table.dup.freeze
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|