Control Flow Graph - Optimisations

Optimisations basées sur le CFG pour réduire la taille et améliorer la performance du code.

Importation

from catnip import Catnip
import catnip._rs as rs

c = Catnip()

Élimination de code mort

Suppression des blocs non atteignables depuis l'entrée.

ir = c.parse('x = 1; y = 2')
cfg = rs.cfg.build_cfg_from_ir(ir, 'test')

print(f'Blocks before: {cfg.num_blocks}')
dead = cfg.eliminate_dead_code()
print(f'Dead blocks removed: {dead}')
print(f'Blocks after: {cfg.num_blocks}')

Les blocs non atteignables peuvent apparaître après d'autres optimisations ou dans du code mal formé.

Fusion de blocs

Fusion de blocs séquentiels qui peuvent être combinés.

Conditions de fusion :

  • A a exactement un successeur (B)
  • B a exactement un prédécesseur (A)
  • L'edge est Fallthrough ou Unconditional
ir = c.parse('''
x = 1
y = 2
z = 3
''')
cfg = rs.cfg.build_cfg_from_ir(ir, 'linear')

print(f'Blocks before: {cfg.num_blocks}')
merged = cfg.merge_blocks()
print(f'Blocks merged: {merged}')
print(f'Blocks after: {cfg.num_blocks}')

La fusion réduit le nombre de sauts et améliore la localité.

Suppression de blocs vides

Suppression des blocs sans instructions qui peuvent être contournés.

code = '''
while x < 10 {
    x = x + 1
}
'''
ir = c.parse(code)
cfg = rs.cfg.build_cfg_from_ir(ir, 'while')

print(f'Blocks before: {cfg.num_blocks}')
removed = cfg.remove_empty_blocks()
print(f'Empty blocks removed: {removed}')
print(f'Blocks after: {cfg.num_blocks}')

Les blocs vides (comme while_exit) peuvent être court-circuités directement.

Élimination de branches constantes

Si les deux branches d'un if vont vers la même cible, la condition est inutile.

# Exemple artificiel - en pratique, détecté par constant folding
ir = c.parse('''
if x {
    y = 1
} else {
    y = 1
}
''')
cfg = rs.cfg.build_cfg_from_ir(ir, 'constant_branch')

branches = cfg.eliminate_constant_branches()
print(f'Constant branches eliminated: {branches}')

Cette optimisation est plus efficace après propagation de constantes.

Pipeline complet

Applique toutes les optimisations dans l'ordre jusqu'à convergence.

code = '''
x = 0
while x < 10 {
    if x % 2 == 0 {
        y = x
    }
    x = x + 1
}
'''
ir = c.parse(code)
cfg = rs.cfg.build_cfg_from_ir(ir, 'complex')

print(f'Before: {cfg.num_blocks} blocks, {cfg.num_edges} edges')

# Optimiser
dead, merged, empty, branches = cfg.optimize()

print(f'Optimizations:')
print(f'  Dead blocks removed: {dead}')
print(f'  Blocks merged: {merged}')
print(f'  Empty blocks removed: {empty}')
print(f'  Constant branches eliminated: {branches}')
print(f'After: {cfg.num_blocks} blocks, {cfg.num_edges} edges')

Le pipeline itère jusqu'à convergence (point fixe).

Optimisations individuelles

Chaque optimisation peut être appelée séparément.

ir = c.parse('while x { y = 1 }')
cfg = rs.cfg.build_cfg_from_ir(ir, 'test')

# Ordre recommandé :
cfg.eliminate_dead_code()
cfg.eliminate_constant_branches()
cfg.remove_empty_blocks()
cfg.merge_blocks()

# Ou tout en une fois :
dead, merged, empty, branches = cfg.optimize()

Idempotence

Les optimisations sont idempotentes : les appliquer plusieurs fois donne le même résultat.

ir = c.parse('while x < 10 { x = x + 1 }')
cfg = rs.cfg.build_cfg_from_ir(ir, 'loop')

# Première passe
cfg.optimize()
blocks_after_first = cfg.num_blocks

# Deuxième passe (ne devrait rien faire)
dead, merged, empty, branches = cfg.optimize()
assert dead == 0 and merged == 0 and empty == 0 and branches == 0
assert cfg.num_blocks == blocks_after_first

print('✓ Optimisations idempotentes')

Préservation de sémantique

Les optimisations préservent la sémantique du programme.

ir = c.parse('''
if x > 0 {
    y = 1
} else {
    y = 2
}
z = y + 1
''')
cfg = rs.cfg.build_cfg_from_ir(ir, 'test')

# Blocs atteignables avant
before = set(cfg.get_reachable_blocks())

# Optimiser
cfg.optimize()

# Blocs atteignables après (peut être réduit par fusion)
after = set(cfg.get_reachable_blocks())

# Tous les blocs d'origine sont toujours représentés
assert len(after) > 0
print(f'Blocks: {len(before)}{len(after)}')

Visualisation avant/après

ir = c.parse('''
while x < 10 {
    if x == 5 {
        break
    }
    x = x + 1
}
''')
cfg = rs.cfg.build_cfg_from_ir(ir, 'example')

# Avant
cfg.visualize('before.dot')
print(f'Before: {cfg.num_blocks} blocks, {cfg.num_edges} edges')

# Optimiser
cfg.optimize()

# Après
cfg.visualize('after.dot')
print(f'After: {cfg.num_blocks} blocks, {cfg.num_edges} edges')

# Comparer :
# dot -Tpng before.dot -o before.png
# dot -Tpng after.dot -o after.png

Métriques d'optimisation

def analyze_optimizations(code, name):
    """Analyse l'impact des optimisations."""
    c = Catnip()
    ir = c.parse(code)
    cfg = rs.cfg.build_cfg_from_ir(ir, name)

    before_blocks = cfg.num_blocks
    before_edges = cfg.num_edges

    dead, merged, empty, branches = cfg.optimize()

    after_blocks = cfg.num_blocks
    after_edges = cfg.num_edges

    print(f'=== {name} ===')
    print(f'Blocks: {before_blocks}{after_blocks} ({before_blocks - after_blocks} removed)')
    print(f'Edges: {before_edges}{after_edges} ({before_edges - after_edges} removed)')
    print(f'Dead: {dead}, Merged: {merged}, Empty: {empty}, Branches: {branches}')
    print()

# Analyser différents patterns
analyze_optimizations('x = 1; y = 2; z = 3', 'linear')
analyze_optimizations('while x < 10 { x = x + 1 }', 'loop')
analyze_optimizations('if x { y = 1 } else { y = 2 }', 'branch')

Impact sur le bytecode

Les optimisations CFG réduisent le bytecode généré :

  • Moins de blocs = moins de labels
  • Moins d'edges = moins de sauts
  • Fusion de blocs = moins de dispatch overhead
ir = c.parse('''
while x < 100 {
    if x % 3 == 0 {
        continue
    }
    y = y + x
    x = x + 1
}
''')
cfg = rs.cfg.build_cfg_from_ir(ir, 'optimizable')

# Avant optimisation
before = cfg.num_blocks * 2 + cfg.num_edges  # Estimation simplifiée

cfg.optimize()

# Après optimisation
after = cfg.num_blocks * 2 + cfg.num_edges

print(f'Estimated bytecode reduction: {before - after} instructions')

Ces optimisations sont des procédures administratives obligatoires. Chaque bloc doit justifier son existence devant le comité des blocs. Les blocs vides sont systématiquement révoqués, sauf s'ils ont un formulaire d'exemption de révocation, disponible uniquement auprès des blocs révoqués.