Zulip Chat Archive

Stream: lean4

Topic: Miscompilation (incorrect code) in new compiler


George Pîrlea (Sep 17 2025 at 14:56):

I've encountered a miscompilation issue in the new compiler (all versions since v4.22.0 up to and including v4.24.0-rc1, as well as 4.25.0-nightly-2025-09-17 manifest it). Basically, the behaviour of a function (Module.getStateBinders) depends on whether a distinct but similar function (Module.getTheoryBinders) has been compiled. I suspect there's some code reuse optimisation going badly wrong.

Is this a known issue? If not, I can report it on GitHub. (I couldn't find anything on the issue tracker, but maybe I missed it.)

Apologies for not being able to minimise this further — it was extracted from a fairly large development. Here's the file: Miscompilation.lean

import Lean
open Lean Parser

namespace Veil

inductive Mutability where
  | mutable
  | immutable
  | inherit
deriving Inhabited, BEq, Hashable, Repr

inductive StateComponentType where
  | simple (t : TSyntax ``Command.structSimpleBinder)
  | complex (binders : TSyntaxArray ``Term.bracketedBinder) (dom : Term)
deriving Inhabited, BEq

structure StateComponent where
  mutability : Mutability
  name       : Name
  type       : StateComponentType
deriving Inhabited, BEq

structure Module where
  name : Name
  signature : Array StateComponent
deriving Inhabited

end Veil

section Util
set_option autoImplicit true
def getSimpleBinderType [Monad m] [MonadError m] (sig : TSyntax `Lean.Parser.Command.structSimpleBinder) : m (TSyntax `term) := do
  match sig with
  | `(Lean.Parser.Command.structSimpleBinder| $_:ident : $tp:term) => pure tp
  | _ => throwError s!"getSimpleBinderType: don't know how to handle {sig}"

def mkArrowStx [Monad m] [MonadQuotation m] [MonadError m] (tps : List Term) (terminator : Option $ TSyntax `term := none) : m (TSyntax `term) := do
  match tps with
  | [] => if let some t := terminator then return t else throwError "empty list of types and no terminator"
  | [a] => match terminator with
    | none => `(term| $a)
    | some t => `(term| $a -> $t)
  | a :: as =>
    let cont  mkArrowStx as terminator
    `(term| $a -> $cont)

def complexBinderToSimpleBinder [Monad m] [MonadQuotation m] [MonadError m] (nm : TSyntax `ident) (br : TSyntaxArray `Lean.Parser.Term.bracketedBinder) (domT : TSyntax `term) : m (TSyntax `Lean.Parser.Command.structSimpleBinder) := do
  let types  br.mapM fun m => match m with
    | `(bracketedBinder| ($_arg:ident : $tp:term)) => return tp
    | _ => throwError "Invalid binder syntax {br}"
  let typeStx  mkArrowStx types.toList domT
  let simple  `(Lean.Parser.Command.structSimpleBinder| $nm:ident : $typeStx)
  return simple
end Util

open Lean Parser Elab Command Term

namespace Veil

instance : ToString Mutability where
  toString
    | Mutability.mutable => "mutable"
    | Mutability.immutable => "immutable"
    | Mutability.inherit => "inherit"

instance : ToString StateComponentType where
  toString
    | StateComponentType.simple t => toString t
    | StateComponentType.complex b d  => s!"{b} : {d}"

instance : ToString StateComponent where
  toString sc := s!"{sc.mutability} {sc.name} {sc.type}"

def StateComponentType.stx [Monad m] [MonadQuotation m] [MonadError m] (sct : StateComponentType) : m (TSyntax `term) := do
  match sct with
  | .simple t => getSimpleBinderType t
  | .complex b d => getSimpleBinderType $  complexBinderToSimpleBinder (mkIdent Name.anonymous) b d

/-- Returns, e.g., `initial_msg : address → address → round → value → Prop` -/
def StateComponent.getSimpleBinder [Monad m] [MonadQuotation m] [MonadError m] (sc : StateComponent) : m (TSyntax ``Command.structSimpleBinder) := do
  match sc.type with
  | .simple t => return t
  | .complex b d => return  complexBinderToSimpleBinder (mkIdent sc.name) b d

def StateComponent.stx {m} [Monad m] [MonadQuotation m] [MonadError m] (sc : StateComponent) : m Syntax := sc.getSimpleBinder
def StateComponent.typeStx [Monad m] [MonadQuotation m] [MonadError m] (sc : StateComponent) : m Term := sc.type.stx

def Module.getTheoryBinders [Monad m] [MonadQuotation m] [MonadError m] (mod : Module) : m (Array (TSyntax `Lean.Parser.Term.bracketedBinder)) := do
  mod.signature.filterMapM fun sc => do
    match sc.mutability with
    | .immutable => return .some $  `(bracketedBinder| ($(mkIdent sc.name) : $( sc.typeStx)))
    | _ => pure .none

def Module.getStateBinders [Monad m] [MonadQuotation m] [MonadError m] (mod : Module) : m (Array (TSyntax `Lean.Parser.Term.bracketedBinder)) := do
  dbg_trace "[INPUT] {mod.signature}"
  let res  mod.signature.filterMapM fun sc => do
    match sc.mutability with
    | .mutable =>
    -- dbg_trace "getStateBinders -> {sc.name} is mutable"
    return .some $  `(bracketedBinder| ($(mkIdent sc.name) : $( sc.typeStx)))
    | _ =>
    -- NOTE 1: uncommenting this line makes `trigger` work correctly
    -- dbg_trace "getStateBinders -> {sc.name} is not mutable"
    pure .none
  dbg_trace "[RESULT] {res}"
  return res

-- NOTE 2: commenting out this function (or the `trace` statement) makes `trigger` work correctly
def interference (mod : Module) : CommandElabM Unit := do
  trace[veil.debug] "theory binders: {← mod.getTheoryBinders}"
  return

end Veil

namespace Veil

open Lean Parser Elab Command
def trigger : CommandElabM Unit := do
  let sig : Array StateComponent := #[
    { mutability := .mutable, name := `leader, «type» := .simple $  `(Command.structSimpleBinder| x : Nat  Bool)},
    { mutability := .mutable, name := `pending, «type» := .simple $  `(Command.structSimpleBinder| pending : Nat  Nat  Bool)}
  ]
  let emptyMod : Module := default
  let mod := { emptyMod with signature := sig}
  trace[debug] "stateBinders: {← mod.getStateBinders}"
  return ()

set_option trace.debug true
#eval trigger
-- incorrect result: `[RESULT] #[]`
-- correct result: (something other than `#[]` gets printed), i.e.

