Named tuple problem

Hi all !

I’m trying for find a solution to shorten my test suite, even slightly

I’ve this function

def check_inst(insts, data)
    data.each do |k, v|
      assert_equal v[:opcode], insts[k].opcode
      v[:operands].each_with_index do |operand, index|
        case operand[:klass]
        when NumberLiteral
          puts "here"
          # assert_equal operand[:value], insts[k].operands[index].value.value
        when Ident
          puts "yo"
        when IR::Register
          puts "reg"
        else
          puts operand[:klass]
        end
      end
    end
  end

That I call this way

exp = {
      0 => {
        opcode:   IR::OpCode::MUL,
        operands: {
          {klass: IR::Register},
          {klass: NumberLiteral, value: 2},
          {klass: NumberLiteral, value: 8},
        },
      },
      1 => {
        opcode:   IR::OpCode::MOV,
        operands: {
          {klass: IR::Register},
          {klass: Ident, name: "b", indiced_by: !nil},
        },
      },
    }
    check_inst(get_insts(opcodes), exp)

The case/when always ends in the else block

When I print out the content of operand[:klass] it appears to be ok, so why the right case is not executed ?

Basically I’m comparing a class names with class names

Ok, got it with case/in with .class added

def check_inst(insts, data)
    data.each_with_index do |v, i1|
      assert_equal v[:opcode], insts[i1].opcode
      v[:operands].each_with_index do |operand, index|
        case operand[:klass]
        in NumberLiteral.class
          puts "here"
          # assert_equal operand[:value], insts[k].operands[index].value.value
        in Ident.class
          puts "yo"
        in IR::Register.class
          puts "reg"
          # else
          #  puts typeof(operand[:klass])
        end
      end
    end
  end
exp = [
      {
        opcode:   IR::OpCode::MUL,
        operands: [
          {klass: IR::Register},
          {klass: NumberLiteral, value: 2},
          {klass: NumberLiteral, value: 8},
        ],
      },
      {
        opcode:   IR::OpCode::MOV,
        operands: [
          {klass: IR::Register},
          {klass: Ident, name: "b", indiced_by: !nil},
        ],
      },
    ]

is there a reason you’re wanting to use named tuples to model your data vs actual types like a struct?

But now the code won’t compile if I do

in NumberLiteral.class
   assert_equal operand[:value], insts[i1].operands[index].value.value

Error: missing key 'value' for named tuple NamedTuple(klass: IR::Register.class)

I’m in the NumberLiteral.class when/in and operand have a :value in the NamedTuple and the compiler complains about IR::Register

I’m lost, once again ;-)

I don’t know, but NameTuple is a kind of polymorphic container (each Tuple can have its own set of keys => values) - How can I do this with structs ?

For what I know, structs are like classes but passed by copy instead of ref/pointer

I fixed a typo in the operands key of named tuple, it’s now a array but still won’t compile

By defining actual types to represent the structure of things. E.g.your exp array could be something like:

record Operand, klass : BaseOperand.class, value : Int32? = nil, name : String? = nil, indiced_by : Bool = true
record Expression, opcode : IR::OpCode, operands : Array(Operand)

exp = [
  Expression.new(:mul, [
    Operand.new(IR::Register),
    Operand.new(NumberLiteral, value: 2),
    Operand.new(NumberLiteral, value: 8),
  ]),
  Expression.new(:mov, [
    Operand.new(IR::Register),
    Operand.new(Ident, name: "b"),
  ]),
]

Assuming all Operand klass types inherit from some abstract base type. Otherwise could maybe use generics like record Operand(K), klass : K.

Provides much more flexibility since you can define additional methods and such on the structs.

Mmm interesting !

No my Operand classe does not inherit, it’s defined as

class Operand
    getter value : (Register | Ident | NumberLiteral | Label)
    property sub_type : OperandSubType
    property indiced_by : Register | Ident | NumberLiteral | Nil

    def initialize(@value, @sub_type = OperandSubType::NONE)
      @indiced_by = nil
    end
   # skip rest of code
end

And there’s one class that uses the Operand class, it’s Inst and defined as

class Inst
    getter opcode : OpCode
    getter operands : Array(Operand)
   # skip
end

And an enum

enum OperandSubType
    NONE
    ADDR_OF
    DEREF
end

Basically this is what I have to test when some source code goes to Tokenize → Parser → IR
And what I’m talking about is testing the IR, which is very very tedious, so my search to code some helper methods to shorten the test code

But one last question

with
record Operand, klass : BaseOperand.class, value : Int32? = nil, name : String? = nil, indiced_by : Bool = true

I might have some other parameters to pass, but will the record Operand allow some parameters and not others ?
Like in

Operand.new(NumberLiteral, name: "foo")

I presume it’s okay because the Record uses named parameters, so if I’m not wrong all default values will be used except for the named parameters really used

If you give a default value to the optional parameters, then yea, you just provide the ones you want via named args.

I’m a bit stubborn (often), and by trial and error I found a solution with the named tuples which is converting the tuple to an Hash (this is a draft far from being bullet proof)

def check_inst(insts, data)
    data.each_with_index do |v, i1|
      assert_equal v[:opcode], insts[i1].opcode
      v[:operands].each_with_index do |o, i2|
        expected = o.to_h
        actual = insts[i1].operands[i2]
        case expected[:klass]
        when IR::Register.class
          # Add  asserts here based on presence of keys in expected
        when NumberLiteral.class
          assert_equal expected[:value], actual.value.as(NumberLiteral).value
          # Add more asserts here based on presence of keys in expected
        when Ident.class
          assert_equal expected[:name], actual.value.as(Ident).name
          assert_equal expected[:sub_type], actual.sub_type if expected.has_key?(:sub_type)
          # Add more asserts here based on presence of keys in expected
        else
          raise "WTF!" # raise something here
        end
      end
    end
  end
exp = [
      {
        opcode:   IR::OpCode::MUL,
        operands: [
          {klass: IR::Register},
          {klass: NumberLiteral, value: 2},
          {klass: NumberLiteral, value: 8},
        ],
      },
      {
        opcode:   IR::OpCode::MOV,
        operands: [
          {klass: IR::Register},
          {klass: Ident, name: "b", indiced_by: !nil, sub_type: IR::OperandSubType::ADDR_OF},
        ],
      },
      {
        opcode:   IR::OpCode::MOV,
        operands: [
          {klass: Ident, name: "c"},
          {klass: IR::Register},
        ],
      },
    ]
    pp opcodes
    check_inst(get_insts(opcodes), exp)

Tomorrow I’ll try to find time to code the same logic using Records and compare the pros and cons of both (balance verbosity vs readabilty)

The scenario above is bit more simple than what I need to test in reality

Thanks for the hints !