Class: Idl::IfAst

Inherits:
AstNode show all
Includes:
Executable, Returns
Defined in:
lib/idlc/ast.rb,
lib/idlc/passes/prune.rb,
lib/idlc/passes/gen_adoc.rb,
lib/idlc/passes/gen_option_adoc.rb,
lib/idlc/passes/find_return_values.rb,
lib/idlc/passes/reachable_functions.rb,
lib/idlc/passes/reachable_exceptions.rb

Constant Summary

Constants inherited from AstNode

AstNode::Bits1Type, AstNode::Bits32Type, AstNode::Bits64Type, AstNode::BoolType, AstNode::ConstBoolType, AstNode::PossiblyUnknownBits1Type, AstNode::PossiblyUnknownBits32Type, AstNode::PossiblyUnknownBits64Type, AstNode::ReachableFunctionCacheType, AstNode::StringType, AstNode::VoidType

Instance Attribute Summary

Attributes inherited from AstNode

#children, #input, #interval, #parent

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Returns

#expected_return_type

Methods included from Executable

#executable?

Methods inherited from AstNode

#always_terminates?, #declaration?, #executable?, extract_base_var_name, #find_ancestor, #find_dst_registers, #find_referenced_csrs, #find_src_registers, #freeze_tree, #input_file, input_from_source_yaml, #inspect, #internal_error, interval_from_source_yaml, #lineno, #lines_around, #nullify_assignments, #path, #print_ast, #set_input_file, #set_input_file_unless_already_set, #source_line_file_offsets, #source_starting_offset, #source_yaml, #starting_line, #text_value, #truncation_warn, #type_error, #unindent, value_else, #value_else, value_error, #value_error, value_try, #value_try, write_back_nested

Constructor Details

#initialize(input, interval, if_cond, if_body, elseifs, final_else_body) ⇒ IfAst

Returns a new instance of IfAst.



9088
9089
9090
9091
9092
9093
9094
9095
9096
# File 'lib/idlc/ast.rb', line 9088

def initialize(input, interval, if_cond, if_body, elseifs, final_else_body)
  children_nodes = [if_cond, if_body]
  children_nodes += elseifs
  children_nodes << final_else_body

  @func_type_cache = {}

  super(input, interval, children_nodes)
end

Class Method Details

.from_h(yaml, source_mapper) ⇒ Object



9321
9322
9323
9324
9325
9326
9327
9328
9329
9330
9331
9332
9333
# File 'lib/idlc/ast.rb', line 9321

def self.from_h(yaml, source_mapper)
  raise "Bad YAML" unless yaml.key?("kind") && yaml.fetch("kind") == "if_stmt"

  input = input_from_source_yaml(yaml.fetch("source"), source_mapper)
  interval = interval_from_source_yaml(yaml.fetch("source"))
  IfAst.new(
    input, interval,
    AstNode.from_h(yaml.fetch("condition"), source_mapper),
    AstNode.from_h(yaml.fetch("taken_body"), source_mapper),
    yaml.fetch("else_ifs").map { |eif| AstNode.from_h(eif, source_mapper) },
    yaml.fetch("else").nil? ? IfBodyAst.new(nil, nil, []) : AstNode.from_h(yaml.fetch("else"), source_mapper)
  )
end

Instance Method Details

#const_eval?(symtab) ⇒ Boolean

Returns:

  • (Boolean)


9069
9070
9071
9072
9073
9074
# File 'lib/idlc/ast.rb', line 9069

def const_eval?(symtab)
  if_cond.const_eval?(symtab) && \
    if_body.const_eval?(symtab) && \
    elseifs.all? { |eif| eif.const_eval?(symtab) } && \
    final_else_body.const_eval?(symtab)
end

#elseifsObject



9083
# File 'lib/idlc/ast.rb', line 9083

def elseifs = T.cast(T.must(@children[2..-2]), T::Array[ElseIfAst])

#execute(symtab) ⇒ void

This method returns an undefined value.

“execute” the statement by updating the variables in the symbol table

Parameters:

  • symtab (SymbolTable)

    The symbol table for the context

