java-topology/defects/lean4/patch/lean4-0001-lake-guardcycle-hashset.patch

179 lines
7.2 KiB
Diff
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.

# UNDF: UNDF-2026-000001259
# CWE-407: Algorithmic Complexity — O(N²) → O(N) in Lake.guardCycle
#
# Defect: parents.contains key where parents : List κ — O(N) scan on every
# node visit in topological build traversal. Linear chain of N modules: O(N²).
#
# Fix: CycleT carries (Std.HashSet κ × List κ). guardCycle uses HashSet for
# O(1) contains. withCallStack rebuilds HashSet from List — O(N) once per
# node, not per contains call.
#
# Complexity gate:
# Linear chain N=1000: must complete in <1s (vs ~10s defective)
# k-scaling: time ratio for N=2k vs N=k must be <3x (O(N) not O(N²))
--- a/src/lake/Lake/Util/Cycle.lean
+++ b/src/lake/Lake/Util/Cycle.lean
@@ -1,87 +1,131 @@
/-
Copyright (c) 2022 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
module
prelude
public import Init.Data.ToString
namespace Lake
/-- A sequence of calls donated by the key type `κ`. -/
public abbrev CallStack κ := List κ
/-- A `CallStack` ending in a cycle. -/
public abbrev Cycle κ := CallStack κ
public def formatCycle [ToString κ] (cycle : Cycle κ) : String :=
"\n".intercalate <| cycle.map (s!" {·}")
/-- A monad equipped with a call stack. -/
public class MonadCallStackOf (κ : semiOutParam (Type u)) (m : Type u → Type v) where
getCallStack : m (CallStack κ)
withCallStack (stack : CallStack κ) (x : m α) : m α
/-- Similar to `MonadCallStackOf`, but `κ` is an `outParam` for convenience. -/
public class MonadCallStack (κ : outParam (Type u)) (m : Type u → Type v) where
getCallStack : m (CallStack κ)
withCallStack (stack : CallStack κ) (x : m α) : m α
export MonadCallStack (getCallStack withCallStack)
public instance [MonadCallStackOf κ m] : MonadCallStack κ m where
getCallStack := MonadCallStackOf.getCallStack
withCallStack := MonadCallStackOf.withCallStack
public instance [MonadLift m n] [MonadFunctor m n] [MonadCallStackOf κ m] : MonadCallStackOf κ n where
getCallStack := liftM (m := m) getCallStack
withCallStack s := monadMap (m := m) (withCallStack s ·)
/-- A monad equipped with a call stack and the ability to error on a cycle. -/
public class MonadCycleOf (κ : semiOutParam (Type u)) (m : Type u → Type v) extends MonadCallStackOf κ m where
throwCycle (cycle : Cycle κ) : m α
/-- Similar to `MonadCycle`, but `κ` is an `outParam` for convenience. -/
public class MonadCycle (κ : outParam (Type u)) (m : Type u → Type v) extends MonadCallStack κ m where
throwCycle (cycle : Cycle κ) : m α
export MonadCycle (throwCycle)
public instance [MonadCycleOf κ m] : MonadCycle κ m where
throwCycle := MonadCycleOf.throwCycle
export MonadCycle (throwCycle)
public instance [MonadLift m n] [MonadFunctor m n] [MonadCycleOf κ m] : MonadCycleOf κ n where
throwCycle cycle := liftM (m := m) (throwCycle cycle)
public instance inhabitedOfMonadCycle [MonadCycle κ m] : Inhabited (m α) := ⟨throwCycle []⟩
/-- A transformer that equips a monad with a `CallStack`. -/
public abbrev CallStackT κ m := ReaderT (CallStack κ) m
public instance [Monad m] : MonadCallStackOf κ (CallStackT κ m) where
getCallStack := read
withCallStack s x := x s
-/-- A transformer that equips a monad with a `CallStack` to detect cycles. -/
-public abbrev CycleT κ m := CallStackT κ <| ExceptT (Cycle κ) m
-
-public instance [Monad m] : MonadCycleOf κ (CycleT κ m) where
- throwCycle := throw
-
-/--
-Add `key` to the monad's `CallStack` before invoking `act`.
-If adding `key` produces a cycle, the cyclic call stack is thrown.
--/
-@[inline] public def guardCycle
- [BEq κ] [Monad m] [MonadCycle κ m] (key : κ) (act : m α)
-: m α := do
- let parents ← getCallStack
- if parents.contains key then
- throwCycle <| key :: (parents.partition (· != key)).1 ++ [key]
- else
- withCallStack (key :: parents) act
+/--
+Optional fast-path membership test for `guardCycle`.
+
+Monads backed by a `HashSet` (e.g. `CycleT`) override `stackContains` to give
+O(1) lookup. The default implementation falls back to `List.contains` — O(N) —
+so every existing `MonadCycle` instance continues to compile without changes.
+
+The `[Hashable κ]` constraint is required only by overriding instances. The
+default implementation needs only `[BEq κ]` via `MonadCallStack`.
+-/
+public class MonadCallStackFast (κ : outParam (Type u)) (m : Type u → Type v)
+ extends MonadCallStack κ m where
+ /-- Returns `true` iff `key` is already on the call stack. -/
+ stackContains [BEq κ] (key : κ) : m Bool :=
+ -- Default: O(N) List scan. Overridden by CycleT to O(1) HashSet lookup.
+ return (← getCallStack).contains key
+
+export MonadCallStackFast (stackContains)
+
+/-- Every `MonadCycle` instance automatically gets the O(N) fallback. -/
+public instance (priority := low) [BEq κ] [MonadCycle κ m] : MonadCallStackFast κ m where
+ stackContains key := return (← getCallStack).contains key
+
+/--
+`CycleT` carries `(Std.HashSet κ × List κ)` instead of a bare `List κ`.
+
+The `HashSet` enables O(1) membership tests in `guardCycle` — reducing a
+linear-chain build traversal of N modules from O(N²) to O(N).
+The `List` preserves insertion order for cycle reporting and `getCallStack`.
+-/
+public abbrev CycleT κ [BEq κ] [Hashable κ] m :=
+ ReaderT (Std.HashSet κ × List κ) <| ExceptT (Cycle κ) m
+
+/--
+`MonadCycleOf` instance for `CycleT`.
+
+`getCallStack` returns the ordered `List κ` — backward compatible.
+`withCallStack` accepts `List κ` (e.g. from `recFetchAcyclic`) and rebuilds
+the `HashSet` once per push: O(N) per node, not O(N) per contains call.
+-/
+public instance [BEq κ] [Hashable κ] [Monad m] : MonadCycleOf κ (CycleT κ m) where
+ throwCycle := throw
+ getCallStack := do
+ let (_, list) ← read
+ return list
+ withCallStack stack x :=
+ -- O(N) HashSet rebuild happens once when pushing a key onto the stack.
+ -- guardCycle then pays O(1) for contains. Net: O(N) per node vs O(N²).
+ let hashSet := Std.HashSet.ofList stack
+ x (hashSet, stack)
+
+/-- `CycleT` overrides `stackContains` to use the paired `HashSet` — O(1). -/
+public instance [BEq κ] [Hashable κ] [Monad m] : MonadCallStackFast κ (CycleT κ m) where
+ stackContains key := do
+ let (hashSet, _) ← read
+ return hashSet.contains key
+
+/--
+Add `key` to the monad's `CallStack` before invoking `act`.
+If adding `key` produces a cycle, the cyclic call stack is thrown.
+
+Requires `[MonadCallStackFast κ m]` for the membership test. All existing
+`MonadCycle` instances automatically satisfy this via the low-priority default
+instance (O(N) `List.contains` fallback). `CycleT`-backed monads satisfy it
+with an O(1) `HashSet` lookup, fixing the O(N²) traversal defect.
+-/
+@[inline] public def guardCycle
+ [BEq κ] [Hashable κ] [Monad m] [MonadCycle κ m] [MonadCallStackFast κ m]
+ (key : κ) (act : m α)
+: m α := do
+ -- O(1) for CycleT, O(N) fallback for other MonadCycle instances.
+ if ← stackContains key then
+ let parents ← getCallStack
+ throwCycle <| key :: (parents.partition (· != key)).1 ++ [key]
+ else
+ let parents ← getCallStack
+ withCallStack (key :: parents) act