Skip to content

Commit b993e9c

Browse files
authored
Merge pull request #875 from smucclaw/thomasgorissen/fix-mcp-errors-and-library-resolution
Fix MCP error reporting, MAYBE param handling, and library import res…
2 parents 2ad304b + f742585 commit b993e9c

11 files changed

Lines changed: 307 additions & 129 deletions

File tree

doc/reference/errors/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,16 +453,21 @@ See [Libraries](../libraries/README.md) for the full list of available libraries
453453

454454
**What went wrong:** L4 could not find the module you are trying to import. L4 searches for modules in this order:
455455

456-
1. Virtual filesystem (VFS) provided by the IDE
457-
2. Relative to the current file
458-
3. XDG data directory (`~/.local/share/jl4/libraries/`)
459-
4. Bundled libraries shipped with L4
456+
1. Virtual filesystem (VFS) provided by the IDE or service
457+
2. `JL4_LIBRARY_PATH` environment variable (if set)
458+
3. Project root directory / relative to the importing file
459+
4. XDG data directory (`~/.local/share/jl4/libraries/`)
460+
5. Bundled with the VSCode extension (`../../libraries/` from executable)
461+
6. Embedded standard libraries compiled into the binary
462+
463+
When `JL4_LIBRARY_PATH` is set, embedded libraries (step 6) are skipped. This gives operators full control over which libraries are available.
460464

461465
**How to fix it:**
462466

463467
- Check the module name for typos.
464-
- For your own modules, use a relative path: `IMPORT ./mymodule`
468+
- For your own modules, place them in the project directory or set `JL4_LIBRARY_PATH`.
465469
- For third-party libraries, install them to `~/.local/share/jl4/libraries/`
470+
- If `JL4_LIBRARY_PATH` is set, ensure it contains the standard libraries you need (e.g., `prelude.l4`).
466471

467472
---
468473

jl4-core/src/L4/Export.hs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import qualified Data.Map.Strict as Map
2929
import qualified Data.Set as Set
3030
import L4.Annotation (getAnno)
3131
import L4.Syntax
32+
import L4.TypeCheck.Environment (maybeUnique)
3233
import L4.TypeCheck.Types (CheckErrorWithContext(..), CheckError(..), CheckEntity(..), EntityInfo)
3334
import Optics
3435

@@ -192,7 +193,7 @@ extractParams typeDescMap (MkTypeSig _ (MkGivenSig _ names) _) =
192193
{ paramName = resolvedToText resolved
193194
, paramType = mType
194195
, paramDescription = paramDesc <|> fallbackDesc
195-
, paramRequired = True
196+
, paramRequired = not (isMaybeType mType)
196197
}
197198

198199
extractReturnType :: TypeSig Resolved -> Maybe (Type' Resolved)
@@ -313,9 +314,14 @@ assumeToParam typeDescMap (MkAssume ann _ (MkAppForm _ name _ _) mType) =
313314
{ paramName = resolvedToText name
314315
, paramType = mType
315316
, paramDescription = paramDesc <|> fallbackDesc
316-
, paramRequired = True
317+
, paramRequired = not (isMaybeType mType)
317318
}
318319

320+
-- | Check if a type annotation is MAYBE (i.e., the parameter is optional).
321+
isMaybeType :: Maybe (Type' Resolved) -> Bool
322+
isMaybeType (Just (TyApp _ name [_inner])) = getUnique name == maybeUnique
323+
isMaybeType _ = False
324+
319325
-- | Extract (name, type) pairs for ASSUME declarations referenced by a DECIDE body.
320326
-- Used by CodeGen to generate LET bindings for ASSUME values.
321327
extractAssumeParamTypes

jl4-core/src/L4/FunctionSchema.hs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import qualified Data.Text as Text
2020

2121
import L4.Export (extractAssumeParamTypes, extractImplicitAssumeParams)
2222
import L4.Syntax
23+
import L4.TypeCheck.Environment (maybeUnique)
2324
import L4.TypeCheck.Types (CheckErrorWithContext)
2425
import qualified Optics
2526

@@ -242,6 +243,12 @@ parametersFromDecideWithErrors resolvedModule decide@(MkDecide _ (MkTypeSig _ (M
242243
in (name, base {parameterDescription = "", parameterAlias = Nothing})
243244

244245
givenParamList = map mkOne names
246+
-- Track which GIVEN params have MAYBE/Optional types (these are not required)
247+
requiredGivenParams =
248+
[ resolvedNameText resolved
249+
| MkOptionallyTypedName _ resolved mType <- names
250+
, not (isMaybeType mType)
251+
]
245252
assumeParamList = map mkAssumeParam assumeParams
246253
implicitParamList = map mkAssumeParam implicitParams
247254

@@ -250,7 +257,7 @@ parametersFromDecideWithErrors resolvedModule decide@(MkDecide _ (MkTypeSig _ (M
250257
in
251258
MkParameters
252259
{ parameterMap = Map.fromList (givenParamList <> allAssumeParams)
253-
, required = map fst givenParamList <> map fst allAssumeParams
260+
, required = requiredGivenParams <> map fst allAssumeParams
254261
}
255262
where
256263
emptyParam :: Text -> Parameter
@@ -266,6 +273,11 @@ parametersFromDecideWithErrors resolvedModule decide@(MkDecide _ (MkTypeSig _ (M
266273
, parameterItems = Nothing
267274
}
268275

276+
-- | Check if a type annotation is MAYBE (i.e., the parameter is optional).
277+
isMaybeType :: Maybe (Type' Resolved) -> Bool
278+
isMaybeType (Just (TyApp _ name [_inner])) = getUnique name == maybeUnique
279+
isMaybeType _ = False
280+
269281
resolvedNameText :: Resolved -> Text
270282
resolvedNameText =
271283
rawNameToText . rawName . getActual

jl4-lsp/src/LSP/L4/Rules.hs

Lines changed: 98 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import qualified Language.LSP.Protocol.Types as LSP
4747
import qualified Data.List as List
4848
import System.Directory
4949
import System.Environment (getExecutablePath, lookupEnv)
50-
import qualified Paths_jl4_core
50+
import qualified L4.API.EmbeddedLibraries as EmbeddedLibraries
5151
import qualified L4.Utils.IntervalMap as IV
5252
import UnliftIO
5353

@@ -232,6 +232,62 @@ instance Pretty Log where
232232
<+> pretty s.category
233233
LogImportResolution msg -> "[Import Resolution]" <+> pretty msg
234234

235+
-- | Result of filesystem library resolution.
236+
data LibraryResolution = LibraryResolution
237+
{ resolvedPath :: !(Maybe FilePath) -- ^ Path found, if any
238+
, searchedPaths :: ![FilePath] -- ^ All paths that were checked
239+
, hasExplicitPath :: !Bool -- ^ True if JL4_LIBRARY_PATH is set (skip embedded libs)
240+
}
241+
242+
-- | Resolve a library module from the filesystem.
243+
-- Checks paths in order of priority:
244+
-- 1. JL4_LIBRARY_PATH environment variable (user/operator override)
245+
-- 2. Root directory (project-local)
246+
-- 3. Relative to importing file
247+
-- 4. XDG data directory (~/.local/share/jl4/libraries/)
248+
-- 5. Bundled with VSCode extension (../../libraries from executable)
249+
--
250+
-- When JL4_LIBRARY_PATH is set, callers should NOT fall back to embedded
251+
-- libraries — the operator has taken explicit control of the library store.
252+
resolveLibraryFromFilesystem :: FilePath -> Maybe NormalizedFilePath -> String -> IO LibraryResolution
253+
resolveLibraryFromFilesystem rootDirectory mImportingFile modName = do
254+
let relPath = do
255+
nfp <- mImportingFile
256+
let dir = takeDirectory (fromNormalizedFilePath nfp)
257+
pure $ dir </> modName <.> "l4"
258+
rootPath = rootDirectory </> modName <.> "l4"
259+
260+
mEnvPath <- lookupEnv "JL4_LIBRARY_PATH"
261+
let hasExplicit = Maybe.isJust mEnvPath
262+
envPaths = case mEnvPath of
263+
Just p -> [p </> modName <.> "l4"]
264+
Nothing -> []
265+
266+
discoverPaths <- do
267+
xdgDataDir <- getXdgDirectory XdgData "jl4"
268+
let xdgPath = xdgDataDir </> "libraries" </> modName <.> "l4"
269+
270+
exePath <- getExecutablePath
271+
let exeDir = takeDirectory exePath
272+
extensionRoot = exeDir </> ".." </> ".."
273+
bundledPath = extensionRoot </> "libraries" </> modName <.> "l4"
274+
275+
pure [xdgPath, bundledPath]
276+
277+
let allPaths = envPaths <> catMaybes [Just rootPath, relPath] <> discoverPaths
278+
279+
result <- runMaybeT $ asum $
280+
flip map allPaths $ \pth -> do
281+
exists <- liftIO (doesFileExist pth)
282+
guard exists
283+
pure pth
284+
285+
pure LibraryResolution
286+
{ resolvedPath = result
287+
, searchedPaths = allPaths
288+
, hasExplicitPath = hasExplicit
289+
}
290+
235291
jl4Rules :: EvaluateLazy.EvalConfig -> FilePath -> Recorder (WithPriority Log) -> Rules ()
236292
jl4Rules evalConfig rootDirectory recorder = do
237293
define shakeRecorder $ \GetLexTokens uri -> do
@@ -302,49 +358,20 @@ jl4Rules evalConfig rootDirectory recorder = do
302358
case vfsResult of
303359
Just vfsUri -> pure $ Just vfsUri
304360
Nothing -> do
305-
-- Fall back to filesystem
306-
let relPath = do
307-
dir <- takeDirectory . fromNormalizedFilePath <$> uriToNormalizedFilePath uri
308-
pure $ dir </> modName <.> "l4"
309-
rootPath = rootDirectory </> modName <.> "l4"
310-
311-
-- Look for libraries in multiple locations, in order of priority:
312-
-- 1. JL4_LIBRARY_PATH environment variable (user override)
313-
-- 2. XDG data directory (~/.local/share/jl4/libraries/) for cabal install
314-
-- 3. Bundled with VSCode extension (../../libraries from executable)
315-
-- 4. Cabal's getDataDir (for cabal run during development)
316-
builtinPaths <- liftIO $ do
317-
-- 1. Check JL4_LIBRARY_PATH environment variable
318-
mEnvPath <- lookupEnv "JL4_LIBRARY_PATH"
319-
let envPaths = case mEnvPath of
320-
Just p -> [p </> modName <.> "l4"]
321-
Nothing -> []
322-
323-
-- 2. XDG data directory (~/.local/share/jl4/libraries/)
324-
xdgDataDir <- getXdgDirectory XdgData "jl4"
325-
let xdgPath = xdgDataDir </> "libraries" </> modName <.> "l4"
326-
327-
-- 3. VSCode extension bundled libraries
328-
exePath <- getExecutablePath
329-
let exeDir = takeDirectory exePath
330-
extensionRoot = exeDir </> ".." </> ".."
331-
bundledPath = extensionRoot </> "libraries" </> modName <.> "l4"
332-
333-
-- 4. Cabal's getDataDir (for development / cabal run)
334-
dataDir <- Paths_jl4_core.getDataDir
335-
let cabalPath = dataDir </> "libraries" </> modName <.> "l4"
336-
337-
pure $ envPaths <> [xdgPath, bundledPath, cabalPath]
338-
339-
let paths = catMaybes [Just rootPath, relPath] <> builtinPaths
340-
341-
existingPath <- runMaybeT $ asum $
342-
flip map paths $ \pth -> do
343-
exists <- liftIO (doesFileExist pth)
344-
guard exists
345-
pure pth
346-
347-
pure $ fmap (toNormalizedUri . filePathToUri) existingPath
361+
let mImportingNfp = uriToNormalizedFilePath uri
362+
res <- liftIO $ resolveLibraryFromFilesystem rootDirectory mImportingNfp modName
363+
case res.resolvedPath of
364+
Just fp -> pure $ Just (toNormalizedUri $ filePathToUri fp)
365+
Nothing
366+
-- Skip embedded libs when operator has set an explicit library path
367+
| res.hasExplicitPath -> pure Nothing
368+
| otherwise ->
369+
case EmbeddedLibraries.lookupEmbeddedLibrary (Text.pack modName) of
370+
Just libContent -> do
371+
let libPath = toNormalizedFilePath ("./" <> modName <.> "l4")
372+
_ <- Shake.addVirtualFile libPath libContent
373+
pure $ Just (normalizedFilePathToUri libPath)
374+
Nothing -> pure Nothing
348375

349376
-- Resolve all import URIs
350377
resolvedUris <- catMaybes <$> traverse resolveImportUri importNames
@@ -422,73 +449,33 @@ jl4Rules evalConfig rootDirectory recorder = do
422449
"Found in VFS: " <> (fromNormalizedUri vfsUri).getUri
423450
pure (rangeOf a, modName, [], vfsUris, Just (Left vfsUri))
424451
Nothing -> do
425-
-- Fall back to filesystem
426-
logWith recorder Debug $ LogImportResolution $
427-
"Not in VFS, checking filesystem..."
428-
429-
paths <- catMaybes <$> do
430-
-- NOTE: if the current URI is a file uri, we first check the directory relative to the current file
431-
--
432-
let relPath = do
433-
dir <- takeDirectory . fromNormalizedFilePath <$> uriToNormalizedFilePath uri
434-
pure $ dir </> modName <.> "l4"
435-
436-
let rootPath = rootDirectory </> modName <.> "l4"
437-
438-
-- Look for libraries in multiple locations, in order of priority:
439-
-- 1. JL4_LIBRARY_PATH environment variable (user override)
440-
-- 2. XDG data directory (~/.local/share/jl4/libraries/) for cabal install
441-
-- 3. Bundled with VSCode extension (../../libraries from executable)
442-
-- 4. Cabal's getDataDir (for cabal run during development)
443-
builtinPaths <- liftIO $ do
444-
-- 1. Check JL4_LIBRARY_PATH environment variable
445-
mEnvPath <- lookupEnv "JL4_LIBRARY_PATH"
446-
let envPaths = case mEnvPath of
447-
Just p -> [p </> modName <.> "l4"]
448-
Nothing -> []
449-
450-
-- 2. Cabal's getDataDir (source tree during development, install prefix when installed)
451-
dataDir <- Paths_jl4_core.getDataDir
452-
let cabalPath = dataDir </> "libraries" </> modName <.> "l4"
453-
454-
-- 3. XDG data directory (~/.local/share/jl4/libraries/)
455-
xdgDataDir <- getXdgDirectory XdgData "jl4"
456-
let xdgPath = xdgDataDir </> "libraries" </> modName <.> "l4"
457-
458-
-- 4. VSCode extension bundled libraries
459-
-- The VSCode extension structure is:
460-
-- extension/
461-
-- ├── bin/<platform>/jl4-lsp[.exe] <- executable is here
462-
-- └── libraries/*.l4 <- libraries are here
463-
-- So we need to go up TWO levels (../../) from the executable.
464-
exePath <- getExecutablePath
465-
let exeDir = takeDirectory exePath
466-
let extensionRoot = exeDir </> ".." </> ".."
467-
let bundledPath = extensionRoot </> "libraries" </> modName <.> "l4"
468-
469-
pure $ envPaths <> [cabalPath, xdgPath, bundledPath]
470-
471-
pure $ [Just rootPath, relPath] <> map Just builtinPaths
472-
473-
logWith recorder Debug $ LogImportResolution $
474-
"Checking filesystem paths: " <> Text.intercalate ", " (map Text.pack paths)
475-
476-
existingPaths <- runMaybeT do
477-
478-
let guardExists pth = do
479-
exists <- liftIO (doesFileExist pth)
480-
guard exists
481-
pure pth
482-
483-
asum $ guardExists <$> paths
484-
485-
case existingPaths of
486-
Just fp -> logWith recorder Info $ LogImportResolution $
487-
"Found on filesystem: " <> Text.pack fp
488-
Nothing -> logWith recorder Warning $ LogImportResolution $
489-
"Module not found: " <> Text.pack modName
490-
491-
pure (rangeOf a, modName, paths, vfsUris, fmap Right existingPaths)
452+
let mImportingNfp = uriToNormalizedFilePath uri
453+
res <- liftIO $ resolveLibraryFromFilesystem rootDirectory mImportingNfp modName
454+
455+
case res.resolvedPath of
456+
Just fp -> do
457+
logWith recorder Info $ LogImportResolution $
458+
"Found on filesystem: " <> Text.pack fp
459+
pure (rangeOf a, modName, res.searchedPaths, vfsUris, Just (Right fp))
460+
Nothing
461+
-- Skip embedded libs when operator has set an explicit library path
462+
| res.hasExplicitPath -> do
463+
logWith recorder Warning $ LogImportResolution $
464+
"Module not found (JL4_LIBRARY_PATH is set, embedded libs skipped): " <> Text.pack modName
465+
pure (rangeOf a, modName, res.searchedPaths, vfsUris, Nothing)
466+
| otherwise ->
467+
case EmbeddedLibraries.lookupEmbeddedLibrary (Text.pack modName) of
468+
Just libContent -> do
469+
logWith recorder Info $ LogImportResolution $
470+
"Found in embedded libraries: " <> Text.pack modName
471+
let libPath = toNormalizedFilePath ("./" <> modName <.> "l4")
472+
_ <- Shake.addVirtualFile libPath libContent
473+
let libUri = normalizedFilePathToUri libPath
474+
pure (rangeOf a, modName, res.searchedPaths, vfsUris, Just (Left libUri))
475+
Nothing -> do
476+
logWith recorder Warning $ LogImportResolution $
477+
"Module not found: " <> Text.pack modName
478+
pure (rangeOf a, modName, res.searchedPaths, vfsUris, Nothing)
492479

493480
mkImportUri (range, modName, fsPaths, vfsUris, mResult) = case mResult of
494481
Just (Left vfsUri) -> do

jl4-service/src/Backend/Api.hs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ data EvaluatorError
258258
deriving stock (Show, Read, Ord, Eq, Generic)
259259
deriving anyclass (FromJSON, ToJSON)
260260

261+
prettyEvaluatorError :: EvaluatorError -> Text
262+
prettyEvaluatorError = \case
263+
InterpreterError msg -> msg
264+
RequiredParameterMissing pm ->
265+
"Required parameter missing: expected " <> Text.pack (show pm.expected)
266+
<> " parameter(s), but got " <> Text.pack (show pm.actual)
267+
UnknownArguments args ->
268+
"Unknown argument(s): " <> Text.intercalate ", " args
269+
CannotHandleParameterType lit ->
270+
"Cannot handle parameter type: " <> Text.pack (show lit)
271+
CannotHandleUnknownVars ->
272+
"Cannot handle unknown variables in input"
273+
261274
data ParameterMismatch = ParameterMismatch
262275
{ expected :: !Int
263276
, actual :: !Int

0 commit comments

Comments
 (0)