phanrahan / magma

magma circuits
Other
253 stars 24 forks source link

Type of IOs appear flipped #1326

Closed rkshthrmsh closed 8 months ago

rkshthrmsh commented 1 year ago

Why do the types of IOs appear flipped (In instead of Out and vice versa):

class Test1(m.Circuit):
    io = m.IO(
        A=m.In(m.SInt[2]),
        O=m.Out(m.SInt[2]),
    )

    # -- snip --
    print(type(io.A)) # shows Out(SInt[2])
    print(type(io.O)) # shows In(SInt[2])
rsetaluri commented 1 year ago

This is bit of a confusing point, but basically an "output" is something that can drive something else, and an "input" is something that can be driven. So if you think about the definition of a module, what we typically think of as input ports in Verilog are really Out in magma because they drive other things. Similarly an output in Verilog is In in magma because it is driven by something else. So in actuality the declaration of the io is what's flipped (i.e. m.IO), with respect to the actual types.

And if you look at the types of ports of an instance vs. ports of a module definition, those are also flipped -- matching the declaration. So if you might observe the following, given your definition of Test1:

type(Test1.A)  # --> Out()

class Top(m.Circuit):
    Test1_inst = Test1()
    type(Test1_inst.A)  # --> In()
rkshthrmsh commented 1 year ago

Hi @rsetaluri, thank you for the explanation. The way I understand it is, the direction types are defined such that they appear correct when a module is instantiated. However, it is common in verilog to have the module definition itself be the circuit that is synthesized, without instantiation. For example, Test1 could be implemented without a Top, and in fact, any Top is also an example of a module that is implemented as defined without an instance. In these cases, the port directions may still need to be reconciled?

leonardt commented 1 year ago

The use of directions is consistent with how it's handled in verilog. Consider a full adder:

module full_adder (
    input A,
    input B,
    input C,
    output O0,
    output O1
);
assign O0 = (A ^ B) ^ C;
assign O1 = ((A & B) | (B & C)) | (C & A);
endmodule

In this case, A, B, and C are labeled input, but in the context of the definition they are values that are read (so from the point of view of our wiring, the circuit is wiring the output of the port A to the input of an xor instance). Similarly, O0 is an output port, but it is a value that is driven (so the output of the logic circuit is wired to the input of the port).

This difference in "view" of the ports depending on the context holds even if full_adder is the top circuit meant to be synthesized (i.e. in verilog you don't change the meaning of input/output in interfaces if the module is top or not, it is consistent regardless of where you are in the hierarchy).