Skip to content
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

[prototype] Support base's classes and methods in Plutus Tx #5219

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

zliu41
Copy link
Member

@zliu41 zliu41 commented Mar 20, 2023

This PR requires a patched GHC, so it won't be mergeable for a while. But it is reviewable and I'm looking to get some early feedback.

This PR adds support for Haskell's Eq, Ord and Num. Other classes can be supported in a similar way. The benefits of doing so include:

  • One would be able to write regular Haskell rather than studying and using PlutusTx's version of base, and having to make sure things are imported from the right places.
  • It opens up the possibilities of using third party libraries.
  • Things like matching on integer literals and list range syntax will work (the latter doesn't currently work because we'd need to support enumFromTo, but is easily fixable)
  • This approach, with the help of the GHC patch, makes it easier to generate good error messages for unsupported features, e.g., we will be able to report Unsupported: (==) @Int rather than Unsupported: I#.

User migration will be easy because one can freely mix Haskell's and PlutusTx's typeclasses.

I've updated the modules in plutus-benchmark to use Haskell's typeclasses whenever possible. There are some cost increases, and as far as I can tell, they are all due to the fact that Haskell's Eq has two methods. This can be fixed separately in multiple ways, e.g., with a better inliner, or with some tricks in the plugin itself.

The main changes are in PlutusTx.Compiler.Expr and PlutusTx.Compiler.Dictionary.*. Other changes are mostly boilerplate.

The GHC patch is a fairly simple one: it adds a field in SimplMode and a GHC flag to turn off builtin rules, which stops GHC from turning (==) @Integer into $fEqInteger_$c==.

@bezirg
Copy link
Contributor

bezirg commented Mar 21, 2023

Great work, is this GHC patch going to be mainlined? If so is there a MR to track it?

@@ -11,7 +11,7 @@ steps:
remove_redundant: false

- trailing_whitespace: {}
columns: 100
columns: 99
Copy link
Contributor

Choose a reason for hiding this comment

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

?

@@ -32,6 +32,7 @@ packages: doc/read-the-docs-site
prettyprinter-configurable
word-array
stubs/plutus-ghc-stub
with-compiler: /home/zliu41/ghc/_build/stage1/bin/ghc
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can integrate the GHC patch with Nix inside a local .nix file?

