Skip to content

Commit

Permalink
Remote imports (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmhat authored Sep 25, 2021
1 parent 74e7eb0 commit 320c184
Show file tree
Hide file tree
Showing 20 changed files with 669 additions and 146 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/haskell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ jobs:
- name: Build
run: cabal build --enable-tests --enable-benchmarks all
- name: Run tests
run: cabal test tasty
run: cabal test grace-core:tasty
68 changes: 68 additions & 0 deletions .hlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# HLint configuration file
# https://github.com/ndmitchell/hlint
##########################

# This file contains a template configuration file, which is typically
# placed as .hlint.yaml in the root of your project


# Specify additional command line arguments
#
# - arguments: [--color, --cpp-simple, -XQuasiQuotes]


# Control which extensions/flags/modules/functions can be used
#
# - extensions:
# - default: false # all extension are banned by default
# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
#
# - flags:
# - {name: -w, within: []} # -w is allowed nowhere
#
# - modules:
# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
#
# - functions:
# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules


# Add custom hints for this project
#
# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}

# The hints are named by the string they display in warning messages.
# For example, if you see a warning starting like
#
# Main.hs:116:51: Warning: Redundant ==
#
# You can refer to that hint with `{name: Redundant ==}` (see below).

# Turn on hints that are off by default
#
# Ban "module X(module X) where", to require a real export list
# - warn: {name: Use explicit module export list}
#
# Replace a $ b $ c with a . b $ c
# - group: {name: dollar, enabled: true}
#
# Generalise map to fmap, ++ to <>
# - group: {name: generalise, enabled: true}


# Ignore some builtin hints
# - ignore: {name: Use let}
# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
- ignore: {name: Use fmap, within: Grace.Parser}
- ignore: {name: Use <$>, within: Grace.Parser}


# Define some custom infix operators
# - fixity: infixr 3 ~^#^~


# To generate a suitable file for HLint do:
# $ hlint --default > .hlint.yaml
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,11 @@ annotation as a last resort.
### Imports
Grace has two ways to import expressions from other sources: Filepath-based
imports and imports using URIs.
#### Imports from files
You can import a Grace subexpression stored within a separate file by
referencing the file's relative or absolute path.
Expand Down Expand Up @@ -974,6 +979,50 @@ $ grace interpret - <<< './greet.ffg "John"'
Any subexpression can be imported in this way.
#### Imports using URIs
Imports with URIs work similar to the ones using a simple filepath.
Suppose you do not have the `greet.ffg` stored locally but instead it resides
on a web server: `http://example.com/grace/greet.ffg`
You could either download it and reference it by its filepath like demonstrated
in the example above or let the Grace interpreter do the job:
```bash
$ grace interpret - <<< 'http://example.com/grace/greet.ffg "John"'
```
```dhall
"Hello, John!"
```
Note that if a particular URI can be handled by the Grace interpreter depends
on its (compile-time) configuration: Internally it relies on a set of _resolvers_
that take care of all the things related to networking like downloading, caching
, verifying the integrity of retrieved file on so on.
For instance, the motivating example will unfortunately not work out-of-the box
since the grace executable has no builtin resolver for HTTP (yet).
Grace comes with two builtin resolvers:
1. One to resolve `env://` URIs,
2. one to resolve `file://` URIs.
Lets have a look at the `env://` resolver first:
```bash
$ MY_VAR='"Hello !"' grace interpret - <<< 'env:///MY_VAR'
```
```dhall
"Hello !"
```
The `file://` resolver is similar to the filepath-based imports we already know:
```bash
$ grace interpret - <<< 'file:///path/to/greet.ffg "John"'
```
```dhall
"Hello, John!"
```
## Name
Like all of my programming language projects, Grace is named after a
Expand Down
6 changes: 6 additions & 0 deletions grace-core/grace-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ library
, insert-ordered-containers
, lens
, megaparsec
, modern-uri
, mtl
, parser-combinators
, prettyprinter
, prettyprinter-ansi-terminal
, safe-exceptions
, scientific
, string-interpolate
, template-haskell
, terminal-size
, text
, typed-process
, unordered-containers
exposed-modules: Grace.Context
, Grace.Domain
, Grace.Existential
, Grace.Import
, Grace.Import.Resolver
, Grace.Interpret
, Grace.Infer
, Grace.Lexer
Expand Down Expand Up @@ -59,6 +64,7 @@ test-suite tasty
, tasty-hunit
, tasty-silver
, text
other-modules: Grace.Test.Resolver
hs-source-dirs: tasty
default-language: Haskell2010
ghc-options: -Wall
Expand Down
124 changes: 124 additions & 0 deletions grace-core/src/Grace/Import.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}

