src/pkg/path.bend
ezx@1.6.0.0 · 206 lines · 0 definitions
1# pkg/path: the path arithmetic the import walk needs, lexical only. Base has
2# no readdir and no realpath, and a module's name in a package is the path it
3# was reached through, so `.` and `..` are resolved by text rather than by
4# asking the file system.
5import Base
6
7# where a normalisation has got to: `abs` is true for a path that began at the
8# root, `out` the components kept so far, reversed
9type Walk is Data:
10 Walk{abs: Bool, out: List<&2, String>}
11
12# what one component of a path turned out to be
13type Comp is Data:
14 CSkip{}
15 CUp{}
16 CName{s: String}
17
18# a component classified. `+c` because it is both asked about and kept.
19def comp_of(+part: String) -> Comp:
20 Bool.pick(Comp, Bool.or(String.is_empty(part), String.eq(part, ".")), CSkip{},
21 Bool.pick(Comp, String.eq(part, ".."), CUp{}, CName{part}))
22
23# a climb with nothing left to climb off: at the root it goes nowhere, and in a
24# relative path it is kept, since the package may still be re-rooted under it
25def up.root(abs: Bool) -> List<&2, String>:
26 match abs:
27 case True{}:
28 []
29 case False{}:
30 [".."]
31
32# a climb past a component that is itself a climb keeps both
33def up.back(same: Bool, head: String, rest: List<&2, String>) -> List<&2, String>:
34 match same:
35 case True{}:
36 ".." <> (head <> rest)
37 case False{}:
38 rest
39
40# a climb against what has been kept: it cancels the last real component
41def up.go(+head: String, rest: List<&2, String>) -> List<&2, String>:
42 up.back(String.eq(head, ".."), head, rest)
43
44# a climb against what has been kept
45def up(abs: Bool, out: List<&2, String>) -> List<&2, String>:
46 match out:
47 case []:
48 up.root(abs)
49 case h <> t:
50 up.go(h, t)
51
52# a climb, with the root flag carried through
53def up.step(+abs: Bool, out: List<&2, String>) -> Walk:
54 Walk{abs, up(abs, out)}
55
56# the state once a component has been classified
57def norm.go(st: Walk, comp: Comp) -> Walk:
58 Walk{abs, out} = st
59 match comp:
60 case CSkip{}:
61 Walk{abs, out}
62 case CUp{}:
63 up.step(abs, out)
64 case CName{s}:
65 Walk{abs, s <> out}
66
67# one component against the state
68def norm.step(st: Walk, part: String) -> Walk:
69 norm.go(st, comp_of(part))
70
71# a relative path with nothing left in it is the current directory
72def norm.rel(+body: String) -> String:
73 Bool.pick(String, String.is_empty(body), ".", body)
74
75# the components put back together, under the root or not
76def norm.text(abs: Bool, body: String) -> String:
77 match abs:
78 case True{}:
79 "/" ++ body
80 case False{}:
81 norm.rel(body)
82
83# the components a normalisation kept, in order
84def norm.fin(st: Walk) -> String:
85 Walk{abs, out} = st
86 norm.text(abs, String.join(List.reverse(&2, String, out), "/"))
87
88# the walk one component at a time. Base's `List.foldl` applies an erased
89# function parameter, which Bend's termination check cannot see through, so
90# every def reaching it is reported as relying on unsafe code; a walk written
91# out is structural and costs nothing.
92def norm.walk(cs: List<&2, String>, st: Walk) -> Walk:
93 match cs:
94 case []:
95 st
96 case h <> t:
97 norm.walk(t, norm.step(st, h))
98
99# a path with `.` and an inner `..` resolved by text. A leading `..` survives,
100# because a module reached through one re-roots the package rather than
101# escaping it.
102def norm(+path: String) -> String:
103 norm.fin(norm.walk(String.split(path, '/'), Walk{String.starts_with(path, "/"), []}))
104
105# the component carried so far, kept when nothing follows it. The list is the
106# first argument because that is the one the walk shrinks.
107def base.go(ps: List<&2, String>, +last: String) -> String:
108 match ps:
109 case []:
110 last
111 case +h2 <> t:
112 base.go(t, h2)
113
114# the last component of a path
115def base.last(ps: List<&2, String>) -> String:
116 match ps:
117 case []:
118 ""
119 case +h <> t:
120 base.go(t, h)
121
122# the last component of a path
123def base(path: String) -> String:
124 base.last(String.split(path, '/'))
125
126# the path with its last component climbed back off
127def dir(+path: String) -> String:
128 norm(path ++ "/..")
129
130# the path before it is normalised: the second alone when there is no
131# directory to take it relative to, the two spliced otherwise. Split out so
132# that `norm` runs once. Under a `Bool.pick` both arms were built, so every
133# join paid two full component walks and one concatenation it threw away.
134def join.at(bare: Bool, left: String, right: String) -> String:
135 match bare:
136 case True{}:
137 right
138 case False{}:
139 left ++ "/" ++ right
140
141# a path taken relative to a directory. An empty directory means the path is
142# already where it should be, and never that it starts at the root.
143def join(+left: String, +right: String) -> String:
144 norm(join.at(String.is_empty(left), left, right))
145
146# the last `n` components of a path
147def tail(path: String, count: Nat) -> String:
148 String.join(List.reverse(&2, String,
149 List.take(&2, String, List.reverse(&2, String, String.split(path, '/')), count)), "/")
150
151# whether a component is a climb
152def up.is(part: String) -> Bool:
153 String.eq(part, "..")
154
155# the climbs among components counted one at a time. Base's `List.filter`
156# applies an erased function parameter, so it falls outside the termination
157# check; this count is structural.
158def ups.go(ps: List<&2, String>) -> Nat:
159 match ps:
160 case []:
161 0n
162 case +h <> t:
163 Nat.add(Bool.pick(Nat, up.is(h), 1n, 0n), ups.go(t))
164
165# how many components of a path are climbs
166def ups(path: String) -> Nat:
167 ups.go(String.split(path, '/'))
168
169# a module path without its `.bend`, which is how bend names the module
170def unbend(+path: String) -> String:
171 Bool.pick(String, String.ends_with(path, ".bend"),
172 String.take(path, Nat.sub(String.length(path), 5n)), path)
173
174# the path with `n` of its components climbed back off, which is where a
175# package re-roots when a module of it was reached through that many `..`
176def climb(count: Nat, path: String) -> String:
177 match count:
178 case 0n:
179 norm(path)
180 case 1n+q:
181 climb(q, dir(path))
182
183# an absolute path, or a `git@` URL, as it is, or a relative path taken from
184# `here`
185def anchor.path(kept: Bool, +here: String, +path: String) -> String:
186 match kept:
187 case True{}:
188 path
189 case False{}:
190 join(here, path)
191
192# a URL as it is, or a path
193def anchor.at(url: Bool, +here: String, +path: String) -> String:
194 match url:
195 case True{}:
196 path
197 case False{}:
198 anchor.path(Bool.or(String.starts_with(path, "/"),
199 String.starts_with(path, "git@")), here, path)
200
201# a source as the directory `here` reads it. The ledger keeps a local path
202# relative to the project, the way it was given, and git reads a relative path
203# from somewhere else, so every git command is handed this instead. A URL is
204# the same wherever it is read from.
205def anchor(+here: String, +path: String) -> String:
206 anchor.at(String.contains(path, "://"), here, path)