Compare commits

...

15 Commits

Author SHA1 Message Date
Enrico Lumetti 8822506d41 Add basic turnt setup 2024-10-29 02:05:16 +01:00
Enrico Lumetti 690985cb87 Add CFG construction 2024-10-13 20:22:53 +02:00
Enrico Lumetti 27f24389ae Fix label and Op show 2024-10-13 20:22:21 +02:00
Enrico Lumetti 08049e9955 JSON serialization 2024-10-06 12:43:08 +02:00
Enrico Lumetti b635fde2c5 Fix id, ret parsing 2024-10-06 12:28:06 +02:00
Enrico Lumetti 8e5112ed93 Fix boolean parsing 2024-10-06 12:07:04 +02:00
Enrico Lumetti 7874b384a5 Parse add.json in tests 2024-10-05 18:04:58 +02:00
Enrico Lumetti 4376596afd Fix JSON number literals 2024-10-05 17:33:19 +02:00
Enrico Lumetti 5a90054972 function to read bril from text file 2024-10-05 17:16:13 +02:00
Enrico Lumetti 031b812c51 More tests and some refactoring 2024-10-05 17:02:03 +02:00
Enrico Lumetti 5c7e330af1 Parsing and validation of instructions 2024-10-05 16:46:10 +02:00
Enrico Lumetti 570ca0cb75 bril lieral parsing 2024-10-05 12:58:58 +02:00
Enrico Lumetti 2d3e48ed69 Barebone test/lib cabal config 2024-10-04 23:41:45 +02:00
Enrico Lumetti a8ec2f9f21 Beginning haskell code 2024-08-14 17:59:11 +02:00
Enrico Lumetti 3265b7bbb0 Move racket code to racket/ 2024-08-14 17:58:09 +02:00
45 changed files with 627 additions and 9 deletions

19
.kakrc
View File

@ -1,9 +1,10 @@
hook -once -group eoc global KakBegin .* %{ set global makecmd 'cabal build'
repl-buffer-new racket -i #hook -once -group eoc global KakBegin .* %{
repl-buffer-send-text "(require xrepl) # repl-buffer-new racket -i
" # repl-buffer-send-text "(require xrepl)
%} #"
#%}
define-command xrepl-enter %{ #
repl-buffer-send-text ",en %val{buffile}" #define-command xrepl-enter %{
} # repl-buffer-send-text ",en %val{buffile}"
#}

2
Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

17
bin/Parse.hs Normal file
View File

@ -0,0 +1,17 @@
module Main where
import Data.Either (isRight)
import qualified Data.ByteString.Lazy as B
import System.Exit
import Bril
main :: IO ()
main = do
json <- B.getContents
let parsed = parseBrilJSON json
if isRight parsed
then do
B.putStr json
exitWith ExitSuccess
else
exitWith (ExitFailure 1)

37
bril-sources/add.json Normal file
View File

@ -0,0 +1,37 @@
{
"functions": [
{
"instrs": [
{
"dest": "v0",
"op": "const",
"type": "int",
"value": 1
},
{
"dest": "v1",
"op": "const",
"type": "int",
"value": 2
},
{
"args": [
"v0",
"v1"
],
"dest": "v2",
"op": "add",
"type": "int"
},
{
"args": [
"v2"
],
"op": "print"
}
],
"name": "main"
}
]
}

58
eoc.cabal Normal file
View File

