Skip to content

Commit 5458fe5

Browse files
authored
[UPLC] [Test] Add scoping tests (#6773)
This adds scoping tests for UPLC the same way we have them for PIR. It also tweaks the way the scoping machinery handles case-expressions for PIR.
1 parent a010dda commit 5458fe5

File tree

13 files changed

+236
-33
lines changed

13 files changed

+236
-33
lines changed

plutus-core/plutus-core.cabal

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ library
195195
UntypedPlutusCore.Check.Scope
196196
UntypedPlutusCore.Check.Uniques
197197
UntypedPlutusCore.Core
198+
UntypedPlutusCore.Core.Instance.Scoping
198199
UntypedPlutusCore.Core.Plated
199200
UntypedPlutusCore.Core.Type
200201
UntypedPlutusCore.Core.Zip
@@ -206,12 +207,19 @@ library
206207
UntypedPlutusCore.Evaluation.Machine.SteppableCek
207208
UntypedPlutusCore.Evaluation.Machine.SteppableCek.DebugDriver
208209
UntypedPlutusCore.Evaluation.Machine.SteppableCek.Internal
210+
UntypedPlutusCore.Mark
209211
UntypedPlutusCore.MkUPlc
210212
UntypedPlutusCore.Parser
211213
UntypedPlutusCore.Purity
212214
UntypedPlutusCore.Rename
215+
UntypedPlutusCore.Rename.Internal
213216
UntypedPlutusCore.Size
214217
UntypedPlutusCore.Transform.CaseOfCase
218+
UntypedPlutusCore.Transform.CaseReduce
219+
UntypedPlutusCore.Transform.Cse
220+
UntypedPlutusCore.Transform.FloatDelay
221+
UntypedPlutusCore.Transform.ForceDelay
222+
UntypedPlutusCore.Transform.Inline
215223
UntypedPlutusCore.Transform.Simplifier
216224

217225
other-modules:
@@ -264,16 +272,9 @@ library
264272
UntypedPlutusCore.Evaluation.Machine.Cek.EmitterMode
265273
UntypedPlutusCore.Evaluation.Machine.Cek.ExBudgetMode
266274
UntypedPlutusCore.Evaluation.Machine.CommonAPI
267-
UntypedPlutusCore.Mark
268-
UntypedPlutusCore.Rename.Internal
269275
UntypedPlutusCore.Simplify
270276
UntypedPlutusCore.Simplify.Opts
271277
UntypedPlutusCore.Subst
272-
UntypedPlutusCore.Transform.CaseReduce
273-
UntypedPlutusCore.Transform.Cse
274-
UntypedPlutusCore.Transform.FloatDelay
275-
UntypedPlutusCore.Transform.ForceDelay
276-
UntypedPlutusCore.Transform.Inline
277278

278279
reexported-modules: Data.SatInt
279280
hs-source-dirs:
@@ -438,6 +439,7 @@ test-suite untyped-plutus-core-test
438439
Evaluation.Regressions
439440
Flat.Spec
440441
Generators
442+
Scoping.Spec
441443
Transform.CaseOfCase.Test
442444
Transform.Simplify
443445
Transform.Simplify.Lib

plutus-core/plutus-core/src/PlutusCore/Check/Scoping.hs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Data.List.NonEmpty (NonEmpty)
2020
import Data.List.NonEmpty qualified as NonEmpty
2121
import Data.Map.Strict as Map
2222
import Data.Maybe
23+
import Data.Proxy
2324
import Data.Set as Set
2425
import Text.Pretty
2526
import Text.PrettyBy
@@ -66,6 +67,11 @@ to touch any annotations) and collect all the available information: which names
6667
which didn't, which appeared etc, simultaneously checking that the names that were supposed to
6768
disappear indeed disappeared and the names that were supposed to stay indeed stayed.
6869
70+
Note that to tell that e.g. a binding disappeared it's crucial that the AST node with the appopriate
71+
annotation is itself preserved, only the name changed. If some pass removes the AST node of a
72+
binding that is supposed to be disappear, it won't be accounted for. Which is precisely what we need
73+
for passes like inlining.
74+
6975
Once all this scoping information is collected, we run 'checkScopeInfo' to check that the
7076
information is coherent. See its docs for the details on what exactly the checked invariants are.
7177
@@ -88,24 +94,24 @@ isSameScope TermName{} TypeName{} = False
8894
data Stays
8995
= StaysOutOfScopeVariable -- ^ An out-of-scope variable does not get renamed and hence stays.
9096
| StaysFreeVariable -- ^ A free variable does not get renamed and hence stays.
91-
deriving stock (Show)
97+
deriving stock (Show, Eq)
9298

