diff --git a/parser-typechecker/src/Unison/Builtin/Decls.hs b/parser-typechecker/src/Unison/Builtin/Decls.hs index 9a30893513..35d70245d7 100644 --- a/parser-typechecker/src/Unison/Builtin/Decls.hs +++ b/parser-typechecker/src/Unison/Builtin/Decls.hs @@ -776,8 +776,8 @@ tupleTerm = foldr tupleConsTerm (unitTerm mempty) forceTerm :: (Var v) => a -> a -> Term v a -> Term v a forceTerm a au e = Term.app a e (unitTerm au) -delayTerm :: (Var v) => a -> Term v a -> Term v a -delayTerm a = Term.lam a $ Var.typed Var.Delay +delayTerm :: (Var v) => a -> a -> Term v a -> Term v a +delayTerm spanAnn argAnn = Term.lam spanAnn (argAnn, Var.typed Var.Delay) unTupleTerm :: Term.Term2 vt at ap v a -> diff --git a/parser-typechecker/src/Unison/Runtime/ANF.hs b/parser-typechecker/src/Unison/Runtime/ANF.hs index 9b77728f60..f5967cf3f2 100644 --- a/parser-typechecker/src/Unison/Runtime/ANF.hs +++ b/parser-typechecker/src/Unison/Runtime/ANF.hs @@ -169,7 +169,7 @@ expandSimple keep (v, bnd) = (v, apps' (var a v) evs) evs = map (var a) . Set.toList $ Set.difference fvs keep abstract :: (Var v) => Set v -> Term v a -> Term v a -abstract keep bnd = lam' a evs bnd +abstract keep bnd = lamWithoutBindingAnns a evs bnd where a = ABT.annotation bnd fvs = ABT.freeVars bnd @@ -205,7 +205,7 @@ enclose keep rec (Let1NamedTop' top v b@(unAnn -> LamsNamed' vs bd) e) = annotate tm | Ann' _ ty <- b = ann a tm ty | otherwise = tm - lamb = lam' a evs (annotate $ lam' a vs lbody) + lamb = lamWithoutBindingAnns a evs (annotate $ lamWithoutBindingAnns a vs lbody) enclose keep rec t@(unLamsAnnot -> Just (vs0, mty, vs1, body)) = Just $ if null evs then lamb else apps' lamb $ map (var a) evs where @@ -218,7 +218,7 @@ enclose keep rec t@(unLamsAnnot -> Just (vs0, mty, vs1, body)) = annotate tm | Just ty <- mty = ann a tm ty | otherwise = tm - lamb = lam' a (evs ++ vs0) . annotate . lam' a vs1 $ lbody + lamb = lamWithoutBindingAnns a (evs ++ vs0) . annotate . lamWithoutBindingAnns a vs1 $ lbody enclose keep rec t@(Handle' h body) | isStructured body = Just . handle (ABT.annotation t) (rec keep h) $ apps' lamb args @@ -232,8 +232,8 @@ enclose keep rec t@(Handle' h body) | null evs = [constructor a (ConstructorReference Ty.unitRef 0)] | otherwise = var a <$> evs lamb - | null evs = lam' a [fv] lbody - | otherwise = lam' a evs lbody + | null evs = lamWithoutBindingAnns a [fv] lbody + | otherwise = lamWithoutBindingAnns a evs lbody enclose keep rec t@(Match' s0 cs0) = Just $ match a s cs where a = ABT.annotation t @@ -331,7 +331,7 @@ beta rec (LetRecNamedTop' top (fmap (fmap rec) -> vbs) (rec -> bd)) = vbs <&> \(v, b0) -> (v,ABT.annotation b0,) $ case b0 of LamsNamed' vs b | Just n <- Map.lookup v m -> - lam' (ABT.annotation b0) (drop n vs) (dropPrefixes m b) + lamWithoutBindingAnns (ABT.annotation b0) (drop n vs) (dropPrefixes m b) -- shouldn't happen b -> dropPrefixes m b @@ -340,7 +340,7 @@ beta rec (Let1NamedTop' top v l@(LamsNamed' vs bd) (rec -> e)) | n > 0 = Just $ let1' top [(v, lamb)] (dropPrefix v n e) | otherwise = Nothing where - lamb = lam' al (drop n vs) (bd) + lamb = lamWithoutBindingAnns al (drop n vs) (bd) al = ABT.annotation l -- Calculate a maximum number of arguments to drop. -- Enclosing doesn't create let-bound lambdas, so we @@ -353,7 +353,7 @@ beta rec (Let1NamedTop' top v l@(LamsNamed' vs bd) (rec -> e)) beta rec (Apps' l@(LamsNamed' vs body) as) | n <- matchVars 0 vs as, n > 0 = - Just $ apps' (lam' al (drop n vs) (rec body)) (drop n as) + Just $ apps' (lamWithoutBindingAnns al (drop n vs) (rec body)) (drop n as) | otherwise = Nothing where al = ABT.annotation l @@ -422,7 +422,7 @@ groupFloater rec vbs = do where rec' b | Just (vs0, mty, vs1, bd) <- unLamsAnnot b = - lam' a vs0 . maybe id (flip $ ann a) mty . lam' a vs1 <$> rec bd + lamWithoutBindingAnns a vs0 . maybe id (flip $ ann a) mty . lamWithoutBindingAnns a vs1 <$> rec bd where a = ABT.annotation b rec' b = rec b @@ -453,12 +453,12 @@ lamFloater closed tm mv a vs bd = let v = ABT.freshIn cvs $ fromMaybe (typed Var.Float) mv in ( v, ( Set.insert v cvs, - ctx <> [(v, lam' a vs bd)], + ctx <> [(v, lamWithoutBindingAnns a vs bd)], floatDecomp closed v tm dcmp ) ) where - tgt = unannotate (lam' a vs bd) + tgt = unannotate (lamWithoutBindingAnns a vs bd) p (_, flam) = unannotate flam == tgt floatDecomp :: @@ -479,7 +479,7 @@ floater top rec tm0@(Ann' tm ty) = floater top rec (LetRecNamed' vbs e) = Just $ letFloater rec vbs e >>= \case - lm@(LamsNamed' vs bd) | top -> lam' a vs <$> rec bd + lm@(LamsNamed' vs bd) | top -> lamWithoutBindingAnns a vs <$> rec bd where a = ABT.annotation lm tm -> rec tm @@ -492,7 +492,7 @@ floater _ rec (Let1Named' v b e) where a = ABT.annotation b floater top rec tm@(LamsNamed' vs bd) - | top = Just $ lam' a vs <$> rec bd + | top = Just $ lamWithoutBindingAnns a vs <$> rec bd | otherwise = Just $ do bd <- rec bd lv <- lamFloater True tm Nothing a vs bd @@ -627,7 +627,7 @@ saturate dat = ABT.visitPure $ \case | m < n, vs <- snd $ mapAccumL frsh fvs [1 .. n - m], nargs <- var mempty <$> vs -> - Just . lam' mempty vs . apps' f $ args' ++ nargs + Just . lamWithoutBindingAnns mempty vs . apps' f $ args' ++ nargs | m > n, (sargs, eargs) <- splitAt n args', sv <- Var.freshIn fvs $ typed Var.Eta -> diff --git a/parser-typechecker/src/Unison/Syntax/TermParser.hs b/parser-typechecker/src/Unison/Syntax/TermParser.hs index 044a29ead5..2c20b59fa3 100644 --- a/parser-typechecker/src/Unison/Syntax/TermParser.hs +++ b/parser-typechecker/src/Unison/Syntax/TermParser.hs @@ -38,8 +38,8 @@ import Unison.NameSegment qualified as NameSegment import Unison.Names (Names) import Unison.Names qualified as Names import Unison.NamesWithHistory qualified as Names -import Unison.Parser.Ann qualified as Ann import Unison.Parser.Ann (Ann) +import Unison.Parser.Ann qualified as Ann import Unison.Pattern (Pattern) import Unison.Pattern qualified as Pattern import Unison.Prelude @@ -348,7 +348,9 @@ parsePattern = label "pattern" root lam :: (Var v) => TermP v m -> TermP v m lam p = label "lambda" $ mkLam <$> P.try (some prefixDefinitionName <* reserved "->") <*> p where - mkLam vs b = Term.lam' (ann (head vs) <> ann b) (map L.payload vs) b + mkLam vs b = + let annotatedArgs = vs <&> \v -> (ann v, L.payload v) + in Term.lam' (ann (head vs) <> ann b) annotatedArgs b letBlock, handle, ifthen :: (Monad m, Var v) => TermP v m letBlock = label "let" $ (snd <$> block "let") @@ -383,7 +385,8 @@ lamCase = do es -> DD.tupleTerm es anns = ann start <> ann (NonEmpty.last cases) matchTerm = Term.match anns lamvarTerm (toList cases) - pure $ Term.lam' anns vars matchTerm + let annotatedVars = (Ann.GeneratedFrom $ ann start,) <$> vars + pure $ Term.lam' anns annotatedVars matchTerm ifthen = label "if" do start <- peekAny @@ -412,7 +415,7 @@ hashQualifiedPrefixTerm = resolveHashQualified =<< hqPrefixId hashQualifiedInfixTerm :: (Monad m, Var v) => TermP v m hashQualifiedInfixTerm = resolveHashQualified =<< hqInfixId -quasikeyword :: Ord v => Text -> P v m (L.Token ()) +quasikeyword :: (Ord v) => Text -> P v m (L.Token ()) quasikeyword kw = queryToken \case L.WordyId (HQ'.NameOnly n) | nameIsKeyword n kw -> Just () _ -> Nothing @@ -595,11 +598,12 @@ doc2Block = do "syntax.docExample" -> do trm <- term endTok <- closeBlock - pure . (ann startTok <> ann endTok,) $ case trm of + let spanAnn = ann startTok <> ann endTok + pure . (spanAnn,) $ case trm of tm@(Term.Apps' _ xs) -> let fvs = List.Extra.nubOrd $ concatMap (toList . Term.freeVars) xs n = Term.nat (ann tm) (fromIntegral (length fvs)) - lam = addDelay $ Term.lam' (ann tm) fvs tm + lam = addDelay $ Term.lam' (ann tm) ((Ann.GeneratedFrom spanAnn,) <$> fvs) tm in Term.apps' f [n, lam] tm -> Term.apps' f [Term.nat (ann tm) 0, addDelay tm] "syntax.docTransclude" -> evalLike id @@ -980,12 +984,13 @@ delayQuote :: (Monad m, Var v) => TermP v m delayQuote = P.label "quote" do start <- reserved "'" e <- termLeaf - pure $ DD.delayTerm (ann start <> ann e) e + pure $ DD.delayTerm (ann start <> ann e) (ann start) e delayBlock :: (Monad m, Var v) => P v m (Ann {- Ann spanning the whole block -}, Term v Ann) delayBlock = P.label "do" do (spanAnn, b) <- block "do" - pure $ (spanAnn, DD.delayTerm (ann b) b) + let argSpan = (ann b {- would be nice to use the annotation for 'do' here, but it's not terribly important -}) + pure $ (spanAnn, DD.delayTerm (ann b) argSpan b) bang :: (Monad m, Var v) => TermP v m bang = P.label "bang" do @@ -993,10 +998,10 @@ bang = P.label "bang" do e <- termLeaf pure $ DD.forceTerm (ann start <> ann e) (ann start) e -force :: forall m v . (Monad m, Var v) => TermP v m +force :: forall m v. (Monad m, Var v) => TermP v m force = P.label "force" $ P.try do -- `forkAt pool() blah` parses as `forkAt (pool ()) blah` - -- That is, empty parens immediately (no space) following a symbol + -- That is, empty parens immediately (no space) following a symbol -- is treated as high precedence function application of `Unit` fn <- hashQualifiedPrefixTerm tok <- ann <$> openBlockWith "(" @@ -1008,10 +1013,10 @@ seqOp :: (Ord v) => P v m Pattern.SeqOp seqOp = Pattern.Snoc <$ matchToken (L.SymbolyId (HQ'.fromName (Name.fromSegment NameSegment.snocSegment))) - <|> Pattern.Cons - <$ matchToken (L.SymbolyId (HQ'.fromName (Name.fromSegment NameSegment.consSegment))) - <|> Pattern.Concat - <$ matchToken (L.SymbolyId (HQ'.fromName (Name.fromSegment NameSegment.concatSegment))) + <|> Pattern.Cons + <$ matchToken (L.SymbolyId (HQ'.fromName (Name.fromSegment NameSegment.consSegment))) + <|> Pattern.Concat + <$ matchToken (L.SymbolyId (HQ'.fromName (Name.fromSegment NameSegment.concatSegment))) term4 :: (Monad m, Var v) => TermP v m term4 = f <$> some termLeaf @@ -1134,7 +1139,8 @@ binding = label "binding" do mkBinding :: Ann -> [L.Token v] -> Term.Term v Ann -> Term.Term v Ann mkBinding _lhsLoc [] body = body mkBinding lhsLoc args body = - (Term.lam' (lhsLoc <> ann body) (L.payload <$> args) body) + let annotatedArgs = args <&> \arg -> (ann arg, L.payload arg) + in Term.lam' (lhsLoc <> ann body) annotatedArgs body customFailure :: (P.MonadParsec e s m) => e -> m a customFailure = P.customFailure diff --git a/parser-typechecker/src/Unison/Syntax/TermPrinter.hs b/parser-typechecker/src/Unison/Syntax/TermPrinter.hs index 59d27cae36..fcbbb296b8 100644 --- a/parser-typechecker/src/Unison/Syntax/TermPrinter.hs +++ b/parser-typechecker/src/Unison/Syntax/TermPrinter.hs @@ -1958,7 +1958,7 @@ toDocExample' suffix ppe (Apps' (Ref' r) [Nat' n, l@(LamsNamed' vs tm)]) | nameEndsWith ppe suffix r, ABT.freeVars l == mempty, ok tm = - Just (lam' (ABT.annotation l) (drop (fromIntegral n + 1) vs) tm) + Just (lamWithoutBindingAnns (ABT.annotation l) (drop (fromIntegral n + 1) vs) tm) where ok (Apps' f _) = ABT.freeVars f == mempty ok tm = ABT.freeVars tm == mempty diff --git a/parser-typechecker/src/Unison/Typechecker/Components.hs b/parser-typechecker/src/Unison/Typechecker/Components.hs index ccef8995d3..72dac37113 100644 --- a/parser-typechecker/src/Unison/Typechecker/Components.hs +++ b/parser-typechecker/src/Unison/Typechecker/Components.hs @@ -78,7 +78,7 @@ minimize (Term.LetRecNamedAnnotatedTop' isTop blockAnn bs e) = blockAnn [(annotatedVar hdv, hdb)] e - | otherwise = Term.singleLet isTop blockAnn (hdv, hdb) e + | otherwise = Term.singleLet isTop blockAnn (annotationFor hdv) (hdv, hdb) e mklet cycle@((_, _) : _) e = Term.letRec isTop @@ -86,10 +86,7 @@ minimize (Term.LetRecNamedAnnotatedTop' isTop blockAnn bs e) = (first annotatedVar <$> cycle) e mklet [] e = e - in -- The outer annotation is going to be meaningful, so we make - -- sure to preserve it, whereas the annotations at intermediate Abs - -- nodes aren't necessarily meaningful - Right . Just . ABT.annotate blockAnn . foldr mklet e $ cs + in Right . Just . foldr mklet e $ cs minimize _ = Right Nothing minimize' :: diff --git a/parser-typechecker/tests/Unison/Test/Term.hs b/parser-typechecker/tests/Unison/Test/Term.hs index 31122f5aac..4791382bd9 100644 --- a/parser-typechecker/tests/Unison/Test/Term.hs +++ b/parser-typechecker/tests/Unison/Test/Term.hs @@ -57,7 +57,7 @@ test = ref = R.Id h 0 v1 = Var.unnamedRef @Symbol ref -- input component: `ref = \v1 -> ref` - component = Map.singleton ref (Term.lam () v1 (Term.refId () ref)) + component = Map.singleton ref (Term.lam () ((), v1) (Term.refId () ref)) component' = Term.unhashComponent component -- expected unhashed component: `v2 = \v1 -> v2`, where `v2 /= v1`, -- i.e. `v2` cannot be just `ref` converted to a ref-named variable, diff --git a/unison-cli/src/Unison/CommandLine/DisplayValues.hs b/unison-cli/src/Unison/CommandLine/DisplayValues.hs index b7b7d3bf65..6bfb43957d 100644 --- a/unison-cli/src/Unison/CommandLine/DisplayValues.hs +++ b/unison-cli/src/Unison/CommandLine/DisplayValues.hs @@ -178,12 +178,12 @@ displayPretty pped terms typeOf eval types tm = go tm DD.Doc2SpecialFormExample n (DD.Doc2Example vs body) -> P.backticked <$> displayTerm pped terms typeOf eval types ex where - ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) vs) body + ex = Term.lamWithoutBindingAnns (ABT.annotation body) (drop (fromIntegral n) vs) body DD.Doc2SpecialFormExampleBlock n (DD.Doc2Example vs body) -> -- todo: maybe do something with `vs` to indicate the variables are free P.indentN 4 <$> displayTerm' True pped terms typeOf eval types ex where - ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) vs) body + ex = Term.lamWithoutBindingAnns (ABT.annotation body) (drop (fromIntegral n) vs) body -- Link (Either Link.Type Doc2.Term) DD.Doc2SpecialFormLink e -> diff --git a/unison-cli/src/Unison/LSP/Diagnostics.hs b/unison-cli/src/Unison/LSP/Diagnostics.hs index bf9d154980..9416fec9bb 100644 --- a/unison-cli/src/Unison/LSP/Diagnostics.hs +++ b/unison-cli/src/Unison/LSP/Diagnostics.hs @@ -9,6 +9,7 @@ import Language.LSP.Protocol.Message qualified as Msg import Language.LSP.Protocol.Types import Unison.LSP.Types import Unison.Prelude +import Unison.Util.Monoid qualified as Monoid reportDiagnostics :: (Foldable f) => @@ -23,15 +24,15 @@ reportDiagnostics docUri fileVersion diags = do let params = PublishDiagnosticsParams {_uri = docUri, _version = fromIntegral <$> fileVersion, _diagnostics = toList $ diags} sendNotification (Msg.TNotificationMessage jsonRPC Msg.SMethod_TextDocumentPublishDiagnostics params) -mkDiagnostic :: Uri -> Range -> DiagnosticSeverity -> Text -> [(Text, Range)] -> Diagnostic -mkDiagnostic uri r severity msg references = +mkDiagnostic :: Uri -> Range -> DiagnosticSeverity -> [DiagnosticTag] -> Text -> [(Text, Range)] -> Diagnostic +mkDiagnostic uri r severity tags msg references = Diagnostic { _range = r, _severity = Just severity, _code = Nothing, -- We could eventually pass error codes here _source = Just "unison", _message = msg, - _tags = Nothing, + _tags = Monoid.whenM (not $ null tags) (Just tags), _relatedInformation = case references of [] -> Nothing diff --git a/unison-cli/src/Unison/LSP/FileAnalysis.hs b/unison-cli/src/Unison/LSP/FileAnalysis.hs index 9e7a5d632a..221e8957f1 100644 --- a/unison-cli/src/Unison/LSP/FileAnalysis.hs +++ b/unison-cli/src/Unison/LSP/FileAnalysis.hs @@ -112,7 +112,7 @@ checkFile doc = runMaybeT do & toRangeMap let typeSignatureHints = fromMaybe mempty (mkTypeSignatureHints <$> parsedFile <*> typecheckedFile) let fileSummary = FileSummary.mkFileSummary parsedFile typecheckedFile - let unusedBindingDiagnostics = fileSummary ^.. _Just . to termsBySymbol . folded . folding (\(topLevelAnn, _refId, trm, _type) -> UnusedBindings.analyseTerm fileUri topLevelAnn trm) + let unusedBindingDiagnostics = fileSummary ^.. _Just . to termsBySymbol . folded . folding (\(_topLevelAnn, _refId, trm, _type) -> UnusedBindings.analyseTerm fileUri trm) let tokenMap = getTokenMap tokens conflictWarningDiagnostics <- fold <$> for fileSummary \fs -> @@ -194,6 +194,7 @@ computeConflictWarningDiagnostics fileUri fileSummary@FileSummary {fileNames} = fileUri newRange DiagnosticSeverity_Information + [] msg mempty pure $ toDiagnostics conflictedTermLocations <> toDiagnostics conflictedTypeLocations @@ -280,7 +281,7 @@ analyseNotes fileUri ppe src notes = do (errMsg, ranges) <- PrintError.renderParseErrors src err let txtMsg = Text.pack $ Pretty.toPlain 80 errMsg range <- ranges - pure $ mkDiagnostic fileUri (uToLspRange range) DiagnosticSeverity_Error txtMsg [] + pure $ mkDiagnostic fileUri (uToLspRange range) DiagnosticSeverity_Error [] txtMsg [] -- TODO: Some parsing errors likely have reasonable code actions pure (diags, []) Result.UnknownSymbol _ loc -> @@ -336,7 +337,7 @@ analyseNotes fileUri ppe src notes = do let msg = Text.pack $ Pretty.toPlain 80 $ PrintError.printNoteWithSource ppe src note in do (range, references) <- ranges - pure $ mkDiagnostic fileUri range DiagnosticSeverity_Error msg references + pure $ mkDiagnostic fileUri range DiagnosticSeverity_Error [] msg references -- Suggest name replacements or qualifications when there's ambiguity nameResolutionCodeActions :: [Diagnostic] -> [Context.Suggestion Symbol Ann] -> [RangedCodeAction] nameResolutionCodeActions diags suggestions = do diff --git a/unison-cli/src/Unison/LSP/FileAnalysis/UnusedBindings.hs b/unison-cli/src/Unison/LSP/FileAnalysis/UnusedBindings.hs index af688aa4df..46d87c6ec1 100644 --- a/unison-cli/src/Unison/LSP/FileAnalysis/UnusedBindings.hs +++ b/unison-cli/src/Unison/LSP/FileAnalysis/UnusedBindings.hs @@ -10,32 +10,26 @@ import U.Core.ABT (ABT (..)) import U.Core.ABT qualified as ABT import Unison.LSP.Conversions qualified as Cv import Unison.LSP.Diagnostics qualified as Diagnostic -import Unison.Lexer.Pos qualified as Pos import Unison.Parser.Ann (Ann) import Unison.Prelude import Unison.Symbol (Symbol (..)) import Unison.Term (Term) -import Unison.Util.Monoid qualified as Monoid import Unison.Util.Range qualified as Range import Unison.Var qualified as Var -analyseTerm :: Lsp.Uri -> Ann -> Term Symbol Ann -> [Diagnostic] -analyseTerm fileUri topLevelTermAnn tm = +analyseTerm :: Lsp.Uri -> Term Symbol Ann -> [Diagnostic] +analyseTerm fileUri tm = let (unusedVars, _) = ABT.cata alg tm - -- Unfortunately we don't capture the annotation of the actual binding when parsing :'(, for now the least - -- annoying thing to do is just highlight the top of the binding. - mayRange = - Cv.annToURange topLevelTermAnn - <&> (\(Range.Range start@(Pos.Pos line _col) _end) -> Range.Range start (Pos.Pos line 9999)) - <&> Cv.uToLspRange vars = - Map.toList unusedVars & mapMaybe \(v, _ann) -> do - getRelevantVarName v - in case mayRange of - Nothing -> [] - Just lspRange -> - let bindings = Text.intercalate ", " (tShow <$> vars) - in Monoid.whenM (not $ null vars) [Diagnostic.mkDiagnostic fileUri lspRange Diagnostic.DiagnosticSeverity_Warning ("Unused binding(s) " <> bindings <> " inside this term.\nUse the binding(s), or prefix them with an _ to dismiss this warning.") []] + Map.toList unusedVars & mapMaybe \(v, ann) -> do + (,ann) <$> getRelevantVarName v + diagnostics = + vars & mapMaybe \(varName, ann) -> do + -- Limit the range to the first line of the binding to not be too annoying. + -- Maybe in the future we can get the actual annotation of the variable name. + lspRange <- Cv.uToLspRange . Range.startingLine <$> Cv.annToURange ann + pure $ Diagnostic.mkDiagnostic fileUri lspRange Diagnostic.DiagnosticSeverity_Warning [Lsp.DiagnosticTag_Unnecessary] ("Unused binding " <> tShow varName <> ". Use the binding, or prefix it with an _ to dismiss this warning.") [] + in diagnostics where getRelevantVarName :: Symbol -> Maybe Text getRelevantVarName = \case diff --git a/unison-cli/src/Unison/LSP/Queries.hs b/unison-cli/src/Unison/LSP/Queries.hs index b6e87497cf..d8391b8bf7 100644 --- a/unison-cli/src/Unison/LSP/Queries.hs +++ b/unison-cli/src/Unison/LSP/Queries.hs @@ -198,14 +198,14 @@ instance Functor SourceNode where -- children contain that position. findSmallestEnclosingNode :: Pos -> Term Symbol Ann -> Maybe (SourceNode Ann) findSmallestEnclosingNode pos term - | annIsFilePosition (ABT.annotation term) && not (ABT.annotation term `Ann.contains` pos) = Nothing + | annIsFilePosition ann && not (ann `Ann.contains` pos) = Nothing | Just r <- cleanImplicitUnit term = findSmallestEnclosingNode pos r | otherwise = do -- For leaf nodes we require that they be an in-file position, not Intrinsic or -- external. -- In some rare cases it's possible for an External/Intrinsic node to have children that -- ARE in the file, so we need to make sure we still crawl their children. - let guardInFile = guard (annIsFilePosition (ABT.annotation term)) + let guardInFile = guard (annIsFilePosition ann) let bestChild = case ABT.out term of ABT.Tm f -> case f of Term.Int {} -> guardInFile *> Just (TermNode term) @@ -244,7 +244,7 @@ findSmallestEnclosingNode pos term ABT.Var _v -> guardInFile *> Just (TermNode term) ABT.Cycle r -> findSmallestEnclosingNode pos r ABT.Abs _v r -> findSmallestEnclosingNode pos r - let fallback = if annIsFilePosition (ABT.annotation term) then Just (TermNode term) else Nothing + let fallback = if annIsFilePosition ann then Just (TermNode term) else Nothing bestChild <|> fallback where -- tuples always end in an implicit unit, but it's annotated with the span of the whole @@ -256,6 +256,14 @@ findSmallestEnclosingNode pos term ABT.Tm' (Term.App (ABT.Tm' (Term.App (ABT.Tm' (Term.Constructor (ConstructorReference ref 0))) x)) trm) | ref == Builtins.pairRef && Term.amap (const ()) trm == Builtins.unitTerm () -> Just x _ -> Nothing + ann = getTermSpanAnn term + + +-- | Most nodes have the property that their annotation spans all their children, but there are some exceptions. +getTermSpanAnn :: Term Symbol Ann -> Ann +getTermSpanAnn tm = case ABT.out tm of + ABT.Abs _v r -> ABT.annotation tm <> getTermSpanAnn r + _ -> ABT.annotation tm findSmallestEnclosingPattern :: Pos -> Pattern.Pattern Ann -> Maybe (Pattern.Pattern Ann) findSmallestEnclosingPattern pos pat diff --git a/unison-cli/tests/Unison/Test/LSP.hs b/unison-cli/tests/Unison/Test/LSP.hs index 5b42467905..880fd6214b 100644 --- a/unison-cli/tests/Unison/Test/LSP.hs +++ b/unison-cli/tests/Unison/Test/LSP.hs @@ -10,6 +10,8 @@ import Data.String.Here.Uninterpolated (here) import Data.Text import Data.Text qualified as Text import EasyTest +import Language.LSP.Protocol.Lens qualified as LSP +import Language.LSP.Protocol.Types qualified as LSP import System.IO.Temp qualified as Temp import Unison.ABT qualified as ABT import Unison.Builtin.Decls (unitRef) @@ -20,6 +22,8 @@ import Unison.Codebase.Init qualified as Codebase.Init import Unison.Codebase.SqliteCodebase qualified as SC import Unison.ConstructorReference (GConstructorReference (..)) import Unison.FileParsers qualified as FileParsers +import Unison.LSP.Conversions qualified as Cv +import Unison.LSP.FileAnalysis.UnusedBindings qualified as UnusedBindings import Unison.LSP.Queries qualified as LSPQ import Unison.Lexer.Pos qualified as Lexer import Unison.Parser.Ann (Ann (..)) @@ -43,6 +47,10 @@ test = do [ refFinding, annotationNesting ] + scope "diagnostics" $ + tests + [ unusedBindingLocations + ] trm :: Term.F Symbol () () (ABT.Term (Term.F Symbol () ()) Symbol ()) -> LSPQ.SourceNode () trm = LSPQ.TermNode . ABT.tm @@ -239,15 +247,39 @@ term = let ) ] --- | Test helper which lets you specify a cursor position inline with source text as a '|'. +-- | Test helper which lets you specify a cursor position inline with source text as a '^'. extractCursor :: Text -> Test (Lexer.Pos, Text) extractCursor txt = - case Text.splitOn "^" txt of + case splitOnDelimiter '^' txt of + Nothing -> crash "expected exactly one cursor" + Just (before, pos, after) -> pure (pos, before <> after) + +-- | Splits a text on a delimiter, returning the text before and after the delimiter, along with the position of the delimiter. +-- +-- >>> splitOnDelimiter '^' "foo b^ar baz" +-- Just ("foo b",Pos {line = 0, column = 5},"ar baz") +splitOnDelimiter :: Char -> Text -> Maybe (Text, Lexer.Pos, Text) +splitOnDelimiter sym txt = + case Text.splitOn (Text.singleton sym) txt of [before, after] -> - let col = Text.length $ Text.takeWhileEnd (/= '\n') before - line = Prelude.length $ Text.lines before - in pure $ (Lexer.Pos line col, before <> after) - _ -> crash "expected exactly one cursor" + let col = (Text.length $ Text.takeWhileEnd (/= '\n') before) + 1 + line = Text.count "\n" before + 1 + in Just $ (before, Lexer.Pos line col, after) + _ -> Nothing + +-- | Test helper which lets you specify a cursor position inline with source text as a '^'. +-- +-- >>> extractDelimitedBlock ('{', '}') "foo {bar} baz" +-- Just (Ann {start = Pos {line = 1, column = 4}, end = Pos {line = 1, column = 7}},"bar","foo bar baz") +-- +-- >>> extractDelimitedBlock ('{', '}') "term =\n {foo} = 12345" +-- Just (Ann {start = Pos {line = 2, column = 2}, end = Pos {line = 2, column = 5}},"foo","term =\n foo = 12345") +extractDelimitedBlock :: (Char, Char) -> Text -> Maybe (Ann {- ann spanning the inside of the delimiters -}, Text {- Text within the delimiters -}, Text {- entire source text with the delimiters stripped -}) +extractDelimitedBlock (startDelim, endDelim) txt = do + (beforeStart, startPos, afterStart) <- splitOnDelimiter startDelim txt + (beforeEnd, endPos, afterEnd) <- splitOnDelimiter endDelim (beforeStart <> afterStart) + let ann = Ann startPos endPos + pure (ann, Text.takeWhile (/= endDelim) afterStart, beforeEnd <> afterEnd) makeNodeSelectionTest :: (String, Text, Bool, LSPQ.SourceNode ()) -> Test () makeNodeSelectionTest (name, testSrc, testTypechecked, expected) = scope name $ do @@ -308,7 +340,7 @@ annotationNestingTest (name, src) = scope name do & traverse_ \(_fileAnn, _refId, _wk, trm, _typ) -> assertAnnotationsAreNested trm --- | Asserts that for all nodes in the provided ABT, the annotations of all child nodes are +-- | Asserts that for all nodes in the provided ABT EXCEPT Abs nodes, the annotations of all child nodes are -- within the span of the parent node. assertAnnotationsAreNested :: forall f. (Foldable f, Functor f, Show (f (Either String Ann))) => ABT.Term f Symbol Ann -> Test () assertAnnotationsAreNested term = do @@ -319,12 +351,19 @@ assertAnnotationsAreNested term = do alg :: Ann -> ABT.ABT f Symbol (Either String Ann) -> Either String Ann alg ann abt = do childSpan <- abt & foldMapM id - case ann `Ann.encompasses` childSpan of - -- one of the annotations isn't in the file, don't bother checking. - Nothing -> pure (ann <> childSpan) - Just isInFile - | isInFile -> pure ann - | otherwise -> Left $ "Containment breach: children aren't contained with the parent:" <> show (ann, abt) + case abt of + -- Abs nodes are the only nodes whose annotations are allowed to not contain their children, + -- they represet the location of the variable being bound instead. Ideally we'd have a separate child + -- node for that, but we can't add it without editing the ABT or Term types. + ABT.Abs _ _ -> + pure (ann <> childSpan) + _ -> do + case ann `Ann.encompasses` childSpan of + -- one of the annotations isn't in the file, don't bother checking. + Nothing -> pure (ann <> childSpan) + Just isInFile + | isInFile -> pure ann + | otherwise -> Left $ "Containment breach: children aren't contained with the parent:" <> show (ann, abt) typecheckSrc :: String -> @@ -374,3 +413,38 @@ withTestCodebase action = do tmpDir <- Temp.createTempDirectory tmp "lsp-test" Codebase.Init.withCreatedCodebase SC.init "lsp-test" tmpDir SC.DontLock action either (crash . show) pure r + +makeDiagnosticRangeTest :: (String, Text) -> Test () +makeDiagnosticRangeTest (testName, testSrc) = scope testName $ do + (ann, _block, cleanSrc) <- case extractDelimitedBlock ('«', '»') testSrc of + Nothing -> crash "expected exactly one delimited block" + Just r -> pure r + (pf, _mayTypecheckedFile) <- typecheckSrc testName cleanSrc + UF.terms pf + & Map.elems + & \case + [(_a, trm)] -> do + case UnusedBindings.analyseTerm (LSP.Uri "test") trm of + [diag] -> do + let expectedRange = Cv.annToRange ann + let actualRange = Just (diag ^. LSP.range) + when (expectedRange /= actualRange) do + crash $ "Expected diagnostic at range: " <> show expectedRange <> ", got: " <> show actualRange + _ -> crash "Expected exactly one diagnostic" + _ -> crash "Expected exactly one term" + +unusedBindingLocations :: Test () +unusedBindingLocations = + scope "unused bindings" . tests . fmap makeDiagnosticRangeTest $ + [ ( "Unused binding in let block", + [here|term = + usedOne = true + «unused = "unused"» + usedTwo = false + usedOne && usedTwo + |] + ), + ( "Unused argument", + [here|term «unused» = 1|] + ) + ] diff --git a/unison-core/src/Unison/DataDeclaration/Records.hs b/unison-core/src/Unison/DataDeclaration/Records.hs index ac12dfb08c..cdbd13fa3e 100644 --- a/unison-core/src/Unison/DataDeclaration/Records.hs +++ b/unison-core/src/Unison/DataDeclaration/Records.hs @@ -41,7 +41,7 @@ generateRecordAccessors namespaced generatedAnn fields typename typ = -- point -> case point of Point _ y _ -> y get = - Term.lam ann argname $ + Term.lam ann (ann, argname) $ Term.match ann (Term.var ann argname) @@ -57,7 +57,7 @@ generateRecordAccessors namespaced generatedAnn fields typename typ = -- y' point -> case point of Point x _ z -> Point x y' z set = - Term.lam' ann [fname', argname] $ + Term.lam' ann [(ann, fname'), (ann, argname)] $ Term.match ann (Term.var ann argname) @@ -86,7 +86,7 @@ generateRecordAccessors namespaced generatedAnn fields typename typ = -- example: `f point -> case point of Point x y z -> Point x (f y) z` modify = - Term.lam' ann [fname', argname] $ + Term.lam' ann [(ann, fname'), (ann, argname)] $ Term.match ann (Term.var ann argname) diff --git a/unison-core/src/Unison/Term.hs b/unison-core/src/Unison/Term.hs index c5f6193e1d..5a3ea2127a 100644 --- a/unison-core/src/Unison/Term.hs +++ b/unison-core/src/Unison/Term.hs @@ -866,20 +866,40 @@ ann :: Term2 vt at ap v a ann a e t = ABT.tm' a (Ann e t) --- arya: are we sure we want the two annotations to be the same? -lam :: (Ord v) => a -> v -> Term2 vt at ap v a -> Term2 vt at ap v a -lam a v body = ABT.tm' a (Lam (ABT.abs' a v body)) +-- | Add a lambda with a single argument. +lam :: + (Ord v) => + -- | Annotation of the whole lambda + a -> + -- Annotation of just the arg binding + (a, v) -> + Term2 vt at ap v a -> + Term2 vt at ap v a +lam spanAnn (bindingAnn, v) body = ABT.tm' spanAnn (Lam (ABT.abs' bindingAnn v body)) + +-- | Add a lambda with a list of arguments. +lam' :: + (Ord v) => + -- | Annotation of the whole lambda + a -> + [(a {- Annotation of the arg binding -}, v)] -> + Term2 vt at ap v a -> + Term2 vt at ap v a +lam' a vs body = foldr (lam a) body vs + +-- | Only use this variant if you don't have source annotations for the binding arguments available. +lamWithoutBindingAnns :: + (Ord v) => + a -> + [v] -> + Term2 vt at ap v a -> + Term2 vt at ap v a +lamWithoutBindingAnns a vs body = lam' a ((a,) <$> vs) body delay :: (Var v) => a -> Term2 vt at ap v a -> Term2 vt at ap v a delay a body = ABT.tm' a (Lam (ABT.abs' a (ABT.freshIn (ABT.freeVars body) (Var.typed Var.Delay)) body)) -lam' :: (Ord v) => a -> [v] -> Term2 vt at ap v a -> Term2 vt at ap v a -lam' a vs body = foldr (lam a) body vs - -lam'' :: (Ord v) => [(a, v)] -> Term2 vt at ap v a -> Term2 vt at ap v a -lam'' vs body = foldr (uncurry lam) body vs - isLam :: Term2 vt at ap v a -> Bool isLam t = arity t > 0 @@ -947,7 +967,7 @@ letRec isTop blockAnn bindings e = (foldr addAbs body bindings) where addAbs :: ((a, v), b) -> ABT.Term f v a -> ABT.Term f v a - addAbs ((_a, v), _b) t = ABT.abs' blockAnn v t + addAbs ((a, v), _b) t = ABT.abs' a v t body :: Term' vt v a body = ABT.tm' blockAnn (LetRec isTop (map snd bindings) e) @@ -978,7 +998,7 @@ let1 :: Term2 vt at ap v a let1 isTop bindings e = foldr f e bindings where - f ((ann, v), b) body = ABT.tm' (ann <> ABT.annotation body) (Let isTop b (ABT.abs' (ABT.annotation body) v body)) + f ((ann, v), b) body = ABT.tm' (ann <> ABT.annotation body) (Let isTop b (ABT.abs' ann v body)) let1' :: (Semigroup a, Ord v) => @@ -997,12 +1017,14 @@ let1' isTop bindings e = foldr f e bindings singleLet :: (Ord v) => IsTop -> - -- Annotation spanning the whole let-binding + -- Annotation spanning the let-binding and its body + a -> + -- Annotation for just the binding, not the body it's used in. a -> (v, Term2 vt at ap v a) -> Term2 vt at ap v a -> Term2 vt at ap v a -singleLet isTop a (v, body) e = ABT.tm' a (Let isTop body (ABT.abs' a v e)) +singleLet isTop spanAnn absAnn (v, body) e = ABT.tm' spanAnn (Let isTop body (ABT.abs' absAnn v e)) -- let1' :: Var v => [(Text, Term0 vt v)] -> Term0 vt v -> Term0 vt v -- let1' bs e = let1 [(ABT.v' name, b) | (name,b) <- bs ] e @@ -1323,7 +1345,7 @@ betaNormalForm e = e -- x -> f x => f etaNormalForm :: (Ord v) => Term0 v -> Term0 v etaNormalForm tm = case tm of - LamNamed' v body -> step . lam (ABT.annotation tm) v $ etaNormalForm body + LamNamed' v body -> step . lam () ((), v) $ etaNormalForm body where step (LamNamed' v (App' f (Var' v'))) | v == v', v `Set.notMember` freeVars f = f @@ -1333,7 +1355,7 @@ etaNormalForm tm = case tm of -- x -> f x => f as long as `x` is a variable of type `Var.Eta` etaReduceEtaVars :: (Var v) => Term0 v -> Term0 v etaReduceEtaVars tm = case tm of - LamNamed' v body -> step . lam (ABT.annotation tm) v $ etaReduceEtaVars body + LamNamed' v body -> step . lam (ABT.annotation tm) ((), v) $ etaReduceEtaVars body where ok v v' f = v == v' @@ -1383,7 +1405,7 @@ containsExpression = ABT.containsExpression -- Used to find matches of `@rewrite case` rules -- Returns `Nothing` if `pat` can't be interpreted as a `Pattern` -- (like `1 + 1` is not a valid pattern, but `Some x` can be) -containsCaseTerm :: Var v1 => Term2 tv ta tb v1 loc -> Term2 typeVar typeAnn loc v2 a -> Maybe Bool +containsCaseTerm :: (Var v1) => Term2 tv ta tb v1 loc -> Term2 typeVar typeAnn loc v2 a -> Maybe Bool containsCaseTerm pat = (\tm -> containsCase <$> pat' <*> pure tm) where @@ -1456,7 +1478,7 @@ rewriteCasesLHS pat0 pat0' = go t = t -- Implementation detail of `@rewrite case` rules (both find and replace) -toPattern :: Var v => Term2 tv ta tb v loc -> Maybe (Pattern loc) +toPattern :: (Var v) => Term2 tv ta tb v loc -> Maybe (Pattern loc) toPattern tm = case tm of Var' v | "_" `Text.isPrefixOf` Var.name v -> pure $ Pattern.Unbound loc Var' _ -> pure $ Pattern.Var loc @@ -1484,7 +1506,7 @@ toPattern tm = case tm of loc = ABT.annotation tm -- Implementation detail of `@rewrite case` rules (both find and replace) -matchCaseFromTerm :: Var v => Term2 typeVar typeAnn a v a -> Maybe (MatchCase a (Term2 typeVar typeAnn a v a)) +matchCaseFromTerm :: (Var v) => Term2 typeVar typeAnn a v a -> Maybe (MatchCase a (Term2 typeVar typeAnn a v a)) matchCaseFromTerm (App' (Builtin' "#case") (ABT.unabsA -> (_, Apps' _ci [pat, guard, body]))) = do p <- toPattern pat let g = unguard guard diff --git a/unison-share-api/src/Unison/Server/Doc.hs b/unison-share-api/src/Unison/Server/Doc.hs index cd4c811ad3..ec2ee1cd1d 100644 --- a/unison-share-api/src/Unison/Server/Doc.hs +++ b/unison-share-api/src/Unison/Server/Doc.hs @@ -333,11 +333,13 @@ evalDoc terms typeOf eval types tm = DD.Doc2SpecialFormExample n (DD.Doc2Example vs body) -> pure $ EExample ex where - ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) vs) body + annotatedVs = ((),) <$> vs + ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) annotatedVs) body DD.Doc2SpecialFormExampleBlock n (DD.Doc2Example vs body) -> pure $ EExampleBlock ex where - ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) vs) body + annotatedVs = ((),) <$> vs + ex = Term.lam' (ABT.annotation body) (drop (fromIntegral n) annotatedVs) body -- Link (Either Link.Type Doc2.Term) DD.Doc2SpecialFormLink e -> diff --git a/unison-syntax/src/Unison/Lexer/Pos.hs b/unison-syntax/src/Unison/Lexer/Pos.hs index b3297b9221..9286d36b05 100644 --- a/unison-syntax/src/Unison/Lexer/Pos.hs +++ b/unison-syntax/src/Unison/Lexer/Pos.hs @@ -4,21 +4,13 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE ViewPatterns #-} -module Unison.Lexer.Pos (Pos (..), Line, Column, line, column) where +module Unison.Lexer.Pos (Pos (..), Line, Column) where type Line = Int type Column = Int -data Pos = Pos {-# UNPACK #-} !Line {-# UNPACK #-} !Column deriving (Eq, Ord) - -line :: Pos -> Line -line (Pos line _) = line - -column :: Pos -> Column -column (Pos _ column) = column - -instance Show Pos where show (Pos line col) = "line " <> show line <> ", column " <> show col +data Pos = Pos { line :: {-# UNPACK #-} !Line, column :: {-# UNPACK #-} !Column} deriving (Show, Eq, Ord) instance Semigroup Pos where Pos line col <> Pos line2 col2 = diff --git a/unison-syntax/src/Unison/Syntax/Lexer.hs b/unison-syntax/src/Unison/Syntax/Lexer.hs index 6ff55150f7..6a2d74911e 100644 --- a/unison-syntax/src/Unison/Syntax/Lexer.hs +++ b/unison-syntax/src/Unison/Syntax/Lexer.hs @@ -8,8 +8,6 @@ module Unison.Syntax.Lexer Pos (..), Lexeme (..), lexer, - line, - column, escapeChars, debugFileLex, debugLex',