Skip to content

Commit a644032

Browse files
fix 5525
1 parent b7b3439 commit a644032

File tree

3 files changed

+202
-38
lines changed

3 files changed

+202
-38
lines changed

Diff for: parser-typechecker/src/Unison/PrettyPrintEnv/MonadPretty.hs

+69
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module Unison.PrettyPrintEnv.MonadPretty
44
runPretty,
55
addTypeVars,
66
willCaptureType,
7+
withBoundTerm,
8+
withBoundTerms,
79
)
810
where
911

@@ -14,12 +16,15 @@ import Unison.Prelude
1416
import Unison.PrettyPrintEnv (PrettyPrintEnv)
1517
import Unison.Util.Set qualified as Set
1618
import Unison.Var (Var)
19+
import Unison.Var qualified as Var
1720

1821
type MonadPretty v m = (Var v, MonadReader (Env v) m)
1922

23+
-- See Note [Bound and free term variables] for an explanation of boundTerms/freeTerms
2024
data Env v = Env
2125
{ boundTerms :: !(Set v),
2226
boundTypes :: !(Set v),
27+
freeTerms :: !(Set v),
2328
ppe :: !PrettyPrintEnv
2429
}
2530
deriving stock (Generic)
@@ -36,12 +41,76 @@ addTypeVars = modifyTypeVars . Set.union . Set.fromList
3641
willCaptureType :: (MonadPretty v m) => [v] -> m Bool
3742
willCaptureType vs = views #boundTypes (Set.intersects (Set.fromList vs))
3843

44+
withBoundTerm :: (MonadPretty v m) => v -> m a -> m a
45+
withBoundTerm v =
46+
local (over #boundTerms (Set.insert (Var.reset v)))
47+
48+
withBoundTerms :: (MonadPretty v m) => [v] -> m a -> m a
49+
withBoundTerms vs =
50+
local (over #boundTerms (Set.union (foldMap (Set.singleton . Var.reset) vs)))
51+
3952
runPretty :: (Var v) => PrettyPrintEnv -> Reader (Env v) a -> a
4053
runPretty ppe m =
4154
runReader
4255
m
4356
Env
4457
{ boundTerms = Set.empty,
4558
boundTypes = Set.empty,
59+
freeTerms = Set.empty,
4660
ppe
4761
}
62+
63+
-- Note [Bound and free term variables]
64+
--
65+
-- When rendering a Unison file, we render top-level bindings independently, which may end up referring to each
66+
-- other after all are parsed together. Any individual term, therefore, may have free variables. For example,
67+
--
68+
-- foo = ... bar ...
69+
-- ^^^
70+
-- this "bar" variable is free in foo
71+
--
72+
-- bar = ...
73+
-- ^^^
74+
-- it's ultimately bound by a different top-level term rendering
75+
--
76+
-- Therefore, we pass down all free variables of a top-level term binding, so that we can decide, when rendering one of
77+
-- them, whether to add a leading dot.
78+
--
79+
-- Now, when do we need to add a leading dot? Basically, any time a binder introduces a var that, when rendered reset,
80+
-- clashes with the free var.
81+
--
82+
-- Here are a few examples using a made-up Unison syntax in which we can see whether a let is recursive or
83+
-- non-recursive, and using "%" to separate a var name from its unique id.
84+
--
85+
-- Example 1
86+
--
87+
-- Made-up syntax Actual syntax
88+
-- -------------- ----------------
89+
-- foo%0 = foo =
90+
-- let bar%0 = bar%0 bar = #someref -- rendered as ".bar", then parsed as var "bar"
91+
-- in 5 5
92+
--
93+
-- bar%0 = ... bar = ...
94+
--
95+
-- In this example, we have a non-recursive let that binds a local variable called bar%0. The body of the bar%0 binding
96+
-- can itself refer to a different bar%0, which isn't captured, since a non-recursive let binding body can't refer to
97+
-- the binding.
98+
--
99+
-- So, when rendering the free bar%0 in the right-hand side, we ask: should we add a leading dot? And the answer is: is
100+
-- the var bar%0 in the set of all reset locally-bound vars {bar%0}? Yes? Then yes.
101+
--
102+
-- Example 2
103+
--
104+
-- Made-up syntax Actual syntax
105+
-- -------------- ----------------
106+
-- foo%0 = foo =
107+
-- letrec bar%1 = do bar%0 hey%0 bar = do #someref hey -- rendered as ".bar", then parsed as var "bar"
108+
-- hey%0 = do bar%1 hey = do bar
109+
-- in 5 5
110+
--
111+
-- bar%0 = ... bar = ...
112+
--
113+
-- In this example, we have a recursive let that binds a bar%1 variable, and refers to bar%0 from inside. Same
114+
-- situation, but variable resetting is relevant, because when walking underneath binder bar%1, we want to add its reset
115+
-- form (bar%0) to the set of bound variables to check against, when rendering a free var (which we assume to have
116+
-- unique id 0).

Diff for: parser-typechecker/src/Unison/Syntax/TermPrinter.hs

+27-38
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,12 @@ pretty0
217217
}
218218
term =
219219
specialCases term \case
220-
Var' (Var.reset -> v) -> do
220+
Var' v -> do
221221
env <- ask
222222
let name =
223-
if Set.member v env.boundTerms
223+
if Set.member v env.freeTerms && Set.member v env.boundTerms
224224
then HQ.fromName (Name.makeAbsolute (Name.unsafeParseVar v))
225-
else elideFQN im $ HQ.unsafeFromVar v
225+
else elideFQN im $ HQ.unsafeFromVar (Var.reset v)
226226
pure . parenIfInfix name ic $ styleHashQualified'' (fmt S.Var) name
227227
Ref' r -> do
228228
env <- ask
@@ -687,30 +687,21 @@ printLetBindings ::
687687
m [Pretty SyntaxText]
688688
printLetBindings context = \case
689689
LetBindings bindings -> traverse (printLetBinding context) bindings
690-
LetrecBindings bindings -> traverse (printLetrecBinding context) bindings
690+
LetrecBindings bindings ->
691+
let boundVars = map fst bindings
692+
in traverse (printLetrecBinding context boundVars) bindings
691693

692694
printLetBinding :: (MonadPretty v m) => AmbientContext -> (v, Term3 v PrintAnnotation) -> m (Pretty SyntaxText)
693695
printLetBinding context (v, binding)
694696
| Var.isAction v = pretty0 context binding
695697
| otherwise =
696-
-- For a non-recursive let binding like "let x = y in z", variable "x" is not bound in "y". Yet, "x" may be free
697-
-- in "y" anyway, referring to some previous binding.
698-
--
699-
-- In Unison we don't have a syntax, for non-recusrive let, though, we just have this:
700-
--
701-
-- x = y
702-
-- z
703-
--
704-
-- So, render free "x" in "y" with a leading dot. This is because we happen to know that the only way to have
705-
-- a free "x" in "y" is if "x" is a top-level binding.
706-
renderPrettyBinding
707-
<$> local (over #boundTerms (Set.insert v1)) (prettyBinding0' context (HQ.unsafeFromVar v1) binding)
698+
renderPrettyBinding <$> withBoundTerm v (prettyBinding0' context (HQ.unsafeFromVar v1) binding)
708699
where
709700
v1 = Var.reset v
710701

711-
printLetrecBinding :: (MonadPretty v m) => AmbientContext -> (v, Term3 v PrintAnnotation) -> m (Pretty SyntaxText)
712-
printLetrecBinding context (v, binding) =
713-
renderPrettyBinding <$> prettyBinding0' context (HQ.unsafeFromVar (Var.reset v)) binding
702+
printLetrecBinding :: (MonadPretty v m) => AmbientContext -> [v] -> (v, Term3 v PrintAnnotation) -> m (Pretty SyntaxText)
703+
printLetrecBinding context vs (v, binding) =
704+
renderPrettyBinding <$> withBoundTerms vs (prettyBinding0' context (HQ.unsafeFromVar (Var.reset v)) binding)
714705

715706
prettyPattern ::
716707
forall v loc.
@@ -735,7 +726,7 @@ prettyPattern n c@AmbientContext {imports = im} p vs patt = case patt of
735726
Pattern.Unbound _ -> (fmt S.DelimiterChar $ l "_", vs)
736727
Pattern.Var _ ->
737728
case vs of
738-
(v : tail_vs) -> (fmt S.Var $ l $ Var.nameStr v, tail_vs)
729+
(v : tail_vs) -> (fmt S.Var $ l $ Var.nameStr (Var.reset v), tail_vs)
739730
_ -> error "prettyPattern: Expected at least one var"
740731
Pattern.Boolean _ b -> (fmt S.BooleanLiteral $ if b then l "true" else l "false", vs)
741732
Pattern.Int _ i -> (fmt S.NumericLiteral $ (if i >= 0 then l "+" else mempty) <> l (show i), vs)
@@ -764,7 +755,7 @@ prettyPattern n c@AmbientContext {imports = im} p vs patt = case patt of
764755
case vs of
765756
(v : tail_vs) ->
766757
let (printed, eventual_tail) = prettyPattern n c Prefix tail_vs pat
767-
in (paren (p >= Prefix) (fmt S.Var (l $ Var.nameStr v) <> fmt S.DelimiterChar (l "@") <> printed), eventual_tail)
758+
in (paren (p >= Prefix) (fmt S.Var (l $ Var.nameStr (Var.reset v)) <> fmt S.DelimiterChar (l "@") <> printed), eventual_tail)
768759
_ -> error "prettyPattern: Expected at least one var"
769760
Pattern.EffectPure _ pat ->
770761
let (printed, eventual_tail) = prettyPattern n c Bottom vs pat
@@ -827,7 +818,7 @@ groupCases ::
827818
(Ord v) =>
828819
[MatchCase' () (Term3 v ann)] ->
829820
[([Pattern ()], [v], [(Maybe (Term3 v ann), Term3 v ann)])]
830-
groupCases ms = go0 ms
821+
groupCases = go0
831822
where
832823
go0 [] = []
833824
go0 ms@((p1, _, AbsN' vs1 _) : _) = go2 (p1, vs1) [] ms
@@ -842,12 +833,11 @@ printCase ::
842833
DocLiteralContext ->
843834
[MatchCase' () (Term3 v PrintAnnotation)] ->
844835
m (Pretty SyntaxText)
845-
printCase im doc ms0 =
836+
printCase im doc ms =
846837
PP.orElse
847838
<$> (PP.lines . alignGrid True <$> grid)
848839
<*> (PP.lines . alignGrid False <$> grid)
849840
where
850-
ms = groupCases ms0
851841
justify rows =
852842
zip (fmap fst . PP.align' $ fmap alignPatterns rows) $ fmap gbs rows
853843
where
@@ -876,19 +866,18 @@ printCase im doc ms0 =
876866
)
877867
justified
878868
justified = PP.leftJustify $ fmap (\(g, b) -> (g, (arrow, b))) gbs
879-
grid = traverse go ms
880-
patLhs env vs pats =
881-
case pats of
882-
[pat] -> PP.group (fst (prettyPattern env (ac Annotation Block im doc) Bottom vs pat))
883-
pats -> PP.group
884-
. PP.sep (PP.indentAfterNewline " " $ "," <> PP.softbreak)
885-
. (`evalState` vs)
886-
. for pats
887-
$ \pat -> do
888-
vs <- State.get
889-
let (p, rem) = prettyPattern env (ac Annotation Block im doc) Bottom vs pat
890-
State.put rem
891-
pure p
869+
grid = traverse go (groupCases ms)
870+
patLhs ppe vs = \cases
871+
[pat] -> PP.group (fst (prettyPattern ppe (ac Annotation Block im doc) Bottom vs pat))
872+
pats -> PP.group
873+
. PP.sep (PP.indentAfterNewline " " $ "," <> PP.softbreak)
874+
. (`evalState` vs)
875+
. for pats
876+
$ \pat -> do
877+
vs <- State.get
878+
let (p, rem) = prettyPattern ppe (ac Annotation Block im doc) Bottom vs pat
879+
State.put rem
880+
pure p
892881
arrow = fmt S.ControlKeyword "->"
893882
-- If there's multiple guarded cases for this pattern, prints as:
894883
-- MyPattern x y
@@ -989,7 +978,7 @@ prettyBinding0 ::
989978
m PrettyBinding
990979
prettyBinding0 ac v tm = do
991980
env <- ask
992-
prettyBinding0' ac v (printAnnotate env.ppe tm)
981+
local (set #freeTerms (ABT.freeVars tm)) (prettyBinding0' ac v (printAnnotate env.ppe tm))
993982

994983
prettyBinding0' ::
995984
(MonadPretty v m) =>

Diff for: unison-src/transcripts/idempotent/fix-5525.md

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
The original bug report identified the mishandling of this simple case involving shadowing, in which we previously
2+
erroneously rendered "bar" with a leading dot.
3+
4+
``` ucm
5+
scratch/main> builtins.merge lib.builtin
6+
7+
Done.
8+
```
9+
10+
``` unison
11+
foo =
12+
bar =
13+
match 5 with
14+
1 -> 2
15+
bar -> bar
16+
bar
17+
```
18+
19+
``` ucm :added-by-ucm
20+
Loading changes detected in scratch.u.
21+
22+
I found and typechecked these definitions in scratch.u. If you
23+
do an `add` or `update`, here's how your codebase would
24+
change:
25+
26+
⍟ These new definitions are ok to `add`:
27+
28+
foo : Nat
29+
```
30+
31+
``` ucm
32+
scratch/main> add
33+
34+
⍟ I've added these definitions:
35+
36+
foo : Nat
37+
38+
scratch/main> view foo
39+
40+
foo : Nat
41+
foo =
42+
bar = match 5 with
43+
1 -> 2
44+
bar -> bar
45+
bar
46+
```
47+
48+
``` ucm
49+
scratch/main> project.delete scratch
50+
```
51+
52+
There's a more complicated case that was also previously mishandled, though, which involves a top-level binding to which
53+
for which we do need to add a leading dot in order to refer to.
54+
55+
``` ucm
56+
scratch/main> builtins.merge lib.builtin
57+
58+
Done.
59+
```
60+
61+
``` unison
62+
foo =
63+
bar =
64+
match 5 with
65+
1 -> 2
66+
bar -> bar + .bar
67+
bar
68+
69+
bar = 17
70+
```
71+
72+
``` ucm :added-by-ucm
73+
Loading changes detected in scratch.u.
74+
75+
I found and typechecked these definitions in scratch.u. If you
76+
do an `add` or `update`, here's how your codebase would
77+
change:
78+
79+
⍟ These new definitions are ok to `add`:
80+
81+
bar : Nat
82+
foo : Nat
83+
```
84+
85+
``` ucm
86+
scratch/main> add
87+
88+
⍟ I've added these definitions:
89+
90+
bar : Nat
91+
foo : Nat
92+
93+
scratch/main> view foo
94+
95+
foo : Nat
96+
foo =
97+
use Nat +
98+
bar = match 5 with
99+
1 -> 2
100+
bar1 -> bar + .bar
101+
bar
102+
```
103+
104+
``` ucm
105+
scratch/main> project.delete scratch
106+
```

0 commit comments

Comments
 (0)