@ -0,0 +1,58 @@
cabal-version: 2.0
-- Initial package description 'eoc.cabal' generated by 'cabal init'. For
-- further documentation, see http://haskell.org/cabal/users-guide/
name: eoc
version: 0.1.0.0
-- synopsis:
-- description:
-- bug-reports:
-- license:
license-file: LICENSE
author: Enrico Lumetti
maintainer: enrico.lumetti@gmail.com
-- copyright:
-- category:
build-type: Simple
extra-source-files: README.md
library eoc-lib
exposed-modules: Eoc, Bril, Bril.CFG
hs-source-dirs: lib
build-depends:
base >=4.13 && <4.18,
text >= 2.0,
aeson >= 2.2,
sorted-list ^>= 0.2,
bytestring ^>= 0.11,
scientific ^>= 0.3,
containers ^>= 0.6.7
default-language: Haskell2010
default-extensions: DeriveGeneric, OverloadedStrings
executable parse
main-is: bin/Parse.hs
-- other-modules:
-- other-extensions:
build-depends:
base >=4.13 && <4.18,
bytestring ^>= 0.11,
eoc-lib
-- hs-source-dirs:
default-language: Haskell2010
default-extensions: DeriveGeneric, OverloadedStrings
test-suite tests
type: exitcode-stdio-1.0
main-is: Main.hs
build-depends:
base >=4.13 && <4.18,
HUnit >=1.6,
QuickCheck ^>=2.15,
eoc-lib,
text >= 2.0,
aeson >= 2.2
hs-source-dirs: tests
default-language: Haskell2010
default-extensions: DeriveGeneric, OverloadedStrings

297
lib/Bril.hs Normal file
View File

