Skip to content

Implement basic dependency solving #3

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

Merged
merged 4 commits into from
Feb 3, 2025
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
1 change: 1 addition & 0 deletions gren.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"CLI.Parser",
"CLI.PrettyPrinter",
"Compiler.Backend",
"Compiler.Dependencies",
"Compiler.ModuleName",
"Compiler.Outline",
"Compiler.PackageName",
Expand Down
86 changes: 86 additions & 0 deletions src/Compiler/Dependencies.gren
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module Compiler.Dependencies exposing
( SimplifiedOutline
, Solution(..)
, solve
)


import Compiler.Outline as Outline exposing (Outline)
import Compiler.PackageName as PackageName exposing (PackageName)
import Dict exposing (Dict)
import SemanticVersion exposing (SemanticVersion)
import SemanticVersionRange exposing (SemanticVersionRange)


type alias SimplifiedOutline =
{ name : PackageName
, version : SemanticVersionRange
, dependencies : Dict String SemanticVersionRange
}


type Solution
= Complete
| Missing { name : PackageName, version : SemanticVersion }
| Conflict { name : PackageName, version1 : SemanticVersionRange, version2 : SemanticVersionRange }


solve : Array { name : PackageName, version : SemanticVersionRange } -> Dict String SimplifiedOutline -> Solution
solve rootRequirements loadedOutlines =
solveHelp rootRequirements Dict.empty loadedOutlines


solveHelp
: Array { name : PackageName, version : SemanticVersionRange }
-> Dict String SimplifiedOutline
-> Dict String SimplifiedOutline
-> Solution
solveHelp pending solved loaded =
when Array.popFirst pending is
Nothing ->
Complete

Just { first = { name = packageName, version = packageVersion }, rest } ->
let
packageNameStr =
PackageName.toString packageName
in
when Dict.get packageNameStr solved is
Just outline ->
when SemanticVersionRange.intersect outline.version packageVersion is
Nothing ->
Conflict
{ name = outline.name
, version1 = outline.version
, version2 = packageVersion
}

Just intersectedVersion ->
solveHelp
rest
(Dict.set packageNameStr { outline | version = intersectedVersion } solved)
loaded

Nothing ->
when Dict.get packageNameStr loaded is
Nothing ->
Missing
{ name = packageName
, version = SemanticVersionRange.lowerBound packageVersion
}

