Skip to content

Commit e1c9990

Browse files
authored
Merge pull request #5757 from unisonweb/topic/affine-improvements
Fix some variable capture potential in the optimizations
2 parents a88a782 + 25f3d7e commit e1c9990

File tree

2 files changed

+154
-78
lines changed

2 files changed

+154
-78
lines changed

unison-core/src/Unison/ABT/Normalized.hs

Lines changed: 113 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ module Unison.ABT.Normalized
1515
Align (..),
1616
alpha,
1717
freshen,
18+
Renaming (..),
19+
isEmptyRenaming,
20+
freshenBinder,
21+
freshenBinders,
22+
pruneRenaming,
23+
renameVar,
24+
mapping,
25+
avoiding,
26+
mappingAndAvoiding,
1827
renames,
1928
renamesAvoiding,
29+
renamesAndFreshen0,
2030
rename,
2131
transform,
2232
visit,
@@ -26,13 +36,13 @@ where
2636

2737
import Data.Bifoldable
2838
import Data.Bifunctor
29-
import Data.Foldable (toList)
3039
import Data.Functor.Identity (Identity (..))
3140
import Data.Map.Strict (Map)
3241
import Data.Map.Strict qualified as Map
3342
import Data.Maybe (fromMaybe)
3443
import Data.Set (Set)
3544
import Data.Set qualified as Set
45+
import Data.Traversable (mapAccumL)
3646
import Unison.ABT (Var (..))
3747

3848
-- ABTs with support for 'normalized' structure where only variables
@@ -107,9 +117,9 @@ class (Bifoldable f, Bifunctor f) => Align f where
107117

108118
alphaErr ::
109119
(Align f) => (Var v) => Map v v -> Term f v -> Term f v -> Either (Term f v, Term f v) a
110-
alphaErr un tml tmr = Left (tml, renamesAndFreshen0 count un tmr)
120+
alphaErr un tml tmr = Left (tml, renamesAndFreshen0 rn tmr)
111121
where
112-
count = Map.fromListWith (+) . flip zip [1, 1 ..] $ toList un
122+
rn = mapping un
113123

114124
-- Checks if two terms are equal up to a given variable renaming. The
115125
-- renaming should map variables in the right hand term to the
@@ -137,6 +147,93 @@ pattern TAbss vs bd <-
137147

