├── .gitignore ├── ChangeLog.md ├── Dockerfile ├── LICENSE ├── README.md ├── Setup.hs ├── app └── Main.hs ├── default.nix ├── fun-with-code ├── hie.yaml ├── infix-lisp.cabal ├── package.yaml ├── src ├── Builtins.hs ├── Eval.hs ├── Lib.hs ├── Parse.hs ├── Types.hs └── lib.rs ├── stack.yaml ├── stack.yaml.lock └── test └── Spec.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | .history 3 | *~ 4 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Changelog for infix-lisp 2 | 3 | ## Unreleased changes 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fpco/stack-build-small as build 2 | 3 | 4 | ADD . /app/ 5 | WORKDIR /app 6 | RUN stack build --jobs 16 7 | RUN cp "$(stack path --local-install-root)"/bin/* /app/infix-lisp 8 | 9 | 10 | # GHC dynamically links its compilation targets to lib gmp 11 | RUN apt-get update \ 12 | && apt-get download libgmp10 13 | RUN mv libgmp*.deb /app/libgmp.deb 14 | 15 | 16 | FROM ubuntu:18.04 17 | RUN mkdir -p /app 18 | WORKDIR /app 19 | 20 | # Install lib gmp 21 | COPY --from=build /app/libgmp.deb /tmp 22 | RUN dpkg -i /tmp/libgmp.deb && rm /tmp/libgmp.deb 23 | 24 | COPY --from=build /app/infix-lisp . 25 | CMD /app/infix-lisp 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Author name here (c) 2020 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Author name here nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # infix-lisp 2 | 3 | This is infix-lisp, an esoteric functional programming language with only infix, binary functions. 4 | 5 | Despite the name, infix-lisp is not a lisp at all! 6 | (in fact, it does not have a list primitive, but uses tuple-based linked lists instead.) 7 | 8 | ## Examples 9 | 10 | ### Basics 11 | 12 | - Basic calculations 13 | ```lisp 14 | (1 + 5) 15 | (5 - 10) 16 | ((1 + 5) * 2) 17 | ``` 18 | - A lambda 19 | ```lisp 20 | [a (a + b) b] 21 | ``` 22 | - Conditionals 23 | ```lisp 24 | ("everything is fine" <(1 < 10)> "Something is very wrong here") 25 | ``` 26 | - Input and output 27 | ```lisp 28 | (_ print ("Hello, " ++ (_ readString _))) 29 | ``` 30 | 31 | ### A simple FizzBuzz 32 | 33 | This example shows an implementation of FizzBuzz. 34 | 35 | The `map` and `:` functions are part of the standard library, so in actual infix-lisp code you would not have to define these. 36 | 37 | ```lisp 38 | ( 39 | { fizzBuzz = 40 | [_ 41 | ( 42 | (nil print "FizzBuzz") 43 | <(((num % 3) == 0) && ((num % 5) == 0))> 44 | ( 45 | (nil print "Fizz") 46 | <((num % 3) == 0)> 47 | ( 48 | (nil print "buzz") 49 | <((num % 5) == 0)> 50 | (nil print num) 51 | ) 52 | ) 53 | ) 54 | num] 55 | 56 | , map = 57 | [tuple 58 | ( 59 | { tail = (_ snd tuple) , } 60 | in 61 | ( 62 | (_ f (_ fst tuple)) 63 | , 64 | ((tail map f) <(tail != nil)> nil) 65 | ) 66 | ) 67 | f] 68 | 69 | , : = 70 | [lower 71 | (lower , (((lower + 1) : upper) <(lower < upper)> nil)) 72 | upper] 73 | , } 74 | in 75 | ((1 : 100) map fizzBuzz) 76 | ) 77 | ``` 78 | 79 | 80 | ### a quine 81 | ```lisp 82 | ( { x = 83 | [_ 84 | ( 85 | _ 86 | print 87 | ( 88 | "( { x = " 89 | + 90 | ( 91 | (_ toString x) 92 | + 93 | " } in ( _ x _ ) ) " 94 | ) 95 | ) 96 | ) 97 | _] 98 | } 99 | in 100 | (_ x _) 101 | ) 102 | 103 | ``` 104 | 105 | ## Syntax 106 | 107 | The syntax of infix-lisp is actually quite simple,... it's just also very ugly. 108 | Have fun reading this! 109 | 110 | ### Literals 111 | 112 | Infix-lisp supports string, integer and list literals. 113 | 114 | - String literals are strings surrounded by either `"` or `'` 115 | - Integer literals work as you would expect (`12`) 116 | - list literal syntax is values seperated by commas, surrounded by `{}`, i.e.: `{ 1, 2, 3, "hi", "ho", 5 }` 117 | 118 | ### Function invocation 119 | 120 | A lot of things, including many symbols, are valid identifiers and thus function names in infix-lisp. 121 | Invoking a function is as simple as putting it in parentheses, between it's two arguments, for example `(1 + 5)`. 122 | 123 | Note that the spaces around the function name as well as the parentheses are required here. 124 | 125 | ### Lambdas 126 | 127 | A lambda takes the shape `[argName1 body argName2]`, where `body` can be any piece of code, i.e.: `[a (a + b) b]`. 128 | 129 | If you want to define a function with a name, you'll have to bind a lambda to a variable. 130 | 131 | ### Bindings 132 | 133 | Everything in infix-lisp has to evaluate to some value. Thus, you cannot define top-level variables, as these would not themselves evaluate to anything. 134 | 135 | To define bindings, you use the `({ name = value, name2 = value2 } in block)` syntax, which will make the defined variables available to use in the block. 136 | Example: 137 | 138 | ```lisp 139 | ( 140 | { number1 = 1 141 | , number2 = 2 142 | , plus = [a (a + b) b] 143 | } 144 | in 145 | (number1 plus number2) 146 | ) 147 | ``` 148 | 149 | Note that in this example, `plus = +` would have been equally valid. 150 | 151 | ### Conditionals 152 | 153 | In infix-lisp, there is no such thing as an `if` without an `else`. 154 | 155 | The syntax for conditionals is `(yesBody noBody)`, where `yesBody`, `condition` and `noBody` can all be arbitrary expressions. 156 | The `condition` expression has to evaluate to a boolean. 157 | Examples: 158 | 159 | ```lisp 160 | ("hi" <(1 == 12)> "ho") 161 | ("hi" "ho") 162 | ``` 163 | 164 | ### Tuples 165 | 166 | A tuple can be created using the `,` function, i.e.: `(1 , 2)`. 167 | The values of that tuple can be accessed with the `fst` and `snd` functions: 168 | 169 | ```lisp 170 | ((nil fst (1 , 2)) == 1) 171 | ((nil snd (1 , 2)) == 2) 172 | ``` 173 | 174 | ### Lists 175 | 176 | Infix-lisp does not have a built-in list primitive. Instead, you just use tuples to build up a linked list: 177 | 178 | ```lisp 179 | (1 , ( 2 , ( 3 , ( 4 , nil ) ) )) 180 | ``` 181 | 182 | As good ergonomics are obviously one of the biggest design goals of infix-lisp, 183 | you can also use the 184 | 185 | ```lisp 186 | { 1, 2, 3, 4 } 187 | ``` 188 | 189 | syntax to create the same value. 190 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | module Main where 3 | 4 | import Parse 5 | import Eval 6 | import Control.Monad ( when ) 7 | import Control.Monad.IO.Class ( liftIO ) 8 | import Control.Exception ( catch ) 9 | import qualified Text.Megaparsec.Error 10 | import Types 11 | import qualified Data.Text as T 12 | import System.Console.Repline 13 | import System.Environment 14 | 15 | type Repl a = HaskelineT IO a 16 | 17 | cmd :: String -> Repl () 18 | cmd input = dontCrash $ do 19 | result <- liftIO $ testEvalExp input 20 | liftIO $ putStrLn $ ">> " ++ show result 21 | pure () 22 | 23 | 24 | ini :: Repl () 25 | ini = 26 | liftIO $ putStrLn "Have fun in one of the dumbest languages in existence!" 27 | 28 | final :: Repl ExitDecision 29 | final = pure Exit 30 | 31 | 32 | completer :: Monad m => WordCompleter m 33 | completer word = pure [] 34 | 35 | repl :: IO () 36 | repl = evalRepl (const $ pure ">>> ") cmd [] Nothing Nothing (Word completer) ini final 37 | 38 | main :: IO () 39 | main = do 40 | args <- getArgs 41 | if null args then repl else runFile (head args) 42 | 43 | runFile :: String -> IO () 44 | runFile path = do 45 | code <- readFile path 46 | result <- testEvalExp code 47 | pure () 48 | 49 | 50 | trustMe (Right x) = x 51 | trustMe (Left e) = error $ Text.Megaparsec.Error.errorBundlePretty e 52 | 53 | testEvalExp :: String -> IO Value 54 | testEvalExp s = evalWithStdlib mempty (trustMe $ parse (T.pack s)) 55 | 56 | 57 | --( 58 | --(num = (nil readInt nil)) 59 | --in 60 | --( 61 | --(((num % 3) == 0) && ((num % 5) == 0)) 62 | --then 63 | --( 64 | --(nil print "FizzBuzz") 65 | --else 66 | --( 67 | --((num % 3) == 0) 68 | --then 69 | --( 70 | --(nil print "Fizz") 71 | --else 72 | --( 73 | --((num % 5) == 0) 74 | --then 75 | --( 76 | --(nil print "buzz") 77 | --else 78 | --(nil print num) 79 | --) 80 | --) 81 | --) 82 | --) 83 | --) 84 | --) 85 | --) 86 | 87 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | # Fetch the latest haskell.nix and import its default.nix 3 | haskellNix ? import (builtins.fetchTarball "https://github.com/input-output-hk/haskell.nix/archive/master.tar.gz") {} 4 | 5 | # haskell.nix provides access to the nixpkgs pins which are used by our CI, 6 | # hence you will be more likely to get cache hits when using these. 7 | # But you can also just use your own, e.g. ''. 8 | , nixpkgsSrc ? haskellNix.sources.nixpkgs-2003 9 | 10 | # haskell.nix provides some arguments to be passed to nixpkgs, including some 11 | # patches and also the haskell.nix functionality itself as an overlay. 12 | , nixpkgsArgs ? haskellNix.nixpkgsArgs 13 | 14 | # import nixpkgs with overlays 15 | , pkgs ? import nixpkgsSrc nixpkgsArgs 16 | }: pkgs.haskell-nix.project { 17 | # 'cleanGit' cleans a source directory based on the files known by git 18 | src = pkgs.haskell-nix.haskellLib.cleanGit { 19 | name = "infix-lisp"; 20 | src = ./.; 21 | }; 22 | # For `cabal.project` based projects specify the GHC version to use. 23 | compiler-nix-name = "ghc882"; # Not used for `stack.yaml` based projects. 24 | } 25 | 26 | 27 | 28 | #{ nixpkgs ? import {} 29 | #, ghc ? nixpkgs.ghc 30 | #}: 31 | #nixpkgs.haskell.lib.buildStackProject { 32 | #inherit ghc; 33 | #name = "infix-lisp"; 34 | #src = ./.; 35 | #} 36 | -------------------------------------------------------------------------------- /fun-with-code: -------------------------------------------------------------------------------- 1 | 2 | ( 3 | (num = (nil readInt nil)) 4 | in 5 | ( 6 | (nil print "FizzBuzz") 7 | <(((num % 3) == 0) && ((num % 5) == 0))> 8 | ( 9 | (nil print "Fizz") 10 | <((num % 3) == 0)> 11 | ( 12 | (nil print "buzz") 13 | <((num % 5) == 0)> 14 | (nil print num) 15 | ) 16 | ) 17 | ) 18 | ) 19 | 20 | 21 | ( 22 | { fizzBuzz = 23 | [_ 24 | ( 25 | (nil print "FizzBuzz") 26 | <(((num % 3) == 0) && ((num % 5) == 0))> 27 | ( 28 | (nil print "Fizz") 29 | <((num % 3) == 0)> 30 | ( 31 | (nil print "buzz") 32 | <((num % 5) == 0)> 33 | (nil print num) 34 | ) 35 | ) 36 | ) 37 | num] 38 | 39 | , map = 40 | [tuple 41 | ( 42 | { tail = (_ snd tuple) , } 43 | in 44 | ( 45 | (_ f (_ fst tuple)) 46 | , 47 | ((tail map f) <(tail != nil)> nil) 48 | ) 49 | ) 50 | f] 51 | 52 | , : = 53 | [lower 54 | (lower , (((lower + 1) : upper) <(lower < upper)> nil)) 55 | upper] 56 | , } 57 | in 58 | ((1 : 100) map fizzBuzz) 59 | ) 60 | 61 | 62 | ( 63 | (map = [tuple 64 | ( 65 | (tail = (_ snd tuple)) 66 | in 67 | ( 68 | (_ f (_ fst tuple)) 69 | , 70 | ((tail map f) <(tail != nil)> nil) 71 | ) 72 | ) 73 | f]) 74 | in 75 | ((1 , (2 , (3 , nil))) map [_ (x + 1) x]) 76 | ) 77 | 78 | 79 | ( 80 | (: = 81 | [lower 82 | (lower , ((lower + 1) : upper) <(lower < upper)> nil) 83 | upper] 84 | ) 85 | in 86 | (1 : 10) 87 | ) 88 | 89 | 90 | 91 | 92 | ( (map = [tuple ( (_ f (_ fst tuple)) , ( ((_ snd tuple) map f) <((_ snd tuple) != nil)> nil)) f]) in ((1 , (2 , (3 , nil))) map [_ (x + 1) x])) 93 | 94 | 95 | ( (map = [tuple ( (_ f (_ fst tuple)) , ( (snd map (_ snd tuple)) <((_ snd tuple) != nil)> nil)) f]) in ((1 , (2 , (3 , nil))) map [_ (x + 1) x])) 96 | 97 | 98 | ((flip2 = [_ [a [b (a (b f c) d) c] d] f]) in ((x = [a [b ((a + b) + (c + d)) c] d]) in ("a" ("b" (nil flip2 x) "c") "d"))) 99 | 100 | 101 | ( (num = (nil readInt nil)) in ( (nil print "FizzBuzz") <(((num % 3) == 0) && ((num % 5) == 0))> ( (nil print "Fizz") <((num % 3) == 0)> ( (nil print "buzz") <((num % 5) == 0)> (nil print num))))) 102 | -------------------------------------------------------------------------------- /hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | stack: 3 | -------------------------------------------------------------------------------- /infix-lisp.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.33.0. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: d531ff53d53e24b1a0138ea4e8db9224f7b8fcc5c66293447da444abe1c8b878 8 | 9 | name: infix-lisp 10 | version: 0.1.0.0 11 | description: Please see the README on GitHub at 12 | homepage: https://github.com/githubuser/infix-lisp#readme 13 | bug-reports: https://github.com/githubuser/infix-lisp/issues 14 | author: Author name here 15 | maintainer: example@example.com 16 | copyright: 2020 Author name here 17 | license: BSD3 18 | license-file: LICENSE 19 | build-type: Simple 20 | extra-source-files: 21 | README.md 22 | ChangeLog.md 23 | 24 | source-repository head 25 | type: git 26 | location: https://github.com/githubuser/infix-lisp 27 | 28 | library 29 | exposed-modules: 30 | Builtins 31 | Eval 32 | Lib 33 | Parse 34 | Types 35 | other-modules: 36 | Paths_infix_lisp 37 | hs-source-dirs: 38 | src 39 | build-depends: 40 | base >=4.7 && <5 41 | , containers >=0.6 42 | , megaparsec >=8.0.0 && <8.1 43 | , repline >=0.4.0.0 && <0.5.0.0 44 | , string-qq ==0.0.4 45 | , text 46 | default-language: Haskell2010 47 | 48 | executable infix-lisp-exe 49 | main-is: Main.hs 50 | other-modules: 51 | Paths_infix_lisp 52 | hs-source-dirs: 53 | app 54 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 55 | build-depends: 56 | base >=4.7 && <5 57 | , containers >=0.6 58 | , infix-lisp 59 | , megaparsec >=8.0.0 && <8.1 60 | , repline >=0.4.0.0 && <0.5.0.0 61 | , string-qq ==0.0.4 62 | , text 63 | default-language: Haskell2010 64 | 65 | test-suite infix-lisp-test 66 | type: exitcode-stdio-1.0 67 | main-is: Spec.hs 68 | other-modules: 69 | Paths_infix_lisp 70 | hs-source-dirs: 71 | test 72 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 73 | build-depends: 74 | base >=4.7 && <5 75 | , containers >=0.6 76 | , infix-lisp 77 | , megaparsec >=8.0.0 && <8.1 78 | , repline >=0.4.0.0 && <0.5.0.0 79 | , string-qq ==0.0.4 80 | , text 81 | default-language: Haskell2010 82 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | library: 2 | source-dirs: src 3 | tests: 4 | infix-lisp-test: 5 | source-dirs: test 6 | main: Spec.hs 7 | ghc-options: 8 | - -threaded 9 | - -rtsopts 10 | - -with-rtsopts=-N 11 | dependencies: 12 | - infix-lisp 13 | copyright: 2020 Author name here 14 | maintainer: example@example.com 15 | dependencies: 16 | - text 17 | - string-qq == 0.0.4 18 | - repline >= 0.4.0.0 && < 0.5.0.0 19 | - containers >= 0.6 20 | - base >= 4.7 && < 5 21 | - megaparsec >= 8.0.0 && < 8.1 22 | name: infix-lisp 23 | version: 0.1.0.0 24 | extra-source-files: 25 | - README.md 26 | - ChangeLog.md 27 | author: Author name here 28 | github: githubuser/infix-lisp 29 | license: BSD3 30 | executables: 31 | infix-lisp-exe: 32 | source-dirs: app 33 | main: Main.hs 34 | ghc-options: 35 | - -threaded 36 | - -rtsopts 37 | - -with-rtsopts=-N 38 | dependencies: 39 | - infix-lisp 40 | description: Please see the README on GitHub at 41 | -------------------------------------------------------------------------------- /src/Builtins.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-missing-signatures -fno-warn-missing-pattern-synonym-signatures #-} 2 | module Builtins 3 | ( builtins 4 | ) 5 | where 6 | import Types 7 | import qualified Data.Map.Strict as M 8 | import qualified Parse 9 | 10 | 11 | builtins :: M.Map String Function 12 | builtins = M.fromList 13 | [ ("+" , Builtin builtinPlus) 14 | , ("-" , Builtin builtinMinus) 15 | , ("*" , Builtin builtinTimes) 16 | , ("/" , Builtin builtinDiv) 17 | , ("%" , Builtin builtinMod) 18 | , ("<" , Builtin builtinLT) 19 | , (">" , Builtin builtinGT) 20 | , ("&&" , Builtin builtinAnd) 21 | , ("||" , Builtin builtinOr) 22 | , ("==" , Builtin builtinEq) 23 | , ("!=" , Builtin builtinNeq) 24 | , ("," , Builtin builtinMakeTuple) 25 | , ("fst" , Builtin builtinFirst) 26 | , ("snd" , Builtin builtinSecond) 27 | , ("head" , Builtin builtinFirst) 28 | , ("tail" , Builtin builtinSecond) 29 | , ("toCharList", Builtin builtinToCharList) 30 | , ("print" , Builtin builtinPrint) 31 | , ("readString", Builtin builtinReadString) 32 | , ("readFile" , Builtin builtinReadFile) 33 | , ("readInt" , Builtin builtinReadInt) 34 | , ("toString" , Builtin builtinToString) 35 | ] 36 | where 37 | builtinPlus (VNum a) (VNum b) = pure . VNum $ a + b 38 | builtinPlus (VStr a) (VStr b) = pure . VStr $ a ++ b 39 | builtinPlus a b = illegalFunctionArguments "+" [a, b] 40 | 41 | builtinMinus = numFunc "-" $ \a b -> VNum $ a - b 42 | builtinTimes = numFunc "*" $ \a b -> VNum $ a * b 43 | builtinMod = numFunc "%" $ \a b -> VNum $ a `mod` b 44 | builtinDiv = numFunc "/" $ \a b -> VNum $ a `div` b 45 | builtinLT = numFunc "<" $ \a b -> VBool $ a < b 46 | builtinGT = numFunc ">" $ \a b -> VBool $ a > b 47 | 48 | builtinAnd = boolFunc "&&" (&&) 49 | builtinOr = boolFunc "||" (||) 50 | 51 | builtinEq a b = pure . VBool $ a == b 52 | builtinNeq a b = pure . VBool $ a /= b 53 | 54 | builtinPrint _ value = 55 | let str = case value of 56 | VStr s -> s 57 | VNum n -> show n 58 | VNil -> "nil" 59 | x -> show x 60 | in putStrLn str >> pure VNil 61 | 62 | builtinReadString _ _ = VStr <$> getLine 63 | builtinReadInt _ _ = VNum <$> readLn 64 | 65 | builtinReadFile _ (VStr path) = VStr <$> readFile path 66 | builtinReadFile a b = illegalFunctionArguments "readFile" [a, b] 67 | 68 | builtinMakeTuple a b = pure $ VTuple a b 69 | 70 | builtinFirst _ (VTuple a _) = pure a 71 | builtinFirst _ (VStr (x:_)) = pure $ VStr [x] 72 | builtinFirst a b = illegalFunctionArguments "fst" [a, b] 73 | 74 | builtinSecond _ (VTuple _ b) = pure b 75 | builtinSecond _ (VStr (_:xs)) = pure $ VStr xs 76 | builtinSecond a b = illegalFunctionArguments "snd" [a, b] 77 | 78 | builtinToString _ value = pure . VStr $ show value 79 | 80 | builtinToCharList :: Value -> Value -> IO Value 81 | builtinToCharList _ (VStr str) = case str of 82 | (x : xs) -> VTuple (VStr [x]) <$> builtinToCharList VNil (VStr xs) 83 | [] -> pure VNil 84 | builtinToCharList a b = illegalFunctionArguments "toCharList" [a, b] 85 | 86 | boolFunc :: String -> (Bool -> Bool -> Bool) -> Value -> Value -> IO Value 87 | boolFunc _ f (VBool a) (VBool b) = pure . VBool $ f a b 88 | boolFunc name _ a b = illegalFunctionArguments name [a, b] 89 | 90 | numFunc :: String -> (Int -> Int -> Value) -> Value -> Value -> IO Value 91 | numFunc _ f (VNum a) (VNum b) = pure $ f a b 92 | numFunc name _ a b = illegalFunctionArguments name [a, b] 93 | 94 | 95 | 96 | 97 | illegalFunctionArguments :: String -> [Value] -> a 98 | illegalFunctionArguments functionName args = 99 | error $ functionName ++ " called with illegal arguments " ++ show args 100 | 101 | -------------------------------------------------------------------------------- /src/Eval.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-missing-signatures -fno-warn-missing-pattern-synonym-signatures #-} 2 | {-# LANGUAGE LambdaCase, PatternSynonyms, OverloadedStrings, QuasiQuotes #-} 3 | module Eval where 4 | 5 | import Prelude hiding ( exp ) 6 | import Parse 7 | import qualified Data.List as L 8 | import Data.Maybe ( fromMaybe ) 9 | import Data.Bifunctor ( bimap 10 | , first 11 | , second 12 | ) 13 | import Builtins 14 | import Types 15 | import qualified Data.Map.Strict as M 16 | import qualified Data.Either 17 | import qualified Data.Text as T 18 | import Data.String.QQ 19 | 20 | 21 | runFunction :: Function -> Env -> Value -> Value -> IO Value 22 | runFunction (Builtin f ) _ arg1 arg2 = f arg1 arg2 23 | runFunction (Lambda lambdaEnv argName1 exp argName2) env arg1 arg2 = evalExp 24 | (envFromArgs <> lambdaEnv <> env) 25 | exp 26 | where 27 | envFromArgs = Env $ M.fromList $ case (argName1, argName2) of 28 | (Ident a , Ident b ) -> [(a, arg1), (b, arg2)] 29 | (IdentIgnored, Ident b ) -> [(b, arg2)] 30 | (Ident a , IdentIgnored) -> [(a, arg1)] 31 | _ -> [] 32 | 33 | 34 | evalWithStdlib :: Env -> NExp -> IO Value 35 | evalWithStdlib env exp = do 36 | stdEnv <- stdlib 37 | evalExp (env <> stdEnv) exp 38 | 39 | 40 | evalExp :: Env -> NExp -> IO Value 41 | evalExp env expression = case expression of 42 | ExpLit lit -> pure $ fromLiteral lit 43 | 44 | ExpIdent (Ident ident) -> pure $ envLookup env ident 45 | ExpIdent IdentIgnored -> error "Tried to reference ignored identifier" 46 | 47 | ExpLambda arg1 body arg2 -> pure $ VFunction $ Lambda env arg1 body arg2 48 | 49 | ExpInvocation arg1 exp arg2 -> evalExp env exp >>= \case 50 | VFunction func -> do 51 | res1 <- evalExp env arg1 52 | res2 <- evalExp env arg2 53 | runFunction func env res1 res2 54 | wannabeFunction -> error $ show wannabeFunction ++ " is not a function" 55 | 56 | ExpInBinding bindings blockExp -> do 57 | bindings' <- evalBindings env (fmap (first getName) bindings) 58 | evalExp (bindings' <> env) blockExp 59 | 60 | ExpCondition yesExp condExp noExp -> do 61 | condResult <- evalExp env condExp 62 | case condResult of 63 | VBool True -> evalExp env yesExp 64 | VBool False -> evalExp env noExp 65 | _ -> error $ "condition " ++ show condResult ++ " is not a boolean" 66 | 67 | 68 | 69 | evalBindings :: Env -> [(String, NExp)] -> IO Env 70 | evalBindings _ [] = pure mempty 71 | evalBindings env ((name, exp) : xs) = do 72 | result <- evalExp env exp 73 | let newEnv = Env (M.singleton name result) <> env 74 | otherBindings <- evalBindings newEnv xs 75 | pure $ Env (M.singleton name result) <> otherBindings 76 | 77 | fromLiteral :: NLiteral -> Value 78 | fromLiteral lit = case lit of 79 | StringLit x -> VStr x 80 | IntLit x -> VNum x 81 | BoolLit x -> VBool x 82 | NilLit -> VNil 83 | 84 | envLookup :: Env -> String -> Value 85 | envLookup (Env env) name = case M.lookup name envWithBuiltins of 86 | Just value -> value 87 | Nothing -> error $ "Variable `" ++ name ++ "` is not in scope." 88 | where envWithBuiltins = env <> M.map VFunction builtins 89 | 90 | getName :: NIdent -> String 91 | getName (Ident x) = x 92 | getName IdentIgnored = error "tried to get the name of ignored identifier `_`" 93 | 94 | 95 | stdlib :: IO Env 96 | stdlib = evalBindings mempty $ Data.Either.rights $ map 97 | (getParsed . second parse) 98 | [ ( "map" 99 | , [s| 100 | [tuple ( 101 | { tail = (_ snd tuple) , } 102 | in 103 | ( 104 | (_ f (_ fst tuple)) 105 | , 106 | ((tail map f) <(tail != nil)> nil) 107 | ) 108 | ) f] 109 | |] 110 | ) 111 | , ( "filter" 112 | , [s| 113 | [tuple ( 114 | { head = (_ fst tuple) 115 | , tail = (_ snd tuple) 116 | , filteredTail = ((tail filter cond) <(tail != nil)> nil) 117 | , } 118 | in 119 | ((head , filteredTail) <(_ cond head)> filteredTail) 120 | ) cond] 121 | |] 122 | ) 123 | , ( ":" 124 | , [s| 125 | [lower ( lower , ( ((lower + 1) : upper) <(lower < upper)> nil)) upper] 126 | |] 127 | ) 128 | , ( "!!" 129 | , [s| 130 | [tuple ( 131 | (_ fst tuple) 132 | <(idx == 0)> 133 | ((_ snd tuple) !! (idx - 1)) 134 | ) idx] 135 | |] 136 | ) 137 | ] 138 | where 139 | getParsed :: (a, Either e b) -> Either e (a, b) 140 | getParsed (a, Right b) = Right (a, b) 141 | getParsed (_, Left e ) = Left e 142 | 143 | -------------------------------------------------------------------------------- /src/Lib.hs: -------------------------------------------------------------------------------- 1 | module Lib 2 | ( ) 3 | where 4 | 5 | -------------------------------------------------------------------------------- /src/Parse.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-missing-signatures #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module Parse 4 | ( NExp(..) 5 | , NLiteral(..) 6 | , NIdent(..) 7 | , parseTest 8 | , parse 9 | ) 10 | where 11 | 12 | import Prelude hiding ( exp ) 13 | import qualified Text.Megaparsec as P 14 | import qualified Text.Megaparsec.Char as P 15 | import qualified Text.Megaparsec.Char.Lexer as L 16 | import Text.Megaparsec ( () ) 17 | import Text.Megaparsec.Debug ( dbg ) 18 | import Data.Functor ( ($>) 19 | , void 20 | ) 21 | import Data.Void ( Void ) 22 | import Control.Applicative ( (<|>) ) 23 | import qualified Data.Text as T 24 | import Data.List ( intercalate ) 25 | 26 | data NExp = ExpInBinding [(NIdent, NExp)] NExp 27 | | ExpCondition NExp NExp NExp 28 | | ExpInvocation NExp NExp NExp 29 | | ExpLambda NIdent NExp NIdent 30 | | ExpLit NLiteral 31 | | ExpIdent NIdent 32 | deriving (Eq) 33 | 34 | data NLiteral = IntLit Int 35 | | StringLit String 36 | | BoolLit Bool 37 | | NilLit deriving (Eq) 38 | 39 | data NIdent = Ident String | IdentIgnored deriving (Eq) 40 | 41 | instance Show NIdent where 42 | show (Ident s) = s 43 | show IdentIgnored = "_" 44 | 45 | instance Show NLiteral where 46 | show (IntLit num ) = show num 47 | show (StringLit str ) = show str 48 | show (BoolLit bool) = if bool then "true" else "false" 49 | show NilLit = "nil" 50 | 51 | 52 | instance Show NExp where 53 | show (ExpCondition yesBody cond noBody) = 54 | unwords ["(", show yesBody, "<", show cond, ">", show noBody, ")"] 55 | show (ExpInBinding bindings exp) = unwords 56 | [ "( {" 57 | , intercalate ", " $ map (\(k, v) -> show k ++ " = " ++ show v) bindings 58 | , "} in" 59 | , show exp 60 | , ")" 61 | ] 62 | show (ExpInvocation exp1 ident exp2) = 63 | unwords ["(", show exp1, show ident, show exp2, ")"] 64 | show (ExpLambda arg1 body arg2) = 65 | unwords ["[", show arg1, show body, show arg2, "]"] 66 | show (ExpLit lit ) = show lit 67 | show (ExpIdent ident) = show ident 68 | 69 | 70 | type Parser = P.Parsec Void T.Text 71 | 72 | parseTest :: String -> IO () 73 | parseTest = P.parseTest parseExp . T.pack 74 | 75 | parse :: T.Text -> Either (P.ParseErrorBundle T.Text Void) NExp 76 | parse = P.runParser (surroundedByMany hiddenSpaceChar parseExp) "" 77 | 78 | 79 | 80 | parseExp :: Parser NExp 81 | parseExp = P.choice 82 | [ P.try parseInBinding 83 | , P.try parseConditional 84 | , P.try parseExpInvocation 85 | , parseList 86 | , parseLambda 87 | , ExpLit <$> parseLit 88 | , ExpIdent <$> parseIdent 89 | ] 90 | where 91 | parseExpInvocation :: Parser NExp 92 | parseExpInvocation = 93 | P.label "invocation block" 94 | $ parens 95 | $ ExpInvocation 96 | <$> (parseExp <|> (ExpLit NilLit <$ P.char '_')) 97 | <*> surroundedBy (P.some hiddenSpaceChar) invokableExpBlock 98 | <*> (parseExp <|> (ExpLit NilLit <$ P.char '_')) 99 | 100 | parseLambda :: Parser NExp 101 | parseLambda = 102 | P.label "lambda" 103 | $ brackets 104 | $ ExpLambda 105 | <$> (parseIdent <|> parseIgnoredIdent) 106 | <*> surroundedBy (P.some hiddenSpaceChar) parseExp 107 | <*> (parseIdent <|> parseIgnoredIdent) 108 | 109 | parseInBinding :: Parser NExp 110 | parseInBinding = P.label "in-binding" $ parens 111 | ( ExpInBinding 112 | <$> parseMapLiteral 113 | <* surroundedBySome hiddenSpaceChar (P.string "in") 114 | <*> parseExp 115 | ) 116 | 117 | parseMapLiteral :: Parser [(NIdent, NExp)] 118 | parseMapLiteral = 119 | P.label "map-literal" 120 | . curlies 121 | . surroundedByMany hiddenSpaceChar 122 | $ parseMultipleBindings 123 | where 124 | parseMultipleBindings = P.sepEndBy 125 | parseSingleBinding 126 | (P.try $ surroundedByMany hiddenSpaceChar (P.char ',')) 127 | 128 | parseSingleBinding = 129 | (,) 130 | <$> parseIdent 131 | <* surroundedBySome hiddenSpaceChar (P.char '=') 132 | <*> parseExp 133 | 134 | parseList :: Parser NExp 135 | parseList = do 136 | results <- P.label "list-expression" $ curlies $ surroundedByMany 137 | hiddenSpaceChar 138 | (parseExp `P.sepBy` surroundedByMany hiddenSpaceChar (P.char ',')) 139 | pure $ listToTupleList results 140 | where 141 | listToTupleList [] = ExpLit NilLit 142 | listToTupleList (x : xs) = 143 | ExpInvocation x (ExpIdent (Ident ",")) (listToTupleList xs) 144 | 145 | parseConditional :: Parser NExp 146 | parseConditional = 147 | P.label "condition" 148 | $ parens 149 | $ ExpCondition 150 | <$> parseExp 151 | <*> surroundedBySome hiddenSpaceChar (betweenChars '<' '>' parseExp) 152 | <*> parseExp 153 | 154 | 155 | invokableExpBlock :: Parser NExp 156 | invokableExpBlock = 157 | P.choice [parseExpInvocation, parseLambda, ExpIdent <$> parseIdent] 158 | 159 | 160 | parseIdent :: Parser NIdent 161 | parseIdent = P.label "identifier" $ P.choice 162 | [ Ident <$> ((:) <$> P.letterChar <*> P.many P.alphaNumChar) 163 | , Ident <$> P.some (P.oneOf ("/+.-*=$!%&,<>:" :: String)) 164 | ] 165 | 166 | parseIgnoredIdent :: Parser NIdent 167 | parseIgnoredIdent = IdentIgnored <$ P.hidden (P.char '_') 168 | 169 | parseLit :: Parser NLiteral 170 | parseLit = P.label "literal" $ P.choice 171 | [ parseStringLit 172 | , parseBoolLit 173 | , IntLit <$> P.try parseInt 174 | , NilLit <$ P.string "nil" 175 | ] 176 | where 177 | parseStringLit :: Parser NLiteral 178 | parseStringLit = 179 | StringLit 180 | <$> ( (P.char '"' <|> P.char '\'') 181 | *> P.manyTill L.charLiteral (P.char '"' <|> P.char '\'') 182 | ) 183 | 184 | parseBoolLit :: Parser NLiteral 185 | parseBoolLit = P.choice 186 | [BoolLit True <$ P.string "true", BoolLit False <$ P.string "false"] 187 | 188 | parseInt :: Parser Int 189 | parseInt = L.signed (pure ()) L.decimal 190 | 191 | 192 | hiddenSpaceChar :: Parser Char 193 | hiddenSpaceChar = P.hidden (P.spaceChar <|> parseComment) 194 | where 195 | parseComment = 196 | ' ' <$ (P.string "/*" *> P.manyTill L.charLiteral (P.string "*/")) 197 | 198 | 199 | parens = betweenChars '(' ')' . surroundedByMany hiddenSpaceChar 200 | brackets = betweenChars '[' ']' . surroundedByMany hiddenSpaceChar 201 | curlies = betweenChars '{' '}' . surroundedByMany hiddenSpaceChar 202 | 203 | surroundedBy :: Parser s -> Parser a -> Parser a 204 | surroundedBy surround p = surround *> p <* surround 205 | 206 | surroundedByMany :: Parser s -> Parser a -> Parser a 207 | surroundedByMany surround p = P.skipMany surround *> p <* P.skipMany surround 208 | 209 | surroundedBySome :: Parser s -> Parser a -> Parser a 210 | surroundedBySome surround p = P.skipSome surround *> p <* P.skipSome surround 211 | 212 | betweenChars :: Char -> Char -> Parser a -> Parser a 213 | betweenChars l r = P.between (P.char l) (P.char r) 214 | -------------------------------------------------------------------------------- /src/Types.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wall -fno-warn-unused-imports -fno-warn-missing-signatures -fno-warn-missing-pattern-synonym-signatures #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving, PatternSynonyms #-} 3 | module Types where 4 | 5 | import Parse 6 | import qualified Data.Map.Strict as M 7 | 8 | 9 | data Value = VNum !Int 10 | | VStr !String 11 | | VBool !Bool 12 | | VNil 13 | | VFunction !Function 14 | | VTuple !Value !Value 15 | deriving (Eq) 16 | data Function = Builtin (Value -> Value -> IO Value) | Lambda Env !NIdent !NExp !NIdent 17 | 18 | instance Eq Function where 19 | (==) (Lambda _ a1 exp1 b1) (Lambda _ a2 exp2 b2) = (a1, exp1, b1) == (a2, exp2, b2) 20 | (==) _ _ = False 21 | 22 | pattern VLambda a b c d = VFunction (Lambda a b c d) 23 | pattern VBuiltin f = VFunction (Builtin f) 24 | 25 | instance Show Value where 26 | show (VFunction f) = show f 27 | show (VTuple a b) = show (a, b) 28 | show (VNum n) = show n 29 | show (VStr s) = s 30 | show (VBool b) = show b 31 | show VNil = "nil" 32 | 33 | 34 | instance Show Function where 35 | show (Builtin _ ) = "builtin" 36 | show (Lambda _ a1 body a2) = unwords ["[", show a1, show body, show a2, "]"] 37 | 38 | newtype Env = Env (M.Map String Value) deriving (Show, Semigroup) 39 | 40 | instance Monoid Env where 41 | mappend (Env a) (Env b) = Env $ M.union a b 42 | mempty = Env mempty 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elkowar/infix-lisp/6acac9b3a3d4d1eb2e9a3a08aedfdf8d0e567c12/src/lib.rs -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-15.0 21 | 22 | # User packages to be built. 23 | # Various formats can be used as shown in the example below. 24 | # 25 | # packages: 26 | # - some-directory 27 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 28 | # subdirs: 29 | # - auto-update 30 | # - wai 31 | packages: 32 | - . 33 | # Dependency packages to be pulled from upstream that are not in the resolver. 34 | # These entries can reference officially published versions as well as 35 | # forks / in-progress versions pinned to a git hash. For example: 36 | # 37 | # extra-deps: 38 | # - acme-missiles-0.3 39 | # - git: https://github.com/commercialhaskell/stack.git 40 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 41 | # 42 | extra-deps: 43 | - repline-0.4.0.0@sha256:3324479e497d27c40c3d4762bffc52058f9921621d20d2947dcf9a554b94cd0d,2253 44 | - haskeline-0.8.0.0@sha256:0630452b759a5b40e0e3e7ca2ef4b0d55d5161d95e8d7b0dd3cf55888971437f,5440 45 | 46 | # Override default flag values for local packages and extra-deps 47 | # flags: {} 48 | 49 | # Extra package databases containing global packages 50 | # extra-package-dbs: [] 51 | 52 | # Control whether we use the GHC we find on the path 53 | # system-ghc: true 54 | # 55 | # Require a specific version of stack, using version ranges 56 | # require-stack-version: -any # Default 57 | # require-stack-version: ">=2.3" 58 | # 59 | # Override the architecture used by stack, especially useful on Windows 60 | # arch: i386 61 | # arch: x86_64 62 | # 63 | # Extra directories used by stack for building 64 | # extra-include-dirs: [/path/to/dir] 65 | # extra-lib-dirs: [/path/to/dir] 66 | # 67 | # Allow a newer minor version of GHC than the snapshot specifies 68 | # compiler-check: newer-minor 69 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: repline-0.4.0.0@sha256:3324479e497d27c40c3d4762bffc52058f9921621d20d2947dcf9a554b94cd0d,2253 9 | pantry-tree: 10 | size: 564 11 | sha256: bff43f9a5b0a212c29c12e2e22b528d9071b2a7d13489ce5bdc54ae9eac853cf 12 | original: 13 | hackage: repline-0.4.0.0@sha256:3324479e497d27c40c3d4762bffc52058f9921621d20d2947dcf9a554b94cd0d,2253 14 | - completed: 15 | hackage: haskeline-0.8.0.0@sha256:0630452b759a5b40e0e3e7ca2ef4b0d55d5161d95e8d7b0dd3cf55888971437f,5440 16 | pantry-tree: 17 | size: 3013 18 | sha256: 235d9b78cb87e21651fd36421c30ff4e0638e463fac7ec8b70887db06c3ff88f 19 | original: 20 | hackage: haskeline-0.8.0.0@sha256:0630452b759a5b40e0e3e7ca2ef4b0d55d5161d95e8d7b0dd3cf55888971437f,5440 21 | snapshots: 22 | - completed: 23 | size: 488576 24 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/0.yaml 25 | sha256: e4b6a87b47ec1cf63a7f1a0884a3b276fce2b0d174a10e8753c4f618e7983568 26 | original: lts-15.0 27 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | 2 | import Parse 3 | import Eval 4 | import Types 5 | import qualified Data.Text as T 6 | 7 | 8 | 9 | main :: IO () 10 | main = do 11 | testParser 12 | _ <- testEval 13 | pure () 14 | 15 | 16 | testEval = do 17 | testEvalExp "\"a\"" 18 | testEvalExp "(1 + 5)" 19 | testEvalExp "((1 + 2) + 5)" 20 | testEvalExp "({ x = 1, } in (x + 2))" 21 | testEvalExp "({ add = [a (a + b) b], } in (2 add 5))" 22 | testEvalExp "(\"yes\" <(1 == 1)> \"no\")" 23 | testEvalExp "({not = [_ (false true) value], } in (nil not true))" 24 | testEvalExp "({not = [_ (false true) value], } in (nil not false))" 25 | testEvalExp "(1 [a (a + b) b] 1)" 26 | testEvalExp "({add4 = [a [b ((a + b) + (c + d)) c] d], } in (1 (1 add4 1) 1))" 27 | 28 | 29 | 30 | 31 | 32 | trustMe :: Either a b -> b 33 | trustMe (Right x) = x 34 | trustMe (Left _) = error "YOU LIED TO ME" 35 | 36 | testEvalExp :: String -> IO () 37 | testEvalExp s = do 38 | result <- evalExp mempty (trustMe $ parse $ T.pack s) 39 | putStrLn $ s ++ " ===> " ++ show result 40 | 41 | 42 | testParser = do 43 | parseTest "({ a = 12, b = 13, c = 14 } in a)" 44 | parseTest "[a a b]" 45 | parseTest "(1 [ a ( a + b ) b ] 2)" 46 | parseTest "12" 47 | parseTest "(12 identifier 12)" 48 | parseTest "((12 ident 15) otherIdent 0)" 49 | parseTest "((\"Muhh string\" ident 15) otherIdent 0)" 50 | parseTest "[arg1 (arg1 + arg2) arg2]" 51 | parseTest "( nil print [ x ( x + 1 ) y ] )" 52 | parseTest "( nil [ _ ( x + 1 ) x ] 5 )" 53 | parseTest "( { x = 1, } in x)" 54 | parseTest "(1 <(true && false)> 2)" 55 | parseTest "({ add4 = [a [b ((a + b) + (c + d)) c] d] } in (1 (1 add4 1) 1))" 56 | --------------------------------------------------------------------------------