@@ -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