Raises:

  • ValueError if some part of the statement cannot be executed at compile time



9257
9258
9259
9260
9261
9262
9263
9264
9265
9266
9267
9268
9269
9270
9271
9272
9273
9274
9275
9276
9277
9278
9279
9280
9281
9282
9283
9284
9285
9286
9287
9288
9289
9290
9291
# File 'lib/idlc/ast.rb', line 9257

def execute(symtab)
  err = T.let(nil, T.nilable(Symbol))
  value_result = value_try do
    if_cond_value = if_cond.value(symtab)
    if if_cond_value
      # if is taken, so only the taken body is executable
      value_result2 = value_try do
        if_body.execute(symtab)
      end
      value_else(value_result2) do
        err = :value_error if err.nil?
      end
    else
      execute_after_if(symtab)
    end
  end
  value_else(value_result) do
    # condition not known; both paths can execute
    value_result2 = value_try do
      if_body.execute(symtab)
    end
    value_else(value_result2) do
      err = :value_error if err.nil?
    end

    value_result2 = value_try do
      execute_after_if(symtab)
    end
    value_else(value_result2) do
      err = :value_error if err.nil?
    end
  end

  value_error "" unless err.nil?
end

#final_else_bodyObject



9086
# File 'lib/idlc/ast.rb', line 9086

def final_else_body = T.cast(T.must(@children.last), IfBodyAst)

#gen_adoc(indent = 0, indent_spaces: 2) ⇒ Object



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/idlc/passes/gen_adoc.rb', line 333

def gen_adoc(indent = 0, indent_spaces: 2)
  lines = ["#{' ' * indent}if pass:[(]#{if_cond.gen_adoc(0, indent_spaces:)}) {"]
  if_body.stmts.each do |s|
    lines << s.gen_adoc(indent + indent_spaces, indent_spaces:)
  end
  elseifs.each do |eif|
    lines << "#{' ' * indent}} else if pass:[(]#{eif.cond.gen_adoc(0, indent_spaces:)}) {"
    eif.body.stmts.each do |s|
      lines << s.gen_adoc(indent + indent_spaces, indent_spaces:)
    end
  end
  unless final_else_body.stmts.empty?
    lines << "#{' ' * indent}} else {"
    final_else_body.stmts.each do |s|
      lines << s.gen_adoc(indent + indent_spaces, indent_spaces:)
    end
  end
  lines << "#{' ' * indent}}"

  lines.join("\n")
end

#gen_option_adocObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/idlc/passes/gen_option_adoc.rb', line 34

def gen_option_adoc
  adoc =
    <<~ADOC
      [when,"#{if_cond.to_idl}"]
      #{if_body.gen_option_adoc}
    ADOC
  elseifs.each do |eif|
    adoc << <<~ADOC
      [when,"#{eif.cond.to_idl}"]
      #{eif.body.gen_option_adoc}
    ADOC
  end
  unless final_else_body.nil?
    if elseifs.empty?
      if if_cond.is_a?(BinaryExpressionAst) || if_cond.is_a?(UnaryOperatorExpressionAst)
        adoc << <<~ADOC
          [when,"#{if_cond.invert(nil).to_idl}"]
          #{final_else_body.gen_option_adoc}
        ADOC
      else
        adoc << <<~ADOC
          [when,"!(#{if_cond.to_idl})"]
          #{final_else_body.gen_option_adoc}
        ADOC
      end
    else
      adoc << <<~ADOC
        [when,"else"]
        #{final_else_body.gen_option_adoc}
      ADOC
    end
  end
  adoc
end

#if_bodyObject



9080
# File 'lib/idlc/ast.rb', line 9080

def if_body = T.cast(@children.fetch(1), IfBodyAst)

#if_condObject



9077
# File 'lib/idlc/ast.rb', line 9077

def if_cond = T.cast(@children.fetch(0), RvalueAst)

#pass_find_return_values(values, current_conditions, symtab) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/idlc/passes/find_return_values.rb', line 35

