veryl-lang / veryl

Veryl: A Modern Hardware Description Language
Other
443 stars 20 forks source link

[Feature] Define functions within type definition #602

Open taichi-ishitani opened 3 months ago

taichi-ishitani commented 3 months ago

It is common for similar pieces of code to appear frequently. For exmaple:

var foo_bar: foo_bar_type; // foo_bar_type is enum
let is_foo: logic = foo_bar == foo_bar_type::FOO;
let is_bar: logic = foo_bar == foo_bar_type::BAR;

I think functions defined within a type definition can resolve this kind of situation.

enum foo_bar_type: logic {
  FOO,
  BAR,
  function is_foo() -> logic = self == FOO,
  function is_bar() -> logic = self == BAR
}

var foo_bar:  foo_bar_type;
let is_foo:   logic = foo_bar.is_foo();
let is_bar:   logic = foo_bar.is_bar();

For ease to generate SV code, function defined within tyep definition can have one expression only like let declaration. Due to this limitation, Veryl has only to copy the body expression to generated SV code.

typedef enum logic {
  foo_bar_type_FOO,
  foo_bar_type_BAR
} foo_bar_type;

//  expand body expression during SV generation
always_comb is_foo = foo_bar == foo_bar_type_FOO;
always_comb is_bar = foo_bar == foo_bar_type_FOO;

syntax suggestion

enum foo_bar_type: logic {
  FOO,
  BAR
} {
  function is_foo() -> logic { self == FOO }
  function is_bar() -> logic { self == BAR }
}

struct foo_bar_type {
  foo: logic,
  bar: logic
}{
  function is_foo_zero() -> logic { self.foo == 0 }
  function is_bar_one() -> logic { self.bar == 1 }
}

type word: logic<w> {
  function get_parity() -> logic { ^self }
  function match_parity(other: input word) -> logic {
    self.get_parity() == other.get_parity()
  }
}
saturn77 commented 3 months ago

Related to above post, would the following below be possible ?


// Veryl example
enum Number : <T : type(int)> {
  Infinite,
  Value(T),
  // implied "method" that very would need to create based on <T : type(int) >
  function __check_on_assign__ -> logic = self == ( type(float) || type(unsigned) ),
}

var some_integer : Number::<i32> = Number::Value(-241); 

var some_integer : Number::<f32> = Number::Value(1.2567);  <--- Veryl error 
nblei commented 3 months ago

A couple of things. Perhaps checker functions should be made by default, or with some type of pragma:

#[derive(EnumCheckers)]
enum foo_bar : logic {
  FOO,
  BAR
}

Also, I think it should be foo_bar::is_foo --- using the scoping :: operator, as is_foo is a type, not an instance of a type.

@saturn77 see #323

There are some complications with tagged unions.

  1. To the best of my knowledge no synthesis tools currently support tagged unions, so they will need to be translated into non-tagged structs, and match / if let / etc statements will have to be built out of SV functions. This is not at all a game-breaking challenge.
  2. SV does not support parametric structs, so unless there is a significant change in Veryl design philosophy, I think that parametric structs in Veryl are a bad choice.
  3. In hardware, how many bits are used to encode the tag? Further, how are those bits specified? Further, what is the location of those bits within the context of the fields?

For example, in hardware I may have some register r such that, when r[5] = 1, {r[6] r[4:0]} is a pointer to memory, when r[5:4] = 2'b01, r[6] is a sign bit and r[3:0] is a magnitude, and when r[5:4] = 2'b'00, {r[6], r[4:3]} and r[2:0] are pointers to registers. A contrived example, but illustrative of the point.

So the syntax for specifying these types of things needs to be determined.

  1. Further, currently, there is no good way of doing type size checks in Veryl. This shouldn't be a game-breaking challenge, but it is also something that needs to be done since SV packed structs must have the same length, Veryl will need to emit SV with unused bits when different variants have different sizes, so this compiler capability will need to be built.
saturn77 commented 3 months ago

Perhaps it would be the most robust path to simply constrain any Enum to be of logic or bit type only.

I personally cannot think of a synthesized case outside of that.

For example, only allowing logic type:

enum ThreePhase : logic[1:0] {
  a, 
  b,
  c,
}

An the analogous bit version :

enum Quadrature : bit[1:0] {
  d, 
  q,
  zero,
}

relative to @nblei comments;

I would suggest a "one-hot" procedural derive macro that has two requirements:

  1. verify that the size of the logic/bit is correct for the number of states and type of encoding
  2. assign the one hot encoding as directed, with default sequence being 000, 001, 010, 100, etc.

Also, if normal integral encoding is to be done the #[derive(one_hot)] could be left off.

module rx_uart {
clock : input bit, 
reset : input bit, 
....
done : output bit, 
}

