packages feed

zwirn-0.1.0.0: src/Zwirn/Language/Lexer.x

{
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
module Zwirn.Language.Lexer
  ( -- * Invoking Alex
    Alex
  , AlexPosn (..)
  , alexGetInput
  , alexError
  , runAlex
  , alexMonadScan

  , Range (..)
  , RangedToken (..)
  , Token (..)
  , scanMany
  , increaseChoice
  , setEditorNum
  , getEditorNum
  , setInitialLineNum
  , lineLexer
  , typeLexer
  ) where

{-
    Lexer.hs - lexer for zwirn, code adapted from
    https://serokell.io/blog/lexing-with-alex
    Copyright (C) 2023, Martin Gius

    This library is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this library.  If not, see <http://www.gnu.org/licenses/>.
-}

import           Data.Text (Text)
import qualified Data.Text as Text
import Control.Monad (when)
}

%wrapper "monadUserState-strict-text"

$digit = [0-9]
$alphasmall = [a-z]
$alpha = [a-zA-Z]

@id = ($alphasmall) ($alpha | $digit | \_ )*
@singles = ("&" | "$" | "?" | "#" | "." | "^")
@otherops = ("|" | "=" | "~" | "<" | ">" | "%")
@specialop = ("*" | "/" | "'" | "+" | "-")
@op = ((@singles (@singles | @otherops | @specialop)*) | ((@otherops | @specialop) (@singles | @otherops | @specialop)+))
@num = ("-")? ($digit)+ ("." ($digit)+)?
@path = $white ($alpha | "/" | ".")+

tokens :-

<0> $white+ ;

<line> (.+ (\n?) | \n)               { mkLine }

<ty> $white+ ;
<ty> $alpha+ "." ;
<ty> "=>"                          { tok Context }
<ty> "->"                          { tok Arrow }
<ty> "("                           { tok LPar }
<ty> ")"                           { tok RPar }
<ty> ","                           { tok Comma }
<ty> "Text"                        { tok TextToken }
<ty> "Number"                      { tok NumberToken }
<ty> "Map"                         { tok MapToken }
<ty> "Bus"                         { tok BusToken }
<ty> @id                           { tokText VarToken }
<ty> [A-Z] $alphasmall+            { tokText TypeClass }
<ty> @id                           { tokText Identifier }
<ty> @op                           { tokText Operator }
<ty> @specialop                    { tokText SpecialOp }

-- Multi Line Comments

<0>       "{-" { nestComment `andBegin` comment }
<0>       "-}" { \_ _ -> alexError "Error: unexpected closing comment" }
<comment> "{-" { nestComment }
<comment> "-}" { unnestComment }
<comment> .    ;
<comment> \n   ;

-- Single Line Comments

<0> "--" .* ;

-- Repeat
<0> "!"               { tok Repeat }
<0> "!"($digit+)      { tokText (\t -> RepeatNum $ Text.drop 1 t) }

-- Parenthesis
<0> "("     { tok LPar }
<0> ")"     { tok RPar }

-- Sequences
<0> "["     { tok LBrack }
<0> "]"     { tok RBrack }

-- Stacks
<0> ","     { tok Comma }

-- Choice
<0> "|"     { tok Pipe }

-- Enum
<0> ".."    { tok Enum }

-- Polyrhythm
<0> "%"     { tok Poly }

-- Euclid
<0> "{"     { tok LBraces }
<0> "}"     { tok RBraces }

-- Lambda
<0> "\"     { tok Lambda }
<0> "->"    { tok Arrow }

-- Actions
<0> ";"                               { tok Colon }
<0> "<-"                              { tok StreamA }
<0> ":cps"                            { tok TempoCps }
<0> ":bpm"                            { tok TempoBpm }
<0> ":t"                              { tok TypeA }
<0> "="                               { tok Assign }
<0> ":show"                           { tok ShowA }
<0> ":config"                         { tok ConfigA }
<0> ":resetconfig"                    { tok ResetConfigA }
<0> ":info"                           { tok InfoA }
<0> (":load") @path                   { tokText (\t -> LoadA $ Text.drop 6 t) }

-- Identifiers
<0> @id             { tokText Identifier }

-- Operator Identifier
<0> \( @op \)       { tokText (Identifier . rmFirstLast) }

-- Constants
<0> @num            { tokText Number }
<0> \"[^\"]*\"      { tokText String }
<0> "~"             { tok Rest }

-- Operators
<0> @op             { tokText Operator }
<0> @specialop      { tokText SpecialOp }

-- Alternations
<0> "<"     { tok LAngle }
<0> ">"     { tok RAngle }

{
data AlexUserState = AlexUserState
  { nestLevel :: Int
  , choiceNum :: Int
  , editorNum :: Int
  }

alexInitUserState :: AlexUserState
alexInitUserState = AlexUserState { nestLevel = 0, choiceNum = 0, editorNum = 0}

get :: Alex AlexUserState
get = Alex $ \s -> Right (s, alex_ust s)

put :: AlexUserState -> Alex ()
put s' = Alex $ \s -> Right (s{alex_ust = s'}, ())

modify :: (AlexUserState -> AlexUserState) -> Alex ()
modify f = Alex $ \s -> Right (s{alex_ust = f (alex_ust s)}, ())

alexEOF :: Alex RangedToken
alexEOF = do
  startCode <- alexGetStartCode
  when (startCode == comment) $
    alexError "Error: unclosed comment"
  (pos, _, _, _) <- alexGetInput
  pure $ RangedToken EOF (Range pos pos)

