Skip to content

Commit 4692410

Browse files
committed
feat: improve errors & clarify behavior
1 parent 2fa17c9 commit 4692410

File tree

2 files changed

+251
-102
lines changed

2 files changed

+251
-102
lines changed

src/lake/Lake/Util/Version.lean

Lines changed: 138 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ open System Lean
2121

2222
namespace Lake
2323

24+
/-! ## Parser Utils -/
25+
2426
/--
2527
Parses version components separated by a `.` from the head of the string.
2628
@@ -68,9 +70,53 @@ private def isWildVer (s : String.Slice) : Bool :=
6870

6971
@[inline] private def parseVerNat (what : String) (s : String.Slice) : EStateM String σ Nat := do
7072
let some v := s.toNat?
71-
| throw s!"expected numeral {what} version, got '{s.copy}'"
73+
| throw s!"invalid {what} version: expected numeral, got '{s.copy}'"
7274
return v
7375

76+
inductive VerComponent
77+
| none | wild | nat (n : Nat)
78+
79+
private def parseVerComponent {σ} (what : String) (s? : Option String.Slice) : EStateM String σ VerComponent := do
80+
let some s := s?
81+
| return .none
82+
if isWildVer s then
83+
return .wild
84+
let some n := s.toNat?
85+
| throw s!"invalid {what} version: expected numeral or wildcard, got '{s.copy}'"
86+
return .nat n
87+
88+
def parseSpecialDescr (s : String) : EStateM String s.ValidPos String := do
89+
let p ← get
90+
if h : p = s.endValidPos then
91+
return ""
92+
else
93+
let c := p.get h
94+
if c == '-' then
95+
let p := p.next h
96+
let p' := nextUntilWhitespace p
97+
set p'
98+
let specialDescr := p.extract p'
99+
if specialDescr.isEmpty then
100+
throw "invalid version: '-' suffix cannot be empty"
101+
return specialDescr
102+
else
103+
return ""
104+
where
105+
nextUntilWhitespace p :=
106+
if h : p = s.endValidPos then
107+
p
108+
else if (p.get h).isWhitespace then
109+
p
110+
else
111+
nextUntilWhitespace (p.next h)
112+
termination_by s.utf8ByteSize - p.offset.byteIdx
113+
decreasing_by
114+
rw [p.byteIdx_offset_next]
115+
have := (p.get h).utf8Size_pos
116+
have := p.byteIdx_lt_utf8ByteSize h
117+
omega
118+
119+
74120
private def runVerParse
75121
(s : String) (x : (s : String) → EStateM String s.ValidPos α)
76122
(startPos := s.startValidPos) (endPos := s.endValidPos)
@@ -84,19 +130,23 @@ private def runVerParse
84130
throw s!"unexpected characters at end of version: {tail}"
85131
| .error e _ => throw e
86132

