class MIPSUnit::MSpec::TestFactoryHelper

These helper methods are used by MIPSUnit::MSpec::TestFactory#set, MIPSUnit::MSpec::TestFactory#call, MIPSUnit::MSpec::TestFactory#verify, etc. to generate assembly code. We moved them to this separate class so they don’t appear in the namespace of describe blocks. (It is important to limit what methods appear in the describe block namespace so users don’t accidentally write helper methods that hide MSpec methods.)

Author

Zachary Kurmas

Copyright

© 2012

Constants

REGISTERS

A Set of valid register names

Public Class Methods

data(hash) { |dl| ... } click to toggle source

creates a new DataLabel. The yield adds the new label to either the specific TestFactory, or the entire block as appropriate.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 61
def self.data(hash)
  unless hash.is_a?(Hash)
    raise InvalidSpecException.new("Parameters to method \"data\" are incorrectly formatted.")
  end

  hash.each do |name, values|
    unless (name.is_a? Symbol) || (name.is_a? String)
      raise InvalidSpecException.new("Must use symbol or string when specifying data.")
    end
    if TestFactoryHelper.is_register?(name)
      raise InvalidSpecException.new("Data labels may not be register names.")
    end
    dl = DataLabel.new(name, *values)
    yield dl
  end
end
fail_unless(condition, test_name, location_name, expected_value, factory, output) { || ... } click to toggle source
generate assembly code that will test a specified condition, then print a failure message unless the
specified condition is met.
- +condition+ is a +beq+ or +bne+ instruction without the branch target. (In other words,
+condition+ does _not_ include the label.)
- +test_name+ is the +String+ that is printed in the failure message to identify the test
- +location_name+ is a +String+ containing the name of the location as it should be printed in the error
#message (.e.g, "$v0")
  • expected_value is the expected value that is printed in the error message (must be an integer)

  • factory the factory

  • output the output stream

The block is responsible for moving the observed value into $a3
# File lib/mipsunit/mspec/test_factory_helper.rb, line 190
def self.fail_unless(condition, test_name, location_name, expected_value, factory, output)
  # create a label for the branch target (i.e., the end if the "if" block)
  target_label = Label.new("pass")

  # equivalent of (if $at != $register_to_check), then ...
  output.puts "#{condition}, #{target_label.unique_name}"

  # create a data label containing the test name. (This label is used to print an error message if necessary)
  test_name_label = Label.new("test_name")
  factory.class.data(test_name_label.unique_name.to_sym => [:asciiz, test_name])

  # create a data label containing the name of the location to check (e.g., register name, memory location)
  # TODO: Search for existing labels with same data.
  location_name_label = Label.new("reg_name")
  factory.class.data(location_name_label.unique_name.to_sym => [:asciiz, location_name.to_s])

  yield
  factory.call_by_name(:fail, test_name_label.unique_name.to_sym, location_name_label.unique_name.to_sym,
                       expected_value)

  # The label at the end of the if block
  output.puts "#{target_label.unique_name}:"
end
get_integer_value(value) click to toggle source

returns value if value is an Integer. Returns the corresponding ASCII value if value is a String of length 1,

# File lib/mipsunit/mspec/test_factory_helper.rb, line 217
def self.get_integer_value(value)
  if (value.is_a?(Integer))
    return value
  elsif (value.is_a?(String))
    if (value.length == 0 || value.length > 1)
      raise ArgumentError.new("If value is a String, it must have length exactly 1.")
    end
    return value.bytes.first
  else
    raise ArgumentError.new("value must be a Integer or a String")
  end
end
get_label_string(label, labels_in_scope) click to toggle source

return the globally unique String that corresponds to the given label. If label is a String, then assume that the name is already unique (e.g., that it is a label hard-coded in the code under test). If label is a Symbol, then assume that it is the local name of a DataLabel and look up its unique name.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 103
def self.get_label_string(label, labels_in_scope)
  return label if label.is_a?(AssemblyLabel)
  return lookup_unique_name(label, labels_in_scope) if label.is_a?(Symbol)
  raise InvalidSpecException.new("#{label.to_s} is not a valid label.  (Must be  must be a Symbol or AssemblyLabel.)")
end
is_register?(symbol) click to toggle source

return whether symbol is a valid register

# File lib/mipsunit/mspec/test_factory_helper.rb, line 30
def self.is_register?(symbol)
  REGISTERS.include?(symbol)
