packages feed

hlex (empty) → 0.1.0

raw patch · 11 files changed

+323/−0 lines, 11 filesdep +HUnitdep +basedep +hlexsetup-changed

Dependencies added: HUnit, base, hlex, regex-tdfa

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+For the package version policy (PVP), see  http://pvp.haskell.org/faq .++### 0.1.0++_2023-05-06, Sebastian Tee_++Initial version
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2023 Sebastian Tee++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ README.md view
@@ -0,0 +1,8 @@+# hlex++![ci](https://github.com/SebTee/Hlex/actions/workflows/ci.yml/badge.svg)++The tools needed to create a lexer from a lexical grammar in Haskell.++You can see an [example language](https://hackage.haskell.org/package/hlex/docs/Hlex.html#g:1)+in the [documentation](https://hackage.haskell.org/package/hlex/docs/doc-index.html).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hlex.cabal view
@@ -0,0 +1,77 @@+cabal-version: 1.12++name:           hlex+version:        0.1.0+synopsis:       Simple Lexer Creation+description:    This package provides the tools to create a simple lexer.+category:       lexer+homepage:       https://github.com/SebTee/hlex#readme+bug-reports:    https://github.com/SebTee/hlex/issues+author:         Sebastian Tee+maintainer:     Sebastian Tee+copyright:      2023 Sebastian Tee+license:        MIT+license-file:   LICENSE+build-type:     Simple+++extra-source-files:+  README.md+  CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/SebTee/hlex++library+  exposed-modules:+      Hlex+  other-modules:+      Paths_hlex+  hs-source-dirs:+      src+  ghc-options:+      -Wall+      -Wcompat+      -Widentities+      -Wincomplete-record-updates+      -Wincomplete-uni-patterns+      -Wmissing-export-lists+      -Wmissing-home-modules+      -Wpartial-fields+      -Wredundant-constraints+  build-depends:+      base >=4.7 && <5+    , regex-tdfa ==1.3.*+  default-language: Haskell2010++test-suite Hlex-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      ExampleLang+      Exceptions+      Successes+      TestResources+      Paths_hlex+  hs-source-dirs:+      test+  ghc-options:+      -Wall+      -Wcompat+      -Widentities+      -Wincomplete-record-updates+      -Wincomplete-uni-patterns+      -Wmissing-export-lists+      -Wmissing-home-modules+      -Wpartial-fields+      -Wredundant-constraints+      -threaded+      -rtsopts+      -with-rtsopts=-N+  build-depends:+      HUnit ==1.6.*+    , hlex+    , base >=4.7 && <5+    , regex-tdfa ==1.3.*+  default-language: Haskell2010
+ src/Hlex.hs view
@@ -0,0 +1,123 @@+{-|+Module      : Hlex+Description : Lexer creation tools+Copyright   : (c) Sebastian Tee, 2023+License     : MIT++Tools needed to create a 'Lexer' from a lexical 'Grammar'.+-}+module Hlex+     ( -- * Example+       -- $example++       -- * Types+       Grammar+     , TokenSyntax(..)+     , Lexer+       -- ** Exceptions+     , LexException(..)+       -- * Functions+     , hlex+     ) where++import Text.Regex.TDFA ((=~))++-- | Exception thrown when a 'Lexer' is unable to lex a string.+data LexException = LexException+  Int -- ^ The line number where the string that couldn't be lexed is located.+  Int -- ^ The column where the string that couldn't be lexed is located.+  String -- ^ The String that couldn't be lexed.+  deriving(Read, Show, Eq)++-- | These are the individual rules that make up a 'Grammar'.+--+-- Takes a __POSIX regular expression__ then converts it to a token or skips it.+data TokenSyntax token+  = Skip -- ^ Skips over any matches.+    String -- ^ Regular expression.+  | Tokenize -- ^ Takes a function that converts the matched string to a token.+    String -- ^ Regular expression.+    (String -> token) -- ^ Function that converts the matched string into a token.+  | JustToken -- ^ Converts any regular expression matches to a given token.+    String -- ^ Regular expression.+    token -- ^ Given token.++type InternalToken token = (String, Maybe (String -> token))++-- | Lexical grammar made up of 'TokenSyntax' rules.+--+-- The __order is important__. The 'Lexer' will apply each 'TokenSyntax' rule in the order listed.+type Grammar token = [TokenSyntax token]++-- | Converts a string into a list of tokens.+-- If the string does not follow the Lexer's 'Grammar' a 'LexException' will be returned.+type Lexer token = String -> Either LexException [token]++tokenizerToInternalToken :: TokenSyntax a -> InternalToken a+tokenizerToInternalToken (Skip regex) = (regex, Nothing)+tokenizerToInternalToken (Tokenize regex toToken) = (regex, Just toToken)+tokenizerToInternalToken (JustToken regex token) = (regex, Just $ \_ -> token)++-- | Takes a given 'Grammar' and turns it into a 'Lexer'.+hlex :: Grammar token -> Lexer token+hlex = lexInternal 1 1 . map tokenizerToInternalToken++lexInternal :: Int -> Int -> [InternalToken token] -> Lexer token+lexInternal _ _ _ "" = Right []+lexInternal row col ((regex, t):grammar) program = if null matchedText+  then lexInternal row col grammar program+  else do+    before <- parsedBefore+    after <- parsedAfter+    case t of+      Nothing -> Right $ before ++ after+      Just tk -> Right $ before ++ tk matchedText : after+  where+    (beforeProgram, matchedText, afterProgram) = program =~ regex :: (String, String, String)+    (afterRow, afterCol) = getLastCharPos row col (beforeProgram ++ matchedText)+    parsedBefore = lexInternal row col grammar beforeProgram+    parsedAfter = lexInternal afterRow afterCol ((regex, t):grammar) afterProgram+lexInternal row col _ invalidString = Left $ LexException row col invalidString++getLastCharPos :: Int -> Int -> String -> (Int, Int)+getLastCharPos startRow startCol x = (startRow + addRow, addCol + if addRow == 0 then startCol else 1)+  where+    ls = lines x+    addRow = length ls - 1+    addCol = length $ last ls++{- $example+Here is an example module for a simple language.++@+  module ExampleLang+       ( MyToken(..) -- Export the language's tokens and the lexer+       , myLexer+       ) where++  import Hlex++  data MyToken = Ident String -- String identifier token+               | Number Float -- Number token and numeric value+               | Assign       -- Assignment operator token+               deriving(Show)++  myGrammar :: Grammar MyToken+  myGrammar = [ JustToken "=" Assign                                       -- "=" Operator becomes the assign token+              , Tokenize "[a-zA-Z]+" (\match -> Ident match)                -- Identifier token with string+              , Tokenize "[0-9]+(\\.[0-9]+)?" (\match -> Number (read match) -- Number token with the parsed numeric value stored as a Float+              , Skip "[ \\n\\r\\t]+"                                          -- Skip whitespace+              ]++  myLexer :: Lexer MyToken+  myLexer = hlex myGrammar -- hlex turns a Grammar into a Lexer+@++Here is the lexer being used on a simple program.++>>> lexer "x = 1.2"+Right [Ident "x", Assign, Number 1.2]++The lexer uses 'Either'. Right means the lexer successfully parsed the program to a list of MyTokens.+If Left was returned it would be a 'LexException'.+-}
+ test/ExampleLang.hs view
@@ -0,0 +1,21 @@+module ExampleLang+     ( Token(..)+     , lexer+     ) where++import Hlex++data Token = Ident String+           | Number Float+           | Assign+           deriving(Read, Show, Eq)++grammar :: Grammar Token+grammar = [ JustToken "=" Assign+          , Tokenize "[a-zA-Z]+" Ident+          , Tokenize "[0-9]+(\\.[0-9]+)?" $ Number . read+          , Skip "[ \n\r\t]+"+          ]++lexer :: Lexer Token+lexer = hlex grammar
+ test/Exceptions.hs view
@@ -0,0 +1,17 @@+module Exceptions (exceptions) where++import Test.HUnit+import Hlex+import TestResources+import ExampleLang++exceptions :: Test+exceptions = TestList [ TestLabel "Location Exception" exceptionLocation+                      , TestLabel "Number Parse Exception" exceptionNumParse+                      ]++exceptionLocation :: Test+exceptionLocation = TestCase $ assertLexException lexer "aaaa\n\naaaaa\naaa\naaa//bbbaa\naaaaa" $ LexException 5 4 "//"++exceptionNumParse :: Test+exceptionNumParse = TestCase $ assertLexException lexer "10.2.3" $ LexException 1 5 "."
+ test/Spec.hs view
@@ -0,0 +1,14 @@+import System.Exit+import Test.HUnit+import Exceptions+import Successes++main :: IO ()+main = do+  results <- runTestTT tests+  case results of+    Counts {errors = 0, failures = 0} -> exitSuccess+    _ -> exitFailure++tests :: Test+tests = TestList [TestLabel "Successes" successes, TestLabel "Exceptions" exceptions]
+ test/Successes.hs view
@@ -0,0 +1,16 @@+module Successes (successes) where++import Test.HUnit+import TestResources+import ExampleLang++successes :: Test+successes = TestList [ TestLabel "Parse Number" parseNum+                     , TestLabel "Assign Number" assignNumber+                     ]++parseNum :: Test+parseNum = TestCase $ assertLexResult lexer "12 3.5" [Number 12, Number 3.5]++assignNumber :: Test+assignNumber = TestCase $ assertLexResult lexer "x = 1" [Ident "x", Assign, Number 1]
+ test/TestResources.hs view
@@ -0,0 +1,17 @@+module TestResources+     ( assertLexResult+     , assertLexException+     ) where++import Test.HUnit+import Hlex++assertLexResult :: (Eq a, Show a) => Lexer a -> String -> [a] -> IO()+assertLexResult lexer program expectedTokens = case lexer program of+  Right tokens -> assertEqual "Incorrectly parsed" expectedTokens tokens+  Left err -> assertFailure $ "Failed with the following exception " ++ (show err)++assertLexException :: (Show a) => Lexer a -> String -> LexException -> IO ()+assertLexException lexer program expectedException = case lexer program of+  Right tokens -> assertFailure $ "Successfully parsed to: " ++ show tokens+  Left err -> assertEqual "Incorrect exception" expectedException err