Skip to content

Commit aea4fb8

Browse files
committed
feat: verified sanity check for CLI
1 parent d9d34eb commit aea4fb8

File tree

4 files changed

+156
-43
lines changed

4 files changed

+156
-43
lines changed

src/lake/Lake/CLI/Help.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,5 @@ public def help : (cmd : String) → String
566566
| "lean" => helpLean
567567
| "translate-config" => helpTranslateConfig
568568
| _ => usage
569+
570+
@[simp] public theorem help_empty : help "" = usage := by rfl

src/lake/Lake/CLI/Main.lean

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,17 @@ export LakeOptions (mkLoadConfig mkBuildConfig)
142142

143143
abbrev CliMainM := ExceptT CliError MainM
144144
abbrev CliStateM := StateT LakeOptions CliMainM
145-
abbrev CliM := StateT ArgList CliStateM
145+
abbrev CliM := ArgsT CliStateM
146146

147-
def CliM.run (self : CliM α) (args : List String) : BaseIO ExitCode := do
147+
@[inline] def CliMainM.run (self : CliMainM α) : BaseIO ExitCode := do
148+
MainM.run <| (ExceptT.run self) >>= fun | .ok a => pure a | .error e => error e.toString
149+
150+
@[inline] def CliStateM.run (self : CliStateM α) (args : List String) : BaseIO ExitCode := do
148151
let (elanInstall?, leanInstall?, lakeInstall?) ← findInstall?
149-
let main := self.run' args |>.run' {args, elanInstall?, leanInstall?, lakeInstall?}
150-
let main := main.run >>= fun | .ok a => pure a | .error e => error e.toString
151-
main.run
152+
StateT.run' self {args, elanInstall?, leanInstall?, lakeInstall?} |>.run
153+
154+
@[inline] def CliM.run (self : CliM α) (args : List String) : BaseIO ExitCode := do
155+
StateT.run' self args |>.run args
152156

153157
def CliStateM.runLogIO (x : LogIO α) : CliStateM α := do
154158
MainM.runLogIO x (← get).toLogConfig
@@ -160,6 +164,11 @@ def CliStateM.runLoggerIO (x : LoggerIO α) : CliStateM α := do
160164

161165
instance (priority := low) : MonadLift LoggerIO CliStateM := ⟨CliStateM.runLoggerIO⟩
162166

167+
def CliStateM.runIO (x : IO α) : CliStateM α := do
168+
MainM.runLogIO x (← get).toLogConfig
169+
170+
instance (priority := low) : MonadLift IO CliStateM := ⟨CliStateM.runIO⟩
171+
163172
@[inline] def throwMissingArg (arg : String) : CliM α := do
164173
throw <| CliError.missingArg arg
165174

@@ -200,7 +209,7 @@ def noArgsRem (act : CliStateM α) : CliM α := do
200209

201210
/-! ## Option Parsing -/
202211

203-
def getWantsHelp : CliStateM Bool :=
212+
@[inline] def getWantsHelp : CliStateM Bool :=
204213
(·.wantsHelp) <$> get
205214

206215
def setConfigOpt (kvPair : String.Slice) : CliStateM PUnit :=
@@ -337,6 +346,10 @@ def processLeadingLakeOptions : CliM PUnit := fun args => do
337346
let args ← processLeadingOptions args lakeOption -- specializes `processLeadingOptions`
338347
return (⟨⟩, args)
339348

349+
theorem run_processLeadingLakeOptions {as} :
350+
StateT.run processLeadingLakeOptions as = (⟨(), ·⟩) <$> processLeadingOptions as lakeOption
351+
:= by rfl
352+
340353
/-! ## Actions -/
341354

