Parser State

An effectful description of the text a parser consumes


module Parser.ParserState

import public Data.String
import public Data.DPair
import public Data.Refined
import public Data.Refined.Int64
import public Data.SortedMap
import public Data.IORef

import Data.Primitives.Interpolation
import System.File

import public Control.Eff

Barbie Basics

Barbies are types that can "change their clothes", in Idris, this manifests as a type indexed by a type-level function that affects the types of the fields.

Since we know our usage here is going to be quite simple, and not even really making use of dependently typed fun, we are going to implement all the barbie functionality we need by hand, but if you feel like barbies might be a good fit for your problem, or you simply want to learn more, please check out a library like barbies1


public export
Id : Type -> Type
Id x = x

Internal State of a Parser

Type alias for our refined Int64s


public export
0 IsIndex : (length : Int64) -> Int64 -> Type
IsIndex length = From 0 && LessThan length 

public export
record Index (length : Int64) where
  constructor MkIndex
  index : Int64
  {auto 0 prf : IsIndex length index}

Stores the location we are currently at in the string, and metadata about it for providing good error messages. Parsing an empty input isn't very interesting, so we exclude inputs of length zero, since that will make other things easier.


||| State representing a parser's position in the text
public export
record ParserInternal (f : Type -> Type) where
  constructor MkInternal
  -- IDEA: Maybe go full barbie and have this be a field, so that we can, say,
  -- read directly from a file instead of from an already loaded string using the
  -- same parser
  ||| The input string
  input : String
  ||| The length of the input string
  length : Int64
  {auto 0 len_prf : length = cast (strLength input)}
  ||| A sorted set containing the positions of the start of each line
  line_starts : SortedMap (Index length) Nat
  ||| The position of the next character to read in the input
  position : f (Index length) 
  ||| True if we have hit the end of input
  end_of_input : f Bool
%name ParserInternal pi, pj, pk

ParserInternal Methods

Construct a ParserInternal from an input string. Will fail if the input is empty, because then we can't index it.


