Skip to content

Commit a13e9a6

Browse files
committed
.
1 parent 2260252 commit a13e9a6

1 file changed

Lines changed: 307 additions & 0 deletions

File tree

zjit/mini_zjit.rb

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ def to_s = "v#{@id}"
279279
# Override in subclasses
280280
def operands = []
281281
def effects = Effects.new(Eff::Empty, Eff::Empty)
282+
283+
# GVN key: [class, *canonical_operand_ids]. Two instructions with the
284+
# same key compute the same value. Returns nil if not numberable
285+
# (side-effecting, control, etc). Subclasses override as needed.
286+
def value_key(fun) = nil
282287
end
283288

284289
class Param < Insn
@@ -295,6 +300,7 @@ def initialize(val, type)
295300
super(type)
296301
@val = val
297302
end
303+
def value_key(_fun) = [:Const, @val]
298304
end
299305

300306
class Snapshot < Insn
@@ -340,6 +346,7 @@ def initialize(val)
340346
@val = val
341347
end
342348
def operands = [val]
349+
def value_key(fun) = [:Test, fun.find(val).id]
343350
end
344351

345352
class FixnumAdd < Insn
@@ -350,6 +357,7 @@ def initialize(left, right, state)
350357
end
351358
def operands = [left, right, state].compact
352359
def effects = Effects.new(Eff::Empty, Eff::Control)
360+
def value_key(fun) = [:FixnumAdd, fun.find(left).id, fun.find(right).id]
353361
end
354362

355363
class FixnumSub < Insn
@@ -360,6 +368,7 @@ def initialize(left, right, state)
360368
end
361369
def operands = [left, right, state].compact
362370
def effects = Effects.new(Eff::Empty, Eff::Control)
371+
def value_key(fun) = [:FixnumSub, fun.find(left).id, fun.find(right).id]
363372
end
364373

365374
class FixnumMult < Insn
@@ -370,6 +379,7 @@ def initialize(left, right, state)
370379
end
371380
def operands = [left, right, state].compact
372381
def effects = Effects.new(Eff::Empty, Eff::Control)
382+
def value_key(fun) = [:FixnumMult, fun.find(left).id, fun.find(right).id]
373383
end
374384

375385
class FixnumLt < Insn
@@ -379,6 +389,7 @@ def initialize(left, right)
379389
@left = left; @right = right
380390
end
381391
def operands = [left, right]
392+
def value_key(fun) = [:FixnumLt, fun.find(left).id, fun.find(right).id]
382393
end
383394

384395
class FixnumEq < Insn
@@ -388,6 +399,7 @@ def initialize(left, right)
388399
@left = left; @right = right
389400
end
390401
def operands = [left, right]
402+
def value_key(fun) = [:FixnumEq, fun.find(left).id, fun.find(right).id]
391403
end
392404

393405
class FixnumGt < Insn
@@ -397,6 +409,7 @@ def initialize(left, right)
397409
@left = left; @right = right
398410
end
399411
def operands = [left, right]
412+
def value_key(fun) = [:FixnumGt, fun.find(left).id, fun.find(right).id]
400413
end
401414

402415
class Send < Insn
@@ -490,6 +503,100 @@ def push(insn)
490503
def to_s = "bb#{@id}"
491504
end
492505

