Skip to content
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

lib.path.hasPrefix: init #237610

Merged
merged 2 commits into from
Jun 21, 2023
Merged

lib.path.hasPrefix: init #237610

merged 2 commits into from
Jun 21, 2023

Conversation

infinisil
Copy link
Member

@infinisil infinisil commented Jun 13, 2023

Description of changes

Adds a function to lib.path:

  • lib.path.hasPrefix :: Path -> Path -> Bool: Whether the first path is a prefix of the second path:
    lib.path.hasPrefix ./. ./lib
    -> true

This is split off from #210423, relating to other work in the path library effort.

This work is sponsored by Antithesis

Things done
  • Wrote docs
  • Wrote tests

With removePrefix introduced in a future commit this law can then be
used to derive

        removePrefix p (append p s) == subpath.normalise s
        => (wrap with append)
        append p (removePrefix p (append p s)) == append p (subpath.normalise s)
        => (append is not influenced by subpath normalisation)
        append p (removePrefix p (append p s)) == append p s
        => (substitute q = append p s)
        append p (removePrefix p q) == q

Not included in the docs because it's not that important, just shows
that the first statement is more general than the second one (because
this derivation doesn't work the other way)
@infinisil infinisil requested a review from roberth June 13, 2023 18:43
@infinisil infinisil requested a review from edolstra as a code owner June 13, 2023 18:43
@ofborg ofborg bot added 10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin 10.rebuild-linux: 1-10 labels Jun 13, 2023
Copy link
Contributor

@fricklerhandwerk fricklerhandwerk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@mweinelt mweinelt added the 12.approvals: 1 This PR was reviewed and approved by one reputable person label Jun 14, 2023

- Equivalent to whether some subpath exists that can be appended to the first path to get the second path:

hasPrefix p q <-> exists s . append p s == q
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hasPrefix p q <-> exists s . append p s == q
hasPrefix p q
=
s. (append p s == q)

A good definition of ∃ is easy to find with a search engine nowadays, whereas exists is imaginary syntax that's impossible to find.

I think it's ok to use = in meta syntax.
I had to look up that <-> is iff. Maybe I'm dumb, because I did take math courses in uni... Anyway, the search result for <-> isn't as good as ∃, for what it's worth, but most of all, we don't need this operator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or double equal sign like the preceding law in the diff

Suggested change
hasPrefix p q <-> exists s . append p s == q
hasPrefix p q
==
s. (append p s == q)

I think the implementation could perhaps be optimized for the true case, because then you don't need to find the other root, or allocate path component lists, but you'd have to measure it. Something for a later PR maybe.

So just the suggestion (either one) and then lgtm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather remove the extra newlines to be consistent with the rest of this file, sounds good otherwise!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what people think, so unscientific twitter poll.

Copy link
Member Author

@infinisil infinisil Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also do this to avoid the $\exists$:

Equivalent to whether some subpath s exists that can be appended to the first path to get the second path:

hasPrefix p q == (append p s == q)

But also, the two =='s do look a bit weird, I think something like <-> would be better, but yeah it's not that well known, I don't mind either way

How about <=> or $\iff$?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<=> is what I would expect.

Copy link
Member Author

@infinisil infinisil Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just having this, no additional formula:

hasPrefix p q is only true if q == append p s for some subpath s.

(latest commit uses this now)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's best, overall.

Though personally any of the notation above is fine too. I've finally stopped overriding \iff from <=> to <-> in LaTeX 😂

@infinisil infinisil force-pushed the lib.path.hasPrefix branch from dd30838 to 78b24ab Compare June 15, 2023 16:30
Comment on lines 177 to 178
Whether the second path is a prefix of the first path, or equal to it.
Throws an error if the paths don't share the same filesystem root.
Copy link
Member Author

@infinisil infinisil Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Whether the second path is a prefix of the first path, or equal to it.
Throws an error if the paths don't share the same filesystem root.
Whether the first path is a prefix of the second path.

It was the wrong way around, and the filesystem root thing really doesn't need to be mentioned, since you can't even produce paths that would cause that error in current Nix.

(applied in latest commit)

@infinisil infinisil force-pushed the lib.path.hasPrefix branch from 78b24ab to 0042d58 Compare June 15, 2023 17:51
@@ -149,6 +173,51 @@ in /* No rec! Add dependencies on this file at the top. */ {
${subpathInvalidReason subpath}'';
path + ("/" + subpath);

/*
Whether the first path is a prefix of the second path.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also something like

Suggested change
Whether the first path is a prefix of the second path.
Whether the first path is a component-wise prefix of the second path.

To make it clear that hasPrefix /foo/b /foo/bar == false.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, applied!

@infinisil infinisil force-pushed the lib.path.hasPrefix branch from 0042d58 to 592213a Compare June 15, 2023 20:29
Comment on lines +213 to +217
assert assertMsg
(path1Deconstructed.root == path2Deconstructed.root) ''
lib.path.hasPrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
Copy link
Member Author

@infinisil infinisil Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not cause the function to fail, it should instead just return false (Edit: I don't think so anymore, see #237610 (comment))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all a bit messed up, because the reason this check is here is because we decided to limit the number of assumptions the path library makes to a minimum, see the design document. Notably we don't assume that there's only one filesystem root, meaning that theoretically paths could have no common ancestor. The reason for this is to ensure compatibility with how NixOS/nix#6530 implements lazy paths, introducing a way to have multiple filesystem roots.

But it's kind of ridiculous, Nixpkgs shouldn't have to ensure forwards compatibility with a potential future Nix version. We can't even test this code here right now without going through hoops!

On the other hand, having this check here ensures that we won't have any buggy behavior in case the above PR gets merged like that, and it doesn't cause any problems if the PR isn't merged either, so it's harmless and just a safety measure.

So because of that I'd say: Let's continue without the assumption of singular filesystem roots and have this check here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding throwing vs not throwing. I think it should definitely throw for now because:

  • We can always remove throws afterwards, it's backwards compatible
  • The notion of different filesystem roots is very unexplored, we aren't sure what the semantics should be like yet
  • I can imagine however that in most cases, this would indicate an user error, and it would be better to notify the user instead of "failing" silently
  • Code that needs to ensure it doesn't throw can still check whether the two paths have the same filesystem root beforehand

So in conclusion, no change is necessary here imo.

And I did test this code path at least locally:

❯ nix run github:edolstra/nix/lazy-trees -- repl -f lib
Welcome to Nix 2.17.0pre20230602_3c4d678. Type :? for help.

Loading installable ''...
Added 411 variables.
nix-repl> p = (builtins.fetchTree { type = "git"; url = ./.; }).outPath

nix-repl> path.hasPrefix p ./.                                         
error: lib.path.hasPrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
    first argument: "/home/tweagysil/antithesis/nixpkgs/" with root "/home/tweagysil/antithesis/nixpkgs/"
    second argument: "/home/tweagysil/antithesis/nixpkgs" with root "/"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We can always remove throws afterwards, it's backwards compatible

We'd break code that relies on this function for input validation. Just because it works for valid inputs doesn't mean that changing it won't lead to hard to troubleshoot problems for others. Basically your third point, but with a time / change aspect added.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, though I'd argue that backwards compatibility conventionally only means "If it worked before it should work in the future", a one-way implication, so something not working now doesn't mean it will continue not working in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's kind of ridiculous, Nixpkgs shouldn't have to ensure forwards compatibility with a potential future Nix version. We can't even test this code here right now without going through hoops!

I kind of agree, but like you, I care too much about the future of lib.

The hoops are packaging that branch and using it in lib/release.nix. However, that would be irresponsible because of how broken it is. While I am confident that this particular behavior is good, users might adapt their programming to behaviors that are bad. Paths are already confusing to users, so we should discourage all adaptation to path related behaviors that will not be released.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users won't run into this error anyways right now though, the PR isn't merged yet, or might not even get merged with this behavior ever.

And if it were to get merged, I still think throwing an error here is better than returning false for the other reasons at least. Whether we want to change this behavior in the future and whether we should consider that backwards incompatible should be left for then, I don't think it makes sense to discuss this much here.

Do you agree that the code is fine as is?

@infinisil infinisil mentioned this pull request Jun 15, 2023
2 tasks
@figsoda figsoda removed the 12.approvals: 1 This PR was reviewed and approved by one reputable person label Jun 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
10.rebuild-darwin: 0 This PR does not cause any packages to rebuild on Darwin 10.rebuild-linux: 1-10
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants