Skip to content

Remove the deriving-aeson dependency - #7871

Open
Unisay wants to merge 2 commits into
masterfrom
yura/issue-7870-remove-deriving-aeson
Open

Remove the deriving-aeson dependency#7871
Unisay wants to merge 2 commits into
masterfrom
yura/issue-7870-remove-deriving-aeson

Conversation

@Unisay

@Unisay Unisay commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

plutus-core used deriving-aeson only for the CustomJSON deriving-via instances of the cost model types. This PR rewrites those as plain aeson generic instances (genericToJSON/genericToEncoding/genericParseJSON with explicit Options) and drops both the dependency and the allow-newer: deriving-aeson:aeson exception that #7820 had to add because deriving-aeson caps aeson < 2.3 on Hackage. The only public API change: PlutusCore.Evaluation.Machine.ExBudget no longer exports LowerInitialCharacter, which existed solely to support deriving-aeson.

The serialized JSON is unchanged, and the two commits are ordered so that this is checkable. The first adds tests comparing the serialized cost models against the checked-in cost-model/data files for every semantics variant, using those files as the expected output rather than a copy of them. The tests pass against the existing instances, and the second commit replaces the instances without touching them.

Closes #7870.

@Unisay
Unisay force-pushed the yura/issue-7870-remove-deriving-aeson branch from 31dd5c7 to 6e995d2 Compare July 28, 2026 08:19
@Unisay Unisay self-assigned this Jul 28, 2026
@Unisay
Unisay marked this pull request as ready for review July 28, 2026 09:04
@Unisay
Unisay requested review from a team and kwxm July 28, 2026 09:04

@zliu41 zliu41 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What tests exist that confirm that the behavior didn't change?

@Unisay
Unisay force-pushed the yura/issue-7870-remove-deriving-aeson branch 3 times, most recently from 9bf6ad6 to 3cb9eae Compare July 28, 2026 17:10
@Unisay

Unisay commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Added them in the first commit, so they run against the old instances and again unchanged against the new ones.