export
newInternal : (input : String) -> Maybe (ParserInternal Id)
newInternal input = 
  -- Check if we have at least one character in the input
  case refine0 0 {p = IsIndex (cast (strLength input))} of
    Nothing => Nothing
    Just (Element position _) => Just $ 
      MkInternal input 
                 (cast (strLength input)) 
                 (mkStarts' input (MkIndex position)) 
                 (MkIndex position) 
                 False
  where
    partial
    mkStarts : 
      (str : String) -> (acc : List (Index (cast (strLength str)), Nat))
      -> (idx : Index (cast (strLength str))) -> (count : Nat) -> (next : Bool)
      -> List (Index (cast (strLength str)), Nat)
    mkStarts str acc idx count True = 
      mkStarts str ((idx, count) :: acc) idx (S count) False
    mkStarts str acc idx count False = 
      case refine0 (idx.index + 1) {p = IsIndex (cast (strLength str))} of
        Nothing => acc
        Just (Element next _) => 
          if strIndex str (cast idx.index) == '\n'
            then mkStarts str acc (MkIndex next) count True
            else mkStarts str acc (MkIndex next) count False
    mkStarts' : (str : String) -> (start : Index (cast (strLength str)))
      -> SortedMap (Index (cast (strLength str))) Nat
    mkStarts' str start = 
      let 
        pairs = assert_total $ 
          mkStarts str [] start 0 True
      in fromList pairs

Get the current line and column number


||| Returns the current position of the parser cursor in, zero indexed, (line, 
||| column) form
export
positionPair : (pi : ParserInternal Id) -> (Nat, Nat)
positionPair pi = 
  case lookup pi.position pi.line_starts of
    Just line => (line, 0)
    Nothing => 
      case lookupBetween pi.position pi.line_starts of
        -- There will always be at least one line start, and we would have hit
        -- the previous case if we were at the start of the first one, so if
        -- there isn't a before, we can return a nonsense value safely
        (Nothing, _) => (0, 0)
        (Just (start, linum), after) => 
          -- Our index will always be after the start of the line, for previously
          -- mentioned reasons, so this cast is safe
          let col = cast {to = Nat} $ pi.position.index - start.index
          in (linum, col)

More Barbie Functionality

Provide the barbie analogs of map and traverse for our ParserInternal type, allowing us to change the type the values in a ParserInternal by mapping over those values.


export
bmap : ({0 a : Type} -> f a -> g a) -> ParserInternal f -> ParserInternal g
-- bmap f = bmap_ (\_ => f)
bmap fun (MkInternal input length line_starts position end_of_input) = 
  let position' = fun position
      end_of_input' = fun end_of_input 
  in MkInternal input length line_starts position' end_of_input'

export
btraverse : Applicative e => ({0 a : Type} -> f a -> e (g a)) 
  -> ParserInternal f -> e (ParserInternal g)
btraverse fun (MkInternal input length line_starts position end_of_input) = 
  let pures = (MkInternal input length line_starts)
  in [| pures (fun position) (fun end_of_input)|]

Three way result


||| Three way result returned from attempting to parse a single char
public export
data ParseCharResult : Type where
  GotChar : (char : Char) -> ParseCharResult
  GotError : (err : Char) -> ParseCharResult
  EndOfInput : ParseCharResult

The Effect Type


export
data ParserState : Type -> Type where
  Save : ParserState (ParserInternal Id)
  Load : (ParserInternal Id) -> ParserState ()
  -- TODO: Maybe add a ParseString that parses a string of characters as a 
  -- string using efficent slicing?
  ParseChar : (predicate : Char -> Bool) -> ParserState ParseCharResult
  ParseExactChar : (char : Char) -> ParserState ParseCharResult
  ParseEoF : ParserState Bool
  Note : Lazy String -> ParserState ()

Actions


||| Return the current state, for potential later reloading
export
save : Has ParserState fs => Eff fs (ParserInternal Id)
save = send Save

||| Reset to the provided state
export
load : Has ParserState fs => ParserInternal Id -> Eff fs ()
load pi = send $ Load pi

||| Attempt to parse a char, checking to see if it complies with the supplied 
||| predicate, updates the state if parsing succeeds, does not alter it in an 
||| error condition.
export 
charPredicate : Has ParserState fs => (predicate : Char -> Bool)
  -> Eff fs ParseCharResult
charPredicate predicate = send $ ParseChar predicate 

||| Parse a char by exact value
export 
charExact' : Has ParserState fs => (chr : Char) -> Eff fs ParseCharResult
charExact' chr = send $ ParseExactChar chr

||| "Parse" the end of input, returning `True` if the parser state is currently
||| at the end of the input
export
parseEoF : Has ParserState fs => Eff fs Bool
parseEoF = send ParseEoF

||| Make a note to be output when running with a debug handler
export
pnote : Has ParserState fs => Lazy String -> Eff fs ()
pnote x = send $ Note x

Handling a ParserState

IO Context


export
handleParserStateIO : HasIO io => 
  IORef (ParserInternal IORef) -> ParserState t -> io t
handleParserStateIO pi Save = do
  pi <- readIORef pi
  btraverse readIORef pi
handleParserStateIO pi (Load pj) = do
  pj <- btraverse newIORef pj
  writeIORef pi pj
handleParserStateIO pi (ParseChar predicate) = do
  pi <- readIORef pi
  False <- readIORef pi.end_of_input
    | _ => pure EndOfInput
  position <- readIORef pi.position
  let char = assert_total $ strIndex pi.input (cast position.index)
  True <- pure $ predicate char
    | _ => pure $ GotError char
  -- Our refinement type on the position forces us to check that the length is
  -- in bounds after incrementing it, if its out of bounds, set the end_of_input
  -- flag
  case refine0 (position.index + 1) {p = IsIndex pi.length} of
    Nothing => do
      writeIORef pi.end_of_input True
      pure $ GotChar char
    Just (Element next _) => do
      writeIORef pi.position $ MkIndex next
      pure $ GotChar char
handleParserStateIO pi (ParseExactChar chr) = do
  -- TODO: do this directly?
  handleParserStateIO pi (ParseChar (== chr))
handleParserStateIO pi ParseEoF = do
  pi <- readIORef pi
  readIORef pi.end_of_input
-- We ignore notes in non-debug mode
handleParserStateIO pi (Note _) = pure ()

export
newInternalIO : HasIO io => String -> io $ Maybe (IORef (ParserInternal IORef))
newInternalIO str = do
  Just internal <- pure $ newInternal str
    | _ => pure Nothing
  internal <- btraverse newIORef internal
  map Just $ newIORef internal

Wrapper with debugging output


export
handleParserStateIODebug : HasIO io => 
  IORef (ParserInternal IORef) -> ParserState t -> io t
handleParserStateIODebug x (Note note) = do
  state <- readIORef x
  state <- btraverse readIORef state
  _ <- fPutStrLn stderr "Note \{note} -> \{show state}"
  pure ()
handleParserStateIODebug x y = do
  state <- readIORef x
  state <- btraverse readIORef state
  _ <- fPutStrLn stderr "\{show y} -> \{show state}"
  handleParserStateIO x y

State context


unPS : ParserInternal Id -> ParserState a -> (a, ParserInternal Id)
unPS pi Save = (pi, pi)
unPS pi (Load pj) = ((), pi)
unPS pi (ParseChar predicate) = 
  if pi.end_of_input
    then (EndOfInput, pi)
    else let
      char = assert_total $ strIndex pi.input (cast pi.position.index)
    in if predicate char
      then case refine0 (pi.position.index + 1) {p = IsIndex pi.length} of
        Nothing => 
          (GotChar char, {end_of_input := True} pi)
        Just (Element next _) => 
          (GotChar char, {position := MkIndex next} pi)
      else (GotError char, pi)
unPS pi (ParseExactChar chr) = unPS pi (ParseChar (== chr))
unPS pi ParseEoF = (pi.end_of_input, pi)
unPS pi (Note _) = ((), pi)

export
runParserState : Has ParserState fs => 
  (s : ParserInternal Id) -> Eff fs t 
  -> Eff (fs - ParserState) (t, ParserInternal Id)
runParserState s = 
  handleRelayS s (\x, y => pure (y, x)) $ \s2,ps,f =>
    let (a, st) = unPS s2 ps 
    in f st a

Footnotes