Skip to content

Commit 4e4183c

Browse files
authored
cover more import cases (#49)
* Fix imports inside scope of other imports. * Named input imports, imports under imports, handle cycles
1 parent dc38311 commit 4e4183c

File tree

5 files changed

+193
-18
lines changed

5 files changed

+193
-18
lines changed

src/Eucalypt/Core/Import.hs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Control.Monad.Trans
1313
import Data.Foldable (toList)
1414
import qualified Data.Graph as G
1515
import qualified Data.Map as M
16-
import Data.Maybe (fromJust)
16+
import Data.Maybe (fromMaybe)
1717
import Eucalypt.Core.Metadata
1818
import Eucalypt.Core.Syn
1919
import Eucalypt.Core.Unit
@@ -44,7 +44,8 @@ processImports ::
4444
processImports load expr@(CoreMeta m body) =
4545
case importsFromMetadata m of
4646
Just imports ->
47-
CoreMeta (pruneImports m) $ foldr (\i e -> rebody (load i) e) body imports
47+
CoreMeta (pruneImports m) $
48+
foldr (\i e -> rebody (load i) e) (processImports load body) imports
4849
Nothing -> expr
4950
processImports load (CoreLet bs b) = CoreLet bs' b'
5051
where
@@ -80,15 +81,25 @@ processUnit load u@TranslationUnit {truCore = body} =
8081

8182

8283

83-
-- | Process all imports in topologically sorted order
84-
applyAllImports :: M.Map Input TranslationUnit -> M.Map Input TranslationUnit
85-
applyAllImports unitMap = foldl processInput unitMap sortedInputs
84+
-- | Process all imports in topologically sorted order unless there
85+
-- are cycles, in which case return the inputs involved in the cycles
86+
-- (in Left)
87+
applyAllImports ::
88+
M.Map Input TranslationUnit -> Either [Input] (M.Map Input TranslationUnit)
89+
applyAllImports unitMap =
90+
if (not . null) cyclicInputs
91+
then Left cyclicInputs
92+
else Right $ foldl processInput unitMap sortedInputs
8693
where
8794
(graph, getVertex) = G.graphFromEdges' $ map edgeSpec $ M.assocs unitMap
8895
edgeSpec (k, v) = (k, k, toList $ truImports v)
89-
sortedInputs = map (toInput . getVertex) $ (reverse . G.topSort) graph
90-
toInput (i, _, _) = i
91-
toLoadFn m k = truCore $ fromJust (M.lookup k m)
92-
processInput ::
93-
M.Map Input TranslationUnit -> Input -> M.Map Input TranslationUnit
96+
sortedInputs = map toInput $ (reverse . G.topSort) graph
97+
toInput v =
98+
case getVertex v of
99+
(i, _, _) -> i
100+
toLoadFn m k =
101+
truCore $ fromMaybe (error $ "no such key: " ++ show k) (M.lookup k m)
94102
processInput m input = M.update (return . processUnit (toLoadFn m)) input m
103+
cyclicInputs =
104+
(map toInput . mconcat . filter isCycle . map toList . G.scc) graph
105+
isCycle cc = length cc > 1 || (minimum cc, minimum cc) `elem` G.edges graph

src/Eucalypt/Driver/Error.hs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
{-|
2+
Module : Eucalypt.Core.Error
3+
Description : Errors detected during driver stages
4+
Copyright : (c) Greg Hawkins, 2018
5+
License :
6+
Maintainer : greg@curvelogic.co.uk
7+
Stability : experimental
8+
-}
19
module Eucalypt.Driver.Error where
210

3-
import Data.Typeable
11+
import Control.Exception.Safe
412
import Eucalypt.Syntax.Input (Input)
513

6-
newtype CommandError = InvalidInput Input
14+
data CommandError
15+
= InvalidInput Input -- ^ invalid input (bad format etc.)
16+
| CyclicInputs [Input] -- ^ input imports form one or more cycles
717
deriving (Show, Typeable)
18+
19+
instance Exception CommandError

src/Eucalypt/Driver/Evaluator.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module Eucalypt.Driver.Evaluator
1111
where
1212

1313
import Control.Applicative ((<|>))
14-
import Control.Exception.Safe (try)
14+
import Control.Exception.Safe (throwM, try)
1515
import Control.Monad (forM_, unless, when)
1616
import Control.Monad.Loops (iterateUntilM)
1717
import Data.Bifunctor
@@ -262,8 +262,10 @@ formEvaluand opts targets source =
262262
parseInputsAndImports :: [Input] -> IO [TranslationUnit]
263263
parseInputsAndImports inputs = do
264264
unitMap <- parseAllUnits inputs
265-
let processedUnitMap = applyAllImports unitMap
266-
return $ mapMaybe (`M.lookup` processedUnitMap) inputs
265+
case applyAllImports unitMap of
266+
Right processedUnitMap ->
267+
return $ mapMaybe (`M.lookup` processedUnitMap) inputs
268+
Left cyclicInputs -> throwM $ CyclicInputs cyclicInputs
267269

268270

269271

test/Eucalypt/Core/ImportSpec.hs

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Eucalypt.Core.ImportSpec
33
, spec
44
) where
55

