Skip to content

Commit 5416269

Browse files
authored
Alternative Imports (#473)
This implements dhall-lang/dhall-lang#172, by adding "alternative imports", as detailed in dhall-lang/dhall-lang#163. We add: - a `?` operator for specifying alternatives for Imports that fail to resolve - a `missing` keyword for impossible imports (identity for the above operator) Also adds tests for all of the above.
1 parent dba035e commit 5416269

17 files changed

+267
-20
lines changed

dhall.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Extra-Source-Files:
152152
tests/tutorial/*.dhall
153153
tests/typecheck/*.dhall
154154
tests/typecheck/examples/Monoid/*.dhall
155+
tests/import/*.dhall
155156
benchmark/examples/*.dhall
156157
benchmark/deep-nested-large-record/*.dhall
157158

@@ -248,6 +249,7 @@ Test-Suite tasty
248249
GHC-Options: -Wall
249250
Other-Modules:
250251
Format
252+
Import
251253
Normalization
252254
Parser
253255
Regression

src/Dhall/Core.hs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ data ImportType
163163
-- ^ URL of remote resource and optional headers stored in an import
164164
| Env Text
165165
-- ^ Environment variable
166+
| Missing
166167
deriving (Eq, Ord, Show)
167168

168169
instance Semigroup ImportType where
@@ -188,6 +189,8 @@ instance Pretty ImportType where
188189

189190
pretty (Env env) = "env:" <> Pretty.pretty env
190191

192+
pretty Missing = "missing"
193+
191194
-- | How to interpret the import's contents (i.e. as Dhall code or raw text)
192195
data ImportMode = Code | RawText deriving (Eq, Ord, Show)
193196

@@ -401,6 +404,8 @@ data Expr s a
401404
| Project (Expr s a) (Set Text)
402405
-- | > Note s x ~ e
403406
| Note s (Expr s a)
407+
-- | > ImportAlt ~ e1 ? e2
408+
| ImportAlt (Expr s a) (Expr s a)
404409
-- | > Embed import ~ import
405410
| Embed a
406411
deriving (Functor, Foldable, Traversable, Show, Eq, Data)
@@ -474,6 +479,7 @@ instance Monad (Expr s) where
474479
Field a b >>= k = Field (a >>= k) b
475480
Project a b >>= k = Project (a >>= k) b
476481
Note a b >>= k = Note a (b >>= k)
482+
ImportAlt a b >>= k = ImportAlt (a >>= k) (b >>= k)
477483
Embed a >>= k = k a
478484

479485
instance Bifunctor Expr where
@@ -538,6 +544,7 @@ instance Bifunctor Expr where
538544
first k (Field a b ) = Field (first k a) b
539545
first k (Project a b ) = Project (first k a) b
540546
first k (Note a b ) = Note (k a) (first k b)
547+
first k (ImportAlt a b ) = ImportAlt (first k a) (first k b)
541548
first _ (Embed a ) = Embed a
542549

543550
second = fmap
@@ -794,6 +801,10 @@ shift d v (Project a b) = Project a' b
794801
shift d v (Note a b) = Note a b'
795802
where
796803
b' = shift d v b
804+
shift d v (ImportAlt a b) = ImportAlt a' b'
805+
where
806+
a' = shift d v a
807+
b' = shift d v b
797808
-- The Dhall compiler enforces that all embedded values are closed expressions
798809
-- and `shift` does nothing to a closed expression
799810
shift _ _ (Embed p) = Embed p
@@ -942,6 +953,10 @@ subst x e (Project a b) = Project a' b
942953
subst x e (Note a b) = Note a b'
943954
where
944955
b' = subst x e b
956+
subst x e (ImportAlt a b) = ImportAlt a' b'
957+
where
958+
a' = subst x e a
959+
b' = subst x e b
945960
-- The Dhall compiler enforces that all embedded values are closed expressions
946961
-- and `subst` does nothing to a closed expression
947962
subst _ _ (Embed p) = Embed p
@@ -1207,6 +1222,11 @@ alphaNormalize (Note s e₀) =
12071222
Note s e₁
12081223
where
12091224
e₁ = alphaNormalize e₀
1225+
alphaNormalize (ImportAlt l₀ r₀) =
1226+
ImportAlt l₁ r₁
1227+
where
1228+
l₁ = alphaNormalize l₀
1229+
r₁ = alphaNormalize r₀
12101230
alphaNormalize (Embed a) =
12111231
Embed a
12121232

@@ -1307,6 +1327,7 @@ denote (Merge a b c ) = Merge (denote a) (denote b) (fmap denote c)
13071327
denote (Constructors a ) = Constructors (denote a)
13081328
denote (Field a b ) = Field (denote a) b
13091329
denote (Project a b ) = Project (denote a) b
1330+
denote (ImportAlt a b ) = ImportAlt (denote a) (denote b)
13101331
denote (Embed a ) = Embed a
13111332

13121333
{-| Reduce an expression to its normal form, performing beta reduction and applying
@@ -1677,6 +1698,7 @@ normalizeWith ctx e0 = loop (denote e0)
16771698
return (x, v)
16781699
r' -> Project r' xs
16791700
Note _ e' -> loop e'
1701+
ImportAlt l _r -> loop l
16801702
Embed a -> Embed a
16811703

16821704
{-| Returns `True` if two expressions are α-equivalent and β-equivalent and
@@ -1890,6 +1912,7 @@ isNormalized e = case denote e of
18901912
else True
18911913
_ -> True
18921914
Note _ e' -> isNormalized e'
1915+
ImportAlt l _r -> isNormalized l
18931916
Embed _ -> True
18941917

18951918
{-| Detect if the given variable is free within the given expression

src/Dhall/Import.hs

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ module Dhall.Import (
113113
, PrettyHttpException(..)
114114
, MissingFile(..)
115115
, MissingEnvironmentVariable(..)
116+
, MissingImports(..)
116117
) where
117118

118119
import Control.Applicative (empty)
119-
import Control.Exception (Exception, SomeException, throwIO)
120-
import Control.Monad (join)
121-
import Control.Monad.Catch (throwM, MonadCatch(catch))
120+
import Control.Exception (Exception, SomeException, throwIO, toException)
121+
import Control.Monad.Catch (throwM, MonadCatch(catch), catches, Handler(..))
122122
import Control.Monad.IO.Class (MonadIO(..))
123123
import Control.Monad.Trans.State.Strict (StateT)
124124
import Crypto.Hash (SHA256)
@@ -269,6 +269,28 @@ instance Show MissingEnvironmentVariable where
269269
<> "\n"
270270
<> "" <> Text.unpack name
271271

272+
-- | List of Exceptions we encounter while resolving Import Alternatives
273+
newtype MissingImports = MissingImports [SomeException]
274+
275+
instance Exception MissingImports
276+
277+
instance Show MissingImports where
278+
show (MissingImports []) =
279+
"\n"
280+
<> "\ESC[1;31mError\ESC[0m: No valid imports"
281+
<> "\n"
282+
show (MissingImports [e]) = show e
283+
show (MissingImports es) =
284+
"\n"
285+
<> "\ESC[1;31mError\ESC[0m: Failed to resolve imports. Error list:"
286+
<> "\n"
287+
<> concatMap (\e -> "\n" <> show e <> "\n") es
288+
<> "\n"
289+
290+
throwMissingImport :: (MonadCatch m, Exception e) => e -> m a
291+
throwMissingImport e = throwM (MissingImports [(toException e)])
292+
293+
272294
-- | Exception thrown when a HTTP url is imported but dhall was built without
273295
-- the @with-http@ Cabal flag.
274296
data CannotImportHTTPURL =
@@ -336,6 +358,9 @@ instance Canonicalize ImportType where
336358
canonicalize (Env name) =
337359
Env name
338360

361+
canonicalize Missing =
362+
Missing
363+
339364
instance Canonicalize ImportHashed where
340365
canonicalize (ImportHashed hash importType) =
341366
ImportHashed hash (canonicalize importType)
@@ -429,7 +454,7 @@ exprFromImport (Import {..}) = do
429454

430455
if exists
431456
then return ()
432-
else throwIO (MissingFile path)
457+
else throwMissingImport (MissingFile path)
433458

434459
text <- Data.Text.IO.readFile path
435460

@@ -487,7 +512,10 @@ exprFromImport (Import {..}) = do
487512
x <- System.Environment.lookupEnv (Text.unpack env)
488513
case x of
489514
Just string -> return (Text.unpack env, Text.pack string)
490-
Nothing -> throwIO (MissingEnvironmentVariable env)
515+
Nothing -> throwMissingImport (MissingEnvironmentVariable env)
516+
517+
Missing -> liftIO $ do
518+
throwM (MissingImports [])
491519

492520
case importMode of
493521
Code -> do
@@ -537,39 +565,61 @@ loadStaticWith
537565
-> Dhall.Core.Normalizer X
538566
-> Expr Src Import
539567
-> StateT Status m (Expr Src X)
540-
loadStaticWith from_import ctx n (Embed import_) = do
568+
loadStaticWith from_import ctx n expr₀ = case expr₀ of
569+
Embed import_ -> do
541570
imports <- zoom stack State.get
542571

543-
let local (Import (ImportHashed _ (URL {})) _) = False
544-
local (Import (ImportHashed _ (Local {})) _) = True
545-
local (Import (ImportHashed _ (Env {})) _) = True
572+
let local (Import (ImportHashed _ (URL {})) _) = False
573+
local (Import (ImportHashed _ (Local {})) _) = True
574+
local (Import (ImportHashed _ (Env {})) _) = True
575+
local (Import (ImportHashed _ (Missing {})) _) = True
546576

547577
let parent = canonicalizeImport imports
548578
let here = canonicalizeImport (import_:imports)
549579

550580
if local here && not (local parent)
551-
then throwM (Imported imports (ReferentiallyOpaque import_))
581+
then throwMissingImport (Imported imports (ReferentiallyOpaque import_))
552582
else return ()
553583

554584
expr <- if here `elem` canonicalizeAll imports
555-
then throwM (Imported imports (Cycle import_))
585+
then throwMissingImport (Imported imports (Cycle import_))
556586
else do
557587
m <- zoom cache State.get
558588
case Map.lookup here m of
559589
Just expr -> return expr
560590
Nothing -> do
561-
let handler
562-
:: MonadCatch m
591+
-- Here we have to match and unwrap the @MissingImports@
592+
-- in a separate handler, otherwise we'd have it wrapped
593+
-- in another @Imported@ when parsing a @missing@, because
594+
-- we are representing it with an empty exception list
595+
-- (which would not be empty if this would happen).
596+
-- TODO: restructure the Exception hierarchy to prevent
597+
-- this nesting from happening in the first place.
598+
let handler₀
599+
:: (MonadCatch m)
600+
=> MissingImports
601+
-> StateT Status m (Expr Src Import)
602+
handler₀ e@(MissingImports []) = throwM e
603+
handler₀ (MissingImports [e]) =
604+
throwMissingImport (Imported (import_:imports) e)
605+
handler₀ (MissingImports es) = throwM
606+
(MissingImports
607+
(fmap
608+
(\e -> (toException (Imported (import_:imports) e)))
609+
es))
610+
handler₁
611+
:: (MonadCatch m)
563612
=> SomeException
564613
-> StateT Status m (Expr Src Import)
565-
handler e = throwM (Imported (import_:imports) e)
614+
handler₁ e =
615+
throwMissingImport (Imported (import_:imports) e)
566616

567617
-- This loads a \"dynamic\" expression (i.e. an expression
568618
-- that might still contain imports)
569619
let loadDynamic =
570620
from_import (canonicalizeImport (import_:imports))
571621

572-
expr' <- loadDynamic `catch` handler
622+
expr' <- loadDynamic `catches` [ Handler handler₀, Handler handler₁ ]
573623

574624
let imports' = import_:imports
575625
zoom stack (State.put imports')
@@ -599,12 +649,80 @@ loadStaticWith from_import ctx n (Embed import_) = do
599649
let actualHash = hashExpression expr
600650
if expectedHash == actualHash
601651
then return ()
602-
else throwM (Imported (import_:imports) (HashMismatch {..}))
652+
else throwMissingImport (Imported (import_:imports) (HashMismatch {..}))
603653

604654
return expr
605-
loadStaticWith from_import ctx n expr = fmap join (traverse process expr)
655+
ImportAlt a b -> loop a `catch` handler₀
656+
where
657+
handler₀ (MissingImports es₀) =
658+
loop b `catch` handler₁
659+
where
660+
handler₁ (MissingImports es₁) =
661+
throwM (MissingImports (es₀ ++ es₁))
662+
Const a -> pure (Const a)
663+
Var a -> pure (Var a)
664+
Lam a b c -> Lam <$> pure a <*> loop b <*> loop c
665+
Pi a b c -> Pi <$> pure a <*> loop b <*> loop c
666+
App a b -> App <$> loop a <*> loop b
667+
Let a b c d -> Let <$> pure a <*> mapM loop b <*> loop c <*> loop d
668+
Annot a b -> Annot <$> loop a <*> loop b
669+
Bool -> pure Bool
670+
BoolLit a -> pure (BoolLit a)
671+
BoolAnd a b -> BoolAnd <$> loop a <*> loop b
672+
BoolOr a b -> BoolOr <$> loop a <*> loop b
673+
BoolEQ a b -> BoolEQ <$> loop a <*> loop b
674+
BoolNE a b -> BoolNE <$> loop a <*> loop b
675+
BoolIf a b c -> BoolIf <$> loop a <*> loop b <*> loop c
676+
Natural -> pure Natural
677+
NaturalLit a -> pure (NaturalLit a)
678+
NaturalFold -> pure NaturalFold
679+
NaturalBuild -> pure NaturalBuild
680+
NaturalIsZero -> pure NaturalIsZero
681+
NaturalEven -> pure NaturalEven
682+
NaturalOdd -> pure NaturalOdd
683+
NaturalToInteger -> pure NaturalToInteger
684+
NaturalShow -> pure NaturalShow
685+
NaturalPlus a b -> NaturalPlus <$> loop a <*> loop b
686+
NaturalTimes a b -> NaturalTimes <$> loop a <*> loop b
687+
Integer -> pure Integer
688+
IntegerLit a -> pure (IntegerLit a)
689+
IntegerShow -> pure IntegerShow
690+
IntegerToDouble -> pure IntegerToDouble
691+
Double -> pure Double
692+
DoubleLit a -> pure (DoubleLit a)
693+
DoubleShow -> pure DoubleShow
694+
Text -> pure Text
695+
TextLit (Chunks a b) -> fmap TextLit (Chunks <$> mapM (mapM loop) a <*> pure b)
696+
TextAppend a b -> TextAppend <$> loop a <*> loop b
697+
List -> pure List
698+
ListLit a b -> ListLit <$> mapM loop a <*> mapM loop b
699+
ListAppend a b -> ListAppend <$> loop a <*> loop b
700+
ListBuild -> pure ListBuild
701+
ListFold -> pure ListFold
702+
ListLength -> pure ListLength
703+
ListHead -> pure ListHead
704+
ListLast -> pure ListLast
705+
ListIndexed -> pure ListIndexed
706+
ListReverse -> pure ListReverse
707+
Optional -> pure Optional
708+
OptionalLit a b -> OptionalLit <$> loop a <*> mapM loop b
709+
OptionalFold -> pure OptionalFold
710+
OptionalBuild -> pure OptionalBuild
711+
Record a -> Record <$> mapM loop a
712+
RecordLit a -> RecordLit <$> mapM loop a
713+
Union a -> Union <$> mapM loop a
714+
UnionLit a b c -> UnionLit <$> pure a <*> loop b <*> mapM loop c
715+
Combine a b -> Combine <$> loop a <*> loop b
716+
CombineTypes a b -> CombineTypes <$> loop a <*> loop b
717+
Prefer a b -> Prefer <$> loop a <*> loop b
718+
Merge a b c -> Merge <$> loop a <*> loop b <*> mapM loop c
719+
Constructors a -> Constructors <$> loop a
720+
Field a b -> Field <$> loop a <*> pure b
721+
Project a b -> Project <$> loop a <*> pure b
722+
Note a b -> Note <$> pure a <*> loop b
606723
where
607-
process import_ = loadStaticWith from_import ctx n (Embed import_)
724+
loop = loadStaticWith from_import ctx n
725+
608726

609727
-- | Resolve all imports within an expression
610728
load :: Expr Src Import -> IO (Expr Src X)

src/Dhall/Lint.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,10 @@ lint expression = loop (Dhall.Core.denote expression)
213213
a' = loop a
214214
loop (Note a _) =
215215
absurd a
216+
loop (ImportAlt a b) =
217+
ImportAlt a' b'
218+
where
219+
a' = loop a
220+
b' = loop b
216221
loop (Embed a) =
217222
Embed a

src/Dhall/Parser/Expression.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ nonEmptyOptional embedded = do
157157
return (OptionalLit b (pure a))
158158

159159
operatorExpression :: Parser a -> Parser (Expr Src a)
160-
operatorExpression = orExpression
160+
operatorExpression = importAltExpression
161161

162162
makeOperatorExpression
163163
:: (Parser a -> Parser (Expr Src a))
@@ -171,6 +171,10 @@ makeOperatorExpression subExpression operatorParser operator embedded =
171171
b <- many (do operatorParser; subExpression embedded)
172172
return (foldr1 operator (a:b)) )
173173

174+
importAltExpression :: Parser a -> Parser (Expr Src a)
175+
importAltExpression =
176+
makeOperatorExpression orExpression _importAlt ImportAlt
177+
174178
orExpression :: Parser a -> Parser (Expr Src a)
175179
orExpression =
176180
makeOperatorExpression plusExpression _or BoolOr
@@ -760,8 +764,13 @@ http = do
760764
(importHashed_ <|> (_openParens *> importHashed_ <* _closeParens)) )
761765
return (URL prefix path suffix headers)
762766

767+
missing :: Parser ImportType
768+
missing = do
769+
_missing
770+
return Missing
771+
763772
importType_ :: Parser ImportType
764-
importType_ = choice [ local, http, env ]
773+
importType_ = choice [ local, http, env, missing ]
765774

766775
importHashed_ :: Parser ImportHashed
767776
importHashed_ = do

0 commit comments

Comments
 (0)