506+
# ─── Dominators (Cooper/Harvey/Kennedy) ─────────────────────────────
507+
# Computes the immediate dominator (idom) of each block using the
508+
# "engineered algorithm" from:
509+
# Cooper, Harvey & Kennedy, "A Simple, Fast Dominance Algorithm", 2001
510+
# https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf
511+
512+
class Dominators
513+
def initialize(fun)
514+
@blocks = fun.rpo
515+
return if @blocks.empty?
516+
517+
# Map block → RPO index for fast comparison
518+
@rpo_index = {}
519+
@blocks.each_with_index { |b, i| @rpo_index[b.id] = i }
520+
521+
# Build predecessor lists
522+
@preds = Hash.new { |h, k| h[k] = [] }
523+
@blocks.each do |block|
524+
block.insns.each do |insn|
525+
insn = fun.find(insn)
526+
case insn
527+
when Jump then @preds[insn.target.target.id] << block
528+
when IfTrue then @preds[insn.target.target.id] << block
529+
when IfFalse then @preds[insn.target.target.id] << block
530+
end
531+
end
532+
end
533+
534+
# idom[block_id] = Block (immediate dominator)
535+
@idoms = {}
536+
root = @blocks[0]
537+
@idoms[root.id] = root # root dominates itself (sentinel)
538+
539+
# Iterate until convergence
540+
changed = true
541+
while changed
542+
changed = false
543+
@blocks.each do |block|
544+
next if block.equal?(root)
545+
preds = @preds[block.id]
546+
next if preds.empty?
547+
548+
# Pick first processed predecessor
549+
new_idom = preds.find { |p| @idoms.key?(p.id) }
550+
next unless new_idom
551+
552+
# Intersect with remaining processed predecessors
553+
preds.each do |p|
554+
next if p.equal?(new_idom)
555+
next unless @idoms.key?(p.id)
556+
new_idom = intersect(new_idom, p)
557+
end
558+
559+
if @idoms[block.id] != new_idom
560+
@idoms[block.id] = new_idom
561+
changed = true
562+
end
563+
end
564+
end
565+
end
566+
567+
# Return the immediate dominator of a block (nil for root)
568+
def idom(block)
569+
d = @idoms[block.id]
570+
(d && !d.equal?(block)) ? d : nil
571+
end
572+
573+
# Does `a` dominate `b`? Walk idom chain from b upward.
574+
def dominates?(a, b)
575+
current = b
576+
while current
577+
return true if current.equal?(a)
578+
current = idom(current)
579+
end
580+
false
581+
end
582+
583+
private
584+
585+
def intersect(b1, b2)
586+
finger1 = b1
587+
finger2 = b2
588+
while !finger1.equal?(finger2)
589+
while @rpo_index[finger1.id] > @rpo_index[finger2.id]
590+
finger1 = @idoms[finger1.id]
591+
end
592+
while @rpo_index[finger2.id] > @rpo_index[finger1.id]
593+
finger2 = @idoms[finger2.id]
594+
end
595+
end
596+
finger1
597+
end
598+
end
599+
493600
# ─── Function (the whole HIR graph) ────────────────────────────────
494601

495602
class Function
@@ -1124,12 +1231,52 @@ def eliminate_dead_code
11241231
end
11251232
end
11261233

1234+
# ── global_value_numbering ─────────────────────────────────────
1235+
# Dominator-tree-based GVN, following the Maxine-VM C1X approach:
1236+
# 1. Compute dominators (Cooper/Harvey/Kennedy)
1237+
# 2. Walk blocks in RPO; each block inherits its dominator's value map
1238+
# 3. For each numberable instruction, findInsert in scoped map
1239+
# 4. If found, make_equal_to the duplicate → the original
1240+
1241+
def global_value_numbering
1242+
doms = Dominators.new(self)
1243+
value_maps = {} # Block -> Hash { value_key => Insn }
1244+
1245+
rpo.each do |block|
1246+
# Inherit dominator's value map (scoped — copy on write via dup)
1247+
idom = doms.idom(block)
1248+
parent_map = (idom && idom != block) ? value_maps[idom.id] : nil
1249+
current_map = parent_map ? parent_map.dup : {}
1250+
1251+
# Rebuild insn list, dropping duplicates (like fold_constants)
1252+
old_insns = block.insns.dup
1253+
block.insns.clear
1254+
old_insns.each do |insn|
1255+
canonical = find(insn)
1256+
key = canonical.value_key(self)
1257+
if key
1258+
existing = current_map[key]
1259+
if existing && !existing.equal?(canonical)
1260+
make_equal_to(canonical, existing)
1261+
next # drop duplicate from block
1262+
else
1263+
current_map[key] = canonical
1264+
end
1265+
end
1266+
block.insns << insn
1267+
end
1268+
1269+
value_maps[block.id] = current_map
1270+
end
1271+
end
1272+
11271273
# ── optimize (the pipeline) ──────────────────────────────────────
11281274
# Runs each pass once, sequentially, just like real ZJIT.
11291275

11301276
def optimize
11311277
type_specialize
11321278
fold_constants
1279+
global_value_numbering
11331280
clean_cfg
11341281
eliminate_dead_code
11351282
end
@@ -1717,6 +1864,166 @@ def test_send_not_removed_by_dce
17171864
end
17181865
end
17191866

