Repository navigation
Expand file tree
/
Copy pathExtractLakefile.lean
More file actions
167 lines (142 loc) · 7.01 KB
/
Copy pathExtractLakefile.lean
File metadata and controls
167 lines (142 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: David Thrane Christiansen
-/
import Lake
import Lake.Load.Lean
import Lake.Load.Package
import SubVerso.Compat
import SubVerso.Highlighting.Code
import SubVerso.Module
import Manual.Meta.LakeToml.PackageTest
/-!
# Lean-format Lakefile Extractor
This executable elaborates a Lean-format `lakefile.lean` and emits, as JSON:
* `package`: the elaborated `Lake.Package`, formatted with the same `Manual.Toml.Test` instances
used to render the “expected elaboration result” for TOML lakefiles. This is the analog of what
`checkTomlPackage` produces for a `lakefile.toml`, so that the `lakeLean` directive can validate
it against an `expected` block exactly as `lakeToml` does.
* `module`: the SubVerso highlighted representation of the lakefile source, so that the `lakeLean`
directive can render the configuration with full syntax highlighting and hover information.
The implementation is based on `leanprover/verso-slides#21`: a Lean-format lakefile cannot be
elaborated by the ordinary module extractor, because it imports `Lake` and uses the package
configuration DSL (`package`, `lean_lib`, `lean_exe`, …). It must therefore be elaborated by a
process that statically links Lake's DSL, which is what this executable does.
-/
open SubVerso
open SubVerso.Highlighting (Highlighted highlightFrontendResult)
open SubVerso.Module (ModuleItem)
open Lean Elab System
open Lean.Elab.Command hiding Context
open Lean.Elab.Frontend
/-- Returns the node kind of the command, skipping outer `in` nodes. -/
partial def commandKind (cmd : Syntax) : SyntaxNodeKind :=
match cmd with
| `(command|$_cmd1 in $cmd2) => commandKind cmd2
| _ => cmd.getKind
/--
Reconstruct a `Lake.Package` from the environment produced by elaborating a configuration file.
Mirrors `Lake.loadLeanConfig`, but operates on an already-elaborated environment so that the same
elaboration can also be highlighted.
-/
def extractPackage (env : Environment) (opts : Options := {}) : Lake.LogIO Lake.Package := do
let fileCfg ← Lake.LakefileConfig.loadFromEnv env opts
let .ok lakeEnv ← (Lake.Env.compute {home := ""} {sysroot := ""} none none).toBaseIO
| Lake.error "Failed to compute a Lake environment"
-- The recorded package directory and configuration file are canonical rather than the actual
-- (temporary) location of the example, so that the elaboration result is deterministic.
let loadCfg : Lake.LoadConfig := {
lakeEnv, wsDir := ".", pkgDir := ".",
relConfigFile := "lakefile.lean", configFile := "lakefile.lean"
}
return Lake.mkPackage loadCfg fileCfg
/--
Elaborate the configuration file at `configFile`, returning both the elaborated `Lake.Package`
and the SubVerso highlighting of its source.
-/
unsafe def extract (realConfigFile : FilePath) :
IO (Lake.Package × SubVerso.Module.Module) := do
initSearchPath (← findSysroot)
-- The recorded package directory and configuration file are canonical rather than the actual
-- (temporary) location of the example, so that the elaboration result is deterministic.
let pkgDir : FilePath := "."
let relConfigFile : FilePath := "lakefile.lean"
let contents ← IO.FS.readFile realConfigFile
let fm := FileMap.ofString contents
let ictx := Parser.mkInputContext contents relConfigFile.toString
let (headerStx, parserState, msgs) ← Parser.parseHeader ictx
let imports := headerToImports headerStx
-- Lake's DSL macros and attributes are registered via initializers, so they must run when the
-- configuration's imports are loaded.
enableInitializersExecution
let env ← importModules imports {} (trustLevel := 1024) (loadExts := true)
-- Configure the Lake DSL environment extensions exactly as `Lake.elabConfigFile` does, so that
-- the package's name and directory defaults resolve the way Lake would resolve them.
let env := env.setMainModule Lake.configModuleName
let env := Lake.nameExt.setState env ⟨0, .anonymous⟩
let env := Lake.dirExt.setState env (some pkgDir)
let env := Lake.optsExt.setState env (some {})
let pctx : Context := { inputCtx := ictx }
let commandState : Command.State := { env, maxRecDepth := defaultMaxRecDepth, messages := msgs }
-- `pp.tagAppFns` makes the highlighter associate constants with their applications.
let scopes :=
let sc := commandState.scopes[0]!
{ sc with opts := sc.opts.setBool `pp.tagAppFns true } :: commandState.scopes.tail!
let commandState := { commandState with scopes }
let cmdSt ← IO.mkRef { commandState, parserState, cmdPos := parserState.pos }
let res ← Compat.Frontend.processCommands headerStx pctx cmdSt
let finalState ← cmdSt.get
let finalEnv := finalState.commandState.env
if finalState.commandState.messages.hasErrors then
let errs ← finalState.commandState.messages.toList.filterMapM fun m => do
if m.severity == .error then pure (some (← m.toString)) else pure none
throw <| .userError s!"{realConfigFile}: configuration has errors:\n{"\n".intercalate errs}"
-- Extract the elaborated package configuration.
let some pkg ← (extractPackage finalEnv).toBaseIO
| throw <| .userError s!"{realConfigFile}: failed to load package configuration"
-- Highlight the source.
let res := res.updateLeading contents
let hls ← (Frontend.runCommandElabM <| liftTermElabM <|
highlightFrontendResult res (suppressNamespaces := [])) pctx cmdSt
let items : Array ModuleItem := hls.zip res.syntax |>.map fun (hl, stx) => {
defines := hl.definedNames.toArray
kind := commandKind stx
range := stx.getRange?.map fun ⟨s, e⟩ => (fm.toPosition s, fm.toPosition e)
code := hl
}
return (pkg, SubVerso.Module.Module.mk items)
structure Args where
lakefile : String
jsonFile : String
pkgDir? : Option String := none
def processArgs (args : List String) : IO Args := do
let rec go (pkgDir? : Option String) (positional : Array String) :
List String → IO Args
| "--pkg-dir" :: dir :: more => go (some dir) positional more
| arg :: more => go pkgDir? (positional.push arg) more
| [] =>
match positional.toList with
| [lakefile, jsonFile] => pure { lakefile, jsonFile, pkgDir? }
| other => throw <| .userError s!"\
Expected: [--pkg-dir DIR] LAKEFILE OUT\n\
where LAKEFILE is the path to a Lean-format lakefile and OUT is the destination of \
the JSON.\n\
Got positional arguments: {other}"
go none #[] args
unsafe def main (args : List String) : IO UInt32 := do
try
let { lakefile, jsonFile, .. } ← processArgs args
let (pkg, mod) ← extract lakefile
let package := (Manual.Toml.Test.toString pkg).pretty ++ "\n"
let out := Json.mkObj [
("package", Json.str package),
("module", mod.toJson)
]
if let some parent := (jsonFile : FilePath).parent then
IO.FS.createDirAll parent
IO.FS.writeFile jsonFile (toString out)
return (0 : UInt32)
catch e =>
IO.eprintln s!"extract-lakefile: {toString e}"
return (1 : UInt32)