Skip to content

Commit c463197

Browse files
authored
Merge pull request #3 from gren-lang/push-nmyutrptlqvy
Implement basic dependency solving
2 parents 8d234d8 + fce94be commit c463197

9 files changed

+387
-1
lines changed

gren.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"CLI.Parser",
1010
"CLI.PrettyPrinter",
1111
"Compiler.Backend",
12+
"Compiler.Dependencies",
1213
"Compiler.ModuleName",
1314
"Compiler.Outline",
1415
"Compiler.PackageName",

src/Compiler/Dependencies.gren

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
module Compiler.Dependencies exposing
2+
( SimplifiedOutline
3+
, Solution(..)
4+
, solve
5+
)
6+
7+
8+
import Compiler.Outline as Outline exposing (Outline)
9+
import Compiler.PackageName as PackageName exposing (PackageName)
10+
import Dict exposing (Dict)
11+
import SemanticVersion exposing (SemanticVersion)
12+
import SemanticVersionRange exposing (SemanticVersionRange)
13+
14+
15+
type alias SimplifiedOutline =
16+
{ name : PackageName
17+
, version : SemanticVersionRange
18+
, dependencies : Dict String SemanticVersionRange
19+
}
20+
21+
22+
type Solution
23+
= Complete
24+
| Missing { name : PackageName, version : SemanticVersion }
25+
| Conflict { name : PackageName, version1 : SemanticVersionRange, version2 : SemanticVersionRange }
26+
27+
28+
solve : Array { name : PackageName, version : SemanticVersionRange } -> Dict String SimplifiedOutline -> Solution
29+
solve rootRequirements loadedOutlines =
30+
solveHelp rootRequirements Dict.empty loadedOutlines
31+
32+
33+
solveHelp
34+
: Array { name : PackageName, version : SemanticVersionRange }
35+
-> Dict String SimplifiedOutline
36+
-> Dict String SimplifiedOutline
37+
-> Solution
38+
solveHelp pending solved loaded =
39+
when Array.popFirst pending is
40+
Nothing ->
41+
Complete
42+
43+
Just { first = { name = packageName, version = packageVersion }, rest } ->
44+
let
45+
packageNameStr =
46+
PackageName.toString packageName
47+
in
48+
when Dict.get packageNameStr solved is
49+
Just outline ->
50+
when SemanticVersionRange.intersect outline.version packageVersion is
51+
Nothing ->
52+
Conflict
53+
{ name = outline.name
54+
, version1 = outline.version
55+
, version2 = packageVersion
56+
}
57+
58+
Just intersectedVersion ->
59+
solveHelp
60+
rest
61+
(Dict.set packageNameStr { outline | version = intersectedVersion } solved)
62+
loaded
63+
64+
Nothing ->
65+
when Dict.get packageNameStr loaded is
66+
Nothing ->
67+
Missing
68+
{ name = packageName
69+
, version = SemanticVersionRange.lowerBound packageVersion
70+
}
71+
72+
Just outline ->
73+
let
74+
newPending =
75+
outline.dependencies
76+
|> Dict.foldl
77+
(\name version acc ->
78+
Array.pushLast
79+
{ name = PackageName.fromString name |> Maybe.withDefault PackageName.example
80+
, version = version
81+
}
82+
acc
83+
)
84+
rest
85+
in
86+
solveHelp newPending (Dict.set packageNameStr outline solved) loaded

