This repository was archived by the owner on Oct 20, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathMain.lean
More file actions
374 lines (325 loc) · 12.8 KB
/
Copy pathMain.lean
File metadata and controls
374 lines (325 loc) · 12.8 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Henrik Böving
-/
import Lean
import Comparator
import Export.Parse
namespace Comparator
structure Context where
projectDir : System.FilePath
challengeModule : Lean.Name
solutionModule : Lean.Name
theoremNames : Array Lean.Name
definitionNames : Array Lean.Name
legalAxioms : Array Lean.Name
leanPrefix : System.FilePath
gitLocation : System.FilePath
whichLandrun : String
whichLean4Export : String
externalKernels : (Std.TreeMap String (Array String))
abbrev M := ReaderT Context IO
structure LandrunArgs where
cmd : String
args : Array String
envPass : Array String
envOverride : Array (String × Option String) := #[]
readablePaths : Array System.FilePath
writablePaths : Array System.FilePath
executablePaths : Array System.FilePath
@[inline]
def getExternalKernels : M (Std.TreeMap String (Array String)) := do return (← read).externalKernels
@[inline]
def getTheoremNames : M (Array Lean.Name) := do return (← read).theoremNames
@[inline]
def getDefinitionNames : M (Array Lean.Name) := do return (← read).definitionNames
@[inline]
def getProjectDir : M System.FilePath := do return (← read).projectDir
@[inline]
def getChallengeModule : M Lean.Name := do return (← read).challengeModule
@[inline]
def getSolutionModule : M Lean.Name := do return (← read).solutionModule
@[inline]
def getLegalAxioms : M (Array Lean.Name) := do return (← read).legalAxioms
@[inline]
def getLeanPrefix : M System.FilePath := do return (← read).leanPrefix
@[inline]
def getGitLocation : M System.FilePath := do return (← read).gitLocation
def queryGitLocation : IO System.FilePath := do
let out ← IO.Process.run {
cmd := "which",
args := #["git"],
stdout := .piped,
}
return out.trimAscii.toString
def queryLeanPrefix (projectDir : System.FilePath) : IO System.FilePath := do
let out ← IO.Process.run {
cmd := "lean",
args := #["--print-prefix"],
stdout := .piped,
cwd := projectDir
}
return out.trimAscii.toString
def buildLandrunArgs (spawnArgs : LandrunArgs) : Array String :=
let args := #["--best-effort", "--ro", "/", "--rw", "/dev", "-ldd", "-add-exec"]
let args := spawnArgs.envPass.foldl (init := args) (fun acc env => acc ++ #["--env", env])
let args := spawnArgs.readablePaths.foldl (init := args) (fun acc path => acc ++ #["--ro", path.toString])
let args := spawnArgs.writablePaths.foldl (init := args) (fun acc path => acc ++ #["--rwx", path.toString])
let args := spawnArgs.executablePaths.foldl (init := args) (fun acc path => acc ++ #["--rox", path.toString])
args ++ #["--", spawnArgs.cmd] ++ spawnArgs.args
def runSandBoxedWithStdout (spawnArgs : LandrunArgs) : M String := do
let args := buildLandrunArgs spawnArgs
let { stdout, stderr, exitCode } ← IO.Process.output {
cmd := (← read).whichLandrun,
args,
env := spawnArgs.envOverride
cwd := (← getProjectDir)
}
IO.eprint stderr
if exitCode != 0 then
throw <| .userError s!"Child exited with {exitCode}"
return stdout
def runSandBoxed (spawnArgs : LandrunArgs) : M Unit := do
let args := buildLandrunArgs spawnArgs
let proc ← IO.Process.spawn {
cmd := (← read).whichLandrun,
args,
env := spawnArgs.envOverride
cwd := (← getProjectDir)
}
let ret ← proc.wait
if ret != 0 then
throw <| .userError s!"Child exited with {ret}"
def safeLakeBuild (target : Lean.Name) : M Unit := do
IO.println s!"Building {target}"
let leanPrefix ← getLeanPrefix
let projectDir ← getProjectDir
let dotLakeDir := projectDir / ".lake"
let gitLocation ← getGitLocation
if !(← System.FilePath.pathExists dotLakeDir) then
IO.FS.createDir dotLakeDir
runSandBoxed {
cmd := "lake",
args := #["build", target.toString],
envPass := #["PATH", "HOME", "LEAN_ABORT_ON_PANIC"]
envOverride := #[("LEAN_ABORT_ON_PANIC", some "1")]
readablePaths := #[projectDir]
writablePaths := #[dotLakeDir]
executablePaths := #[leanPrefix, gitLocation]
}
def safeExport (module : Lean.Name) (decls : Array Lean.Name) : M String := do
IO.println s!"Exporting {decls} from {module}"
let baseArgs := #[module.toString, "--"]
let args := decls.foldl (·.push <| ·.toString) baseArgs
let leanPrefix ← getLeanPrefix
let projectDir ← getProjectDir
let dotLakeDir := projectDir / ".lake"
runSandBoxedWithStdout {
cmd := (← read).whichLean4Export
args := args,
envPass := #["PATH", "HOME", "LEAN_PATH", "LEAN_ABORT_ON_PANIC"]
envOverride := #[("LEAN_ABORT_ON_PANIC", some "1")]
readablePaths := #[projectDir, dotLakeDir]
writablePaths := #[]
executablePaths := #[leanPrefix]
}
def runExternalKernel (kernelName : String) (kernelCommand : Array String)
(solutionExport : String) : M (Option String) := do
IO.println s!"Running {kernelName} kernel on solution"
-- just always put out a nanoda-like config file for now
IO.FS.withTempFile fun configHandle configPath => do
IO.FS.withTempFile fun solutionHandle solutionPath => do
let legalAxioms ← getLegalAxioms
configHandle.putStr <| Lean.Json.compress <| Lean.Json.mkObj [
("use_stdin", false),
("export_file_path", solutionPath.toString),
("permitted_axioms", .arr <| legalAxioms.map (.str ∘ Lean.Name.toString)),
("unpermitted_axiom_hard_error", true),
("num_threads", 4),
("nat_extension", true),
("string_extension", true),
]
configHandle.flush
solutionHandle.putStr solutionExport
solutionHandle.flush
let mut kernelArgs := kernelCommand[1...*].toArray
if isNanodaKernel kernelName then
kernelArgs := kernelArgs.push configPath.toString
else
kernelArgs := kernelArgs.push solutionPath.toString
let spawnArgs := {
cmd := kernelCommand[0]!,
args := kernelArgs,
envPass := #[]
readablePaths := #[configPath.toString, solutionPath.toString]
writablePaths := #[]
executablePaths := #[]
}
let args := buildLandrunArgs spawnArgs
try
let proc ← IO.Process.spawn {
cmd := (← read).whichLandrun,
args,
env := spawnArgs.envOverride
cwd := (← getProjectDir)
}
let ret ← proc.wait
if ret != 0 then
IO.println s!"{kernelName} kernel rejected the solution"
return some s!"{kernelName} exited with {ret}"
else
IO.println s!"{kernelName} kernel accepts the solution"
return none
catch e => do
IO.println s!"Error while interacting with {kernelName} kernel"
return some s!"Error while interacting with {kernelName} kernel: {e.toString}"
where
isNanodaKernel (kernelName : String) : Bool :=
-- TODO: get rid of this heuristic
kernelName.contains "noda"
def runBuiltinKernel (solution : Export.ExportedEnv) : M (Option String) := do
IO.println "Running Lean default kernel on solution."
let env ← Lean.mkEmptyEnvironment
let mut kernelEnv := env.toKernelEnv
let origConstMap := solution.constMap
-- Lean's kernel interprets just the addition of `Quot as adding all of these so adding them
-- multiple times leads to errors.
let quotTargets := [`Quot.mk, `Quot.lift, `Quot.ind]
let kernelConstMap := quotTargets.foldl (init := origConstMap) (·.erase ·)
try
kernelEnv ← kernelEnv.replay kernelConstMap
IO.println "Lean default kernel accepts the solution"
catch e =>
IO.println "Lean default kernel rejects the solution"
return some e.toString
try
let verifyTargets := `Quot :: quotTargets
for quotTarget in verifyTargets do
if let some info := origConstMap[quotTarget]? then
let some info' := kernelEnv.find? quotTarget |
throw <| .userError s!"Could not find quotient constant in final kernel env: {quotTarget}"
if info != info' then
throw <| .userError s!"Quotient constant mismatch on: {quotTarget}"
return none
catch e =>
IO.println "Quotient post-check rejects the solution"
return some e.toString
def primitiveTargets : M (Array Lean.Name) := do
-- The challenge needs to have all the built-in constants of the kernel, as the
-- kernel makes no guarantees when fed other definitions here.
-- List from `git grep new_persistent_expr_const src/kernel/`
return #[
-- ``Nat.zero,
-- ``Nat.succ,
``Nat.add,
``Nat.sub,
``Nat.mul,
``Nat.pow,
``Nat.gcd,
``Nat.div,
``Nat.mod,
``Nat.beq,
``Nat.ble,
``Nat.land,
``Nat.lor,
``Nat.xor,
``Nat.shiftLeft,
``Nat.shiftRight,
``String.ofList,
``Char.ofNat,
``List,
``eagerReduce,
``Nat,
``String,
``String.mk,
``Char,
``optParam,
``autoParam,
``semiOutParam,
``outParam
]
def builtinTargets : M (Array Lean.Name) := do
let mut additional := #[]
if (← getLegalAxioms).contains ``Quot.sound then
additional := additional ++ #[``Quot, ``Quot.mk, ``Quot.lift, ``Quot.ind]
return additional
def stringStream (s : String) : BaseIO IO.FS.Stream := do
let ref ← IO.mkRef {
data := s.toByteArray
}
return IO.FS.Stream.ofBuffer ref
def verifyMatch (challengeExport : String) (solutionExport : String) :
M Unit := do
let challenge ← Export.parseStream (← stringStream challengeExport)
let solution ← Export.parseStream (← stringStream solutionExport)
let theoremNames ← getTheoremNames
let definitionNames ← getDefinitionNames
let targets := (← getTheoremNames) ++ (← getLegalAxioms)
IO.ofExcept <| Comparator.compareAt challenge solution targets definitionNames (← primitiveTargets)
IO.ofExcept <| Comparator.checkAxioms solution theoremNames definitionNames (← getLegalAxioms)
let mut result := none
for (kernelName, kernelCommand) in ← getExternalKernels do
result := result <|> (← runExternalKernel kernelName kernelCommand solutionExport)
result := result <|> (← runBuiltinKernel solution)
if let some error := result then
throw <| IO.userError error
def compareIt : M Unit := do
let exportTargets := (← builtinTargets) ++ (← getTheoremNames) ++ (← getLegalAxioms)
++ (← primitiveTargets) ++ (← getDefinitionNames)
let challengeModule ← getChallengeModule
safeLakeBuild challengeModule
let challengeExport ← safeExport challengeModule exportTargets
let solutionModule ← getSolutionModule
safeLakeBuild solutionModule
let solutionExport ← safeExport solutionModule exportTargets
verifyMatch challengeExport solutionExport
IO.println "Your solution is okay!"
structure Config where
challenge_module : String
solution_module : String
theorem_names : Array String
definition_names : Option (Array String) := none
permitted_axioms : Array String
enable_nanoda? : Option Bool
external_kernels? : Option (Std.TreeMap String (Array String))
deriving Lean.FromJson, Lean.ToJson, Repr
def M.run (x : M α) (cfg : Config) : IO α := do
let cwd ← IO.Process.getCurrentDir
let leanPrefix ← queryLeanPrefix cwd
let gitLocation ← queryGitLocation
let whichLean4Export := (← IO.getEnv "COMPARATOR_LEAN4EXPORT").getD "lean4export"
let whichLandrun := (← IO.getEnv "COMPARATOR_LANDRUN").getD "landrun"
let mut externalKernels := cfg.external_kernels?.getD {}
let defaultNanoda := "nanoda_bin"
let nanodaOverride? ← IO.getEnv "COMPARATOR_NANODA"
if cfg.enable_nanoda?.getD false && !externalKernels.isEmpty then
throw <| .userError "Cannot use enable_nanoda and an external kernel list at the same time, register nanoda in the list instead."
for (kernelName, kernelCommand) in externalKernels do
if kernelCommand.isEmpty then
throw <| .userError s!"{kernelName} has an empty command"
if cfg.enable_nanoda?.getD false then
let whichNanoda := nanodaOverride?.getD defaultNanoda
externalKernels := externalKernels.insert "nanoda" #[whichNanoda]
else if let some nanodaOverride := nanodaOverride? then
externalKernels := externalKernels.modify "nanoda" fun cmd => cmd.set! 0 nanodaOverride
ReaderT.run x {
projectDir := cwd
challengeModule := cfg.challenge_module.toName,
solutionModule := cfg.solution_module.toName,
theoremNames := cfg.theorem_names.map String.toName,
definitionNames := cfg.definition_names.getD #[] |>.map String.toName,
legalAxioms := cfg.permitted_axioms.map String.toName,
leanPrefix := leanPrefix,
gitLocation := gitLocation,
whichLean4Export := whichLean4Export,
whichLandrun := whichLandrun,
externalKernels := externalKernels
}
end Comparator
def main (args : List String) : IO Unit := do
let some (configPath : String) := args[0]?
| throw <| .userError "Expected config file path as first argument."
let content ← IO.FS.readFile configPath
let config ← IO.ofExcept <| Lean.FromJson.fromJson? <| ← IO.ofExcept <| Lean.Json.parse content
Comparator.M.run Comparator.compareIt config