lean4-htt/src/Lean/Elab/Task.lean
Joachim Breitner 9efe4283e2
fix: propagate parent cancel token to parallel tactic subtasks (#13428)
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>
2026-04-30 09:22:33 +00:00

136 lines
5.1 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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