1867+
# ── dominators tests ──────────────────────────────────────────────
1868+
1869+
class DominatorsTest < Minitest::Test
1870+
include MiniZJIT
1871+
1872+
def test_single_block_dominates_itself
1873+
fun = Function.new("test")
1874+
bb0 = fun.new_block
1875+
fun.push_insn(bb0, Return.new(fun.push_insn(bb0, Const.new(1, Types::Fixnum))))
1876+
doms = Dominators.new(fun)
1877+
assert_nil doms.idom(bb0), "root has no idom"
1878+
assert doms.dominates?(bb0, bb0), "root dominates itself"
1879+
end
1880+
1881+
def test_linear_chain
1882+
fun = Function.new("test")
1883+
bb0 = fun.new_block
1884+
bb1 = fun.new_block
1885+
fun.push_insn(bb0, Jump.new(BranchEdge.new(bb1)))
1886+
fun.push_insn(bb1, Return.new(fun.push_insn(bb1, Const.new(1, Types::Fixnum))))
1887+
doms = Dominators.new(fun)
1888+
assert_equal bb0, doms.idom(bb1)
1889+
assert doms.dominates?(bb0, bb1)
1890+
refute doms.dominates?(bb1, bb0)
1891+
end
1892+
1893+
def test_diamond
1894+
fun = Function.new("test")
1895+
bb0 = fun.new_block
1896+
bb1 = fun.new_block
1897+
bb2 = fun.new_block
1898+
bb3 = fun.new_block
1899+
cond = fun.push_insn(bb0, Const.new(true, Types::TrueClass))
1900+
test = fun.push_insn(bb0, Test.new(cond))
1901+
fun.push_insn(bb0, IfTrue.new(test, BranchEdge.new(bb1)))
1902+
fun.push_insn(bb0, Jump.new(BranchEdge.new(bb2)))
1903+
fun.push_insn(bb1, Jump.new(BranchEdge.new(bb3)))
1904+
fun.push_insn(bb2, Jump.new(BranchEdge.new(bb3)))
1905+
fun.push_insn(bb3, Return.new(fun.push_insn(bb3, Const.new(1, Types::Fixnum))))
1906+
doms = Dominators.new(fun)
1907+
# bb0 dominates everything
1908+
assert doms.dominates?(bb0, bb1)
1909+
assert doms.dominates?(bb0, bb2)
1910+
assert doms.dominates?(bb0, bb3)
1911+
# bb1 and bb2 don't dominate bb3 (both paths lead there)
1912+
refute doms.dominates?(bb1, bb3)
1913+
refute doms.dominates?(bb2, bb3)
1914+
# bb3's idom is bb0
1915+
assert_equal bb0, doms.idom(bb3)
1916+
end
1917+
end
1918+
1919+
# ── GVN tests ────────────────────────────────────────────────────
1920+
1921+
class GVNTest < Minitest::Test
1922+
include MiniZJIT
1923+
1924+
def test_duplicate_fixnum_add_eliminated
1925+
# Build: bb0(a:Fixnum, b:Fixnum)
1926+
# v2 = FixnumAdd a, b
1927+
# v3 = FixnumAdd a, b ← duplicate, GVN should unify with v2
1928+
# v4 = FixnumAdd v2, v3
1929+
# Return v4
1930+
fun = Function.new("test")
1931+
bb0 = fun.new_block
1932+
a = fun.push_insn(bb0, Param.new(0, Types::Fixnum))
1933+
b = fun.push_insn(bb0, Param.new(1, Types::Fixnum))
1934+
snap = fun.push_insn(bb0, Snapshot.new({}, []))
1935+
add1 = fun.push_insn(bb0, FixnumAdd.new(a, b, snap))
1936+
add2 = fun.push_insn(bb0, FixnumAdd.new(a, b, snap))
1937+
sum = fun.push_insn(bb0, FixnumAdd.new(add1, add2, snap))
1938+
fun.push_insn(bb0, Return.new(sum))
1939+
1940+
fun.global_value_numbering
1941+
fun.eliminate_dead_code
1942+
1943+
hir = fun.to_s.strip
1944+
# add2 should be eliminated — only one FixnumAdd of a,b remains
1945+
add_count = hir.scan("FixnumAdd").size
1946+
assert_equal 2, add_count,
1947+
"Expected 2 FixnumAdd (a+b and result+result), got #{add_count}:\n#{hir}"
1948+
end
1949+
1950+
def test_duplicate_const_eliminated
1951+
# Two identical Const(42) in the same block — GVN deduplicates
1952+
fun = Function.new("test")
1953+
bb0 = fun.new_block
1954+
c1 = fun.push_insn(bb0, Const.new(42, Types::Fixnum.with_const(42)))
1955+
c2 = fun.push_insn(bb0, Const.new(42, Types::Fixnum.with_const(42)))
1956+
snap = fun.push_insn(bb0, Snapshot.new({}, []))
1957+
add = fun.push_insn(bb0, FixnumAdd.new(c1, c2, snap))
1958+
fun.push_insn(bb0, Return.new(add))
1959+
1960+
fun.global_value_numbering
1961+
fun.eliminate_dead_code
1962+
1963+
hir = fun.to_s.strip
1964+
const_count = hir.scan("Const 42").size
1965+
assert_equal 1, const_count,
1966+
"Expected 1 Const 42 after GVN, got #{const_count}:\n#{hir}"
1967+
end
1968+
1969+
def test_gvn_across_dominator
1970+
# bb0: v0 = Const 42, Jump bb1
1971+
# bb1: v1 = Const 42, Return v1
1972+
# GVN should unify v1 with v0 since bb0 dominates bb1
1973+
fun = Function.new("test")
1974+
bb0 = fun.new_block
1975+
bb1 = fun.new_block
1976+
c0 = fun.push_insn(bb0, Const.new(42, Types::Fixnum.with_const(42)))
1977+
fun.push_insn(bb0, Jump.new(BranchEdge.new(bb1)))
1978+
c1 = fun.push_insn(bb1, Const.new(42, Types::Fixnum.with_const(42)))
1979+
fun.push_insn(bb1, Return.new(c1))
1980+
1981+
fun.global_value_numbering
1982+
fun.eliminate_dead_code
1983+
1984+
hir = fun.to_s.strip
1985+
const_count = hir.scan("Const 42").size
1986+
assert_equal 1, const_count,
1987+
"Expected 1 Const 42 after GVN across dominator:\n#{hir}"
1988+
end
1989+
1990+
def test_gvn_does_not_unify_across_non_dominator
1991+
# Diamond: bb0 → bb1, bb0 → bb2, bb1 → bb3, bb2 → bb3
1992+
# bb1 and bb2 each have FixnumAdd(a,b) — neither dominates the other
1993+
# so GVN should NOT unify them
1994+
fun = Function.new("test")
1995+
bb0 = fun.new_block
1996+
bb1 = fun.new_block
1997+
bb2 = fun.new_block
1998+
bb3 = fun.new_block
1999+
a = fun.push_insn(bb0, Param.new(0, Types::Fixnum))
2000+
b = fun.push_insn(bb0, Param.new(1, Types::Fixnum))
2001+
cond = fun.push_insn(bb0, Const.new(true, Types::TrueClass))
2002+
test = fun.push_insn(bb0, Test.new(cond))
2003+
fun.push_insn(bb0, IfTrue.new(test, BranchEdge.new(bb1)))
2004+
fun.push_insn(bb0, Jump.new(BranchEdge.new(bb2)))
2005+
2006+
snap1 = fun.push_insn(bb1, Snapshot.new({}, []))
2007+
add1 = fun.push_insn(bb1, FixnumAdd.new(a, b, snap1))
2008+
fun.push_insn(bb1, Jump.new(BranchEdge.new(bb3, [add1])))
2009+
2010+
snap2 = fun.push_insn(bb2, Snapshot.new({}, []))
2011+
add2 = fun.push_insn(bb2, FixnumAdd.new(a, b, snap2))
2012+
fun.push_insn(bb2, Jump.new(BranchEdge.new(bb3, [add2])))
2013+
2014+
p0 = fun.push_insn(bb3, Param.new(:result, Types::Fixnum))
2015+
fun.push_insn(bb3, Return.new(p0))
2016+
2017+
fun.global_value_numbering
2018+
2019+
# Both adds should survive — neither dominates the other
2020+
hir = fun.to_s.strip
2021+
add_count = hir.scan("FixnumAdd").size
2022+
assert_equal 2, add_count,
2023+
"Expected 2 FixnumAdd (non-dominating branches), got #{add_count}:\n#{hir}"
2024+
end
2025+
end
2026+
17202027
# ── clean_cfg tests ─────────────────────────────────────────────
17212028

17222029
class CleanCFGTest < Minitest::Test

0 commit comments

Comments
 (0)