133+
/-! ## SemVerCore -/
134+
87135
/-- The major-minor-patch triple of a [SemVer](https://semver.org/). -/
88136
public structure SemVerCore where
89137
major : Nat := 0
90138
minor : Nat := 0
91139
patch : Nat := 0
92140
deriving Inhabited, Repr, DecidableEq, Ord
93141

142+
namespace SemVerCore
143+
94144
public instance : LT SemVerCore := ltOfOrd
95145
public instance : LE SemVerCore := leOfOrd
96146
public instance : Min SemVerCore := minOfLe
97147
public instance : Max SemVerCore := maxOfLe
98148

99-
def SemVerCore.parseM (s : String) : EStateM String s.ValidPos SemVerCore := do
149+
def parseM (s : String) : EStateM String s.ValidPos SemVerCore := do
100150
try
101151
let cs ← parseVerComponents s
102152
if h : cs.size = 3 then
@@ -109,16 +159,20 @@ def SemVerCore.parseM (s : String) : EStateM String s.ValidPos SemVerCore := do
109159
catch e =>
110160
throw s!"invalid version core: {e}"
111161

112-
@[inline] public def SemVerCore.parse (s : String) : Except String SemVerCore := do
162+
@[inline] public def parse (s : String) : Except String SemVerCore := do
113163
runVerParse s parseM
114164

115-
public protected def SemVerCore.toString (ver : SemVerCore) : String :=
165+
public protected def toString (ver : SemVerCore) : String :=
116166
s!"{ver.major}.{ver.minor}.{ver.patch}"
117167

118168
public instance : ToString SemVerCore := ⟨SemVerCore.toString⟩
119169
public instance : ToJson SemVerCore := ⟨(·.toString)⟩
120170
public instance : FromJson SemVerCore := ⟨(do SemVerCore.parse <| ← fromJson? ·)⟩
121171

172+
end SemVerCore
173+
174+
/-! ## StdVer -/
175+
122176
/--
123177
A Lean-style semantic version.
124178
A major-minor-patch triple with an optional arbitrary `-` suffix.
@@ -130,14 +184,16 @@ public structure StdVer extends SemVerCore where
130184
/-- A Lean version. -/
131185
public abbrev LeanVer := StdVer
132186

187+
namespace StdVer
188+
133189
public instance : Coe StdVer SemVerCore := ⟨StdVer.toSemVerCore⟩
134190

135-
@[inline] public def StdVer.ofSemVerCore (ver : SemVerCore) : StdVer :=
191+
@[inline] public def ofSemVerCore (ver : SemVerCore) : StdVer :=
136192
{toSemVerCore := ver, specialDescr := ""}
137193

138194
public instance : Coe SemVerCore StdVer := ⟨StdVer.ofSemVerCore⟩
139195

140-
public protected def StdVer.compare (a b : StdVer) : Ordering :=
196+
public protected def compare (a b : StdVer) : Ordering :=
141197
match compare a.toSemVerCore b.toSemVerCore with
142198
| .eq =>
143199
match a.specialDescr, b.specialDescr with
@@ -154,42 +210,15 @@ public instance : LE StdVer := leOfOrd
154210
public instance : Min StdVer := minOfLe
155211
public instance : Max StdVer := maxOfLe
156212

157-
public def StdVer.parseM (s : String) : EStateM String s.ValidPos StdVer := do
213+
public def parseM (s : String) : EStateM String s.ValidPos StdVer := do
158214
let core ← SemVerCore.parseM s
159-
let p ← get
160-
if h : p = s.endValidPos then
161-
return core
162-
else
163-
let c := p.get h
164-
if c == '-' then
165-
let p := p.next h
166-
let p' := nextUntilWhitespace p
167-
set p'
168-
let specialDescr := p.extract p'
169-
if specialDescr.isEmpty then
170-
throw "invalid version: '-' suffix cannot be empty"
171-
return {toSemVerCore := core, specialDescr}
172-
else
173-
return core
174-
where
175-
nextUntilWhitespace p :=
176-
if h : p = s.endValidPos then
177-
p
178-
else if (p.get h).isWhitespace then
179-
p
180-
else
181-
nextUntilWhitespace (p.next h)
182-
termination_by s.utf8ByteSize - p.offset.byteIdx
183-
decreasing_by
184-
rw [p.byteIdx_offset_next]
185-
have := (p.get h).utf8Size_pos
186-
have := p.byteIdx_lt_utf8ByteSize h
187-
omega
215+
let specialDescr ← parseSpecialDescr s
216+
return {toSemVerCore := core, specialDescr}
188217

189-
@[inline] public def StdVer.parse (s : String) : Except String StdVer := do
218+
@[inline] public def parse (s : String) : Except String StdVer := do
190219
runVerParse s parseM
191220

192-
public protected def StdVer.toString (ver : StdVer) : String :=
221+
public protected def toString (ver : StdVer) : String :=
193222
if ver.specialDescr.isEmpty then
194223
ver.toSemVerCore.toString
195224
else
@@ -199,6 +228,10 @@ public instance : ToString StdVer := ⟨StdVer.toString⟩
199228
public instance : ToJson StdVer := ⟨(·.toString)⟩
200229
public instance : FromJson StdVer := ⟨(do StdVer.parse <| ← fromJson? ·)⟩
201230

231+
end StdVer
232+
233+
/-! ## ToolchainVer -/
234+
202235
/-- The `elan` toolchain file name (i.e., `lean-toolchain`). -/
203236
public def toolchainFileName : FilePath := "lean-toolchain"
204237

@@ -301,6 +334,8 @@ end ToolchainVer
301334
public def normalizeToolchain (s : String) : String :=
302335
ToolchainVer.ofString s |>.toString
303336

337+
/-! ## DecodeVersion -/
338+
304339
/-- Parses a version from a string. -/
305340
public class DecodeVersion (α : Type u) where
306341
decodeVersion : String → Except String α
@@ -311,9 +346,11 @@ public instance : DecodeVersion SemVerCore := ⟨SemVerCore.parse⟩
311346
@[default_instance] public instance : DecodeVersion StdVer := ⟨StdVer.parse⟩
312347
public instance : DecodeVersion ToolchainVer := ⟨(pure <| ToolchainVer.ofString ·)⟩
313348

349+
/-! ## VerRange -/
350+
314351
public inductive Comparator
315352
| lt | le | gt | ge | eq | ne
316-
deriving Repr
353+
deriving Repr, Inhabited
317354

318355
namespace Comparator
319356

@@ -366,6 +403,12 @@ public structure VerComparator where
366403

367404
namespace VerComparator
368405

406+
/-- A version comparator that matches any version (i.e., `≥0.0.0`). -/
407+
def any : VerComparator :=
408+
{op := .ge, ver := .ofSemVerCore {}}
409+
410+
instance : Inhabited VerComparator := ⟨.any⟩
411+
369412
def parseM (s : String) : EStateM String s.ValidPos VerComparator := do
370413
let op ← Comparator.parseM s
371414
let ver ← StdVer.parseM s
@@ -375,13 +418,28 @@ def parseM (s : String) : EStateM String s.ValidPos VerComparator := do
375418
runVerParse s parseM
376419

377420
public def test (self : VerComparator) (ver : StdVer) : Bool :=
378-
match self.op with
379-
| .lt => ver < self.ver
380-
| .le => ver ≤ self.ver
381-
| .gt => ver > self.ver
382-
| .ge => ver ≥ self.ver
383-
| .eq => ver = self.ver
384-
| .ne => ver ≠ self.ver
421+
match self.ver.specialDescr, ver.specialDescr with
422+
| _, "" =>
423+
match self.op with
424+
| .lt => ver < self.ver
425+
| .le => ver ≤ self.ver
426+
| .gt => ver > self.ver
427+
| .ge => ver ≥ self.ver
428+
| .eq => ver = self.ver
429+
| .ne => ver ≠ self.ver
430+
| "", _ =>
431+
false
432+
| selfDescr, verDescr =>
433+
if self.ver.toSemVerCore = ver.toSemVerCore then
434+
match self.op with
435+
| .lt => verDescr < selfDescr
436+
| .le => verDescr ≤ selfDescr
437+
| .gt => verDescr > selfDescr
438+
| .ge => verDescr ≥ selfDescr
439+
| .eq => verDescr = selfDescr
440+
| .ne => verDescr ≠ selfDescr
441+
else
442+
false
385443

386444
public protected def toString (self : VerComparator) : String :=
387445
s!"{self.op}{self.ver}"
@@ -394,7 +452,7 @@ public structure VerRange where
394452
private innerMk ::
395453
toString : String
396454
clauses : Array (Array VerComparator)
397-
deriving Repr
455+
deriving Repr, Inhabited
398456

399457
namespace VerRange
400458

@@ -433,6 +491,10 @@ where
433491
| .ok ands p =>
434492
go false ors ands p
435493
| .error e p => .error e p
494+
else if c == '^' then
495+
.error "caret ranges are unsupported; \
496+
if a specific major version is desired, use a tilde or wildcard range; \
497+
otherwise, use '≥' instead" p
436498
else if c == '~' then
437499
match parseTilde s ands (p.next h) with
438500
| .ok ands p =>
@@ -461,57 +523,50 @@ where
461523
| .ok cmp p =>
462524
go false ors (ands.push cmp) p
463525
| .error e p => .error e p
464-
@[inline] appendRange ands minVer maxVer :=
465-
let minVer := StdVer.ofSemVerCore minVer
526+
@[inline] appendRange ands minVer maxVer (specialDescr := "") :=
527+
let minVer := StdVer.mk minVer specialDescr
466528
let maxVer := StdVer.ofSemVerCore maxVer
467529
ands.push {op := .ge, ver := minVer} |>.push {op := .lt, ver := maxVer}
468-
parseVerNat? {σ} what (s? : Option String.Slice) : EStateM String σ (Option Nat) := do
469-
let some s := s?
470-
| return none
471-
if isWildVer s then
472-
return none
473-
let some n := s.toNat?
474-
| throw s!"expected numeral or wildcard {what} version, got '{s.copy}'"
475-
return some n
476-
parseWild s ands : EStateM String s.ValidPos _ := do
530+
parseWild (s : String) ands : EStateM String s.ValidPos _ := do
477531
let cs ← parseVerComponents s
478-
if cs.size = 0 ∨ cs.size > 3 then
479-
throw s!"invalid wildcard version: incorrect number of components: got {cs.size}, expected 1-3"
532+
if (← get).get?.any (· == '-') then
533+
throw s!"invalid wildcard range: wildcard versions do not support suffixes"
534+
else if cs.size = 0 ∨ cs.size > 3 then
535+
throw s!"invalid wildcard range: incorrect number of components: got {cs.size}, expected 1-3"
480536
else
481-
let major? ← parseVerNat? "major" cs[0]?
482-
let minor? ← parseVerNat? "minor" cs[1]?
483-
let patch? ← parseVerNat? "patch" cs[2]?
484-
if let some major := major? then
485-
if let some minor := minor? then
486-
if let some patch := patch? then
487-
-- TODO: decide on an approach for plain versions
488-
return ands.push {op := .eq, ver := .ofSemVerCore {major, minor, patch}}
489-
else
490-
return appendRange ands {major, minor} {major, minor := minor + 1}
491-
else if patch?.isSome then
492-
throw s!"invalid patch version: components after a wildcard must be wildcards"
493-
else
494-
return appendRange ands {major} {major := major + 1}
495-
else if minor?.isSome then
496-
throw "invalid minor version: components after a wildcard must be wildcards"
497-
else if patch?.isSome then
537+
let major? ← parseVerComponent "major" cs[0]?
538+
let minor? ← parseVerComponent "minor" cs[1]?
539+
let patch? ← parseVerComponent "patch" cs[2]?
540+
match major?, minor?, patch? with
541+
| .nat major, .nat minor, .wild =>
542+
return appendRange ands {major, minor} {major, minor := minor + 1}
543+
| .wild, _, .nat _ | _, .wild, .nat _ =>
498544
throw "invalid patch version: components after a wildcard must be wildcards"
499-
else
500-
return ands.push {op := .ge, ver := .ofSemVerCore {}}
501-
parseTilde s ands : EStateM String s.ValidPos _ := do
545+
| .wild, .nat _, _ =>
546+
throw "invalid minor version: components after a wildcard must be wildcards"
547+
| .nat major, .wild, _ =>
548+
return appendRange ands {major} {major := major + 1}
549+
| .nat _, _, _ =>
550+
throw "invalid version range: bare versions are not supported; \
551+
if you want to fix a specific version, use '=' before the full version; \
552+
otherwise, use '≥' to support it and future versions"
553+
| _, _, _ =>
554+
return ands.push .any
555+
parseTilde (s : String) ands : EStateM String s.ValidPos _ := do
502556
let cs ← parseVerComponents s
557+
let specialDescr ← parseSpecialDescr s
503558
if h : cs.size = 1 then
504559
let major ← parseVerNat "major" cs[0]
505-
return appendRange ands {major} {major := major + 1}
560+
return appendRange ands {major} {major := major + 1} specialDescr
506561
else if h : cs.size = 2 then
507562
let major ← parseVerNat "major" cs[0]
508563
let minor ← parseVerNat "minor" cs[1]
509-
return appendRange ands {major, minor} {major, minor := minor + 1}
564+
return appendRange ands {major, minor} {major, minor := minor + 1} specialDescr
510565
else if h : cs.size = 3 then
511566
let major ← parseVerNat "major" cs[0]
512567
let minor ← parseVerNat "minor" cs[1]
513568
let patch ← parseVerNat "patch" cs[2]
514-
return appendRange ands {major, minor, patch} {major, minor := minor + 1}
569+
return appendRange ands {major, minor, patch} {major, minor, patch := patch + 1} specialDescr
515570
else
516571
throw s!"invalid tilde range: incorrect number of components: got {cs.size}, expected 1-3"
517572

0 commit comments

Comments
 (0)