@ -0,0 +1,297 @@
module Bril (
Function (..), Type (..), Value (..), InstrOperation (..), FunctionArg (..), Program (..),
Instruction (..),
parseValue,
parseBrilFromPath, parseBrilJSON,
)where
import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON, object, withObject, withText, withBool, withScientific, (.:), (.:?), (.=), (.?=), eitherDecode)
import Data.Aeson.Types (Parser, modifyFailure)
import qualified Data.Aeson (Value, Object, Key)
import Data.Aeson.KeyMap
import Data.Int (Int64)
import qualified Data.Text as T
import Data.Text.Read (Reader, signed, decimal)
import Data.SortedList
import Data.Maybe (fromMaybe, isJust)
import Data.Scientific (isInteger, toBoundedInteger)
import qualified Data.ByteString.Lazy as LB
data Program = Program { programFns :: [Function] }
deriving (Show, Eq)
type VarName = String
type FuncName = String
data Function = Function {
functionName :: FuncName,
returnType :: Maybe Type,
functionArgs :: [FunctionArg],
instructions :: [Instruction]
} deriving (Show, Eq)
data Type = Int | Bool
deriving (Show, Eq)
data Value = IntValue Int64 | BoolValue Bool
deriving (Eq, Show)
data FunctionArg = FunctionArg {
argName :: VarName,
argType :: Type
} deriving (Show, Eq)
data Instruction =
Label String
| LiteralInstr {
value :: Value,
dest :: VarName
}
| ValueInstr {
op :: InstrOperation,
type_ :: Type,
dest :: VarName,
args :: [VarName],
funcs :: [FuncName],
labels :: [String]
}
| EffectInstr {
op :: InstrOperation,
args :: [VarName],
funcs :: [FuncName],
labels :: [String]
}
deriving (Show, Eq)
data InstrOperation =
Add | Mul | Sub | Div | Eq | Lt | Gt | Le | Ge | Not | And | Or |
Jmp | Br | Call | Ret | Id | Print | Nop | Phi
deriving (Eq)
data Arity =
Arity Int Int Int -- min, max, labels arity
| VarArgs
deriving (Eq, Show)
valueOps = toSortedList ([
"add", "mul", "sub", "div", "eq", "lt", "gt", "le", "ge", "not", "and", "or", "id"
] :: [T.Text])
effectOps = toSortedList ([
"nop", "call", "jmp", "br", "print", "ret"
] :: [T.Text])
arity instr
| instr `elem` [Add, Mul, Sub, Div, Eq, Lt, Gt, Le, Ge, And, Or] = Arity 2 2 0
| instr `elem` [Id, Not] = Arity 1 1 0
| instr == Ret = Arity 0 1 0
| instr == Jmp = Arity 0 0 1
| instr == Br = Arity 1 1 2
| instr == Nop = Arity 0 0 0
| otherwise = VarArgs
checkArity :: InstrOperation -> [b] -> [c] -> Bool
checkArity instr args labels =
case arity instr of
Arity n m l ->
(((length args) >= n) && ((length args) <= m)) && ((length labels) == l)
VarArgs -> True
instrFromString s
| s == "add" = Add
| s == "sub" = Sub
| s == "mul" = Mul
| s == "eq" = Eq
| s == "lt" = Lt
| s == "gt" = Gt
| s == "le" = Le
| s == "ge" = Ge
| s == "not" = Not
| s == "and" = And
| s == "or" = Or
| s == "jmp" = Jmp
| s == "br" = Br
| s == "ret" = Ret
| s == "call" = Call
| s == "print" = Print
| s == "id" = Id
| s == "nop" = Nop
| otherwise = error "wrong instruction name"
instance Show InstrOperation where
show op = case op of
Add -> "add"
Sub -> "sub"
Mul -> "mul"
Eq -> "eq"
Lt -> "lt"
Gt -> "gt"
Le -> "le"
Ge -> "ge"
Not -> "not"
And -> "and"
Or -> "or"
Jmp -> "jmp"
Br -> "br"
Ret -> "ret"
Call -> "call"
Print -> "print"
Id -> "id"
Nop -> "nop"
optionalArrayField :: FromJSON a => Data.Aeson.Object -> Data.Aeson.Key -> Parser [a]
optionalArrayField v field =
((v .:? field) >>= (return . fromMaybe []))
instance FromJSON Program where
parseJSON = withObject "Program" $ \v -> Program
<$> v .: "functions"
instance FromJSON Function where
parseJSON = withObject "Function" $ \v -> Function
<$> v .: "name"
<*> v .:? "type"
<*> v `optionalArrayField` "args"
<*> v .: "instrs"
instance FromJSON Instruction where
parseJSON = withObject "Instruction" $ \v ->
if (Data.Aeson.KeyMap.member "label" v)
then Label <$> v .: "label"
else do
op <- (v .: "op") :: Parser T.Text
parseInstr op v
where
parseInstr op
| op == "const" = parseLiteralInstr
| op `elemOrd` valueOps = parseValueInstr op
| op `elemOrd` effectOps = parseEffectInstr op
| otherwise = const $ fail ("Invalid op: " ++ (T.unpack op))
parseLiteralInstr v = do
type_ <- (v .: "type") :: Parser Type
LiteralInstr
<$> ((v .: "value") >>= (parseValue type_))
<*> v .: "dest"
parseValueInstr op v = do
dest <- (v .: "dest") :: Parser VarName
args <- (v `optionalArrayField` "args") :: Parser [VarName]
labels <- (v `optionalArrayField` "labels") :: Parser [VarName]
funcs <- (v `optionalArrayField` "funcs") :: Parser [VarName]
let instr = instrFromString op
if not (checkArity instr args labels)
then fail $ unexpectedArityError op args labels
else ValueInstr
<$> return instr
<*> v .: "type"
<*> v .: "dest"
<*> return args
<*> return funcs
<*> return labels
parseEffectInstr op v = do
args <- (v `optionalArrayField` "args") :: Parser [VarName]
labels <- (v `optionalArrayField` "labels") :: Parser [VarName]
funcs <- (v `optionalArrayField` "funcs") :: Parser [VarName]
dest <- (v .:? "dest") :: Parser (Maybe VarName)
if (op == "call") && ((length funcs) /= 1)
then fail $ "call op must have exactly one entry in \"funcs\""
else if (isJust dest) && (op == "call")
then parseValueInstr op v
else do
let instr = instrFromString op
if not (checkArity instr args labels)
then fail $ unexpectedArityError op args labels
else EffectInstr
<$> return instr
<*> return args
<*> return funcs
<*> return labels
unexpectedArityError op args labels =
"wrong arity for " ++ (T.unpack op) ++ ": got " ++ (show (length args)) ++ ", " ++ (show (length labels))
instance FromJSON FunctionArg where
parseJSON = withObject "FunctionArg" $ \v -> FunctionArg
<$> v .: "name"
<*> v .: "type"
instance FromJSON Type where
parseJSON = withText "Type" $ \v ->
case v of
"int" -> return Int
"bool" -> return Bool
_ -> fail "wrong type"
parseValue :: Type -> Data.Aeson.Value -> Parser Value
parseValue Bool =
withBool "BoolValue" (return . BoolValue)
parseValue Int =
withScientific "IntValue" $ \v ->
if (not (isInteger v))
then fail $ "Invalid integer literal: " ++ (show v)
else do
let num = toBoundedInteger v :: Maybe Int64
case num of
(Just n) -> return $ IntValue n
_ -> fail $ "Integer exceeds int64: " ++ (show v)
parseBrilJSON text = (eitherDecode text :: Either String Program)
parseBrilFromPath path = do
contents <- LB.readFile path
return $ parseBrilJSON contents
instance ToJSON Type where
toJSON Bool = toJSON ("bool" :: T.Text)
toJSON Int = toJSON ("int" :: T.Text)
instance ToJSON FunctionArg where
toJSON (FunctionArg argName argType) = object [
"name" .= argName,
"type" .= argType
]
instance ToJSON Value where
toJSON (IntValue val) = toJSON val
toJSON (BoolValue val) = toJSON val
instance ToJSON Instruction where
toJSON (Label name) = object [ "label" .= name ]
toJSON (LiteralInstr value dest) = object [
"op" .= ("const" :: T.Text),
"dest" .= dest,
"type" .= case value of
(IntValue _) -> Int
(BoolValue _) -> Bool,
"value" .= value
]
toJSON (EffectInstr op args funcs labels) = object [
"op" .= (show op),
"args" .= args,
"funcs" .= funcs,
"labels" .= labels
]
toJSON (ValueInstr op type_ dest args funcs labels) = object [
"op" .= (show op),
"dest" .= dest,
"type" .= type_,
"args" .= args,
"funcs" .= funcs,
"labels" .= labels
]
instance ToJSON Function where
toJSON (Function name type_ args instrs) = object $ [
"name" .= name,
"args" .= args,
"instrs" .= instrs
] ++ case type_ of
Nothing -> []
(Just t) -> ["type" .= t]
instance ToJSON Program where
toJSON (Program functions) = object [ "functions" .= functions ]

85
lib/Bril/CFG.hs Normal file
View File

@ -0,0 +1,85 @@
module Bril.CFG (BasicBlock, CFG, createCFG) where
import Bril
import qualified Data.Map.Strict as Map
data BasicBlock = BasicBlock {
instrs :: [Instruction],
incoming :: [String]
}
deriving (Show)
data CFG = CFG {
cfgBlocks :: Map.Map String BasicBlock,
cfgStart :: String
}
deriving (Show)
createCFG :: [Instruction] -> CFG
createCFG instrs =
let
(initCFG, firstLabel) = initializeCFG $ addFallthroughJmp $ splitBBs $ addInitialLabel instrs
cfgBlocks' = foldr processBB (cfgBlocks initCFG) (Map.keys (cfgBlocks initCFG))
in
CFG cfgBlocks' firstLabel
where
processBB :: String -> Map.Map String BasicBlock -> Map.Map String BasicBlock
processBB label m = addBBJumps label (outgoingCFGLabels m label) m
addBBJumps :: String -> [String] -> Map.Map String BasicBlock -> Map.Map String BasicBlock
addBBJumps bblockLabel incomingLabels m = Map.adjust (addLabels incomingLabels) bblockLabel m
addLabels :: [String] -> BasicBlock -> BasicBlock
addLabels labels (BasicBlock instrs incoming) = (BasicBlock instrs (incoming ++ labels))
initializeCFG :: [([Instruction], String)] -> (CFG, String)
initializeCFG instrs =
let mappedBlocks = foldr mapBlock Map.empty instrs -- Map.fromList $ map swap instrs
firstLabel = snd . head $ instrs
in (CFG mappedBlocks firstLabel, firstLabel)
where
mapBlock (instrs, label) m = Map.insert label (BasicBlock instrs []) m
addFallthroughJmp :: [([Instruction], String)] -> [([Instruction], String)]
addFallthroughJmp blocks =
let blocks' = map addJumpToNextBB $ zip blocks $ tail $ map snd blocks
in blocks' ++ [last blocks]
where
addJumpToNextBB (block@(instrs, label), nextLabel)
-- TODO: handle empty instrs
| isJump (last instrs) = block
| otherwise = (instrs ++ [EffectInstr Jmp [] [] [nextLabel]], label)
splitBBs :: [Instruction] -> [([Instruction], String)]
splitBBs instrs = zip (tail basicBlocks) labels
where
(basicBlocks, labels) = foldr splitBBs' ([[]], []) instrs
splitBBs' instr (bblocks, labels)
| isLabel instr = ([] : bblocks, (labelName instr) : labels)
| isJump instr = ([instr] : (tail bblocks) , labels)
| otherwise = ((instr : (head bblocks)) : (tail bblocks), labels)
addInitialLabel :: [Instruction] -> [Instruction]
addInitialLabel instrs@(firstInstr : instrs')
| isLabel firstInstr = instrs
| otherwise = (Label "@start") : instrs
addInitialLabel [] = []
labelName (Label name) = name
isLabel instr = case instr of
(Label _) -> True
_ -> False
isJump instr = case instr of
(EffectInstr op _ _ _) -> op == Jmp || op == Br
_ -> False
outgoingCFGLabels :: Map.Map String BasicBlock -> String -> [String]
outgoingCFGLabels m label =
case Map.lookup label m of
Just bb -> outgoingLabels bb
_ -> []
outgoingLabels :: BasicBlock -> [String]
outgoingLabels (BasicBlock instrs _) =
let (EffectInstr lastOp _ _ labels) = last instrs
in labels

2
lib/Eoc.hs Normal file
View File

@ -0,0 +1,2 @@
module Eoc where

3
run-tests.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
turnt bril-sources/*.json

115
tests/Main.hs Normal file
View File

@ -0,0 +1,115 @@
module Main where
import qualified Bril as B
import qualified Bril.CFG as BCFG
import Data.Aeson (eitherDecode, decode, encode, toJSON, parseJSON, Result(..))
import Data.Aeson.Types (parse)
import Data.Text
import Data.Maybe
import Data.Either
import Data.Function ((&))
import Control.Exception (try)
import Test.HUnit
import qualified Test.QuickCheck as QC
import qualified System.Exit as Exit
testTypes :: Test
testTypes = TestCase $ do
assertEqual "int type" (decode "\"int\"" :: Maybe B.Type) (Just B.Int)
assertEqual "bool type" (decode "\"bool\"" :: Maybe B.Type) (Just B.Bool)
testLiterals :: Test
testLiterals = TestCase $ do
assertEqual "true literal"
(parseLiteral B.Bool True) (Success $ B.BoolValue True)
assertEqual "false literal"
(parseLiteral B.Bool False) (Success $ B.BoolValue False)
assertEqual "int literal"
(parseLiteral B.Int (1434 :: Int)) (Success $ B.IntValue 1434)
assertEqual "wrong int literal"
(parseLiteral B.Int (100000000000000000000 :: Integer))
(Error "Integer exceeds int64: 1.0e20")
assertEqual "wrong int literal"
(parseLiteral B.Int (123.5 :: Double)) (Error "Invalid integer literal: 123.5")
where
parseLiteral type_ x = (parse (B.parseValue type_) (toJSON x))
testInstrs :: Test
testInstrs = TestCase $ do
assertEqual "add instr"
(eitherDecode "{\"op\": \"add\", \"type\": \"int\", \"dest\": \"x\", \"args\": [\"a\", \"b\"]}")
(Right $ B.ValueInstr B.Add B.Int "x" ["a", "b"] [] [])
assertEqual "sub instr"
(decode "{\"op\": \"sub\", \"type\": \"int\", \"dest\": \"x\", \"args\": [\"a\", \"b\"]}")
(Just $ B.ValueInstr B.Sub B.Int "x" ["a", "b"] [] [])
assertEqual "wrong instr"
(eitherDecode "{\"op\": \"wrnog\", \"type\": \"int\", \"dest\": \"x\", \"args\": [\"a\", \"b\"]}")
(Left "Error in $: Invalid op: wrnog" :: Either String B.Instruction)
assertEqual "wrong arity"
(eitherDecode "{\"op\": \"add\", \"type\": \"int\", \"dest\": \"x\", \"args\": [\"b\"]}")
(Left "Error in $: wrong arity for add: got 1, 0" :: Either String B.Instruction)
assertEqual "wrong label arity"
(eitherDecode "{\"op\": \"add\", \"type\": \"int\", \"dest\": \"x\", \"args\": [\"a\", \"b\"], \"labels\": [\"a\"]}")
(Left "Error in $: wrong arity for add: got 2, 1" :: Either String B.Instruction)
assertEqual "parse effect call"
(eitherDecode "{\"op\": \"call\", \"args\": [\"a\", \"b\"], \"funcs\": [\"a\"]}")
(Right $ B.EffectInstr B.Call ["a", "b"] ["a"] [])
assertEqual "parse value call"
(eitherDecode "{\"op\": \"call\", \"type\": \"bool\", \"dest\": \"z\", \"args\": [\"a\", \"b\"], \"funcs\": [\"a\"]}")
(Right $ B.ValueInstr B.Call B.Bool "z" ["a", "b"] ["a"] [])
assertEqual "parse invalid call"
(eitherDecode "{\"op\": \"call\", \"type\": \"bool\", \"dest\": \"z\", \"args\": [\"a\", \"b\"], \"funcs\": []}")
(Left "Error in $: call op must have exactly one entry in \"funcs\"" :: Either String B.Instruction)
assertEqual "parse jmp"
(eitherDecode "{\"op\": \"jmp\", \"labels\": [\"a\"]}")
(Right $ B.EffectInstr B.Jmp [] [] ["a"])
assertEqual "parse br"
(eitherDecode "{\"op\": \"br\", \"labels\": [\"a\", \"b\"], \"args\": [\"z\"]}")
(Right $ B.EffectInstr B.Br ["z"] [] ["a", "b"])
assertEqual "parse id"
(eitherDecode "{\"op\": \"id\", \"dest\": \"a\", \"type\": \"bool\", \"args\": [\"b\"]}")
(Right $ B.ValueInstr B.Id B.Bool "a" ["b"] [] [])
assertEqual "parse label"
(eitherDecode "{\"label\": \"test\"}")
(Right $ B.Label "test")
testProgramFromFiles :: Test
testProgramFromFiles = TestCase $ do
res <- try $ (B.parseBrilFromPath "bril-sources/add.json") :: IO (Either IOError (Either String B.Program))
case res of
(Right parseResult) -> assertBool "parse add.json" $ isRight parseResult
_ -> assertFailure "cannot open file"
prop_InvariantBrilSerialization :: B.Program -> Bool
prop_InvariantBrilSerialization program =
(decode (encode program)) == (Just program)
tests :: Test
tests = TestList [
TestLabel "bril parser: type" testTypes,
TestLabel "bril parser: literal" testLiterals,
TestLabel "bril parser: instruction" testInstrs,
TestLabel "bril parser: parse files" testProgramFromFiles
]
main :: IO ()
main = do
result <- runTestTT tests
-- QC.quickCheck prop_InvariantBrilSerialization
if failures result > 0 then Exit.exitFailure else Exit.exitSuccess

1
turnt.toml Normal file
View File

@ -0,0 +1 @@
command = "cabal run -v0 parse < {filename}"