Skip to content

Commit 8d047e5

Browse files
committed
.
1 parent ba884b9 commit 8d047e5

1 file changed

Lines changed: 281 additions & 50 deletions

File tree

zjit/mini_zjit.rb

Lines changed: 281 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,79 +15,213 @@
1515
module 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

Comments
 (0)