Skip to content

Commit e4e6afb

Browse files
committed
refactor(cek): fuse free-variable shifting into discharge traversal
Replace the two-pass dischargeCekValue implementation (discharge + shiftTermBy post-pass) with a single-pass approach that threads a global shift parameter through goValue/goValEnv. This avoids a separate traversal for shifting and handles truly free variables (not found in the environment) consistently. - Add shiftNamedDeBruijn utility to PlutusCore.DeBruijn - Thread `global` shift parameter through goValue and goValEnv - Delete the standalone shiftTermBy function - Add 4 new tests for truly free vars past non-empty environments
1 parent 80124dc commit e4e6afb

4 files changed

Lines changed: 116 additions & 43 deletions

File tree

plutus-core/plutus-core/src/PlutusCore/DeBruijn.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module PlutusCore.DeBruijn
3535
, deBruijnInitIndex
3636
, fromFake
3737
, toFake
38+
, shiftNamedDeBruijn
3839
) where
3940

4041
import PlutusCore.DeBruijn.Internal

plutus-core/plutus-core/src/PlutusCore/DeBruijn/Internal.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ module PlutusCore.DeBruijn.Internal
4242
, deBruijnInitIndex
4343
, toFake
4444
, fromFake
45+
, shiftNamedDeBruijn
4546
) where
4647

4748
import PlutusCore.Name.Unique
@@ -107,6 +108,10 @@ newtype Index = Index Word64
107108
deBruijnInitIndex :: Index
108109
deBruijnInitIndex = 0
109110

111+
-- | Shift a 'NamedDeBruijn' index by a given amount.
112+
shiftNamedDeBruijn :: Word64 -> NamedDeBruijn -> NamedDeBruijn
113+
shiftNamedDeBruijn i (NamedDeBruijn t (Index n)) = NamedDeBruijn t (Index (n + i))
114+
110115
-- The bangs gave us a speedup of 6%.
111116

112117
-- | A term name as a de Bruijn index.

plutus-core/untyped-plutus-core/src/UntypedPlutusCore/Evaluation/Machine/Cek/Internal.hs

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -653,76 +653,61 @@ dischargeResultToTerm (DischargeConstant val) = Constant () val
653653
dischargeResultToTerm (DischargeNonConstant term) = term
654654

655655
{-| Convert a 'CekValue' into a 'Term' by replacing all bound variables with the terms
656-
they're bound to (which themselves have to be obtained by recursively discharging values). -}
656+
they're bound to (which themselves have to be obtained by recursively discharging values).
657+
658+
The @global@ parameter threads a cumulative shift through the traversal: when a value looked
659+
up from an environment is discharged, its own free variables must be shifted by the number of
660+
binders that were between the reference site and the environment boundary. Instead of doing a
661+
separate post-pass ('shiftTermBy'), we fuse the shifting into the discharge by passing @global@
662+
down through 'goValue'. -}
657663
dischargeCekValue :: forall uni fun ann. CekValue uni fun ann -> DischargeResult uni fun
658664
dischargeCekValue (VCon val) = DischargeConstant val
659-
dischargeCekValue value0 = DischargeNonConstant $ goValue value0
665+
dischargeCekValue value0 = DischargeNonConstant $ goValue 0 value0
660666
where
661-
goValue :: CekValue uni fun ann -> NTerm uni fun ()
662-
goValue = \case
667+
goValue :: Word64 -> CekValue uni fun ann -> NTerm uni fun ()
668+
goValue !global = \case
663669
VCon val -> Constant () val
664-
VDelay body env -> Delay () $ goValEnv env 0 body
670+
VDelay body env -> Delay () $ goValEnv env global 0 body
665671
VLamAbs (NamedDeBruijn n _ix) body env ->
666672
-- The index on the binder is meaningless, we put @0@ by convention, see 'Binder'.
667-
LamAbs () (NamedDeBruijn n deBruijnInitIndex) $ goValEnv env 1 body
673+
LamAbs () (NamedDeBruijn n deBruijnInitIndex) $ goValEnv env global 1 body
668674
-- We only return a discharged builtin application when (a) it's being returned by the
669675
-- machine, or (b) it's needed for an error message.
670676
-- @term@ is fully discharged, so we can return it directly without any further discharging.
671677
VBuiltin _ term _ -> term
672-
VConstr ind args -> Constr () ind . map goValue $ argStackToList args
678+
VConstr ind args -> Constr () ind . map (goValue global) $ argStackToList args
673679