def pass_find_return_values(values, current_conditions, symtab)
  current_conditions.push if_cond
  if_body.elements.each do |e|
    e.e.pass_find_return_values(values, current_conditions, symtab)
  end
  current_conditions.pop

  unless elseifs.empty?
    elseifs.elements.each do |eif|
      current_conditions.push eif.expression

      eif.body.elements.each do |e|
        e.e.pass_find_return_values(values, current_conditions, symtab)
      end

      current_conditions.pop
    end
  end

  unless final_else.empty?
    current_conditions.push if_cond.invert(symtab)

    final_else.body.elements.each do |e|
      e.e.pass_find_return_values(values, current_conditions, symtab)
    end
    current_conditions.pop
  end
end

#prune(symtab, forced_type: nil) ⇒ Object



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/idlc/passes/prune.rb', line 526

def prune(symtab, forced_type: nil)
  value_result = value_try do
    if if_cond.value(symtab)
      return if_body.prune(symtab, restore: false)
    elsif !elseifs.empty?
      # we know that the if condition is false, so now we treat the else if
      # as the starting point and try again
      return IfAst.new(
        input, interval,
        elseifs[0].cond.dup,
        elseifs[0].body.dup,
        elseifs[1..].map(&:dup),
        final_else_body.dup).prune(symtab)
    elsif !final_else_body.stmts.empty?
      # the if is false, and there are no else ifs, so the result of the prune is just the pruned else body
      return final_else_body.prune(symtab, restore: false)
    else
      # the if is false, and there are no else ifs or elses. This is just a no-op
      return NoopAst.new
    end
  end
  value_else(value_result) do
    # we don't know the value of the if condition
    # we still might know the value of an else if
    unknown_elsifs = []
    elseifs.each do |eif|
      value_result = value_try do
        if eif.cond.value(symtab)
          # this elseif is true, so turn it into an else and then we are done
          return IfAst.new(
            input, interval,
            if_cond.dup,
            if_body.dup,
            unknown_elsifs.map(&:dup),
            eif.body.dup
          ).prune(symtab)
        else
          # this elseif is false, so we can remove it
          next :ok
        end
      end
      value_else(value_result) do
        unknown_elsifs << eif
      end
    end
    # we get here, then we don't know the value of anything. just return this if with everything pruned
    # After pruning, some elseif conditions may resolve to a literal (e.g., `false && <runtime_csr_read>`
    # fails value() due to the CSR read but prune() short-circuits the && to false). Filter those out.
    pruned_elsifs = unknown_elsifs.filter_map do |eif|
      pruned = eif.prune(symtab)
      next nil if pruned.cond.is_a?(FalseExpressionAst)
      pruned
    end
    result = IfAst.new(
      input, interval,
      if_cond.prune(symtab),
      if_body.prune(symtab),
      pruned_elsifs,
      final_else_body.prune(symtab)
    )
    # Nullify any variable assigned in any branch, since we don't know which ran
    if_body.nullify_assignments(symtab)
    pruned_elsifs.each { |eif| eif.body.nullify_assignments(symtab) }
    final_else_body.nullify_assignments(symtab)
    result
  end
end

#reachable_exceptions(symtab, cache = {}) ⇒ Object



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
# File 'lib/idlc/passes/reachable_exceptions.rb', line 94

