feat: lake: lift FetchM into JobM (and vice versa) (#6771)

This PR enables `FetchM` to be run from `JobM` / `SpawnM` and
vice-versa. This allows calls of `fetch` to asynchronously depend on the
outputs of other jobs.
This commit is contained in:
Mac Malone 2025-01-24 22:59:47 -05:00 committed by GitHub
parent c8be581bc8
commit 58c7a4f15e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 626 additions and 550 deletions

View file

@ -10,3 +10,4 @@ import Lake.Build.Package
import Lake.Build.Library
import Lake.Build.Imports
import Lake.Build.Targets
import Lake.Build.Job

View file

@ -5,8 +5,9 @@ Authors: Mac Malone
-/
prelude
import Lake.Config.Monad
import Lake.Build.Actions
import Lake.Util.JsonObject
import Lake.Build.Actions
import Lake.Build.Job
/-! # Common Build Tools
This file defines general utilities that abstract common
@ -19,6 +20,9 @@ namespace Lake
/-! ## General Utilities -/
/-- Exit code to return if `--no-build` is set and a build is required. -/
def noBuildCode : ExitCode := 3
/--
Build trace for the host platform.
If an artifact includes this trace, it is platform-dependent

View file

@ -4,20 +4,12 @@ Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Util.Log
import Lake.Util.Exit
import Lake.Util.Lift
import Lake.Util.Task
import Lake.Util.Opaque
import Lake.Config.Context
import Lake.Build.Trace
import Lake.Build.Job.Basic
open System
namespace Lake
/-- Exit code to return if `--no-build` is set and a build is required. -/
def noBuildCode : ExitCode := 3
/-- Configuration options for a Lake build. -/
structure BuildConfig where
/-- Use modification times for trace checking. -/
@ -55,58 +47,6 @@ Whether the build should show progress information.
def BuildConfig.showProgress (cfg : BuildConfig) : Bool :=
(cfg.noBuild ∧ cfg.verbosity == .verbose) cfg.verbosity != .quiet
/-- Information on what this job did. -/
inductive JobAction
/-- No information about this job's action is available. -/
| unknown
/-- Tried to replay a cached build action (set by `buildFileUnlessUpToDate`) -/
| replay
/-- Tried to fetch a build from a store (can be set by `buildUnlessUpToDate?`) -/
| fetch
/-- Tried to perform a build action (set by `buildUnlessUpToDate?`) -/
| build
deriving Inhabited, Repr, DecidableEq, Ord
instance : LT JobAction := ltOfOrd
instance : LE JobAction := leOfOrd
instance : Min JobAction := minOfLe
instance : Max JobAction := maxOfLe
def JobAction.merge (a b : JobAction) : JobAction :=
max a b
/-- Mutable state of a Lake job. -/
structure JobState where
/-- The job's log. -/
log : Log := {}
/-- Tracks whether this job performed any significant build action. -/
action : JobAction := .unknown
/-- Current trace of a build job. -/
trace : BuildTrace := .nil
deriving Inhabited
/-- The result of a Lake job. -/
abbrev JobResult α := EResult Log.Pos JobState α
/-- The `Task` of a Lake job. -/
abbrev JobTask α := BaseIOTask (JobResult α)
/-- A Lake job. -/
structure Job (α : Type u) where
/-- The Lean `Task` object for the job. -/
task : JobTask α
/--
A caption for the job in Lake's build monitor.
Will be formatted like `✔ [3/5] Ran <caption>`.
-/
caption : String
/-- Whether this job failing should cause the build to fail. -/
optional : Bool := false
deriving Inhabited
/-- A Lake job with an opaque value in `Type`. -/
abbrev OpaqueJob := Job Opaque
/-- A Lake context with a build configuration and additional build data. -/
structure BuildContext extends BuildConfig, Context where
leanTrace : BuildTrace
@ -119,6 +59,9 @@ abbrev BuildT := ReaderT BuildContext
abbrev MonadBuild (m : Type → Type u) :=
MonadReaderOf BuildContext m
instance [Pure m] : MonadLift LakeM (BuildT m) where
monadLift x := fun ctx => pure <| x.run ctx.toContext
@[inline] def getBuildContext [MonadBuild m] : m BuildContext :=
readThe BuildContext
@ -145,17 +88,3 @@ abbrev MonadBuild (m : Type → Type u) :=
@[inline] def getIsQuiet [Functor m] [MonadBuild m] : m Bool :=
(· == .quiet) <$> getVerbosity
/-- The internal core monad of Lake builds. Not intended for user use. -/
abbrev CoreBuildM := BuildT LogIO
/--
Logs a build step with `message`.
**Deprecated:** Build steps are now managed by a top-level build monitor.
As a result, this no longer functions the way it used to. It now just logs the
`message` via `logVerbose`.
-/
@[deprecated "See doc-string for deprecation information." (since := "2024-05-25"), inline]
def logStep [Monad m] [MonadLog m] (message : String) : m Unit := do
logVerbose message

View file

@ -4,8 +4,8 @@ Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Build.Job
import Lake.Build.Data
import Lake.Build.Job.Basic
/-!
# Simple Builtin Facet Declarations

View file

@ -4,11 +4,13 @@ Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Util.Lift
import Lake.Util.Error
import Lake.Util.Cycle
import Lake.Util.EquipT
import Lake.Build.Info
import Lake.Build.Store
import Lake.Build.Context
/-! # Recursive Building
@ -20,60 +22,54 @@ using the `fetch` function defined in this module.
namespace Lake
/-- A recursive build of a Lake build store that may encounter a cycle. -/
abbrev RecBuildM :=
CallStackT BuildKey <| BuildT <| ELogT <| StateRefT BuildStore BaseIO
instance : MonadLift LogIO RecBuildM := ⟨ELogT.takeAndRun⟩
/-- The internal core monad of Lake builds. Not intended for user use. -/
abbrev CoreBuildM := BuildT LogIO
/--
Run a `JobM` action in `RecBuildM` (and thus `FetchM`).
A recursive build of a Lake build store that may encounter a cycle.
Generally, this should not be done, and instead a job action
should be run asynchronously in a Job (e.g., via `Job.async`).
An internal monad. Not intended for user use.
-/
@[inline] def RecBuildM.runJobM (x : JobM α) : RecBuildM α := fun _ ctx log => do
match (← x ctx {log}) with
| .ok a s => return .ok a s.log
| .error e s => return .error e s.log
instance : MonadLift JobM RecBuildM := ⟨RecBuildM.runJobM⟩
/-- Run a recursive build. -/
@[inline] def RecBuildM.run
(stack : CallStack BuildKey) (store : BuildStore) (build : RecBuildM α)
: CoreBuildM (α × BuildStore) := fun ctx log => do
match (← (build stack ctx log).run store) with
| (.ok a log, store) => return .ok (a, store) log
| (.error e log, _) => return .error e log
/-- Run a recursive build in a fresh build store. -/
@[inline] def RecBuildM.run' (build : RecBuildM α) : CoreBuildM α := do
(·.1) <$> build.run {} {}
abbrev RecBuildT (m : Type → Type) :=
CallStackT BuildKey <| StateRefT' IO.RealWorld BuildStore <| BuildT m
/-- Log build cycle and error. -/
@[specialize] def buildCycleError [MonadError m] (cycle : Cycle BuildKey) : m α :=
error s!"build cycle detected:\n{"\n".intercalate <| cycle.map (s!" {·}")}"
instance : MonadCycleOf BuildKey RecBuildM where
instance [Monad m] [MonadError m] : MonadCycleOf BuildKey (RecBuildT m) where
throwCycle := buildCycleError
/--
A recursive build of a Lake build store that may encounter a cycle.
An internal monad. Not intended for user use.
-/
abbrev RecBuildM := RecBuildT LogIO
/-- Run a recursive build. -/
@[inline] def RecBuildM.run
(stack : CallStack BuildKey) (store : BuildStore) (build : RecBuildM α)
: CoreBuildM (α × BuildStore) :=
build stack |>.run store
/-- Run a recursive build in a fresh build store. -/
@[inline] def RecBuildM.run' (build : RecBuildM α) : CoreBuildM α := do
(·.1) <$> build.run {} {}
/-- A build function for any element of the Lake build index. -/
abbrev IndexBuildFn (m : Type → Type v) :=
-- `DFetchFn BuildInfo (BuildData ·.key) m` with less imports
(info : BuildInfo) → m (BuildData info.key)
/-- A transformer to equip a monad with a build function for the Lake index. -/
abbrev IndexT (m : Type → Type v) := EquipT (IndexBuildFn m) m
abbrev IndexT (m : Type → Type v) := EquipT (IndexBuildFn RecBuildM) m
/-- The top-level monad transformer for Lake build functions. -/
abbrev FetchT (m : Type → Type) := IndexT <| RecBuildT m
/-- The top-level monad for Lake build functions. -/
abbrev FetchM := IndexT RecBuildM
/-- Ensures that `JobM` lifts into `FetchM`. -/
example : MonadLiftT JobM FetchM := inferInstance
/-- Ensures that `SpawnM` lifts into `FetchM`. -/
example : MonadLiftT SpawnM FetchM := inferInstance
abbrev FetchM := FetchT LogIO
/-- The top-level monad for Lake build functions. **Renamed `FetchM`.** -/
@[deprecated FetchM (since := "2024-04-30")] abbrev IndexBuildM := FetchM
@ -86,52 +82,3 @@ example : MonadLiftT SpawnM FetchM := inferInstance
fun build => cast (by simp) <| build self
export BuildInfo (fetch)
/-- Wraps stray I/O, logs, and errors in `x` into the produced job. -/
def ensureJob (x : FetchM (Job α))
: FetchM (Job α) := fun fetch stack ctx log store => do
let iniPos := log.endPos
match (← (withLoggedIO x) fetch stack ctx log store) with
| .ok job log =>
if iniPos < log.endPos then
let (log, jobLog) := log.split iniPos
let job := job.mapResult (sync := true) (·.prependLog jobLog)
return .ok job log
else
return .ok job log
| .error _ log =>
let (log, jobLog) := log.split iniPos
return .ok (.error jobLog) log
/--
Registers the job for the top-level build monitor,
(e.g., the Lake CLI progress UI), assigning it `caption`.
-/
def registerJob (caption : String) (job : Job α) (optional := false) : FetchM (Job α) := do
let job : Job α := {job with caption, optional}
(← getBuildContext).registeredJobs.modify (·.push job)
return job.renew
/--
Registers the produced job for the top-level build monitor
(e.g., the Lake CLI progress UI), assigning it `caption`.
Stray I/O, logs, and errors produced by `x` will be wrapped into the job.
-/
def withRegisterJob
(caption : String) (x : FetchM (Job α)) (optional := false)
: FetchM (Job α) := do
let job ← ensureJob x
registerJob caption job optional
/--
Registers the produced job for the top-level build monitor
if it is not already (i.e., it has an empty caption).
-/
@[inline] def maybeRegisterJob
(caption : String) (job : Job α)
: FetchM (Job α) := do
if job.caption.isEmpty then
registerJob caption job
else
return job

View file

@ -1,387 +1,9 @@
/-
Copyright (c) 2022 Mac Malone. All rights reserved.
Copyright (c) 2025 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Build.Basic
open System
namespace Lake
def JobAction.verb (failed : Bool) : JobAction → String
| .unknown => if failed then "Running" else "Ran"
| .replay => if failed then "Replaying" else "Replayed"
| .fetch => if failed then "Fetching" else "Fetched"
| .build => if failed then "Building" else "Built"
/--
Resets the job state after a checkpoint (e.g., registering the job).
Preserves state that downstream jobs want to depend on while resetting
job-local state that should not be inherited by downstream jobs.
-/
@[inline] def JobState.renew (s : JobState) : JobState where
trace := s.trace
def JobState.merge (a b : JobState) : JobState where
log := a.log ++ b.log
action := a.action.merge b.action
trace := mixTrace a.trace b.trace
@[inline] def JobState.modifyLog (f : Log → Log) (s : JobState) :=
{s with log := f s.log}
@[inline] def JobState.logEntry (e : LogEntry) (s : JobState) :=
s.modifyLog (·.push e)
/-- Add log entries to the beginning of the job's log. -/
def JobResult.prependLog (log : Log) (self : JobResult α) : JobResult α :=
match self with
| .ok a s => .ok a <| s.modifyLog (log ++ ·)
| .error e s => .error ⟨log.size + e.val⟩ <| s.modifyLog (log ++ ·)
/--
The monad of asynchronous Lake jobs.
While this can be lifted into `FetchM`, job action should generally
be wrapped into an asynchronous job (e.g., via `Job.async`) instead of being
run directly in `FetchM`.
-/
abbrev JobM := BuildT <| EStateT Log.Pos JobState BaseIO
instance [Pure m] : MonadLift LakeM (BuildT m) where
monadLift x := fun ctx => pure <| x.run ctx.toContext
instance : MonadStateOf Log JobM where
get := (·.log) <$> get
set log := modify fun s => {s with log}
modifyGet f := modifyGet fun s => let (a, log) := f s.log; (a, {s with log})
instance : MonadStateOf JobState JobM := inferInstance
instance : MonadLog JobM := .ofMonadState
instance : MonadError JobM := ELog.monadError
instance : Alternative JobM := ELog.alternative
instance : MonadLift LogIO JobM := ⟨ELogT.takeAndRun⟩
/-- Record that this job is trying to perform some action. -/
@[inline] def updateAction (action : JobAction) : JobM PUnit :=
modify fun s => {s with action := s.action.merge action}
/-- Returns the current job's build trace. -/
@[inline] def getTrace : JobM BuildTrace :=
(·.trace) <$> get
/-- Sets the current job's build trace. -/
@[inline] def setTrace (trace : BuildTrace) : JobM PUnit :=
modify fun s => {s with trace := trace}
/-- Mix a trace into the current job's build trace. -/
@[inline] def addTrace (trace : BuildTrace) : JobM PUnit :=
modify fun s => {s with trace := s.trace.mix trace}
/-- Returns the current job's build trace and removes it from the state. -/
@[inline] def takeTrace : JobM BuildTrace :=
modifyGet fun s => (s.trace, {s with trace := nilTrace})
/-- The monad used to spawn asynchronous Lake build jobs. Lifts into `FetchM`. -/
abbrev SpawnM := BuildT <| ReaderT BuildTrace <| BaseIO
@[inline] def JobM.runSpawnM (x : SpawnM α) : JobM α := fun ctx s =>
return .ok (← x ctx s.trace) s
instance : MonadLift SpawnM JobM := ⟨JobM.runSpawnM⟩
/-- The monad used to spawn asynchronous Lake build jobs. **Replaced by `SpawnM`.** -/
@[deprecated SpawnM (since := "2024-05-21")] abbrev SchedulerM := SpawnM
@[inline] private unsafe def Job.toOpaqueImpl (job : Job α) : OpaqueJob :=
unsafeCast job
/-- Forget the value of a job. Implemented as a no-op cast. -/
@[implemented_by toOpaqueImpl]
def Job.toOpaque (job : Job α) : OpaqueJob :=
{job with task := job.task.map (·.map Opaque.mk)}
instance : CoeOut (Job α) OpaqueJob := ⟨.toOpaque⟩
namespace Job
@[inline] def ofTask (task : JobTask α) (caption := "") : Job α :=
{task, caption}
@[inline] protected def error (log : Log := {}) (caption := "") : Job α :=
{task := Task.pure (.error 0 {log}), caption}
@[inline] protected def pure (a : α) (log : Log := {}) (caption := "") : Job α :=
{task := Task.pure (.ok a {log}), caption}
instance : Pure Job := ⟨Job.pure⟩
instance [Inhabited α] : Inhabited (Job α) := ⟨pure default⟩
@[inline] protected def nop (log : Log := {}) (caption := "") : Job Unit :=
.pure () log caption
@[inline] def nil : Job Unit :=
.pure ()
/-- Sets the job's caption. -/
@[inline] def setCaption (caption : String) (job : Job α) : Job α :=
{job with caption}
/-- Sets the job's caption if the job's current caption is empty. -/
@[inline] def setCaption? (caption : String) (job : Job α) : Job α :=
if job.caption.isEmpty then {job with caption} else job
@[inline] def mapResult
(f : JobResult α → JobResult β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β := {self with task := self.task.map f prio sync}
@[inline] def mapOk
(f : α → JobState → JobResult β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β :=
self.mapResult (prio := prio) (sync := sync) fun
| .ok a s => f a s
| .error e s => .error e s
@[inline] def bindTask [Monad m]
(f : JobTask α → m (JobTask β)) (self : Job α)
: m (Job β) := return {self with task := ← f self.task}
@[inline] protected def map
(f : α → β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β := self.mapResult (·.map f) prio sync
instance : Functor Job where map := Job.map
/--
Resets the job's state after a checkpoint (e.g., registering the job).
Preserves information that downstream jobs want to depend on while resetting
job-local information that should not be inherited by downstream jobs.
-/
def renew (self : Job α) : Job α :=
self.mapResult (sync := true) fun
| .ok a s => .ok a s.renew
| .error _ s => .error 0 s.renew
/-- Spawn a job that asynchronously performs `act`. -/
@[inline] protected def async
(act : JobM α) (prio := Task.Priority.default)
: SpawnM (Job α) := fun ctx => .ofTask <$> do
BaseIO.asTask (prio := prio) do (withLoggedIO act) ctx {}
/-- Wait a the job to complete and return the result. -/
@[inline] protected def wait (self : Job α) : BaseIO (JobResult α) := do
IO.wait self.task
/--
Wait for a job to complete and return the produced value.
If an error occurred, return `none` and discarded any logs produced.
-/
@[inline] protected def wait? (self : Job α) : BaseIO (Option α) := do
return (← self.wait).result?
/--
Wait for a job to complete and return the produced value.
Logs the job's log and throws if there was an error.
-/
@[inline] protected def await (self : Job α) : LogIO α := do
match (← self.wait) with
| .error n {log, ..} => log.replay; throw n
| .ok a {log, ..} => log.replay; pure a
/-- Apply `f` asynchronously to the job's output. -/
protected def mapM
(self : Job α) (f : α → JobM β)
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) :=
fun ctx trace => do
self.bindTask fun task => do
BaseIO.mapTask (t := task) (prio := prio) (sync := sync) fun
| .ok a s =>
let trace := mixTrace trace s.trace
withLoggedIO (f a) ctx {s with trace}
| .error n s => return .error n s
@[deprecated Job.mapM (since := "2024-12-06")]
protected abbrev bindSync
(self : Job α) (f : α → JobM β)
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) := self.mapM f prio sync
/--
Apply `f` asynchronously to the job's output
and asynchronously await the resulting job.
-/
def bindM
(self : Job α) (f : α → JobM (Job β))
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) :=
fun ctx trace => do
self.bindTask fun task => do
BaseIO.bindTask task (prio := prio) (sync := sync) fun
| .ok a sa => do
let trace := mixTrace trace sa.trace
match (← withLoggedIO (f a) ctx {sa with trace}) with
| .ok job sa =>
return job.task.map (prio := prio) (sync := true) fun
| .ok b sb => .ok b {sa.merge sb with trace := sb.trace}
| .error e sb => .error ⟨sa.log.size + e.val⟩ {sa.merge sb with trace := sb.trace}
| .error e sa => return Task.pure (.error e sa)
| .error e sa => return Task.pure (.error e sa)
@[deprecated bindM (since := "2024-12-06")]
protected abbrev bindAsync
(self : Job α) (f : α → SpawnM (Job β))
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) := self.bindM (fun a => f a) prio sync
/--
`a.zipWith f b` produces a new job `c` that applies `f` to the
results of `a` and `b`. The job `c` errors if either `a` or `b` error.
-/
@[inline] def zipResultWith
(f : JobResult α → JobResult β → JobResult γ) (self : Job α) (other : Job β)
(prio := Task.Priority.default) (sync := true)
: Job γ := Job.ofTask $
self.task.bind (prio := prio) (sync := true) fun rx =>
other.task.map (prio := prio) (sync := sync) fun ry =>
f rx ry
/--
`a.zipWith f b` produces a new job `c` that applies `f` to the
results of `a` and `b`. The job `c` errors if either `a` or `b` error.
-/
@[inline] def zipWith
(f : α → β → γ) (self : Job α) (other : Job β)
(prio := Task.Priority.default) (sync := true)
: Job γ :=
self.zipResultWith (other := other) (prio := prio) (sync := sync) fun
| .ok a sa, .ok b sb => .ok (f a b) (sa.merge sb)
| ra, rb => .error 0 (ra.state.merge rb.state)
/-- Merges this job with another, discarding its output and trace. -/
def add (self : Job α) (other : Job β) : Job α :=
self.zipResultWith (other := other) fun
| .ok a sa, .ok _ sb => .ok a {sa.merge sb with trace := sa.trace}
| ra, rb => .error 0 {ra.state.merge rb.state with trace := ra.state.trace}
/-- Merges this job with another, discarding both outputs. -/
def mix (self : Job α) (other : Job β) : Job Unit :=
self.zipWith (fun _ _ => ()) other
/-- Merge a `List` of jobs into one, discarding their outputs. -/
def mixList (jobs : List (Job α)) : Job Unit :=
jobs.foldr (·.mix ·) nil
/-- Merge an `Array` of jobs into one, discarding their outputs. -/
def mixArray (jobs : Array (Job α)) : Job Unit :=
jobs.foldl (·.mix ·) nil
/-- Merge a `List` of jobs into one, collecting their outputs into a `List`. -/
def collectList (jobs : List (Job α)) : Job (List α) :=
jobs.foldr (zipWith List.cons) (pure [])
/-- Merge an `Array` of jobs into one, collecting their outputs into an `Array`. -/
def collectArray (jobs : Array (Job α)) : Job (Array α) :=
jobs.foldl (zipWith Array.push) (pure (Array.mkEmpty jobs.size))
end Job
/-- A Lake build job. -/
abbrev BuildJob α := Job α
namespace BuildJob
@[inline, deprecated "Obsolete." (since := "2024-12-06")]
def mk (job : Job (α × BuildTrace)) : BuildJob α :=
job.mapOk fun (a, trace) s => .ok a {s with trace}
@[inline, deprecated "Obsolete." (since := "2024-12-06")]
def ofJob (job : Job BuildTrace) : BuildJob Unit :=
job.mapOk fun trace s => .ok () {s with trace}
abbrev toJob (self : BuildJob α) : Job α :=
self
@[deprecated Job.nil (since := "2024-12-06")]
abbrev nil : BuildJob Unit :=
Job.pure ()
@[deprecated Job.map (since := "2024-12-06")]
protected abbrev pure (a : α) : BuildJob α :=
Job.pure a
instance : Pure BuildJob := ⟨Job.pure⟩
@[deprecated Job.map (since := "2024-12-06")]
protected abbrev map (f : α → β) (self : BuildJob α) : BuildJob β :=
self.toJob.map f
instance : Functor BuildJob where
map := Job.map
@[inline, deprecated "Removed without replacement." (since := "2024-12-06")]
def mapWithTrace (f : α → BuildTrace → β × BuildTrace) (self : BuildJob α) : BuildJob β :=
self.toJob.mapOk fun a s =>
let (b, trace) := f a s.trace
.ok b {s with trace}
@[inline, deprecated Job.mapM (since := "2024-12-06")]
protected def bindSync
(self : BuildJob α) (f : α → BuildTrace → JobM (β × BuildTrace))
(prio : Task.Priority := .default) (sync := false)
: SpawnM (Job β) :=
self.toJob.mapM (prio := prio) (sync := sync) fun a => do
let (b, trace) ← f a (← getTrace)
setTrace trace
return b
@[inline, deprecated Job.bindM (since := "2024-12-06")]
protected def bindAsync
(self : BuildJob α) (f : α → BuildTrace → SpawnM (Job β))
(prio : Task.Priority := .default) (sync := false)
: SpawnM (Job β) :=
self.toJob.bindM (prio := prio) (sync := sync) fun a => do
f a (← getTrace)
@[deprecated Job.wait? (since := "2024-12-06")]
protected abbrev wait? (self : BuildJob α) : BaseIO (Option α) :=
self.toJob.wait?
@[deprecated Job.add (since := "2024-12-06")]
abbrev add (self : BuildJob α) (other : BuildJob β) : BuildJob α :=
self.toJob.add other.toJob
@[deprecated Job.mix (since := "2024-12-06")]
abbrev mix (self : BuildJob α) (other : BuildJob β) : BuildJob Unit :=
self.toJob.mix other.toJob
@[deprecated Job.mixList (since := "2024-12-06")]
abbrev mixList (jobs : List (BuildJob α)) : Id (BuildJob Unit) :=
return Job.mixList jobs
@[deprecated Job.mixArray (since := "2024-12-06")]
abbrev mixArray (jobs : Array (BuildJob α)) : Id (BuildJob Unit) :=
return Job.mixArray jobs
@[deprecated Job.zipWith (since := "2024-12-06")]
abbrev zipWith
(f : α → β → γ) (self : BuildJob α) (other : BuildJob β)
: BuildJob γ :=
self.toJob.zipWith f other.toJob
@[deprecated Job.collectList (since := "2024-12-06")]
abbrev collectList (jobs : List (BuildJob α)) : Id (BuildJob (List α)) :=
return Job.collectList jobs
@[deprecated Job.collectArray (since := "2024-12-06")]
abbrev collectArray (jobs : Array (BuildJob α)) : Id (BuildJob (Array α)) :=
return Job.collectArray jobs
attribute [deprecated Job (since := "2024-12-06")] BuildJob
import Lake.Build.Job.Basic
import Lake.Build.Job.Monad
import Lake.Build.Job.Register

