| /- | |
| Copyright (c) 2025 Lean FRO. All rights reserved. | |
| Released under Apache 2.0 license as described in the file LICENSE. | |
| Authors: Sebastian Ullrich | |
| -/ | |
| module | |
| import Lean.CoreM | |
| import Lean.Util.ForEachExpr | |
| import all Lean.Util.Path | |
| import all Lean.Environment | |
| import Lean.Compiler.Options | |
| import Lean.Compiler.IR.CompilerM | |
| import Lean.Compiler.ModPkgExt | |
| import all Lean.Compiler.CSimpAttr | |
| import Lean.Compiler.LCNF.EmitC | |
| import Lean.Language.Lean | |
| import Lean.Compiler.LCNF.PhaseExt | |
| import Lean.Compiler.LCNF.Main | |
| -- `leanir` uses `lean_initialize_minimal`, which leaves initializing the `Lean` | |
| -- modules to this module's import closure. These are the modules defining an `@[export]` whose | |
| -- `@[extern]` counterpart is in that closure but which the closure does not otherwise reach. | |
| import Lean.Meta.ExprDefEq | |
| import Lean.Meta.LevelDefEq | |
| import Lean.Meta.Match.MatchEqs | |
| import Lean.Elab.PreDefinition.Structural.Eqns | |
| import Lean.Parser | |
| /-! Lean codegen as a separate process. -/ | |
| open Lean Compiler LCNF | |
| /-- Leaner alternative to `.ir` for non-`import all` compilation. -/ | |
| def mkIRSigData (env : Environment) : IO ModuleData := do | |
| let data ← mkModuleData env .exported | |
| return { data with | |
| extraConstNames := getIRExtraConstNames env .exported | |
| } | |
| def mkIRData (env : Environment) : IO ModuleData := do | |
| -- TODO: should we use a more specific/efficient data format for IR? | |
| -- `exportIREntries` provides full IR for `declMapExt` and `regularInitAttr`; filter them from | |
| -- `mkModuleData` to prevent the latter's opaque-extern entries from overwriting the full IR | |
| -- in `setImportedEntries` | |
| let irEntries := exportIREntries env | |
| let irExtNames := irEntries.map (·.1) | |
| let modEntries := (← mkModuleData env .private).entries.filter (!irExtNames.contains ·.1) | |
| return { env.header with | |
| entries := irEntries ++ modEntries | |
| constants := default | |
| constNames := default | |
| -- make sure to include all names in case only `.ir` is loaded | |
| -- TODO: `.private` because `import all` may require otherwise unreachable IR entries | |
| extraConstNames := getIRExtraConstNames env .private (includeDecls := true) | |
| } | |
| def setConfigOption (opts : Options) (arg : String) : IO Options := do | |
| let some arg := arg.dropPrefix? "-D" | | |
| throw <| .userError s!"invalid trailing argument `{arg}`, expected argument of the form `-Dopt=val`" | |
| let pos := arg.find '=' | |
| if h : pos.IsAtEnd then | |
| throw <| .userError "invalid -D parameter, argument must contain '='" | |
| else | |
| let name := arg.sliceTo pos |>.toName | |
| let val := arg.sliceFrom (pos.next h) |>.copy | |
| if let some decl := (← getOptionDecls).find? name then | |
| Language.Lean.setOption opts decl name val | |
| else | |
| throw <| .userError s!"unknown option '{name}'" | |
| public def main (args : List String) : IO UInt32 := do | |
| let setupFile::irFile::c::optArgs := args | do | |
| IO.println s!"usage: leanir <setup.json> <output.ir> <output.c> [--stat] <-Dopt=val>..." | |
| return 1 | |
| let setup ← ModuleSetup.load setupFile | |
| let modName := setup.name | |
| let mut printStats := false | |
| let mut opts := setup.options.toOptions | |
| for optArg in optArgs do | |
| if optArg == "--stat" then | |
| printStats := true | |
| else | |
| opts ← setConfigOption opts optArg | |
| opts := Compiler.compiler.inLeanIR.set opts true | |
| opts := maxHeartbeats.set opts 0 | |
| initSearchPathInternal | |
| -- Provide access to private scope of target module but no others; provide all IR | |
| let env ← profileitIO "import" opts <| withImporting do | |
| -- `importAll` so we have access to all private data | |
| let imports := #[{ module := modName, importAll := true : Import }] | |
| let (_, s) ← importModulesCore (globalLevel := .exported) (loadIRSig := true) | |
| (arts := setup.importArts) imports |>.run | |
| let s := { s with moduleNameMap := s.moduleNameMap.modify modName fun m => { m with irPhases := .runtime } } | |
| -- level exported because otherwise we would try to load the current module's `.ir` | |
| finalizeImport (leakEnv := true) (loadExts := false) (level := .exported) (loadIRSig := true) s imports opts | |
| let env := env.setMainModule modName | |
| let env := env.setModulePackage setup.package? | |
| let initExt {α β σ} [Inhabited σ] (ext : PersistentEnvExtension α β σ) (env : Environment) : IO Environment := do | |
| let s := ext.toEnvExtension.getState env | |
| let newState ← ext.addImportedFn s.importedEntries { env := env, opts := {} } | |
| -- unlogged: nothing has been recorded against this environment yet | |
| return ext.toEnvExtension.setState (asyncMode := .sync) (log := .unlogged) env { s with state := newState } | |
| let env ← initExt Lean.Compiler.CSimp.ext.ext env | |
| let env ← initExt Meta.instanceExtension.ext env | |
| let env ← initExt classExtension env | |
| let env ← initExt Meta.Match.Extension.extension env | |
| let some modIdx := env.getModuleIdx? modName | |
| | throw <| IO.userError s!"module '{modName}' not found" | |
| let decls := impureSigExt.getModuleEntries env modIdx | |
| let decls := decls.filter (isExtern env ·.name) | |
| let env := decls.foldl (fun env decl => impureSigExt.addEntry env decl) env | |
| let env := decls.foldl (fun env decl => setDeclPublic env decl.name) env | |
| -- Fill `declMapExt` with functions compiled already in `lean` so the set of "local" decls is | |
| -- unchanged and also for calculation of `extraConstNames` above | |
| -- TODO: we do manually-added externs only as others need more state sync around ground exprs etc | |
| let is := Lean.IR.declMapExt.toEnvExtension.getState env | |
| let unbox : Name → Name | |
| | .str f "_boxed" => f | |
| | f => f | |
| let newState := is.importedEntries[modIdx]!.foldl (fun (decls, m) d => if isExtern env (unbox d.name) then (d::decls, m.insert d.name d) else (decls, m)) is.state | |
| let env := Lean.IR.declMapExt.toEnvExtension.setState (asyncMode := .sync) env { is with state := newState } | |
| let some mod := env.header.moduleData[modIdx]? | unreachable! | |
| -- Make sure we record the actual IR dependencies, not ourselves | |
| let env := { env with base.private.header.imports := mod.imports } | |
| let _ : MonadExceptOf _ CoreM := MonadAlwaysExcept.except | |
| let res? ← EIO.toBaseIO <| Core.CoreM.run (ctx := { fileName := irFile, fileMap := default, options := opts }) | |
| (s := { env }) try | |
| let decls := postponedCompileDeclsExt.getModuleEntries env modIdx | |
| -- Load postponed decls and iterate over them, removing each entry to avoid infinite recursion. | |
| -- NOTE: `decls` is in the original order of declarations so initializing and managing the | |
| -- extension would not be necessary for that, we could just leave it empty. However, there are | |
| -- cases like a `[csimp]` replacement being defined below the original function in the same file | |
| -- where allowing `LCNF.main` to reorder compilation based on dependencies seems to be the | |
| -- simpler solution (as we may make the csimp replacement before the replacement has been | |
| -- compiled). | |
| modifyEnv (postponedCompileDeclsExt.setState · (decls.foldl (fun s e => e.declNames.foldl (·.insert · e) s) {})) | |
| for decl in decls do | |
| for decl in decl.declNames do | |
| try | |
| resumeCompilation decl (← getOptions) | |
| finally | |
| addTraceAsMessages | |
| for msg in (← Core.getAndEmptyMessageLog).unreported do | |
| IO.eprintln (← msg.toString) | |
| catch e => | |
| unless e.isInterrupt do | |
| logError e.toMessageData | |
| let .ok (_, s) := res? | unreachable! | |
| let mut env := s.env | |
| for msg in s.messages.unreported do | |
| IO.eprintln (← msg.toString) | |
| if s.messages.hasErrors then | |
| return 1 | |
| -- Save, basing `.ir` on top of `.ir.sig` | |
| let irSigFile := (irFile : System.FilePath).addExtension "sig" | |
| saveModuleDataParts (env.mainModule ++ `ir) #[ | |
| (irSigFile, ← mkIRSigData env), | |
| (irFile, ← mkIRData env)] | |
| let .ok out ← IO.FS.Handle.mk c .write |>.toBaseIO | |
| | IO.eprintln s!"failed to create '{c}'" | |
| return 1 | |
| profileitIO "C code generation" opts do | |
| let data ← Compiler.LCNF.emitC modName | |
| |>.toIO' { fileName := irFile, fileMap := default } { env } | |
| out.write data.toUTF8 | |
| displayCumulativeProfilingTimes | |
| if printStats then | |
| env.displayStats | |
| return 0 |