perf: add parseImports'

Faster version of `parserImports` for Lake
This commit is contained in:
Leonardo de Moura 2022-11-09 14:50:11 -08:00
parent 69bd25af4f
commit 1704debcd0
2 changed files with 205 additions and 0 deletions

View file

@ -44,3 +44,4 @@ import Lean.Elab.RecAppSyntax
import Lean.Elab.Eval
import Lean.Elab.Calc
import Lean.Elab.InheritDoc
import Lean.Elab.ParseImportsFast

View file

@ -0,0 +1,204 @@
/-
Copyright (c) 2022 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
import Lean.Parser.Module
namespace Lean
namespace ParseImports
structure State where
imports : Array Import := #[]
pos : String.Pos := 0
error? : Option String := none
deriving Inhabited
def Parser := String → State → State
instance : Inhabited Parser where
default := fun _ s => s
@[inline] def State.setPos (s : State) (pos : String.Pos) : State :=
{ s with pos := pos }
@[inline] def State.mkError (s : State) (msg : String) : State :=
{ s with error? := some msg }
def State.mkEOIError (s : State) : State :=
s.mkError "unexpected end of input"
@[inline] def State.next (s : State) (input : String) (pos : String.Pos) : State :=
{ s with pos := input.next pos }
@[inline] def State.next' (s : State) (input : String) (pos : String.Pos) (h : ¬ input.atEnd pos): State :=
{ s with pos := input.next' pos h }
partial def finishCommentBlock (nesting : Nat) : Parser := fun input s =>
let input := input
let i := s.pos
if h : input.atEnd i then eoi s
else
let curr := input.get' i h
let i := input.next' i h
if curr == '-' then
if h : input.atEnd i then eoi s
else
let curr := input.get' i h
if curr == '/' then -- "-/" end of comment
if nesting == 1 then s.next input i
else finishCommentBlock (nesting-1) input (s.next' input i h)
else
finishCommentBlock nesting input (s.next' input i h)
else if curr == '/' then
if h : input.atEnd i then eoi s
else
let curr := input.get' i h
if curr == '-' then finishCommentBlock (nesting+1) input (s.next' input i h)
else finishCommentBlock nesting input (s.setPos i)
else finishCommentBlock nesting input (s.setPos i)
where
eoi s := s.mkError "unterminated comment"
@[specialize] partial def takeUntil (p : Char → Bool) : Parser := fun input s =>
let i := s.pos
if h : input.atEnd i then s
else if p (input.get' i h) then s
else takeUntil p input (s.next' input i h)
@[inline] def takeWhile (p : Char → Bool) : Parser :=
takeUntil (fun c => !p c)
@[inline] def andthen (p q : Parser) : Parser := fun input s =>
let s := p input s
if s.error? matches some .. then s else q input s
instance : AndThen Parser where
andThen p q := andthen p (q ())
partial def whitespace : Parser := fun input s =>
let i := s.pos
if h : input.atEnd i then s
else
let curr := input.get' i h
if curr == '\t' then
s.mkError "tabs are not allowed; please configure your editor to expand them"
else if curr.isWhitespace then whitespace input (s.next input i)
else if curr == '-' then
let i := input.next' i h
let curr := input.get i
if curr == '-' then andthen (takeUntil (fun c => c = '\n')) whitespace input (s.next input i)
else s
else if curr == '/' then
let i := input.next' i h
let curr := input.get i
if curr == '-' then
let i := input.next i
let curr := input.get i
if curr == '-' || curr == '!' then s -- "/--" and "/-!" doc comment are actual tokens
else andthen (finishCommentBlock 1) whitespace input (s.next input i)
else s
else s
@[inline] partial def keywordCore (k : String) (failure : Parser) (success : Parser) : Parser := fun input s =>
let rec @[specialize] go (i j : String.Pos) : State :=
if h₁ : k.atEnd i then
success input <| whitespace input (s.setPos j)
else if h₂ : input.atEnd j then
failure input s
else
let curr₁ := k.get' i h₁
let curr₂ := input.get' j h₂
if curr₁ != curr₂ then
failure input s
else
go (k.next' i h₁) (input.next' j h₂)
go 0 s.pos
@[inline] partial def keyword (k : String) : Parser :=
keywordCore k (fun _ s => s.mkError s!"`{k}` expected") (fun _ s => s)
@[inline] partial def keywordOpt (k : String) : Parser :=
keywordCore k (fun _ s => s) (fun _ s => s)
@[inline] def isIdCont : String → State → Bool := fun input s =>
let i := s.pos
let curr := input.get i
if curr == '.' then
let i := input.next i
if h : input.atEnd i then
false
else
let curr := input.get' i h
isIdFirst curr || isIdBeginEscape curr
else
false
def State.pushModule (module : Name) (runtimeOnly : Bool) (s : State) : State :=
{ s with imports := s.imports.push { module, runtimeOnly } }
@[inline] def isIdRestCold (c : Char) : Bool :=
c = '_' || c = '\'' || c == '!' || c == '?' || isLetterLike c || isSubScriptAlnum c
@[inline] def isIdRestFast (c : Char) : Bool :=
c.isAlphanum || (c != '.' && c != '\n' && c != ' ' && isIdRestCold c)
partial def moduleIdent (runtimeOnly : Bool) : Parser := fun input s =>
let rec parse (module : Name) (s : State) :=
let i := s.pos
if h : input.atEnd i then
s.mkEOIError
else
let curr := input.get' i h
if isIdBeginEscape curr then
let startPart := input.next' i h
let s := takeUntil isIdEndEscape input (s.setPos startPart)
if h : input.atEnd s.pos then
s.mkError "unterminated identifier escape"
else
let stopPart := s.pos
let s := s.next' input s.pos h
let module := .str module (input.extract startPart stopPart)
if isIdCont input s then
let s := s.next input s.pos
parse module s
else
whitespace input (s.pushModule module runtimeOnly)
else if isIdFirst curr then
let startPart := i
let s := takeWhile isIdRestFast input (s.next' input i h)
let stopPart := s.pos
let module := .str module (input.extract startPart stopPart)
if isIdCont input s then
let s := s.next input s.pos
parse module s
else
whitespace input (s.pushModule module runtimeOnly)
else
s.mkError "expected identifier"
parse .anonymous s
@[specialize] partial def many (p : Parser) : Parser := fun input s =>
let pos := s.pos
let size := s.imports.size
let s := p input s
match s.error? with
| none => many p input s
| some _ => { pos, error? := none, imports := s.imports.shrink size }
def main : Parser :=
keywordOpt "prelude" >>
many (keyword "import" >> keywordCore "runtime" (moduleIdent false) (moduleIdent true))
end ParseImports
/--
Simpler and faster version of `parseImports`. We use it to implement Lake.
-/
def parseImports' (input : String) (fileName : String) : IO (Array Lean.Import) := do
let s := ParseImports.main input (ParseImports.whitespace input {})
match s.error? with
| none => return s.imports
| some err => throw <| IO.userError s!"{fileName}: {err}"
end Lean