end
lookup_unique_name(label, labels_in_scope) click to toggle source

return the unique name for the given label (i.e., local name). Raises an InvalidSpecException if label is not the local name of a label currently in scope. Raises an ArgumentError if (1) label is not a symbol, or (2) there are multiple labels with the same name currently in scope.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 83
def self.lookup_unique_name(label, labels_in_scope)
  raise ArgumentError.new("label must be a symbol") unless label.is_a?(Symbol)

  matching_label = labels_in_scope.select { |dl| dl.local_name == label }
  if (matching_label.size == 0)
    raise InvalidSpecException.new("There is no label \"#{label.to_s}\" in this scope.")
  elsif (matching_label.size == 1)
    matching_label.first.unique_name.to_s
  else
    raise ArgumentError.new("TestFactoryHelper.set_address was passed a data label list" +
                                "that contained duplicate labels. ")
  end
end
print_lw(output, target, base, offset, size) click to toggle source

generates an assembly lw (load word) instruction. This method generates the appropriate lw pseudo-instruction based on the type of base. If base is a String (which is assumed to be a label) then an instruction of the form “lw target, base+offset” is generated. If base is a Symbol (which is assumed to be a register), an instruction of the form +lw target, offset(base)+ is generated.

  • Warning: In each case, the resulting machine instructions are likely to use $at.

  • Note: Symbols are always assumed to be registers. This method does not look up local label names. When using local labels, you must call ::lookup_unique_name before calling print_lw.

process_expected_memory_value(value, base, current_offset, current_increment, factory, test_name, output) click to toggle source

This is a helper method for ::verify_memory_array that processes a single element of an array containing expected values.

  • If value is a String or Integer, verify that the memory location under test (base + current_offset) is equal to value.

  • If value is a assembler directive (e.g., “+:word”, “:byte”, “:ascii”), return the corresponding data size.

current_increment is the size of the datum under test (i.e., the amount by which the offset should be increased to reach the next value)

This method returns two values:

  • the offset of the next element to be examined, and

  • the current increment (which changes if value corresponds to an assembler directive)

# File lib/mipsunit/mspec/test_factory_helper.rb, line 265
def self.process_expected_memory_value(value, base, current_offset, current_increment, factory, test_name,
    output)
  if value.is_a?(Symbol)
    #TODO: properly align data (e.g., :word should move to next 4-byte boundary)
    return [current_offset, DataLabel.size_of(value)]
  else
    current_increment = 1 if (value.is_a?(String))
    # TODO:  When the input is a character, the failure message will print the
    # ASCII value, not the character.  This could lead to confusing error messages.
    int_value = get_integer_value(value)

    if (int_value > -2**15 && int_value <= 2**15)
      print_lw(output, :at, base, current_offset, current_increment)
      output.puts "addi $at, $at, #{-int_value}"

      # TODO Have the failure message print the local name, not the global name.
      fail_unless("beq $at, $zero", test_name, "Memory location\\n#{base}[#{current_offset}]", int_value,
                  factory, output) do
        print_lw(output, :a3, base, current_offset, current_increment)
      end
    else # end if value is 16 bits or less
      print_lw(output, :at, base, current_offset, current_increment)
      output.puts "xori $at, $at, #{int_value & 0x0000ffff}"
      output.puts "sll $at, $at, 16"

      # TODO Have the failure message print the local name, not the global name.
      fail_unless("beq $at, $zero", test_name, "Memory location\\n#{base}[#{current_offset}]", int_value,
                  factory, output) do
        print_lw(output, :a3, base, current_offset, current_increment)
      end
      print_lw(output, :at, base, current_offset, current_increment)
      output.puts "srl $at, $at, 16"
      output.puts "xori $at, $at, #{(int_value >> 16)}"


      # TODO Have the failure message print the local name, not the global name.
      fail_unless("beq $at, $zero", test_name, "Memory location\\n#{base}[#{current_offset}]", int_value,
                  factory, output) do
        print_lw(output, :a3, base, current_offset, current_increment)
      end
    end # if value is 16 bits or more
    [current_offset + current_increment, current_increment]
  end # else
end
register_to_string(register) click to toggle source
# File lib/mipsunit/mspec/test_factory_helper.rb, line 41
def self.register_to_string(register)
  validate_register(register)
  reg_string = register.to_s
  if reg_string =~ /^r(\d+)$/
    "$#{$1}"
  else
    "$#{reg_string}"
  end
end
remove_warning_from_data_method(factory) click to toggle source