/-
Either of the following actions seem to "fix" the issue:
  - removing the `inherit` constructor from the `Mutability` enum (and from the `ToString Mutability` instance)
  - uncommenting one of the `dbg_trace` statements in `getStateBinders`
  - commenting out the `interference` function (or just the `trace` statement within it)

My (uninformed) guess is somehow the closure passed to `filterMapM` in
`getTheoryBinders` gets confused/conflated with the one in `getStateBinders`.
-/

#version
-- This manifests with:
-- Lean 4.25.0-nightly-2025-09-17 / Target: x86_64-unknown-linux-gnu
-- Lean 4.24.0-rc1 / Target: arm64-apple-darwin23.6.0 macOS
-- Lean 4.23.0 / Target: arm64-apple-darwin23.6.0 macOS
-- Lean 4.22.0 / Target: arm64-apple-darwin23.6.0 macOS
-- Does NOT manifest with:
-- Lean 4.21.0 / Target: arm64-apple-darwin23.6.0 macOS

end Veil

Sebastian Ullrich (Sep 17 2025 at 15:35):

Thank you for the comprehensive report, this is indeed an incorrect reuse of a specialization result. We are looking into it.

Henrik Böving (Sep 17 2025 at 17:34):

fixed in lean4#10429 @George Pîrlea

George Pîrlea (Sep 17 2025 at 23:24):

Thank you very much for the quick fix!

George Pîrlea (Sep 22 2025 at 11:51):

@Sebastian Ullrich @Henrik Böving Would it be possible to push this into an v4.24.0 RC release?

George Pîrlea (Sep 22 2025 at 12:02):

For context: I worked around the specific instance I reported, but I keep running into strange behaviour that I suspect is due to the same issue. It seems quite easy to trigger.

Kim Morrison (Sep 22 2025 at 12:12):

@George Pîrlea, I'm happy to put this on the releases/v4.24.0 branch, which means that if we cut a new release candidate it will arrive there, and it will end up in the stable release v4.24.0, but it's substantial work for make a new release candidate. Can you just live on the nightly toolchains in the meantime?

George Pîrlea (Sep 22 2025 at 12:19):

I can wait for the next RC. Thank you!

I'm not on nightly, since we indirectly depend on mathlib, but I can keep working around the issue when I encounter it (by adding dbg_trace to my code). It's annoying, but not a show-stopper.


Last updated: Dec 20 2025 at 21:32 UTC