Skip to content

Commit 59eaddd

Browse files
committed
Fix variable capture bug during context flattening
Includes proper test case result
1 parent e18c304 commit 59eaddd

File tree

2 files changed

+188
-2
lines changed

2 files changed

+188
-2
lines changed

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

Lines changed: 113 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,25 @@ 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
1996+
18861997
anfBlock (Apps' (Blank' b) args) = do
18871998
nm <- fresh
18881999
(actx, cas) <- anfArgs args
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)