src/Compiler/PackageName.gren

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Compiler.PackageName exposing
22
( PackageName
3+
, example
34
--
45
, author
56
, name
@@ -24,6 +25,13 @@ import Json.Encode as Json
2425
import Json.Decode as Decode exposing (Decoder)
2526

2627

28+
{-| An example package name. Useful for tests.
29+
-}
30+
example : PackageName
31+
example =
32+
PackageName { author = "example", name = "package" }
33+
34+
2735
{-| A package is identified by a string in the following format:
2836

2937
author/name

src/SemanticVersion.gren

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module SemanticVersion exposing
22
( SemanticVersion
3+
, compare
34
, fromString
45
, cliParser
56
, jsonDecoder
@@ -35,6 +36,21 @@ type alias SemanticVersion =
3536
}
3637

3738

39+
compare : SemanticVersion -> SemanticVersion -> Order
40+
compare left right =
41+
when Basics.compare left.major right.major is
42+
EQ ->
43+
when Basics.compare left.minor right.minor is
44+
EQ ->
45+
Basics.compare left.patch right.patch
46+
47+
otherMinor ->
48+
otherMinor
49+
50+
otherMajor ->
51+
otherMajor
52+
53+
3854
{-| Convert a `String` into a [SemanticVersion](#SemanticVersion).
3955
-}
4056
fromString : String -> Maybe SemanticVersion

src/SemanticVersionRange.gren

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
module SemanticVersionRange exposing
22
( SemanticVersionRange
3+
, of
4+
, example
35
, lowerBound
46
, upperBound
7+
, intersect
58
, fromString
69
, jsonDecoder
710
, toString
@@ -18,6 +21,23 @@ type SemanticVersionRange
1821
= SemanticVersionRange { lower : SemanticVersion, upper : SemanticVersion }
1922

2023

24+
of : SemanticVersion -> SemanticVersion -> Maybe SemanticVersionRange
25+
of lower upper =
26+
if SemanticVersion.compare lower upper /= GT then
27+
Just (SemanticVersionRange { lower = lower, upper = upper })
28+
29+
else
30+
Nothing
31+
32+
33+
example : SemanticVersionRange
34+
example =
35+
SemanticVersionRange
36+
{ lower = { major = 1, minor = 0, patch = 0 }
37+
, upper = { major = 2, minor = 0, patch = 0 }
38+
}
39+
40+
2141
lowerBound : SemanticVersionRange -> SemanticVersion
2242
lowerBound (SemanticVersionRange { lower }) =
2343
lower
@@ -28,6 +48,60 @@ upperBound (SemanticVersionRange { upper }) =
2848
upper
2949

3050

51+
intersect : SemanticVersionRange -> SemanticVersionRange -> Maybe SemanticVersionRange
52+
intersect left right =
53+
let
54+
(SemanticVersionRange { lower = lowerLeft, upper = upperLeft }) =
55+
left
56+
57+
(SemanticVersionRange { lower = lowerRight, upper = upperRight }) =
58+
right
59+
in
60+
when SemanticVersion.compare upperLeft upperRight is
61+
LT ->
62+
if SemanticVersion.compare upperLeft lowerRight /= GT then
63+
Nothing
64+
65+
else
66+
Just <|
67+
when SemanticVersion.compare lowerLeft lowerRight is
68+
LT ->
69+
SemanticVersionRange { lower = lowerRight, upper = upperLeft }
70+
71+
EQ ->
72+
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }
73+
74+
GT ->
75+
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }
76+
77+
EQ ->
78+
Just <|
79+
when SemanticVersion.compare lowerLeft lowerRight is
80+
LT ->
81+
SemanticVersionRange { lower = lowerRight, upper = upperLeft }
82+
83+
EQ ->
84+
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }
85+
86+
GT ->
87+
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }
88+
89+
GT ->
90+
if SemanticVersion.compare upperRight lowerLeft /= GT then
91+
Nothing
92+
else
93+
Just <|
94+
when SemanticVersion.compare lowerLeft lowerRight is
95+
LT ->
96+
SemanticVersionRange { lower = lowerLeft, upper = upperRight }
97+
98+
EQ ->
99+
SemanticVersionRange { lower = lowerLeft, upper = upperRight }
100+
101+
GT ->
102+
SemanticVersionRange { lower = lowerRight, upper = upperRight }
103+
104+
31105
fromString : String -> Maybe SemanticVersionRange
32106
fromString str =
33107
when str |> String.keepIf (\char -> char /= ' ') |> String.split "<=v<" is

