Deriving Via#965
Conversation
Now that there will be another way to derive instaces I think multiline deriving declarations a'la Haskell should be permitted.
Since now there will be multiple deriving strategies there is a need to record that additional information in the syntax tree. Original deriving mechanism is called stock deriving (like in Haskell). There is no changes planned to the way this works. Follow up commits will add new deriving functions.
This actually generates some plausibly passing code. There are probably quite few situations where it'll fail. Like for example when the associated typeclass function uses the datatype as an argument to higher kinded type. Current solution won't substitute this variable. I don't think there is anything to be done here tbh, we won't be able to figure out how to wrap/unwrap it. Perhaps we should error with better message in that scenario?
be71076 to
8a7b2f8
Compare
|
|
||
| data CDeriving | ||
| = CStock [CTypeclass] | ||
| -- TODO: expr? |
There was a problem hiding this comment.
Not sure what this TODO is about?
There was a problem hiding this comment.
This was my first change in the code, I wasn't sure if the Id is the proper node to hold via target. Actually I'm still not sure if that is the correct choice here, but it worked out.
There was a problem hiding this comment.
I guess in theory we could have a partially-applied type constructor instead of the name of a type here? Does Haskell support that?
There was a problem hiding this comment.
I don't think so. Haskell expects a type here. Given that DerivingVia works by the way of coerce, it can be arbitrarily complex type, see example from ghc user guide:
newtype Kleisli m a b = Kleisli (a -> m b)
deriving (Semigroup, Monoid)
via (a -> App m b)In here (bsc) I've assumed, that the type and constructor names must be the same. Perhaps this could only be constructor name, if I were not to generate type signature for the typeclass methods. But I've had the hunch the errors would be slightly better if I were to do it.
There was a problem hiding this comment.
I would either remove the TODO comment or make it more clear.
| @@ -0,0 +1,12 @@ | |||
| package DerivingViaHK() where | |||
There was a problem hiding this comment.
Does HK mean higher-kinded? That kind of threw me at first because a is kind *. But I guess Maybe is kind * -> *? The issue here is that a isn't at the top-level, I guess.
There was a problem hiding this comment.
yeah I wasn't sure how to name it. Indeed that's about for example Maybe, I'm not making any effort to substitute a for concrete data type we are deriving instance for, when a is used as argument to TAp. I don't think I could derive that kind of function anyway.
|
Actually it looks like there are some testsuite failures, nothing major: https://github.com/B-Lang-org/bsc/actions/runs/27140760096/job/80181556806?pr=965 |
If I saw it right failing tests were: |
|
Thanks, looks good to me. @matx-chris, can we include this in our next internal BSC release? |
| pDerivingStock = lp ..+ sepBy pTypeclass cm +.. rp >>- CStock | ||
|
|
||
| pDerivingVia :: CParser CDeriving | ||
| pDerivingVia = pTypeclass +.+ l L_via ..+ pTyConId >>> CVia |
There was a problem hiding this comment.
is there any chance this could be generalized to allow a parenthesized type? deriving Foo via (Bar Baz) is useful
There was a problem hiding this comment.
I don't think so, at least not using the syntax proposed in the #964.
Right now the generated code uses the via identifier in two places, the class method type signature and data constructor. I guess the type signature isn't really needed here, I've added it because I've had a hunch it could lead to better error messages when something is not right.
What is necessary is data constructor that is used in the patterns in the generated code, and Bar Baz is a type not a constructor.
This all comes from the fact that it works quite different from the Haskell DerivingVia. Haskell variant just uses coerce. Here we generate as described in the issue that relies on the convention (single constructor datatype, with constructor name same as the type name), since there are no newtypes available (right?).
quark17
left a comment
There was a problem hiding this comment.
Thanks for your interest in developing BSC and contributing this!
I have not closely looked at doVia yet, but have comments on everything else. I think we need to decide whether this constructor approach is what we want to implement, or whether we should do something like use coerce (should be an easy simplification to doVia) or take an arbitrary expression for what to call (would require parsing an expression, also possibly not difficult?).
| getFTCDn (Cdata { cd_internal_summands = summands, | ||
| cd_derivings = derivings }) = | ||
| S.unions (map f summands) `S.union` S.fromList (map typeclassId derivings) | ||
| S.unions (map f summands) `S.union` mconcat (map getFTDer derivings) |
There was a problem hiding this comment.
Here and a few lines below, I would probably use S.unions instead of mconcat
There was a problem hiding this comment.
While doing so I've noticed that it might clearer (for me at least) to write it as: S.unions $ map f summands ++ map getFTDer derivings
But I've noticed that below there are many cases where there are unions of unions taken instead. So I've just did the simple change.
| > pDerivationsIf True = option [] $ | ||
| > do pKeyword SV_KW_deriving | ||
| > pInParens (pCommaSep1 (pTypeclass <?> "typeclass name")) | ||
| > pDerivationsIf True = many pDeriving |
There was a problem hiding this comment.
You've changed BH syntax to allow multiple deriving clauses. While you haven't enabled deriving-via for BSV, here you have enabled multiple deriving clauses, which wasn't the syntax before. If we're not sure about the BSV syntax yet, then maybe we shouldn't change this.
|
|
||
| data CDeriving | ||
| = CStock [CTypeclass] | ||
| -- TODO: expr? |
There was a problem hiding this comment.
I would either remove the TODO comment or make it more clear.
| ppDer d [] = empty | ||
| ppDer d is = text "deriving (" <> sepList (map (pvPrint d 0) is) (text ",") <> text ")" | ||
| ppDer :: PDetail -> [CDeriving] -> Doc | ||
| ppDer d drvs = vcatList (map (pPrint d 0) drvs) empty |
There was a problem hiding this comment.
I'm not sure we can use pPrint this way. The identifier in "deriving" can be qualified, and BSV and BH print qualifiers differently. I think we have to duplicate what pPrint of CDeriving is doing, except use pvPrint inside for IDs.
| in Right $ pure $ Cinstance (CQType ctx appliedClass) (map mkFun fs) | ||
|
|
||
| doVia _ _ _ (di, tgt) = | ||
| Left (getPosition di, ECannotDerive (pfpString di ++ " via " ++ pfpString tgt)) |
There was a problem hiding this comment.
Do we want to add a test case for this error situation?
| # tests for deriving via | ||
|
|
||
| compile_pass DerivingVia.bs | ||
| compile_fail_error DerivingViaHK.bs T0030 |
There was a problem hiding this comment.
This needs a comment explaining what's being tested. Is this a bug in BSC, that it gives an error or at least gives an unhelpful error? Or is this the message (and position) that you expect? (If it's a bug, we'll want to file it and record the number here.)
| package DerivingVia() where | ||
|
|
||
| import SplitPorts | ||
|
|
There was a problem hiding this comment.
I would also add uses of the derived instances, to prove that they exist and were defined properly. I don't know if that's easy to do with ShallowSplit or if we should use a simpler typeclass for this example.
|
|
||
| import SplitPorts | ||
|
|
||
| data Foo = Foo (Int 8) |
There was a problem hiding this comment.
You should also test on a data type with multiple disjuncts.
| > (SV_KW_actionvalue, "actionvalue", Bluespec38), | ||
| > (SV_KW_endactionvalue, "endactionvalue", Bluespec38), | ||
| > (SV_KW_deriving, "deriving", Bluespec38), | ||
| > (SV_KW_deriving, "deriving", Bluespec38), |
There was a problem hiding this comment.
This duplicate line should be removed.
| > | SV_KW_actionvalue | ||
| > | SV_KW_endactionvalue | ||
| > | SV_KW_deriving | ||
| > | SV_KW_via |
There was a problem hiding this comment.
I'd probably comment this out, if it's not being used yet.
|
Adversarial review of a tree carrying this PR found one wrong-results-class gap, confirmed by reproduction: data W a = W a | X a
instance (MyC a) => MyC (W a) where def = X def
data T = T (Int 8)
deriving MyC via Wthis compiles with zero warnings and the derived method elaborates to an unspecified value in the generated Verilog ( Validated fix (guard clause before the main viaTargetShapeOK :: [(Id, CDefn)] -> Id -> Bool
viaTargetShapeOK xs tgt =
case lookup (unQualId tgt) xs of
Just (Cdata { cd_original_summands = [summand] }) ->
length (cos_arg_types summand) == 1 &&
any (qualEq tgt) (cos_names summand)
_ -> Falserejecting with a positioned |
…wrap doVia wraps arguments with (CCon tgt [x]) and unwraps results with a single-arm case on (CPCon tgt [y]), silently assuming the target is a single-constructor, single-argument wrapper named like the type. A multi-constructor target compiled without any diagnostic, and the partial unwrap case elaborated to an unspecified value in the generated hardware whenever the delegated instance produced the other constructor. Check the shape up front and reject with a positioned error. When the target is written qualified, the candidate definition must match the written qualifier, not just be the first unqualified hit: a local type may shadow the imported one the user named, and judging the wrong definition's shape would let a bad target through (or reject a good one). Unqualified targets keep normal locals-first scoping, which is also what the generated CCon reference resolves to. Add a regression test asserting the rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cqQdWKp7jr73GHQYnvhiq
|
Follow-up to the above: the fix is now available as a ready-made commit on top of this branch — nanavati/bsc@bd91fcf1 (branch One refinement over the snippet posted earlier: when the via target is written qualified ( The identical change is validated in our integration tree (multi-constructor target → positioned error; the existing |
I did, thanks |
| @@ -0,0 +1,3 @@ | |||
| Error: "DerivingViaMultiCon.bs", line 17, column 11: (T0000) | |||
There was a problem hiding this comment.
@nanavati I'm getting a following diff here:
--- DerivingViaMultiCon.bs.bsc-out.expected 2026-07-13 14:34:56.121794265 +0200
+++ DerivingViaMultiCon.bs.bsc-out 2026-07-13 21:02:35.052396080 +0200
@@ -1,3 +1,5 @@
+checking package dependencies
+compiling DerivingViaMultiCon.bs
Error: "DerivingViaMultiCon.bs", line 17, column 11: (T0000)
Illegal deriving: MyC via W: the via target must be a data type whose single
constructor is named W and takes a single argument
looking at other examples I think those, checking (...) compiling (...) lines should be there, right? I'm doubting myself given that you've said that you have pulled those changes out of the internal repo, which I presume is passing tests with this exact expected?
My take on #964 during ZuriHac.
For now it has a commit with my dev env setup that I'll get rid of later.