-
Notifications
You must be signed in to change notification settings - Fork 14
/
LLMstep.lean
173 lines (157 loc) · 6 KB
/
LLMstep.lean
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
/-
`llmstep` tactic for LLM-based next-step suggestions in Lean4.
Examples:
llmstep ""
llmstep "have"
llmstep "apply Continuous"
Author: Sean Welleck
-/
import Lean.Widget.UserWidget
import Std.Lean.Position
import Std.Lean.Format
import Std.Data.String.Basic
open Lean
/- Calls a `suggest.py` python script with the given prefix and pretty-printed goal. -/
def runSuggestAux (path goal pre ctx: String) : IO (List String) := do
let s ← IO.Process.run { cmd := "python3", args := #[path, goal, pre, ctx] }
return s.splitOn "[SUGGESTION]"
def runSuggest (pre goal ctx: String) : IO (List String) := do
let cwd ← IO.currentDir
let path := cwd / "python" / "suggest.py"
match ← path.pathExists with
| true => runSuggestAux path.toString goal pre ctx
| false => do
let path := cwd / "lake-packages" / "llmstep" / "python" / "suggest.py"
match ← path.pathExists with
| true => runSuggestAux path.toString goal pre ctx
| false => do
dbg_trace f!"{path}"
throw <| IO.userError "could not find python script suggest.py"
/- Display clickable suggestions in the VSCode Lean Infoview.
When a suggestion is clicked, this widget replaces the `llmstep` call
with the suggestion, and saves the call in an adjacent comment.
Code based on `Std.Tactic.TryThis.tryThisWidget`. -/
@[widget] def llmstepTryThisWidget : Widget.UserWidgetDefinition where
name := "llmstep suggestions"
javascript := "
import * as React from 'react';
import { EditorContext } from '@leanprover/infoview';
const e = React.createElement;
export default function(props) {
const editorConnection = React.useContext(EditorContext)
function onClick(suggestion) {
editorConnection.api.applyEdit({
changes: { [props.pos.uri]: [{ range:
props.range,
newText: suggestion[0]
}] }
})
}
const suggestionElement = props.suggestions.length > 0
? [
'Try this: ',
...(props.suggestions.map((suggestion, i) =>
e('li', {onClick: () => onClick(suggestion),
className:
suggestion[1] === 'ProofDone' ? 'link pointer dim green' :
suggestion[1] === 'Valid' ? 'link pointer dim blue' :
'link pointer dim',
title: 'Apply suggestion'},
suggestion[1] === 'ProofDone' ? '🎉 ' + suggestion[0] : suggestion[0]
)
)),
props.info
]
: 'No valid suggestions.';
return e('div',
{className: 'ml1'},
e('ul', {className: 'font-code pre-wrap'},
suggestionElement))
}"
inductive CheckResult : Type
| ProofDone
| Valid
| Invalid
deriving ToJson, Ord
/- Check whether the suggestion `s` completes the proof, is valid (does
not result in an error message), or is invalid. -/
def checkSuggestion (s: String) : Lean.Elab.Tactic.TacticM CheckResult := do
withoutModifyingState do
try
match Parser.runParserCategory (← getEnv) `tactic s with
| Except.ok stx =>
try
_ ← Lean.Elab.Tactic.evalTactic stx
let goals ← Lean.Elab.Tactic.getUnsolvedGoals
if (← getThe Core.State).messages.hasErrors then
pure CheckResult.Invalid
else if goals.isEmpty then
pure CheckResult.ProofDone
else
pure CheckResult.Valid
catch _ =>
pure CheckResult.Invalid
| Except.error _ =>
pure CheckResult.Invalid
catch _ => pure CheckResult.Invalid
/- Adds multiple suggestions to the Lean InfoView.
Code based on `Std.Tactic.addSuggestion`. -/
def addSuggestions (tacRef : Syntax) (pfxRef: Syntax) (suggestions: List String)
(origSpan? : Option Syntax := none)
(extraMsg : String := "") : Lean.Elab.Tactic.TacticM Unit := do
if let some tacticRange := (origSpan?.getD tacRef).getRange? then
if let some argRange := (origSpan?.getD pfxRef).getRange? then
let map ← getFileMap
let start := findLineStart map.source tacticRange.start
let body := map.source.findAux (· ≠ ' ') tacticRange.start start
let checks ← suggestions.mapM checkSuggestion
let texts := suggestions.map fun text => (
(Std.Format.prettyExtra (text.stripSuffix "\n")
(indent := (body - start).1)
(column := (tacticRange.start - start).1)
))
let textsAndChecks := (texts.zip checks |>.toArray |>.qsort
fun a b => compare a.2 b.2 = Ordering.lt).filter fun x =>
match x.2 with
| CheckResult.ProofDone => true
| CheckResult.Valid => true
| CheckResult.Invalid => false
let start := (tacRef.getRange?.getD tacticRange).start
let stop := (pfxRef.getRange?.getD argRange).stop
let stxRange :=
{ start := map.lineStart (map.toPosition start).line
stop := map.lineStart ((map.toPosition stop).line + 1) }
let full_range : String.Range :=
{ start := tacticRange.start, stop := argRange.stop }
let full_range := map.utf8RangeToLspRange full_range
let tactic := Std.Format.prettyExtra f!"{tacRef.prettyPrint}{pfxRef.prettyPrint}"
let json := Json.mkObj [
("tactic", tactic),
("suggestions", toJson textsAndChecks),
("range", toJson full_range),
("info", extraMsg)
]
Widget.saveWidgetInfo ``llmstepTryThisWidget json (.ofRange stxRange)
/--
Call the LLM on a goal, asking for suggestions beginning with a prefix.
-/
def llmStep (pre : String) (ctx : String) (g : MVarId) : MetaM (List String) := do
let pp := toString (← Meta.ppGoal g)
runSuggest pre pp ctx
open Lean Elab Tactic
/- `llmstep` tactic.
Examples:
llmstep ""
llmstep "have"
llmstep "apply Continuous" -/
syntax "llmstep" str: tactic
elab_rules : tactic
| `(tactic | llmstep%$tac $pfx:str) => do
let map ← getFileMap
let ctx := map.source
if let some range := tac.getRange? then
let ctx_ := ctx.extract ctx.toSubstring.startPos range.start
addSuggestions tac pfx (← liftMetaMAtMain (llmStep pfx.getString ctx_))
else
let ctx_ := ""
addSuggestions tac pfx (← liftMetaMAtMain (llmStep pfx.getString ctx_))