-
Notifications
You must be signed in to change notification settings - Fork 9
Consider redesigning how Module
(hierarchies) work
#23
Comments
I just realized, considering moving signal bit widths into the type system (#13), it fits really nicely with the proposed |
@yupferris I think I can get something like this to work: |
The generic S could also just be a non-generic struct (like Signal) but the current signal would not work because of all of the lifetime arguments. Module definitions would become overly verbose. In fact, with this shorthand S I kind of like it |
The only thing I'm not sure about is the fact that previously you could name inputs and outputs. You can't really name type arguments. If we want to keep naming, the only solution I can think of would be adding a method to Module which returns the names of all signals (which I hate). Macros could help with that but still. For example, a macro like this: impl<S: Signal> Module<S> for And {
inputs!("i1", "i2");
} which would expand to something like: type Inputs = (S, S);
fn inputs(&self) -> {("i1", "i2")} but I don't like that much The naming of the module itself is less of a problem, as we can get the name of the struct at runtime (https://doc.rust-lang.org/std/any/fn.type_name.html) |
Thanks for the feedback. I have to be honest, though - I have no idea what you're talking about/suggesting, or how to interpret your feedback! It may not have been written clearly above in my original wall of text, especially with a bunch of unresolved issues outlined (this was all written before I actually started on the implementation), so I can understand that it may have been easy to gravitate towards that, and for that I apologize. I'll attempt to clarify a bit: The TL;DR is that the current approach and syntax for declaring and instantiating new What I'm specifically after is:
While I understand that these questions are inherently tied to what's actually implementable within Rust, I would appreciate it if the feedback were more general, perhaps even assuming an ideal implementation that couldn't necessarily exist. I already have implementations underway on this branch where I'm working through potential issues and changing things around where it makes sense. I'm not interested and explicitly do not want help/collaboration for the concrete details; it's something I've been working on for some time and need to continue doing on my own. |
I see. I haven't been too clear either. My comments specifically answer point 2: alternatives. I think it might be possible to make modules be structs which implements the a "Module" trait. The reasoning behind that is that when modules become structs, instances of modules become instances of struct structs in rust. In my example of "and", I have an However, about your first and last point: I think the new way is still quite verbose and not that much better than the old way. What might help it is if names are automatically assigned by default (though names can still be optionally given). When you need to refer to a specific input. In many cases the names add a lot of verbosity while you can just work with references to that input. An example: does struct Test<'a> {
pub i1: &'a Input<'a>,
pub i2: &'a Input<'a>,
pub i3: &'a Input<'a>,
pub i4: &'a Input<'a>,
pub o: &'a Output<'a>,
}
impl<'a> Test<'a> {
// TODO: Optionally make this a submodule
// TODO: How do we get the inner module again in order to generate code?
pub fn new(c: &'a Context<'a>) -> Test<'a> {
let m = c.module();
let i1 = m.input(32);
let i2 = m.input(32);
let i3 = m.input(32);
let i4 = m.input(32);
let inner1 = TestInner::new(c);
inner1.i1.drive(i1);
inner1.i2.drive(i2);
let inner2 = TestInner::new(c);
inner2.i1.drive(i3);
inner2.i2.drive(i4);
let inner3 = TestInner::new(c);
inner3.i1.drive(inner1.o);
inner3.i2.drive(inner2.o);
let o = m.output("o", inner3.o);
Test {
i1,
i2,
i3,
i4,
o,
}
}
}
struct TestInner<'a> {
pub i1: &'a Input<'a>,
pub i2: &'a Input<'a>,
pub o: &'a Output<'a>,
}
impl<'a> TestInner<'a> {
// TODO: Optionally make this a submodule
// TODO: How do we get the inner module again in order to generate code?
pub fn new(c: &'a Context<'a>) -> TestInner<'a> {
let m = c.module();
let i1 = m.input(32);
let i2 = m.input(32);
let o = m.output(i1 & i2);
TestInner {
i1,
i2,
o,
}
}
} Optional names could be added with a sort of builder pattern like this: let i1 = m.input(32).with_name("i1") Generating names could be done using a scheme (for example, call inputs "i1", "i2", "i3", etc.). I do not entirely get your TODO about "making this a submodule". If it means you want to make a distinction between modules and submodules I must say I don't think that's a good idea. I do think it makes sense to generate code for all top level modules as you call it. In fact, I think you could just generate code for every module that is added to the context. Or is that something you don't want? About macros: I too am quite hesitant about them. I especially don't like that getting error reporting right in them is so damn hard and if you don't have nice error reporting in them it's a pain to work with them as you also don't get autocomplete with them in most/all editors. However, I think normal macros can already help a lot, and it's much easier to work with those. But more generally, I do think working with structs like this is a good way to go forward. It's definitely much better than what's there right now. |
Apologies for not writing back about this until now, but I've struggled to be able to make time to internalize what you've written and respond properly while prioritizing IRL things and the projects I'd like to use kaze for 🙂 . At this point, I'd like to finalize this work as it is today. Though it's not 1:1 with what we've discussed a bit above, and design-wise I wouldn't consider it complete, I believe it's a step in the right direction, and I need to move on to be able to properly work on other things on top of it (including your other pull requests). Anyways, to address some specific points and clarify some things:
The first thing I gather from this (which you also mention a bit later) is that representing instances of modules as instance of Rust As for doing this via implementing a I would really like inputs/outputs to be expressed as fields on a given module Additionally (and this paragraph also answers your "does an output really need a name?" question), a very large amount of time developing a kaze module is spent looking at trace output, and having proper names to refer to all ports (and registers, and potentially other signals as well, though those should probably be opt-in with a
Right, what I mean by this is there needs to be a way for a module instance to be declared as an instance inside of another module that kaze has knowledge of. This is mostly for codegen, but it's also for error checking. If kaze The way I've handled this currently is that I've introduced a There is still a distinction between top-level and other modules though, but this doesn't exist in the module code itself; it only exists at codegen time. kaze needs to know which modules to generate. For verilog, kaze has previously gotten away with just generating all modules, and since kaze instances mapped 1:1 with verilog instances, the verilog compiler would later be able to put the pieces back together. For sim, this is different, as the module hierarchy is flattened (to simplify how the simulator works), so a user must specify which modules to actually generate simulators for, and all of the logic in child modules is included in that simulator. With the suggestion above, the verilog output will work in a similar manner, which is more symmetric/easier to use (and doesn't require some hacks that I previously added to be able to iterate over all of the modules in a context in order to actually generate everything for the verilog case). Anyways, in light of all of that, one thing is for sure:
I definitely agree with the bit about verbosity; it's really unfortunate/annoying. Common things like making registers is not terrible (it's better than the equivalent verilog in most cases), but creating a module (or even just a port) is too much hassle. I do think it's important to note that with the new way, it's easier to build ad-hoc abstractions. For example, one can now collect a bunch of The more I think about it, the more I think I might want a module (proc) macro to declare a module struct that desugars to something similar to what I've implemented that's similar to the above. This could handle repeating port names and some plumbing details gracefully, while users who are allergic to macros still don't have to use it if they don't want to (in fact, they can opt out of the 1:1 Some other loose ends:
This I was unaware of, thanks for bringing it to my attention! I think I'll definitely want to use this. Another thing that bugs me when working with ad-hoc abstractions, and in particular the "meta-port" example above: It's actually annoying that this only works for inputs/outputs, because it forces these abstractions to only work at module boundaries, and sometimes it might be nice to use them entirely within a module as well. This requires something like verilog's Anyways, I'm going to work on getting what I have on |
Also, just to have an example built with what I currently have, here's an LFSR: use kaze::*;
pub struct Lfsr<'a> {
pub m: &'a Module<'a>,
pub shift_enable: &'a Input<'a>,
pub value: &'a Output<'a>,
}
impl<'a> Lfsr<'a> {
pub fn new(instance_name: impl Into<String>, p: &'a impl ModuleParent<'a>) -> Lfsr<'a> {
let m = p.module(instance_name, "Lfsr");
let shift_enable = m.input("shift_enable", 1);
let state = m.reg("state", 16);
state.default_value(0xace1u32);
let value = m.output("value", state.bits(7, 0));
state.drive_next(if_(shift_enable, {
let feedback_bit = state.bit(0) ^ state.bit(2) ^ state.bit(3) ^ state.bit(5);
feedback_bit.concat(state.bits(15, 1))
}).else_({
state
}));
Lfsr {
m,
shift_enable,
value,
}
}
} |
Has the I have to admit, I'm not the familiar with Rust, so I may just be doing something incorrectly. |
It's implemented on master, which can be used from cargo but not in a proper release, as I still haven't gone back and fixed tests, added docs, etc. Not sure when I'll ever get around to this beyond "eventually". That said, I don't have any immediate plans to roll back these changes or anything like that, and in general the xenowing code should be considered the closest thing to "best practices" as kaze has. |
Sketch/braindump:
Currently, a module hierarchy in kaze might look like this (taken/adapted from cases in sim tests):
This fn creates a simple inner module ("TestInner") instantiated 3 times inside the top-level module ("Test").
While this is easy enough to understand, the code itself has a few problems:
Additionally, there are some more general loose ends with the current pattern for creating/instantiating modules:
Module
at the end? This makes it easy to generate code for the top-level module, but a more common case actually is just to call a module generation function, ignore the return value, and then instantiate the module after (by string name, not the reference we just got back!), as is done forTestInner
above. Feels a bit janky to say the least.Module
that's been instantiated in their design, but other than providing a way to iterate over allModule
s in aContext
(which is a hack) there's no useful way to query which modules are instantiated (so we don't necessarily know which modules are/aren't important and the only conservative thing is to always generate all modules).Module
s with hand-generated mangled names. If we're already mangling names, why not try to make this automatic somehow and save some typing/decision making?Generally, module generation/instantiation feels a bit half-baked, and I'd really like to improve things. I feel it's quite important to have a good pattern for this - we want to encourage the use of several decoupled modules in designs, and if it's too hairy to do this meaningfully in a lot of cases, then users will tend to create larger modules with much more logic, which are harder to build and maintain (I find myself doing this more often than I would like).
One thought I've had after looking a bit at nMigen is for a
Module
to not represent a verilog module 1:1, but instead have it represent a specific instance (which is currently represented byInstance
in kaze). Then the convention might be to wrap eachModule
(instance!) in a wrapper struct that exposes the inputs/outputs asSignal
s (or relevant wrappers) as fields. This way, adding a new module into theContext
and instantiating it (which are very commonly done one right after another) are merged into one action. For codegen, we can make verilog and rust sim gen more symmetric by always specifying a top-level module and having kaze always output the whole hierarchy (with mangled names for inner modules in the verilog case now, and still flattening the graph for rust sim). This then greatly simplifies codegen for the verilog case, since we only write code to output top-level modules.Mockup:
Overall, I think this will make modules a bit more verbose (mostly vertically due to struct wrapping etc), but I think the verbosity adds clarity, and the code that actually constructs
Module
s by combiningSignal
s should get a bit lighter. Further, I think it's very useful to have instance signals exposed on a struct, as this could be extended by having other patterns - for example, we can group some signals with "bus port" structs, and add convenience functions to bind compatible bus ports to one another. Abstractions like this would be made ad-hoc and should reduce a lot of boilerplate in larger designs, especially where certain buses are used a lot (eg. xenowing). These abstractions can also potentially provide better semantic errors (eg. non-matching bus widths) before actually connecting signals (and deferring errors to lower-level kaze signal errors).It might also be possible/desirable to hide some of the boilerplate with proc macros, but I'm always hesitant about magic syntax like this. If this is a common pattern that works well it might be worth it tho.
Whatever pattern(s) we end up with, even if we support them with proc macros for syntactic convenience, I think it's crucial that they be easy to understand/expressible in plain code. Further, a user can choose to forego this pattern entirely!
Some unresolved issues include:
Elaboratable
I believe it's called), so the new module is the same object we'd generate code for, rather than a wrapper. Do we want to use traits/inheritance of some kind to mimic this? If we eventually move to an API where we separate unvalidated/validated graphs and we produce a validated graph by consuming an unvalidated graph (and transitively, all of the references to nodes in the graph, which includes modules!), how do we get ahold of the module(s) again in order to specify the top-level one(s) for codegen? Do we need to do that, or does it makes sense to "generate all top-level modules in this validated graph" always?Input
s transparently asSignal
s and as sinks forSignal
s depending on context? Does it make sense to doInto<&'a Signal<'a>>
everywhere instead of&'a Signal<'a>
directly? Can this extend toRegister
as well so we don't have to useRegister::value
(which I tend to forget a lot at least)?The text was updated successfully, but these errors were encountered: