Enable spago install
to work on advanced Dhall expressions
#849
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description of the change
Fixes #813. This is the same work as #815 but with a very simplified commit history (largely to see how much is being added when tests are not included) that has been updated to work on
master
. Unlike #815, this also includes an explanation for this PR.Checklist:
README
Overview of PR
Currently,
spago install
works by finding the firstdependencies
key in the Dhall expression. If the value stored at the key is an empty list (i.e.[] : List Text
) or a list literal (i.e.[ "effect", "prelude"]
), spago adds the dependencies to the list and succeeds. Spago will crash when one uses a list append (i.e.config.dependencies # someOtherList
) or any expression more complicated than that.The current
spago install
implementation takes a very simplistic approach to installing dependencies because of how complex it can be to correctly update the raw Dhall expression's source code (i.e. this PR) without doing something wrong. Enablingspago install
to work (and perhaps in the futurespago uninstall
) requires dealing with more complex possible Dhall expressions.The present PR enables
spago install
to work on these more advanced features by taking the below approach:spago.dhall
file before editing it (i.e. resolve imports likehttps://github.com/purescript/package-sets/releases/download/psc-0.14.1-20210419/packages.dhall
)difference requestedPkgs currentPkgs
)spago.dhall
file with the new Dhall expressionPossible Concerns
Obviously, there's no way to guarantee that this command will succeed 100% of the time, but I don't think that's a major concern due to widespread usage of source control. There could be a bug in this implementation that causes it to fail. The verification step above will guarantee that the
spago.dhall
file isn't edited unless it's a valid change. However, if this check ever produces a false positive, then one's invalidspago.dhall
file could cause futurespago
commands to fail. Moreover, the user would need to revert these changes done by the CLI. Since most if not all users are using source control, this isn't a huge concern to me. In 80% of the cases, this command should work. In the 20% that it doesn't succeed or produces an invalid file that breaks future runs, source control can revert the changes and the user can edit the file manually.One potential issue is if
spago install
is run with theoffline
flag (see #841). If the Internet cannot be accessed, then we can no longer do Step 1, which means there isn't a way to do Step 4. Rather, we'd have to assume that the package exists in the config folder and that the user is ok with creating aspago.dhall
file that may not work. Since most people are using source control, I don't personally see this as an issue because discarding these changes is easy.Another issue with this PR is the maintenance burden it imposes. This PR adds 1.8KLOC to this codebase, most of which are test files, verifying that the implementation is correct. The heart of this PR is the
Spago/Config/AST.hs
file (680 LOC, a lot of which is documentation). If we ever need to change the config format, at the very least, many of these test files would need to be updated. One possible way to implement this feature without imposing its corresponding maintenance burden is to port the updating code to its own library and havespago
depend on it. I'm not sure how that would affect the test files (i.e. which would stay and which would be removed), but it relieves the burden of having to be directly responsible for any issues that may arise.Implementation Details
There are a few things that make this implementation tricky. A raw Dhall expression...
Dealing with Unresolved Imports
Regarding the first... If we are trying to update something in the expression and we hit this case, the implementation doesn't know what the value represented by that import is. In that case, it must make a "best guess" effort. For example, the
spago.dhall
file always has at least one unresolved import when it references thepackages.dhall
file. Another example is taking an existingspago.dhall
file and adding or overwriting some part of it.let config = ./spago.dhall in config with sources = config.sources # [ "example/**/*.purs" ]
However, our "best guess" isn't entirely uninformed. When the original raw Dhall expression is fully normalized (i.e. Step 1), we can verify that the expression produces a valid Config value that has a
dependencies
key whose contents are a list of text (i.e. packages). As long as we traverse through the expression correctly, we know what changes need to be done if we hit such a case.For example, each of the following expressions below (where some parts of the
spago.dhall
file has been omitted for brevity) has an unresolved import. The commented out code indicate what the correct update would be.These updates are safe because the full normalization (Step 1) has already told us that the expression eventually normalizes to a valid Spago schema. If we cannot do Step 1 (e.g.
offline
flag is enabled), we can still attempt to make these updates, but all risk is on the user at that point.Updating let bindings correctly
Regarding the second... This is the issue I ran into here. Let's assume we have the following Dhall expression:
Ideally, running
spago install new
would update the above expression torather than
It's more idiomatic in style. However, this can (but doesn't necessarily) introduce a side-effect. Let's change the initial Dhall expression to the following (this might be incorrect Dhall, but it gets the point across):
Applying the same "stylistic" logic above to this new expression, we unintentionally modify the
name
field. If we had used the "unsylistic" logic, no side-effect would have occurred.This might not seem like an issue, but consider the idea of targets. In #681, targets were originally proposed using let bindings, so that one target's dependencies and source globs could be easily depended upon by another target:
If we run
spago install "new"
on themain
target, we want to update themain
binding'sdependencies
field, so that the change propagates to thetest
target's dependencies. In other words, we want to use a "stylistic" logic update.However, this is not as ambiguous as it may appear. So long as we verify that the only change made to the expression was what we intended to do, the "stylistic" logic update is valid. If it fails, we could fallback to the "unsylistic" logic. This PR's current state always uses the "unstylistic" logic. It should be updated to use this "fallback" idea.
Traversing the Expression
Updating a raw Dhall expression entails traversing through it correctly and then making an change in the correct location, so that future parses produces the intended value. We can think of the expression as a Zipper. As long as we know the "shape" of the Zipper, we can traverse through it to the correct location, apply our change, and then recurse back to the root, rebuilding the surrounding structure along the way.
The
spago.dhall
file format is just an eventual Record expression that eventually refers to a "dependencies" field that eventually refers to a list of text values. By "eventual"/"eventually", I mean there may be other expressions in-between one part (e.g. the root expression) and the next part (e.g. the "dependencies" field).Fortunately, the traversal logic is pretty straight forward.
["dependencies"]
.{ config = ./spago.dhall }.config
, we add the new field to the stack as we have to "go down" that key before we can "go down" the key currently at the top of the key stack.Finally, for some other context, see this summary I wrote for the Contributors working group call