This PR fixes parallel tactic combinators (`attempt_all_par`, `first_par`) leaking their subtasks when the server cancels elaboration on re-elaboration. Subtasks spawned via `CoreM.asTask` (and its `MetaM`/`TermElabM`/`TacticM` variants) get a fresh `IO.CancelToken`, which previously had no link to the parent token; `cancelRec` would set the command-level token but the children kept running. The fix is one line in `CoreM.asTask`: when a parent token is in scope, register `cancelToken.set` as an `onSet` callback on the parent. Server-level cancellation now flows down to every parallel subtask, and `Core.checkInterrupted` inside the child sees the token set as expected. Fixes #13300. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
5.1 KiB
Text
136 lines
5.1 KiB
Text
/-
|
||
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
|
||
Released under Apache 2.0 license as described in the file LICENSE.
|
||
Authors: Kim Morrison
|
||
-/
|
||
module
|
||
|
||
prelude
|
||
public import Lean.Elab.Tactic.Basic
|
||
|
||
/-!
|
||
# Task creation utilities for Lean's tactic monads.
|
||
|
||
This file provides `asTask` and `asTask'` functions for various monads
|
||
(`CoreM`, `MetaM`, `TermElabM`, `TacticM`), which convert monadic computations
|
||
into tasks that run in parallel while preserving state.
|
||
|
||
Each `asTask` function returns:
|
||
* a cancellation hook and
|
||
* a `Task` containing a monadic value with the cached result and updated state
|
||
|
||
The `asTask'` variants omit the cancellation hook for convenience.
|
||
|
||
Note: Calling `IO.cancel` on `t.map f` does not cancel `t`,
|
||
so these functions are careful to construct cancellation hooks
|
||
connected to the underlying task rather than various maps of it.
|
||
-/
|
||
|
||
public section
|
||
|
||
namespace Lean.Core.CoreM
|
||
|
||
/--
|
||
Given a monadic value in `CoreM`, creates a task that runs it in the current state,
|
||
returning
|
||
* a cancellation hook and
|
||
* a monadic value with the cached result (and subsequent state as it was after running).
|
||
|
||
The task is run with a fresh `CancelToken` in its context, so it can detect cancellation
|
||
via `Core.checkInterrupted`. The cancellation hook sets this token.
|
||
|
||
Uses `Core.wrapAsync` internally to properly handle name generators and heartbeats.
|
||
|
||
Note: We only set the cancel token and don't call `IO.cancel task`. We're uncertain whether
|
||
`IO.cancel` is also necessary - it may be required for tasks that use `IO.checkCanceled`
|
||
instead of `Core.checkInterrupted`.
|
||
-/
|
||
def asTask (t : CoreM α) : CoreM (BaseIO Unit × Task (CoreM α)) := do
|
||
let cancelToken ← IO.CancelToken.new
|
||
-- Use wrapAsync to properly handle name generators and heartbeats,
|
||
-- but modify it to return both the result and state
|
||
let wrappedAct ← Core.wrapAsync (fun () => do let a ← t; let s ← get; return (a, s)) (some cancelToken)
|
||
let task ← (wrappedAct ()).asTask
|
||
-- Propagate parent cancel token to child: when the parent is cancelled,
|
||
-- the child token is set too. This ensures that subtasks spawned via `asTask`
|
||
-- respond to server-level cancellation (e.g. on re-elaboration).
|
||
if let some parentTk := (← read).cancelTk? then
|
||
parentTk.onSet cancelToken.set
|
||
return (cancelToken.set, task.map (sync := true) fun result =>
|
||
match result with
|
||
| .ok (a, s) => do
|
||
-- Set state to the task's state (not merging)
|
||
set s
|
||
return a
|
||
| .error e => throw e)
|
||
|
||
/--
|
||
Given a monadic value in `CoreM`, creates a task that runs it in the current state,
|
||
returning a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask' (t : CoreM α) : CoreM (Task (CoreM α)) := (·.2) <$> asTask t
|
||
|
||
end Lean.Core.CoreM
|
||
|
||
namespace Lean.Meta.MetaM
|
||
|
||
/--
|
||
Given a monadic value in `MetaM`, creates a task that runs it in the current state,
|
||
returning
|
||
* a cancellation hook and
|
||
* a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask (t : MetaM α) : MetaM (BaseIO Unit × Task (MetaM α)) := do
|
||
let (cancel, task) ← (t.run (← read) (← get)).asTask
|
||
return (cancel, task.map (sync := true)
|
||
fun c : CoreM (α × Meta.State) => do let (a, s) ← c; set s; pure a)
|
||
|
||
/--
|
||
Given a monadic value in `MetaM`, creates a task that runs it in the current state,
|
||
returning a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask' (t : MetaM α) : MetaM (Task (MetaM α)) := (·.2) <$> asTask t
|
||
|
||
end Lean.Meta.MetaM
|
||
|
||
namespace Lean.Elab.Term.TermElabM
|
||
|
||
/--
|
||
Given a monadic value in `TermElabM`, creates a task that runs it in the current state,
|
||
returning
|
||
* a cancellation hook and
|
||
* a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask (t : TermElabM α) : TermElabM (BaseIO Unit × Task (TermElabM α)) := do
|
||
let (cancel, task) ← (t.run (← read) (← get)).asTask
|
||
return (cancel, task.map (sync := true)
|
||
fun c : MetaM (α × Term.State) => do let (a, s) ← c; set s; pure a)
|
||
|
||
/--
|
||
Given a monadic value in `TermElabM`, creates a task that runs it in the current state,
|
||
returning a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask' (t : TermElabM α) : TermElabM (Task (TermElabM α)) := (·.2) <$> asTask t
|
||
|
||
end Lean.Elab.Term.TermElabM
|
||
|
||
namespace Lean.Elab.Tactic.TacticM
|
||
|
||
/--
|
||
Given a monadic value in `TacticM`, creates a task that runs it in the current state,
|
||
returning
|
||
* a cancellation hook and
|
||
* a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask (t : TacticM α) : TacticM (BaseIO Unit × Task (TacticM α)) := do
|
||
let (cancel, task) ← (t (← read) |>.run (← get)).asTask
|
||
return (cancel, task.map (prio := .max)
|
||
fun c : TermElabM (α × Tactic.State) => do let (a, s) ← c; set s; pure a)
|
||
|
||
/--
|
||
Given a monadic value in `TacticM`, creates a task that runs it in the current state,
|
||
returning a monadic value with the cached result (and subsequent state as it was after running).
|
||
-/
|
||
def asTask' (t : TacticM α) : TacticM (Task (TacticM α)) := (·.2) <$> asTask t
|
||
|
||
end Lean.Elab.Tactic.TacticM
|