-
Notifications
You must be signed in to change notification settings - Fork 150
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
Support splitting up struct method parameters into multiple input ports #729
base: main
Are you sure you want to change the base?
Conversation
…sn't work b/c lambda bodies aren't partially evaluated before iExpandMethod.
…ment construction
@@ -6,7 +6,7 @@ Error: "ClockCheckCond.bsv", line 6, column 8: (G0007) | |||
Method calls by clock domain: | |||
Clock domain 1: | |||
default_clock: | |||
the_y.read at "ClockCheckCond.bsv", line 2, column 18, | |||
the_y.read at "ClockCheckCond.bsv", line 2, column 10, |
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 you know why this position changed?
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.
I'm not sure I understand why this error message had the position that it did in the first place. It is still pointing to the same interface field at least.
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 message is about the definition for out
in the module (at line 10, though referring to the use of y
at line 11). However, what is happening under the covers is that BSC is creating an interface struct from all the definitions, and the names for the fields of that struct are taken from the interface type declaration, and out
is declared at line 2. So that causes an error on out
in the module to end up pointing to out
in the type. It's a known issue that we should fix at some point (Bluespec Inc internal bug database issue 1238).
However, it's interesting that your PR has changed the position from the name (out
) to the type (Bit#(8)
). It could be due to how you're picking positions for any ISyntax/CSyntax constructs that you're building, or maybe how you've changed the expression structures (causing getPosition
to find a different position first). I'll try to keep an eye out for it while looking through your changes. (I do think it's worth understanding why positions change -- regardless of what we think of the specific test example, because it could show up as a position change in other situations that we're not testing.)
…rs for a non-synthesizable subinterface
I think it would be good to have tests for the error messages you get if SplitPorts instances are wrong (wrong number of names, name conflicts between generated ports and any others you can think of). |
idEither = prelude_id_no fsEither | ||
idLeft = prelude_id_no fsLeft | ||
idRight = prelude_id_no fsRight | ||
idPreludeCons = prelude_id_no fsCons -- idCons isn't qualified |
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.
Why did you need a qualified version of idCons? Are the existing uses of idCons wrong?
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.
Yeah the existing idCons
is unqualified for some reason. I am comparing equality with the id coming out of ICon
, which is qualified. I suppose I could just compare the base string but comparing with the whole id seems better?
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.
I agree with Ravi that instead of defining idPreludeCons
, we should fix idCons
(and idNil
) to be qualified. (I'd also remove the neighboring idConcat
, since it's unused, and remove idNothing
, replacing its few uses in the BSV parser with idInvalid
-- and remove their associated definitions in PreStrings
.) That could be done in a separate PR.
endfunction | ||
method wset = primMethod(Cons("v", Nil), rw_wset); | ||
method wget = primMethod(Nil, _rw.wget); | ||
method whas = primMethod(Nil, pack(_rw.whas)); |
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.
If the user writes a conflict_free
attribute for two rules, then BSC will add logic to dynamically check that any conflicting methods between the two have not been called at the same time. BSC does this by instantiating an RWire
for each method called by each rule, and inserting a wset
action at the place in the rule where it calls the method; then a $display is inserted into the module, which prints an error message if the wires for two conflicting methods is ever true.
This occurs in BSC after scheduling, when the module is in ASyntax
. To add a submodule instantiation at that point, BSC needs an AVInst
value. The reason that vMkRWire
exists is so that BSC can get its hands on the AVInst
for RWire
. It does this by simulating the entire compilation flow for vMkRWire
-- first typecheck, then convert to ISyntax, then elaboration, then convert to ASyntax -- and then looking for the sole submodule in the resulting ASyntax.
The interface for vMkRWire
is entirely unneeded -- BSC just wants to get the AVInst
structure for the _rw
submodule instantiation. We might consider changing this module to have an empty interface, and then you don't need to worry about adding primMethod
to any methods.
The reason that you needed to add primMethod
is because BSC's simulated compilation flow starts at typecheck, without any GenWrap
stage. It used to be OK for BSC to skip GenWrap
, because later stages only required that the interface be raw types (bit and PrimAction). But you've changed the evaluator to expect that all methods evaluate to IMethod
, and that only happens when there's a call to primMethod
, and that gets inserted by the toWrapField
call that GenWrap
now inserts on the interface. So AAddSchedAssumps
would need to simulate the GenWrap
step by inserting toWrapField
; but if that's not done, then you would need to manually insert the toWrapField
, or manually write the result of that (by inserting primMethod
on each method).
I think it might be better to write toWrapField
on _rw
, rather than explicitly spell out the interface; and better than that would be to update AAddSchedAssump
to insert it; but better still would be to eliminate the interface on vMkRWire1
altogether -- but actually, I would prefer that we eliminate the simulated compile flow altogether and just hardcode an AVInst
value for RWire in AAddSchedAssump
.
However, all of this does make me wonder: For imported modules in BH code, we have written them as an import of the raw interface and then a wrapper module (that converts from the raw interface and inserts calls to primitives to save types etc) -- for example, mkReg
is a wrapper around vMkReg
. Can we simplify those wrappers now with just a call to toWrapField
?
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.
Also, it looks like someone added a vMkUnsafeRWire1
version of this module, when they were creating Unsafe
versions of all the RWire
modules, but there's no need for that, so I'd delete that module.
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.
To be honest I haven't dug into AAddSchedAssump
suffieciently to understand what is going on there. Although constructing an AVInst
directly does seem a lot nicer than simulating the whole compilation flow, if doing so is reasonable.
I did try changing the primMethod
to toWrapField
, but that doesn't work because VRWireN.wset
returns a PrimAction
, but toWrapField
uses ActionValue_ 0
. The implementation of ActionValue_
is not exported from the Prelude, and even if it was, we can't manipulate the result of toWrapField
since it is wrapped in primMethod
. I suppose we could make toWrapMethod
use PrimAction
instead of ActionValue_ 0
for wrapping ActionValue
s? But this is likely to change again anyway in the future when we add support for methods with multiple output ports.
I suppose it is possible to use toWrapMethod
in the manually-written wrapper modules for imported Verilog. Although in those cases the wrapped interface field types need to be written down anyway. Also this would require fixing the above-mentioned PrimAction
/ActionValue_ 0
disparity.
src/Libraries/Base1/Prelude.bs
Outdated
MetaConsNamed(..), MetaConsAnon(..), MetaField(..) | ||
MetaConsNamed(..), MetaConsAnon(..), MetaField(..), | ||
|
||
primMethod, WrapField(..), WrapMethod(..), WrapPorts(..), |
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.
I assume that primMethod
here is only exported so that it can be used in PreludeBSV
for vMkRWire1
and can be removed when that's resolved.
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.
Correct.
class TupleSize a n | a -> n where {} | ||
instance TupleSize () 0 where {} | ||
instance TupleSize a 1 where {} | ||
instance (TupleSize b n) => TupleSize (a, b) (TAdd n 1) where {} |
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.
Hm, would the size of (x,())
be reported as 1? Do we think it should be 2?
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.
Hmm, I suppose it would be reported as 1.
I don't think it's too unreasonable to treat tuples that end in ()
as one element smaller. Note that AppendTuple
also would drop the ()
if the first tuple being appended ends in ()
, and the same for CurryN
.
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.
I recall that there is a general issue with tuples: because they are build from nested pairs, the last element can't be of a tuple type, otherwise it's indistinguishable from one large tuple. For example (and this works in BSV too), if you have a variable of type Tuple2 a (Tuple2 b c)
, you can assign to it with tuple3
-- and the current implementation of TupleSize
considers it 3. Here's code that you can try:
package TupleSizeBH where
type T3 = Tuple2 Bool (Tuple2 Bool Bool)
mkTupleSizeBH :: (TupleSize T3 3) => Module Empty
mkTupleSizeBH =
module
let x :: T3
x = tuple3 True False True
However, if we have a variable of type Tuple3 a b ()
, that cannot be assigned with tuple2
-- BSC gives a type error unless you use tuple3
. However TupleSize
says its 2. This inconsistency seems wrong to me.
The issue is that the expression(e1,e2)
parses into PrimPair e1 e2
, and (e)
parses the same as e
, and ()
parses into PrimUnit
. Longer tuples are nestings of PrimPair
, but PrimUnit
is not a zero of PrimPair
-- this is perhaps clearer in BSV, where you can't write ()
, it has to be written as void
.
The inconsistency can be fixed for TupleSize
by defining it like this:
instance TupleSize a 1 where {}
instance TupleSize (a,b) 2 where {}
instance (TupleSize b n) => TupleSize (a, b) (TAdd n 1) where {}
But you're saying that AppendTuple
and curryN
also use PrimUnit
as a zero. Here is an example showing that appending a Tuple3
and Tuple2
will give Tuple4
if the Tuple3
ends with PrimUnit
:
package AppendTupleBH where
type TA = Tuple3 Bool Bool ()
type TB = Tuple2 Bool Bool
-- type TC = tuple5 Bool Bool () Bool Bool
type TC = Tuple4 Bool Bool Bool Bool
mkAppendTupleBH :: (AppendTuple TA TB TC) => Module Empty
mkAppendTupleBH =
module
let x :: TA
x = tuple3 True False ()
y :: TB
y = tuple2 True False
z :: TC
z = appendTuple x y
This seems to be because you want to support ()
as a zero in calls to appendTuple
and splitTuple
, and so there need to be instances for ()
, but you have also written your typeclass so that it reuses the top-level instances for sub-elements of the tuple. If you separate the processing of the sub-elements from the top-level, I think the issue goes away. Of course, there's also a complication that BSC won't allow overlapping instance of the form T () x
and T x ()
, even if a more explicit instance for the intersection T () ()
is given -- which is unfortunate, and maybe something that can be fixed one day? You got around that by adding a second level of typeclass. However, that second level is still conflating top-level ()
with sub-element ()
. I think you just need to add a third level of typeclass, that's only for processing once top-level ()
has been eliminated -- and in fact I confirmed that this works:
class AppendTuple a b c | a b -> c where
appendTuple :: a -> b -> c
splitTuple :: c -> (a, b)
instance AppendTuple a () a where
appendTuple x _ = x
splitTuple x = (x, ())
instance (AppendTuple' a b c) => AppendTuple a b c where
appendTuple = appendTuple'
splitTuple = splitTuple'
class AppendTuple' a b c | a b -> c where
appendTuple' :: a -> b -> c
splitTuple' :: c -> (a, b)
instance AppendTuple' () a a where
appendTuple' _ x = x
splitTuple' x = ((), x)
instance (AppendTuple'' a b c) => AppendTuple' a b c where
appendTuple' = appendTuple''
splitTuple' = splitTuple''
class AppendTuple'' a b c | a b -> c where
appendTuple'' :: a -> b -> c
splitTuple'' :: c -> (a, b)
-- Top-level () has been handled before AppendTuple''
-- so no instance is needed for () because occurrences
-- can only be sub-elements at this point and are handled
-- by the instance for sub-elements of any type
instance AppendTuple'' a b (a, b) where
appendTuple'' a b = (a, b)
splitTuple'' = id
instance (AppendTuple'' a b c) => AppendTuple'' (h, a) b (h, c) where
appendTuple'' (x, y) z = (x, appendTuple'' y z)
splitTuple'' (x, y) = case splitTuple'' y of
(w, z) -> ((x, w), z)
I haven't looked closely at curryN
, but I assume that a layer of hierarchy could be added, with ()
not handled in the second layer.
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.
I was worried that there's still the issue that splitTuple
of Tuple3 Bool Bool ()
to split off the last element and then appendTuple
to put it back won't result in the original tuple. But actually, both definitions are allowed -- by your implementation and mine! Unless I'm missing some reason why this example compiles:
package TestSplitAppend where
type TA = Tuple3 Bool Bool ()
mkTestSplitAppend :: Module Empty
mkTestSplitAppend =
module
let x :: TA
x = tuple3 True False ()
p :: ((Bool,Bool), ())
p = splitTuple x
y1 :: Tuple2 Bool Bool
y1 = case p of
(a,b) -> appendTuple a b
y2 :: TA
y2 = case p of
(a,b) -> appendTuple a b
let e1 :: (Bool, Bool)
e1 = (True, False)
e2 :: ()
e2 = ()
e31 :: (Bool, Bool)
e31 = appendTuple e1 e2
e32 :: (Bool, Bool, ())
e32 = appendTuple e1 e2
And it's not because the types are considered the same, because adding y1 == y2
causes a type error.
There's a dependency on the typeclass, so for the same arguments, only one result type should be possible:
class AppendTuple a b c | a b -> c where
Separately, isn't there something missing in the dependencies you declared? Shouldn't it also have a c -> b
and b c -> a
?
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.
I'm just getting back to your comments here - I'm not sure I agree your version of TupleSize
is what we want?
The version you specified would have TupleSize () 1
, but we want ()
to have size 0
for the purposes of checkPortNames
. I suppose we could introduce TupleSize'
and make (a, ())
have size 2 but ()
have size 0 - would that address your concern?
@@ -63,6 +63,10 @@ mkMaybe :: (Maybe CExpr) -> CExpr | |||
mkMaybe Nothing = CCon idInvalid [] | |||
mkMaybe (Just e) = CCon idValid [e] | |||
|
|||
mkList :: [CExpr] -> CExpr |
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.
I think I'd prefer mkList
to take a Position
argument, and use that for the position of Nil (instead of no position) and Cons (instead of the position of the expression). The function mkList
is used in GenWrap
in places that are processing FInf f
and can use the position of f
for this.
handleCtxRedWrapField:: Position -> (VPred, [VPred]) -> FString -> Type -> EMsg | ||
handleCtxRedWrapField pos (vp, reduced_ps) name userty = | ||
(pos, EBadIfcType (getFString name) | ||
"This method uses types that are not in the Bits or SplitPorts typeclasses.") |
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.
It would be good for the error message to point to the specific types that are a problem. (Caveat: I haven't yet looked at the testcases yet, to see examples of the error message.)
This handle*
function is called when WrapField
didn't reduce; there is probably an instance, but that instance introduces provisos (like Bits
or SplitPorts
) which don't reduce. As some of the other handle*
functions demonstrate, it's possible to get a list of those leaf provisos, and then you can extract the list of types whose provisos didn't reduce, and can report those in the error message.
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.
Ah, an example of the error is testsuite/bsc.verilog/noinline/NoInline_ArgNotInBits.bsv
. Before there was one message that mentioned the type not in Bits
; now there's the above EBadIfcType
and a separate message for the Bits
class. I'd rather that handleCtxRedWrapField
filter out the Bits
failures and report the same single error message.
@@ -87,4 +88,7 @@ getDefArgs dcls t = | |||
|
|||
-- ==================== | |||
|
|||
mkProxy :: CType -> CExpr |
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.
This name wasn't clear to me. I don't think I'd heard of these kind of dummy values as being called proxies before, but that's fine, as long as there's some more info about what it's a proxy for. Maybe mkTypeProxy
-- or mkTypeProxyExpr
if that's not too long.
@@ -964,8 +965,10 @@ instance PPrint CDefn where | |||
(IdK i) -> ppConId d i | |||
(IdKind i k) -> ppConId d i <+> t "::" <+> pp d k | |||
(IdPKind i pk) -> ppConId d i <+> t "::" <+> pp d pk | |||
pPrint d p (Cforeign i ty oname opnames) = | |||
text "foreign" <+> ppVarId d i <+> t "::" <+> pp d ty <> (case oname of Nothing -> text ""; Just s -> text (" = " ++ show s)) <> (case opnames of Nothing -> text ""; Just (is, os) -> t"," <> pparen True (sep (map (text . show) is ++ po os))) | |||
pPrint d p (Cforeign i ty oname opnames _) = |
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.
Why is the new argument (cforg_is_noinline
) not being printed in the output? I believe that it can be printed as a noinline
pragma just above the statement.
@@ -18,8 +18,8 @@ if { $ctest == 1 } { | |||
# backend, and only then if the user has specified that it's OK | |||
# for the Verilog and Bluesim backends to diverge). | |||
# | |||
find_regexp mkTop.cxx {2047u \& \(\(\(\(\(tUInt32\)\(\(tUInt8\)0u\)\) << 3u\) \| \(\(\(tUInt32\)\(DEF_cond__h[0-9]+\)\) << 2u\)\) \| \(tUInt32\)\(DEF_v__h172\)\);} | |||
find_regexp mkTop.cxx {DEF_v__h172 = DEF_AVMeth_s_m;} | |||
find_regexp mkTop.cxx {2047u \& \(\(\(\(\(tUInt32\)\(\(tUInt8\)0u\)\) << 3u\) \| \(\(\(tUInt32\)\(DEF_cond__h[0-9]+\)\) << 2u\)\) \| \(tUInt32\)\(DEF_v__h\d+\)\);} |
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.
Your change is no longer testing that the DEF found in the first line is the same DEF that's assigned in the second line. Maybe we can find a way to pattern match to get the variable name and then use it in the other pattern, but that's probably too much for this PR. I think you're best just changing 172
to be whatever the specific new number is, for now.
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.
This just seemed like a brittle test to be checking for a specific generated h number. But I can change it to the new number if that is fine for now.
assign x__h704 = { 1'd0, i_mult[3:1] } ; | ||
assign x__h741 = { i_multiplicand[6:0], 1'd0 } ; | ||
assign x__h778 = i_count + 4'd1 ; | ||
assign x__h1001 = i_count + 4'd1 ; |
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 testsuite's Verilog comparison ignores the h# in the def name, so this file would only need to be updated if something other than the names changed -- and that difference appears to be a reordering of these assignments. And the order appears to have changed only because the h# rolled over from 900 to 1000, and the sorting order considers 1 to come before 9 (regardless of length). I think this is because AVerilog.hs
uses sort
(see sort other_defs
around line 428).
Is there a Haskell sort function that can treat numbers better? I'd be interested in adjusting the sort, so that this expected file doesn't need to change. But I recognize that's outside the scope of this PR.
However, the increase from 778 to 1005 (in the latest version) is an interesting indication of additional work that is being done during elaboration. Could there be any example designs for which this overhead becomes significant? For example, do we have some examples of how BSC time/memory scales as the number of methods in an interface becomes large?
mkBar = module | ||
interface | ||
f = interface Foo | ||
put _ = noAction |
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.
This file is missing a final newline. (GitHub shows this in the "Files changed" tab of the PR with a red circle at the end.)
@@ -0,0 +1,14 @@ | |||
package NestedIfcIntegerArg where |
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.
Can you say what prompted this example?
@@ -6,7 +6,7 @@ Error: "ClockCheckCond.bsv", line 6, column 8: (G0007) | |||
Method calls by clock domain: | |||
Clock domain 1: | |||
default_clock: | |||
the_y.read at "ClockCheckCond.bsv", line 2, column 18, | |||
the_y.read at "ClockCheckCond.bsv", line 2, column 10, |
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 message is about the definition for out
in the module (at line 10, though referring to the use of y
at line 11). However, what is happening under the covers is that BSC is creating an interface struct from all the definitions, and the names for the fields of that struct are taken from the interface type declaration, and out
is declared at line 2. So that causes an error on out
in the module to end up pointing to out
in the type. It's a known issue that we should fix at some point (Bluespec Inc internal bug database issue 1238).
However, it's interesting that your PR has changed the position from the name (out
) to the type (Bit#(8)
). It could be due to how you're picking positions for any ISyntax/CSyntax constructs that you're building, or maybe how you've changed the expression structures (causing getPosition
to find a different position first). I'll try to keep an eye out for it while looking through your changes. (I do think it's worth understanding why positions change -- regardless of what we think of the specific test example, because it could show up as a position change in other situations that we're not testing.)
@@ -44,8 +44,7 @@ test_c_veri_bsv_modules \ | |||
# The typedef fails because BSC doesn't expand the synonym before checking |
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.
It looks like this comment is no longer correct, and should be removed. (It appears that the bug has been fixed, because the checked has moved out of GenWrap.)
compare_file BadSplitInst_PortNameConflict.bs.bsc-vcomp-out | ||
|
||
compile_verilog_fail_error BadSplitInst_TooManyPortNames.bs S0015 | ||
compare_file BadSplitInst_TooManyPortNames.bs.bsc-vcomp-out |
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.
Missing final newline
After some back and forth with @nanavati, we ended up going with a design that is closer to my original proposal in #714. It is really hard to do port splitting in
GenWrap.hs
with the degree of flexibility that we want, and there are some limitations like not being able to resolve numeric type operations, e.g. for computing the size of a vector. Not to mention that code is incredibly tedious and hard to modify, and would only make it harder to implement features like methods with multiple output ports. Doing this with type classes makes the logic a bit more transparent and easier to modify in the future.There are several significant changes bundled together in this pull request:
GenWrap.hs
;BetterInfo
(this allows determining input port names at elaboration time);Implementation
I'm not changing the fundamental structure of wrapper interfaces, only changing how field types are determined - flattening of nested interfaces still happens in
genwrap
as usual. The types of fields in a wrapper interface are determined by the typeclassWrapField
. ThetoWrapField
method converts from a field in the original interface (e.g.Int 8 -> ActionValue Bool
) to the type of field in the wrapper interface (e.g.Bit 8 -> ActionValue_1
), whilefromWrapField
is the inverse of this. The special cases forClock
,Reset
andInout
are also handled byWrapField
.WrapField
uses a type classWrapMethod
to compute the wrapped type of a method field. This type class uses theSplitPorts
type class to convert the type of a method input into a tuple ofPort
s, and theWrapPorts
type class converts this into a tuple ofBit
values. (Thus one determines how a method argument type should be split into ports by defining instances ofSplitPorts
. More on that later.) The individual tuples of method argumentBit
values then get turned into a single curried function type for the wrapper interface method.These type classes are also used to compute the names of input ports. This is happening on the value level as lists of strings1, not as type-level strings as I was originally thinking. I hit some sort of snag with this, although I don't remember exactly what it was, and being able to just compute this in the evaluator seemed like less of a pain than dealing with type-level lists of strings, adding type-level number to type-level-string conversion, etc.2 The only reason a type-level string is there in
WrapField
is to give the original field name to generate a better error message when context resolution fails due to a type not being in theBits
type class.The list of argument names are tagged on to the wrapper method function/value with
primMethod
. The evaluator now expects this primitive to exist on method fields iniExpandField
.3 We could potentially stick additional metadata that is computed at elaboration-time here in the future. The field name/result
pragma and thearg_names
pragma (if present) are passed as arguments totoWrapField
, which are used to compute the base names of input ports, which are and tagged on to the converted method value.Because port names are now determined at elaboration time, I had to move the port name collision checks to after elaboration. This is maybe slightly less nice as some error messages show up latter, but this sort of error isn't super common. It does feel like a more natural place to implement these checks anyway, instead of needing to figure out the port names from the pragmas before type checking.
Saving port types, on both sides of the synthesis boundary, is also handled via these type classes. See the
saveFieldPortTypes
method inWrapField
type class. Calls to this method get inserted in both genwrap and wrappergen. This method also requires the same field naming arguments astoWrapField
. I considered makingtoWrapperField
/fromWrapperField
be in theModule
monad and do the port type saving too, but that complicates the code generation in genwrap a fair bit as every field value needs to be bound in a giantdo
-block.Specifying port splitting
How a method argument type gets split up, and how the resulting ports are named is determined by the
SplitPorts
type class. There is a default instance that doesn't do any flattening, which preserves the current behavior:If we have a struct
then for
putBar
to have separate input ports for each field, we need an instanceOne can write this sort of instance explicitly. However there are a few ways that this can be done with less boilerplate.
I added a library
SplitPorts
in Base1 with a couple of utility type classes.ShallowSplitPorts
uses generics to flatten out a struct by one level, using theSplitPorts
instances for each of its fields. One can use these to define aSplitPorts
instance:This would be a bit nicer to use if we had
deriving via
. In fact, I'm wondering if we should makederive SplitPorts
generate the above sort of instance automatically.DeepSplitPorts
fully flattens a struct, including nested struct, tuple andVector
4 fields, down to primitives and types with multiple constructors. When using this type class, if one wishes for some nested struct type not to be flattened, they can define aDeepSplitPorts
instance that does nothing to prevent this.Sometimes one might wish for a type to be flattened in only some places. Instead of defining a
SplitPorts
instance, you can insert theShallowSplit
orDeepSplit
"newtype" wrapper on your interface method parameters:I added test cases illustrating all these different patterns/approaches. There are probably more possibilities and I'm not sure what will prove to be the most ergonomic in practice, but these utilities are easy to add/change later.
Future considerations
I designed this with support for methods with multiple output ports in mind, which I may or may not attempt next depending on how much time I have. The
SplitPorts
type class could be reused to also determine how results of value/ActionValue
methods are split into output ports.I'm not quite sure what the wrapper type representation looks like for types with multiple output ports. Just using a tuple of
Bit
values for methods with multiple output ports might work for value methods, butActionValue_
only accepts a single numeric size parameter. My current thinking is that we should ditchActionValue_
and havea struct
PrimValue :: (# -> * -> *) n a
that tags aBit n
value onto a chain of output valuesa
, ending in aPrimAction
orPrimUnit
.Remaining issues
The error message when a method yields a port that isn't in
Bits
is fine, but there is another error message about aBits
context that didn't reduce, with unknown position. See for instance testsuite/bsc.verilog/noinline/NoInline_ArgNotInBits.bsv.bsc-vcomp-out.expected. I'm not totally sure where this is coming from or how to suppress it, but it maybe isn't too bad.Congrats on making it to the end of this wall of text. Hopefully @quark17 has time to look this over before the sync meeting on Friday?
Footnotes
Really, this should be using
ListN
to ensure that the list of port names is always the correct length. But sadly that doesn't exist in the Prelude, andSplitPorts
needs to be. ↩Although in retrospect the various tuple shenanigans I needed were just as complicated, and I could maybe have just stuck the port name in the
Port
type constructor. So I'm not sure if it ended up being much simpler. Having the evaluator is more flexible, at least. ↩This required a corresponding tweak in
vMkRWire1
, which is a handwritten interface LARPing as a generated wrapper interface, to be instantiated way later by the scheduler. ↩Making this work reasonably for large vectors required a fun bit of awesomeness in the
ConcatTuple
type class I added, which converts a vector of tuples to and from a flattened tuple. ↩