diff --git a/unison-core/src/Unison/Name.hs b/unison-core/src/Unison/Name.hs index 371a567e66..0bbe9ba4a8 100644 --- a/unison-core/src/Unison/Name.hs +++ b/unison-core/src/Unison/Name.hs @@ -32,6 +32,7 @@ module Unison.Name parent, stripNamePrefix, unqualified, + isUnqualified, -- * To organize later commonPrefix, @@ -504,6 +505,11 @@ unqualified :: Name -> Name unqualified (Name _ (s :| _)) = Name Relative (s :| []) +isUnqualified :: Name -> Bool +isUnqualified = \case + Name Relative (_ :| []) -> True + Name _ (_ :| _) -> False + -- Tries to shorten `fqn` to the smallest suffix that still unambiguously refers to the same name. Uses an efficient -- logarithmic lookup in the provided relation. -- diff --git a/unison-src/transcripts/fix2822.md b/unison-src/transcripts/fix2822.md new file mode 100644 index 0000000000..e2d414b629 --- /dev/null +++ b/unison-src/transcripts/fix2822.md @@ -0,0 +1,53 @@ +# Inability to reference a term or type with a name that has segments starting with an underscore + +```ucm:hide +scratch/main> builtins.mergeio +``` + +There should be no issue having terms with an underscore-led component + +```unison +_a.blah = 2 + +b = _a.blah + 1 +``` + +Or even that _are_ a single “blank” component + +```unison +_b = 2 + +x = _b + 1 +``` +Types can also have underscore-led components. + +```unison +unique type _a.Blah = A + +c : _a.Blah +c = A +``` + +And we should also be able to access underscore-led fields. + +```unison +type Hello = {_value : Nat} + +doStuff = _value.modify +``` + +But pattern matching shouldn’t bind to underscore-led names. + +```unison:error +dontMap f = cases + None -> false + Some _used -> f _used +``` + +But we can use them as unbound patterns. + +```unison +dontMap f = cases + None -> false + Some _unused -> f 2 +``` diff --git a/unison-src/transcripts/fix2822.output.md b/unison-src/transcripts/fix2822.output.md new file mode 100644 index 0000000000..08f321eaad --- /dev/null +++ b/unison-src/transcripts/fix2822.output.md @@ -0,0 +1,141 @@ +# Inability to reference a term or type with a name that has segments starting with an underscore + +There should be no issue having terms with an underscore-led component + +``` unison +_a.blah = 2 + +b = _a.blah + 1 +``` + +``` ucm + + Loading changes detected in scratch.u. + + I found and typechecked these definitions in scratch.u. If you + do an `add` or `update`, here's how your codebase would + change: + + ⍟ These new definitions are ok to `add`: + + _a.blah : Nat + b : Nat + +``` +Or even that *are* a single “blank” component + +``` unison +_b = 2 + +x = _b + 1 +``` + +``` ucm + + Loading changes detected in scratch.u. + + I found and typechecked these definitions in scratch.u. If you + do an `add` or `update`, here's how your codebase would + change: + + ⍟ These new definitions are ok to `add`: + + _b : Nat + x : Nat + +``` +Types can also have underscore-led components. + +``` unison +unique type _a.Blah = A + +c : _a.Blah +c = A +``` + +``` ucm + + Loading changes detected in scratch.u. + + I found and typechecked these definitions in scratch.u. If you + do an `add` or `update`, here's how your codebase would + change: + + ⍟ These new definitions are ok to `add`: + + type _a.Blah + c : Blah + +``` +And we should also be able to access underscore-led fields. + +``` unison +type Hello = {_value : Nat} + +doStuff = _value.modify +``` + +``` ucm + + Loading changes detected in scratch.u. + + I found and typechecked these definitions in scratch.u. If you + do an `add` or `update`, here's how your codebase would + change: + + ⍟ These new definitions are ok to `add`: + + type Hello + Hello._value : Hello -> Nat + Hello._value.modify : (Nat ->{g} Nat) -> Hello ->{g} Hello + Hello._value.set : Nat -> Hello -> Hello + doStuff : (Nat ->{g} Nat) -> Hello ->{g} Hello + +``` +But pattern matching shouldn’t bind to underscore-led names. + +``` unison +dontMap f = cases + None -> false + Some _used -> f _used +``` + +``` ucm + + Loading changes detected in scratch.u. + + I couldn't figure out what _used refers to here: + + 3 | Some _used -> f _used + + I also don't know what type it should be. + + Some common causes of this error include: + * Your current namespace is too deep to contain the + definition in its subtree + * The definition is part of a library which hasn't been + added to this project + * You have a typo in the name + +``` +But we can use them as unbound patterns. + +``` unison +dontMap f = cases + None -> false + Some _unused -> f 2 +``` + +``` ucm + + Loading changes detected in scratch.u. + + I found and typechecked these definitions in scratch.u. If you + do an `add` or `update`, here's how your codebase would + change: + + ⍟ These new definitions are ok to `add`: + + dontMap : (Nat ->{g} Boolean) -> Optional a ->{g} Boolean + +``` diff --git a/unison-syntax/src/Unison/Syntax/Lexer/Unison.hs b/unison-syntax/src/Unison/Syntax/Lexer/Unison.hs index 18a5f7d0f4..9c50e2731f 100644 --- a/unison-syntax/src/Unison/Syntax/Lexer/Unison.hs +++ b/unison-syntax/src/Unison/Syntax/Lexer/Unison.hs @@ -29,6 +29,7 @@ import Control.Lens qualified as Lens import Control.Monad.State qualified as S import Data.Char (isAlphaNum, isDigit, isSpace, ord, toLower) import Data.Foldable qualified as Foldable +import Data.Functor.Classes (Show1 (..), showsPrec1) import Data.List qualified as List import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty qualified as Nel @@ -46,9 +47,7 @@ import U.Codebase.Reference (ReferenceType (..)) import Unison.HashQualifiedPrime qualified as HQ' import Unison.Name (Name) import Unison.Name qualified as Name -import Unison.NameSegment (NameSegment) import Unison.NameSegment qualified as NameSegment (docSegment) -import Unison.NameSegment.Internal qualified as NameSegment import Unison.Prelude import Unison.ShortHash (ShortHash) import Unison.ShortHash qualified as SH @@ -56,7 +55,7 @@ import Unison.Syntax.HashQualifiedPrime qualified as HQ' (toText) import Unison.Syntax.Lexer import Unison.Syntax.Lexer.Token (posP, tokenP) import Unison.Syntax.Name qualified as Name (isSymboly, nameP, toText, unsafeParseText) -import Unison.Syntax.NameSegment qualified as NameSegment (ParseErr (..), wordyP) +import Unison.Syntax.NameSegment qualified as NameSegment (ParseErr (..)) import Unison.Syntax.Parser.Doc qualified as Doc import Unison.Syntax.Parser.Doc.Data qualified as Doc import Unison.Syntax.ReservedWords (delimiters, typeModifiers, typeOrAbility) @@ -105,18 +104,28 @@ data Err -- further knowledge of spacing or indentation levels -- any knowledge of comments data Lexeme - = Open String -- start of a block - | Semi IsVirtual -- separator between elements of a block - | Close -- end of a block - | Reserved String -- reserved tokens such as `{`, `(`, `type`, `of`, etc - | Textual String -- text literals, `"foo bar"` - | Character Char -- character literals, `?X` - | WordyId (HQ'.HashQualified Name) -- a (non-infix) identifier. invariant: last segment is wordy - | SymbolyId (HQ'.HashQualified Name) -- an infix identifier. invariant: last segment is symboly - | Blank String -- a typed hole or placeholder - | Numeric String -- numeric literals, left unparsed - | Bytes Bytes.Bytes -- bytes literals - | Hash ShortHash -- hash literals + = -- | start of a block + Open String + | -- | separator between elements of a block + Semi IsVirtual + | -- | end of a block + Close + | -- | reserved tokens such as `{`, `(`, `type`, `of`, etc + Reserved String + | -- | text literals, `"foo bar"` + Textual String + | -- | character literals, `?X` + Character Char + | -- | a (non-infix) identifier. invariant: last segment is wordy + WordyId (HQ'.HashQualified Name) + | -- | an infix identifier. invariant: last segment is symboly + SymbolyId (HQ'.HashQualified Name) + | -- | numeric literals, left unparsed + Numeric String + | -- | bytes literals + Bytes Bytes.Bytes + | -- | hash literals + Hash ShortHash | Err Err | Doc (Doc.UntitledSection (Doc.Tree (ReferenceType, HQ'.HashQualified Name) [Token Lexeme])) deriving stock (Eq, Show, Ord) @@ -330,7 +339,6 @@ displayLexeme = \case Character c -> "?" <> [c] WordyId hq -> Text.unpack (HQ'.toTextWith Name.toText hq) SymbolyId hq -> Text.unpack (HQ'.toTextWith Name.toText hq) - Blank b -> b Numeric n -> n Bytes _b -> "bytes literal" Hash h -> Text.unpack (SH.toText h) @@ -436,7 +444,6 @@ lexemes eof = <|> token numeric <|> token character <|> reserved - <|> token blank <|> token identifierLexemeP <|> (asum . map token) [semi, textual, hash] @@ -469,12 +476,6 @@ lexemes eof = t <- tok identifierLexemeP pure $ (fmap Reserved <$> typ) <> t - blank = - separated wordySep do - _ <- char '_' - seg <- P.optional wordyIdSegP - pure (Blank (maybe "" (Text.unpack . NameSegment.toUnescapedText) seg)) - semi = char ';' $> Semi False textual = Textual <$> quoted quoted = quotedRaw <|> quotedSingleLine @@ -758,10 +759,6 @@ identifierLexeme name = then SymbolyId name else WordyId name -wordyIdSegP :: P.ParsecT (Token Err) String m NameSegment -wordyIdSegP = - PI.withParsecT (fmap (ReservedWordyId . Text.unpack)) NameSegment.wordyP - shortHashP :: P.ParsecT (Token Err) String m ShortHash shortHashP = PI.withParsecT (fmap (InvalidShortHash . Text.unpack)) ShortHash.shortHashP @@ -838,17 +835,36 @@ headToken (Block a _ _) = a headToken (Leaf a) = a instance (Show a) => Show (BlockTree a) where - show (Leaf a) = show a - show (Block open mid close) = - show open - ++ "\n" - ++ indent " " (intercalateMap "\n" (intercalateMap " " show) mid) - ++ "\n" - ++ maybe "" show close + showsPrec = showsPrec1 + +-- | This instance should be compatible with `Read`, but inserts newlines and indentation to make it more +-- /human/-readable. +instance Show1 BlockTree where + liftShowsPrec spa sla = shows "" where - indent by s = by ++ (s >>= go by) - go by '\n' = '\n' : by - go _ c = [c] + shows by prec = + showParen (prec > appPrec) . \case + Leaf a -> showString "Leaf " . showsNext spa "" a + Block open mid close -> + showString "Block " + . showsNext spa "" open + . showString "\n" + . showIndentedList (showIndentedList (\b -> showsIndented (shows b 0) b)) (" " <> by) mid + . showString "\n" + . showsNext (liftShowsPrec spa sla) (" " <> by) close + appPrec = 10 + showsNext :: (Int -> x -> ShowS) -> String -> x -> ShowS + showsNext fn = showsIndented (fn $ appPrec + 1) + showsIndented :: (x -> ShowS) -> String -> x -> ShowS + showsIndented fn by x = showString by . fn x + showIndentedList :: (String -> x -> ShowS) -> String -> [x] -> ShowS + showIndentedList fn by xs = + showString by + . showString "[" + . foldr (\x acc -> showString "\n" . fn (" " <> by) x . showString "," . acc) id xs + . showString "\n" + . showString by + . showString "]" reorderTree :: ([[BlockTree a]] -> [[BlockTree a]]) -> BlockTree a -> BlockTree a reorderTree f (Block open mid close) = Block open (f (fmap (reorderTree f) <$> mid)) close @@ -993,7 +1009,6 @@ instance P.VisualStream [Token Lexeme] where Nothing -> '?' : [c] pretty (WordyId n) = Text.unpack (HQ'.toText n) pretty (SymbolyId n) = Text.unpack (HQ'.toText n) - pretty (Blank s) = "_" ++ s pretty (Numeric n) = n pretty (Hash sh) = show sh pretty (Err e) = show e diff --git a/unison-syntax/src/Unison/Syntax/Parser.hs b/unison-syntax/src/Unison/Syntax/Parser.hs index deb1e89f4f..bd243b0d3d 100644 --- a/unison-syntax/src/Unison/Syntax/Parser.hs +++ b/unison-syntax/src/Unison/Syntax/Parser.hs @@ -81,6 +81,8 @@ import Unison.HashQualified qualified as HQ import Unison.HashQualifiedPrime qualified as HQ' import Unison.Hashable qualified as Hashable import Unison.Name as Name +import Unison.NameSegment (NameSegment) +import Unison.NameSegment.Internal qualified as INameSegment import Unison.Names (Names) import Unison.Names.ResolutionResult qualified as Names import Unison.Parser.Ann (Ann (..), Annotated (..)) @@ -90,7 +92,7 @@ import Unison.Prelude import Unison.Reference (Reference) import Unison.Referent (Referent) import Unison.Syntax.Lexer.Unison qualified as L -import Unison.Syntax.Name qualified as Name (toVar, unsafeParseText) +import Unison.Syntax.Name qualified as Name (toVar) import Unison.Syntax.Parser.Doc qualified as Doc import Unison.Syntax.Parser.Doc.Data qualified as Doc import Unison.Term (MatchCase (..)) @@ -279,9 +281,19 @@ closeBlock = void <$> matchToken L.Close optionalCloseBlock :: (Ord v) => P v m (L.Token ()) optionalCloseBlock = closeBlock <|> (\() -> L.Token () mempty mempty) <$> P.eof +-- | A `Name` is blank when it is unqualified and begins with a `_` (also implying that it is wordy) +isBlank :: Name -> Bool +isBlank n = Name.isUnqualified n && Text.isPrefixOf "_" (INameSegment.toUnescapedText $ Name.lastSegment n) + +-- | A HQ Name is blank when its Name is blank and it has no hash. +isBlank' :: HQ'.HashQualified Name -> Bool +isBlank' = \case + HQ'.NameOnly n -> isBlank n + HQ'.HashQualified _ _ -> False + wordyPatternName :: (Var v) => P v m (L.Token v) wordyPatternName = queryToken \case - L.WordyId (HQ'.NameOnly n) -> Just $ Name.toVar n + L.WordyId (HQ'.NameOnly n) -> if isBlank n then Nothing else Just $ Name.toVar n _ -> Nothing -- | Parse a prefix identifier e.g. Foo or (+), discarding any hash @@ -296,7 +308,6 @@ prefixTermName = wordyTermName <|> parenthesize symbolyTermName where wordyTermName = queryToken \case L.WordyId (HQ'.NameOnly n) -> Just $ Name.toVar n - L.Blank s -> Just $ Var.nameds ("_" <> s) _ -> Nothing symbolyTermName = queryToken \case L.SymbolyId (HQ'.NameOnly n) -> Just $ Name.toVar n @@ -306,14 +317,12 @@ prefixTermName = wordyTermName <|> parenthesize symbolyTermName wordyDefinitionName :: (Var v) => P v m (L.Token v) wordyDefinitionName = queryToken \case L.WordyId n -> Just $ Name.toVar (HQ'.toName n) - L.Blank s -> Just $ Var.nameds ("_" <> s) _ -> Nothing -- | Parse a wordyId as a Name, rejecting any hash importWordyId :: (Ord v) => P v m (L.Token Name) importWordyId = queryToken \case L.WordyId (HQ'.NameOnly n) -> Just n - L.Blank s | not (null s) -> Just $ Name.unsafeParseText (Text.pack ("_" <> s)) _ -> Nothing -- | The `+` in: use Foo.bar + as a Name @@ -348,7 +357,6 @@ hqWordyId_ :: (Ord v) => P v m (L.Token (HQ.HashQualified Name)) hqWordyId_ = queryToken \case L.WordyId n -> Just $ HQ'.toHQ n L.Hash h -> Just $ HQ.HashOnly h - L.Blank s | not (null s) -> Just $ HQ.NameOnly (Name.unsafeParseText (Text.pack ("_" <> s))) _ -> Nothing -- | Parse a hash-qualified symboly ID like >>=#foo or && @@ -365,10 +373,10 @@ reserved w = label w $ queryToken getReserved getReserved _ = Nothing -- | Parse a placeholder or typed hole -blank :: (Ord v) => P v m (L.Token String) +blank :: (Ord v) => P v m (L.Token NameSegment) blank = label "blank" $ queryToken getBlank where - getBlank (L.Blank s) = Just ('_' : s) + getBlank (L.WordyId n) = if isBlank' n then Just (Name.lastSegment $ HQ'.toName n) else Nothing getBlank _ = Nothing numeric :: (Ord v) => P v m (L.Token String)