View file

@ -0,0 +1,165 @@
/-
Copyright (c) 2021 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Util.Log
import Lake.Util.Task
import Lake.Util.Opaque
import Lake.Build.Trace
/-! # Job Primitives
This module contains the basic definitions of a Lake `Job`. In particular,
it defines `OpaqueJob`, which is needed for `BuildContext`. More complex
utilities are defined in `Lake.Build.Job.Monad`, which depends on `BuildContext`.
-/
open System
namespace Lake
/-! ## JobAction -/
/-- Information on what this job did. -/
inductive JobAction
/-- No information about this job's action is available. -/
| unknown
/-- Tried to replay a cached build action (set by `buildFileUnlessUpToDate`) -/
| replay
/-- Tried to fetch a build from a store (can be set by `buildUnlessUpToDate?`) -/
| fetch
/-- Tried to perform a build action (set by `buildUnlessUpToDate?`) -/
| build
deriving Inhabited, Repr, DecidableEq, Ord
instance : LT JobAction := ltOfOrd
instance : LE JobAction := leOfOrd
instance : Min JobAction := minOfLe
instance : Max JobAction := maxOfLe
def JobAction.merge (a b : JobAction) : JobAction :=
max a b
def JobAction.verb (failed : Bool) : JobAction → String
| .unknown => if failed then "Running" else "Ran"
| .replay => if failed then "Replaying" else "Replayed"
| .fetch => if failed then "Fetching" else "Fetched"
| .build => if failed then "Building" else "Built"
/-! ## JobState -/
/-- Mutable state of a Lake job. -/
structure JobState where
/-- The job's log. -/
log : Log := {}
/-- Tracks whether this job performed any significant build action. -/
action : JobAction := .unknown
/-- Current trace of a build job. -/
trace : BuildTrace := .nil
deriving Inhabited
def JobState.merge (a b : JobState) : JobState where
log := a.log ++ b.log
action := a.action.merge b.action
trace := mixTrace a.trace b.trace
@[inline] def JobState.modifyLog (f : Log → Log) (s : JobState) : JobState :=
{s with log := f s.log}
@[inline] def JobState.logEntry (e : LogEntry) (s : JobState) :=
s.modifyLog (·.push e)
/-! ## JobTask -/
/-- The result of a Lake job. -/
abbrev JobResult α := EResult Log.Pos JobState α
/-- Add log entries to the beginning of the job's log. -/
def JobResult.prependLog (log : Log) (self : JobResult α) : JobResult α :=
match self with
| .ok a s => .ok a <| s.modifyLog (log ++ ·)
| .error e s => .error ⟨log.size + e.val⟩ <| s.modifyLog (log ++ ·)
/-- The `Task` of a Lake job. -/
abbrev JobTask α := BaseIOTask (JobResult α)
/-! ## Job -/
/-- A Lake job. -/
structure Job (α : Type u) where
/-- The Lean `Task` object for the job. -/
task : JobTask α
/--
A caption for the job in Lake's build monitor.
Will be formatted like `✔ [3/5] Ran <caption>`.
-/
caption : String
/-- Whether this job failing should cause the build to fail. -/
optional : Bool := false
deriving Inhabited
namespace Job
@[inline] def ofTask (task : JobTask α) (caption := "") : Job α :=
{task, caption}
@[inline] protected def error (log : Log := {}) (caption := "") : Job α :=
{task := Task.pure (.error 0 {log}), caption}
@[inline] protected def pure (a : α) (log : Log := {}) (caption := "") : Job α :=
{task := Task.pure (.ok a {log}), caption}
instance : Pure Job := ⟨Job.pure⟩
@[inline] protected def nop (log : Log := {}) (caption := "") : Job Unit :=
.pure () log caption
@[inline] def nil : Job Unit :=
.pure ()
/-- Sets the job's caption. -/
@[inline] def setCaption (caption : String) (job : Job α) : Job α :=
{job with caption}
/-- Sets the job's caption if the job's current caption is empty. -/
@[inline] def setCaption? (caption : String) (job : Job α) : Job α :=
if job.caption.isEmpty then {job with caption} else job
@[inline] def mapResult
(f : JobResult α → JobResult β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β := {self with task := self.task.map f prio sync}
@[inline] def mapOk
(f : α → JobState → JobResult β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β :=
self.mapResult (prio := prio) (sync := sync) fun
| .ok a s => f a s
| .error e s => .error e s
@[inline] protected def map
(f : α → β) (self : Job α)
(prio := Task.Priority.default) (sync := false)
: Job β := self.mapResult (·.map f) prio sync
instance : Functor Job where map := Job.map
end Job
/-! ## OpaqueJob -/
/-- A Lake job with an opaque value in `Type`. -/
abbrev OpaqueJob := Job Opaque
@[inline] private unsafe def Job.toOpaqueImpl (job : Job α) : OpaqueJob :=
unsafeCast job
/-- Forget the value of a job. Implemented as a no-op cast. -/
@[implemented_by toOpaqueImpl]
def Job.toOpaque (job : Job α) : OpaqueJob :=
{job with task := job.task.map (·.map Opaque.mk)}
instance : CoeOut (Job α) OpaqueJob := ⟨.toOpaque⟩

View file

@ -0,0 +1,328 @@
/-
Copyright (c) 2022 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Build.Fetch
open System
/-! # Job Monad
This module contains additional definitions for Lake `Job`.
In particular, it defines `JobM`, which uses `BuildContext`, which contains
`OpaqueJob`s, hence the module split.
-/
namespace Lake
/--
The monad of asynchronous Lake jobs.
While this can be lifted into `FetchM`, job action should generally
be wrapped into an asynchronous job (e.g., via `Job.async`) instead of being
run directly in `FetchM`.
-/
abbrev JobM := FetchT <| EStateT Log.Pos JobState BaseIO
instance (priority := high) : MonadStateOf JobState JobM := inferInstance
instance : MonadStateOf Log JobM where
get := (·.log) <$> get
set log := modify fun s => {s with log}
modifyGet f := modifyGet fun s => let (a, log) := f s.log; (a, {s with log})
instance : MonadLog JobM := .ofMonadState
instance : MonadError JobM := ELog.monadError
instance : Alternative JobM := ELog.alternative
instance : MonadLift LogIO JobM := ⟨ELogT.takeAndRun⟩
/-- Record that this job is trying to perform some action. -/
@[inline] def updateAction (action : JobAction) : JobM PUnit :=
modify fun s => {s with action := s.action.merge action}
/-- Returns the current job's build trace. -/
@[inline] def getTrace : JobM BuildTrace :=
(·.trace) <$> get
/-- Sets the current job's build trace. -/
@[inline] def setTrace (trace : BuildTrace) : JobM PUnit :=
modify fun s => {s with trace := trace}
/-- Mix a trace into the current job's build trace. -/
@[inline] def addTrace (trace : BuildTrace) : JobM PUnit :=
modify fun s => {s with trace := s.trace.mix trace}
/-- Returns the current job's build trace and removes it from the state. -/
@[inline] def takeTrace : JobM BuildTrace :=
modifyGet fun s => (s.trace, {s with trace := nilTrace})
/-- The monad used to spawn asynchronous Lake build jobs. Lifts into `FetchM`. -/
abbrev SpawnM := FetchT <| ReaderT BuildTrace <| BaseIO
@[inline] def JobM.runSpawnM (x : SpawnM α) : JobM α := fun fn stack store ctx s =>
return .ok (← x fn stack store ctx s.trace) s
instance : MonadLift SpawnM JobM := ⟨JobM.runSpawnM⟩
/-- The monad used to spawn asynchronous Lake build jobs. **Replaced by `SpawnM`.** -/
@[deprecated SpawnM (since := "2024-05-21")] abbrev SchedulerM := SpawnM
/--
Run a `JobM` action in `FetchM`.
Generally, this should not be done, and instead a job action
should be run asynchronously in a Job (e.g., via `Job.async`).
-/
@[inline] def FetchM.runJobM (x : JobM α) : FetchM α := fun fetch stack store ctx log => do
match (← x fetch stack store ctx {log}) with
| .ok a s => return .ok a s.log
| .error e s => return .error e s.log
instance : MonadLift JobM FetchM := ⟨FetchM.runJobM⟩
/-- Ensures that `JobM` lifts into `FetchM`. -/
example : MonadLiftT JobM FetchM := inferInstance
/-- Ensures that `SpawnM` lifts into `FetchM`. -/
example : MonadLiftT SpawnM FetchM := inferInstance
/-- Run a `FetchM` action in `JobM`. -/
@[inline] def JobM.runFetchM (x : FetchM α) : JobM α := fun fetch stack store ctx s => do
match (← x fetch stack store ctx s.log) with
| .ok a log => return .ok a {s with log}
| .error e log => return .error e {s with log}
instance : MonadLift FetchM JobM := ⟨JobM.runFetchM⟩
/-- Ensures that `FetchM` lifts into `JobM`. -/
example : MonadLiftT FetchM JobM := inferInstance
/-- Ensures that `FetchM` lifts into `SpawnM`. -/
example : MonadLiftT SpawnM FetchM := inferInstance
namespace Job
@[inline] def bindTask [Monad m]
(f : JobTask α → m (JobTask β)) (self : Job α)
: m (Job β) := return {self with task := ← f self.task}
/-- Spawn a job that asynchronously performs `act`. -/
@[inline] protected def async
(act : JobM α) (prio := Task.Priority.default)
: SpawnM (Job α) := fun fetch stack store ctx => .ofTask <$> do
BaseIO.asTask (prio := prio) do (withLoggedIO act) fetch stack store ctx {}
/-- Wait a the job to complete and return the result. -/
@[inline] protected def wait (self : Job α) : BaseIO (JobResult α) := do
IO.wait self.task
/--
Wait for a job to complete and return the produced value.
If an error occurred, return `none` and discarded any logs produced.
-/
@[inline] protected def wait? (self : Job α) : BaseIO (Option α) := do
return (← self.wait).result?
/--
Wait for a job to complete and return the produced value.
Logs the job's log and throws if there was an error.
-/
@[inline] protected def await (self : Job α) : LogIO α := do
match (← self.wait) with
| .error n {log, ..} => log.replay; throw n
| .ok a {log, ..} => log.replay; pure a
/-- Apply `f` asynchronously to the job's output. -/
protected def mapM
(self : Job α) (f : α → JobM β)
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) :=
fun fetch stack store ctx trace => do
self.bindTask fun task => do
BaseIO.mapTask (t := task) (prio := prio) (sync := sync) fun
| .ok a s =>
let trace := mixTrace trace s.trace
withLoggedIO (f a) fetch stack store ctx {s with trace}
| .error n s => return .error n s
@[deprecated Job.mapM (since := "2024-12-06")]
protected abbrev bindSync
(self : Job α) (f : α → JobM β)
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) := self.mapM f prio sync
/--
Apply `f` asynchronously to the job's output
and asynchronously await the resulting job.
-/
def bindM
(self : Job α) (f : α → JobM (Job β))
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) :=
fun fetch stack store ctx trace => do
self.bindTask fun task => do
BaseIO.bindTask task (prio := prio) (sync := sync) fun
| .ok a sa => do
let trace := mixTrace trace sa.trace
match (← withLoggedIO (f a) fetch stack store ctx {sa with trace}) with
| .ok job sa =>
return job.task.map (prio := prio) (sync := true) fun
| .ok b sb => .ok b {sa.merge sb with trace := sb.trace}
| .error e sb => .error ⟨sa.log.size + e.val⟩ {sa.merge sb with trace := sb.trace}
| .error e sa => return Task.pure (.error e sa)
| .error e sa => return Task.pure (.error e sa)
@[deprecated bindM (since := "2024-12-06")]
protected abbrev bindAsync
(self : Job α) (f : α → SpawnM (Job β))
(prio := Task.Priority.default) (sync := false)
: SpawnM (Job β) := self.bindM (fun a => f a) prio sync
/--
`a.zipWith f b` produces a new job `c` that applies `f` to the
results of `a` and `b`. The job `c` errors if either `a` or `b` error.
-/
@[inline] def zipResultWith
(f : JobResult α → JobResult β → JobResult γ) (self : Job α) (other : Job β)
(prio := Task.Priority.default) (sync := true)
: Job γ := Job.ofTask $
self.task.bind (prio := prio) (sync := true) fun rx =>
other.task.map (prio := prio) (sync := sync) fun ry =>
f rx ry
/--
`a.zipWith f b` produces a new job `c` that applies `f` to the
results of `a` and `b`. The job `c` errors if either `a` or `b` error.
-/
@[inline] def zipWith
(f : α → β → γ) (self : Job α) (other : Job β)
(prio := Task.Priority.default) (sync := true)
: Job γ :=
self.zipResultWith (other := other) (prio := prio) (sync := sync) fun
| .ok a sa, .ok b sb => .ok (f a b) (sa.merge sb)
| ra, rb => .error 0 (ra.state.merge rb.state)
/-- Merges this job with another, discarding its output and trace. -/
def add (self : Job α) (other : Job β) : Job α :=
self.zipResultWith (other := other) fun
| .ok a sa, .ok _ sb => .ok a {sa.merge sb with trace := sa.trace}
| ra, rb => .error 0 {ra.state.merge rb.state with trace := ra.state.trace}
/-- Merges this job with another, discarding both outputs. -/
def mix (self : Job α) (other : Job β) : Job Unit :=
self.zipWith (fun _ _ => ()) other
/-- Merge a `List` of jobs into one, discarding their outputs. -/
def mixList (jobs : List (Job α)) : Job Unit :=
jobs.foldr (·.mix ·) nil
/-- Merge an `Array` of jobs into one, discarding their outputs. -/
def mixArray (jobs : Array (Job α)) : Job Unit :=
jobs.foldl (·.mix ·) nil
/-- Merge a `List` of jobs into one, collecting their outputs into a `List`. -/
def collectList (jobs : List (Job α)) : Job (List α) :=
jobs.foldr (zipWith List.cons) (pure [])
/-- Merge an `Array` of jobs into one, collecting their outputs into an `Array`. -/
def collectArray (jobs : Array (Job α)) : Job (Array α) :=
jobs.foldl (zipWith Array.push) (pure (Array.mkEmpty jobs.size))
end Job
/-! ## BuildJob (deprecated) -/
/-- A Lake build job. -/
abbrev BuildJob α := Job α
namespace BuildJob
@[inline, deprecated "Obsolete." (since := "2024-12-06")]
def mk (job : Job (α × BuildTrace)) : BuildJob α :=
job.mapOk fun (a, trace) s => .ok a {s with trace}
@[inline, deprecated "Obsolete." (since := "2024-12-06")]
def ofJob (job : Job BuildTrace) : BuildJob Unit :=
job.mapOk fun trace s => .ok () {s with trace}
abbrev toJob (self : BuildJob α) : Job α :=
self
@[deprecated Job.nil (since := "2024-12-06")]
abbrev nil : BuildJob Unit :=
Job.pure ()
@[deprecated Job.map (since := "2024-12-06")]
protected abbrev pure (a : α) : BuildJob α :=
Job.pure a
instance : Pure BuildJob := ⟨Job.pure⟩
@[deprecated Job.map (since := "2024-12-06")]
protected abbrev map (f : α → β) (self : BuildJob α) : BuildJob β :=
self.toJob.map f
instance : Functor BuildJob where
map := Job.map
@[inline, deprecated "Removed without replacement." (since := "2024-12-06")]
def mapWithTrace (f : α → BuildTrace → β × BuildTrace) (self : BuildJob α) : BuildJob β :=
self.toJob.mapOk fun a s =>
let (b, trace) := f a s.trace
.ok b {s with trace}
@[inline, deprecated Job.mapM (since := "2024-12-06")]
protected def bindSync
(self : BuildJob α) (f : α → BuildTrace → JobM (β × BuildTrace))
(prio : Task.Priority := .default) (sync := false)
: SpawnM (Job β) :=
self.toJob.mapM (prio := prio) (sync := sync) fun a => do
let (b, trace) ← f a (← getTrace)
setTrace trace
return b
@[inline, deprecated Job.bindM (since := "2024-12-06")]
protected def bindAsync
(self : BuildJob α) (f : α → BuildTrace → SpawnM (Job β))
(prio : Task.Priority := .default) (sync := false)
: SpawnM (Job β) :=
self.toJob.bindM (prio := prio) (sync := sync) fun a => do
f a (← getTrace)
@[deprecated Job.wait? (since := "2024-12-06")]
protected abbrev wait? (self : BuildJob α) : BaseIO (Option α) :=
self.toJob.wait?
@[deprecated Job.add (since := "2024-12-06")]
abbrev add (self : BuildJob α) (other : BuildJob β) : BuildJob α :=
self.toJob.add other.toJob
@[deprecated Job.mix (since := "2024-12-06")]
abbrev mix (self : BuildJob α) (other : BuildJob β) : BuildJob Unit :=
self.toJob.mix other.toJob
@[deprecated Job.mixList (since := "2024-12-06")]
abbrev mixList (jobs : List (BuildJob α)) : Id (BuildJob Unit) :=
return Job.mixList jobs
@[deprecated Job.mixArray (since := "2024-12-06")]
abbrev mixArray (jobs : Array (BuildJob α)) : Id (BuildJob Unit) :=
return Job.mixArray jobs
@[deprecated Job.zipWith (since := "2024-12-06")]
abbrev zipWith
(f : α → β → γ) (self : BuildJob α) (other : BuildJob β)
: BuildJob γ :=
self.toJob.zipWith f other.toJob
@[deprecated Job.collectList (since := "2024-12-06")]
abbrev collectList (jobs : List (BuildJob α)) : Id (BuildJob (List α)) :=
return Job.collectList jobs
@[deprecated Job.collectArray (since := "2024-12-06")]
abbrev collectArray (jobs : Array (BuildJob α)) : Id (BuildJob (Array α)) :=
return Job.collectArray jobs
attribute [deprecated Job (since := "2024-12-06")] BuildJob

