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

[ elab ] Support limited file operations in elab scripts, e.g. enable type providers #3099

Merged
merged 1 commit into from
Oct 13, 2023
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* New `fromTTImp`, `fromName`, and `fromDecls` names for custom `TTImp`,
`Name`, and `Decls` literals.
* Call to `%macro`-functions do not require the `ElabReflection` extension.
* Elaborator scripts were made to be able to access project files,
allowing the support for type providers and similar stuff.

### REPL changes

Expand Down
41 changes: 40 additions & 1 deletion libs/base/Language/Reflection.idr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ import public Control.Monad.Trans
--- Elaboration data structure ---
----------------------------------

public export
data LookupDir =
||| The dir of the `ipkg`-file, or the current dir if there is no one
ProjectDir |
||| The source dir set in the `ipkg`-file, or the current dir if there is no one
SourceDir |
||| The dir where the current module is located
|||
||| For the module `Language.Reflection` it would be `<source_dir>/Language/`
CurrentModuleDir |
||| The dir where submodules of the current module are located
|||
||| For the module `Language.Reflection` it would be `<source_dir>/Language/Reflection/`
SubmodulesDir |
||| The dir where built files are located, set in the `ipkg`-file and defaulted to `build`
BuildDir

||| Elaboration scripts
||| Where types/terms are returned, binders will have unique, if not
||| necessarily human readabe, names
Expand Down Expand Up @@ -73,6 +90,13 @@ data Elab : Type -> Type where
-- Check a group of top level declarations
Declare : List Decl -> Elab ()

-- Read the contents of a file, if it is present
ReadFile : LookupDir -> (path : String) -> Elab $ Maybe String
-- Writes to a file, replacing existing contents, if were present
WriteFile : LookupDir -> (path : String) -> (contents : String) -> Elab ()
-- Returns the specified type of dir related to the current idris project
IdrisDir : LookupDir -> Elab String

export
Functor Elab where
map f e = Bind e $ Pure . f
Expand Down Expand Up @@ -148,7 +172,7 @@ interface Monad m => Elaboration m where
||| Given a possibly ambiguous name, get all the matching names and their types
getType : Name -> m (List (Name, TTImp))

||| Get the metadata associated with a name. Returns all matching namea and their types
||| Get the metadata associated with a name. Returns all matching names and their types
getInfo : Name -> m (List (Name, NameInfo))

||| Get the type of a local variable
Expand All @@ -160,6 +184,15 @@ interface Monad m => Elaboration m where
||| Make some top level declarations
declare : List Decl -> m ()

||| Read the contents of a file, if it is present
readFile : LookupDir -> (path : String) -> m $ Maybe String

||| Writes to a file, replacing existing contents, if were present
writeFile : LookupDir -> (path : String) -> (contents : String) -> m ()

||| Returns the specified type of dir related to the current idris project
idrisDir : LookupDir -> m String

export %inline
||| Report an error in elaboration
fail : Elaboration m => String -> m a
Expand Down Expand Up @@ -195,6 +228,9 @@ Elaboration Elab where
getLocalType = GetLocalType
getCons = GetCons
declare = Declare
readFile = ReadFile
writeFile = WriteFile
idrisDir = IdrisDir

public export
Elaboration m => MonadTrans t => Monad (t m) => Elaboration (t m) where
Expand All @@ -216,6 +252,9 @@ Elaboration m => MonadTrans t => Monad (t m) => Elaboration (t m) where
getLocalType = lift . getLocalType
getCons = lift . getCons
declare = lift . declare
readFile = lift .: readFile
writeFile d = lift .: writeFile d
idrisDir = lift . idrisDir

||| Catch failures and use the `Maybe` monad instead
export
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Name/Namespace.idr
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ namespace ModuleIdent
toPath : ModuleIdent -> String
toPath = joinPath . reverse . unsafeUnfoldModuleIdent

export
parent : ModuleIdent -> Maybe ModuleIdent
parent (MkMI (_::rest)) = Just $ MkMI rest
parent _ = Nothing

-------------------------------------------------------------------------------------
-- HIERARCHICAL STRUCTURE
-------------------------------------------------------------------------------------
Expand Down
66 changes: 66 additions & 0 deletions src/TTImp/Elab/RunElab.idr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module TTImp.Elab.RunElab
import Core.Context
import Core.Context.Log
import Core.Core
import Core.Directory
import Core.Env
import Core.Metadata
import Core.Normalise
Expand All @@ -16,13 +17,17 @@ import Idris.Resugar
import Idris.REPL.Opts
import Idris.Syntax

import Libraries.Utils.Path

import TTImp.Elab.Check
import TTImp.Elab.Delayed
import TTImp.Reflect
import TTImp.TTImp
import TTImp.TTImp.Functor
import TTImp.Unelab

import System.File.Meta

%default covering

record NameInfo where
Expand Down Expand Up @@ -85,6 +90,47 @@ elabScript rig fc nest env script@(NDCon nfc nm t ar args) exp
EmptyFC => fc
x => x

-- parses and resolves `Language.Reflection.LookupDir`
lookupDir : Defs -> NF vars -> Core String
lookupDir defs (NDCon _ conName _ _ [])
= do defs <- get Ctxt
NS ns (UN (Basic n)) <- toFullNames conName
| fnm => failWith defs $ "bad lookup dir fullnames " ++ show fnm
let True = ns == reflectionNS
| False => failWith defs $ "bad lookup dir namespace " ++ show ns
let wd = defs.options.dirs.working_dir
let sd = maybe wd (\sd => joinPath [wd, sd]) defs.options.dirs.source_dir
let modDir : Bool -> Core String := \doParent => do
syn <- get Syn
let parentIfNeeded = if doParent then parent else pure
let moduleSuffix = toList $ (head' syn.saveMod >>= parentIfNeeded) <&> toPath
pure $ joinPath $ sd :: moduleSuffix
case n of
"ProjectDir" => pure wd
"SourceDir" => pure sd
"CurrentModuleDir" => modDir True
"SubmodulesDir" => modDir False
"BuildDir" => pure $ joinPath [wd, defs.options.dirs.build_dir]
_ => failWith defs $ "bad lookup dir value"
lookupDir defs lk
= do defs <- get Ctxt
empty <- clearDefs defs
throw (BadRunElab fc env !(quote empty env lk) "lookup dir is not a data value")

validatePath : Defs -> String -> Core ()
validatePath defs path = do
let True = isRelative path
| False => failWith defs $ "path must be relative"
let True = pathDoesNotEscape 0 $ splitPath path
| False => failWith defs $ "path must not escape the directory"
pure ()
where
pathDoesNotEscape : (depth : Nat) -> List String -> Bool
pathDoesNotEscape _ [] = True
pathDoesNotEscape Z (".."::rest) = False
pathDoesNotEscape (S n) (".."::rest) = pathDoesNotEscape n rest
pathDoesNotEscape n (_ ::rest) = pathDoesNotEscape (S n) rest

elabCon : Defs -> String -> List (Closure vars) -> Core (NF vars)
elabCon defs "Pure" [_,val]
= do empty <- clearDefs defs
Expand Down Expand Up @@ -228,6 +274,26 @@ elabScript rig fc nest env script@(NDCon nfc nm t ar args) exp
decls <- reify defs d'
traverse_ (processDecl [] (MkNested []) []) decls
scriptRet ()
elabCon defs "ReadFile" [lk, pth]
= do pathPrefix <- lookupDir defs !(evalClosure defs lk)
path <- reify defs !(evalClosure defs pth)
validatePath defs path
let fullPath = joinPath [pathPrefix, path]
buzden marked this conversation as resolved.
Show resolved Hide resolved
True <- coreLift $ exists fullPath
| False => scriptRet $ Nothing {ty=String}
contents <- readFile fullPath
scriptRet $ Just contents
elabCon defs "WriteFile" [lk, pth, contents]
= do pathPrefix <- lookupDir defs !(evalClosure defs lk)
path <- reify defs !(evalClosure defs pth)
validatePath defs path
contents <- reify defs !(evalClosure defs contents)
let fullPath = joinPath [pathPrefix, path]
whenJust (parent fullPath) ensureDirectoryExists
writeFile fullPath contents
scriptRet ()
elabCon defs "IdrisDir" [lk]
= do evalClosure defs lk >>= lookupDir defs >>= scriptRet
elabCon defs n args = failWith defs $ "unexpected Elab constructor " ++ n ++
", or incorrect count of arguments: " ++ show (length args)
elabScript rig fc nest env script exp
Expand Down
3 changes: 3 additions & 0 deletions tests/idris2/reflection/reflection024/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src/*xistent*
src/..a-dot-named-file
src/another-fancy-record.json
61 changes: 61 additions & 0 deletions tests/idris2/reflection/reflection024/expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--- Print dirs with ipkg ---
1/1: Building Inside.PrintDirs (src/Inside/PrintDirs.idr)
LOG elab:0: project dir: __TEST_DIR__
LOG elab:0: source dir: __TEST_DIR__/src
LOG elab:0: current module dir: __TEST_DIR__/src/Inside
LOG elab:0: submodules dir: __TEST_DIR__/src/Inside/PrintDirs
LOG elab:0: build dir: __TEST_DIR__/build

--- Print dirs with ipkg (with changed pwd) ---
1/1: Building Inside.PrintDirs (src/Inside/PrintDirs.idr)
LOG elab:0: project dir: __TEST_DIR__
LOG elab:0: source dir: __TEST_DIR__/src
LOG elab:0: current module dir: __TEST_DIR__/src/Inside
LOG elab:0: submodules dir: __TEST_DIR__/src/Inside/PrintDirs
LOG elab:0: build dir: __TEST_DIR__/build

--- Print dirs without ipkg (with changed pwd) ---
1/1: Building Inside.PrintDirs (Inside/PrintDirs.idr)
LOG elab:0: project dir: __TEST_DIR__
LOG elab:0: source dir: __TEST_DIR__
LOG elab:0: current module dir: __TEST_DIR__/Inside
LOG elab:0: submodules dir: __TEST_DIR__/Inside/PrintDirs
LOG elab:0: build dir: __TEST_DIR__/build

--- Simple reads and writes ---
1/1: Building SimpleRW (src/SimpleRW.idr)
LOG elab:0: reading existentToRead:
LOG elab:0: contents:
existent to read
second line

LOG elab:0: reading nonExistentToRead:
LOG elab:0: FILE DOES NOT EXIST
LOG elab:0: written to existentToWrite
LOG elab:0: written to nonExistentToWrite
existent to read
second line
cat: src/nonExistentToRead: No such file or directory
WRITTEN CONTENTS
LA-LA-LA
WRITTEN CONTENTS
LA-LA-LA

--- A little but less simple reads and writes ---
1/1: Building LessSimpleRW (src/LessSimpleRW.idr)
LOG elab:0: written to nonExistentOriginally/a-generated-file
LOG elab:0: reading nonExistentOriginally/../nonExistentOriginally/a-generated-file:
LOG elab:0: contents:
WRITTEN CONTENTS
LA-LA-LA

LOG elab:0: written to ..a-dot-named-file
WRITTEN CONTENTS
LA-LA-LA
WRITTEN CONTENTS
LA-LA-LA

--- Type providers ---
1/1: Building TypeProviders (src/TypeProviders.idr)
Derived:
{"veryStringField":"String","veryIntegerField":"Integer","varyNatField":"Prelude.Types.Nat"}
77 changes: 77 additions & 0 deletions tests/idris2/reflection/reflection024/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
. ../../../testutils.sh

echo "--- Print dirs with ipkg ---"
check --find-ipkg src/Inside/PrintDirs.idr | filter_test_dir

##############################

echo
. ../../../testutils.sh

(
cd src
echo "--- Print dirs with ipkg (with changed pwd) ---"
check --find-ipkg Inside/PrintDirs.idr | filter_test_dir
)

##############################

echo

(
cd src
. ../../../../testutils.sh # this is here because we will pollute with `build` dir here
echo "--- Print dirs without ipkg (with changed pwd) ---"
check Inside/PrintDirs.idr | filter_test_dir
)

##############################

echo
. ../../../testutils.sh

echo "--- Simple reads and writes ---"

rm -rf src/nonExistent*
echo "non-overwritten existent to write" > src/existentToWrite

check --find-ipkg src/SimpleRW.idr

cat src/existentToRead
cat src/nonExistentToRead 2>&1
cat src/existentToWrite
cat src/nonExistentToWrite 2>&1

rm -rf src/nonExistent*
echo "non-overwritten existent to write" > src/existentToWrite

##############################

echo
. ../../../testutils.sh

echo "--- A little but less simple reads and writes ---"

rm -rf src/nonExistent*
rm -f src/..a-dot-named-file

check --find-ipkg src/LessSimpleRW.idr

cat src/nonExistentOriginally/a-generated-file 2>&1
cat src/..a-dot-named-file 2>&1

rm -rf src/nonExistent*
rm -f src/..a-dot-named-file

##############################

echo
. ../../../testutils.sh

echo "--- Type providers ---"
rm -rf src/another-fancy-record.json
check --find-ipkg src/TypeProviders.idr

echo "Derived:"
cat src/another-fancy-record.json
echo
19 changes: 19 additions & 0 deletions tests/idris2/reflection/reflection024/src/Inside/PrintDirs.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Inside.PrintDirs

import Language.Reflection

dirs : List (String, LookupDir)
dirs =
[ ("project dir" , ProjectDir )
, ("source dir" , SourceDir )
, ("current module dir", CurrentModuleDir)
, ("submodules dir" , SubmodulesDir )
, ("build dir" , BuildDir )
]

logAllDirs : Elab ()
logAllDirs = for_ dirs $ \(msg, lk) => logMsg "elab" 0 "\{msg}: \{!(idrisDir lk)}"

%language ElabReflection

%runElab logAllDirs
Loading
Loading