diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml new file mode 100644 index 0000000..006f04e --- /dev/null +++ b/.github/workflows/haskell-ci.yml @@ -0,0 +1,168 @@ +# This GitHub workflow config has been generated by a script via +# +# haskell-ci 'github' 'mcc.cabal' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# version: 0.13.20210525 +# +# REGENDATA ("0.13.20210525",["github","mcc.cabal"]) +# +name: Haskell-CI +on: + - push + - pull_request +jobs: + linux: + name: Haskell-CI - Linux - ${{ matrix.compiler }} + runs-on: ubuntu-18.04 + container: + image: buildpack-deps:bionic + continue-on-error: ${{ matrix.allow-failure }} + strategy: + matrix: + include: + - compiler: ghc-8.10.4 + allow-failure: false + fail-fast: false + steps: + - name: apt + run: | + apt-get update + apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common + apt-add-repository -y 'ppa:hvr/ghc' + apt-get update + apt-get install -y $CC cabal-install-3.4 + env: + CC: ${{ matrix.compiler }} + - name: Set PATH and environment variables + run: | + echo "$HOME/.cabal/bin" >> $GITHUB_PATH + echo "LANG=C.UTF-8" >> $GITHUB_ENV + echo "CABAL_DIR=$HOME/.cabal" >> $GITHUB_ENV + echo "CABAL_CONFIG=$HOME/.cabal/config" >> $GITHUB_ENV + HCDIR=$(echo "/opt/$CC" | sed 's/-/\//') + HCNAME=ghc + HC=$HCDIR/bin/$HCNAME + echo "HC=$HC" >> $GITHUB_ENV + echo "HCPKG=$HCDIR/bin/$HCNAME-pkg" >> $GITHUB_ENV + echo "HADDOCK=$HCDIR/bin/haddock" >> $GITHUB_ENV + echo "CABAL=/opt/cabal/3.4/bin/cabal -vnormal+nowrap" >> $GITHUB_ENV + HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') + echo "HCNUMVER=$HCNUMVER" >> $GITHUB_ENV + echo "ARG_TESTS=--enable-tests" >> $GITHUB_ENV + echo "ARG_BENCH=--enable-benchmarks" >> $GITHUB_ENV + echo "HEADHACKAGE=false" >> $GITHUB_ENV + echo "ARG_COMPILER=--$HCNAME --with-compiler=$HC" >> $GITHUB_ENV + echo "GHCJSARITH=0" >> $GITHUB_ENV + env: + CC: ${{ matrix.compiler }} + - name: env + run: | + env + - name: write cabal config + run: | + mkdir -p $CABAL_DIR + cat >> $CABAL_CONFIG < cabal-plan.xz + echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c - + xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan + rm -f cabal-plan.xz + chmod a+x $HOME/.cabal/bin/cabal-plan + cabal-plan --version + - name: checkout + uses: actions/checkout@v2 + with: + path: source + - name: initial cabal.project for sdist + run: | + touch cabal.project + echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project + cat cabal.project + - name: sdist + run: | + mkdir -p sdist + $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist + - name: unpack + run: | + mkdir -p unpacked + find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; + - name: generate cabal.project + run: | + PKGDIR_mcc="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/mcc-[0-9.]*')" + echo "PKGDIR_mcc=${PKGDIR_mcc}" >> $GITHUB_ENV + touch cabal.project + touch cabal.project.local + echo "packages: ${PKGDIR_mcc}" >> cabal.project + echo "package mcc" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project + cat >> cabal.project <> cabal.project.local + cat cabal.project + cat cabal.project.local + - name: dump install plan + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all + cabal-plan + - name: cache + uses: actions/cache@v2 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store + restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- + - name: install dependencies + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all + - name: build w/o tests + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: build + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always + - name: tests + run: | + $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + - name: cabal check + run: | + cd ${PKGDIR_mcc} || false + ${CABAL} -vnormal check + - name: haddock + run: | + $CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + - name: unconstrained build + run: | + rm -f cabal.project.local + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml new file mode 100644 index 0000000..12cf4c6 --- /dev/null +++ b/.github/workflows/haskell.yml @@ -0,0 +1,39 @@ +name: Haskell CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-haskell@v1 + with: + ghc-version: latest + cabal-version: latest + + - name: Cache + uses: actions/cache@v1 + env: + cache-name: cache-cabal + with: + path: ~/.cabal + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install dependencies + run: | + cabal update + cabal build --only-dependencies --enable-tests --enable-benchmarks + - name: Build + run: cabal build --enable-tests --enable-benchmarks all + - name: Run tests + run: cabal test all diff --git a/.gitignore b/.gitignore index 49cb2f2..4d5b3fb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ cabal-dev *.chs.h *.dyn_o *.dyn_hi +.hie .hpc .hsenv .cabal-sandbox/ diff --git a/.travis.yml b/.travis.yml index 409541e..47c5a5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,137 @@ -language: nix -nix: 2.3.2 -script: nix-shell --pure --run 'cabal new-test' +# This Travis job script has been generated by a script via +# +# haskell-ci 'travis' 'mcc.cabal' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# version: 0.13.20210525 +# +version: ~> 1.0 +language: c +os: linux +dist: bionic +git: + # whether to recursively clone submodules + submodules: false +cache: + directories: + - $HOME/.cabal/packages + - $HOME/.cabal/store + - $HOME/.hlint +before_cache: + - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log + # remove files that are regenerated by 'cabal update' + - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.* + - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json + - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache + - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar + - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx + - rm -rfv $CABALHOME/packages/head.hackage +jobs: + include: + - compiler: ghc-8.10.4 + addons: {"apt":{"packages":["ghc-8.10.4","cabal-install-3.4"],"sources":[{"key_url":"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x063dab2bdc0b3f9fcebc378bff3aeacef6f88286","sourceline":"deb http://ppa.launchpad.net/hvr/ghc/ubuntu bionic main"}]}} + os: linux +before_install: + - HC=$(echo "/opt/$CC/bin/ghc" | sed 's/-/\//') + - WITHCOMPILER="-w $HC" + - HADDOCK=$(echo "/opt/$CC/bin/haddock" | sed 's/-/\//') + - HCPKG="$HC-pkg" + - unset CC + - CABAL=/opt/ghc/bin/cabal + - CABALHOME=$HOME/.cabal + - export PATH="$CABALHOME/bin:$PATH" + - TOP=$(pwd) + - "HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')" + - echo $HCNUMVER + - CABAL="$CABAL -vnormal+nowrap" + - set -o pipefail + - TEST=--enable-tests + - BENCH=--enable-benchmarks + - HEADHACKAGE=false + - rm -f $CABALHOME/config + - | + echo "verbose: normal +nowrap +markoutput" >> $CABALHOME/config + echo "remote-build-reporting: anonymous" >> $CABALHOME/config + echo "write-ghc-environment-files: never" >> $CABALHOME/config + echo "remote-repo-cache: $CABALHOME/packages" >> $CABALHOME/config + echo "logs-dir: $CABALHOME/logs" >> $CABALHOME/config + echo "world-file: $CABALHOME/world" >> $CABALHOME/config + echo "extra-prog-path: $CABALHOME/bin" >> $CABALHOME/config + echo "symlink-bindir: $CABALHOME/bin" >> $CABALHOME/config + echo "installdir: $CABALHOME/bin" >> $CABALHOME/config + echo "build-summary: $CABALHOME/logs/build.log" >> $CABALHOME/config + echo "store-dir: $CABALHOME/store" >> $CABALHOME/config + echo "install-dirs user" >> $CABALHOME/config + echo " prefix: $CABALHOME" >> $CABALHOME/config + echo "repository hackage.haskell.org" >> $CABALHOME/config + echo " url: http://hackage.haskell.org/" >> $CABALHOME/config +install: + - ${CABAL} --version + - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]" + - | + echo "program-default-options" >> $CABALHOME/config + echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config + - cat $CABALHOME/config + - rm -fv cabal.project cabal.project.local cabal.project.freeze + - travis_retry ${CABAL} v2-update -v + # Generate cabal.project + - rm -rf cabal.project cabal.project.local cabal.project.freeze + - touch cabal.project + - | + echo "packages: ." >> cabal.project + - echo 'package mcc' >> cabal.project + - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project" + - | + - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(mcc)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" + - cat cabal.project || true + - cat cabal.project.local || true + - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi + - ${CABAL} v2-freeze $WITHCOMPILER ${TEST} ${BENCH} + - "cat cabal.project.freeze | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'" + - rm cabal.project.freeze + - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} --dep -j2 all + - travis_wait 40 ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks --dep -j2 all +script: + - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX) + # Packaging... + - ${CABAL} v2-sdist all + # Unpacking... + - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/ + - cd ${DISTDIR} || false + - find . -maxdepth 1 -type f -name '*.tar.gz' -exec tar -xvf '{}' \; + - find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \; + - PKGDIR_mcc="$(find . -maxdepth 1 -type d -regex '.*/mcc-[0-9.]*')" + # Generate cabal.project + - rm -rf cabal.project cabal.project.local cabal.project.freeze + - touch cabal.project + - | + echo "packages: ${PKGDIR_mcc}" >> cabal.project + - echo 'package mcc' >> cabal.project + - "echo ' ghc-options: -Werror=missing-methods' >> cabal.project" + - | + - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(mcc)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done" + - cat cabal.project || true + - cat cabal.project.local || true + # Building... + # this builds all libraries and executables (without tests/benchmarks) + - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all + # Building with tests and benchmarks... + # build & run tests, build benchmarks + - ${CABAL} v2-build $WITHCOMPILER ${TEST} ${BENCH} all --write-ghc-environment-files=always + # Testing... + - ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all --test-show-details=direct + # cabal check... + - (cd ${PKGDIR_mcc} && ${CABAL} -vnormal check) + # haddock... + - ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all + # Building without installed constraints for packages in global-db... + - rm -f cabal.project.local + - ${CABAL} v2-build $WITHCOMPILER --disable-tests --disable-benchmarks all + +# REGENDATA ("0.13.20210525",["travis","mcc.cabal"]) +# EOF diff --git a/README.md b/README.md index 4456628..31c1cca 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,120 @@ # mcc + Another MicroC compiler. This used to be a fork of the reference compiler for Columbia's _Programming Languages and Translators_ course, but has since grown to include very important C features such as pointers and structs. The original can be found at https://github.com/cwabbott0/microc-llvm. Students in the class are allowed to write the compiler for their own final semester project in OCaml or Haskell, but since the provided reference compiler is in OCaml, almost no one chooses Haskell. This project is an attempt to remedy that situation and open the possibility of using Haskell to more students. The `compat` branch of the repo has only features that are present in the original repo, whereas master has some additions, such as pointers, structs, bitwise operators and an exponentiation operator that demonstrates how to link llvm intrinsic functions. For comparison, two separate parsers are provided, one recursive descent parser implemented with [Megaparsec](https://hackage.haskell.org/package/megaparsec) and one more classical LR-style parser implemented with [Alex](https://hackage.haskell.org/package/alex) and [Happy](https://hackage.haskell.org/package/happy), Haskell's analogues to the venerable lex and yacc. The test suite checks that they return identical parse trees for identical valid source files, although for files which fail to parse correctly, they do not emit the same error messages. ## Setup -*Note*: As of March 2020, the recommended setup for hacking on `mcc` is to just use [nix](https://nixos.org/nix/download.html). Running `nix-shell --pure` at the root of the project _should_ drop you into a shell that has the right versions of ghc, llvm, clang, and all the libraries that `mcc` depends on. The `default.nix` file included is pinned to a particular nixpkgs commit, so hopefully this will continue to be true in perpetuity. To run the test suite, `nix-shell --pure --run 'cabal new-test'` should do. Using stack to build `mcc` is no longer supported, as I don't use it anymore. The legacy instructions below are included for posterity. + +_Note_: As of March 2020, the recommended setup for hacking on `mcc` is to just use [nix](https://nixos.org/nix/download.html). Running `nix-shell --pure` at the root of the project _should_ drop you into a shell that has the right versions of ghc, llvm, clang, and all the libraries that `mcc` depends on. The `default.nix` file included is pinned to a particular nixpkgs commit, so hopefully this will continue to be true in perpetuity. To run the test suite, `nix-shell --pure --run 'cabal new-test'` should do. Using stack to build `mcc` is no longer supported, as I don't use it anymore. The legacy instructions below are included for posterity. + +### Manual installation + +As an alternative to using `nix`, you can use the following recipe: + +1. Assuming that you have `ghc` and `cabal` installed (at least versions 8.8 and 3.2, recommended), install `alex` and `happy`: + +```sh +> cabal update +> cabal install alex happy +``` + +and update `~/.cabal/config` to refer to them. For example: + +``` + alex-location: /Users/xxx/.cabal/bin/alex + happy-location: /Users/xxx/.cabal/bin/happy +``` + +2. Install `llvm`, which includes `llc`, `clang`, and a host of other compiler tools. + Go to http://releases.llvm.org/download.html for further information. There are + also some more [detailed instructions](https://github.com/llvm-hs/llvm-hs#installing-llvm). + +For macOS, for example, use `brew install llvm` and add `/usr/local/opt/llvm/bin` to your `$PATH`. + +3. Check that + +```sh +> llvm-config --libs +-lLLVM-12 +``` + +works. You may need to create the symlinks described at step 5 in the +more [detailed instructions](https://github.com/llvm-hs/llvm-hs#installing-llvm) on how +to install llvm. + +Note that unlike the OCaml version of MicroC, this project requires a more cecent version of LLVM. +(These instructions use LLVM 12.) + +4. Pull down the necessary sources and make sure that you have the branches + that correspond to the version of `llvm` that you are using checked out: + +```sh +> mkdir myproject +> cd myproject +> git clone https://github.com/llvm-hs/llvm-hs.git +> git checkout llvm-12 +> git clone https://github.com/llvm-hs/llvm-hs-pretty.git +> git checkout llvm-12 +> git clone https://github.com/jmorag/mcc.git +> git checkout llvm-12 +``` + +5. Create a `cabal.project` file in `myproject` (to ensure that you are using the right version + of `llvm-hs`. (Otherwise you risk using the hackage version, which may be + problematic) + +``` +packages: ./llvm-hs-pretty + ./llvm-hs/llvm-hs-pure + ./llvm-hs/llvm-hs + ./mcc +``` + +6. Build and test `mcc`: + +```sh +> cd mcc +> cabal build +> cabal test +``` + +7. You can either install `mcc` to your `~/.cabal/bin` by running `cabal install exe:mcc` or you can `cabal run mcc` from the `mcc` folder: + +```sh +❯ cabal run mcc -- --help +Up to date +Usage: mcc [(-a|--ast) | (-s|--sast) | (-l|--llvm) | (-c|--compile) [-o FILE]] + FILE [--combinator | (-g|--generator)] + Run the mcc compiler on the given file. Passing no flags will compile the + file, execute it, and print the output. + +Available options: + -h,--help Show this help text + -a,--ast Pretty print the ast + -s,--sast Pretty print the sast + -l,--llvm Pretty print the generated llvm + -c,--compile Compile to an executable + FILE Source file + --combinator Use the megaparsec parser implementation (default). + -g,--generator Use alex and happy to parse. + +> cabal run mcc -- tests/pass/test-add1.mc +Up to date +42 +``` + ### Legacy Setup + `mcc` requires the `stack` package manager for Haskell. To install `stack`, see https://docs.haskellstack.org/en/stable/README/. It also requires that `clang` and `llc` binaries be available in your PATH. There are two options to ensure that these will be available: + 1. **Use Nix**: -Using `stack`'s `nix` integration will set up a local environment with the correct versions of `clang` and `llc` installed and added to the PATH. To acquire nix, follow the installation [instructions](https://nixos.org/nix/download.html). Nix is enabled by default, so simply running `stack test` should work if everything is nix and stack are installed correctly. + Using `stack`'s `nix` integration will set up a local environment with the correct versions of `clang` and `llc` installed and added to the PATH. To acquire nix, follow the installation [instructions](https://nixos.org/nix/download.html). Nix is enabled by default, so simply running `stack test` should work if everything is nix and stack are installed correctly. 2. **Manual Installation**: -To install the `llvm` toolchain, which includes `llc`, `clang`, and a host of other compiler tools, go to http://releases.llvm.org/download.html#6.0.1. For macOS, the maintainers of the Haskell-LLVM bindings provide a brew tap, so running `brew install llvm-hs/llvm/llvm-6.0` and adding `/usr/local/opt/llvm/bin` to your `$PATH` should be sufficient. They also provide more [detailed instructions](https://github.com/llvm-hs/llvm-hs#installing-llvm) on how to install llvm on other platforms. Note that unlike the OCaml version of MicroC, this project requires LLVM 6, not LLVM 3.4. To use stack commands without nix integration, pass the `--no-nix` flag to them. + To install the `llvm` toolchain, which includes `llc`, `clang`, and a host of other compiler tools, go to http://releases.llvm.org/download.html#6.0.1. For macOS, the maintainers of the Haskell-LLVM bindings provide a brew tap, so running `brew install llvm-hs/llvm/llvm-6.0` and adding `/usr/local/opt/llvm/bin` to your `$PATH` should be sufficient. They also provide more [detailed instructions](https://github.com/llvm-hs/llvm-hs#installing-llvm) on how to install llvm on other platforms. Note that unlike the OCaml version of MicroC, this project requires LLVM 6, not LLVM 3.4. To use stack commands without nix integration, pass the `--no-nix` flag to them. ### Running and testing the compiler + Once everything is installed, to run the compiler's test suite, navigate to the project home directory and run `stack test`. To build compiler executable, run `stack build mcc`. Remember to add `--no-nix` to these commands if you would like to disable `nix` integration. ## Questions, contributions + Please don't hesitate to open an issue or pull request if you don't understand something in here or you see something that could be improved! Once most of the functionality is complete, I'm planning on releasing a detailed walkthrough on the whole compilation process in a series of posts, but until then, documentation is very sparse. diff --git a/app/Main.hs b/app/Main.hs index a1a97aa..980b7cc 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,4 +1,5 @@ -module Main where + {-# LANGUAGE StrictData #-} +module Main(main) where import Microc hiding ( Parser ) @@ -14,7 +15,7 @@ import Data.Text.Prettyprint.Doc.Render.Text data Action = Ast | Sast | LLVM | Compile FilePath | Run data ParserType = Combinator | Generator -data Options = Options { action :: Action, infile :: FilePath, parser :: ParserType } +data Options = Options { _action :: Action, _infile :: FilePath, _parser :: ParserType } actionP :: Parser Action actionP = flag' Ast (long "ast" <> short 'a' <> help "Pretty print the ast") diff --git a/bench/Bench.hs b/bench/Bench.hs index fd13c3d..8ca9e21 100644 --- a/bench/Bench.hs +++ b/bench/Bench.hs @@ -1,24 +1,30 @@ -module Main where +module Main ( main) where + +-- TODO:: incomplete + import Microc -import Data.String.Conversions -import qualified Data.Text.IO as T +-- import Data.String.Conversions +-- import qualified Data.Text.IO as T import Data.Text ( Text - , unpack - ) -import System.FilePath ( takeBaseName - , replaceExtension +-- , unpack ) +-- import System.FilePath ( takeBaseName +-- , replaceExtension +-- ) import Criterion.Main import Test.Tasty.Golden -import Control.Monad +-- import Control.Monad +megaparse :: Text -> Program megaparse input = case runParser programP "" input of Left _ -> error "We only like success" Right p -> p -happyparse = alexScanTokens . parse +happyparse :: String -> Program +happyparse = parse . alexScanTokens +main :: IO() main = do mcFiles <- findByExtension [".mc"] "tests/pass" defaultMain [] diff --git a/mcc.cabal b/mcc.cabal index d92b875..89a1bf8 100644 --- a/mcc.cabal +++ b/mcc.cabal @@ -1,4 +1,4 @@ -cabal-version: 1.12 +cabal-version: 2.4 -- This file has been generated from package.yaml by hpack version 0.33.0. -- @@ -6,157 +6,130 @@ cabal-version: 1.12 -- -- hash: a168355244c92a5a39b4a5a9f6accd48993a937062b5c7daca3f02e5c8838a2c -name: mcc -version: 0.1.0.0 -synopsis: A microc compiler in Haskell -category: Compilers/Interpreters -homepage: https://github.com/jmorag/mcc#readme -bug-reports: https://github.com/jmorag/mcc/issues -author: Joseph Morag -maintainer: Joseph Morag -license: BSD3 -license-file: LICENSE -build-type: Simple -extra-source-files: - README.md +name: mcc +version: 0.1.0.0 +synopsis: A microc compiler in Haskell +description: A microc compiler in Haskell. +category: Compilers/Interpreters +homepage: https://github.com/jmorag/mcc#readme +bug-reports: https://github.com/jmorag/mcc/issues +author: Joseph Morag +maintainer: Joseph Morag +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: README.md + +tested-with: GHC ==8.10.4 source-repository head - type: git + type: git location: https://github.com/jmorag/mcc +common common-options + build-depends: + , base >=4.7 && <5 + , megaparsec + , string-conversions + , text + + ghc-options: + -Wall -Wextra -Wcompat -Widentities -Wincomplete-uni-patterns + -Wincomplete-record-updates -Wredundant-constraints + -Wnoncanonical-monad-instances -fno-warn-name-shadowing + + default-extensions: + LambdaCase + OverloadedStrings + + if impl(ghc >=8.2) + ghc-options: -fhide-source-paths + default-extensions: + DerivingStrategies + + if impl(ghc >=8.4) + ghc-options: -Wmissing-export-lists -Wpartial-fields + + if impl(ghc >=8.8) + ghc-options: -Wmissing-deriving-strategies -fwrite-ide-info -hiedir=.hie + + if impl(ghc >=8.10) + ghc-options: -Wunused-packages + + other-modules: Paths_mcc + autogen-modules: Paths_mcc + default-language: Haskell2010 + library + import: common-options exposed-modules: - Microc - Microc.Ast - Microc.Codegen - Microc.Parser.Combinator - Microc.Parser.Generator - Microc.Sast - Microc.Scanner.Combinator - Microc.Scanner.Generator - Microc.Semant - Microc.Semant.Analysis - Microc.Semant.Error - Microc.Toplevel - Microc.Utils - other-modules: - Paths_mcc - hs-source-dirs: - src - default-extensions: OverloadedStrings, LambdaCase - ghc-options: -Wall -fno-warn-name-shadowing -Wcompat -Wincomplete-uni-patterns + Microc + Microc.Ast + Microc.Codegen + Microc.Parser.Combinator + Microc.Parser.Generator + Microc.Sast + Microc.Scanner.Combinator + Microc.Scanner.Generator + Microc.Semant + Microc.Semant.Analysis + Microc.Semant.Error + Microc.Toplevel + Microc.Utils + + hs-source-dirs: src build-depends: - array - , base >=4.7 && <5 + , array , bytestring , containers , directory - , filepath - , llvm-hs >=9 && <10 - , llvm-hs-pretty >=0.9 && <1 - , llvm-hs-pure >=9 && <10 - , megaparsec + , llvm-hs >=9 && <13 + , llvm-hs-pure >=9 && <13 , mtl , parser-combinators , prettyprinter , process - , string-conversions - , text , unix - default-language: Haskell2010 executable mcc - main-is: Main.hs - other-modules: - Paths_mcc - hs-source-dirs: - app - default-extensions: OverloadedStrings, LambdaCase - ghc-options: -Wall -fno-warn-name-shadowing -Wcompat -Wincomplete-uni-patterns + import: common-options + main-is: Main.hs + other-modules: Paths_mcc + hs-source-dirs: app build-depends: - array - , base >=4.7 && <5 - , bytestring - , containers - , directory - , filepath - , llvm-hs >=9 && <10 - , llvm-hs-pretty >=0.9 && <1 - , llvm-hs-pure >=9 && <10 + , llvm-hs-pretty >=0.9 && <13 , mcc - , megaparsec - , mtl , optparse-applicative - , parser-combinators , pretty-simple , prettyprinter - , process - , string-conversions - , text - , unix - default-language: Haskell2010 +-- ghc-options: -threaded -rtsopts -with-rtsopts=-N test-suite testall - type: exitcode-stdio-1.0 - main-is: Testall.hs - other-modules: - Paths_mcc - hs-source-dirs: - tests - default-extensions: OverloadedStrings, LambdaCase - ghc-options: -Wall -fno-warn-name-shadowing -Wcompat -Wincomplete-uni-patterns + import: common-options + type: exitcode-stdio-1.0 + main-is: Testall.hs + other-modules: Paths_mcc + hs-source-dirs: tests build-depends: - array - , base >=4.7 && <5 - , bytestring - , containers - , directory , filepath - , llvm-hs >=9 && <10 - , llvm-hs-pretty >=0.9 && <1 - , llvm-hs-pure >=9 && <10 , mcc - , megaparsec - , mtl - , parser-combinators , prettyprinter - , process - , string-conversions , tasty , tasty-golden , tasty-hunit - , text - , unix - default-language: Haskell2010 +-- The following breaks the running of the test suite by changing the current +-- working directory +-- ghc-options: -threaded -rtsopts -with-rtsopts=-N benchmark bench - type: exitcode-stdio-1.0 - main-is: Bench.hs - other-modules: - Paths_mcc - hs-source-dirs: - bench - default-extensions: OverloadedStrings, LambdaCase - ghc-options: -Wall -fno-warn-name-shadowing -Wcompat -Wincomplete-uni-patterns + import: common-options + type: exitcode-stdio-1.0 + main-is: Bench.hs + other-modules: Paths_mcc + hs-source-dirs: bench build-depends: - array - , base >=4.7 && <5 - , bytestring - , containers , criterion - , directory , filepath - , llvm-hs >=9 && <10 - , llvm-hs-pretty >=0.9 && <1 - , llvm-hs-pure >=9 && <10 , mcc - , megaparsec - , mtl - , parser-combinators - , prettyprinter - , process - , string-conversions , tasty-golden - , text - , unix - default-language: Haskell2010 +-- ghc-options: -threaded -rtsopts -with-rtsopts=-N diff --git a/src/Microc/Ast.hs b/src/Microc/Ast.hs index 39b36a8..4bf808f 100644 --- a/src/Microc/Ast.hs +++ b/src/Microc/Ast.hs @@ -1,7 +1,19 @@ -module Microc.Ast where + {-# LANGUAGE StrictData #-} +module Microc.Ast + ( Op(..) + , Uop(..) + , Struct(..) + , Type(..) + , Bind(..) + , Expr(..) + , Statement(..) + , Function(..) + , Program(..) + ) where + +import Data.Char ( chr ) import Data.Text ( Text ) import Data.Text.Prettyprint.Doc -import Data.Char ( chr ) data Op = Add | Sub @@ -18,14 +30,17 @@ data Op = Add | Or | BitAnd | BitOr - deriving (Show, Eq) + deriving stock (Show, Eq) data Uop = Neg | Not - deriving (Show, Eq) + deriving stock (Show, Eq) -data Struct = Struct { structName :: Text, structFields :: [Bind] } - deriving (Show, Eq) +data Struct = Struct + { structName :: Text + , structFields :: [Bind] + } + deriving stock (Show, Eq) data Type = Pointer Type | TyInt @@ -34,8 +49,12 @@ data Type = Pointer Type | TyChar | TyVoid | TyStruct Text - deriving (Show, Eq) -data Bind = Bind { bindType :: Type, bindName :: Text } deriving (Show, Eq) + deriving stock (Show, Eq) +data Bind = Bind + { bindType :: Type + , bindName :: Text + } + deriving stock (Show, Eq) data Expr = Literal Int | StrLit Text @@ -54,7 +73,7 @@ data Expr = Literal Int | Assign Expr Expr | Sizeof Type | Noexpr - deriving (Show, Eq) + deriving stock (Show, Eq) data Statement = Expr Expr | Block [Statement] @@ -62,39 +81,40 @@ data Statement = Expr Expr | If Expr Statement Statement | For Expr Expr Expr Statement | While Expr Statement - deriving (Show, Eq) + deriving stock (Show, Eq) data Function = Function - { typ :: Type - , name :: Text + { typ :: Type + , name :: Text , formals :: [Bind] - , locals :: [Bind] - , body :: [Statement] + , locals :: [Bind] + , body :: [Statement] } - deriving (Show, Eq) + deriving stock (Show, Eq) -data Program = Program [Struct] [Bind] [Function] deriving (Eq, Show) +data Program = Program [Struct] [Bind] [Function] + deriving stock (Eq, Show) -------------------------------------------- -- Pretty instances -------------------------------------------- instance Pretty Op where pretty = \case - Add -> "+" - Sub -> "-" - Mult -> "*" - Div -> "/" - Power -> "**" - Equal -> "==" - Neq -> "!=" - Less -> "<" - Leq -> "<=" + Add -> "+" + Sub -> "-" + Mult -> "*" + Div -> "/" + Power -> "**" + Equal -> "==" + Neq -> "!=" + Less -> "<" + Leq -> "<=" Greater -> ">" - Geq -> ">=" - And -> "&&" - Or -> "||" - BitAnd -> "&" - BitOr -> "|" + Geq -> ">=" + And -> "&&" + Or -> "||" + BitAnd -> "&" + BitOr -> "|" instance Pretty Uop where pretty = \case @@ -102,18 +122,24 @@ instance Pretty Uop where Not -> "!" instance Pretty Struct where - pretty (Struct nm binds) = "struct" <+> - pretty nm <+> lbrace <> hardline <> indent 4 (vsep (map (\b -> pretty b <> ";") binds)) - <> hardline <> rbrace <> ";" + pretty (Struct nm binds) = + "struct" + <+> pretty nm + <+> lbrace + <> hardline + <> indent 4 (vsep (map (\b -> pretty b <> ";") binds)) + <> hardline + <> rbrace + <> ";" instance Pretty Type where pretty = \case - TyInt -> "int" - TyBool -> "bool" - TyChar -> "char" - TyFloat -> "float" - TyVoid -> "void" - Pointer t -> pretty t <+> "*" + TyInt -> "int" + TyBool -> "bool" + TyChar -> "char" + TyFloat -> "float" + TyVoid -> "void" + Pointer t -> pretty t <+> "*" TyStruct n -> "struct" <+> pretty n instance Pretty Bind where @@ -121,53 +147,63 @@ instance Pretty Bind where instance Pretty Expr where pretty = \case - Literal i -> pretty i - Fliteral f -> pretty f - CharLit c -> squotes $ pretty (chr c) - StrLit s -> dquotes $ pretty s - BoolLit b -> if b then "true" else "false" - Null -> "NULL" - Id t -> pretty t - Binop op lhs rhs -> hsep [pretty lhs, pretty op, pretty rhs] - Unop op e -> pretty op <> parens (pretty e) - Call f es -> pretty f <> tupled (map pretty es) - Cast t e -> parens (pretty t) <> parens (pretty e) + Literal i -> pretty i + Fliteral f -> pretty f + CharLit c -> squotes $ pretty (chr c) + StrLit s -> dquotes $ pretty s + BoolLit b -> if b then "true" else "false" + Null -> "NULL" + Id t -> pretty t + Binop op lhs rhs -> hsep [pretty lhs, pretty op, pretty rhs] + Unop op e -> pretty op <> parens (pretty e) + Call f es -> pretty f <> tupled (map pretty es) + Cast t e -> parens (pretty t) <> parens (pretty e) Access struct field -> pretty struct <> "." <> pretty field - Assign lhs rhs -> pretty lhs <+> "=" <+> pretty rhs - Deref e -> "*" <> parens (pretty e) - Addr e -> "&" <> parens (pretty e) - Sizeof t -> "sizeof" <> parens (pretty t) - Noexpr -> mempty + Assign lhs rhs -> pretty lhs <+> "=" <+> pretty rhs + Deref e -> "*" <> parens (pretty e) + Addr e -> "&" <> parens (pretty e) + Sizeof t -> "sizeof" <> parens (pretty t) + Noexpr -> mempty instance Pretty Statement where pretty = \case Expr e -> pretty e <> semi - Block ss -> lbrace <> hardline <> indent 4 (vsep (map pretty ss)) - <> hardline <> rbrace + Block ss -> + lbrace + <> hardline + <> indent 4 (vsep (map pretty ss)) + <> hardline + <> rbrace Return e -> "return" <+> pretty e <> semi If pred cons alt -> "if" <+> parens (pretty pred) <+> pretty cons <> prettyAlt - where - prettyAlt = - case alt of - Block [] -> mempty - _ -> hardline <> "else" <+> pretty alt - For init cond inc body -> "for" <+> - encloseSep lparen rparen semi [pretty init, pretty cond, pretty inc] - <+> pretty body + where + prettyAlt = case alt of + Block [] -> mempty + _ -> hardline <> "else" <+> pretty alt + For init cond inc body -> + "for" + <+> encloseSep lparen rparen semi [pretty init, pretty cond, pretty inc] + <+> pretty body While cond body -> "while" <+> parens (pretty cond) <+> pretty body instance Pretty Function where pretty (Function typ name formals locals body) = - pretty typ <+> pretty name <> tupled (map pretty formals) - <> hardline <> lbrace <> hardline <> - indent 4 (hardsep (map decl locals ++ map pretty body)) - <> hardline <> rbrace <> hardline + pretty typ + <+> pretty name + <> tupled (map pretty formals) + <> hardline + <> lbrace + <> hardline + <> indent 4 (hardsep (map decl locals ++ map pretty body)) + <> hardline + <> rbrace + <> hardline instance Pretty Program where - pretty (Program structs binds funcs) = hardsep - (map pretty structs ++ map decl binds ++ map pretty funcs) + pretty (Program structs binds funcs) = + hardsep (map pretty structs ++ map decl binds ++ map pretty funcs) decl :: Pretty a => a -> Doc ann decl bind = pretty bind <> semi diff --git a/src/Microc/Codegen.hs b/src/Microc/Codegen.hs index 2fbc5c4..b4f03a2 100644 --- a/src/Microc/Codegen.hs +++ b/src/Microc/Codegen.hs @@ -3,6 +3,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Microc.Codegen ( codegenProgram @@ -50,7 +51,7 @@ data Env = Env { operands :: M.Map Text Operand , structs :: [ Struct ] , strings :: M.Map Text Operand } - deriving (Eq, Show) + deriving stock (Eq, Show) -- LLVM and Codegen type synonyms allow us to emit module definitions and basic -- block instructions at the top level without being forced to pass explicit @@ -80,7 +81,7 @@ ltypeOfTyp = \case TyFloat -> pure AST.double TyBool -> pure AST.i1 -- (void *) is invalid LLVM - Pointer TyVoid -> pure $ charStar + Pointer TyVoid -> pure charStar -- special case to handle recursively defined structures -- TODO: add real cycle checking so that improperly defined -- recursive types case the compiler to hang forever diff --git a/src/Microc/Parser/Combinator.hs b/src/Microc/Parser/Combinator.hs index 95d1e90..d81fef6 100644 --- a/src/Microc/Parser/Combinator.hs +++ b/src/Microc/Parser/Combinator.hs @@ -23,7 +23,7 @@ opTable = , unary (Unop Not) "!" , unary Deref "*" , unary Addr "&" - , Prefix (try $ Cast <$> (parens typeP)) + , Prefix (try $ Cast <$> parens typeP) ] , [infixR Power "**"] , [infixL Mult "*", infixL Div "/"] diff --git a/src/Microc/Sast.hs b/src/Microc/Sast.hs index 22f45d0..4abc84c 100644 --- a/src/Microc/Sast.hs +++ b/src/Microc/Sast.hs @@ -1,4 +1,12 @@ -module Microc.Sast where + {-# LANGUAGE StrictData #-} +module Microc.Sast + ( SExpr + , SExpr'(..) + , LValue(..) + , SStatement(..) + , SFunction(..) + , SProgram + ) where import Microc.Ast import Data.Text ( Text ) @@ -20,13 +28,13 @@ data SExpr' = | SAddr LValue | SSizeof Type | SNoexpr - deriving (Show, Eq) + deriving stock (Show, Eq) -- | LValues are the class of assignable expressions that can appear -- on the Left side on the '=' operator and that can have their addresses -- taken. data LValue = SDeref SExpr | SAccess LValue Int | SId Text - deriving (Show, Eq) + deriving stock (Show, Eq) data SStatement = SExpr SExpr @@ -34,7 +42,7 @@ data SStatement = | SReturn SExpr | SIf SExpr SStatement SStatement | SDoWhile SExpr SStatement - deriving (Show, Eq) + deriving stock (Show, Eq) data SFunction = SFunction { styp :: Type @@ -43,6 +51,6 @@ data SFunction = SFunction , slocals :: [Bind] , sbody :: SStatement } - deriving (Show, Eq) + deriving stock (Show, Eq) type SProgram = ([Struct], [Bind], [SFunction]) diff --git a/src/Microc/Scanner/Combinator.hs b/src/Microc/Scanner/Combinator.hs index dc3ace6..31c19ea 100644 --- a/src/Microc/Scanner/Combinator.hs +++ b/src/Microc/Scanner/Combinator.hs @@ -1,14 +1,31 @@ -module Microc.Scanner.Combinator where +module Microc.Scanner.Combinator + ( Parser + , braces + , charlit + , comma + , float + , identifier + , int + , lexeme + , parens + , rword + , rws + , sc + , semi + , star + , strlit + , symbol + ) where -import Data.Void +import Control.Monad ( void ) import Data.Char +import Data.String.Conversions +import Data.Text ( Text ) +import qualified Data.Text as T +import Data.Void import Text.Megaparsec import Text.Megaparsec.Char import qualified Text.Megaparsec.Char.Lexer as L -import Data.Text ( Text ) -import qualified Data.Text as T -import Control.Monad ( void ) -import Data.String.Conversions type Parser = Parsec Void Text diff --git a/src/Microc/Scanner/Generator.x b/src/Microc/Scanner/Generator.x index 0012931..6f4d54e 100644 --- a/src/Microc/Scanner/Generator.x +++ b/src/Microc/Scanner/Generator.x @@ -1,5 +1,5 @@ { -module Microc.Scanner.Generator where +module Microc.Scanner.Generator ( alexScanTokens, Lexeme(..) ) where import Microc.Ast } diff --git a/src/Microc/Semant.hs b/src/Microc/Semant.hs index beee3d1..e74de0e 100644 --- a/src/Microc/Semant.hs +++ b/src/Microc/Semant.hs @@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} module Microc.Semant ( checkProgram @@ -62,7 +63,7 @@ builtIns = M.fromList $ map ] where toFunc (name, tys, retty) = - (name, Function retty name (map (flip Bind "x") tys) [] []) + (name, Function retty name (map (`Bind` "x") tys) [] []) checkExpr :: Expr -> Semant SExpr checkExpr expr = case expr of diff --git a/src/Microc/Semant/Analysis.hs b/src/Microc/Semant/Analysis.hs index 455681e..bb63ed1 100644 --- a/src/Microc/Semant/Analysis.hs +++ b/src/Microc/Semant/Analysis.hs @@ -1,4 +1,5 @@ -module Microc.Semant.Analysis where +{-# LANGUAGE StrictData #-} +module Microc.Semant.Analysis ( genCFG, validate ) where import Microc.Sast diff --git a/src/Microc/Semant/Error.hs b/src/Microc/Semant/Error.hs index dc6d095..d00472e 100644 --- a/src/Microc/Semant/Error.hs +++ b/src/Microc/Semant/Error.hs @@ -1,11 +1,19 @@ -module Microc.Semant.Error where +{-# LANGUAGE StrictData #-} +module Microc.Semant.Error + ( BindingKind (..), + BindingLoc (..), + SemantError (..), + SymbolKind (..), + VarKind (..), + ) +where import Microc.Ast import Data.Text ( Text ) import Data.Text.Prettyprint.Doc type Name = Text -data BindingLoc = F Function | S Struct | Toplevel deriving Show +data BindingLoc = F Function | S Struct | Toplevel deriving stock Show data SemantError = IllegalBinding Name BindingKind VarKind BindingLoc | UndefinedSymbol Name SymbolKind Expr @@ -18,12 +26,12 @@ data SemantError = | AssignmentError { lhs :: Expr, rhs :: Expr } | AccessError { struct :: Expr, field :: Expr } | DeadCode Statement -- ^ For statements in a block following a return - deriving (Show) + deriving stock (Show) -data BindingKind = Duplicate | Void deriving (Show) -data SymbolKind = Var | Func deriving (Show) +data BindingKind = Duplicate | Void deriving stock (Show) +data SymbolKind = Var | Func deriving stock (Show) -data VarKind = Global | Formal | Local | StructField deriving (Show, Eq, Ord) +data VarKind = Global | Formal | Local | StructField deriving stock (Show, Eq, Ord) instance Pretty VarKind where pretty = unsafeViaShow diff --git a/src/Microc/Toplevel.hs b/src/Microc/Toplevel.hs index 462581a..0483981 100644 --- a/src/Microc/Toplevel.hs +++ b/src/Microc/Toplevel.hs @@ -1,4 +1,4 @@ -module Microc.Toplevel where +module Microc.Toplevel ( compile, run ) where import LLVM.AST diff --git a/src/Microc/Utils.hs b/src/Microc/Utils.hs index 601dbf8..6eec46e 100644 --- a/src/Microc/Utils.hs +++ b/src/Microc/Utils.hs @@ -1,4 +1,4 @@ -module Microc.Utils where +module Microc.Utils ( locally ) where import Control.Monad.State @@ -7,4 +7,4 @@ locally computation = do oldState <- get result <- computation put oldState - return result \ No newline at end of file + return result diff --git a/tests/Testall.hs b/tests/Testall.hs index 5eed16d..81f3a7d 100644 --- a/tests/Testall.hs +++ b/tests/Testall.hs @@ -1,5 +1,5 @@ {-# LANGUAGE TypeApplications #-} -module Main where +module Main ( main) where import Test.Tasty ( defaultMain , TestTree @@ -48,7 +48,7 @@ parsing = do files <- concat <$> mapM (findByExtension [".mc"]) ["tests/pass", "tests/fail"] fmap (testGroup "parsing") $ forM files $ \file -> do input <- T.readFile file - combinator <- pure $ runParser programP file input + let combinator = runParser programP file input generator <- try @IOError . evaluate . parse . alexScanTokens $ cs input pure . testCase file $ case (combinator, generator) of (Right ast, Right ast') -> assertEqual file ast ast' diff --git a/tests/pass/test-linkedlist.mc b/tests/pass/test-linkedlist.mc index b1bbc16..1d4918b 100644 --- a/tests/pass/test-linkedlist.mc +++ b/tests/pass/test-linkedlist.mc @@ -33,6 +33,10 @@ int main() { int i; struct List *l; struct List *l2; + + l = NULL; + l2 = NULL; + for (i = 0; i < 20; i = i + 1) { l = cons(i, l); }