def reachable_exceptions(symtab, cache = {})
  mask = 0
  value_try do
    mask = if_cond.reachable_exceptions(symtab, cache) if if_cond.is_a?(FunctionCallExpressionAst)
    value_result = value_try do
      if (if_cond.value(symtab))
        mask |= if_body.reachable_exceptions(symtab, cache)
        return mask # no need to continue
      else
        elseifs.each do |eif|
          mask |= eif.cond.reachable_exceptions(symtab, cache) if eif.cond.is_a?(FunctionCallExpressionAst)
          value_result = value_try do
            if (eif.cond.value(symtab))
              mask |= eif.body.reachable_exceptions(symtab, cache)
              return mask # no need to keep going
            end
          end
          value_else(value_result) do
            # condition isn't known; body is potentially reachable
            mask |= eif.body.reachable_exceptions(symtab, cache)
          end
        end
        mask |= final_else_body.reachable_exceptions(symtab, cache)
      end
    end
    value_else(value_result) do
      mask |= if_body.reachable_exceptions(symtab, cache)

      elseifs.each do |eif|
        mask |= eif.cond.reachable_exceptions(symtab, cache) if eif.cond.is_a?(FunctionCallExpressionAst)
        value_result = value_try do
          if (eif.cond.value(symtab))
            mask |= eif.body.reachable_exceptions(symtab, cache)
            return mask # no need to keep going
          end
        end
        value_else(value_result) do
          # condition isn't known; body is potentially reachable
          mask |= eif.body.reachable_exceptions(symtab, cache)
        end
      end
      mask |= final_else_body.reachable_exceptions(symtab, cache)
    end
  end
  return mask
end

#reachable_functions(symtab, cache = T.let({}, ReachableFunctionCacheType)) ⇒ Object



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/idlc/passes/reachable_functions.rb', line 103

def reachable_functions(symtab, cache = T.let({}, ReachableFunctionCacheType))
  fns = []
  value_try do
    fns.concat if_cond.reachable_functions(symtab, cache)
    value_result = value_try do
      if (if_cond.value(symtab))
        fns.concat if_body.reachable_functions(symtab, cache)
        return fns # no need to continue
      else
        if (if_cond.text_value == "pending_and_enabled_interrupts != 0")
          warn symtab.get("pending_and_enabled_interrupts")
          raise "???"
        end
        elseifs.each do |eif|
          fns.concat eif.cond.reachable_functions(symtab, cache)
          value_result = value_try do
            if (eif.cond.value(symtab))
              fns.concat eif.body.reachable_functions(symtab, cache)
              return fns # no need to keep going
            end
          end
          value_else(value_result) do
            # condition isn't known; body is potentially reachable
            fns.concat eif.body.reachable_functions(symtab, cache)
          end
        end
        fns.concat final_else_body.reachable_functions(symtab, cache)
      end
    end
    value_else(value_result) do
      fns.concat if_body.reachable_functions(symtab, cache)

      elseifs.each do |eif|
        fns.concat eif.cond.reachable_functions(symtab, cache)
        value_result = value_try do
          if (eif.cond.value(symtab))
            fns.concat eif.body.reachable_functions(symtab, cache)
            return fns # no need to keep going
          end
        end
        value_else(value_result) do
          # condition isn't known; body is potentially reachable
          fns.concat eif.body.reachable_functions(symtab, cache)
        end
      end
      fns.concat final_else_body.reachable_functions(symtab, cache)
    end
  end
  return fns
end

#return_type(symtab) ⇒ Object



9150
9151
9152
9153
9154
9155
9156
# File 'lib/idlc/ast.rb', line 9150

def return_type(symtab)
  begin
    rt = if_body.return_type(symtab)
    rt = elseifs.map { |eif| eif.return_type(symtab) }.compact[0] if rt.nil?
    rt ||= final_else_body.return_type(symtab)
  end
end

#return_value(symtab) ⇒ Integer, ...

Evaluate the compile-time return value of this node, or, if the node does not return (e.g., because it is an IfAst but there is no return on the taken path), execute the node and update the symtab

Parameters:

  • symtab (SymbolTable)

    The symbol table for the context

Returns:

  • (Integer)

    The return value, if it is integral

  • (Boolean)

    The return value, if it is boolean

  • (nil)

    if the return value is not compile-time-known

Raises:

  • ValueError if, during evaluation, a node without a compile-time value is found



9159
9160
9161
9162
9163
9164
9165
# File 'lib/idlc/ast.rb', line 9159

def return_value(symtab)

  body = taken_body(symtab)
  return nil if body.nil?

  body.return_value(symtab)
end

#return_values(symtab) ⇒ Array<Integer,Bool>

Returns a list of all possible return values, if known. Otherwise, raises a ValueError

Parameters:

Returns:

  • (Array<Integer,Bool>)

    List of all possible return values

