Skip to content
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

Enumerations and methods on components #125

Open
rachitnigam opened this issue May 9, 2023 · 3 comments
Open

Enumerations and methods on components #125

rachitnigam opened this issue May 9, 2023 · 3 comments
Labels
C: language Component: the Filament language

Comments

@rachitnigam
Copy link
Member

rachitnigam commented May 9, 2023

Oftentimes, components can have different sets of behavior depending on the subset of the signals being assigned to. For example, a memory will often have two different interfaces: one for reads and one for writes

comp Mem(addr: 8, in: 32, read_en: 1, write_en: 1) -> (out: 32, write_done: 1);

This component has two interfaces:

  1. write_en tells the component that the data on the input needs to be written to the address.
  2. read_en tells the component that the user is trying to read data from an address.

In Filament land, we can write the following signature:

comp Mem<G: 1>(
  @interface[G] read_en: 1, 
  @interface[G] write_en: 1, 
  @[G, G+1] addr: 8, 
  @[G, G+1] in: 32
) -> (
  @[G+1, G+2] out: 32,
  @[G+2, G+3] write_done: 1
);

Of course, this gives you the usual goodness of knowing that writes take two cycles while reads take one cycle. However, the weird thing is that it has two interface ports: both read_en and write_en represent the event G. Additionally, only a subset of ports are meaningful. For example, when the memory is being used to perform a read, the out port will have a meaningful output while performing a write, it won't. Similarly, the in port doesn't require an input if we're performing a write.

Enumeration: Representing Method Calls

The solution proposed here is using enumerations (similar to Rust's enum) as a way to represent method calls. For example:

enum MemInterface {
  Read(@interface[G] read_en: 1) -> (@[G+1, G+2] out: 32),
  Write(@interface[G] write_en: 1, @[G, G+1] in: 32) -> (@[G+2, G+3] write_done: 1)
}

(We use the function-like port signature to represent the inputs and outputs related to the call)

Next, we can change the definition of the component to use this enum:

comp Mem<G: 1>(addr: 8, interface: MemInterface) -> (...interface.outs) {...}

(TK: Not sure what the best way to represent the output associated with the enum is here. Just using something dumb for now)

When using this component, you must explicitly construct the enumeration associated with the "method" you're trying to use: either a read or a write

comp main<G: 1>)(...) {
  M := new Mem;
  mw := M<G>(1 /*addr*/, MemInterface::Write { in: 15 }) // again, not the best syntax here
  out = mw.out;  // error because we called the Write method
}

This all gives us the usual goodness of Filament of checking that signals are available at the right times etc. However, this leaves out a big question.

A Matter of Delay

Delays are the heart of Filament's reasoning about pipelining. The challenge with enumerations is that different methods might have different affects on pipelining: do reads and writes use the same underlying circuits and therefore might affect each other's delay. The simple solution is just making the whole enum have the same delay which might be a deal breaker but perhaps something to start with.

The other question is how to represent methods at the source level? This just talks about external primitives but overall, we want people to implement their own methods. I think the solution to this second question will probably elucidate the answer for the first one.

@rachitnigam rachitnigam added this to the High-level interfaces milestone May 9, 2023
@rachitnigam rachitnigam added the C: language Component: the Filament language label May 9, 2023
@rachitnigam
Copy link
Member Author

Couple of related issues:

  1. Doing a good job with modules (A package manager and module system for Filament #118) becomes even more important now since the enumerations might be tied to the definition or usage of particular components. A pleasant experience with methods requires careful thought about namespacing and various related issues
  2. We need to make this work with existential quantified parameters (Existentially quantified parameters #121). This will allow us to implement components where we can say "the latency of read and write interface for this memory is not defined yet. We'll do some compiler magic once we have the whole design and figure out the right latency so that your design can run efficiently but the trade-off is that you need to write more abstract code". This related to how we're thinking about virtual operations in Calyx (Mapping logical memories to physical memories calyxir/calyx#1151 and mult_pipe not inferred correctly pipelined for DSP inference calyxir/calyx#1175).

@sampsyo
Copy link

sampsyo commented May 10, 2023

Drive-by comment that may be entirely obvious: in an extremely high-level way, this resembles method overloading in OO languages. That is, in overloading, the behavior (and the return type) depends on the types of the arguments at the call site. Anyway, this isn't the same because you're explicitly choosing which behavior to use at the cal site rather than using the types, but it's not completely different!

@rachitnigam
Copy link
Member Author

A close analogy is Scala's match types which allow return types to be changed based on the call site argument. Unsurprisingly, they very quickly become useful in defining dependently typed methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: language Component: the Filament language
Projects
None yet
Development

No branches or pull requests

2 participants