674680
-- Instantiate all the free variables of a term by looking them up in an environment.
675681
-- Mutually recursive with @goValue@.
676-
goValEnv :: CekValEnv uni fun ann -> Word64 -> NTerm uni fun ann -> NTerm uni fun ()
682+
goValEnv :: CekValEnv uni fun ann -> Word64 -> Word64 -> NTerm uni fun ann -> NTerm uni fun ()
677683
goValEnv env = go
678684
where
679-
-- @shift@ is just a counter that measures how many lambda-abstractions we have descended
680-
-- into so far.
681-
go :: Word64 -> NTerm uni fun ann -> NTerm uni fun ()
682-
go !shift = \case
683-
LamAbs _ name body -> LamAbs () name $ go (shift + 1) body
685+
-- @global@ is the accumulated shift from outer discharge contexts.
686+
-- @shift@ counts how many lambda-abstractions we have descended into so far.
687+
go :: Word64 -> Word64 -> NTerm uni fun ann -> NTerm uni fun ()
688+
go !global !shift = \case
689+
LamAbs _ name body -> LamAbs () name $ go global (shift + 1) body
684690
Var _ named@(NamedDeBruijn _ (coerce -> idx)) ->
685691
if shift >= idx
686692
-- the index n is less-than-or-equal than the number of lambdas we have descended
687693
-- this means that n points to a bound variable, so we don't discharge it.
688694
then Var () named
689695
else
690696
maybe
691-
-- var is free, leave it alone
692-
(Var () named)
693-
-- var is in the env, discharge its value and shift free vars
694-
(shiftTermBy shift . goValue)
697+
-- var is free and not in the env, shift it
698+
(Var () (shiftNamedDeBruijn (global + shift) named))
699+
-- var is in the env, discharge its value with the accumulated shift
700+
(goValue (global + shift))
695701
-- index relative to (as seen from the point of view of) the environment
696702
(Env.indexOne env $ idx - shift)
697-
Apply _ fun arg -> Apply () (go shift fun) $ go shift arg
698-
Delay _ term -> Delay () $ go shift term
699-
Force _ term -> Force () $ go shift term
703+
Apply _ fun arg -> Apply () (go global shift fun) $ go global shift arg
704+
Delay _ term -> Delay () $ go global shift term
705+
Force _ term -> Force () $ go global shift term
700706
Constant _ val -> Constant () val
701707
Builtin _ fun -> Builtin () fun
702708
Error _ -> Error ()
703-
Constr _ ind args -> Constr () ind $ map (go shift) args
704-
Case _ scrut alts -> Case () (go shift scrut) $ fmap (go shift) alts
705-
706-
{-| Shift all free variables in a term by the given amount.
707-
A variable is free if its index is greater than the current binding depth. -}
708-
shiftTermBy :: Word64 -> NTerm uni fun () -> NTerm uni fun ()
709-
shiftTermBy 0 term = term -- Optimization: no-op when shift is 0
710-
shiftTermBy shiftAmount term = go 0 term
711-
where
712-
go :: Word64 -> NTerm uni fun () -> NTerm uni fun ()
713-
go !depth = \case
714-
Var ann (NamedDeBruijn n (coerce -> idx))
715-
| idx <= depth -> Var ann (NamedDeBruijn n (coerce idx)) -- Bound: unchanged
716-
| otherwise -> Var ann (NamedDeBruijn n (coerce (idx + shiftAmount))) -- Free: shift
717-
LamAbs ann name body -> LamAbs ann name $ go (depth + 1) body
718-
Apply ann fun arg -> Apply ann (go depth fun) (go depth arg)
719-
Delay ann t -> Delay ann $ go depth t
720-
Force ann t -> Force ann $ go depth t
721-
Constant ann val -> Constant ann val
722-
Builtin ann fun -> Builtin ann fun
723-
Error ann -> Error ann
724-
Constr ann ind args -> Constr ann ind $ map (go depth) args
725-
Case ann scrut alts -> Case ann (go depth scrut) $ fmap (go depth) alts
709+
Constr _ ind args -> Constr () ind $ map (go global shift) args
710+
Case _ scrut alts -> Case () (go global shift scrut) $ fmap (go global shift) alts
726711

727712
instance (PrettyUni uni, Pretty fun) => PrettyBy PrettyConfigPlc (CekValue uni fun ann) where
728713
prettyBy cfg = prettyBy cfg . dischargeResultToTerm . dischargeCekValue

