-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Moore] Improve declaration and instantiation of SVModule operation #7019
Conversation
It is still in testing and developing. The SVModuleOp will be pushed later. |
return {}; | ||
switch (symbol->kind) { | ||
case slang::ast::SymbolKind::Port: | ||
case slang::ast::SymbolKind::MultiPort: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MultiPort code was not included. The logic is added into creating SVModuleOp.
The test of InstnaceOp / unconnectedOp / outputOp / SVModuleOp will be added soon after finishing SVModuleOp. |
eacbf5d
to
a777ac4
Compare
Variadic<AnyType>:$inputs, | ||
Variadic<AnyType>:$outputs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to split inputs and outputs? It looks in Moore we pass even output values to an instance op so I feel it's easier to merge them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the Moore InstanceOp does not have results. Distinguish the inputs and outputs will be beneficial to the next lowering procedure. Unless we add DirectionAttr into components of the op.
The example for instanceOp & UnconnectedOp: module sub(input a, output b);
endmodule
module top;
wire i;
sub sub (.a(), .b(i));
endmodule To generate: module {
......
moore.module @top {
%i = moore.net wire : !moore.logic
%0 = moore.unconnected In : !moore.logic
moore.instance "sub" @sub(a: %0: !moore.logic) -> (b: %i: !moore.logic)
}
} |
HasParent<"SVModuleOp">, | ||
AttrSizedOperandSegments, | ||
DeclareOpInterfaceMethods<SymbolUserOpInterface>, | ||
// DeclareOpInterfaceMethods<InstanceGraphInstanceOpInterface> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing but have problems
This is interesting. You've mentioned that the updated I'm wondering if instances then shouldn't also follow this pattern and mimic what HW does: have a list of operands for all inputs, and a list of results for the outputs. At the moment, the outputs are operands of the instance op. I agree that this matches SV by requiring you to attach a wire to that output in a sense, but it feels like it breaks the pattern established by For example, how could we express the following snippet of Verilog? module Foo(input int x, output int y);
assign y = x*2;
wire int z = y;
endmodule
int a;
Foo foo(.x(42), .y(a)); Using a Terminator and Instance Results 😃One approach would be moore.module @Foo(in %x: !moore.i32, out y: !moore.i32) {
%y = moore.variable : !moore.i32 // module defines no %y SSA value
%0 = moore.constant 2 : !moore.i32
%1 = moore.mul %x, %0 : !moore.i32
moore.assign %y, %1 : !moore.i32 // assign y = x*2
%z = moore.net wire %y : !moore.i32 // wire int z = y
moore.output %y : !moore.i32 // use value of var "y" for output "y"
} Correspondingly, the instance of Foo should then probably also just accept a bunch of SSA values as input operands, and define a bunch of SSA values as results for its outputs. This means that you cannot just connect a wire/variable to an output port, but instead requires you to use an explicit connect. I think that's a good thing again, and if I remember correctly the Slang AST already contains this assignment. %a = moore.variable : !moore.i32
%0 = moore.constant 42 : !moore.i32
%1 = moore.instance "foo" @Foo(x: %0: !moore.i32) -> (y: !moore.i32)
moore.assign %a, %1 // assigns output "y" to var "a", making the connection I really do like this approach. It makes module and instance ports a very lightweight concept in the IR: it's only about transporting an SSA value into the module or out of the module. There are no implicit wires, variables, or connections happing here. All connections are explicitly done as Using Block Arguments for Outputs 😞Another approach would be to make moore.module @Foo(in %x: !moore.i32, out %y: !moore.i32) {
%0 = moore.constant 2 : !moore.i32
%1 = moore.mul %x, %0 : !moore.i32
moore.assign %y, %1 : !moore.i32 // is %y a net or var?
%z = moore.net wire %y : !moore.i32
} Instances should probably reflect this pattern and carry all ports as a single list of operands. They would also have to encode the direction of each port, and whether the ports are understood to be wires or nets, and if they contain implicit buffers/drivers that decouple the wires inside the module from the wires attached to the instance. %a = moore.variable : !moore.i32
%0 = moore.constant 42 : !moore.i32
moore.instance "foo" @Foo(in x: %0: !moore.i32, out y: %a: !moore.i32) I'm not a big fan of this approach, because it makes ports a very clunky and heavyweight concept in the IR. They define a mapping from instance ports to module ports, but also act as net/var declarations, and they imply assignments and connections. I'd much rather have us define ports as a mapping between SSA values, and then sort out any SystemVerilog connectivity and semantics via explicit IR ops in the module and the instance. What do you think @uenoku, @cepheus69, @hailongSun2000, @maerhart? |
For For {
"port": "2199025174904 y",
"expr": {
"kind": "Assignment",
"type": "int",
"left": {
"kind": "NamedValue",
"type": "int",
"symbol": "2199025172832 a"
},
"right": {
"kind": "EmptyArgument",
"type": "int"
},
"isNonBlocking": false
}
} You're totally right! We can capture the output I agree with Using a Terminator and Instance Results. |
Agree with the first design. The reason why we should use a terminator with net / variable declaration instead of block arguments: We should not eliminate variable/net logic in the Moore layer. It will make the conversion procedure more complicated. Removing the logic too early will cause problems in analyzing and transforming passes at the Moore level. This design actually minimizes the coupling of SVModuleOp (initially portOp) to the net / variable logic. because of preserving the net / variable logic, instanceOp has no result but attaches the original logic in the SV module via net / variable operation, etc. |
@fabianschuiki I agree with you and am in favor of the terminator and instance results approach since it is easier to analyze and optimize. We need to be sure that we can model SV semantics with that approach, though. I'm not familiar enough with all the details in the standard to tell that there isn't a "feature" that could have some issues with this way of modeling things (but I'd guess most likely not, I think they are equally powerful in expressiveness). Obviously, we can't get rid of connect semantics entirely and modeling instances this way means we push the variable/net declaration to the leaf modules and pass them bottom-up instead of top-down, which makes sense since it minimizes the number of wire connects we need to model in a typical design. Some pseudocode as an illustration (let me know if there are any conceptional issues with it): module A(output a);
read a
drive a
endmodule
module B(output b);
inst a1 A(b)
inst a2 A(b)
read b
endmodule
module C(input c);
read c
endmodule
module top();
wire d
inst b B(d)
inst c C(d)
endmodule moore.module @A(out a) {
%0 = moore.variable
read %0
drive %0
moore.output %0
}
moore.module @B(out b) {
%0 = moore.instance @A()
%1 = moore.instance @A()
%2 = connect %0, %1
read %2
moore.output %2
}
moore.module @top() {
%0 = moore.instance @B()
moore.instance @C(%0)
moore.output
}
Do you have a reference to the part in the standard that talks about the implicit buffers and drivers? Why is this not a concern in the other approach as well? They seem equivalent to me w.r.t that. |
I suddenly remember a case--BCD_Adder--it also exists the wire connections like
|
It looks like: moore.module @A(out a : !moore.xxxx) {
%a = moore.variable "a" : !moore.xxxx
moore.output %a : !moore.xxxx
}
moore.module @B(out b : !moore.xxxx) {
%b = moore.net wire : !moore.xxxx
moore.instance "xxxx" @A() -> (a: %b: !moore.xxxx)
moore.instance "xxxx" @A() -> (a: %b: !moore.xxxx)
moore.output %b : !moore.xxxx
}
moore.module @top() {
%0 = moore.net wire : !moore.xxxx
moore.instance "xxxx" @B(c: %0: !moore.xxxx) -> ()
moore.instance "xxxx" @C(c: %0: !moore.xxxx) -> ()
moore.output
} It indeed needs var or net to express the semantics of the connection. |
I was thinking about the convoluted rules in "23.2.2.3 Rules for determining port kind, data type, and direction". There's a distinction between wire and variable ports. If I recall correctly, variable ports imply a buffer that decouples the module internals from the outside. If you have a wire input port, the wire connected to the instance and the wire inside the module will be the same object in the event queue, and if you force a value onto that input inside the module (why you'd do that I don't know 😬), that value would also show up at the instantiation site. If you have a variable input port however, there is an implied buffer gate that reads the value of the wire connected to the instance and drives it onto the variable inside the module. But the two are distinct entities; if you force the variable port inside the module, the buffer prevents that force from propagating to the instance site. You're totally right, this is a concern in both approaches. But if we use instance results and a terminator op we'd explicitly create these buffers in the IR. If the ports were all wire-style operands, these subtle differences between port types/kinds would be part of |
@maerhart I think in your example we'd create explicit variable/net ops in the IR to represent the ports (and whether the ports are variables or nets). And then have explicit assigns at the instantiation sites to capture how the values coming out of the instance as results get driven onto the connected variables/nets. The instance only copies the value out of the module body, and the details of the connects done by the instance are handled next to the instance explicitly: moore.module @A(out a) {
%0 = moore.variable
read %0
drive %0
moore.output %0
}
moore.module @B(out b) {
%0 = moore.variable
%1 = moore.instance @A()
%2 = moore.instance @A()
moore.assign %0, %1 // multi-driver -- resolved in a pass?
moore.assign %0, %2 // multi-driver -- resolved in a pass?
read %0
moore.output %0
}
moore.module @top() {
%0 = moore.variable
%1 = moore.instance @B()
moore.assign %0, %1
moore.instance @C(%1)
moore.output
} Your example is great because it also contains a multiply-driven variable in module "@b". Having the assignments explicitly in the IR is pretty cool because we could push the handling of multiple drivers on variables and nets into its own pass. Mem2reg could then get rid of quite a few of the explicit variables and nets that we insert for instances. |
@hailongSun2000 In your example, does the Slang AST contain assignment expressions for things like
If that's the case, this would pretty trivially expand to an IR like: %Sum = moore.variable : l4
%w1 = moore.net wire : l1
// ...
%fa1.Sum, %fa1.Cout = moore.instance "fa1" @FullAdder (
A: %a0: l1, B: %b0: l1, Cin: %Cin: l1) -> (Sum: l1, Cout: l1)
%0 = moore.extract %Sum, 0 : l4 // Sum[0]
moore.assign %0, %fa1.Sum : l4 // .Sum(Sum[0])
moore.assign %w1, %fa1.Cout : l4 // .Cout(w1)
// ... |
@fabianschuiki. I'll show you the related Slang AST tomorrow(I have off today). But I can promise that we can capture the |
Here is the related AST: {
"port": "2199025212832 Sum",
"expr": {
"kind": "Assignment",
"type": "logic",
"left": {
"kind": "ElementSelect",
"type": "logic",
"value": {
"kind": "NamedValue",
"type": "logic[3:0]",
"symbol": "2199025203920 Sum"
},
"selector": {
"kind": "IntegerLiteral",
"type": "int",
"value": "0",
"constant": "0"
}
},
"right": {
"kind": "EmptyArgument",
"type": "logic"
},
"isNonBlocking": false
}
}, We can capture the
|
I think you're right, at some point we'll probably need to add a concatenation to have a single |
That's very exciting! It looks like Slang already has all these assignments that we need if instances simply move values in and out of modules. Feels like we're onto a nice design here 😃 |
The current SVModuleOp does not provide port lists and instanceOp can not establish a connection between SV modules. They completely do not meet growing demand in Moore (
circt-verilog
frontend) dialect. This PR bring newly improved SVModuleOp and instanceOp. By mature parser and printer provided by HW dialect. We could provide more better IR on basis of those functions. New functions around new operation has been added. Additionally, UnconnectedOp and OutputOp has been added into Moore dialect. TheUnconnectedOp
denotes unconnected behavior in circuit design. It provides better strategy encountering such a case. (The design refers to IEEE 1800 - 2017 § 23.3.3 Port connection rules) TheOutputOp
serves as terminator of new SVModuleOp. The original portOp was combined into new SVModuleOp. In this new procedure, basic systemVerilog code could be translated into Moore IR.