6+
import Data.Either (fromLeft, fromRight)
67
import qualified Data.Map as M
78
import Data.Maybe (fromJust)
89
import qualified Data.Set as S
@@ -103,16 +104,145 @@ unitsABC :: M.Map Input TranslationUnit
103104
unitsABC =
104105
M.fromList [(unitAInput, unitA), (unitBInput, unitB), (unitCInput, unitC)]
105106

107+
namedInput :: Input
108+
namedInput = fromJust $ parseInputFromString "namedInput"
109+
110+
namedUnit :: TranslationUnit
111+
namedUnit = applyName "name"
112+
TranslationUnit
113+
{ truCore = unitACore
114+
, truImports = mempty
115+
, truTargets = mempty
116+
}
117+
118+
unitDInput :: Input
119+
unitDInput = fromJust $ parseInputFromString "unitD"
120+
121+
unitDCore :: CoreExpr
122+
unitDCore =
123+
letexp
124+
[ ( "nest"
125+
, withMeta (block [element "import" $ str "namedInput"]) $
126+
block [element "name" $ var "name"])
127+
] $
128+
block [element "quux" $ var "quux"]
129+
130+
unitDCoreResult :: CoreExpr
131+
unitDCoreResult =
132+
letexp
133+
[ ( "nest"
134+
, withMeta (block []) $
135+
letexp
136+
[ ( "name"
137+
, letexp [("foo", sym "foo"), ("bar", sym "bar")] $
138+
block [element "foo" $ var "foo", element "bar" $ var "bar"])
139+
] $
140+
block [element "name" $ var "name"])
141+
] $
142+
block [element "quux" $ var "quux"]
143+
144+
unitD :: TranslationUnit
145+
unitD =
146+
TranslationUnit
147+
{ truCore = unitDCore
148+
, truImports = S.fromList [namedInput]
149+
, truTargets = mempty
150+
}
151+
152+
unitsNamedAndD :: M.Map Input TranslationUnit
153+
unitsNamedAndD =
154+
M.fromList [(namedInput, namedUnit), (unitDInput, unitD)]
155+
156+
157+
importUnderImportInput :: Input
158+
importUnderImportInput = fromJust $ parseInputFromString "importUnderImport"
159+
160+
importUnderImportCore :: CoreExpr
161+
importUnderImportCore =
162+
withMeta (block [element "import" $ str "unitA"]) $
163+
letexp
164+
[ ( "nest"
165+
, withMeta (block [element "import" $ str "unitA"]) $
166+
block [element "foo" $ var "foo"])
167+
] $
168+
block [element "foo" $ var "foo"]
169+
170+
importUnderImportCoreResult :: CoreExpr
171+
importUnderImportCoreResult =
172+
withMeta (block []) $
173+
letexp [("foo", sym "foo"), ("bar", sym "bar")] $
174+
letexp
175+
[ ( "nest"
176+
, withMeta (block []) $
177+
letexp [("foo", sym "foo"), ("bar", sym "bar")] $
178+
block [element "foo" $ var "foo"])
179+
] $
180+
block [element "foo" $ var "foo"]
181+
182+
183+
importUnderImport :: TranslationUnit
184+
importUnderImport =
185+
TranslationUnit
186+
{ truCore = importUnderImportCore
187+
, truImports = S.fromList [unitAInput]
188+
, truTargets = mempty
189+
}
190+
191+
unitsImportUnderImportAndA :: M.Map Input TranslationUnit
192+
unitsImportUnderImportAndA =
193+
M.fromList [(unitAInput, unitA), (importUnderImportInput, importUnderImport)]
194+
195+
circularImportInput :: Input
196+
circularImportInput = fromJust $ parseInputFromString "circularImport"
197+
198+
circularImportCore :: CoreExpr
199+
circularImportCore =
200+
letexp
201+
[ ( "z"
202+
, withMeta (block [element "import" $ str "circularImport"]) $
203+
block [element "foo" $ var "foo"])
204+
] $
205+
block [element "z" $ var "z"]
206+
207+
circularImport :: TranslationUnit
208+
circularImport =
209+
TranslationUnit
210+
{ truCore = circularImportCore
211+
, truImports = S.fromList [circularImportInput]
212+
, truTargets = mempty
213+
}
214+
215+
unitsCircularImport :: M.Map Input TranslationUnit
216+
unitsCircularImport =
217+
M.fromList [(circularImportInput, circularImport)]
218+
219+
importAll :: M.Map Input TranslationUnit -> M.Map Input TranslationUnit
220+
importAll = fromRight mempty . applyAllImports
221+
106222
spec :: Spec
107223
spec =
108224
describe "Import processing" $ do
109225
context "single imports" $ do
110226
it "processes a single import" $
111227
processImports (const unitACore) unitBCore `shouldBe` unitBCoreResult
112228
it "processes single import from unit map" $
113-
truCore <$> M.lookup unitBInput (applyAllImports unitsAB) `shouldBe` Just unitBCoreResult
229+
truCore <$>
230+
M.lookup unitBInput (importAll unitsAB) `shouldBe` Just unitBCoreResult
231+
it "processes single named import from unit map" $
232+
truCore <$>
233+
M.lookup unitDInput (importAll unitsNamedAndD) `shouldBe`
234+
Just unitDCoreResult
235+
it "processes imports under imports" $
236+
truCore <$>
237+
M.lookup importUnderImportInput (importAll unitsImportUnderImportAndA) `shouldBe`
238+
Just importUnderImportCoreResult
239+
it "handles circular imports gracefully" $
240+
fromLeft [] (applyAllImports unitsCircularImport) `shouldBe`
241+
[circularImportInput]
114242
context "transitive imports" $ do
115243
it "intermediates are correct" $
116-
truCore <$> M.lookup unitBInput (applyAllImports unitsABC) `shouldBe` Just unitBCoreResult
244+
truCore <$>
245+
M.lookup unitBInput (importAll unitsABC) `shouldBe` Just unitBCoreResult
117246
it "end result is correct" $
118-
truCore <$> M.lookup unitCInput (applyAllImports unitsABC) `shouldBe` Just unitCCoreResult
247+
truCore <$>
248+
M.lookup unitCInput (importAll unitsABC) `shouldBe` Just unitCCoreResult

test/Eucalypt/Core/MetadataSpec.hs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Eucalypt.Core.MetadataSpec
2+
( main
3+
, spec
4+
) where
5+
6+
import Eucalypt.Core.Metadata
7+
import Eucalypt.Core.Syn
8+
import Eucalypt.Syntax.Input
9+
import Test.Hspec
10+
11+
main :: IO ()
12+
main = hspec spec
13+
14+
spec :: Spec
15+
spec =
16+
describe "import recognition" $
17+
it "recognises imports in { import: \"a=blah\" }" $
18+
importsFromMetadata (block [element "import" $ str "a=blah"] :: CoreExpr) `shouldBe`
19+
pure <$>
20+
parseInputFromString "a=blah"

0 commit comments

Comments
 (0)