{

#[derive(one_hot)]
enum Rx : bit[2:0] {
idle,  // 000
shifting,  // 001
load,  // 010 
done,  // 100 
}

var state : Rx ; 

always_ff (clock, reset) 
if_reset  {
  state = Rx::idle; 
} else {
case(state) ...
....
}

assign done = state[2]; 

}

The advantage of the one_hot derive macro would be to also

  1. allow arbitrary insertion of additional states into the state machine
  2. without having to renumber the state

for example, the Veryl compiler will also determine the one hot encoding in the derived SV:


typedef enum logic[2:0] {

idle = 3'b000, 
shifting = 3'b001, 
load = 3'b010,
done = 3'b100,
};

or let's say you inserted another state in the middle :

#[derive(one_hot)]
enum Rx : bit[3:0] {
idle,  
shifting,  
debounce, 
load,  
done,  
}

then Veryl would compile this into :


typedef enum logic[3:0] {

idle = 4'b0000, 
shifting = 4'b0001, 
debounce = 4'b0010,
load = 4'b0100,
done = 4'b1000,
};

Finally, as a bonus ( I know this is a long post)

I am suggest "Auto" sort of like C++ auto :

#[derive(one_hot)]
enum Rx : auto  {
idle,  
shifting,  
debounce, 
load,  
done,  
}

This would be worthwhile IF :

  1. Enum's were constrained to logic / bit type
  2. Veryl has to calculate the size of appropriate logic / bit type
    • for integral types
    • for one hot type
taichi-ishitani commented 3 months ago

I am suggest "Auto" sort of like C++ auto

I think this is related to #539.

saturn77 commented 3 months ago

@taichi-ishitani yes, it is very similar to https://github.com/veryl-lang/veryl/issues/539.

I think the main difference is that "auto" would be a placeholder, but it could be removed as in your example.

taichi-ishitani commented 3 months ago

@saturn77 , I think you should put your suggestion to #539 too.

taichi-ishitani commented 3 months ago

Ruby's struct definition can taka a block including method definitions like below.

Customer = Struct.new(:name, :address) do
  def greeting
    "Hello #{name}!"
  end
end
Customer.new("Dave", "123 Main").greeting # => "Hello Dave!"

Refering this syntax, how bout the syntax for this feature?

enum foo_bar_type: logic {
  FOO,
  BAR
}{
  function is_foo() -> logic = self == FOO;
  function is_bar() -> logic = self == BAR;
}

By using an additional block, we can also introduce this feature to type feature.

type word: logic<w> {
  function get_parity() -> logic = ^self;
  function match_parity(other: input word) -> logic = self.get_parity() == other.get_parity();
}
nblei commented 3 months ago

@saturn77

I like your suggestions for non-tagged unions. I would suggest changing #[derive(one_hot)] to #[encoding(one_hot0)] to keep in line with SV ($onehot vs $onehot0). I would also suggest one_cold and one_cold1 as complementary versions of one_hot and one_hot0.

I also did not want to try to convince you that tagged unions are a bad idea --- in fact, I think they are a potential killer feature (e.g., tagged union would be a fantastic way to implement a decoder in a microprocessor). I just wanted to suggest that there needs to be something akin to an RFC for that feature, due to its complexity.

I think that untagged unions must be restricted to integral types, as stated in IEEE STD 1800-2023 7.3.1. Integral types include vectors of logic (synonymous with reg), and bit, as well as 2-state and 4-state integers (time, integer, int, etc.), so your suggestion to limit them to vectors of bit and logicmakes sense. Using int types such as int may make sense in simulation software, but makes very little sense in hardware.

nblei commented 3 months ago

@taichi-ishitani I really like your idea for creating class member functions for enums / user defined data types. Only thing I would suggest is that we do not introduce new syntax for functions, but continue to borrow rust style syntax (i.e., function body is declared inside { }).

taichi-ishitani commented 3 months ago

@nblei

we do not introduce new syntax for functions

I think functions defined within typedefs should only have one expression instead of a statement for ease of SV code generation. Due to this limitation, Veryl only have to copy the function body to SV code. Therefore, I intorduce Veryl's let style function definition syntax.

taichi-ishitani commented 3 months ago

if/case expression can take a statement-less block. So no new functin syntax is needed.

enum foo_bar_type: logic {
  FOO,
  BAR
} {
  function is_foo() -> logic { self == FOO }
  function is_bar() -> logic { self == BAR }
}

struct foo_bar_type {
  foo: logic,
  bar: logic
}{
  function is_foo_zero() -> logic { self.foo == 0 }
  function is_bar_one() -> logic { self.bar == 1 }
}

type word: logic<w> {
  function get_parity() -> logic { ^self }
  function match_parity(other: input word) -> logic {
    self.get_parity() == other.get_parity()
  }
}
saturn77 commented 3 months ago

@nblei I like the suggestions for discriminating of one_hot, one_hot0, one_cold, and one_cold1. I think this would be a very good feature.

An idea is here:

#[encode_one_hot0]
enum Rx : auto(logic) {
Idle,
Shiting,
Load,
Done,
}

Considering that the enum has to be inspected to determine the integral size (Auto), and also determine encoding, a procedural derive macro may still be warranted such as below:

#[derive(Auto, Onehot0)]
enum Rx  {
Idle,
Shiting,
Load,
Done,
}

Actually I like the above syntax format using (Auto, Onehot0) and this could also be (Auto, Onecold0) for example.

This is somewhat similar to using multiple derives in Rust such as

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    let serialized = serde_json::to_string(&point).unwrap();
    println!("serialized = {}", serialized);

    let deserialized: Point = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);
}

Overall, having Veryl automatically determine the size for the integral type AND the encoding is exciting, and automate the process of writing state machine a little further than currently exists.

saturn77 commented 3 months ago

I like @taichi-ishitani suggestion regarding adding function to type as this :

type word: logic<w> {
  function get_parity() -> logic { ^self }
  function match_parity(other: input word) -> logic {
    self.get_parity() == other.get_parity()
  }
}

My suggestion is to think about adding a "trait" type object in Veryl, such as shown below. The purpose of adding the trait type object would be to create a separate syntax from "type" or "typedef" when attaching functions to that type . . .

For example a trait in Rust could like this:

pub trait Counter {
    type IncrementFuture<'m>: Future<Output = u32> where Self: 'm;
    fn increment<'m>(&'m mut self) -> Self::IncrementFuture<'m>;

    type AddFuture<'m>: Future<Output = u32> where Self: 'm;
    fn add<'m>(&'m mut self, value: u32) -> Self::AddFuture<'m>;
}

where as in Veryl a trait could be such as :

pub very_trait Word  {
  type word : logic<w>; 
  function get_parity() -> logic { ^self };
  function match_parity(other: input word) -> logic {
    self.get_parity() == other.get_parity()
  };
}

in the case above for veryl_trait, then type or typedef would remain simple typing.

Overall, I am suggesting this as an option. But either way, I think attaching functions to type is powerful.

dalance commented 3 months ago

At the viewpoint of syntax complexity, introducing a impldeclaration is probably better than adding function block to each enum, struct and type individually.

enum foo_bar_type: logic {
  FOO,
  BAR
}

impl foo_bar_type {
  function is_foo() -> logic { self == FOO }
  function is_bar() -> logic { self == BAR }
}

struct foo_bar_type {
  foo: logic,
  bar: logic
}

impl foo_bar_type {
  function is_foo_zero() -> logic { self.foo == 0 }
  function is_bar_one() -> logic { self.bar == 1 }
}

type word = logic<W>;

impl word {
  function get_parity() -> logic { ^self }
  function match_parity(other: input word) -> logic {
    self.get_parity() == other.get_parity()
  }
}
taichi-ishitani commented 3 months ago

At the viewpoint of syntax complexity, introducing a impldeclaration is probably better than adding function block to each enum, struct and type individually.

This declaration looks good for me. But I'm not sure how to use this kind of type because I'm not familiar with rust style syntax. Like below?

enum foo_bar_type {
  FOO,
  BAR
}

impl foo_bar_type {
  function is_foo() -> logic { self == FOO }
  function is_bar() -> logic { self == BAR }
}

var foo_bar: foo_bar_type;
let is_foo: logic foo_bar.is_foo();
let is_bar: logic foo_bar.is_bar();
dalance commented 3 months ago

Yes. impl TYPE {} means "define functions associated to TYPE".

taichi-ishitani commented 3 months ago

Thank you, I understood.

In addition, referring Ruby's mix-in feature, I think that defining a function block which is not yet associated to a TYPE is useful to share common code.

unbinded_block parity_calurator { // need to consier keyword
  function get_parity() -> logic { ^self }
}

var foo: impl logic<8> parity_calurator;
let foo_parity: logic foo.get_parity();

type bar_type = logic<10>;
impl bar_type parity_calurator;

var bar: bar_type;
let bar_parity: logic = bar.get_parity();
taichi-ishitani commented 1 month ago

メモ

メソッドの中身を式に限定せず、任意のメソッドを定義できるようする。 そして、generics の様に、メソッド定義を展開するのが良いかもしれない。

これを、

enum foo_bar_type {
  FOO,
  BAR
}

impl foo_bar_type {
  function is_foo() -> logic { return self == FOO; }
}

let foo: foo_bar_type = FOO;
let is_foo: logic = foo.is_foo();

以下の様に展開する。

typedef enum logic {
  FOO,
  BAR
} foo_bar_type;

function automatic __foo_bar_type_is_foo(foo_bar_type self);
  return self == FOO;
endfunction

foo_bar_type foo;
always_comb foo = FOO;
logic is_foo;
always_comb is_foo = __foo_bar_type_is_foo(foo);

これなら、メソッド中に if やループを書くことができる。