9399
-- | Changing names.
94100
data Disappears
95101
= DisappearsBinding -- ^ A binding gets renamed and hence the name that it binds disappears.
96102
| DisappearsVariable -- ^ A bound variable gets renamed and hence its name disappears.
97-
deriving stock (Show)
103+
deriving stock (Show, Eq)
98104

99105
-- | A name either stays or disappears.
100106
data NameAction
101107
= Stays Stays
102108
| Disappears Disappears
103-
deriving stock (Show)
109+
deriving stock (Show, Eq)
104110

105111
data NameAnn
106112
= NameAction NameAction ScopedName
107113
| NotAName
108-
deriving stock (Show)
114+
deriving stock (Show, Eq)
109115

110116
instance Pretty NameAnn where
111117
pretty = viaShow
@@ -282,6 +288,12 @@ class CollectScopeInfo t where
282288
-}
283289
collectScopeInfo :: t NameAnn -> ScopeErrorOrInfo
284290

291+
instance EstablishScoping Proxy where
292+
establishScoping _ = pure Proxy
293+
294+
instance CollectScopeInfo Proxy where
295+
collectScopeInfo _ = mempty
296+
285297
-- See Note [Example of a scoping check].
286298
type Scoping t = (EstablishScoping t, CollectScopeInfo t)
287299

@@ -290,7 +302,7 @@ type Scoping t = (EstablishScoping t, CollectScopeInfo t)
290302
-- original binder with the annotated sort and its value, but also decorate the reassembled binder
291303
-- with one out-of-scope variable and one in-scope one.
292304
establishScopingBinder
293-
:: (Reference name value, ToScopedName name, Scoping sort, Scoping value)
305+
:: (Reference name value, ToScopedName name, EstablishScoping sort, EstablishScoping value)
294306
=> (NameAnn -> name -> sort NameAnn -> value NameAnn -> value NameAnn)
295307
-> name
296308
-> sort ann
@@ -459,19 +471,22 @@ checkScopeInfo bindRem scopeInfo = do
459471