replaces the initial implementation of MIPSUnit::MSpec::TestFactory#data with one that does not print a warning.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 53
def self.remove_warning_from_data_method(factory)
  def factory.data(hash)
    data!(hash)
  end
end
set_address(register, label, output, data_labels) click to toggle source

produce a line of assembly to set register to the unique label corresponding to label This method will raise an InvalidSpecException if register is not a valid register, or if label is not a valid label.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 150
def self.set_address(register, label, output, data_labels)
  validate_register(register)
  output.puts "la #{register_to_string(register)}, #{get_label_string(label, data_labels)}"
end
set_boolean(register, value, output) click to toggle source

produce a line of assembly to set register to value. This method will raise an InvalidSpecException if register is not a valid register, or if value is not true or false.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 137
def self.set_boolean(register, value, output)
  validate_register(register)

  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
    set_integer(register, value ? 1 : 0, output)
  else
    raise InvalidSpecException.new("#{value} is not a valid boolean variable.")
  end
end
set_integer(register, value, output) click to toggle source

produce a line of assembly to set register to value. This method will raise an InvalidSpecException if register is not a valid register, or if value is not an integer. The method will also produce a warning on $stderr if the given value is outside the range of a 32-bit integer.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 120
def self.set_integer(register, value, output)
  validate_register(register)

  if !value.is_a? Integer
    raise InvalidSpecException.new("#{value} is not an integer.")
  end

  if value > 0xFFFFFFFF || value < -0x80000000
    $stderr.puts "Warning!  Value #{value} is outside the 32-bit range for integers."
  end

  output.puts "li #{register_to_string(register)}, #{value}"
end
set_register(register, value, output, data_labels) click to toggle source

Write an assembly statement to output setting register to value. This method raises an InvalidSpecException if either

  • register does not refer to a valid register, or

  • value is not of a recognized type.

This method does not accept String values.

# File lib/mipsunit/mspec/test_factory_helper.rb, line 160
def self.set_register(register, value, output, data_labels)
  if (value.is_a?(TrueClass) || value.is_a?(FalseClass))
    set_boolean(register, value, output)
  elsif value.is_a?(Integer)
    set_integer(register, value, output)
  elsif value.is_a?(Symbol) || value.is_a?(AssemblyLabel)
    set_address(register, value, output, data_labels)
  else
    raise InvalidSpecException.new("Cannot set a register to type #{value.class}.")
  end
end
validate_register(register) click to toggle source

raises an InvalidSpecException if register is not a valid register

# File lib/mipsunit/mspec/test_factory_helper.rb, line 35
def self.validate_register(register)
  if !REGISTERS.include?(register)
    raise InvalidSpecException.new("#{register} is not a valid register.")
  end
end
verify_memory_array(base, values, factory, test_name, labels_in_scope, output) click to toggle source

Verify that the memory beginning at the given label/register matches the sequence of values given

# File lib/mipsunit/mspec/test_factory_helper.rb, line 312
def self.verify_memory_array(base, values, factory, test_name, labels_in_scope, output)
  if (is_register?(base))
    label_to_use = base
  else
    label_to_use = get_label_string(base, labels_in_scope)
  end

  current_offset = 0
  current_increment = DataLabel.size_of(:word)

  # TODO: Refactor this out into a separate method
  # (And update the tests as well)
  # replace Strings with the sequence of individual letters
  previous_string_type = nil
  values = values.map do |item|
    previous_string_type = item if (item == :ascii || item == :asciiz)
    if item.is_a?(String)
      if previous_string_type.nil?
        raise InvalidSpecException.new("String values must be tagged as .ascii or .asciiz.")
      elsif previous_string_type == :asciiz
        item.chars.to_a + [0]
      else
        item.chars.to_a
      end
    else
      item
    end
  end
  values.flatten!

  values.each do |value|
    (current_offset, current_increment) = process_expected_memory_value(value, label_to_use, current_offset,
                                                                        current_increment, factory,
                                                                        test_name, output)
  end
end
verify_memory_memory(expected_base, observed_base, factory, labels_in_scope, output) click to toggle source

Verify that the memory beginning at expected_base matches the memory at observed_base

# File lib/mipsunit/mspec/test_factory_helper.rb, line 350
def self.verify_memory_memory(expected_base, observed_base, factory, labels_in_scope, output)
  # problem:  Need two registers (one for each value loaded)  :at can be one.  No good choice for the second.
end