tests/src/Main.gren

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Test.CLI.Parser as CLIParser
66
import Test.CLI.PrettyPrinter as PrettyPrinter
77
import Test.SemanticVersion as SemanticVersion
88
import Test.SemanticVersionRange as SemanticVersionRange
9+
import Test.Compiler.Dependencies as Dependencies
910
import Test.Compiler.PackageName as PackageName
1011
import Test.Compiler.ModuleName as ModuleName
1112
import Test.String.EditDistance as EditDistance
@@ -17,6 +18,7 @@ main =
1718
TestRunner.run <|
1819
Test.describe "Gren Compiler Node tests"
1920
[ CLIParser.tests
21+
, Dependencies.tests
2022
, PrettyPrinter.tests
2123
, SemanticVersion.tests
2224
, SemanticVersionRange.tests
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
module Test.Compiler.Dependencies exposing (tests)
2+
3+
import Expect exposing (Expectation)
4+
import Test exposing (Test, describe, test)
5+
import Compiler.Dependencies as Deps
6+
import Compiler.PackageName as PackageName exposing (PackageName)
7+
import Dict exposing (Dict)
8+
import SemanticVersion exposing (SemanticVersion)
9+
import SemanticVersionRange exposing (SemanticVersionRange)
10+
11+
12+
tests : Test
13+
tests =
14+
describe "Compiler.Dependencies"
15+
[ describe "solve"
16+
[ test "returns empty array for a project without dependencies" <| \{} ->
17+
Deps.solve [] Dict.empty
18+
|> Expect.equal Deps.Complete
19+
, test "If root defines a dependency that isn't provided with an outline, that package is returned" <| \{} ->
20+
let
21+
rootDeps =
22+
[ { name = packageName "my/first", version = versionRange 1 1 0 }
23+
]
24+
in
25+
Deps.solve rootDeps Dict.empty
26+
|> Expect.equal (missing "my/first" 1 1 0)
27+
, test "Finds missing package" <| \{} ->
28+
let
29+
rootDeps =
30+
[ { name = packageName "my/first", version = versionRange 1 1 0 }
31+
, { name = packageName "my/second", version = versionRange 2 0 0 }
32+
, { name = packageName "your/first", version = versionRange 1 0 0 }
33+
]
34+
35+
loaded =
36+
Dict.empty
37+
|> insertDep "my/first" 1 1 0 []
38+
|> insertDep "your/first" 1 0 0 []
39+
in
40+
Deps.solve rootDeps loaded
41+
|> Expect.equal (missing "my/second" 2 0 0)
42+
, test "Finds missing transitive package" <| \{} ->
43+
let
44+
rootDeps =
45+
[ { name = packageName "my/first", version = versionRange 1 1 0 }
46+
]
47+
48+
loaded =
49+
Dict.empty
50+
|> insertDep "my/first" 1 1 0
51+
[ { name = packageName "your/first", version = versionRange 1 5 0 }]
52+
in
53+
Deps.solve rootDeps loaded
54+
|> Expect.equal (missing "your/first" 1 5 0)
55+
, test "Reports unsolvable conflicts" <| \{} ->
56+
let
57+
rootDeps =
58+
[ { name = packageName "my/first", version = versionRange 1 1 0 }
59+
, { name = packageName "your/first", version = versionRange 2 0 0 }
60+
]
61+
62+
loaded =
63+
Dict.empty
64+
|> insertDep "my/first" 1 1 0
65+
[ { name = packageName "your/first", version = versionRange 1 5 0 }]
66+
|> insertDep "your/first" 2 0 0 []
67+
in
68+
Deps.solve rootDeps loaded
69+
|> Expect.equal (conflict "your/first" (versionRange 2 0 0) (versionRange 1 5 0))
70+
]
71+
]
72+
73+
74+
missing : String -> Int -> Int -> Int -> Deps.Solution
75+
missing name major minor patch =
76+
Deps.Missing
77+
{ name = packageName name
78+
, version = { major = major, minor = minor, patch = patch }
79+
}
80+
81+
82+
conflict : String -> SemanticVersionRange -> SemanticVersionRange -> Deps.Solution
83+
conflict name versionOne versionTwo =
84+
Deps.Conflict
85+
{ name = packageName name
86+
, version1 = versionOne
87+
, version2 = versionTwo
88+
}
89+
90+
91+
packageName : String -> PackageName
92+
packageName str =
93+
PackageName.fromString str
94+
|> Maybe.withDefault PackageName.example
95+
96+
97+
versionRange : Int -> Int -> Int -> SemanticVersionRange
98+
versionRange major minor patch =
99+
let
100+
upperBound =
101+
{ major = major + 1
102+
, minor = 0
103+
, patch = 0
104+
}
105+
in
106+
SemanticVersionRange.of { major = major, minor = minor, patch = patch } upperBound
107+
|> Maybe.withDefault SemanticVersionRange.example
108+
109+
110+
insertDep
111+
: String
112+
-> Int
113+
-> Int
114+
-> Int
115+
-> Array { name : PackageName, version : SemanticVersionRange }
116+
-> Dict String Deps.SimplifiedOutline
117+
-> Dict String Deps.SimplifiedOutline
118+
insertDep name major minor patch deps dict =
119+
Dict.set
120+
name
121+
{ name = packageName name
122+
, version = versionRange major minor patch
123+
, dependencies =
124+
Array.foldl
125+
(\dep -> Dict.set (PackageName.toString dep.name) dep.version)
126+
Dict.empty
127+
deps
128+
}
129+
dict

0 commit comments

Comments
 (0)