View file

@ -0,0 +1,78 @@
/-
Copyright (c) 2024 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Build.Fetch
/-! # Job Registration -/
namespace Lake
/--
Resets the job state after a checkpoint (e.g., registering the job).
Preserves state that downstream jobs want to depend on while resetting
job-local state that should not be inherited by downstream jobs.
-/
@[inline] def JobState.renew (s : JobState) : JobState where
trace := s.trace
/--
Resets the job's state after a checkpoint (e.g., registering the job).
Preserves information that downstream jobs want to depend on while resetting
job-local information that should not be inherited by downstream jobs.
-/
def Job.renew (self : Job α) : Job α :=
self.mapResult (sync := true) fun
| .ok a s => .ok a s.renew
| .error _ s => .error 0 s.renew
/--
Registers the job for the top-level build monitor,
(e.g., the Lake CLI progress UI), assigning it `caption`.
-/
def registerJob (caption : String) (job : Job α) (optional := false) : FetchM (Job α) := do
let job : Job α := {job with caption, optional}
(← getBuildContext).registeredJobs.modify (·.push job)
return job.renew
/-- Wraps stray I/O, logs, and errors in `x` into the produced job. -/
def ensureJob (x : FetchM (Job α))
: FetchM (Job α) := fun fetch stack store ctx log => do
let iniPos := log.endPos
match (← (withLoggedIO x) fetch stack store ctx log) with
| .ok job log =>
if iniPos < log.endPos then
let (log, jobLog) := log.split iniPos
let job := job.mapResult (sync := true) (·.prependLog jobLog)
return .ok job log
else
return .ok job log
| .error _ log =>
let (log, jobLog) := log.split iniPos
return .ok (.error jobLog) log
/--
Registers the produced job for the top-level build monitor
(e.g., the Lake CLI progress UI), assigning it `caption`.
Stray I/O, logs, and errors produced by `x` will be wrapped into the job.
-/
def withRegisterJob
(caption : String) (x : FetchM (Job α)) (optional := false)
: FetchM (Job α) := do
let job ← ensureJob x
registerJob caption job optional
/--
Registers the produced job for the top-level build monitor
if it is not already (i.e., it has an empty caption).
-/
@[inline] def maybeRegisterJob
(caption : String) (job : Job α)
: FetchM (Job α) := do
if job.caption.isEmpty then
registerJob caption job
else
return job

