Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggest alternate variable names based on edit distance #378

Merged
merged 3 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion disco.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,8 @@ library
-- oeis2 < 1.1,
algebraic-graphs >= 0.5 && < 0.8,
pretty-show >= 1.10 && < 1.11,
boxes >= 0.1.5 && < 0.2
boxes >= 0.1.5 && < 0.2,
edit-distance >= 0.2 && < 0.3,

hs-source-dirs: src
default-language: Haskell2010
Expand Down
4 changes: 0 additions & 4 deletions src/Disco/Context.hs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{-# LANGUAGE DeriveTraversable #-}

-----------------------------------------------------------------------------

-----------------------------------------------------------------------------

-- SPDX-License-Identifier: BSD-3-Clause

-- |
Expand Down
44 changes: 24 additions & 20 deletions src/Disco/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@
CyclicImport ms -> cyclicImportError ms
TypeCheckErr (LocTCError Nothing te) -> prettyTCError te
TypeCheckErr (LocTCError (Just n) te) ->
nest 2 $ vcat
[ "While checking " <> pretty' n <> ":"
, prettyTCError te
]
nest 2 $
vcat
[ "While checking " <> pretty' n <> ":"
, prettyTCError te
]
ParseErr pe -> text (errorBundlePretty pe)
EvalErr ee -> prettyEvalError ee
Panic s ->
Expand All @@ -109,17 +110,21 @@
rtd page = "https://disco-lang.readthedocs.io/en/latest/reference/" <> text page <> ".html"

issue :: Int -> Sem r (Doc ann)
issue n = "See https://github.com/disco-lang/disco/issues/" <> text (show n)

Check warning on line 113 in src/Disco/Error.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 8.8

Defined but not used: ‘issue’

Check warning on line 113 in src/Disco/Error.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 8.10

Defined but not used: ‘issue’

Check warning on line 113 in src/Disco/Error.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.0

Defined but not used: ‘issue’

squote :: String -> String
squote x = "'" ++ x ++ "'"

cyclicImportError ::
Members '[Reader PA, LFresh] r =>
[ModuleName] ->
Sem r (Doc ann)
cyclicImportError ms =
nest 2 $ vcat
[ "Error: module imports form a cycle:"
, intercalate " ->" (map pretty ms)
]
nest 2 $
vcat
[ "Error: module imports form a cycle:"
, intercalate " ->" (map pretty ms)
]

prettyEvalError :: Members '[Reader PA, LFresh] r => EvalError -> Sem r (Doc ann)
prettyEvalError = \case
Expand All @@ -142,11 +147,11 @@
prettyTCError = \case
-- XXX include some potential misspellings along with Unbound
-- see https://github.com/disco-lang/disco/issues/180
Unbound x ->
vcat
[ "Error: there is nothing named" <+> pretty' x <> "."
, rtd "unbound"
]
Unbound x suggestions ->
vcat $
["Error: there is nothing named" <+> pretty' x <> "."]
++ ["Perhaps you meant" <+> intercalate " or" (map (text . squote) suggestions) <> "?" | not (null suggestions)]
++ [rtd "unbound"]
Ambiguous x ms ->
vcat
[ "Error: the name" <+> pretty' x <+> "is ambiguous. It could refer to:"
Expand Down Expand Up @@ -249,13 +254,12 @@
[ "Error: too many arguments for the type '" <> pretty' con <> "'."
, rtd "num-args-type"
]
-- XXX Mention the definition in which it was found, suggest adding the variable
-- as a parameter
UnboundTyVar v ->
vcat
[ "Error: Unknown type variable '" <> pretty' v <> "'."
, rtd "unbound-tyvar"
]
-- XXX Mention the definition in which it was found
UnboundTyVar v suggestions ->
vcat $
["Error: Unknown type variable '" <> pretty' v <> "'."]
++ ["Perhaps you meant" <+> intercalate " or" (map (text . squote) suggestions) <> "?" | not (null suggestions)]
++ [rtd "unbound-tyvar"]
NoPolyRec s ss tys ->
vcat
[ "Error: in the definition of " <> text s <> parens (intercalate "," (map text ss)) <> ": recursive occurrences of" <+> text s <+> "may only have type variables as arguments."
Expand Down
5 changes: 5 additions & 0 deletions src/Disco/Syntax/Operators.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module Disco.Syntax.Operators (
opTable,
uopMap,
bopMap,
opNames,
uPrec,
bPrec,
assoc,
Expand All @@ -36,6 +37,7 @@ import Data.Data (Data)
import GHC.Generics (Generic)
import Unbound.Generics.LocallyNameless

import Data.Char (isAlpha)
import Data.Map (Map, (!))
import qualified Data.Map as M

Expand Down Expand Up @@ -264,6 +266,9 @@ bopMap =
M.fromList $
[(op, info) | opLevel <- opTable, info@(OpInfo (BOpF _ op) _ _) <- opLevel]

opNames :: [String]
opNames = [syn | OpInfo _ syns _ <- concat opTable, syn <- filter (all isAlpha) syns]

-- | A convenient function for looking up the precedence of a unary operator.
uPrec :: UOp -> Int
uPrec = opPrec . (uopMap !)
Expand Down
69 changes: 38 additions & 31 deletions src/Disco/Typecheck.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
{-# LANGUAGE NondecreasingIndentation #-}
{-# LANGUAGE OverloadedStrings #-}

-----------------------------------------------------------------------------

-----------------------------------------------------------------------------

-- |
-- Module : Disco.Typecheck
-- Copyright : disco team and contributors
Expand All @@ -30,32 +26,11 @@ import qualified Data.Map as M
import Data.Maybe (isJust)
import Data.Set (Set)
import qualified Data.Set as S
import Prelude as P hiding (lookup)

import Unbound.Generics.LocallyNameless (
Alpha,
Bind,
Name,
bind,
embed,
name2String,
string2Name,
substs,
unembed,
)
import Unbound.Generics.LocallyNameless.Unsafe (unsafeUnbind)

import Disco.Effects.Fresh
import Polysemy hiding (embed)
import Polysemy.Error
import Polysemy.Output
import Polysemy.Reader
import Polysemy.Writer

import Disco.AST.Surface
import Disco.AST.Typed
import Disco.Context hiding (filter)
import qualified Disco.Context as Ctx
import Disco.Effects.Fresh
import Disco.Messages
import Disco.Module
import Disco.Names
Expand All @@ -67,6 +42,25 @@ import Disco.Typecheck.Constraints
import Disco.Typecheck.Util
import Disco.Types
import Disco.Types.Rules
import Polysemy hiding (embed)
import Polysemy.Error
import Polysemy.Output
import Polysemy.Reader
import Polysemy.Writer
import Text.EditDistance (defaultEditCosts, restrictedDamerauLevenshteinDistance)
import Unbound.Generics.LocallyNameless (
Alpha,
Bind,
Name,
bind,
embed,
name2String,
string2Name,
substs,
unembed,
)
import Unbound.Generics.LocallyNameless.Unsafe (unsafeUnbind)
import Prelude as P hiding (lookup)

------------------------------------------------------------
-- Container utilities
Expand Down Expand Up @@ -104,6 +98,13 @@ inferTelescope inferOne tel = do
(tybs, ctx') <- go bs
return (tyb : tybs, ctx <> ctx')

------------------------------------------------------------
-- Variable name utilities
------------------------------------------------------------

suggestionsFrom :: String -> [String] -> [String]
suggestionsFrom x = filter ((<= 1) . restrictedDamerauLevenshteinDistance defaultEditCosts x)

------------------------------------------------------------
-- Modules
------------------------------------------------------------
Expand Down Expand Up @@ -214,8 +215,11 @@ checkUnboundVars :: Members '[Reader TyDefCtx, Error TCError] r => TypeDefn -> S
checkUnboundVars (TypeDefn _ args body) = go body
where
go (TyAtom (AVar (U x)))
| name2String x `elem` args = return ()
| otherwise = throw $ UnboundTyVar x
| xn `elem` args = return ()
| otherwise = throw $ UnboundTyVar x suggestions
where
xn = name2String x
suggestions = suggestionsFrom xn args
go (TyAtom _) = return ()
go (TyUser name tys) = lookupTyDefn name tys >> mapM_ go tys
go (TyCon _ tys) = mapM_ go tys
Expand Down Expand Up @@ -539,7 +543,10 @@ typecheck Infer (TVar x) = do
-- Pick the first method that succeeds; if none do, throw an unbound
-- variable error.
mt <- runMaybeT . F.asum . map MaybeT $ [tryLocal, tryModule, tryPrim]
maybe (throw (Unbound x)) return mt
ctx <- ask @TyCtx
let inScope = map name2String (Ctx.names ctx) ++ opNames ++ [syn | PrimInfo _ syn _ <- M.elems primMap]
suggestions = suggestionsFrom (name2String x) inScope
maybe (throw $ Unbound x suggestions) return mt
where
-- 1. See if the variable name is bound locally.
tryLocal = do
Expand All @@ -559,13 +566,13 @@ typecheck Infer (TVar x) = do
(_, ty) <- unbind sig
return . Just $ ATVar ty (m .- coerce x)
[] -> return Nothing
_ -> throw $ Ambiguous x (map fst bs)
_nonEmpty -> throw $ Ambiguous x (map fst bs)

-- 3. See if we should convert it to a primitive.
tryPrim =
case toPrim (name2String x) of
(prim : _) -> Just <$> typecheck Infer (TPrim prim)
_ -> return Nothing
[] -> return Nothing

--------------------------------------------------
-- Primitives
Expand Down
11 changes: 6 additions & 5 deletions src/Disco/Typecheck/Util.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

-- |
-- Module : Disco.Typecheck.Util
-- Copyright : (c) 2016 disco team (see LICENSE)
Expand Down Expand Up @@ -52,8 +51,10 @@ noLoc = LocTCError Nothing

-- | Potential typechecking errors.
data TCError
= -- | Encountered an unbound variable
Unbound (Name Term)
= -- | Encountered an unbound variable. The offending variable
-- together with some suggested in-scope names with small edit
-- distance.
Unbound (Name Term) [String]
| -- | Encountered an ambiguous name.
Ambiguous (Name Term) [ModuleName]
| -- | No type is specified for a definition
Expand Down Expand Up @@ -91,8 +92,8 @@ data TCError
NotEnoughArgs Con
| -- | Too many arguments provided to type constructor.
TooManyArgs Con
| -- | Unbound type variable
UnboundTyVar (Name Type)
| -- | Unbound type variable, together with suggested edits
UnboundTyVar (Name Type) [String]
| -- | Polymorphic recursion is not allowed
NoPolyRec String [String] [Type]
| -- | Not an error. The identity of the
Expand Down
14 changes: 14 additions & 0 deletions test/error-unbound/expected
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
Error: encountered undefined name REPL.even. Maybe you haven't defined it yet?
Error: there is nothing named fo.
Perhaps you meant 'foo'?
https://disco-lang.readthedocs.io/en/latest/reference/unbound.html
Error: there is nothing named ofo.
Perhaps you meant 'foo'?
https://disco-lang.readthedocs.io/en/latest/reference/unbound.html
Error: there is nothing named for.
Perhaps you meant 'foo' or 'or'?
https://disco-lang.readthedocs.io/en/latest/reference/unbound.html
Error: there is nothing named oof.
https://disco-lang.readthedocs.io/en/latest/reference/unbound.html
Error: there is nothing named Foo.
Perhaps you meant 'foo'?
https://disco-lang.readthedocs.io/en/latest/reference/unbound.html
7 changes: 7 additions & 0 deletions test/error-unbound/input
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
even : Z -> Bool
even(2)
foo : N
foo = 3
fo + 1
ofo + 1
for + 1
oof + 1
Foo + 1
1 change: 1 addition & 0 deletions test/error-unboundtyvar/expected
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Loading unboundtyvar.disco...
While checking unboundtyvar.Ty:
Error: Unknown type variable 'b'.
Perhaps you meant 'a'?
https://disco-lang.readthedocs.io/en/latest/reference/unbound-tyvar.html
Loading