Class: Optimize::Passes::ConstFoldEnvPass

Inherits:
Optimize::Pass show all
Defined in:
lib/optimize/passes/const_fold_env_pass.rb

Overview

Tier 4 const-fold: ENV -> its snapshot value (or nil).

Runs per-function like every other pass, but soundness is tree-wide: if any function in the IR tree has a tainted ENV use (a write, a ‘fetch`, any send other than the safe `opt_aref` consumer), every fold site in the tree is skipped and one :env_write_observed log entry is emitted. The “anywhere in tree” flag is memoized on the root function’s ‘misc` via `@root ||= function` — a fresh pass instance per `Pipeline.default.run` call keeps the scoping per-run.

Task 3 adds the scanner; Task 4 adds the fold loop.

Operand shape (confirmed via decode diagnostics on Ruby 4.0.2):

opt_getconstant_path operands=[N]  where N is an object-table index.
object_table.objects[N] is an Array of object-table indices whose
resolved elements are the constant-path symbols, e.g. [:ENV].

Constant Summary collapse

TAINT_FLAG_KEY =
:const_fold_env_tree_tainted
SAFE_ENV_READ_METHODS =

Read-only ENV methods that cannot mutate ENV. A send on ENV with one of these mids does NOT taint the tree. v1: argc 0 and 1 only. Expanding this set is safe iff the method is guaranteed non-mutating.

%i[
  fetch to_h to_hash key? has_key? include? member?
  values_at assoc size length empty? keys values
  inspect to_s hash ==
].to_set.freeze
PURE_DEFAULT_OPCODES =

Default-producer opcodes that are safe to drop when folding ‘ENV.fetch(LIT, default)` on a key hit: each is a single-instruction side-effect-free producer. Extending this set requires the producer to be observably pure (no autoload, no side effects, no raises).

%i[
  putnil putobject putstring putchilledstring putself
].to_set.freeze

Instance Method Summary collapse

Methods inherited from Optimize::Pass

#one_shot?

Instance Method Details

#apply(function, type_env:, log:, object_table: nil, env_snapshot: nil, **_extras) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/optimize/passes/const_fold_env_pass.rb', line 48

def apply(function, type_env:, log:, object_table: nil, env_snapshot: nil, **_extras)
  _ = type_env
  return unless object_table
  return unless env_snapshot

  root = tree_root(function)
  root.misc ||= {}
  unless root.misc.key?(:const_fold_env_tree_scanned)
    root.misc[:const_fold_env_tree_scanned] = true
    scan_tree_for_taint(root, object_table, log)
  end

  insts = function.instructions
  return unless insts
  return if root.misc[TAINT_FLAG_KEY]

  # Fold phase. Match the 3-tuple:
  #   opt_getconstant_path <ENV>; putchilledstring/putstring KEY; opt_aref
  # Splice to a single putobject <idx> or putnil, where idx comes from
  # object_table.index_for(snapshot_value). If the snapshot value isn't
  # already interned, skip that fold site (intern can't append strings).
  i = 0
  while i <= insts.size - 3
    a  = insts[i]
    b  = insts[i + 1]
    op = insts[i + 2]

    unless env_producer?(a, object_table) && literal_string?(b, object_table)
      i += 1
      next
    end

    d   = insts[i + 2]
    op4 = insts[i + 3]
    if d && op4 && op4.opcode == :opt_send_without_block &&
       fetch_send_argc2?(op4, object_table) && pure_default?(d)
      key = LiteralValue.read(b, object_table: object_table)
      if env_snapshot.key?(key)
        value = env_snapshot[key]
        if value.is_a?(String)
          idx = object_table.intern(value)
          replacement = IR::Instruction.new(opcode: :putobject, operands: [idx], line: a.line)
          function.splice_instructions!(i..(i + 3), [replacement])
          log.rewrite(pass: :const_fold_env, reason: :folded,
                      file: function.path, line: (a.line || function.first_lineno || 0))
        else
          log.skip(pass: :const_fold_env, reason: :env_value_not_string,
                   file: function.path, line: (a.line || function.first_lineno || 0))
        end
      else
        # Key absent: return the default. Keep `d` as the sole instruction.
        function.splice_instructions!(i..(i + 3), [d])
        log.rewrite(pass: :const_fold_env, reason: :folded,
                    file: function.path, line: (a.line || function.first_lineno || 0))
      end
      i += 1
      next
    end

    if op.opcode == :opt_aref
      key = LiteralValue.read(b, object_table: object_table)
      value = env_snapshot[key]

      replacement =
        if value.nil?
          IR::Instruction.new(opcode: :putnil, operands: [], line: a.line)
        elsif value.is_a?(String)
          idx = object_table.intern(value)
          IR::Instruction.new(opcode: :putobject, operands: [idx], line: a.line)
        else
          log.skip(pass: :const_fold_env, reason: :env_value_not_string,
                   file: function.path, line: (a.line || function.first_lineno || 0))
          nil
        end

      if replacement
        function.splice_instructions!(i..(i + 2), [replacement])
        log.rewrite(pass: :const_fold_env, reason: :folded,
                    file: function.path, line: (a.line || function.first_lineno || 0))
      end
      i += 1
    elsif op.opcode == :opt_send_without_block && fetch_send?(op, object_table)
      key = LiteralValue.read(b, object_table: object_table)
      if env_snapshot.key?(key)
        value = env_snapshot[key]
        if value.is_a?(String)
          idx = object_table.intern(value)
          replacement = IR::Instruction.new(opcode: :putobject, operands: [idx], line: a.line)
          function.splice_instructions!(i..(i + 2), [replacement])
          log.rewrite(pass: :const_fold_env, reason: :folded,
                      file: function.path, line: (a.line || function.first_lineno || 0))
        else
          log.skip(pass: :const_fold_env, reason: :env_value_not_string,
                   file: function.path, line: (a.line || function.first_lineno || 0))
        end
      else
        log.skip(pass: :const_fold_env, reason: :fetch_key_absent,
                 file: function.path, line: (a.line || function.first_lineno || 0))
      end
      i += 1
    else
      i += 1
    end
  end
end

#nameObject



46
# File 'lib/optimize/passes/const_fold_env_pass.rb', line 46

def name = :const_fold_env