460472
-- | The type of errors that the scope checking machinery returns.
461473
data ScopeCheckError t = ScopeCheckError
462-
{ _input :: !(t NameAnn) -- ^ What got fed to the scoping check pass.
463-
, _output :: !(t NameAnn) -- ^ What got out of it.
464-
, _error :: !ScopeError -- ^ The error returned by the scoping check pass.
474+
{ _input :: !(t NameAnn) -- ^ What got fed to the scoping check pass before preparation.
475+
, _prepared :: !(t NameAnn) -- ^ What got fed to the scoping check pass after preparation.
476+
, _output :: !(t NameAnn) -- ^ What got out of it.
477+
, _error :: !ScopeError -- ^ The error returned by the scoping check pass.
465478
}
466479

467480
deriving stock instance Show (t NameAnn) => Show (ScopeCheckError t)
468481

469482
instance PrettyBy config (t NameAnn) => PrettyBy config (ScopeCheckError t) where
470-
prettyBy config (ScopeCheckError input output err) = vsep
483+
prettyBy config (ScopeCheckError input prepared output err) = vsep
471484
[ pretty err
472485
, "when checking that transformation of" <> hardline
473486
, indent 2 $ prettyBy config input <> hardline
474487
, "to" <> hardline
488+
, indent 2 $ prettyBy config prepared <> hardline
489+
, "to" <> hardline
475490
, indent 2 $ prettyBy config output <> hardline
476491
, "is correct"
477492
]
@@ -490,8 +505,9 @@ checkRespectsScoping
490505
-> t ann
491506
-> Either (ScopeCheckError t) ()
492507
checkRespectsScoping bindRem prep run thing =
493-
first (ScopeCheckError input output) $
508+
first (ScopeCheckError input prepared output) $
494509
unScopeErrorOrInfo (collectScopeInfo output) >>= checkScopeInfo bindRem
495510
where
496-
input = prep . runQuote $ establishScoping thing
497-
output = run input
511+
input = runQuote $ establishScoping thing
512+
prepared = prep input
513+
output = run prepared

plutus-core/plutus-core/src/PlutusCore/Core/Instance/Scoping.hs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
module PlutusCore.Core.Instance.Scoping () where
88

9+
import PlutusPrelude
10+
911
import PlutusCore.Check.Scoping
1012
import PlutusCore.Core.Type
1113
import PlutusCore.Name.Unique
@@ -51,14 +53,18 @@ instance tyname ~ TyName => EstablishScoping (Type tyname uni) where
5153
establishScoping (TySOP _ tyls) =
5254
TySOP NotAName <$> (traverse . traverse) establishScoping tyls
5355

56+
firstBound :: Term tyname name uni fun ann -> [name]
57+
firstBound (Apply _ (LamAbs _ name _ body) _) = name : firstBound body
58+
firstBound _ = []
59+
5460
instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Term tyname name uni fun) where
5561
establishScoping (LamAbs _ nameDup ty body) = do
5662
name <- freshenName nameDup
5763
establishScopingBinder LamAbs name ty body
5864
establishScoping (TyAbs _ nameDup kind body) = do
5965
name <- freshenTyName nameDup
6066
establishScopingBinder TyAbs name kind body
61-
establishScoping (IWrap _ pat arg term) =
67+
establishScoping (IWrap _ pat arg term) =
6268
IWrap NotAName <$> establishScoping pat <*> establishScoping arg <*> establishScoping term
6369
establishScoping (Apply _ fun arg) =
6470
Apply NotAName <$> establishScoping fun <*> establishScoping arg
@@ -73,8 +79,18 @@ instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Term tyname name un
7379
establishScoping (Builtin _ bi) = pure $ Builtin NotAName bi
7480
establishScoping (Constr _ ty i es) =
7581
Constr NotAName <$> establishScoping ty <*> pure i <*> traverse establishScoping es
76-
establishScoping (Case _ ty a es) =
77-
Case NotAName <$> establishScoping ty <*> establishScoping a <*> traverse establishScoping es
82+
establishScoping (Case _ ty a es) = do
83+
esScoped <- traverse establishScoping es
84+
let esScopedPoked = addTheRest $ map (\e -> (e, firstBound e)) esScoped
85+
branchBounds = map (snd . fst) esScopedPoked
86+
referenceInBranch ((branch, _), others) = referenceOutOfScope (map snd others) branch
87+
tyScoped <- establishScoping ty
88+
aScoped <- establishScoping a
89+
-- For each of the branches reference (as out-of-scope) the variables bound in that branch
90+
-- in all the other ones, as well as outside of the whole case-expression. This is to check
91+
-- that none of the transformations leak variables outside of the branch they're bound in.
92+
pure . referenceOutOfScope branchBounds $
93+
Case NotAName tyScoped aScoped $ map referenceInBranch esScopedPoked
7894

7995
instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Program tyname name uni fun) where
8096
establishScoping (Program _ ver term) =

plutus-core/plutus-ir/src/PlutusIR/Core/Instance/Scoping.hs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
{-# OPTIONS_GHC -Wno-orphans #-}
99
module PlutusIR.Core.Instance.Scoping where
1010

11+
import PlutusPrelude
12+
1113
import PlutusIR.Core.Type
1214

1315
import PlutusCore.Check.Scoping
1416
import PlutusCore.MkPlc
1517
import PlutusCore.Quote
1618

17-
import Data.Foldable
18-
import Data.List.NonEmpty (NonEmpty (..), (<|))
19+
import Data.List.NonEmpty ((<|))
1920
import Data.List.NonEmpty qualified as NonEmpty
20-
import Data.Traversable
2121

2222
instance tyname ~ TyName => Reference TyName (Term tyname name uni fun) where
2323
referenceVia reg tyname term = TyInst NotAName term $ TyVar (reg tyname) tyname
@@ -219,6 +219,10 @@ registerByRecursivity :: ToScopedName name => Recursivity -> name -> NameAnn
219219
registerByRecursivity Rec = registerBound
220220
registerByRecursivity NonRec = registerOutOfScope
221221

222+
firstBound :: Term tyname name uni fun ann -> [name]
223+
firstBound (Apply _ (LamAbs _ name _ body) _) = name : firstBound body
224+
firstBound _ = []
225+
222226
instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Term tyname name uni fun) where
223227
establishScoping (Let _ recy bindingsDup body) = do
224228
bindings <- establishScopingBindings (registerByRecursivity recy) bindingsDup
@@ -249,7 +253,18 @@ instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Term tyname name un
249253
establishScoping (Constant _ con) = pure $ Constant NotAName con
250254
establishScoping (Builtin _ bi) = pure $ Builtin NotAName bi
251255
establishScoping (Constr _ ty i es) = Constr NotAName <$> establishScoping ty <*> pure i <*> traverse establishScoping es
252-
establishScoping (Case _ ty arg cs) = Case NotAName <$> establishScoping ty <*> establishScoping arg <*> traverse establishScoping cs
256+
establishScoping (Case _ ty a es) = do
257+
esScoped <- traverse establishScoping es
258+
let esScopedPoked = addTheRest $ map (\e -> (e, firstBound e)) esScoped
259+
branchBounds = map (snd . fst) esScopedPoked
260+
referenceInBranch ((branch, _), others) = referenceOutOfScope (map snd others) branch
261+
tyScoped <- establishScoping ty
262+
aScoped <- establishScoping a
263+
-- For each of the branches reference (as out-of-scope) the variables bound in that branch
264+
-- in all the other ones, as well as outside of the whole case-expression. This is to check
265+
-- that none of the transformations leak variables outside of the branch they're bound in.
266+
pure . referenceOutOfScope branchBounds $
267+
Case NotAName tyScoped aScoped $ map referenceInBranch esScopedPoked
253268