The serialized cost models are compared against the checked-in cost-model/data/*.json files for every semantics variant. Those files are the frozen format, so they serve as the expected output directly and there is no second copy to keep in sync. That covers renamed fields, renamed constructor tags, and fields appearing or disappearing, and it is stronger than what we had: the TH loading in ExBudgetingDefaults only checks that the files still parse, never that the encoder reproduces them. The remaining cases cover the field-omitting instances that generate-cost-model relies on.

Everything is compared as Values rather than bytes, since field order carries no meaning here: every consumer either parses the JSON or goes through CostModelParams, which is a sorted map. Differences are reported as a JSON patch via aeson-diff, so a failure names the offending entries by path instead of printing both cost models in full.

One thing worth being explicit about: the models under test are loaded from those same files by TH, so this is a fixed-point check on the encoder rather than on the cost model numbers, and anything that breaks decoding already fails at the splice. What it adds on top is the case where toJSON drifts away from parseJSON, which is newly possible now that the two are written out separately.

The rest was already covered elsewhere: CostModelInterface.Spec pins the flattened parameter keys against a checked-in params file, plutus-ledger-api has goldens for the ledger parameter names and counts, and the Agda conformance suite runs fromJSON @BuiltinCostKeyMap . toJSON over the builtin costs, cross-checking the generic encoder against the hand-written SimpleJSON parser.

@zliu41

zliu41 commented Jul 28, 2026

Copy link
Copy Markdown
Member

Also, allow-newer is bad if it is needed by a package released on CHaP, since all downstream repos must also declare allow-newer. Is this the case with deriving-aeson? If not, then I'm not sure what the advantage of removing it is, since it does make our code more complex.

@Unisay
Unisay force-pushed the yura/issue-7870-remove-deriving-aeson branch 2 times, most recently from 5255399 to 461ecfb Compare July 29, 2026 08:19
@Unisay

Unisay commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Also, allow-newer is bad if it is needed by a package released on CHaP, since all downstream repos must also declare allow-newer. Is this the case with deriving-aeson?

Indeed, this is the reason - deriving-aeson has incompatible constraints, so without this PR all downstream consumers that include it in their build plan (directly or transitively) would need to declare the same allow-newer exception.

@Unisay
Unisay requested a review from zliu41 July 29, 2026 13:37
@zliu41

zliu41 commented Jul 30, 2026

Copy link
Copy Markdown
Member

@kwxm please review

Unisay added 2 commits July 30, 2026 12:23
Compare the serialized form of BuiltinCostModelBase and CekMachineCostsBase
against the checked-in cost-model/data files for every semantics variant,
and cover the field-omitting instances used by generate-cost-model.
Differences are reported as a JSON patch so that a failure names the
offending entries instead of printing both cost models in full.
Replace the CustomJSON deriving-via JSON instances of the cost model
types with plain aeson generic instances (genericToJSON,
genericToEncoding, genericParseJSON with explicit Options). The JSON
format is unchanged. Drops the allow-newer exception for
deriving-aeson:aeson from cabal.project.

Issue: #7870
@Unisay
Unisay force-pushed the yura/issue-7870-remove-deriving-aeson branch from 461ecfb to 63512e4 Compare July 30, 2026 10:25
@IntersectMBO IntersectMBO deleted a comment Jul 30, 2026

@kwxm kwxm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fine: it simplifies the dependencies and doesn't make things significantly more complicated: if fact making things a bit more explicit may be clearer.

I had a couple of comments where I wondered if things could be improved/simplified, but they're not urgent; I think @Unisay is off at the moment and I don't want to delay merging until he gets back. I made a ticket to look at those later.

constructor name and the remaining `MinSize` is converted to
`min_size` by the Aeson library's
[`CamelToSnake`](https://hackage.haskell.org/package/deriving-aeson-0.2.8/docs/Deriving-Aeson.html#t:CamelToSnake)
[`camelTo2`](https://hackage.haskell.org/package/aeson/docs/Data-Aeson.html#v:camelTo2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit worried that these might give different results, but if you look at the implementation in deriving-aeson it has

data CamelTo (separator :: Symbol)
...
type CamelToSnake = CamelTo  "_"
...
instance (KnownSymbol separator, NonEmptyString separator) => StringModifier (CamelTo separator) where
  getStringModifier = camelTo2 char
    where
      char = case symbolVal (Proxy @separator) of
        c : _ -> c
        _ -> error "Impossible"

so the previous code was secretly using camelTo2 anyway.

It does look as if you have to be a bit careful about what conversion functions like this do in general. For example we have a constructor called LinearInMaxYZ and there's a question of what happens if you have a sequence of multiple uppercase letters. In this case camelTo2 '_' gives linear_in_max_yz, but linear_in_max_y_z might be justifiable as well (and it looks as if there are functions in other libraries that would do that).

ModelJSON "modelConstantOrTwoArguments" ModelConstantOrTwoArguments
instance
ToJSON ModelConstantOrTwoArguments
instance FromJSON ModelOneArgument where

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code here at least is at about the same level of complexity as the previous version, but maybe a bit more explicit, so it seems like a reasonable replacement. It's still very repetitive though (and that maybe makes it tempting to cut and paste when we introduce something new, which carries a risk of failing to update some of the pasted code). I wonder if we can simplify it by introducing a typeclass of our own to do some of the work? We probably want to merge this quite soon though, so we could come back to that later instead of trying to fix it now. I've made a ticket to think about that.

The field names, constructor tags and structure of 'BuiltinCostModelBase' and
'CekMachineCostsBase' are a frozen format: the serialized form has to match the checked-in
@cost-model/data/*.json@ files, and the flattened keys become the ledger's cost model
parameter names. Renaming any of them is an observable breakage rather than an implementation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment true? The ledger currently just uses an array of untagged numbers, although I think it maybe needed the tags in the past. It's probably best not to change anything though, especially if there's a risk of changing the ordering.

-- | Every semantics variant, with the data files holding its frozen cost model.
variants :: [(String, BuiltinSemanticsVariant DefaultFun, FilePath, FilePath)]
variants =
[ ("A", DefaultFunSemanticsVariantA, DFP.builtinCostModelFileA, DFP.cekMachineCostsFileA)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to remember to update this if we ever add a new variant. Can we add a check that we've covered all of the variants here? It's possible that we need stuff like this elsewhere, so maybe we could implement a single function somewhere that returns all of the variants and their associated files and use that here.

import PlutusCore.Evaluation.Machine.CostingFun.JSON ()
import PlutusCore.Evaluation.Machine.ExBudget

import Barbies

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe qualify this so we can see what we're using?

I tried import Barbies (ConstraintsB, TraversableB, FunctorB) but that mysteriously gave me several errors like this:

plutus-core/src/PlutusCore/Evaluation/Machine/BuiltinCostModel.hs:202:46: error: [GHC-18872]
    • Couldn't match representation of type: GHC.Generics.K1
                                               GHC.Generics.R (f ModelFourArguments)
                               with that of: barbies-2.1.1.0:Data.Generics.GenericN.Rec
                                               (barbies-2.1.1.0:Data.Generics.GenericN.Param
                                                  0 f ModelFourArguments)
                                               (f ModelFourArguments)
        arising from the 'deriving' clause of a data type declaration
        The data constructor ‘GHC.Generics.K1’
          of newtype ‘GHC.Generics.K1’ is not in scope
    • When deriving the instance for (ConstraintsB
                                        BuiltinCostModelBase)
    |
202 |   deriving anyclass (FunctorB, TraversableB, ConstraintsB)
    |                                              ^^^^^^^^^^^^

Not sure what's going on there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove the deriving-aeson dependency

3 participants