View file

@ -47,12 +47,12 @@ we equip the monad `m` with a fetch function of its own.
-/
/-- A transformer that equips a monad with a `DFetchFn`. -/
abbrev DFetchT (α : Type u) (β : α → Type v) (m : Type v → Type w) :=
abbrev DFetchFnT (α : Type u) (β : α → Type v) (m : Type v → Type w) :=
EquipT (DFetchFn α β m) m
/-- A `DFetchT` that is not dependently typed. -/
abbrev FetchT (α : Type u) (β : Type v) (m : Type v → Type w) :=
DFetchT α (fun _ => β) m
/-- A `DFetchFnT` that is not dependently typed. -/
abbrev FetchFnT (α : Type u) (β : Type v) (m : Type v → Type w) :=
DFetchFnT α (fun _ => β) m
/-!
We can then use the such a monad as the basis for a fetch function itself.
@ -64,11 +64,11 @@ fetch values. It is thus usually implemented recursively via some variation
of the `recFetch` function below, hence the "rec" in both names.
-/
abbrev DRecFetchFn (α : Type u) (β : α → Type v) (m : Type v → Type w) :=
DFetchFn α β (DFetchT α β m)
DFetchFn α β (DFetchFnT α β m)
/-- A `DRecFetchFn` that is not dependently typed. -/
abbrev RecFetchFn (α : Type u) (β : Type v) (m : Type v → Type w) :=
α → FetchT α β m β
α → FetchFnT α β m β
/-- A `DFetchFn` that provides its base `DRecFetchFn` with itself. -/
@[specialize] partial def recFetch

View file

@ -5,6 +5,7 @@ Authors: Mac Malone
-/
prelude
import Lake.Config.Monad
import Lake.Build.Job
import Lake.CLI.Error
namespace Lake

View file

@ -4,8 +4,8 @@ Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
prelude
import Lake.Build.Job
import Lake.Build.Data
import Lake.Build.Job.Basic
namespace Lake
open Lean System

View file

@ -267,6 +267,7 @@ end MonadLogT
/- A Lake log. An `Array` of log entries. -/
structure Log where
entries : Array LogEntry
deriving Inhabited
instance : ToJson Log := ⟨(toJson ·.entries)⟩
instance : FromJson Log := ⟨(Log.mk <$> fromJson? ·)⟩