Comment on lines +137 to +139
instance Haskell.Eq ScriptContext where
{-# INLINABLE (==) #-}
(==) = (==)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: use some module qualification to make it look less like a loop

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, what is the reasoning behind this explicit instance instead of the derived one? I am not saying it is wrong, but since we are transitioning to Haskell's builtin Eq,Ord,etc then why not use its builtin deriving as well?

@@ -161,6 +161,7 @@ mkSimplPass flags logger =
, GHC.sm_uf_opts = GHC.defaultUnfoldingOpts
, GHC.sm_dflags = flags
, GHC.sm_rules = False
, GHC.sm_builtin_rules = False
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! Add a comment here or a note on why we have this set to False (default for ghc being True)

@@ -174,13 +177,69 @@ stableModuleCmp m1 m2 =
-- See Note [Stable name comparisons]
(GHC.moduleUnit m1 `GHC.stableUnitCmp` GHC.moduleUnit m2)

class Monad m => MonadCoreM m where
Copy link
Contributor

@bezirg bezirg Mar 21, 2023

Choose a reason for hiding this comment

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

You forgot thelookupThing method.
Wouldn't it be better to just add some orphan instances of the MonadThings class instead? Then only the thNameToGhcName method would be missing, but then you could create a subclass:

class MonadThings m => MonadCoreM m where
    thNameToGhcName = ...

https://hackage.haskell.org/package/ghc-9.2.7/docs/GHC-Types-TyThing.html#t:MonadThings

Copy link
Contributor

@michaelpj michaelpj left a comment

Choose a reason for hiding this comment

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

The implementation seems very fiddly at the moment, I'd be keen to try and reduce that. I also think we probably shouldn't actually merge this unless we can optimize away the worse codegen for basic equality and comparison operations. But it's relatively clear how to do that at least in the monomorphic case, which is most important.

@@ -706,8 +715,6 @@ compileExpr e = withContextM 2 (sdToTxt $ "Compiling expr:" GHC.<+> GHC.ppr e) $
GHC.Var (isErrorId -> True) `GHC.App` GHC.Type t `GHC.App` _ ->
PIR.TyInst annMayInline <$> errorFunc <*> compileTypeNorm t

-- See Note [Uses of Eq]
GHC.Var n | GHC.getName n == GHC.eqName -> throwPlain $ UnsupportedError "Use of == from the Haskell Eq typeclass"
GHC.Var n | GHC.getName n == GHC.integerEqName -> throwPlain $ UnsupportedError "Use of Haskell Integer equality, possibly via the Haskell Eq typeclass"
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this is still bad? i.e. if you get the non-typeclass integer equality function, we still want to die?

Maybe (m (PIRTerm uni fun))
isKnownApp fun args = Map.lookup (splitGhcName (GHC.varName fun)) knownApps >>= ($ args)

-- | A list of applications that should be compiled in specific ways.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is big and complicated, let's put it in its own module.

-- | A list of applications that should be compiled in specific ways.
knownApps ::
CompilingDefault uni fun m ann =>
Map (Maybe String, String) ([GHC.CoreExpr] -> Maybe (m (PIRTerm uni fun)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just define this as a function?

mkKnownApp :: Maybe String -> String -> [GHC.CoreExpr] -> Maybe (m (PIRTerm uni fun))

I think that would make it more readable, and generally I think if you have a statically known map that's only used to do lookups it should probably just be a function?

Copy link
Contributor

Choose a reason for hiding this comment

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

Haven't reviewed this in full, I think it'll be a lot more readable as a function 😇

Map (Maybe String, String) ([GHC.CoreExpr] -> Maybe (m (PIRTerm uni fun)))
knownApps =
Map.fromListWithKey (\n -> error ("knownApps: key defined more than once: " <> show n))
. fmap (first splitThName)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is rather odd: you split the name here and also to look up into it... why not just compare the names for equality?

compileExpr . GHC.Var =<< lookupId =<< thNameToGhcNameOrFail 'Builtins.equalsInteger
[GHC.Type ty, _numDict, GHC.Lit (GHC.LitNumber _ i), GHC.Lit (GHC.LitNumber _ j)]
| ty `GHC.eqType` GHC.integerTy -> Just $ do
-- If both arguments of `(==) @Integer` are literals, we perform constant folding.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't agree with this at all. Keep codegen stupid! Write optimization passes to do optimizations!

@@ -23,7 +30,103 @@ letrec
!acc : Bool = go xs
in
Bool_match
(ifThenElse {Bool} (lessThanEqualsInteger 1 x) False True)
((let
Copy link
Contributor

Choose a reason for hiding this comment

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

wow this is terrible D:

in
Ord_match
{integer}
v
Copy link
Contributor

Choose a reason for hiding this comment

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

A context-based inlining heuristic would help here: inline if the thing you are inlining is a constructor application and the variable is the scrutinee of a pattern match!

Copy link
Contributor

Choose a reason for hiding this comment

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

Also need case-of-known-constructor.

@@ -270,6 +270,82 @@ letrec
{all dead. dead})
{all dead. dead}
in
let
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm unsure why this one changed so much?

Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable PlutusTx.Builtins.Internal.$fEqBuiltinByteString
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we should do the trick for == too, but we also probably want to keep the old fallback error which was helpful.

Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable $c/=
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto


max, min :: Integer -> Integer -> Integer
{-# INLINEABLE max #-}
max x y = if x <= y then y else x
Copy link
Contributor

Choose a reason for hiding this comment

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

wow, I assumed that max is "left-biased" like min, but this is not the case; I learned something today!
Ofcourse you need a "wrong" Eq instance to observe this bias:

data D = D1 | D2

instance Eq D where
    _ == _ = True -- foul instance

deriving instance Ord D
deriving instance Show D

-- >>> D1 == D2
-- True
-- >>> min D1 D2  (leftbiased)
-- D1
-- >>> max D1 D2 (rightbiased)
-- D2

Comment on lines +872 to +875
knownApps ::
CompilingDefault uni fun m ann =>
Map (Maybe String, String) ([GHC.CoreExpr] -> Maybe (m (PIRTerm uni fun)))
knownApps =
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this function could be code-deduplicated slightly

@bezirg
Copy link
Contributor

bezirg commented May 17, 2024

I am not sure if this is still relevant, but here I paste an old comment of mine when I was debugging an older patched GHC (not this one):

Problems encountered:

  1. breaks on banged ! data constructors iff they are defined and compiled in the same module. BangPatterns on expression code works fine however.

  2. Even if we try to avoid relying on ghc base code by using our own prelude, ghc will emit core that implicitly calls some base functions. Implicitly means that even if we never explicitly mentioned base code in Plutus, ghc will emit base code. This happens in at least two cases: a) literal patterns b) (list) stream fusion code.

this is also a problem that should be fixed by the patched-ghc team, by having extensible fields also for base modules.

@zeme-wana zeme-wana force-pushed the master branch 2 times, most recently from a161078 to db5cabb Compare July 9, 2024 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants