Open taichi-ishitani opened 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
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.
match
/ if let
/ etc statements will have to be built out of SV functions. This is not at all a game-breaking challenge.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.
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:
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
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 :
I am suggest "Auto" sort of like C++ auto
I think this is related to #539.
@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.
@saturn77 , I think you should put your suggestion to #539 too.
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();
}
@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 logic
makes sense. Using int types such as int
may make sense in simulation software, but makes very little sense in hardware.
@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 {
}
).
@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.
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()
}
}
@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.
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.
At the viewpoint of syntax complexity, introducing a impl
declaration 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()
}
}
At the viewpoint of syntax complexity, introducing a
impl
declaration is probably better than adding function block to eachenum
,struct
andtype
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();
Yes. impl TYPE {}
means "define functions associated to TYPE".
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();
メモ
メソッドの中身を式に限定せず、任意のメソッドを定義できるようする。 そして、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 やループを書くことができる。
It is common for similar pieces of code to appear frequently. For exmaple:
I think functions defined within a type definition can resolve this kind of situation.
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.syntax suggestion