ollama-holes-plugin (empty) → 0.1.0.0
raw patch · 5 files changed
+393/−0 lines, 5 filesdep +basedep +ghcdep +ollama-haskell
Dependencies added: base, ghc, ollama-haskell, text
Files
- CHANGELOG.md +5/−0
- LICENSE +20/−0
- README.md +75/−0
- ollama-holes-plugin.cabal +115/−0
- src/GHC/Plugin/OllamaHoles.hs +178/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for OllamaHoles++## 0.1.0.0 -- 2024-04-25++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2025 Matthias Pall Gissurarson++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,75 @@+# Ollama Holes++++## Introduction+This is an example of a typed-hole plugin for GHC that uses the [Ollama](https://ollama.com/) to host a local LLM to fill in holes in Haskell code.+++## Example+Given ++```haskell+{-# OPTIONS_GHC -fplugin=GHC.Plugin.OllamaHoles #-}+{-# OPTIONS_GHC -fplugin-opt=GHC.Plugin.OllamaHoles:model=gemma3:27b #-}+{-# OPTIONS_GHC -fplugin-opt=GHC.Plugin.OllamaHoles:n=5 #-}++module Main where++import Data.List+++main :: IO ()+main = do let k = (_b :: [Int] -> [String])+ print (k [1,2,3])++```++We get the following output:+++```text+ Main.hs:12:20: error: [GHC-88464]+ • Found hole: _b :: [Int] -> [String]+ Or perhaps ‘_b’ is mis-spelled, or not in scope+ • In the expression: _b :: [Int] -> [String]+ In an equation for ‘k’: k = (_b :: [Int] -> [String])+ In the expression:+ do let k = (_b :: [Int] -> [String])+ print (k [1, 2, ....])+ • Relevant bindings include+ k :: [Int] -> [String] (bound at Main.hs:12:15)+ main :: IO () (bound at Main.hs:12:1)+ Valid hole fits include+ \x -> map show x+ map (show)+ \x -> map (\y -> "Number: " ++ show y) x+ \x -> replicate (length x) "Hello"+ []+ |+ 12 | main = do let k = (_b :: [Int] -> [String])+ | ^^+ Error: cabal: Failed to build exe:main from OllamaHolesTest-1.0.0.+```++## Installation++1. Install [Ollama](https://ollama.com/download)+2. Install the `gemma3:27b` model (or any other model you prefer) using the following command:++```bash++ollama pull gemma3:27b+```+3. Clone this repository and navigate to the directory, and build the project using:++```bash+cabal build+```+4. Run the example using:++```bash+cabal build Test+```++5. Enjoy! If you want to change the underlying model, make sure to pass the model name via the plugin arguments (see example)
+ ollama-holes-plugin.cabal view
@@ -0,0 +1,115 @@+cabal-version: 3.0+-- The cabal-version field refers to the version of the .cabal specification,+-- and can be different from the cabal-install (the tool) version and the+-- Cabal (the library) version you are using. As such, the Cabal (the library)+-- version used must be equal or greater than the version stated in this field.+-- Starting from the specification version 2.2, the cabal-version field must be+-- the first thing in the cabal file.++-- Initial package description 'OllamaHoles' generated by+-- 'cabal init'. For further documentation, see:+-- http://haskell.org/cabal/users-guide/+--+-- The name of the package.+name: ollama-holes-plugin++-- The package version.+-- See the Haskell package versioning policy (PVP) for standards+-- guiding when and how versions should be incremented.+-- https://pvp.haskell.org+-- PVP summary: +-+------- breaking API changes+-- | | +----- non-breaking API additions+-- | | | +--- code changes with no API change+version: 0.1.0.0++-- A short (one-line) description of the package.+synopsis: A typed-hole plugin that uses LLMs via Ollama to generate valid hole-fits++-- A longer description of the package.+description: This package provides a GHC plugin that uses LLMs via Ollama to generate valid hole-fits.+ .+ The following flags are available:+ .+ To specify the model to use:+ .+ > -fplugin-opt=GHC.Plugin.OllamaHoles:model=<model_name>+ .+ To specify how many fits to generate (passed to the model)+ .+ > -fplugin-opt=GHC.Plugin.OllamaHoles:n=5+ .+ To enable debug output:+ .+ > -fplugin-opt=GHC.Plugin.OllamaHoles:debug=True+ .+ Before using this plugin, make sure you have the Ollama CLI installed and the model+ you want to use is available. You can install the Ollama CLI by following the+ instructions at [https://ollama.com/download](https://ollama.com/download),+ and you can install the default model (gemma3:27b) by running `ollama pull gemma3:27b`.+ .+ Note that the speed and quality of the hole-fits generated by the plugin depends on+ the model you use, and the default model requires a GPU to run efficiently.+ For a smaller model, we suggest `gemma3:4b-it-qat`, or `deepcoder:1.5b`.++++-- The license under which the package is released.+license: MIT++-- The file containing the license text.+license-file: LICENSE++-- The package author(s).+author: Matthias Pall Gissurarson <mpg@mpg.is>++-- An email address to which users can send suggestions, bug reports, and patches.+maintainer: Matthias Pall Gissurarson <mpg@mpg.is>+++-- A copyright notice.+copyright: 2025- Matthias Pall Gissurarson +category: Development, Compiler Plugin+tested-with: GHC == 9.6.*+build-type: Simple++-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.+extra-doc-files: CHANGELOG.md+ README.md++homepage: https://github.com/Tritlo/OllamaHoles++-- Extra source files to be distributed with the package, such as examples, or a tutorial module.+-- extra-source-files:++source-repository head+ type: git+ location: git://github.com/Tritlo/OllamaHoles.git++common warnings+ ghc-options: -Wall++library+ -- Import common warning flags.+ import: warnings++ -- Modules exported by the library.+ exposed-modules: GHC.Plugin.OllamaHoles++ -- Modules included in this library but not exported.+ -- other-modules:++ -- LANGUAGE extensions used by modules in this package.+ -- other-extensions:++ -- Other library packages from which modules are imported.+ build-depends: base ^>=4.18.2.1,+ ghc ^>=9.6,+ ollama-haskell ^>= 0.1,+ text ^>= 2.1,+++ -- Directories containing source files.+ hs-source-dirs: src++ -- Base language which the package is written in.+ default-language: GHC2021
+ src/GHC/Plugin/OllamaHoles.hs view
@@ -0,0 +1,178 @@+{-# 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