Just outline ->
let
newPending =
outline.dependencies
|> Dict.foldl
(\name version acc ->
Array.pushLast
{ name = PackageName.fromString name |> Maybe.withDefault PackageName.example
, version = version
}
acc
)
rest
in
solveHelp newPending (Dict.set packageNameStr outline solved) loaded
8 changes: 8 additions & 0 deletions src/Compiler/PackageName.gren
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Compiler.PackageName exposing
( PackageName
, example
--
, author
, name
Expand All @@ -24,6 +25,13 @@ import Json.Encode as Json
import Json.Decode as Decode exposing (Decoder)


{-| An example package name. Useful for tests.
-}
example : PackageName
example =
PackageName { author = "example", name = "package" }


{-| A package is identified by a string in the following format:

author/name
Expand Down
16 changes: 16 additions & 0 deletions src/SemanticVersion.gren
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module SemanticVersion exposing
( SemanticVersion
, compare
, fromString
, cliParser
, jsonDecoder
Expand Down Expand Up @@ -35,6 +36,21 @@ type alias SemanticVersion =
}


compare : SemanticVersion -> SemanticVersion -> Order
compare left right =
when Basics.compare left.major right.major is
EQ ->
when Basics.compare left.minor right.minor is
EQ ->
Basics.compare left.patch right.patch

otherMinor ->
otherMinor

otherMajor ->
otherMajor


{-| Convert a `String` into a [SemanticVersion](#SemanticVersion).
-}
fromString : String -> Maybe SemanticVersion
Expand Down
74 changes: 74 additions & 0 deletions src/SemanticVersionRange.gren
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
module SemanticVersionRange exposing
( SemanticVersionRange
, of
, example
, lowerBound
, upperBound
, intersect
, fromString
, jsonDecoder
, toString
Expand All @@ -18,6 +21,23 @@ type SemanticVersionRange
= SemanticVersionRange { lower : SemanticVersion, upper : SemanticVersion }


of : SemanticVersion -> SemanticVersion -> Maybe SemanticVersionRange
of lower upper =
if SemanticVersion.compare lower upper /= GT then
Just (SemanticVersionRange { lower = lower, upper = upper })

else
Nothing


example : SemanticVersionRange
example =
SemanticVersionRange
{ lower = { major = 1, minor = 0, patch = 0 }
, upper = { major = 2, minor = 0, patch = 0 }
}


lowerBound : SemanticVersionRange -> SemanticVersion
lowerBound (SemanticVersionRange { lower }) =
lower
Expand All @@ -28,6 +48,60 @@ upperBound (SemanticVersionRange { upper }) =
upper


intersect : SemanticVersionRange -> SemanticVersionRange -> Maybe SemanticVersionRange
intersect left right =
let
(SemanticVersionRange { lower = lowerLeft, upper = upperLeft }) =
left

(SemanticVersionRange { lower = lowerRight, upper = upperRight }) =
right
in
when SemanticVersion.compare upperLeft upperRight is
LT ->
if SemanticVersion.compare upperLeft lowerRight /= GT then
Nothing

else
Just <|
when SemanticVersion.compare lowerLeft lowerRight is
LT ->
SemanticVersionRange { lower = lowerRight, upper = upperLeft }

EQ ->
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }

GT ->
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }

EQ ->
Just <|
when SemanticVersion.compare lowerLeft lowerRight is
LT ->
SemanticVersionRange { lower = lowerRight, upper = upperLeft }

EQ ->
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }

GT ->
SemanticVersionRange { lower = lowerLeft, upper = upperLeft }

GT ->
if SemanticVersion.compare upperRight lowerLeft /= GT then
Nothing
else
Just <|
when SemanticVersion.compare lowerLeft lowerRight is
LT ->
SemanticVersionRange { lower = lowerLeft, upper = upperRight }

EQ ->
SemanticVersionRange { lower = lowerLeft, upper = upperRight }

GT ->
SemanticVersionRange { lower = lowerRight, upper = upperRight }


fromString : String -> Maybe SemanticVersionRange
fromString str =
when str |> String.keepIf (\char -> char /= ' ') |> String.split "<=v<" is
Expand Down
2 changes: 2 additions & 0 deletions tests/src/Main.gren
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Test.CLI.Parser as CLIParser
import Test.CLI.PrettyPrinter as PrettyPrinter
import Test.SemanticVersion as SemanticVersion
import Test.SemanticVersionRange as SemanticVersionRange
import Test.Compiler.Dependencies as Dependencies
import Test.Compiler.PackageName as PackageName
import Test.Compiler.ModuleName as ModuleName
import Test.String.EditDistance as EditDistance
Expand All @@ -17,6 +18,7 @@ main =
TestRunner.run <|
Test.describe "Gren Compiler Node tests"
[ CLIParser.tests
, Dependencies.tests
, PrettyPrinter.tests
, SemanticVersion.tests
, SemanticVersionRange.tests
Expand Down
129 changes: 129 additions & 0 deletions tests/src/Test/Compiler/Dependencies.gren
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
module Test.Compiler.Dependencies exposing (tests)

import Expect exposing (Expectation)
import Test exposing (Test, describe, test)
import Compiler.Dependencies as Deps
import Compiler.PackageName as PackageName exposing (PackageName)
import Dict exposing (Dict)
import SemanticVersion exposing (SemanticVersion)
import SemanticVersionRange exposing (SemanticVersionRange)


tests : Test
tests =
describe "Compiler.Dependencies"
[ describe "solve"
[ test "returns empty array for a project without dependencies" <| \{} ->
Deps.solve [] Dict.empty
|> Expect.equal Deps.Complete
, test "If root defines a dependency that isn't provided with an outline, that package is returned" <| \{} ->
let
rootDeps =
[ { name = packageName "my/first", version = versionRange 1 1 0 }
]
in
Deps.solve rootDeps Dict.empty
|> Expect.equal (missing "my/first" 1 1 0)
, test "Finds missing package" <| \{} ->
let
rootDeps =
[ { name = packageName "my/first", version = versionRange 1 1 0 }
, { name = packageName "my/second", version = versionRange 2 0 0 }
, { name = packageName "your/first", version = versionRange 1 0 0 }
]

loaded =
Dict.empty
|> insertDep "my/first" 1 1 0 []
|> insertDep "your/first" 1 0 0 []
in
Deps.solve rootDeps loaded
|> Expect.equal (missing "my/second" 2 0 0)
, test "Finds missing transitive package" <| \{} ->
let
rootDeps =
[ { name = packageName "my/first", version = versionRange 1 1 0 }
]

loaded =
Dict.empty
|> insertDep "my/first" 1 1 0
[ { name = packageName "your/first", version = versionRange 1 5 0 }]
in
Deps.solve rootDeps loaded
|> Expect.equal (missing "your/first" 1 5 0)
, test "Reports unsolvable conflicts" <| \{} ->
let
rootDeps =
[ { name = packageName "my/first", version = versionRange 1 1 0 }
, { name = packageName "your/first", version = versionRange 2 0 0 }
]

loaded =
Dict.empty
|> insertDep "my/first" 1 1 0
[ { name = packageName "your/first", version = versionRange 1 5 0 }]
|> insertDep "your/first" 2 0 0 []
in
Deps.solve rootDeps loaded
|> Expect.equal (conflict "your/first" (versionRange 2 0 0) (versionRange 1 5 0))
]
]


missing : String -> Int -> Int -> Int -> Deps.Solution
missing name major minor patch =
Deps.Missing
{ name = packageName name
, version = { major = major, minor = minor, patch = patch }
}


conflict : String -> SemanticVersionRange -> SemanticVersionRange -> Deps.Solution
conflict name versionOne versionTwo =
Deps.Conflict
{ name = packageName name
, version1 = versionOne
, version2 = versionTwo
}


packageName : String -> PackageName
packageName str =
PackageName.fromString str
|> Maybe.withDefault PackageName.example


versionRange : Int -> Int -> Int -> SemanticVersionRange
versionRange major minor patch =
let
upperBound =
{ major = major + 1
, minor = 0
, patch = 0
}
in
SemanticVersionRange.of { major = major, minor = minor, patch = patch } upperBound
|> Maybe.withDefault SemanticVersionRange.example


insertDep
: String
-> Int
-> Int
-> Int
-> Array { name : PackageName, version : SemanticVersionRange }
-> Dict String Deps.SimplifiedOutline
-> Dict String Deps.SimplifiedOutline
insertDep name major minor patch deps dict =
Dict.set
name
{ name = packageName name
, version = versionRange major minor patch
, dependencies =
Array.foldl
(\dep -> Dict.set (PackageName.toString dep.name) dep.version)
Dict.empty
deps
}
dict
Loading