plutus-core/untyped-plutus-core/testlib/Evaluation/FreeVars.hs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ testDischargeFree =
6969
, ("freeRemainsInNestedEnv", freeRemainsInNestedEnv)
7070
, ("deepFreeVarRemains", deepFreeVarRemains)
7171
, ("multipleFreeVarsRemain", multipleFreeVarsRemain)
72+
, -- Tests for truly free vars that go *past* a non-empty env.
73+
-- These exercise the global shift parameter threading approach:
74+
-- instead of a separate shiftTermBy post-pass, the shift is threaded through
75+
-- goValue and applied to free vars as they are encountered.
76+
("freeVarPastNonEmptyEnvNoLambda", freeVarPastNonEmptyEnvNoLambda)
77+
, ("freeVarPastNonEmptyEnvWithLambda", freeVarPastNonEmptyEnvWithLambda)
78+
, ("freeVarPastNonEmptyEnvNested", freeVarPastNonEmptyEnvNested)
79+
, ("freeVarMixedBoundAndTrulyFree", freeVarMixedBoundAndTrulyFree)
7280
]
7381
where
7482
delayWithEmptyEnv =
@@ -208,6 +216,80 @@ testDischargeFree =
208216
Delay () (v 3 @@ [v 4]) -- var 1 -> var 3, var 2 -> var 4
209217
)
210218

219+
freeVarPastNonEmptyEnvNoLambda =
220+
-- VDelay (var 2) [VCon unit]
221+
-- Env has 1 entry; var 2 at shift=0 looks up idx 2, past env size 1.
222+
-- Truly free var at top level (global=0, shift=0): shifted by 0, stays as var 2.
223+
dis
224+
( VDelay
225+
(toFakeTerm $ v 2)
226+
[VCon $ someValue ()]
227+
)
228+
@?= DischargeNonConstant
229+
( toFakeTerm . Delay () $
230+
v 2 -- free var past env, no lambda context, unchanged
231+
)
232+
233+
freeVarPastNonEmptyEnvWithLambda =
234+
-- VDelay (\x -> var 3) [VCon unit]
235+
-- Under \x (shift=1), var 3 looks up idx 3-1=2, past env size 1.
236+
-- Truly free, shifted by (global + shift). At top level: global=0, shift=1 → var 4.
237+
-- Note: with the previous shiftTermBy approach, this would have given var 3
238+
-- (the post-pass shiftTermBy was never reached at the top level for free vars
239+
-- not looked up from an env). The global shift approach applies shift uniformly.
240+
dis
241+
( VDelay
242+
(toFakeTerm $ LamAbs () (DeBruijn deBruijnInitIndex) (v 3))
243+
[VCon $ someValue ()]
244+
)
245+
@?= DischargeNonConstant
246+
( toFakeTerm . Delay () . lamAbs0 $
247+
v 4 -- var 3 shifted by (0 + 1) = 1
248+
)
249+
250+
freeVarPastNonEmptyEnvNested =
251+
-- Outer: VLamAbs _ (var 2) [innerDelay]
252+
-- Inner: VDelay (\x -> var 3) [VCon unit]
253+
-- Outer body var 2 under 1 lambda (shift=1) → look up 1 → found innerDelay
254+
-- Discharge innerDelay with global=(0+1)=1.
255+
-- Inner: \x -> var 3 in env [VCon unit], global=1, shift=1
256+
-- var 3 at shift=1: look up 2, past env → truly free
257+
-- Shifted by (global + shift) = (1 + 1) = 2 → var 5.
258+
dis
259+
( VLamAbs
260+
(fakeNameDeBruijn $ DeBruijn deBruijnInitIndex)
261+
(toFakeTerm $ v 2)
262+
[ VDelay
263+
(toFakeTerm $ LamAbs () (DeBruijn deBruijnInitIndex) (v 3))
264+
[VCon $ someValue ()]
265+
]
266+
)
267+
@?= DischargeNonConstant
268+
( toFakeTerm . lamAbs0 . Delay () . lamAbs0 $
269+
v 5 -- var 3 shifted by (1 + 1) = 2
270+
)
271+
272+
freeVarMixedBoundAndTrulyFree =
273+
-- VDelay (\x -> x @ var 2 @ var 3) [VCon unit]
274+
-- Under \x (shift=1):
275+
-- var 1 (x): bound by lambda
276+
-- var 2: look up 1 in env → found VCon unit → discharged as (con unit)
277+
-- var 3: look up 2 in env → not found → truly free
278+
-- Truly free var 3 shifted by (global=0 + shift=1) = 1 → var 4
279+
dis
280+
( VDelay
281+
( toFakeTerm $
282+
LamAbs () (DeBruijn deBruijnInitIndex) $
283+
v 1 @@ [v 2, v 3]
284+
)
285+
[VCon $ someValue ()]
286+
)
287+
@?= DischargeNonConstant
288+
( toFakeTerm . Delay () . lamAbs0 $
289+
v 1 @@ [Constant () (someValue ()), v 4]
290+
-- x stays, unit substituted, free var 3 → var 4
291+
)
292+
211293
dis = dischargeCekValue @DefaultUni @DefaultFun
212294
v = Var () . DeBruijn
213295

0 commit comments

Comments
 (0)