138148
{-# COMPLETE TAbss #-}
139149

150+
-- Renaming data.
151+
--
152+
-- `renames` contains an actual mapping from old variables to new
153+
-- variables, which is used for the actual substitution.
154+
--
155+
-- `conflicts` stores information about which variables should be
156+
-- avoided. The value at a variable `u` should _at least_ be the
157+
-- number of variables in `renames` that are being substituted _to_
158+
-- `u`, because substituting under a binder for `u` will capture those
159+
-- substitutions. However, `conflicts` can be bootstrapped with extra
160+
-- counts to avoid ambient variables as well, so that it is possible
161+
-- to substitute/rewrite expressions without introducing variable
162+
-- captures.
163+
data Renaming v = RN
164+
{ conflicts :: Map v Int,
165+
renamings :: Map v v
166+
}
167+
168+
-- Creates a Renaming that will avoid the given set of variables.
169+
avoiding :: Set v -> Renaming v
170+
avoiding avoid = RN (Map.fromSet (const 1) avoid) Map.empty
171+
172+
mapping :: (Var v) => Map v v -> Renaming v
173+
mapping rn = RN cf rn
174+
where
175+
cf = Map.fromListWith (+) . fmap (,1) $ Map.elems rn
176+
177+
mappingAndAvoiding :: (Var v) => Map v v -> Set v -> Renaming v
178+
mappingAndAvoiding rn avoid = RN cf rn
179+
where
180+
cf =
181+
Map.unionWith
182+
(+)
183+
(Map.fromSet (const 1) avoid)
184+
(Map.fromListWith (+) . fmap (,1) $ Map.elems rn)
185+
186+
-- Adjusts a renaming with respect to a remaining set of free
187+
-- variables. Unnecessary renamings are discarded.
188+
pruneRenaming :: (Var v) => Set v -> Renaming v -> Renaming v
189+
pruneRenaming fvs (RN cf rn) =
190+
RN
191+
{ renamings = Map.restrictKeys rn fvs,
192+
conflicts = Map.foldl' decrement cf $ Map.withoutKeys rn fvs
193+
}
194+
where
195+
decrement sv v = Map.update drop v sv
196+
drop n
197+
| n <= 1 = Nothing
198+
| otherwise = Just (n - 1)
199+
200+
renameVar :: (Var v) => Renaming v -> v -> v
201+
renameVar (RN _ rn) u = Map.findWithDefault u u rn
202+
203+
-- Tests if the renaming is empty in the sense that it will never
204+
-- cause bound variables to be renamed. This is _not_ just a test of
205+
-- whether the substitutions are empty, because the conflicts can
206+
-- cause variables to need freshening even without variable
207+
-- substitutions.
208+
isEmptyRenaming :: Renaming v -> Bool
209+
isEmptyRenaming = null . conflicts
210+
211+
-- Freshens a bound variable with regard to a renaming, yielding the
212+
-- fresh variable and a renaming appropriate for the term within the
213+
-- binder. The `Set` should be the free variables of the expression
214+
-- within the binder, for proper freshening.
215+
freshenBinder :: (Var v) => Set v -> Renaming v -> v -> (Renaming v, v)
216+
freshenBinder fvs rn0@(RN cf rn) u = (rn', u')
217+
where
218+
-- if u conflicts with the renaming, freshen it
219+
u'
220+
| u `Map.member` cf = freshIn (fvs `Set.union` Map.keysSet cf) u
221+
| otherwise = u
222+
223+
-- if u needs to be renamed, and it actually occurs in the body,
224+
-- add it to the Renaming.
225+
rn'
226+
| u /= u' && u `Set.member` fvs =
227+
RN
228+
{ conflicts = Map.insertWith (+) u' 1 cf,
229+
renamings = Map.alter (const $ Just u') u rn
230+
}
231+
| otherwise = rn0
232+
233+
freshenBinders ::
234+
(Var v) => Set v -> Renaming v -> [v] -> (Renaming v, [v])
235+
freshenBinders fvs = mapAccumL (freshenBinder fvs)
236+
140237
-- Simultaneous variable renaming and freshening implementation.
141238
--
142239
-- subvs0 is a count of the number of conflicts associated with a
@@ -153,48 +250,22 @@ pattern TAbss vs bd <-
153250
-- rnv0 is the variable renaming map.
154251
renamesAndFreshen0 ::
155252
(Var v, Bifunctor f, Bifoldable f) =>
156-
Map v Int ->
157-
Map v v ->
253+
Renaming v ->
158254
Term f v ->
159255
Term f v
160-
renamesAndFreshen0 subvs0 rnv0 tm = case tm of
256+
renamesAndFreshen0 rn0 tm = case tm of
161257
TAbs u body
162-
| not $ Map.null subvs' ->
163-
TAbs u' (renamesAndFreshen0 subvs' rnv' body)
164-
where
165-
rnv' = Map.alter (const $ adjustment) u rnv
166-
bfvs = freeVars body
167-
-- if u is in the set of variables we're substituting in, it
168-
-- needs to be renamed to avoid capturing things.
169-
u'
170-
| u `Map.member` subvs = freshIn (bfvs `Set.union` Map.keysSet subvs) u
171-
| otherwise = u
172-
173-
-- if u needs to be renamed to avoid capturing subvs
174-
-- and u actually occurs in the body, then add it to
175-
-- the substitutions
176-
(adjustment, subvs')
177-
| u /= u' && u `Set.member` bfvs =
178-
(Just u', Map.insertWith (+) u' 1 subvs)
179-
| otherwise = (Nothing, subvs)
258+
| (rn, u') <- freshenBinder (freeVars body) rn u,
259+
u /= u' || not (isEmptyRenaming rn) ->
260+
TAbs u' (renamesAndFreshen0 rn body)
180261
TTm body
181-
| not $ Map.null subvs ->
182-
TTm $ bimap lkup (renamesAndFreshen0 subvs rnv) body
262+
| not $ isEmptyRenaming rn ->
263+
TTm $ bimap (renameVar rn) (renamesAndFreshen0 rn) body
183264
_ -> tm
184265
where
185266
fvs = freeVars tm
186267

187-
-- throw out irrelevant renamings
188-
rnv = Map.restrictKeys rnv0 fvs
189-
190-
lkup u = Map.findWithDefault u u rnv
191-
192-
-- decrement the variable usage counts for the renamings we threw away
193-
subvs = Map.foldl' decrement subvs0 $ Map.withoutKeys rnv0 fvs
194-
decrement sv v = Map.update drop v sv
195-
drop n
196-
| n <= 1 = Nothing
197-
| otherwise = Just (n - 1)
268+
rn = pruneRenaming fvs rn0
198269

199270
-- Freshens the bound variables in a term to avoid capturing variables
200271
-- in the set.
@@ -203,9 +274,7 @@ freshen ::
203274
Set v ->
204275
Term f v ->
205276
Term f v
206-
freshen avoid = renamesAndFreshen0 subvs Map.empty
207-
where
208-
subvs = Map.fromSet (const 1) avoid
277+
freshen avoid = renamesAndFreshen0 (avoiding avoid)
209278

210279
-- Renames some variables while also avoiding a given set of variables
211280
-- for any bindings in the term.
@@ -215,30 +284,24 @@ renamesAvoiding ::
215284
Map v v ->
216285
Term f v ->
217286
Term f v
218-
renamesAvoiding avoid rnv = renamesAndFreshen0 subvs rnv
219-
where
220-
suba = Map.fromSet (const 1) avoid
221-
subr = Map.fromListWith (+) . fmap (,1) $ Map.elems rnv
222-
223-
subvs = Map.unionWith (+) suba subr
287+
renamesAvoiding avoid rnv =
288+
renamesAndFreshen0 (mappingAndAvoiding rnv avoid)
224289

225290
-- Simultaneous variable renaming.
226291
renames ::
227292
(Var v, Bifunctor f, Bifoldable f) =>
228293
Map v v ->
229294
Term f v ->
230295
Term f v
231-
renames rnv tm = renamesAndFreshen0 subvs rnv tm
232-
where
233-
subvs = Map.fromListWith (+) . fmap (,1) $ Map.elems rnv
296+
renames rnv tm = renamesAndFreshen0 (mapping rnv) tm
234297

235298
rename ::
236299
(Var v, Ord v, Bifunctor f, Bifoldable f) =>
237300
v ->
238301
v ->
239302
Term f v ->
240303
Term f v
241-
rename old new = renamesAndFreshen0 (Map.singleton new 1) (Map.singleton old new)
304+
rename old new = renamesAndFreshen0 (mapping $ Map.singleton old new)
242305

243306
transform ::
244307
(Var v, Bifunctor g, Bifoldable f, Bifoldable g) =>

unison-runtime/src/Unison/Runtime/ANF/Optimize.hs

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -489,47 +489,57 @@ pattern HandlerResume lz f as lh h bs rs <-
489489
matchHandledThunk ::
490490
(Var v) => ANormal v -> Maybe (Reference, Int, Bool, ANormal v)
491491
matchHandledThunk (TLet _ th _ (TCom r vs) bd) =
492-
final <$> runWriterT (prefix bd)
492+
final <$> runWriterT (prefix (ABTN.avoiding . Set.fromList $ th : vs) bd)
493493
where
494494
none = WriterT Nothing
495495

496496
final (bd, All lazy) = (r, length vs, lazy, bd)
497497

498-
prefix (TName v g bs bd)
498+
prefix rn (TName v g bs bd)
499499
| v /= th,
500500
all (/= th) g,
501-
all (/= th) bs =
502-
TName v g bs <$> prefix bd
503-
prefix (TLets d vs ccs bn bd)
501+
all (/= th) bs,
502+
g <- ABTN.renameVar rn <$> g,
503+
bs <- ABTN.renameVar rn <$> bs,
504+
(rn, v) <- ABTN.freshenBinder (ABTN.freeVars bd) rn v =
505+
TName v g bs <$> prefix rn bd
506+
prefix rn (TLets d vs ccs bn bd)
504507
| all (/= th) vs,
505-
th `Set.notMember` ABTN.freeVars bn =
506-
TLets d vs ccs bn <$> prefix bd <* tell (All False)
507-
prefix (TMatch sc bs) =
508-
TMatch sc <$> traverse under bs
508+
th `Set.notMember` ABTN.freeVars bn,
509+
bn <- ABTN.renamesAndFreshen0 rn bn,
510+
(rn, vs) <- ABTN.freshenBinders (ABTN.freeVars bd) rn vs =
511+
TLets d vs ccs bn <$> prefix rn bd <* tell (All False)
512+
prefix rn (TMatch sc bs) =
513+
TMatch (ABTN.renameVar rn sc) <$> traverse under bs
509514
where
510515
under (ABTN.TAbss vs bd)
511-
| all (/= th) vs =
512-
ABTN.TAbss vs <$> prefix bd
516+
| all (/= th) vs,
517+
(rn, vs) <- ABTN.freshenBinders (ABTN.freeVars bd) rn vs =
518+
ABTN.TAbss vs <$> prefix rn bd
513519
under _ = none
514-
prefix (THnd rs nh ah bd)
520+
prefix rn (THnd rs nh ah bd)
515521
| nh /= th,
516-
all (/= th) ah =
517-
THnd rs nh ah <$> suffix bd
518-
prefix _ = none
522+
all (/= th) ah,
523+
ah <- ABTN.renameVar rn <$> ah,
524+
nh <- ABTN.renameVar rn nh =
525+
THnd rs nh ah <$> suffix rn bd
526+
prefix _ _ = none
519527

520528
-- Some values may be bound before the thunk call as long as
521529
-- they're 'direct' calls that can't capture stacks and reveal
522530
-- that we've changed the convention.
523-
suffix (TLets d vs ccs bn bd)
531+
suffix rn (TLets d vs ccs bn bd)
524532
| all (/= th) vs,
525-
th `Set.notMember` ABTN.freeVars bn =
526-
TLets d vs ccs bn <$> suffix bd <* tell (All $ d == Direct)
533+
th `Set.notMember` ABTN.freeVars bn,
534+
bn <- ABTN.renamesAndFreshen0 rn bn,
535+
(rn, vs) <- ABTN.freshenBinders (ABTN.freeVars bd) rn vs =
536+
TLets d vs ccs bn <$> suffix rn bd <* tell (All $ d == Direct)
527537
-- final expression in handle body is a call to the thunk.
528-
suffix (TApv h us)
538+
suffix rn (TApv h us)
529539
| h == th,
530540
all (/= th) us =
531-
pure . TCom r $ vs ++ us
532-
suffix _ = none
541+
pure . TCom r $ vs ++ fmap (ABTN.renameVar rn) us
542+
suffix _ _ = none
533543
matchHandledThunk _ = Nothing
534544

535545
-- th = f <vs> -- undersaturated
@@ -748,9 +758,10 @@ affineHandlerCase opts self vs rec br
748758
| ABTN.TAbss us body <- br,
749759
TShift _ kf0 body <- body,
750760
TName kf (Left (Builtin "jumpCont")) [kf1] body <- body,
761+
bound <- Set.fromList (kf0 : kf : us),
751762
kf0 == kf1 =
752763
ABTN.TAbss us
753-
<$> affinePreBranch opts self Set.empty vs rec ar kf body
764+
<$> affinePreBranch opts self bound vs rec ar kf body
754765
| otherwise = Nothing
755766
where
756767
ar = freshAff 2
@@ -850,7 +861,8 @@ linearTail opts self vs bound rec ar kf0 tm
850861
rh /= kf0, -- no shadowing or non-linearity
851862
THnd _rs hh Nothing bd <- tm,
852863
rh == hh, -- handle recursively
853-
bd <- replaceLinearBody opts bd,
864+
avoid <- Set.insert rh bound,
865+
bd <- replaceLinearBody opts avoid bd,
854866
SimpleBody pre ind shad free kf1 result <- bd, -- simple enough body
855867
kf0 `Set.notMember` shad, -- kf is not shadowed in body
856868
kf0 `Set.notMember` free, -- kf is not free in `pre`
@@ -888,17 +900,18 @@ linearTail opts self vs bound rec ar kf0 tm
888900
-- continuations aren't captured, so we don't actually need a correct
889901
-- numbering. If this is ever changed, then the numbering here must be
890902
-- adjusted.
891-
replaceLinearBody :: (Var v) => OptInfos v -> ANormal v -> ANormal v
892-
replaceLinearBody opts@(arities, inls) bd
903+
replaceLinearBody ::
904+
(Var v) => OptInfos v -> Set v -> ANormal v -> ANormal v
905+
replaceLinearBody opts@(arities, inls) avoid bd
893906
| TLetD v cc bn bd <- bd =
894-
TLetD v cc bn $ replaceLinearBody opts bd
907+
TLetD v cc bn $ replaceLinearBody opts (Set.insert v avoid) bd
895908
| TCom r vs <- bd,
896909
Just n <- Map.lookup r arities,
897910
length vs == n,
898911
Just (InlInfo _ (ABTN.TAbss us expr)) <- Map.lookup r inls,
899912
rn <- Map.fromList (zip us vs) =
900-
ABTN.renames rn expr
901-
replaceLinearBody _ bd = bd
913+
ABTN.renamesAvoiding avoid rn expr
914+
replaceLinearBody _ _ bd = bd
902915

903916
parseSimpleHandlerBody ::
904917
(Var v) =>

0 commit comments

Comments
 (0)