Skip to content

Commit 8e29b0a

Browse files
authored
Merge pull request #5420 from unisonweb/fix/5419
Fix variable capture issue during scope flattening
2 parents 3420cf3 + ca48c9c commit 8e29b0a

File tree

4 files changed

+238
-9
lines changed

4 files changed

+238
-9
lines changed

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class (Bifoldable f, Bifunctor f) => Align f where
103103

104104
alphaErr ::
105105
(Align f) => (Var v) => Map v v -> Term f v -> Term f v -> Either (Term f v, Term f v) a
106-
alphaErr un tml tmr = Left (tml, renames count un tmr)
106+
alphaErr un tml tmr = Left (tml, renames0 count un tmr)
107107
where
108108
count = Map.fromListWith (+) . flip zip [1, 1 ..] $ toList un
109109

@@ -133,21 +133,21 @@ pattern TAbss vs bd <-
133133

134134
{-# COMPLETE TAbss #-}
135135

136-
-- Simultaneous variable renaming.
136+
-- Simultaneous variable renaming implementation.
137137
--
138138
-- subvs0 counts the number of variables being renamed to a particular
139139
-- variable
140140
--
141141
-- rnv0 is the variable renaming map.
142-
renames ::
142+
renames0 ::
143143
(Var v, Ord v, Bifunctor f, Bifoldable f) =>
144144
Map v Int ->
145145
Map v v ->
146146
Term f v ->
147147
Term f v
148-
renames subvs0 rnv0 tm = case tm of
148+
renames0 subvs0 rnv0 tm = case tm of
149149
TAbs u body
150-
| not $ Map.null rnv' -> TAbs u' (renames subvs' rnv' body)
150+
| not $ Map.null rnv' -> TAbs u' (renames0 subvs' rnv' body)
151151
where
152152
rnv' = Map.alter (const $ adjustment) u rnv
153153
-- if u is in the set of variables we're substituting in, it
@@ -164,7 +164,7 @@ renames subvs0 rnv0 tm = case tm of
164164
| otherwise = (Nothing, subvs)
165165
TTm body
166166
| not $ Map.null rnv ->
167-
TTm $ bimap (\u -> Map.findWithDefault u u rnv) (renames subvs rnv) body
167+
TTm $ bimap (\u -> Map.findWithDefault u u rnv) (renames0 subvs rnv) body
168168
_ -> tm
169169
where
170170
fvs = freeVars tm
@@ -179,13 +179,23 @@ renames subvs0 rnv0 tm = case tm of
179179
| n <= 1 = Nothing
180180
| otherwise = Just (n - 1)
181181

182+
-- Simultaneous variable renaming.
183+
renames ::
184+
(Var v, Ord v, Bifunctor f, Bifoldable f) =>
185+
Map v v ->
186+
Term f v ->
187+
Term f v
188+
renames rnv tm = renames0 subvs rnv tm
189+
where
190+
subvs = Map.fromListWith (+) . fmap (,1) $ Map.elems rnv
191+
182192
rename ::
183193
(Var v, Ord v, Bifunctor f, Bifoldable f) =>
184194
v ->
185195
v ->
186196
Term f v ->
187197
Term f v
188-
rename old new = renames (Map.singleton new 1) (Map.singleton old new)
198+
rename old new = renames0 (Map.singleton new 1) (Map.singleton old new)
189199

190200
transform ::
191201
(Var v, Bifunctor g, Bifoldable f, Bifoldable g) =>

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

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,13 @@ data CTE v s
693693
pattern ST1 :: Direction Word16 -> v -> Mem -> s -> CTE v s
694694
pattern ST1 d v m s = ST d [v] [m] s
695695

696+
-- All variables, both bound and free occurring in a CTE. This is
697+
-- useful for avoiding both free and bound variables when
698+
-- freshening.
699+
cteVars :: (Ord v) => Cte v -> Set v
700+
cteVars (ST _ vs _ e) = Set.fromList vs `Set.union` ABTN.freeVars e
701+
cteVars (LZ v r as) = Set.fromList (either (const id) (:) r $ v : as)
702+
696703
data ANormalF v e
697704
= ALet (Direction Word16) [Mem] e e
698705
| AName (Either Reference v) [v] e
@@ -1706,8 +1713,16 @@ tru = TCon Ty.booleanRef 1 []
17061713
-- binding during ANF translation. Renames a variable in a
17071714
-- context, and returns an indication of whether the varible
17081715
-- was shadowed by one of the context bindings.
1716+
--
1717+
-- Note: this assumes that `u` is not bound by any of the context
1718+
-- entries, as no effort is made to rename them to avoid capturing
1719+
-- `u`.
17091720
renameCtx :: (Var v) => v -> v -> Ctx v -> (Ctx v, Bool)
1710-
renameCtx v u (d, ctx) | (ctx, b) <- rn [] ctx = ((d, ctx), b)
1721+
renameCtx v u (d, ctx) | (ctx, b) <- renameCtes v u ctx = ((d, ctx), b)
1722+
1723+
-- As above, but without the Direction.
1724+
renameCtes :: (Var v) => v -> v -> [Cte v] -> ([Cte v], Bool)
1725+
renameCtes v u = rn []
17111726
where
17121727
swap w
17131728
| w == v = u
@@ -1725,7 +1740,92 @@ renameCtx v u (d, ctx) | (ctx, b) <- rn [] ctx = ((d, ctx), b)
17251740
where
17261741
e = LZ w (swap <$> f) (swap <$> as)
17271742

1728-
anfBlock :: (Var v) => Term v a -> ANFM v (Ctx v, DNormal v)
1743+
-- Simultaneously renames variables in a list of context entries.
1744+
--
1745+
-- Assumes that the variables being renamed to are not bound by the
1746+
-- context entries, so that it is unnecessary to rename them.
1747+
renamesCtes :: (Var v) => Map v v -> [Cte v] -> [Cte v]
1748+
renamesCtes rn = map f
1749+
where
1750+
swap w
1751+
| Just u <- Map.lookup w rn = u
1752+
| otherwise = w
1753+
1754+
f (ST d vs ccs b) = ST d vs ccs (ABTN.renames rn b)
1755+
f (LZ v r as) = LZ v (second swap r) (map swap as)
1756+
1757+
-- Calculates the free variables occurring in a context. This
1758+
-- consists of the free variables in the expressions being bound,
1759+
-- but with previously bound variables subtracted.
1760+
freeVarsCtx :: (Ord v) => Ctx v -> Set v
1761+
freeVarsCtx = freeVarsCte . snd
1762+
1763+
freeVarsCte :: (Ord v) => [Cte v] -> Set v
1764+
freeVarsCte = foldr m Set.empty
1765+
where
1766+
m (ST _ vs _ bn) rest =
1767+
ABTN.freeVars bn `Set.union` (rest Set.\\ Set.fromList vs)
1768+
m (LZ v r as) rest =
1769+
Set.fromList (either (const id) (:) r as)
1770+
`Set.union` Set.delete v rest
1771+
1772+
-- Conditionally freshens a list of variables. The predicate
1773+
-- argument selects which variables to freshen, and the set is a set
1774+
-- of variables to avoid for freshness. The process ensures that the
1775+
-- result is mutually fresh, and returns a new set of variables to
1776+
-- avoid, which includes the freshened variables.
1777+
--
1778+
-- Presumably any variables selected by the predicate should be
1779+
-- included in the set, but the set may contain additional variables
1780+
-- to avoid, when freshening.
1781+
freshens :: (Var v) => (v -> Bool) -> Set v -> [v] -> (Set v, [v])
1782+
freshens p avoid0 vs =
1783+
mapAccumL f (Set.union avoid0 (Set.fromList vs)) vs
1784+
where
1785+
f avoid v
1786+
| p v, u <- Var.freshIn avoid v = (Set.insert u avoid, u)
1787+
| otherwise = (avoid, v)
1788+
1789+
-- Freshens the variable bindings in a context to avoid a set of
1790+
-- variables. Returns the renaming necessary for anything that was
1791+
-- bound in the freshened context.
1792+
--
1793+
-- Note: this only freshens if it's necessary to avoid variables in
1794+
-- the _original_ set. We need to keep track of other variables to
1795+
-- avoid when making up new names for those, but it it isn't
1796+
-- necessary to freshen variables to remove shadowing _within_ the
1797+
-- context, since it is presumably already correctly formed.
1798+
freshenCtx :: (Var v) => Set v -> Ctx v -> (Map v v, Ctx v)
1799+
freshenCtx avoid0 (d, ctx) =
1800+
case go lavoid Map.empty [] $ reverse ctx of
1801+
(rn, ctx) -> (rn, (d, ctx))
1802+
where
1803+
-- precalculate all variable occurrences in the context to just
1804+
-- completely avoid those as well.
1805+
lavoid =
1806+
foldl (flip $ Set.union . cteVars) avoid0 ctx
1807+
1808+
go _ rns fresh [] = (rns, fresh)
1809+
go avoid rns fresh (bn : bns) = case bn of
1810+
LZ v r as
1811+
| v `Set.member` avoid0,
1812+
u <- Var.freshIn avoid v,
1813+
(fresh, _) <- renameCtes v u fresh,
1814+
avoid <- Set.insert u avoid,
1815+
rns <- Map.alter (Just . fromMaybe u) v rns ->
1816+
go avoid rns (LZ u r as : fresh) bns
1817+
ST d vs ccs expr
1818+
| (avoid, us) <- freshens (`Set.member` avoid0) avoid vs,
1819+
rn <- Map.fromList (filter (uncurry (/=)) $ zip vs us),
1820+
not (Map.null rn),
1821+
fresh <- renamesCtes rn fresh,
1822+
-- Note: rns union left-biased, so inner contexts take
1823+
-- priority.
1824+
rns <- Map.union rns rn ->
1825+
go avoid rns (ST d us ccs expr : fresh) bns
1826+
_ -> go avoid rns (bn : fresh) bns
1827+
1828+
anfBlock :: (Ord v, Var v) => Term v a -> ANFM v (Ctx v, DNormal v)
17291829
anfBlock (Var' v) = pure (mempty, pure $ TVar v)
17301830
anfBlock (If' c t f) = do
17311831
(cctx, cc) <- anfBlock c
@@ -1875,14 +1975,24 @@ anfBlock (Let1Named' v b e) =
18751975
anfBlock b >>= \case
18761976
(bctx, (Direct, TVar u)) -> do
18771977
(ectx, ce) <- anfBlock e
1978+
(brn, bctx) <- fixupBctx bctx ectx ce
1979+
u <- pure $ Map.findWithDefault u u brn
18781980
(ectx, shaded) <- pure $ renameCtx v u ectx
18791981
ce <- pure $ if shaded then ce else ABTN.rename v u <$> ce
18801982
pure (bctx <> ectx, ce)
18811983
(bctx, (d0, cb)) -> bindLocal [v] $ do
18821984
(ectx, ce) <- anfBlock e
18831985
d <- bindDirection d0
1986+
(brn, bctx) <- fixupBctx bctx ectx ce
1987+
cb <- pure $ ABTN.renames brn cb
18841988
let octx = bctx <> directed [ST1 d v BX cb] <> ectx
18851989
pure (octx, ce)
1990+
where
1991+
fixupBctx bctx ectx (_, ce) =
1992+
pure $ freshenCtx (Set.union ecfvs efvs) bctx
1993+
where
1994+
ecfvs = freeVarsCtx ectx
1995+
efvs = ABTN.freeVars ce
18861996
anfBlock (Apps' (Blank' b) args) = do
18871997
nm <- fresh
18881998
(actx, cas) <- anfArgs args

unison-src/transcripts/fix5419.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
```ucm:hide
2+
scratch/main> builtins.merge
3+
```
4+
5+
Below is an example of variable capture occuring from pattern matching.
6+
7+
```unison
8+
9+
foo w = match (5, w) with
10+
x ->
11+
y = toText x
12+
match 99 with _ -> ()
13+
z = toText x
14+
(y,z)
15+
16+
> foo 8
17+
```
18+
19+
Arguably, the root cause is flattening of nested lets like this one.
20+
21+
```unison
22+
23+
bar x =
24+
-- argument here
25+
y = Debug.toText x
26+
let
27+
x = 5
28+
()
29+
-- 5 here, before fix
30+
z = Debug.toText x
31+
(y, z)
32+
33+
> bar 3
34+
```
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
Below is an example of variable capture occuring from pattern matching.
2+
3+
``` unison
4+
5+
foo w = match (5, w) with
6+
x ->
7+
y = toText x
8+
match 99 with _ -> ()
9+
z = toText x
10+
(y,z)
11+
12+
> foo 8
13+
```
14+
15+
``` ucm
16+
17+
Loading changes detected in scratch.u.
18+
19+
I found and typechecked these definitions in scratch.u. If you
20+
do an `add` or `update`, here's how your codebase would
21+
change:
22+
23+
⍟ These new definitions are ok to `add`:
24+
25+
foo : w
26+
-> ( Optional (Either Text Text),
27+
Optional (Either Text Text))
28+
29+
Now evaluating any watch expressions (lines starting with
30+
`>`)... Ctrl+C cancels.
31+
32+
9 | > foo 8
33+
34+
(Some (Right "(5, 8)"), Some (Right "(5, 8)"))
35+
36+
```
37+
Arguably, the root cause is flattening of nested lets like this one.
38+
39+
``` unison
40+
41+
bar x =
42+
-- argument here
43+
y = Debug.toText x
44+
let
45+
x = 5
46+
()
47+
-- 5 here, before fix
48+
z = Debug.toText x
49+
(y, z)
50+
51+
> bar 3
52+
```
53+
54+
``` ucm
55+
56+
Loading changes detected in scratch.u.
57+
58+
I found and typechecked these definitions in scratch.u. If you
59+
do an `add` or `update`, here's how your codebase would
60+
change:
61+
62+
⍟ These new definitions are ok to `add`:
63+
64+
bar : x
65+
-> ( Optional (Either Text Text),
66+
Optional (Either Text Text))
67+
68+
Now evaluating any watch expressions (lines starting with
69+
`>`)... Ctrl+C cancels.
70+
71+
12 | > bar 3
72+
73+
(Some (Right "3"), Some (Right "3"))
74+
75+
```

0 commit comments

Comments
 (0)