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

[Builtins] Add detection of stuck 'ToHoles' #4345

Merged
merged 1 commit into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 36 additions & 48 deletions plutus-core/plutus-core/src/PlutusCore/Constant/Debug.hs
Original file line number Diff line number Diff line change
@@ -1,63 +1,51 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
{-# OPTIONS_GHC -fno-warn-simplifiable-class-constraints #-}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}

-- | This module helps to visualize and debug the 'TypeScheme' inference machinery from the
-- @Meaning@ module.
module PlutusCore.Constant.Debug where
module PlutusCore.Constant.Debug
( module PlutusCore.Constant.Debug
-- Reexporting a bunch of stuff, so that debug output is not littered with module names.
, module Export
) where

import PlutusCore.Constant.Meaning
import PlutusCore.Constant.Typed
import PlutusCore.Core
import PlutusCore.Default
import PlutusCore.Name
import PlutusCore.Constant.Meaning as Export
import PlutusCore.Constant.Typed as Export
import PlutusCore.Core as Export
import PlutusCore.Default as Export
import PlutusCore.Name as Export

type TermDebug = Term TyName Name DefaultUni DefaultFun ()

-- | Instantiate type variables in the type of a value using 'EnumerateFromTo'.
-- This function only performs the enumeration and checks that the result does not have certain
-- constraints, so it's allowed for the type to reference types that are not in the universe.
-- Example usages:
-- | Instantiate type variables in the type of a value using 'ElaborateFromTo'. Example usages:
--
-- >>> :t enumerateDebug False
-- enumerateDebug False :: Bool
-- >>> :t enumerateDebug $ \_ -> ()
-- enumerateDebug $ \_ -> ()
-- >>> :t elaborateDebug False
-- elaborateDebug False :: Bool
-- >>> :t elaborateDebug $ \_ -> ()
-- elaborateDebug $ \_ -> ()
-- :: Opaque
-- (Term TyName Name DefaultUni DefaultFun ())
-- (TyVarRep ('TyNameRep "a" 0))
-- -> ()
-- >>> :t enumerateDebug 42
-- <interactive>:1:16: error:
-- >>> :t elaborateDebug $ Just ()
-- <interactive>:1:1: error:
-- • No instance for ‘KnownTypeAst DefaultUni (Maybe ())’
-- Which means
-- ‘Maybe ()’
-- is neither a built-in type, nor one of the control types.
-- If it can be represented in terms of one of the built-in types
-- then go add the instance (you may need a ‘KnownTypeIn’ one too)
-- alongside the instance for the built-in type.
-- Otherwise you may need to add a new built-in type
-- (provided you're doing something that can be supported in principle)
-- • In the expression: elaborateDebug $ Just ()
-- >>> :t elaborateDebug $ 0 + 42
-- <interactive>:1:18: error:
-- • Built-in functions are not allowed to have constraints
-- To fix this error instantiate all constrained type variables
-- • In the first argument of ‘enumerateDebug’, namely ‘42’
-- In the expression: enumerateDebug 42
specializeDebug :: forall a j. SpecializeFromTo 0 j TermDebug a => a -> a
specializeDebug = id

-- | Instantiate type variables in the type of a value using 'EnumerateFromTo' and check that it's
-- possible to construct a 'TypeScheme' out of the resulting type. Example usages:
--
-- >>> :t inferDebug False
-- inferDebug False :: Bool
-- >>> :t inferDebug $ Just 'a'
-- <interactive>:1:1: error:
-- • No instance for (KnownPolytype
-- (ToBinds (Maybe Char)) TermDebug '[] (Maybe Char) (Maybe Char))
-- arising from a use of ‘inferDebug’
-- • In the expression: inferDebug $ Just 'a'
-- >>> :t inferDebug $ \_ -> ()
-- inferDebug $ \_ -> ()
-- :: Opaque
-- (Term TyName Name DefaultUni DefaultFun ())
-- (TyVarRep ('TyNameRep "a" 0))
-- -> ()
inferDebug
:: forall a binds args res j.
( args ~ GetArgs a, a ~ FoldArgs args res, binds ~ Merge (ListToBinds args) (ToBinds res)
, KnownPolytype binds TermDebug args res a, SpecializeFromTo 0 j TermDebug a
)
Comment on lines -57 to -61
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the last major refactoring of the builtins API elaboration needs KnownTypeAst instances and so there isn't much sense in having two debug functions anymore, hence I only left one. A single function makes for a better UX anyway.

-- • In the second argument of ‘($)’, namely ‘0 + 42’
-- In the expression: elaborateDebug $ 0 + 42
elaborateDebug
:: forall a j. ElaborateFromTo 0 j (Term TyName Name DefaultUni DefaultFun ()) a
=> a -> a
inferDebug = id
elaborateDebug = id
41 changes: 36 additions & 5 deletions plutus-core/plutus-core/src/PlutusCore/Constant/Meaning.hs
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,46 @@ instance
, HandleHolesGo j k term holes
) => HandleHolesGo i k term (hole ': holes)

-- | If the outermost constructor of the second argument is known and happens to be one of the
-- constructors of the list data type, then the second argument is returned back. Otherwise the
-- first one is returned, which is meant to be a custom type error.
type ThrowOnStuckList :: forall a. [a] -> [a] -> [a]
type family ThrowOnStuckList err xs where
ThrowOnStuckList _ '[] = '[]
ThrowOnStuckList _ (x ': xs) = x ': xs
ThrowOnStuckList err _ = err
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 kind of horrified that this works 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have you read the linked blog post? The err does not actually get returned. ThrowOnStuckList is as stuck as the stuck list that it's applied to. And that stuckiness causes err to appear in the application and be triggered eventually.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I understand that, it's just kind of bizarre.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not as bizarre as the fact that ThrowOnStuckList can actually return the first argument if the second one is neither stuck nor a constructor of [], but that's Haskell's type-level Python.


type UnknownTypeError :: forall a any. GHC.Type -> a -> any
type family UnknownTypeError term x where
UnknownTypeError term x = TypeError
( 'Text "No instance for ‘KnownTypeAst "
':<>: 'ShowType (UniOf term)
':<>: 'Text " ("
':<>: 'ShowType x
':<>: 'Text ")’"
':$$: 'Text "Which means"
':$$: 'Text " ‘" ':<>: 'ShowType x ':<>: 'Text "’"
':$$: 'Text "is neither a built-in type, nor one of the control types."
':$$: 'Text "If it can be represented in terms of one of the built-in types"
':$$: 'Text " then go add the instance (you may need a ‘KnownTypeIn’ one too)"
':$$: 'Text " alongside the instance for the built-in type."
':$$: 'Text "Otherwise you may need to add a new built-in type"
':$$: 'Text " (provided you're doing something that can be supported in principle)"
)

-- | Get the holes of @x@ and recurse into them.
type HandleHoles :: forall a. Nat -> Nat -> GHC.Type -> a -> GHC.Constraint
type HandleHoles i j term x = HandleHolesGo i j term (ToHoles x)
type HandleHoles i j term x =
-- Here we detect a stuck application of 'ToHoles' and throw 'UnknownTypeError' on it.
-- See https://blog.csongor.co.uk/report-stuck-families for a detailed description of how
-- detection of stuck type families works.
HandleHolesGo i j term (ThrowOnStuckList (UnknownTypeError term x) (ToHoles x))

-- | Specialize each Haskell type variable in @a@ as a type representing a PLC type variable.
-- @i@ is a fresh id and @j@ is a final one as in 'TrySpecializeAsVar', but since 'HandleHole' can
-- specialize multiple variables, @j@ can be equal to @i + n@ for any @n@ (including @0@).
type SpecializeFromTo :: Nat -> Nat -> GHC.Type -> GHC.Type -> GHC.Constraint
type SpecializeFromTo i j term a = HandleHole i j term (TypeHole a)
type ElaborateFromTo :: Nat -> Nat -> GHC.Type -> GHC.Type -> GHC.Constraint
type ElaborateFromTo i j term a = HandleHole i j term (TypeHole a)

-- See Note [Automatic derivation of type schemes]
-- | Construct the meaning for a built-in function by automatically deriving its
Expand All @@ -306,8 +337,8 @@ type SpecializeFromTo i j term a = HandleHole i j term (TypeHole a)
-- 2. an uninstantiated costing function
makeBuiltinMeaning
:: forall a term cost binds args res j.
( args ~ GetArgs a, a ~ FoldArgs args res, binds ~ Merge (ListToBinds args) (ToBinds res)
, KnownPolytype binds term args res a, SpecializeFromTo 0 j term a
( binds ~ ToBinds a, args ~ GetArgs a, a ~ FoldArgs args res
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I realized I can now call ToBinds directly over a, no need to deconstruct a to get its bound variables.

, ElaborateFromTo 0 j term a, KnownPolytype binds term args res a
)
=> a -> (cost -> FoldArgsEx args) -> BuiltinMeaning term cost
makeBuiltinMeaning = BuiltinMeaning (knownPolytype (Proxy @binds) :: TypeScheme term args res)
13 changes: 0 additions & 13 deletions plutus-core/plutus-core/src/PlutusCore/Constant/Typed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ module PlutusCore.Constant.Typed
, KnownBuiltinTypeAst
, KnownTypeAst (..)
, Merge
, ListToBinds
, KnownBuiltinTypeIn
, KnownBuiltinType
, readKnownConstant
Expand Down Expand Up @@ -622,18 +621,6 @@ type family Merge xs ys :: [a] where
Merge '[] ys = ys
Merge (x ': xs) ys = x ': Delete x (Merge xs ys)

-- There's no sensible way to provide a 'KnownTypeAst' instance for a type-level list, so we
-- create a separate type family. We could have a single type family on the top level for both
-- 'ToBinds' and 'ListToBinds', but then we'd lose a very convenient default of each type from the
-- universe returning an empty list from 'ToBinds' and the user would need to do provide a
-- @type instance@ themselves (which is no big deal, but it's nicer not to ask the user to do that).
-- | Collect all unique variables (a variable consists of a textual name, a unique and a kind)
-- in a list.
type ListToBinds :: forall a. [a] -> [GADT.Some TyNameRep]
type family ListToBinds xs
type instance ListToBinds '[] = '[]
type instance ListToBinds (x ': xs) = Merge (ToBinds x) (ListToBinds xs)

-- | A constraint for \"@a@ is a 'KnownType' by means of being included in @uni@\".
type KnownBuiltinTypeIn uni term a = (HasConstantIn uni term, GShow uni, GEq uni, uni `Contains` a)

Expand Down