{- | This module contains the functions and types that power to URI-base imports
-}

module Grace.Import
( Input(..)
, Resolver(..)
, resolverToCallback
, ImportError(..)
) where

import Control.Exception.Safe (Exception(..), throw)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Text (Text)
import Grace.Location (Location)
import Grace.Pretty (Pretty(..))
import Grace.Syntax (Syntax)
import System.FilePath ((</>))

import qualified Data.Text as Text
import qualified System.FilePath as FilePath
import qualified Text.URI as URI

{-| Input to the interpreter.
You should prefer to use `Path` if possible (for better error messages and
correctly handling transitive imports). The `Code` constructor is intended
for cases like interpreting code read from standard input.
-}
data Input
= Path FilePath
-- ^ The path to the code
| Code String Text
-- ^ Source code: @Code name content@
| URI URI.URI
deriving (Eq, Show)

instance Semigroup Input where
_ <> URI uri = URI uri

_ <> Code name code = Code name code

Code _ _ <> Path child = Path child
Path parent <> Path child = Path (FilePath.takeDirectory parent </> child)
URI parent <> Path child
| FilePath.isRelative child
, Just uri <- URI.relativeTo childURI parent =
URI uri
| otherwise =
Path child
where
uriPath = do
c : cs <- traverse (URI.mkPathPiece . Text.pack) (FilePath.splitPath child)

return (FilePath.hasTrailingPathSeparator child, c :| cs)

childURI =
URI.URI
{ URI.uriScheme = Nothing
, URI.uriAuthority = Left False
, URI.uriPath = uriPath
, URI.uriQuery = []
, URI.uriFragment = Nothing
}

instance Pretty Input where
pretty (Code _ code) = pretty code
pretty (Path path) = pretty path
pretty (URI uri) = pretty uri

{- | A resolver for an URI.
When the interpreter tries to resolve an URI pointing to some source code
it will try multiple resolvers sequentially and stops if one returns a
@Just code@ value where @code@ is the source code of an expression.
It will then try to parse and interpret that expression.
Here are some good practices for the development of resolvers:
* A resolver should handle exactly one URI scheme.
* If a resolver encounters an URI which it cannot process (e.g. a
@file://@ URI is passed to a HTTP resolver) it should return @Nothing@
as fast as possible.
* Exceptions thrown in resolvers will be caught and rethrown as an
`ImportError` by the interpreter.
-}
newtype Resolver = Resolver
{ runResolver :: Input -> IO (Maybe (Syntax Location Input))
}

instance Semigroup Resolver where
x <> y = Resolver \uri -> do
maybeResult <- runResolver x uri
case maybeResult of
Nothing -> runResolver y uri
_ -> return maybeResult

instance Monoid Resolver where
mempty = Resolver (const (return Nothing))

-- | Convert a resolver to a callback function
resolverToCallback :: Resolver -> Input -> IO (Syntax Location Input)
resolverToCallback resolver uri = do
maybeResult <- runResolver resolver uri
case maybeResult of
Nothing -> throw UnsupportedInput
Just result -> return result

-- | Errors that might be raised during import resolution.
data ImportError
= UnsupportedInput
deriving stock Show

instance Exception ImportError where
displayException UnsupportedInput = "Resolving this input is not supported"
Loading

0 comments on commit 320c184

Please sign in to comment.