/- Copyright (c) 2020 Wojciech Nawrocki. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: Wojciech Nawrocki -/ import Init.System.IO import Lean.Elab.Import import Lean.Elab.Command /-! One can think of this module as being a partial reimplementation of Lean.Elab.Frontend which also stores a snapshot of the world after each command. Importantly, we allow (re)starting compilation from any snapshot/position in the file for interactive editing purposes. -/ namespace Lean namespace Server namespace Snapshots open Elab /-- The data associated with a snapshot is different depending on whether it was produced from the header or from a command. -/ inductive SnapshotData where | headerData : Environment → MessageLog → Options → SnapshotData | cmdData : Command.State → SnapshotData deriving Inhabited /-- What Lean knows about the world after the header and each command. -/ structure Snapshot where /- Where the command which produced this snapshot begins. Note that neighbouring snapshots are *not* necessarily attached beginning-to-end, since inputs outside the grammar advance the parser but do not produce snapshots. -/ beginPos : String.Pos stx : Syntax mpState : Parser.ModuleParserState data : SnapshotData deriving Inhabited namespace Snapshot def endPos (s : Snapshot) : String.Pos := s.mpState.pos def env : Snapshot → Environment | ⟨_, _, _, SnapshotData.headerData env_ _ _⟩ => env_ | ⟨_, _, _, SnapshotData.cmdData cmdState⟩ => cmdState.env def msgLog : Snapshot → MessageLog | ⟨_, _, _, SnapshotData.headerData _ msgLog_ _⟩ => msgLog_ | ⟨_, _, _, SnapshotData.cmdData cmdState⟩ => cmdState.messages def toCmdState : Snapshot → Command.State | ⟨_, _, _, SnapshotData.headerData env msgLog opts⟩ => Command.mkState env msgLog opts | ⟨_, _, _, SnapshotData.cmdData cmdState⟩ => cmdState end Snapshot -- TODO(WN): fns here should probably take inputCtx and live -- in some SnapshotsM := ReaderT Context (EIO Empty) def compileHeader (contents : String) (opts : Options := {}) : IO Snapshot := do let inputCtx := Parser.mkInputContext contents "" let (headerStx, headerParserState, msgLog) ← Parser.parseHeader inputCtx let (headerEnv, msgLog) ← Elab.processHeader headerStx opts msgLog inputCtx pure { beginPos := 0 stx := headerStx mpState := headerParserState data := SnapshotData.headerData headerEnv msgLog opts } def reparseHeader (contents : String) (header : Snapshot) (opts : Options := {}) : IO Snapshot := do let inputCtx := Parser.mkInputContext contents "" let (_, newHeaderParserState, _) ← Parser.parseHeader inputCtx pure { header with mpState := newHeaderParserState } private def ioErrorFromEmpty (ex : Empty) : IO.Error := nomatch ex /-- Parses the next command occurring after the given snapshot without elaborating it. -/ def parseNextCmd (contents : String) (snap : Snapshot) : IO Syntax := do let inputCtx := Parser.mkInputContext contents "" let cmdState := snap.toCmdState let scope := cmdState.scopes.head! let pmctx := { env := cmdState.env, options := scope.opts, currNamespace := scope.currNamespace, openDecls := scope.openDecls } let (cmdStx, _, _) := Parser.parseCommand inputCtx pmctx snap.mpState snap.msgLog cmdStx /-- Parse remaining file without elaboration. NOTE that doing so can lead to parse errors or even wrong syntax objects, so it should only be done for reporting preliminary results! -/ partial def parseAhead (contents : String) (snap : Snapshot) : IO (Array Syntax) := do let inputCtx := Parser.mkInputContext contents "" let cmdState := snap.toCmdState let scope := cmdState.scopes.head! let pmctx := { env := cmdState.env, options := scope.opts, currNamespace := scope.currNamespace, openDecls := scope.openDecls } go inputCtx pmctx snap.mpState #[] where go inputCtx pmctx cmdParserState stxs := do let (cmdStx, cmdParserState, _) := Parser.parseCommand inputCtx pmctx cmdParserState snap.msgLog if Parser.isEOI cmdStx || Parser.isExitCommand cmdStx then stxs.push cmdStx else go inputCtx pmctx cmdParserState (stxs.push cmdStx) /-- Compiles the next command occurring after the given snapshot. If there is no next command (file ended), returns messages produced through the file. -/ -- NOTE: This code is really very similar to Elab.Frontend. But generalizing it -- over "store snapshots"/"don't store snapshots" would likely result in confusing -- isServer? conditionals and not be worth it due to how short it is. def compileNextCmd (contents : String) (snap : Snapshot) : IO (Sum Snapshot MessageLog) := do let inputCtx := Parser.mkInputContext contents "" let cmdState := snap.toCmdState let scope := cmdState.scopes.head! let pmctx := { env := cmdState.env, options := scope.opts, currNamespace := scope.currNamespace, openDecls := scope.openDecls } let (cmdStx, cmdParserState, msgLog) := Parser.parseCommand inputCtx pmctx snap.mpState snap.msgLog let cmdPos := cmdStx.getHeadInfo.get!.pos.get! -- TODO(WN): always `some`? if Parser.isEOI cmdStx || Parser.isExitCommand cmdStx then Sum.inr msgLog else let cmdStateRef ← IO.mkRef { snap.toCmdState with messages := msgLog } let cmdCtx : Elab.Command.Context := { cmdPos := snap.endPos fileName := inputCtx.fileName fileMap := inputCtx.fileMap } EIO.toIO ioErrorFromEmpty $ Elab.Command.catchExceptions (Elab.Command.elabCommand cmdStx) cmdCtx cmdStateRef let postCmdState ← cmdStateRef.get let postCmdSnap : Snapshot := { beginPos := cmdPos stx := cmdStx mpState := cmdParserState data := SnapshotData.cmdData postCmdState } Sum.inl postCmdSnap /-- Compiles all commands after the given snapshot. Returns them as a list, together with the final message log. -/ partial def compileCmdsAfter (contents : String) (snap : Snapshot) : IO (List Snapshot × MessageLog) := do let cmdOut ← compileNextCmd contents snap match cmdOut with | Sum.inl snap => let (snaps, msgLog) ← compileCmdsAfter contents snap (snap :: snaps, msgLog) | Sum.inr msgLog => ([], msgLog) end Snapshots end Server end Lean