342355
/-- Verify the Lean version Lake was built with matches that of the give Lean installation. -/
@@ -961,19 +974,46 @@ def lake : CliM PUnit := do
961974
match (← getArgs) with
962975
| [] => IO.println usage
963976
| ["--version"] => IO.println uiVersionString
964-
| _ => -- normal CLI
965-
processLeadingLakeOptions -- between `lake` and command
966-
if let some cmd ← takeArg? then
967-
processLeadingLakeOptions -- between `lake <cmd>` and args
968-
if (← getWantsHelp) then
969-
IO.println <| help cmd
970-
else
971-
lakeCli cmd
977+
| _ => go -- normal CLI
978+
where @[inline] go := do
979+
processLeadingLakeOptions -- between `lake` and command
980+
if let some cmd ← takeArg? then
981+
processLeadingLakeOptions -- between `lake <cmd>` and args
982+
if (← getWantsHelp) then
983+
IO.println <| help cmd
972984
else
973-
if (← getWantsHelp) then
974-
IO.println usage
975-
else
976-
throw <| CliError.missingCommand
985+
lakeCli cmd
986+
else
987+
if (← getWantsHelp) then
988+
IO.println usage
989+
else
990+
throw <| CliError.missingCommand
977991

978992
public def cli (args : List String) : BaseIO ExitCode :=
979-
inline <| (lake).run args
993+
(lake).run args
994+
995+
/- CLI sanity check -/
996+
997+
theorem run_lake_cons_of_ne_version (h : a ≠ "--version") :
998+
StateT.run lake (a :: as) = StateT.run lake.go (a :: as)
999+
:= by
1000+
simp only [lake, StateT.run_bind, ArgsT.run_getArgs, pure_bind]
1001+
split
1002+
· contradiction
1003+
· next h_eq => simp [h] at h_eq
1004+
· rfl
1005+
1006+
theorem cli_of_isArg (h : IsArg cmd) :
1007+
cli [cmd] = (StateT.run' (lakeCli cmd) []).run [cmd]
1008+
:= by
1009+
have ne_version : cmd ≠ "--version" := h.ne_of_isOpt (by decide)
1010+
simp only [cli, CliM.run, CliStateM.run, StateT.run'_eq]
1011+
simp only [run_lake_cons_of_ne_version ne_version, lake.go, getWantsHelp]
1012+
simp [processLeadingOptions_cons_of_isArg h, run_processLeadingLakeOptions, parseArg]
1013+
1014+
theorem cli_help :
1015+
cli ["help"] = (CliStateM.runIO <| IO.println usage).run ["help"]
1016+
:= by
1017+
have isArg_help : IsArg "help" := by decide
1018+
have lakeCli_help : lakeCli "help" = lake.help := by rfl
1019+
simp [cli_of_isArg isArg_help, lakeCli_help, lake.help, monadLift, MonadLift.monadLift]

src/lake/Lake/Util/Cli.lean

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public def Arg := String
3030
public abbrev MonadArg (m) := MonadReaderOf Arg m
3131

3232
/-- A List of command line arguments. -/
33-
@[expose] public def ArgList := List Arg
33+
@[expose] public def ArgList := List String
3434

3535
public noncomputable instance : SizeOf ArgList := inferInstanceAs (SizeOf (List String))
3636

@@ -56,7 +56,37 @@ public instance [MonadStateOf ArgList m] : MonadArgs m where
5656
takeArg? := modifyGet fun | [] => (none, []) | arg :: args => (some arg, args)
5757
takeArgs := modifyGet fun args => (args, [])
5858

59-
/-- A monad transformer to equip a monad with a state that monotonically decreasing in size. -/
59+
/-- A monad transformer to equip a monad with a command-line argument list. -/
60+
@[expose] -- for codegen
61+
public abbrev ArgsT := StateT ArgList
62+
63+
namespace ArgsT
64+
65+
/--
66+
Executes an action in the underlying monad {lean}`m` with the added state of a command-line
67+
argument list. It returns the monadic value paired with the final argument list.
68+
-/
69+
public abbrev run (self : ArgsT m α) (args : List String) : m (α × List String) :=
70+
StateT.run self args
71+
72+
/--
73+
Executes an action in the underlying monad {lean}`m` with the added state of a command-line
74+
argument list. It returns the monadic value, discarding any remaining arguments list.
75+
-/
76+
public abbrev run' [Functor m] (self : ArgsT m α) (args : List String) : m α :=
77+
StateT.run' self args
78+
79+
@[simp] public theorem run_getArgs {as : List String} [Monad m] :
80+
StateT.run (σ := ArgList) (m := m) getArgs as = pure (as, as)
81+
:= by rfl
82+
83+
@[simp] public theorem run_takeArgs {as : List String} [Monad m] :
84+
StateT.run (σ := ArgList) (m := m) takeArgs as = pure (as, [])
85+
:= by rfl
86+
87+
end ArgsT
88+
89+
/-- A monad transformer to equip a monad with a state that monotonically non-increasing in size. -/
6090
@[expose] -- for codegen
6191
public def MonoStateT (σ : Type u) [SizeOf σ] (m : Type max u v → Type w) (α : Type v) :=
6292
(s : σ) → m (α × {s' : σ // sizeOf s' ≤ sizeOf s})
@@ -120,25 +150,25 @@ public protected def get [SizeOf σ] [Pure m] : MonoStateT σ m σ :=
120150

121151
end MonoStateT
122152

123-
/-- A monad transformer to equip a monad with an argument list. -/
124-
public abbrev ArgsT (m : TypeType v) := MonoStateT ArgList m
153+
/-- A monad transformer to equip a monad with a monotonically non-increasing argument list. -/
154+
public abbrev MonoArgsT (m : TypeType v) := MonoStateT ArgList m
125155

126-
namespace ArgsT
156+
namespace MonoArgsT
127157

128158
@[inline, always_inline, inherit_doc MonadArgs.getArgs]
129-
public protected def get [Pure m] : ArgsT m (List String) :=
159+
public protected def get [Pure m] : MonoArgsT m (List String) :=
130160
MonoStateT.get
131161

132162
@[inline, always_inline, inherit_doc MonadArgs.takeArg?]
133-
public protected def takeArg? [Monad m] : ArgsT m (Option String) :=
163+
public protected def takeArg? [Monad m] : MonoArgsT m (Option String) :=
134164
MonoStateT.modifyGet fun
135165
| [] => (none, ⟨[], Nat.le_refl _⟩)
136166
| arg :: args => (some arg, ⟨args, mono_cons⟩)
137167
where
138168
mono_cons {a} {as : List String} : sizeOf as ≤ sizeOf (a :: as) := by simp
139169

140170
@[inline, always_inline, inherit_doc MonadArgs.takeArgs]
141-
public protected def takeArgs [Monad m] : ArgsT m (List String) :=
171+
public protected def takeArgs [Monad m] : MonoArgsT m (List String) :=
142172
MonoStateT.modifyGet fun args => (args, ⟨[], mono_nil⟩)
143173
where
144174
mono_nil {as : List String} : sizeOf ([] : List String) ≤ sizeOf as := by
@@ -147,12 +177,12 @@ where
147177
· simp only [List.nil.sizeOf_spec, List.cons.sizeOf_spec]
148178
omega
149179

150-
public instance [Monad m] : MonadArgs (ArgsT m) where
151-
getArgs := ArgsT.get
152-
takeArg? := ArgsT.takeArg?
153-
takeArgs := ArgsT.takeArgs
180+
public instance [Monad m] : MonadArgs (MonoArgsT m) where
181+
getArgs := MonoArgsT.get
182+
takeArg? := MonoArgsT.takeArg?
183+
takeArgs := MonoArgsT.takeArgs
154184

155-
end ArgsT
185+
end MonoArgsT
156186

157187
/-! ## Monadic Argument Parsing Utilities -/
158188

@@ -192,6 +222,17 @@ If no argument is available, returns {lean}`none`.
192222
else
193223
return none
194224

225+
@[simp] public theorem ArgsT.run_takeArg?_nil
226+
[Monad m] [LawfulMonad m] {_ : MonadParseArg α (ArgsT m)} :
227+
StateT.run (σ := ArgList) (m := m) takeArg? [] = pure ((none : Option α), [])
228+
:= by simp [takeArg?, MonadArgs.takeArg?]
229+
230+
@[simp] public theorem ArgsT.run_takeArg?_cons
231+
[Monad m] [LawfulMonad m] [MonadParseArg α (ArgsT m)] :
232+
StateT.run (σ := ArgList) (m := m) (α := Option α) takeArg? (a :: as) =
233+
StateT.run (σ := ArgList) (some <$> parseArg a) as
234+
:= by simp [takeArg?, MonadArgs.takeArg?]
235+
195236
/--
196237
Removes and returns the head of the remaining argument list, parsing the argument into {lean}`α`.
197238
@@ -201,6 +242,11 @@ If no argument is available, returns {lean}`default`.
201242
[Monad m] [MonadArgs m] [MonadParseArg α m] (default : α)
202243
: m α := return (← takeArg?).getD default
203244

245+
@[simp] public theorem takeArgD_eq_getD_takeArg?
246+
[Monad m] [LawfulMonad m] [MonadArgs m] [MonadParseArg α m] :
247+
takeArgD (α := α) (m := m) d = (·.getD d) <$> takeArg?
248+
:= by simp [takeArgD]
249+
204250
/--
205251
Removes and returns the head of the remaining argument list, parsing the argument into {lean}`α`.
206252
@@ -209,11 +255,23 @@ If no argument is available,, errors using {name}`throwExpectedArg` with {lean}`
209255
@[inline] public def takeArg
210256
[Monad m] [MonadArgs m] [MonadParseArg α m] [MonadThrowExpectedArg m] (descr : String)
211257
: m α := do
212-
if let some arg ← takeArg? then
258+
if let some arg ← MonadArgs.takeArg? then
213259
parseArg arg
214260
else
215261
throwExpectedArg descr
216262

263+
@[simp] public theorem ArgsT.run_takeArg_nil
264+
[Monad m] [LawfulMonad m] [MonadParseArg α (ArgsT m)] [MonadThrowExpectedArg m] :
265+
StateT.run (σ := ArgList) (m := m) (α := α) (takeArg d) [] =
266+
((·, [])) <$> throwExpectedArg (m := m) d
267+
:= by simp [takeArg, MonadArgs.takeArg?, throwExpectedArg]
268+
269+
@[simp] public theorem ArgsT.run_takeArg_cons
270+
[Monad m] [LawfulMonad m] [MonadParseArg α (ArgsT m)] {_ : MonadThrowExpectedArg m} :
271+
StateT.run (σ := ArgList) (m := m) (α := α) (takeArg d) (a :: as) =
272+
StateT.run (σ := ArgList) (parseArg a) as
273+
:= by simp [takeArg, MonadArgs.takeArg?]
274+
217275
/-! ## Argument Handler Type -/
218276

219277
/-- A monadic callback for an arbitrary command line argument. -/
@@ -226,7 +284,7 @@ public def ArgHandler (m : Type → Type v) :=
226284
read
227285

228286
/-- A monad transformer to equip a monad with a command line argument. -/
229-
public abbrev ArgT (m) := ReaderT Arg <| ArgsT m
287+
public abbrev ArgT (m) := ReaderT Arg <| MonoArgsT m
230288

231289
/--
232290
Construct an argument handler from a monadic action.
@@ -260,7 +318,7 @@ public structure IsArg (arg : String) : Prop where
260318
namespace IsOpt
261319

262320
/-- A decision procedure for determining if {lean}`arg` is an option. -/
263-
public def dec (arg : String) : Decidable (IsOpt arg) :=
321+
@[reducible, expose] public def dec (arg : String) : Decidable (IsOpt arg) :=
264322
if h0 : arg.startPos = arg.endPos then
265323
isFalse fun ⟨_, _, _⟩ => by contradiction
266324
else
@@ -274,11 +332,14 @@ public def dec (arg : String) : Decidable (IsOpt arg) :=
274332

275333
public instance instDecidable {arg : String} : Decidable (IsOpt arg) := dec arg
276334

335+
public theorem ne_empty {arg} (h : IsOpt arg) : arg ≠ "" :=
336+
String.startPos_eq_endPos_iff.subst h.startPos_ne_endPos
337+
277338
public theorem not_isArg {arg} (h : IsOpt arg) : ¬ IsArg arg :=
278339
fun h' => h'.not_isOpt h
279340

280-
public theorem ne_empty {arg} (h : IsOpt arg) : arg ≠ "" :=
281-
String.startPos_eq_endPos_iff.subst h.startPos_ne_endPos
341+
public theorem ne_of_isArg (ha : IsOpt a) (hb : IsArg b) : a ≠ b :=
342+
fun heq => ha.not_isArg.elim (heq ▸ hb)
282343

283344
public theorem offset_next_startPos (h : IsOpt a) :
284345
(a.startPos.next h.startPos_ne_endPos).offset = ⟨1
@@ -334,7 +395,7 @@ end IsOpt
334395
namespace IsArg
335396

336397
/-- A decision procedure for determining if {lean}`arg` is an non-option command line argument. -/
337-
public def dec (arg : String) : Decidable (IsArg arg) :=
398+
@[reducible, expose] public def dec (arg : String) : Decidable (IsArg arg) :=
338399
if h0 : arg.startPos = arg.endPos then
339400
isFalse fun h => h.ne_empty (String.startPos_eq_endPos_iff.mp h0)
340401
else
@@ -349,6 +410,9 @@ public def dec (arg : String) : Decidable (IsArg arg) :=
349410

350411
public instance instDecidable {arg : String} : Decidable (IsArg arg) := dec arg
351412

413+
public theorem ne_of_isOpt (ha : IsArg a) (hb : IsOpt b) : a ≠ b :=
414+
fun heq => ha.not_isOpt.elim (heq ▸ hb)
415+
352416
public theorem startPos_ne_endPos (h : IsArg a) : a.startPos ≠ a.endPos :=
353417
String.startPos_eq_endPos_iff.symm.subst h.ne_empty
354418

@@ -378,7 +442,7 @@ public def OptArg := Option String.Slice
378442
public abbrev MonadOptArg (m) := MonadReaderOf OptArg m
379443

380444
/-- A monad transformer to equip a monad with a command line option and optional argument. -/
381-
public abbrev OptT (m) := ReaderT Opt <| ReaderT OptArg <| ArgsT m
445+
public abbrev OptT (m) := ReaderT Opt <| ReaderT OptArg <| MonoArgsT m
382446

383447
/-- The character of a command line short option (e.g., {lit}`x` of {lit}`-x`). -/
384448
@[expose] -- for codegen
@@ -636,11 +700,11 @@ public def processLeadingOptions
636700
return args
637701
termination_by args
638702

639-
public theorem processLeadingOptions_nil [Monad m] :
703+
@[simp] public theorem processLeadingOptions_nil [Monad m] :
640704
processLeadingOptions (m := m) [] f = pure []
641705
:= by unfold processLeadingOptions; simp
642706

643-
public theorem processLeadingOptions_cons_empty [Monad m] :
707+
@[simp] public theorem processLeadingOptions_cons_empty [Monad m] :
644708
processLeadingOptions (m := m) ("" :: as) f = processLeadingOptions as f
645709
:= by conv => {lhs; unfold processLeadingOptions}; simp
646710

@@ -674,11 +738,11 @@ public theorem processLeadingOptions_cons_of_isArg [Monad m] (h : IsArg a) :
674738
termination_by args
675739
loop args
676740

677-
public theorem processArgs_nil [Monad m] :
741+
@[simp] public theorem processArgs_nil [Monad m] :
678742
processArgs (m := m) [] optH argH = pure ()
679743
:= by unfold processArgs processArgs.loop; simp
680744

681-
public theorem processArgs_cons_empty [Monad m] :
745+
@[simp] public theorem processArgs_cons_empty [Monad m] :
682746
processArgs (m := m) ("" :: as) optH argH = processArgs as optH argH
683747
:= by
684748
unfold processArgs

src/lake/Lake/Util/MainM.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module
88
prelude
99
public import Lake.Util.Log
1010
public import Lake.Util.Exit
11+
import all Init.System.ST
1112

1213
namespace Lake
1314

@@ -21,6 +22,12 @@ public instance : Monad MainM := inferInstanceAs (Monad (EIO ExitCode))
2122
public instance : MonadFinally MainM := inferInstanceAs (MonadFinally (EIO ExitCode))
2223
public instance : MonadLift BaseIO MainM := inferInstanceAs (MonadLift BaseIO (EIO ExitCode))
2324

25+
/- Adaption of `LawfulMonad` for `EST` from batteries. -/
26+
public instance : LawfulMonad MainM := .mk' _
27+
(id_map := fun x => funext fun v => by dsimp [Functor.map, EST.bind]; cases x v <;> rfl)
28+
(pure_bind := fun x f => rfl)
29+
(bind_assoc := fun f g x => funext fun v => by dsimp [Bind.bind, EST.bind]; cases f v <;> rfl)
30+
2431
namespace MainM
2532

2633
/-! # Basics -/

0 commit comments

Comments
 (0)