1515module MiniZJIT
1616
1717 # ═══════════════════════════════════════════════════════════════════
18- # Type Lattice
18+ # Type Lattice — bitset-based, like real ZJIT
1919 #
20- # Any (top)
21- # ├── BasicObject
22- # │ ├── Fixnum (may carry a constant: Fixnum[3])
23- # │ ├── Float
24- # │ ├── String
25- # │ ├── Array
26- # │ ├── NilClass
27- # │ ├── TrueClass
28- # │ ├── FalseClass
29- # │ └── Object
30- # ├── CBool (internal C boolean from Test/IsNil)
31- # └── Empty (bottom — unreachable)
20+ # Each leaf type gets one bit. Composite types are bitwise OR of their
21+ # children. Subtype check is (a.bits & b.bits) == a.bits. Intersection
22+ # is bitwise AND. An optional Specialization carries constant info.
23+ #
24+ # Bit layout (leaf types):
25+ # 0: Fixnum 4: NilClass 8: Array 12: CBool
26+ # 1: Flonum 5: TrueClass 9: Hash
27+ # 2: Bignum 6: FalseClass 10: Symbol
28+ # 3: String 7: Object 11: Undef
29+ #
30+ # Composite types (bitwise OR):
31+ # Integer = Fixnum | Bignum
32+ # Float = Flonum
33+ # Numeric = Integer | Float
34+ # Immediate = Fixnum | Flonum | NilClass | TrueClass | FalseClass | Symbol
35+ # Falsy = NilClass | FalseClass
36+ # BasicObject = Fixnum | Flonum | Bignum | String | NilClass | TrueClass
37+ # | FalseClass | Object | Array | Hash | Symbol | Undef
38+ # CValue = CBool
39+ # RubyValue = BasicObject | Undef
40+ # Any = RubyValue | CValue
41+ # Empty = 0 (bottom)
3242 # ═══════════════════════════════════════════════════════════════════
3343
34- RUBY_TYPES = %i[ Fixnum Float String Array NilClass TrueClass FalseClass Object ] . freeze
44+ module Bits
45+ Fixnum = 1 << 0
46+ Flonum = 1 << 1
47+ Bignum = 1 << 2
48+ String = 1 << 3
49+ NilClass = 1 << 4
50+ TrueClass = 1 << 5
51+ FalseClass = 1 << 6
52+ Object = 1 << 7
53+ Array = 1 << 8
54+ Hash = 1 << 9
55+ Symbol = 1 << 10
56+ Undef = 1 << 11
57+ CBool = 1 << 12
58+
59+ Integer = Fixnum | Bignum
60+ Float = Flonum
61+ Numeric = Integer | Float
62+ Immediate = Fixnum | Flonum | NilClass | TrueClass | FalseClass | Symbol
63+ Falsy = NilClass | FalseClass
64+ BasicObject = Fixnum | Flonum | Bignum | String | NilClass | TrueClass |
65+ FalseClass | Object | Array | Hash | Symbol | Undef
66+ CValue = CBool
67+ RubyValue = BasicObject | Undef
68+ Any = RubyValue | CValue
69+ Empty = 0
70+
71+ # Map from single-bit leaf → display name (sorted by bit position)
72+ LEAF_NAMES = {
73+ Fixnum => "Fixnum" , Flonum => "Flonum" , Bignum => "Bignum" ,
74+ String => "String" , NilClass => "NilClass" , TrueClass => "TrueClass" ,
75+ FalseClass => "FalseClass" , Object => "Object" , Array => "Array" ,
76+ Hash => "Hash" , Symbol => "Symbol" , Undef => "Undef" , CBool => "CBool" ,
77+ } . freeze
78+
79+ # Named composite patterns for display (checked in order, most specific first)
80+ NAMED_COMPOSITES = [
81+ [ Integer , "Integer" ] , [ Float , "Float" ] , [ Numeric , "Numeric" ] ,
82+ [ Immediate , "Immediate" ] , [ Falsy , "Falsy" ] ,
83+ [ BasicObject , "BasicObject" ] , [ CValue , "CValue" ] ,
84+ [ RubyValue , "RubyValue" ] , [ Any , "Any" ] ,
85+ ] . freeze
86+ end
87+
88+ # Specialization — optional constant/object info attached to a Type.
89+ # Like real ZJIT: bits tell you the set of possible types, spec tells
90+ # you "we know exactly this value/class".
91+ module Spec
92+ NONE = :none # No specialization (like Specialization::Any)
93+ # Otherwise, the spec is the Ruby constant value itself (like Specialization::Object)
94+ end
3595
3696 class Type
37- attr_reader :name , :const_val
97+ attr_reader :bits , :spec
3898
39- def initialize ( name , const_val = :none )
40- @name = name
41- @const_val = const_val
99+ def initialize ( bits , spec = Spec :: NONE )
100+ @bits = bits
101+ @spec = spec
42102 end
43103
44- def with_const ( val ) = Type . new ( @name , val )
45- def has_const? = @const_val != :none
46- def fixnum? = @name == :Fixnum
47- def nilclass? = @name == :NilClass
48- def empty? = @name == :Empty
49- def any? = @name == :Any
50- def cbool? = @name == :CBool
51- def basic_object? = @name == :BasicObject
104+ # ── Constructors ──
105+
106+ def with_const ( val ) = Type . new ( @bits , val )
107+
108+ # ── Predicates ──
52109
110+ def has_const? = @spec != Spec ::NONE
111+ def fixnum? = ( @bits & ~Bits ::Fixnum ) == 0 && @bits != 0
112+ def nilclass? = ( @bits & ~Bits ::NilClass ) == 0 && @bits != 0
113+ def empty? = @bits == Bits ::Empty
114+ def any? = @bits == Bits ::Any
115+ def cbool? = ( @bits & ~Bits ::CBool ) == 0 && @bits != 0
116+ def basic_object? = ( @bits & ~Bits ::BasicObject ) == 0 && @bits != 0
117+ def const_val = @spec
118+
119+ # ── Lattice operations ──
120+
121+ # Subtype: self's bits are a subset of other's bits, and specs are compatible
53122 def <=( other )
54- return true if other . any?
55- return true if self . empty?
56- return true if @name == other . name
57- return true if other . name == :BasicObject && RUBY_TYPES . include? ( @name )
123+ return true if @bits == Bits ::Empty
124+ return true if ( @bits & other . bits ) == @bits && spec_compatible? ( other )
58125 false
59126 end
60127
128+ # Intersection (meet): bitwise AND of bits, keep more specific spec
61129 def &( other )
62- return self if self <= other
63- return other if other <= self
64- Types ::Empty
130+ new_bits = @bits & other . bits
131+ return Types ::Empty if new_bits == Bits ::Empty
132+ if self <= other
133+ Type . new ( new_bits , @spec )
134+ elsif other <= self
135+ Type . new ( new_bits , other . spec )
136+ else
137+ Type . new ( new_bits )
138+ end
65139 end
66140
141+ # Union (join): bitwise OR of bits, keep spec only if identical
142+ def |( other )
143+ new_bits = @bits | other . bits
144+ new_spec = ( @spec == other . spec ) ? @spec : Spec ::NONE
145+ Type . new ( new_bits , new_spec )
146+ end
147+
148+ # ── Display ──
149+
67150 def to_s
68- s = @name . to_s
69- s += "[#{ @const_val . inspect } ]" if has_const?
70- s
151+ return "Empty" if @bits == Bits ::Empty
152+
153+ # Try exact match against leaf types first (most specific)
154+ Bits ::LEAF_NAMES . each do |pattern , name |
155+ if @bits == pattern
156+ s = name
157+ s += "[#{ @spec . inspect } ]" if has_const?
158+ return s
159+ end
160+ end
161+
162+ # Try exact match against named composites
163+ Bits ::NAMED_COMPOSITES . each do |pattern , name |
164+ if @bits == pattern
165+ s = name
166+ s += "[#{ @spec . inspect } ]" if has_const?
167+ return s
168+ end
169+ end
170+
171+ # Decompose into union of smallest named parts
172+ remaining = @bits
173+ parts = [ ]
174+ Bits ::LEAF_NAMES . each do |pattern , name |
175+ if @bits == pattern
176+ s = name
177+ s += "[#{ @spec . inspect } ]" if has_const?
178+ return s
179+ end
180+ end
181+
182+ # Decompose into union of smallest named parts
183+ remaining = @bits
184+ parts = [ ]
185+ Bits ::LEAF_NAMES . each do |pattern , name |
186+ if ( remaining & pattern ) == pattern
187+ parts << name
188+ remaining &= ~pattern
189+ end
190+ end
191+ parts . join ( "|" )
71192 end
72193
73- def ==( other ) = other . is_a? ( Type ) && @name == other . name && @const_val == other . const_val
194+ def ==( other ) = other . is_a? ( Type ) && @bits == other . bits && @spec == other . spec
74195 def eql? ( other ) = self == other
75- def hash = [ @name , @const_val ] . hash
196+ def hash = [ @bits , @spec ] . hash
197+
198+ private
199+
200+ def spec_compatible? ( other )
201+ return true if other . spec == Spec ::NONE # other is unspecialized (Any)
202+ return true if @spec == Spec ::NONE && @bits == Bits ::Empty # self is Empty
203+ @spec == other . spec || @spec == Spec ::NONE
204+ end
76205 end
77206
78207 module Types
79- Any = Type . new ( :Any )
80- BasicObject = Type . new ( :BasicObject )
81- Fixnum = Type . new ( :Fixnum )
82- Float = Type . new ( :Float )
83- String = Type . new ( :String )
84- Array = Type . new ( :Array )
85- NilClass = Type . new ( :NilClass )
86- TrueClass = Type . new ( :TrueClass )
87- FalseClass = Type . new ( :FalseClass )
88- Object = Type . new ( :Object )
89- CBool = Type . new ( :CBool )
90- Empty = Type . new ( :Empty )
208+ Any = Type . new ( Bits ::Any )
209+ BasicObject = Type . new ( Bits ::BasicObject )
210+ Fixnum = Type . new ( Bits ::Fixnum )
211+ Float = Type . new ( Bits ::Float )
212+ Integer = Type . new ( Bits ::Integer )
213+ Numeric = Type . new ( Bits ::Numeric )
214+ String = Type . new ( Bits ::String )
215+ Array = Type . new ( Bits ::Array )
216+ Hash = Type . new ( Bits ::Hash )
217+ Symbol = Type . new ( Bits ::Symbol )
218+ NilClass = Type . new ( Bits ::NilClass )
219+ TrueClass = Type . new ( Bits ::TrueClass )
220+ FalseClass = Type . new ( Bits ::FalseClass )
221+ Falsy = Type . new ( Bits ::Falsy )
222+ Object = Type . new ( Bits ::Object )
223+ CBool = Type . new ( Bits ::CBool )
224+ Empty = Type . new ( Bits ::Empty )
91225 end
92226
93227 # ═══════════════════════════════════════════════════════════════════
@@ -1108,6 +1242,8 @@ def self.hir(code, optimize: true)
11081242 class TypeTest < Minitest ::Test
11091243 include MiniZJIT
11101244
1245+ # ── Bitset subtype checks ──
1246+
11111247 def test_subtype_fixnum_under_basic_object
11121248 assert Types ::Fixnum <= Types ::BasicObject
11131249 end
@@ -1125,17 +1261,112 @@ def test_basic_object_not_subtype_of_fixnum
11251261 refute Types ::BasicObject <= Types ::Fixnum
11261262 end
11271263
1264+ def test_fixnum_subtype_of_integer
1265+ assert Types ::Fixnum <= Types ::Integer
1266+ end
1267+
1268+ def test_integer_not_subtype_of_fixnum
1269+ refute Types ::Integer <= Types ::Fixnum
1270+ end
1271+
1272+ def test_integer_subtype_of_numeric
1273+ assert Types ::Integer <= Types ::Numeric
1274+ end
1275+
1276+ def test_cbool_subtype_of_any
1277+ assert Types ::CBool <= Types ::Any
1278+ end
1279+
1280+ def test_cbool_not_subtype_of_basic_object
1281+ refute Types ::CBool <= Types ::BasicObject
1282+ end
1283+
1284+ def test_nilclass_subtype_of_falsy
1285+ assert Types ::NilClass <= Types ::Falsy
1286+ end
1287+
1288+ def test_falseclass_subtype_of_falsy
1289+ assert Types ::FalseClass <= Types ::Falsy
1290+ end
1291+
1292+ def test_fixnum_not_subtype_of_falsy
1293+ refute Types ::Fixnum <= Types ::Falsy
1294+ end
1295+
1296+ # ── Intersection (meet) via bitwise AND ──
1297+
11281298 def test_meet_narrows
11291299 assert_equal Types ::Fixnum , ( Types ::Fixnum & Types ::BasicObject )
11301300 end
11311301
1302+ def test_meet_integer_and_fixnum
1303+ assert_equal Types ::Fixnum , ( Types ::Integer & Types ::Fixnum )
1304+ end
1305+
1306+ def test_meet_disjoint_is_empty
1307+ assert_equal Types ::Empty , ( Types ::Fixnum & Types ::String )
1308+ end
1309+
1310+ def test_meet_cbool_and_basic_object_is_empty
1311+ assert_equal Types ::Empty , ( Types ::CBool & Types ::BasicObject )
1312+ end
1313+
1314+ # ── Union (join) via bitwise OR ──
1315+
1316+ def test_union_fixnum_and_nilclass
1317+ union = Types ::Fixnum | Types ::NilClass
1318+ assert Types ::Fixnum <= union
1319+ assert Types ::NilClass <= union
1320+ refute Types ::String <= union
1321+ end
1322+
1323+ def test_union_preserves_const_if_same
1324+ a = Types ::Fixnum . with_const ( 1 )
1325+ b = Types ::Fixnum . with_const ( 1 )
1326+ assert ( a | b ) . has_const?
1327+ end
1328+
1329+ def test_union_drops_const_if_different
1330+ a = Types ::Fixnum . with_const ( 1 )
1331+ b = Types ::Fixnum . with_const ( 2 )
1332+ refute ( a | b ) . has_const?
1333+ end
1334+
1335+ # ── Specialization (constant info) ──
1336+
11321337 def test_type_display_with_const
11331338 assert_equal "Fixnum[42]" , Types ::Fixnum . with_const ( 42 ) . to_s
11341339 end
11351340
11361341 def test_type_display_without_const
11371342 assert_equal "Fixnum" , Types ::Fixnum . to_s
11381343 end
1344+
1345+ def test_const_type_subtype_of_unspecialized
1346+ assert Types ::Fixnum . with_const ( 42 ) <= Types ::Fixnum
1347+ end
1348+
1349+ def test_unspecialized_subtype_of_const
1350+ # Fixnum (unspecialized) <= Fixnum[42] is true because bits match
1351+ # and spec_compatible allows NONE <= specific
1352+ assert Types ::Fixnum <= Types ::Fixnum . with_const ( 42 )
1353+ end
1354+
1355+ # ── Display ──
1356+
1357+ def test_display_composite_types
1358+ assert_equal "BasicObject" , Types ::BasicObject . to_s
1359+ assert_equal "Any" , Types ::Any . to_s
1360+ assert_equal "Empty" , Types ::Empty . to_s
1361+ assert_equal "Integer" , Types ::Integer . to_s
1362+ assert_equal "Falsy" , Types ::Falsy . to_s
1363+ end
1364+
1365+ def test_display_union_decomposition
1366+ # A non-named union shows as pipe-separated leaves
1367+ union = Types ::Fixnum | Types ::String
1368+ assert_equal "Fixnum|String" , union . to_s
1369+ end
11391370 end
11401371
11411372 class EffectsTest < Minitest ::Test
0 commit comments