Zulip Chat Archive

Stream: lean4

Topic: InfoTrees in FrontendM


Adam Topaz (Nov 06 2025 at 20:47):

I have some code which extracts infotrees from an elaborated Lean file and processes them in some way. A minimal version of this code is here:

import Lean

open Lean Elab Frontend

unsafe
def main (args : List String) : IO Unit := do
  let path : System.FilePath := args[0]!
  initSearchPath ( findSysroot)
  enableInitializersExecution
  let inputCtx : Parser.InputContext := Parser.mkInputContext ( IO.FS.readFile path) "<input>"
  let (header, parserState, messages)  Parser.parseHeader inputCtx
  let (env, messages)  processHeader header {} messages inputCtx
  let commandState := { Command.mkState env messages {} with infoState.enabled := true }
  go.run { inputCtx } |>.run' { commandState, parserState, cmdPos := parserState.pos }
where go : Elab.Frontend.FrontendM Unit := do
  processCommands
  println! ( get).commandState.infoState.trees.size

When I run this as lake exe foo Test.lean with the contents of Test.lean being

example (a b : Nat) : a + b = b + a := by
  rw [Nat.add_comm]

I get 2 as the output, which is what I expect, if I run this in leanprover/lean4:v4.24.0. When using leanprover/lean4:v4.25.0-rc2, I end up with 1 (just the elabEoi infotree). I couldn't find anything in the release notes that would indicate this change in behavior, but it's possible I didn't look hard enough. Is this a bug, or is there a different way to obtain the infotrees after elaboration in FrontendM?

Kim Morrison (Nov 07 2025 at 10:20):

Is the change of behavior specific to example (rather than theorem)?

Adam Topaz (Nov 07 2025 at 11:31):

It's not specific to example, the same happens with theorem. What seems to happen in 4.25.0 is that CommandState.infoState.trees is a singleton after every processCommand step, where the infotree is only the one obtained from elaborating the last command. Here's a standalone code block if anyone wants to test:

import Lean

open Lean Elab Frontend

def file : String := "\
theorem foo (a b : Nat) : a + b = b + a := by
  rw [Nat.add_comm]
"

unsafe
def main : IO Unit := do
  initSearchPath ( findSysroot)
  enableInitializersExecution
  let inputCtx : Parser.InputContext := Parser.mkInputContext file "<input>"
  let (header, parserState, messages)  Parser.parseHeader inputCtx
  let (env, messages)  processHeader header {} messages inputCtx
  let commandState := { Command.mkState env messages {} with infoState.enabled := true }
  go.run { inputCtx } |>.run' { commandState, parserState, cmdPos := parserState.pos }
where go : Elab.Frontend.FrontendM Unit := do
  processCommands
  println! ( get).commandState.infoState.trees.size

#eval main

Thomas Murrills (Nov 07 2025 at 20:08):

Aha, it looks like this recent commit which changed elabCommandTopLevel is responsible! It seems that the new behavior is intentional, and not a bug.

perf: reset InfoState.lazyAssignment before each command (#10744)

This PR fixes a performance regression introduced in #10518. More generally, it ensures both message log and info state are per-command, which has been the case in practice ever since the asynchronous language driver was introduced.

Adam Topaz (Nov 07 2025 at 20:30):

Great find! Okay, so it seems that indeed I will have to get the infotree after every invokation of processCommand.

Adam Topaz (Nov 07 2025 at 20:31):

Given this change, it is a bit strange though that InfoState.trees is of type PersistentArray InfoTree and not just InfoTree (or Option InfoTree.

Kim Morrison (Nov 11 2025 at 05:45):

Does a mutual block return multiple InfoTrees?


Last updated: Dec 20 2025 at 21:32 UTC