diff --git a/cabal.project b/cabal.project index 920ecd76..ba959c04 100644 --- a/cabal.project +++ b/cabal.project @@ -15,4 +15,5 @@ package * -- Build static linked, vanilla libraries to reduce build time. shared: False -executable-dynamic: False \ No newline at end of file +executable-dynamic: False +tests: True \ No newline at end of file diff --git a/lam4-backend/lam4-backend.cabal b/lam4-backend/lam4-backend.cabal index bcdc902c..9500a60e 100644 --- a/lam4-backend/lam4-backend.cabal +++ b/lam4-backend/lam4-backend.cabal @@ -43,10 +43,12 @@ library Base.Aeson Base.ByteString Base.Grisette + Lam4.Main Lam4.Expr.Name Lam4.Expr.ConcreteSyntax Lam4.Expr.CommonSyntax Lam4.Expr.Parser + Lam4.Expr.Printer Lam4.Parser.Type Lam4.Parser.Monad Lam4.Expr.ConEvalAST @@ -60,7 +62,8 @@ library base, containers, foldable1-classes-compat, - lens, + lens-regex-pcre, + lens, -- just for Control.Lens.Plated optics, mtl, @@ -70,6 +73,10 @@ library aeson, aeson-optics, bytestring, + cradle, + optparse-applicative, + filepath, + string-interpolate, either, pretty-show, grisette >= 0.8, @@ -94,23 +101,35 @@ library -- bytestring, -- lam4-backend --- test-suite lam4-backend-test --- import: defaults --- default-language: GHC2021 - --- -- Modules included in this executable, other than Main. --- -- other-modules: - --- -- The interface type and version of the test suite. --- type: exitcode-stdio-1.0 - --- -- Directories containing source files. --- hs-source-dirs: test - --- -- The entrypoint to the test suite. --- main-is: Main.hs +test-suite lam4-backend-test + import: defaults + ghc-options: -threaded + default-language: GHC2021 --- -- Test dependencies. --- build-depends: --- base ^>=4.18.2.0, --- lam4-backend + -- Modules included in this executable, other than Main. + other-modules: + PrinterSpec + + -- The interface type and version of the test suite. + type: + exitcode-stdio-1.0 + + -- Directories containing source files. + hs-source-dirs: + test + build-tool-depends: + hspec-discover:hspec-discover + -- The entrypoint to the test suite. + main-is: + Spec.hs + + -- Test dependencies. + build-depends: + lam4-backend, + base, + hspec, + hspec-golden, + filepath, + directory, + text, + bytestring diff --git a/lam4-backend/src/Lam4/Expr/ConcreteSyntax.hs b/lam4-backend/src/Lam4/Expr/ConcreteSyntax.hs index b8dad28e..0c0d8bd5 100644 --- a/lam4-backend/src/Lam4/Expr/ConcreteSyntax.hs +++ b/lam4-backend/src/Lam4/Expr/ConcreteSyntax.hs @@ -28,6 +28,8 @@ module Lam4.Expr.ConcreteSyntax -- * Statements , Statement(..) , DeonticModal(..) + , Action(..) + , PrimAction(..) -- * Traversals , exprSubexprs diff --git a/lam4-backend/src/Lam4/Expr/Printer.hs b/lam4-backend/src/Lam4/Expr/Printer.hs new file mode 100644 index 00000000..4b788766 --- /dev/null +++ b/lam4-backend/src/Lam4/Expr/Printer.hs @@ -0,0 +1,471 @@ +{-# LANGUAGE FlexibleInstances, LambdaCase, OverloadedRecordDot, DisambiguateRecordFields, QuasiQuotes #-} + +module Lam4.Expr.Printer (printTree) + where +import Base.NonEmpty (NonEmpty(..)) +import qualified Base.Text as T +import Lam4.Expr.CommonSyntax +import Lam4.Expr.ConcreteSyntax +import Lam4.Expr.Name (Name (..)) +import Data.Char (isSpace) +import qualified Data.String.Interpolate as I (i) + +-- NB. Most of this file is generated by BNFC from a very ugly grammar that Inari hacked together. +-- This printer (I dare not call it pretty) is a first proof-of-concept, which I intend to throw away and make a better one. Better in the sense of more readable and maintainable. + +-- | The top-level printing method. +printTree :: Print a => a -> String +printTree = render . prt 0 + +type Doc = [ShowS] -> [ShowS] + +doc :: ShowS -> Doc +doc = (:) + +render :: Doc -> String +render d = rend 0 False (map ($ "") $ d []) "" + where + rend + :: Int -- ^ Indentation level. + -> Bool -- ^ Pending indentation to be output before next character? + -> [String] + -> ShowS + rend i p = \case + "STRUCTURE":t:"END":ts -> showString [I.i|STRUCTURE #{t} END|] . new i ts + "STRUCTURE":t :ts -> showString [I.i|STRUCTURE #{t}|] . new (i+1) ts + "END" :ts -> onNewLine (i-1) p . showString "END" . new (i-1) ts + "[" :ts -> char '[' . rend i False ts + "(" :ts -> char '(' . rend i False ts + "{": "}" :ts -> showString "{}" . rend i False ts + "{" :ts -> onNewLine i p . showChar '{' . new (i+1) ts + "}" : ";":ts -> onNewLine (i-1) p . showString "};" . new (i-1) ts + "}" :ts -> onNewLine (i-1) p . showChar '}' . new (i-1) ts + "--" : t :ts -> onNewLine i p . showString "-- " . showString t . new i ts + "/-" :ts -> onNewLine i p . showString "/-" . new (i+1) ts + "-/" :ts -> onNewLine (i-1) p . showString "-/" . new (i-1) ts + "AND" :ts -> onNewLine i p . showString "AND" . spaces 1 . rend i False ts + "FOLD_LEFT" : ts -> onNewLine (i+1) p . showString "FOLD_LEFT" . new (i+2) ts + "FOLD_RIGHT" : ts -> onNewLine (i+1) p . showString "FOLD_RIGHT" . new (i+2) ts + "using" :t:ts -> pending . showString "using" . spaces 10 . showString t . new i ts + "starting_with" :t:ts -> pending . showString "starting_with" . spaces 2 . showString t . new i ts + "over" :t:"'s":u:ts -> pending . showString "over" . spaces 11 . showString [I.i|#{t}'s #{u}|] . new (i-1) ts + "over" :t:ts -> pending . showString "over" . spaces 11 . showString t . new (i-1) ts + "FUNCTION" : ts -> onNewLine i p . showString "FUNCTION //eventual type signature" . new i ts + [";"] -> char ';' + ";" :ts -> char ';' . new i ts + t : ts@(s:_) | closingOrPunctuation s + -> pending . showString t . rend i False ts + t :ts -> pending . space t . rend i False ts + [] -> id + where + -- Output character after pending indentation. + char :: Char -> ShowS + char c = pending . showChar c + + -- Output pending indentation. + pending :: ShowS + pending = if p then indent i else id + + -- Indentation (spaces) for given indentation level. + indent :: Int -> ShowS + indent i = replicateS (2*i) (showChar ' ') + + spaces :: Int -> ShowS + spaces i = replicateS i (showChar ' ') + + + -- Continue rendering in new line with new indentation. + new :: Int -> [String] -> ShowS + new j ts = showChar '\n' . rend j True ts + + -- Make sure we are on a fresh line. + onNewLine :: Int -> Bool -> ShowS + onNewLine i p = (if p then id else showChar '\n') . indent i + + -- Separate given string from following text by a space (if needed). + space :: String -> ShowS + space t s = + case (all isSpace t, null spc, null rest) of + (True , _ , True ) -> [] -- remove trailing space + (False, _ , True ) -> t -- remove trailing space + (False, True, False) -> t ++ ' ' : s -- add space if none + _ -> t ++ s + where + (spc, rest) = span isSpace s + + closingOrPunctuation :: String -> Bool + closingOrPunctuation [c] = c `elem` closerOrPunct + closingOrPunctuation _ = False + + closerOrPunct :: String + closerOrPunct = ")],;" + +parenth :: Doc -> Doc +parenth ss = doc (showChar '(') . ss . doc (showChar ')') + +concatS :: [ShowS] -> ShowS +concatS = foldr (.) id + +concatD :: [Doc] -> Doc +concatD = foldr (.) id + +replicateS :: Int -> ShowS -> ShowS +replicateS n f = concatS (replicate n f) + +-- | The printer class does the job. + +class Print a where + prt :: Int -> a -> Doc + +instance {-# OVERLAPPABLE #-} Print a => Print [a] where + prt i = concatD . map (prt i) + +instance Print Char where + prt _ c = doc (showChar '\'' . mkEsc '\'' c . showChar '\'') + +instance Print String where + prt _ = printString + +instance Print T.Text where + prt _ x = doc (showString (backtickIfSpaces x)) + +backtickIfSpaces :: T.Text -> String +backtickIfSpaces t = if T.any (==' ') t then [I.i|`#{t}`|] else T.unpack t + +printString :: String -> Doc +printString s = doc (showChar '"' . concatS (map (mkEsc '"') s) . showChar '"') + +mkEsc :: Char -> Char -> ShowS +mkEsc q = \case + s | s == q -> showChar '\\' . showChar s + '\\' -> showString "\\\\" + '\n' -> showString "\\n" + '\t' -> showString "\\t" + s -> showChar s + +prPrec :: Int -> Int -> Doc -> Doc +prPrec i j = if j < i then parenth else id + +instance Print Int where + prt _ x = doc (shows x) + +instance Print Double where + prt _ x = doc (shows x) + + +-- Function arguments are separated by newlines +newtype FunArg = FunArg {unFunArg :: String} + +-- Anonymous functions have a special empty list +newtype AnonFunArg = AnonFunArg {unAnonFunArg :: Name} + +mkFunArg :: T.Text -> FunArg +mkFunArg t = FunArg [I.i|#{backtickIfSpaces t}|] +-- mkFunArg t = FunArg [I.i|#{backtickIfSpaces t} : TypeMissing|] -- TODO: add type information + +instance Print FunArg where + prt _ x = doc (showString (unFunArg x)) + +instance Print [FunArg] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showChar '\n'), prt 0 xs] + +instance Print AnonFunArg where + prt i x = prt i (unAnonFunArg x) + +instance Print [AnonFunArg] where + prt _ [] = doc (showString "()") + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showChar ' '), prt 0 xs] + +instance Print [Expr] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString ","), prt 0 xs] + +instance Print [Name] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString ","), prt 0 xs] + +instance Print [Statement] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString "\n\n"), prt 0 xs] + +instance Print [Decl] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString "\n\n"), prt 0 xs] + +instance Print [RowTypeDecl] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString "\n "), prt 0 xs] + +-- type Row a = [(Name, a)] where +instance Print (Row Expr) where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString ","), prt 0 xs] + +-- Building block for Row +instance Print (Name, Expr) where + prt i (name, expr) = prPrec i 0 (concatD [prt 0 name, doc (showString "="), prt 0 expr]) + +instance Print Decl where + prt i = \case + NonRec name expr -> prPrec i 0 (concatD [ + doc (showString "DEFINE") + , prt 0 (mkFunArg name.name) + , doc (showString "=") + , prt 0 expr]) + + -- Name is actually part of the Predicate. + -- doing this quick hack to move it to where it belongs. + Rec name (Predicate md vars expr) -> prPrec i 0 (concatD [prt 0 (Predicate md (name:vars) expr)]) + + Rec name (Fun md vars expr) -> prPrec i 0 (concatD [prt 0 (Fun md (name:vars) expr)]) + + -- if Expr is not Predicate or Fun, render Name here + Rec name expr -> prPrec i 0 (concatD [prt 0 name, prt 0 expr]) + + -- Name is actually part of the RecordDecl. + -- doing this quick hack to move it to where it belongs. + DataDecl name (RecordDecl rowtypedecls parents descr) -> + let newDecl = RecordDecl (dummyRowTypeDecl name:rowtypedecls) parents descr + in prPrec i 0 (concatD [prt 0 newDecl]) + + Eval (FunApp (Fun _md args body) xs) -> prPrec i 0 (concatD [ + doc (showString "@REPORT (\\") + , prt 0 (fmap AnonFunArg args) + , doc (showString "=>") + , prt 0 body + , doc (showString ")") + , parenth (prt 0 xs) + ]) + Eval expr -> prPrec i 0 (concatD [doc (showString "@REPORT"), prt 0 expr]) + +dummyRowTypeDecl :: Name -> RowTypeDecl +dummyRowTypeDecl name = MkRowTypeDecl name (TyBuiltin BuiltinTypeBoolean) (MkRowMetadata Nothing) + +instance Print RuleMetadata where + prt i md = + case md.description of + Nothing -> id + Just descr -> prPrec i 0 (concatD [doc (showString "--"), prt 0 (T.unpack descr)]) + +instance Print RecordDeclMetadata where + prt i md = + case md.description of + Nothing -> id + Just descr -> prPrec i 0 (concatD [doc (showString "/-"), doc (showString "About:"), prt 0 (T.unpack descr), doc (showString "-/")]) + +instance Print RowMetadata where + prt i md = + case md.description of + Nothing -> id + Just descr -> prPrec i 0 (concatD [ + doc (showString "--") + , prt 0 (T.unpack descr) + ]) + +instance Print DataDecl where + prt i = \case + decl@(RecordDecl [] _ _) -> error [I.i|Trying to print DataDecl without a name: #{decl}|] + RecordDecl (recname:rowtypedecls) [] metadata -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "STRUCTURE") + , prt 0 recname.name + , prt 0 rowtypedecls + , doc (showString "END") + ]) + RecordDecl (recname:rowtypedecls) parents metadata -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "STRUCTURE") + , prt 0 recname.name + , doc (showString "SPECIALIZES") + , prt 0 parents + , prt 0 rowtypedecls + , doc (showString "END") + ]) + +instance Print Expr where + prt i = \case + Var name -> prPrec i 0 (concatD [prt 0 name]) + Lit lit -> prPrec i 0 (concatD [prt 0 lit]) + Cons expr1 expr2 -> prPrec i 0 (concatD [prt 0 expr1, doc (showString "followed_by_items_in"), prt 0 expr2]) + List exprs -> prPrec i 0 (concatD [doc (showString "LIST_OF"), prt 0 exprs, doc (showString ".")]) + -- TODO: prPrec is actually supposed to do something smart with the numbers, but whatever, this will be thrown away anyway + Unary IntegerToFraction expr@(BinExpr{}) -> prPrec i 0 (concatD [prt 0 IntegerToFraction, parenth (prt 0 expr)]) + Unary IntegerToFraction expr@(Lit{}) -> parenth (prPrec i 0 (concatD [prt 0 IntegerToFraction, prt 0 expr])) + Unary unaryop expr -> prPrec i 0 (concatD [prt 0 unaryop, prt 0 expr]) + BinExpr binop expr1 expr2 -> prPrec i 0 (concatD [prt 0 expr1, prt 0 binop, prt 0 expr2]) + IfThenElse expr1 expr2 expr3 -> prPrec i 0 (concatD [doc (showString "IF"), prt 0 expr1, doc (showString "THEN"), prt 0 expr2, doc (showString "ELSE"), prt 0 expr3]) + FunApp expr exprs -> prPrec i 0 (concatD [prt 0 expr, parenth (prt 0 exprs)]) + Record rows -> prPrec i 0 (concatD [doc (showString "{|"), prt 0 rows, doc (showString "|}")]) + Project expr name -> prPrec i 0 (concatD [prt 0 expr, doc (showString "'s"), prt 0 name]) + -- f@(Fun _mdata [] _expr) -> error [I.i|Trying to print Fun without a name: #{f}|] + Fun metadata (name:names) expr -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "FUNCTION") + , prt 0 name + , parenth (prt 0 names) + , doc (showString "=") + , prt 0 expr + , doc (showString "END")]) + Fun metadata names expr -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "FUNCTION") + , parenth (prt 0 names) + , doc (showString "=") + , prt 0 expr + , doc (showString "END")]) + Let decl expr -> prPrec i 0 (concatD [doc (showString "LET"), doc (showString "{"), prt 0 decl, doc (showString "}"), doc (showString "IN"), doc (showString "{"), prt 0 expr, doc (showString "}")]) + StatementBlock (st :| sts) -> prPrec i 0 (concatD [prt 0 (st:sts)]) + NormIsInfringed name -> prPrec i 0 (concatD [prt 0 name, doc (showString "IS_INFRINGED")]) + p@(Predicate _mdata [] _expr) -> error [I.i|Trying to print Predicate without a name: #{p}|] + Predicate metadata [funname] expr -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "\nDECIDE") + , prt 0 funname + , doc (showString "\nIF") + , prt 0 expr + ]) + Predicate metadata (funname:args) expr -> + prPrec i 0 (concatD [ + prt 0 metadata + , doc (showString "GIVEN") + , parenth $ prt 0 (fmap (\a -> mkFunArg a.name) args) -- custom newtype to print args in a specific way + , doc (showString "\nDECIDE") + , prt 0 funname + , doc (showString "\nIF") + , prt 0 expr]) + PredApp f [] -> + prPrec i 0 (concatD [ + prt 0 f + , doc (showString "HOLDS?") + ]) + PredApp f [arg] -> + prPrec i 0 (concatD [ + prt 0 arg + , prt 0 f -- postfix by default. Could match prefix "is" to determine if postfix or prefix? + ]) + PredApp f [left,right] -> + prPrec i 0 (concatD [ + prt 0 left + , prt 0 f -- infix by default + , prt 0 right + ]) + PredApp f args -> prPrec i 0 (concatD [prt 0 f, prt 0 args]) + Foldr using starting over -> prPrec i 0 (concatD [ + doc (showString "FOLD_RIGHT") + , doc (showString "using") + , prt 0 using + , doc (showString "starting_with") + , prt 0 starting + , doc (showString "over") + , prt 0 over + ]) + Foldl using starting over -> prPrec i 0 (concatD [ + doc (showString "FOLD_LEFT") + , doc (showString "using") + , prt 0 using + , doc (showString "starting_with") + , prt 0 starting + , doc (showString "over") + , prt 0 over + ]) + Sig{} -> error "not yet implemented: Sig" + Relation{} -> error "not yet implemented: Relation" + +instance Print OriginalRuleRef where + prt i = \case + MkOriginalRuleRef n -> prPrec i 0 (concatD [doc (showString "\167"), prt 0 n]) + +instance Print Lit where + prt i = \case + IntLit n -> prPrec i 0 (concatD [prt 0 n]) + BoolLit bool -> prPrec i 0 (concatD [prt 0 bool]) + StringLit str -> prPrec i 0 (concatD [prt 0 str]) + FracLit frac -> prPrec i 0 (concatD [prt 0 frac]) + +instance Print Statement where + prt i = \case + IfStatement expr statement statements -> prPrec i 0 (concatD [doc (showString "IF"), prt 0 expr, doc (showString "THEN"), prt 0 statement, doc (showString "ELSE"), prt 0 statements]) + Norm name deonticmodal action -> prPrec i 0 (concatD [prt 0 name, prt 0 deonticmodal, prt 0 action]) + +instance Print Action where + prt i = \case + ActionBlock (Just name) params primactions -> prPrec i 0 (concatD [doc (showString "ACTION"), prt 0 name, doc (showString "="), doc (showString "DO"), doc (showString "{"), prt 0 params, prt 0 primactions, doc (showString "}")]) + ActionBlock Nothing params primactions -> prPrec i 0 (concatD [doc (showString "DO"), doc (showString "{"), prt 0 params, prt 0 primactions, doc (showString "}")]) + PrimitiveAction primaction -> prPrec i 0 (concatD [prt 0 primaction]) + +instance Print [PrimAction] where + prt _ [] = concatD [] + prt _ [x] = concatD [prt 0 x] + prt _ (x:xs) = concatD [prt 0 x, doc (showString "then"), prt 0 xs] + +instance Print PrimAction where + prt i = \case + Assign name expr -> prPrec i 0 (concatD [prt 0 name, doc (showString "="), prt 0 expr]) + ActionRef name -> prPrec i 0 (concatD [prt 0 name]) + +instance Print DeonticModal where + prt i = \case + Obligation -> prPrec i 0 (concatD [doc (showString "MUST")]) + Permission -> prPrec i 0 (concatD [doc (showString "MAY")]) + +instance Print UnaryOp where + prt i = \case + Not -> prPrec i 0 (concatD [doc (showString "NOT")]) + UnaryMinus -> prPrec i 0 (concatD [doc (showString "-")]) + Floor -> prPrec i 0 (concatD [doc (showString "floor")]) + Ceiling -> prPrec i 0 (concatD [doc (showString "ceiling")]) + IntegerToFraction -> prPrec i 0 (concatD [doc (showString "integer_to_fraction")]) + + +instance Print BinOp where + prt i = \case + Or -> prPrec i 0 (concatD [doc (showString "OR")]) + And -> prPrec i 0 (concatD [doc (showString "AND")]) + Plus -> prPrec i 0 (concatD [doc (showString "+")]) + Minus -> prPrec i 0 (concatD [doc (showString "-")]) + Modulo -> prPrec i 0 (concatD [doc (showString "%")]) + Mult -> prPrec i 0 (concatD [doc (showString "*")]) + Divide -> prPrec i 0 (concatD [doc (showString "/")]) + Lt -> prPrec i 0 (concatD [doc (showString "<")]) + Le -> prPrec i 0 (concatD [doc (showString "<=")]) + Gt -> prPrec i 0 (concatD [doc (showString ">")]) + Ge -> prPrec i 0 (concatD [doc (showString ">=")]) + Eq -> prPrec i 0 (concatD [doc (showString "EQUALS")]) + Ne -> prPrec i 0 (concatD [doc (showString "!=")]) + StrAppend -> prPrec i 0 (concatD [doc (showString "++")]) -- is this even in the Langium grammar yet? +instance Print Bool where + prt i = \case + True -> prPrec i 0 (concatD [doc (showString "True")]) + False -> prPrec i 0 (concatD [doc (showString "False")]) + +instance Print Name where + prt i n = prPrec i 0 (concatD [prt 0 n.name]) + +instance Print TypeExpr where + prt i = \case + TyCustom name -> prPrec i 0 (concatD [prt 0 name]) + TyBuiltin builtintype -> prPrec i 0 (concatD [prt 0 builtintype]) + TyFun arg ret -> prPrec i 0 (concatD [prt 0 arg, doc (showString "=>"), prt 0 ret]) + +instance Print TyBuiltin where + prt i = \case + BuiltinTypeString -> prPrec i 0 (concatD [doc (showString "String")]) + BuiltinTypeInteger -> prPrec i 0 (concatD [doc (showString "Integer")]) + BuiltinTypeBoolean -> prPrec i 0 (concatD [doc (showString "Boolean")]) + +instance Print RowTypeDecl where + prt i rtd = prPrec i 0 (concatD [prt 0 rtd.metadata, prt 0 rtd.name, doc (showString ":"), prt 0 rtd.typeAnnot]) diff --git a/lam4-backend/src/Lam4/Expr/ToSimala.hs b/lam4-backend/src/Lam4/Expr/ToSimala.hs index c4412649..81a246d4 100644 --- a/lam4-backend/src/Lam4/Expr/ToSimala.hs +++ b/lam4-backend/src/Lam4/Expr/ToSimala.hs @@ -99,7 +99,7 @@ compileDecl = \case ------------------------------------ NonRec name Atom {} -> let smName = lam4ToSimalaName name - in SM.NonRec defaultTransparency smName (SM.Atom $ SM.MkAtom smName) + in SM.NonRec defaultTransparency smName (SM.Atom smName) -- TODO: May not want to translate ONE SIG with no relations this way -- TODO: Not sure if naming is ok diff --git a/lam4-backend/src/Lam4/Main.hs b/lam4-backend/src/Lam4/Main.hs new file mode 100644 index 00000000..930781c6 --- /dev/null +++ b/lam4-backend/src/Lam4/Main.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} + +module Lam4.Main where + +import Base +import qualified Base.ByteString as BL +import Control.Lens.Regex.ByteString (groups, regex) +import Data.ByteString as BS hiding (concat, concatMap, + map, null, putStr) +import qualified Data.Text as T +import qualified Data.String.Interpolate as I (i) + +import Cradle +import qualified Lam4.Expr.ConcreteSyntax as CST (Decl) +import Lam4.Expr.Parser (parseProgramByteStr) +import Lam4.Expr.ToConcreteEvalAST (cstProgramToConEvalProgram) +import Lam4.Expr.ToSimala () +import qualified Lam4.Expr.ToSimala as ToSimala +import Lam4.Parser.Monad (evalParserFromScratch) +import Lam4.Expr.Printer (printTree) +import Options.Applicative as Options +import System.FilePath (()) + +data FrontendConfig = + MkFrontendConfig { frontendDir :: FilePath + , runner :: String + , args :: [String] + } + +lam4_frontend_dir :: FilePath +lam4_frontend_dir = "lam4-frontend" + +frontendConfig :: FrontendConfig +frontendConfig = MkFrontendConfig { runner = "node" + , frontendDir = lam4_frontend_dir + , args = [lam4_frontend_dir "bin" "cli", "toMinimalAst"] } + +-- TODO: Think about exposing a tracing option? +data Options = + MkOptions + { tracing :: ToSimala.TraceMode + , files :: [FilePath] + } + +-- | Copied from Simala +toTracingMode :: String -> ToSimala.TraceMode +toTracingMode "full" = ToSimala.TraceFull +toTracingMode "results" = ToSimala.TraceResults +toTracingMode "off" = ToSimala.TraceOff +toTracingMode _ = ToSimala.TraceFull + + +optionsDescription :: Options.Parser Options +optionsDescription = + MkOptions + <$> (toTracingMode <$> strOption (long "tracing" <> help "Tracing, one of \"off\", \"full\" (default), \"results\"") <|> pure ToSimala.TraceResults) + <*> many (strArgument (metavar ".l4 FILES...")) + +optionsConfig :: Options.ParserInfo Options +optionsConfig = + info (optionsDescription <**> helper) + ( fullDesc + <> header "Lam4 (an experimental variant of the L4 legal DSL)" + ) + +main :: IO () +main = do + options <- Options.execParser optionsConfig + if null options.files + then do + hPutStrLn stderr "Lam4: no input files given; use --help for help" + else do + frontendCSTJsons <- getCSTJsonFromFrontend frontendConfig options.files + let cstDecls = concatMap parseCSTByteString frontendCSTJsons + smDecls = ToSimala.compile . cstProgramToConEvalProgram $ cstDecls + putStrLn "------- Prettyprinted -------------" + mapM_ (\x -> putStrLn (printTree x) >> putStrLn "") cstDecls + print "------- CST -------------" + pPrint cstDecls + print "-------- Simala exprs ---------" + putStr $ T.unpack $ ToSimala.render smDecls + print "-------------------------------" + -- TODO: What to do if no explicit Eval? + _ <- ToSimala.doEvalDeclsTracing options.tracing ToSimala.emptyEnv smDecls + pure () + +getCSTJsonFromFrontend :: FrontendConfig -> [FilePath] -> IO [ByteString] +getCSTJsonFromFrontend config files = do + let argsForFrontendCLI = config.args <> files + (exitCode :: ExitCode, StdoutRaw rawstdout, StderrRaw err) <- run $ cmd config.runner + & addArgs argsForFrontendCLI + case exitCode of + ExitFailure _ -> + error ("Frontend parser failed:\n" <> ppShow err) + -- TODO: Improve the error reporting + ExitSuccess -> + pure $ concat $ rawstdout ^.. cstJsonTraversal + where + cstJsonTraversal = traversalVL [regex|(?s)(.*?)|] % traversalVL groups + +parseCSTByteString :: StrictByteString -> [CST.Decl] +parseCSTByteString bs = + case evalParserFromScratch . parseProgramByteStr . BL.fromStrict $ bs of + Left err -> error ("Parse error:\n" <> ppShow err) + Right cstDecls -> cstDecls + +-- Version that doesn't call error, but returns the error in the [ByteString]. +-- Because I want to print out the failure in the test. +-- TODO: nicer error logging, less copypaste +getCSTJsonFromFrontendNoFail :: FrontendConfig -> [FilePath] -> IO [ByteString] +getCSTJsonFromFrontendNoFail config files = do + let argsForFrontendCLI = config.args <> files + (exitCode :: ExitCode, StdoutRaw rawstdout, StderrRaw err) <- run $ cmd config.runner + & addArgs argsForFrontendCLI + case exitCode of + ExitFailure _ -> pure [ [I.i|trying to parse #{files}, got #{ppShow err}|] ] + -- TODO: Improve the error reporting + ExitSuccess -> + pure $ concat $ rawstdout ^.. cstJsonTraversal + where + cstJsonTraversal = traversalVL [regex|(?s)(.*?)|] % traversalVL groups \ No newline at end of file diff --git a/lam4-backend/test/PrinterSpec.hs b/lam4-backend/test/PrinterSpec.hs new file mode 100644 index 00000000..0591e3ac --- /dev/null +++ b/lam4-backend/test/PrinterSpec.hs @@ -0,0 +1,54 @@ +{-# LANGUAGE QuasiQuotes #-} + +module PrinterSpec (spec) where + +import Test.Hspec +import Test.Hspec.Golden +import Lam4.Main (getCSTJsonFromFrontendNoFail, parseCSTByteString, FrontendConfig(..)) +import Lam4.Expr.Printer (printTree) +import Control.Monad (forM_) +import Data.List (intercalate) +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.IO as TL +import System.FilePath ((<.>), (), takeBaseName) +import System.Directory (listDirectory) + +goldenGeneric :: String -> String -> Golden TL.Text +goldenGeneric name output_ = Golden + { output = TL.pack output_ + , encodePretty = TL.unpack + , writeToFile = TL.writeFile + , readFromFile = TL.readFile + , goldenFile = testPath <.> "expected" + , actualFile = Just (testPath <.> "actual") + , failFirstTime = False + } + where + testPath = "test" "testdata" "golden" "PrinterSpec" name + +spec :: Spec +spec = do + files <- runIO $ listDirectory examplesDir + forM_ files $ \file -> do + frontendCSTJsons <- runIO $ getCSTJsonFromFrontendNoFail frontendConfig [examplesDir file] + let decls = concatMap parseCSTByteString frontendCSTJsons + fname = takeBaseName file + descr = "Testing " <> fname + printedDecls = if null decls + then show frontendCSTJsons + else intercalate "\n\n" $ fmap printTree decls + testGolden descr fname printedDecls + where + examplesDir = "../examples" + +testGolden :: String -> String -> String -> Spec +testGolden desc fname expected = it desc $ goldenGeneric fname expected + +lam4_frontend_dir :: FilePath +lam4_frontend_dir = "../lam4-frontend" + +frontendConfig :: FrontendConfig +frontendConfig = MkFrontendConfig { runner = "node" + , frontendDir = lam4_frontend_dir + , args = [lam4_frontend_dir "bin" "cli", "toMinimalAst"] } + diff --git a/lam4-backend/test/Spec.hs b/lam4-backend/test/Spec.hs new file mode 100644 index 00000000..52ef578f --- /dev/null +++ b/lam4-backend/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/PostfixPredicateApplication.expected b/lam4-backend/test/testdata/golden/PrinterSpec/PostfixPredicateApplication.expected new file mode 100644 index 00000000..7d1ea7d0 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/PostfixPredicateApplication.expected @@ -0,0 +1,27 @@ + +/- + About: "A Person is pretty much what you would expect" +-/ +STRUCTURE Person + some_number : Integer +END + + + +FUNCTION //eventual type signature +f (x) = 2 +END + + + +DECIDE `some fact` +IF True + +GIVEN (person) +DECIDE `is eligible` +IF `some fact` HOLDS? +AND f (1) + person 's some_number EQUALS 2 + +DEFINE x = {| some_number = 1 |} + +@REPORT x `is eligible` \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/anon_function.expected b/lam4-backend/test/testdata/golden/PrinterSpec/anon_function.expected new file mode 100644 index 00000000..cd1db7c2 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/anon_function.expected @@ -0,0 +1,5 @@ +@REPORT (\ a b => 1 + a + b) (7, 3) + +@REPORT (\ () => 3) () + +@REPORT (\ x y z => x - y - z) (1, 2, 3) \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/arithmetic.expected b/lam4-backend/test/testdata/golden/PrinterSpec/arithmetic.expected new file mode 100644 index 00000000..1a525eea --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/arithmetic.expected @@ -0,0 +1,17 @@ + +FUNCTION //eventual type signature +f () = 3 + 5 +END + + + +FUNCTION //eventual type signature +g (x) = x * 2 - 1 / 3 +END + + +@REPORT g (0) + +@REPORT g (- 1) + +@REPORT f () \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/ceiling.expected b/lam4-backend/test/testdata/golden/PrinterSpec/ceiling.expected new file mode 100644 index 00000000..c4f0a0ef --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/ceiling.expected @@ -0,0 +1,5 @@ +DEFINE ceilinged = ceiling 0.5 + +DEFINE ceilinged2 = ceiling 0.5 + +@REPORT ceilinged EQUALS ceilinged2 \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/chained_record_deref.expected b/lam4-backend/test/testdata/golden/PrinterSpec/chained_record_deref.expected new file mode 100644 index 00000000..9149af9f --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/chained_record_deref.expected @@ -0,0 +1,27 @@ + +/- + About: "a description could go here" +-/ +STRUCTURE F + a : Integer + c : G +END + + +STRUCTURE G + b : Integer +END + + + +FUNCTION //eventual type signature +f (x) = x 's c 's b +END + + +@REPORT `test syntax highlighting` + + +FUNCTION //eventual type signature +f (x) = x 's a + 2 +END diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/cons.expected b/lam4-backend/test/testdata/golden/PrinterSpec/cons.expected new file mode 100644 index 00000000..c528fea2 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/cons.expected @@ -0,0 +1 @@ +@REPORT 1 followed_by_items_in LIST_OF 2, 3, 4 . \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/decimal.expected b/lam4-backend/test/testdata/golden/PrinterSpec/decimal.expected new file mode 100644 index 00000000..be1ae156 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/decimal.expected @@ -0,0 +1,15 @@ +@REPORT 1.333 + +@REPORT 1333.0 / 1000.0 + +@REPORT 1.0 / 1000.0 + 333.0 / 1000.0 + +@REPORT 1.0 + +@REPORT 1 + +@REPORT 0.0 + +@REPORT 0 + +@REPORT 1.0 + 0.333 \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/floor.expected b/lam4-backend/test/testdata/golden/PrinterSpec/floor.expected new file mode 100644 index 00000000..93d92466 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/floor.expected @@ -0,0 +1,3 @@ +DEFINE floored = floor 0.5 + +@REPORT 1 + floored \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/foldr_length.expected b/lam4-backend/test/testdata/golden/PrinterSpec/foldr_length.expected new file mode 100644 index 00000000..84016b1b --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/foldr_length.expected @@ -0,0 +1,27 @@ +STRUCTURE Publication END + + +STRUCTURE Applicant + publications : Publication +END + + + +FUNCTION //eventual type signature +increment (_, acc) = acc + 1 +END + + + +FUNCTION //eventual type signature +numberOfPublications (applicant) = + FOLD_RIGHT + using increment + starting_with 0 + over applicant's publications +END + + +GIVEN (applicant) +DECIDE `is eligible for grant` +IF numberOfPublications (applicant) >= 3 \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/fractionals.expected b/lam4-backend/test/testdata/golden/PrinterSpec/fractionals.expected new file mode 100644 index 00000000..dbb1b970 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/fractionals.expected @@ -0,0 +1,17 @@ +DEFINE fractional = 0.5 * 0.5 + +DEFINE negative = - 0.3 + +DEFINE squared = negative * negative + +@REPORT fractional + +@REPORT floor fractional + +@REPORT ceiling fractional + +@REPORT fractional * (integer_to_fraction 2) + +@REPORT negative + +@REPORT squared \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/function_application.expected b/lam4-backend/test/testdata/golden/PrinterSpec/function_application.expected new file mode 100644 index 00000000..746e5029 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/function_application.expected @@ -0,0 +1,11 @@ + +FUNCTION //eventual type signature +sumIntegers (x, y) = x + y +END + + + +DECIDE `some predicate` +IF sumIntegers (1, 2) > 2 + +@REPORT `some predicate` HOLDS? \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/infix_pred_app.expected b/lam4-backend/test/testdata/golden/PrinterSpec/infix_pred_app.expected new file mode 100644 index 00000000..88de8df3 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/infix_pred_app.expected @@ -0,0 +1,24 @@ + +/- + About: "A Person is pretty much what you would expect" +-/ +STRUCTURE Person END + + +GIVEN (some_person +another_person) +DECIDE `has helped` +IF True + +GIVEN (some_person +another_person) +DECIDE `has met` +IF some_person `has helped` another_person + +GIVEN (person) +DECIDE `is fun` +IF True + +GIVEN (x) +DECIDE bleh +IF x `is fun` \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/integer_to_fraction.expected b/lam4-backend/test/testdata/golden/PrinterSpec/integer_to_fraction.expected new file mode 100644 index 00000000..be9a43ee --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/integer_to_fraction.expected @@ -0,0 +1,3 @@ +@REPORT (integer_to_fraction 2) + (integer_to_fraction 1) + 0.0 + +@REPORT 2.0 - integer_to_fraction (2 - 0 + 0) \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/life_assured_predicate_fun_decl.expected b/lam4-backend/test/testdata/golden/PrinterSpec/life_assured_predicate_fun_decl.expected new file mode 100644 index 00000000..a89e0f67 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/life_assured_predicate_fun_decl.expected @@ -0,0 +1,22 @@ +STRUCTURE Accident END + + +STRUCTURE LifeAssured END + + + +-- "Calculates how much the life assured can get for their claim" +FUNCTION //eventual type signature +payout (life_assured, accident) = 0 +END + + + +-- "Checks if the life assured is eligible for a payout." +GIVEN (x) +DECIDE `is eligible for a payout` +IF True + +DEFINE x = {| |} + +@REPORT x `is eligible for a payout` \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/list.expected b/lam4-backend/test/testdata/golden/PrinterSpec/list.expected new file mode 100644 index 00000000..0a2e9ce3 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/list.expected @@ -0,0 +1 @@ +@REPORT LIST_OF 1, 2, 3, 4 . \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/list2.expected b/lam4-backend/test/testdata/golden/PrinterSpec/list2.expected new file mode 100644 index 00000000..67a839a8 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/list2.expected @@ -0,0 +1 @@ +@REPORT LIST_OF 1 + 1, 2 + 2, 3 + 3 . \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/lottery.expected b/lam4-backend/test/testdata/golden/PrinterSpec/lottery.expected new file mode 100644 index 00000000..b9ba7b76 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/lottery.expected @@ -0,0 +1,11 @@ + +/- + About: "Info about a lottery." +-/ +STRUCTURE Lottery + -- "how much can be won from the jackpot." + total_jackpot : Integer + + -- "whether buying tickets from this lottery is tax deductible. " + `tax deductible status` : Boolean +END diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/not.expected b/lam4-backend/test/testdata/golden/PrinterSpec/not.expected new file mode 100644 index 00000000..84b9ec3e --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/not.expected @@ -0,0 +1,23 @@ +STRUCTURE Applicant + publications : Integer +END + + + +FUNCTION //eventual type signature +numberOfPublications (applicant) = applicant 's publications +END + + +GIVEN (x) +DECIDE `is eligible for grant` +IF NOT x 's publications < 2 + +GIVEN (x) +DECIDE happy +IF x `is eligible for grant` +AND NOT NOT x `is eligible for grant` + +DEFINE x = {| publications = 3 |} + +@REPORT x `is eligible for grant` \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/predicate_and_givens_variations.expected b/lam4-backend/test/testdata/golden/PrinterSpec/predicate_and_givens_variations.expected new file mode 100644 index 00000000..d4bd607e --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/predicate_and_givens_variations.expected @@ -0,0 +1,20 @@ +STRUCTURE Person + age : Integer +END + + + +DECIDE `pred blah blah` +IF True + +GIVEN (x +y +z) +DECIDE pred2 +IF x 's age > 10 +AND y 's age > 10 +AND z 's age > 10 + + +DECIDE no_givens_pred +IF True \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/simple_join_or_record_field_deref.expected b/lam4-backend/test/testdata/golden/PrinterSpec/simple_join_or_record_field_deref.expected new file mode 100644 index 00000000..d738e8b4 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/simple_join_or_record_field_deref.expected @@ -0,0 +1,20 @@ +STRUCTURE Applicant + publications : Integer +END + + +DEFINE someApplicant = {| publications = 1000 |} + + +FUNCTION //eventual type signature +numberOfPublications (applicant) = applicant 's publications +END + + +GIVEN (x) +DECIDE `is eligible for grant` +IF x 's publications > 10 + +GIVEN (x) +DECIDE happy +IF x `is eligible for grant` \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/simple_life_assured.expected b/lam4-backend/test/testdata/golden/PrinterSpec/simple_life_assured.expected new file mode 100644 index 00000000..337c4fdf --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/simple_life_assured.expected @@ -0,0 +1,11 @@ + +/- + About: "Information about the life assured for this policy." +-/ +STRUCTURE LifeAssured + -- "whether policy active " + policy_active : Boolean + + -- "how old the life assured is" + age : Integer +END diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/stringlit.expected b/lam4-backend/test/testdata/golden/PrinterSpec/stringlit.expected new file mode 100644 index 00000000..51fdfcda --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/stringlit.expected @@ -0,0 +1 @@ +@REPORT "I am a lovely string literal" \ No newline at end of file diff --git a/lam4-backend/test/testdata/golden/PrinterSpec/unary_minus_integer.expected b/lam4-backend/test/testdata/golden/PrinterSpec/unary_minus_integer.expected new file mode 100644 index 00000000..5cff6851 --- /dev/null +++ b/lam4-backend/test/testdata/golden/PrinterSpec/unary_minus_integer.expected @@ -0,0 +1,21 @@ +DEFINE negativeIntegerLiteralZero = 0 + +DEFINE negativeInteger = -3 + +DEFINE squared = negativeInteger * negativeInteger + +DEFINE subtracted = 0 - 3 + 0 + 1 - 1 + +@REPORT negativeIntegerLiteralZero + +@REPORT negativeIntegerLiteralZero EQUALS 0 + +@REPORT negativeInteger + +@REPORT negativeInteger EQUALS -3 + +@REPORT squared + +@REPORT subtracted + +@REPORT subtracted * subtracted EQUALS squared \ No newline at end of file diff --git a/lam4-cli/app/Main.hs b/lam4-cli/app/Main.hs index 60a6045f..695ae2de 100644 --- a/lam4-cli/app/Main.hs +++ b/lam4-cli/app/Main.hs @@ -19,6 +19,7 @@ import Lam4.Expr.ToConcreteEvalAST (cstProgramToConEvalProgram) import Lam4.Expr.ToSimala () import qualified Lam4.Expr.ToSimala as ToSimala import Lam4.Parser.Monad (evalParserFromScratch) +import Lam4.Expr.Printer (printTree) import Options.Applicative as Options import System.FilePath (()) import System.Directory @@ -75,6 +76,8 @@ main = do frontendCSTJsons <- getCSTJsonFromFrontend frontendConfig options.files let cstDecls = concatMap parseCSTByteString frontendCSTJsons smDecls = ToSimala.compile . cstProgramToConEvalProgram $ cstDecls + putStrLn "------- Prettyprinted -------------" + mapM_ (\x -> putStrLn (printTree x) >> putStrLn "") cstDecls print "------- CST -------------" pPrint cstDecls print "-------- Simala exprs ---------"