data Range = Range
  { start :: AlexPosn
  , stop :: AlexPosn
  } deriving (Eq, Show)

data RangedToken = RangedToken
  { rtToken :: Token
  , rtRange :: Range
  } deriving (Eq, Show)

data Token
  -- Identifiers
  = Identifier Text
  -- Constants
  | String Text
  | Number Text
  | Rest
  -- Operators
  | Operator Text
  | SpecialOp Text
  -- Repeat
  | Repeat
  | RepeatNum Text
  -- Parenthesis
  | LPar
  | RPar
  -- Sequences
  | LBrack
  | RBrack
  -- Stacks
  | Comma
  -- Alternations
  | LAngle
  | RAngle
  -- Choice
  | Pipe
  -- Polyrhythm
  | Poly
  -- Euclid
  | LBraces
  | RBraces
  -- Lambda
  | Lambda
  | Arrow
  -- Enum
  | Enum
  -- Actions
  | Colon
  | StreamA
  | TempoCps
  | TempoBpm
  | TypeA
  | ShowA
  | ConfigA
  | ResetConfigA
  | Assign
  | LoadA Text
  | InfoA
  -- Line & Block Tokens
  | LineT Text
  | BlockSep
  -- Type Tokens
  | Context
  | TextToken
  | NumberToken
  | MapToken
  | BusToken
  | VarToken Text
  | TypeClass Text
  -- EOF
  | EOF
  deriving (Eq)

instance Show Token where
 show (Identifier s) = show s
 show (String s) = show s
 show (Number d) = show d
 show Rest = quoted "~"
 show (Operator o) = show o
 show (SpecialOp o) = show o
 show Repeat = quoted "!"
 show (RepeatNum x) = quoted "!" ++ show x
 show LPar = quoted "("
 show RPar = quoted ")"
 show LBrack = quoted "["
 show RBrack = quoted "]"
 show Comma = quoted ","
 show LAngle = quoted "<"
 show RAngle = quoted ">"
 show Pipe = quoted "|"
 show Poly = quoted "%"
 show LBraces = quoted "{"
 show RBraces = quoted "}"
 show Lambda = quoted "\\"
 show Arrow = quoted "->"
 show Colon = quoted ";"
 show Enum = quoted ".."
 show StreamA = quoted "<-"
 show TempoCps = ":cps"
 show TempoBpm = ":bpm"
 show TypeA = quoted ":t"
 show ShowA = quoted ":show"
 show ConfigA = quoted ":config"
 show ResetConfigA = quoted ":resetconfig"
 show Assign = quoted "="
 show (LoadA x) = ":load " <> show x
 show InfoA = quoted ":info"
 show (LineT t) = "line " <> show t
 show BlockSep = "block"
 show Context = "=>"
 show TextToken = "Text"
 show NumberToken = "Number"
 show MapToken = "Map"
 show BusToken = "Bus"
 show (VarToken t) = show t
 show (TypeClass c) = show c
 show EOF = "end of file"

quoted :: String -> String
quoted s = "'" ++ s ++ "'"

mkRange :: AlexInput -> Int -> Range
mkRange (st, _, _, str) len = Range{start = st, stop = end}
  where
    end = Text.foldl' alexMove st $ Text.take len str

mkLine :: AlexAction RangedToken
mkLine inp@(_, _, _, str) len = case Text.all (\c -> elem c ("\n\t " :: String)) (Text.take len str) of
                            True -> tok BlockSep inp len
                            False -> pure RangedToken
                              { rtToken = LineT $ Text.map replaceTab (Text.take len str)
                              , rtRange = mkRange inp len
                              }

-- | replace all tabs with a single space, since codemirror sees tabs as one column
replaceTab :: Char  -> Char
replaceTab '\t' = ' '
replaceTab x = x

rmFirstLast :: Text -> Text
rmFirstLast t = Text.init (Text.tail t)

tok :: Token -> AlexAction RangedToken
tok ctor inp len =
  pure RangedToken
    { rtToken = ctor
    , rtRange = mkRange inp len
    }

tokText :: (Text -> Token) -> AlexAction RangedToken
tokText f inp@(_, _, _, str) len =
  pure RangedToken
    { rtToken = f $ Text.take len str
    , rtRange = mkRange inp len
    }

nestComment :: AlexAction RangedToken
nestComment input len = do
  modify $ \s -> s{nestLevel = nestLevel s + 1}
  skip input len

unnestComment :: AlexAction RangedToken
unnestComment input len = do
  state <- get
  let level = nestLevel state - 1
  put state{nestLevel = level}
  when (level == 0) $
    alexSetStartCode 0
  skip input len

increaseChoice :: Alex Int
increaseChoice = do
  (AlexUserState c x e) <- get
  put $ AlexUserState c (x+1) e
  return x

getEditorNum :: Alex Int
getEditorNum = do
  (AlexUserState _ _ e) <- get
  return e

setEditorNum :: Int -> Alex ()
setEditorNum i = do
  (AlexUserState c x _) <- get
  put $ AlexUserState c x i

setInitialLineNum :: Int -> Alex ()
setInitialLineNum i = Alex alex
                    where alex s = Right (s {alex_pos = AlexPn x i c }, ())
                                 where AlexPn x _ c = alex_pos s

lineLexer :: Alex ()
lineLexer = alexSetStartCode line

typeLexer :: Alex ()
typeLexer = alexSetStartCode ty

scanMany :: Text -> Either String [RangedToken]
scanMany input = runAlex input go
  where
    go = do
      output <- lineLexer >> alexMonadScan
      if rtToken output == EOF
        then pure [output]
        else ((output) :) <$> go
}