254269
instance (tyname ~ TyName, name ~ Name) => EstablishScoping (Program tyname name uni fun) where
255270
establishScoping (Program _ v term) = Program NotAName v <$> establishScoping term

plutus-core/plutus-ir/test/PlutusIR/Scoping/Tests.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test_names :: TestTree
3030
test_names = testGroup "names"
3131
[ T.test_scopingGood "beta-reduction" genTerm T.BindingRemovalNotOk T.PrerenameYes $
3232
pure . beta
33-
, T.test_scopingGood "case-of-known-constructor" genTerm T.BindingRemovalNotOk T.PrerenameYes $
33+
, T.test_scopingGood "case-of-known-constructor" genTerm T.BindingRemovalOk T.PrerenameYes $
3434
pure . caseReduce
3535
, T.test_scopingGood "commuteFnWithConst" genTerm T.BindingRemovalNotOk T.PrerenameYes $
3636
pure . commuteFnWithConst

plutus-core/prelude/PlutusPrelude.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ module PlutusPrelude
9898
, distinct
9999
, unsafeFromRight
100100
, tryError
101+
, addTheRest
101102
, modifyError
102103
, lowerInitialChar
103104
) where
@@ -266,6 +267,14 @@ tryError :: MonadError e m => m a -> m (Either e a)
266267
tryError a = (Right <$> a) `catchError` (pure . Left)
267268
{-# INLINE tryError #-}
268269

270+
-- | Pair each element of the given list with all the other elements.
271+
--
272+
-- >>> addTheRest "abcd"
273+
-- [('a',"bcd"),('b',"acd"),('c',"abd"),('d',"abc")]
274+
addTheRest :: [a] -> [(a, [a])]
275+
addTheRest [] = []
276+
addTheRest (x:xs) = (x, xs) : map (fmap (x :)) (addTheRest xs)
277+
269278
{- A different 'MonadError' analogue to the 'withExceptT' function.
270279
Modify the value (and possibly the type) of an error in an @ExceptT@-transformed
271280
monad, while stripping the @ExceptT@ layer.

plutus-core/testlib/PlutusCore/Test.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,8 @@ prop_scopingFor ::
604604
Property
605605
prop_scopingFor gen bindRem preren run = withTests 200 . property $ do
606606
prog <- forAllNoShow $ runAstGen gen
607-
let catchEverything = unsafePerformIO . try @SomeException . evaluate
607+
let -- TODO: use @enclosed-exceptions@.
608+
catchEverything = unsafePerformIO . try @SomeException . evaluate
608609
prep = runPrerename preren
609610
case catchEverything $ checkRespectsScoping bindRem prep (TPLC.runQuote . run) prog of
610611
Left exc -> fail $ displayException exc

plutus-core/untyped-plutus-core/src/UntypedPlutusCore/Core/Instance.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ module UntypedPlutusCore.Core.Instance (module Export) where
33
import UntypedPlutusCore.Core.Instance.Eq ()
44
import UntypedPlutusCore.Core.Instance.Flat as Export
55
import UntypedPlutusCore.Core.Instance.Pretty ()
6+
import UntypedPlutusCore.Core.Instance.Scoping ()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{-# OPTIONS_GHC -fno-warn-orphans #-}
2+
3+
{-# LANGUAGE MultiParamTypeClasses #-}
4+
{-# LANGUAGE TypeFamilies #-}
5+
{-# LANGUAGE TypeOperators #-}
6+
7+
module UntypedPlutusCore.Core.Instance.Scoping () where
8+
9+
import PlutusPrelude
10+
11+
import UntypedPlutusCore.Core.Type
12+
13+
import PlutusCore.Check.Scoping
14+
import PlutusCore.Name.Unique
15+
import PlutusCore.Quote
16+
17+
import Data.Proxy
18+
import Data.Vector qualified as Vector
19+
20+
firstBound :: Term name uni fun ann -> [name]
21+
firstBound (Apply _ (LamAbs _ name body) _) = name : firstBound body
22+
firstBound _ = []
23+
24+
instance name ~ Name => Reference Name (Term name uni fun) where
25+
referenceVia reg name term = Apply NotAName term $ Var (reg name) name
26+
27+
instance name ~ Name => EstablishScoping (Term name uni fun) where
28+
establishScoping (LamAbs _ nameDup body) = do
29+
name <- freshenName nameDup
30+
establishScopingBinder (\ann name' _ -> LamAbs ann name') name Proxy body
31+
establishScoping (Delay _ body) = Delay NotAName <$> establishScoping body
32+
establishScoping (Apply _ fun arg) =
33+
Apply NotAName <$> establishScoping fun <*> establishScoping arg
34+
establishScoping (Error _) = pure $ Error NotAName
35+
establishScoping (Force _ term) = Force NotAName <$> establishScoping term
36+
establishScoping (Var _ nameDup) = do
37+
name <- freshenName nameDup
38+
pure $ Var (registerFree name) name
39+
establishScoping (Constant _ con) = pure $ Constant NotAName con
40+
establishScoping (Builtin _ bi) = pure $ Builtin NotAName bi
41+
establishScoping (Constr _ i es) = Constr NotAName <$> pure i <*> traverse establishScoping es
42+
establishScoping (Case _ a es) = do
43+
esScoped <- traverse establishScoping es
44+
let esScopedPoked = addTheRest . map (\e -> (e, firstBound e)) $ Vector.toList esScoped
45+
branchBounds = map (snd . fst) esScopedPoked
46+
referenceInBranch ((branch, _), others) = referenceOutOfScope (map snd others) branch
47+
aScoped <- establishScoping a
48+
-- For each of the branches reference (as out-of-scope) the variables bound in that branch
49+
-- in all the other ones, as well as outside of the whole case-expression. This is to check
50+
-- that none of the transformations leak variables outside of the branch they're bound in.
51+
pure . referenceOutOfScope branchBounds $
52+
Case NotAName aScoped . Vector.fromList $ map referenceInBranch esScopedPoked
53+
54+
instance name ~ Name => EstablishScoping (Program name uni fun) where
55+
establishScoping (Program _ ver term) = Program NotAName ver <$> establishScoping term
56+
57+
instance name ~ Name => CollectScopeInfo (Term name uni fun) where
58+
collectScopeInfo (LamAbs ann name body) = handleSname ann name <> collectScopeInfo body
59+
collectScopeInfo (Delay _ body) = collectScopeInfo body
60+
collectScopeInfo (Apply _ fun arg) = collectScopeInfo fun <> collectScopeInfo arg
61+
collectScopeInfo (Error _) = mempty
62+
collectScopeInfo (Force _ term) = collectScopeInfo term
63+
collectScopeInfo (Var ann name) = handleSname ann name
64+
collectScopeInfo (Constant _ _) = mempty
65+
collectScopeInfo (Builtin _ _) = mempty
66+
collectScopeInfo (Constr _ _ es) = foldMap collectScopeInfo es
67+
collectScopeInfo (Case _ arg cs) = collectScopeInfo arg <> foldMap collectScopeInfo cs
68+
69+
instance name ~ Name => CollectScopeInfo (Program name uni fun) where
70+
collectScopeInfo (Program _ _ term) = collectScopeInfo term

0 commit comments

Comments
 (0)