ollama-holes-plugin-0.1.0.0: src/GHC/Plugin/OllamaHoles.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- | The Ollama plugin for GHC
module GHC.Plugin.OllamaHoles (plugin) where
import Control.Monad (unless, when)
import Data.Char (isSpace)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import GHC.Plugins hiding ((<>))
import GHC.Tc.Types
import GHC.Tc.Types.Constraint (Hole (..), ctLocEnv, ctLocSpan)
import GHC.Tc.Utils.Monad (getGblEnv, newTcRef)
import Ollama (GenerateOps (..))
import Ollama qualified
-- | Options for the LLM model
genOps :: Ollama.GenerateOps
genOps =
Ollama.defaultGenerateOps
{ modelName = ""
, prompt = ""
}
promptTemplate :: Text
promptTemplate =
"You are a typed-hole plugin within GHC, the Glasgow Haskell Compiler.\n"
<> "You are given a hole in a Haskell program, and you need to fill it in.\n"
<> "The hole is represented by the following information:\n"
<> "{module}\n{location}\n{imports}\n{hole_var}\n{hole_type}\n{relevant_constraints}\n{local_env}\n{global_env}\n{candidate_fits}\n\n"
<> "Provide one or more Haskell expressions that could fill this hole.\n"
<> "This means coming up with an expression of the correct type that satisfies the constraints.\n"
<> "Pay special attention to the type of the hole, specifically whether it is a function.\n"
<> "Make sure you synthesize an expression that matches the type of the hole.\n"
<> "Output ONLY the raw Haskell expression(s), one per line.\n"
<> "Do not include explanations, introductions, or any surrounding text.\n"
<> "Feel free to include any other functions from the list of imports to generate more complicated expressions.\n"
<> "Output a maximum of {numexpr} expresssions.\n"
-- | Ollama plugin for GHC
plugin :: Plugin
plugin =
defaultPlugin
{ holeFitPlugin = \opts ->
Just $
HoleFitPluginR
{ hfPluginInit = newTcRef ()
, hfPluginStop = \_ -> return ()
, hfPluginRun =
const
HoleFitPlugin
{ candPlugin = \_ c -> return c -- Don't filter candidates
, fitPlugin = \hole fits -> do
let Flags{..} = parseFlags opts
dflags <- getDynFlags
gbl_env <- getGblEnv
let mod_name = moduleNameString $ moduleName $ tcg_mod gbl_env
imports = tcg_imports gbl_env
liftIO $ do
available_models <- Ollama.list
case available_models of
Nothing -> T.putStrLn "--- Ollama plugin: No models available.--"
Just (Ollama.Models models) -> do
unless (any ((== model_name) . Ollama.name) models) $
error $
"--- Ollama plugin: Model "
<> T.unpack model_name
<> " not found. "
<> "Use `ollama pull` to download the model, or specify another model using "
<> "`-fplugin-opt=GHC.Plugin.OllamaHoles:model=<model_name>` ---"
when debug $ T.putStrLn "--- Ollama Plugin: Hole Found ---"
let mn = "Module: " <> mod_name
let lc = "Location: " <> showSDoc dflags (ppr $ ctLocSpan . hole_loc <$> th_hole hole)
let im = "Imports: " <> showSDoc dflags (ppr $ moduleEnvKeys $ imp_mods imports)
case th_hole hole of
Just h -> do
let lcl_env = ctLocEnv (hole_loc h)
let hv = "Hole variable: _" <> occNameString (occName $ hole_occ h)
let ht = "Hole type: " <> showSDoc dflags (ppr $ hole_ty h)
let rc = "Relevant constraints: " <> showSDoc dflags (ppr $ th_relevant_cts hole)
let le = "Local environment (bindings): " <> showSDoc dflags (ppr $ tcl_rdr lcl_env)
let ge = "Global environment (bindings): " <> showSDoc dflags (ppr $ tcg_binds gbl_env)
let cf = "Candidate fits: " <> showSDoc dflags (ppr fits)
let prompt' =
replacePlaceholders
promptTemplate
[ ("{module}", mn)
, ("{location}", lc)
, ("{imports}", im)
, ("{hole_var}", hv)
, ("{hole_type}", ht)
, ("{relevant_constraints}", rc)
, ("{local_env}", le)
, ("{global_env}", ge)
, ("{candidate_fits}", cf)
, ("{numexpr}", show num_expr)
]
res <- Ollama.generate genOps{prompt = prompt', modelName = model_name}
case res of
Right rsp -> do
let lns = (preProcess . T.lines) $ Ollama.response_ rsp
when debug $ do
T.putStrLn $ "--- Ollama Plugin: Prompt ---\n" <> prompt'
T.putStrLn $ "--- Ollama Plugin: Response ---\n" <> Ollama.response_ rsp
let fits' = map (RawHoleFit . text . T.unpack) lns
-- Return the generated fits
return fits'
Left err -> do
when debug $
putStrLn $
"Ollama plugin failed to generate a response.\n" <> err
-- Return the original fits without modification
return fits
Nothing -> return fits
}
}
}
-- | Preprocess the response to remove empty lines, lines with only spaces, and code blocks
preProcess :: [Text] -> [Text]
preProcess [] = []
preProcess (ln : lns) | should_drop = preProcess lns
where
should_drop :: Bool
should_drop =
T.null ln
|| T.all isSpace ln
|| T.isPrefixOf "```" ln
preProcess (ln : lns) = transform ln : preProcess lns
where
transform :: Text -> Text
transform = id
-- | Command line options for the plugin
data Flags = Flags
{ model_name :: Text
, num_expr :: Int
, debug :: Bool
}
-- | Default flags for the plugin
defaultFlags :: Flags
defaultFlags =
Flags
{ model_name = "gemma3:27b-it-qat"
, num_expr = 5
, debug = False
}
-- | Parse command line options
parseFlags :: [CommandLineOption] -> Flags
parseFlags = parseFlags' defaultFlags
where
parseFlags' :: Flags -> [CommandLineOption] -> Flags
parseFlags' flags [] = flags
parseFlags' flags (opt : opts)
| T.isPrefixOf "model=" (T.pack opt) =
let model_name = T.drop (T.length "model=") (T.pack opt)
in parseFlags' flags{model_name = model_name} opts
parseFlags' flags (opt : opts)
| T.isPrefixOf "debug=" (T.pack opt) =
let debug = T.unpack $ T.drop (T.length "debug=") (T.pack opt)
in parseFlags' flags{debug = read debug} opts
parseFlags' flags (opt : opts)
| T.isPrefixOf "n=" (T.pack opt) =
let num_expr = T.unpack $ T.drop (T.length "n=") (T.pack opt)
in parseFlags' flags{num_expr = read num_expr} opts
parseFlags' flags _ = flags
-- | Helper function to replace placeholders in a template string
replacePlaceholders :: Text -> [(Text, String)] -> Text
replacePlaceholders = foldl replacePlaceholder
where
replacePlaceholder :: Text -> (Text, String) -> Text
replacePlaceholder str (placeholder, value) = T.replace placeholder (T.pack value) str