Raises:

  • ValueError if it is not possible to determine all return values at compile time



9200
9201
9202
9203
9204
9205
9206
9207
9208
9209
9210
9211
9212
9213
9214
9215
# File 'lib/idlc/ast.rb', line 9200

def return_values(symtab)
  value_result = value_try do
    if_cond_value = if_cond.value(symtab)
    if if_cond_value
      # if is taken, so the only possible return values are those in the if body
      return if_body.return_values(symtab)
    else
      # if cond not taken; check else ifs and possibly final else
      return return_values_after_if(symtab)
    end
  end
  value_else(value_result) do
    # if condition not known; both paths are possible
    (if_body.return_values(symtab) + return_values_after_if(symtab)).uniq
  end
end

#taken_body(symtab) ⇒ Boolean

Returns true if the taken path is knowable at compile-time.

Returns:

  • (Boolean)

    true if the taken path is knowable at compile-time

Raises:

  • ValueError if the take path is not known at compile time



9137
9138
9139
9140
9141
9142
9143
9144
9145
9146
9147
# File 'lib/idlc/ast.rb', line 9137

def taken_body(symtab)
  return if_body if if_cond.value(symtab)

  unless elseifs.empty?
    elseifs.each do |eif|
      return eif.body if eif.cond.value(symtab)
    end
  end

  final_else_body.stmts.empty? ? nil : final_else_body
end

#to_hObject



9311
9312
9313
9314
9315
9316
9317
9318
# File 'lib/idlc/ast.rb', line 9311

def to_h = {
  "kind" => "if_stmt",
  "condition" => if_cond.to_h,
  "taken_body" => if_body.to_h,
  "else_ifs" => elseifs.map(&:to_h),
  "else" => final_else_body.stmts.empty? ? nil : final_else_body.to_h,
  "source" => source_yaml
}

#to_idlObject



9295
9296
9297
9298
9299
9300
9301
9302
9303
9304
9305
9306
9307
9308
# File 'lib/idlc/ast.rb', line 9295

def to_idl
  result = "if (#{if_cond.to_idl}) { "
  result << if_body.to_idl
  result << "} "
  elseifs.each do |eif|
    result << eif.to_idl
  end
  unless final_else_body.stmts.empty?
    result << " else { "
    result << final_else_body.to_idl
    result << "} "
  end
  result
end

#type_check(symtab, strict:) ⇒ void

This method returns an undefined value.

type check this node and all children

Calls to #type and/or #value may depend on type_check being called first with the same symtab. If not, those functions may raise an AstNode::InternalError

Parameters:

Raises:



9099
9100
9101
9102
9103
9104
9105
9106
9107
9108
9109
9110
9111
9112
9113
9114
9115
9116
9117
9118
9119
9120
9121
9122
9123
9124
9125
9126
9127
9128
9129
9130
9131
9132
9133
# File 'lib/idlc/ast.rb', line 9099

def type_check(symtab, strict:)
  level = symtab.levels
  if_cond.type_check(symtab, strict:)


  unless if_cond.type(symtab).convertable_to?(:boolean)
    if if_cond.type(symtab).kind == :bits
      type_error "'#{if_cond.text_value}' is not boolean. Maybe you meant 'if ((#{if_cond.text_value}) != 0)'?"
    else
      type_error "'#{if_cond.text_value}' is not boolean"
    end
  end

  if_cond_value = T.let(nil, T.nilable(ValueRbType))
  value_try do
    if_cond_value = if_cond.value(symtab)
  end

  # short-circuit the if body if we can
  if_body.type_check(symtab, strict:) unless if_cond_value == false

  internal_error "not at same level #{level} #{symtab.levels}" unless level == symtab.levels

  unless (if_cond_value == true) || elseifs.empty?
    elseifs.each do |eif|
      eif.type_check(symtab, strict:)
    end
  end

  internal_error "not at same level #{level} #{symtab.levels}" unless level == symtab.levels

  final_else_body.type_check(symtab, strict:) unless if_cond_value == true

  internal_error "not at same level #{level} #{symtab.levels}" unless level == symtab.levels
end