Skip to content

Commit

Permalink
adding workflows and README
Browse files Browse the repository at this point in the history
  • Loading branch information
nlsandler committed Jun 25, 2024
1 parent b714bac commit 813c4fd
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 0 deletions.
89 changes: 89 additions & 0 deletions .github/run_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import platform
import subprocess
import sys

# get/sanity check chapter number
cc: str = sys.argv[1]
chapter: int = int(sys.argv[2])
if chapter < 1 or chapter > 20:
exit("Bad chapter number")

# do we need rosetta?
prefix = ""
if platform.machine().lower() == "arm64":
prefix = "arch -x86_64 "

# define the stages we're going to test
stages: list[str] = ["lex", "parse", "tacky", "validate", "codegen", "run"]
if chapter == 1:
# tacky and validate stages not added yet
stages.remove("tacky")
stages.remove("validate")
elif chapter < 5:
# TACKY stage added, validate not
stages.remove("validate")
elif chapter > 18:
# don't test intermediate stages for chapters 19/20
stages = ["run"]

# info about extra credit features
ALL_EXTRA_CRED = {
"bitwise": 3,
"compound": 5,
"increment": 5,
"goto": 6,
"switch": 8,
"nan": 13,
"union": 18,
}


def get_idx(flag: str) -> tuple[int, str]:
# sort flags by chapter, use alphabetical order as tiebreaker
return (ALL_EXTRA_CRED[flag], flag)


all_implemented_extra_credit: tuple = tuple(
sorted([f for f in ALL_EXTRA_CRED if ALL_EXTRA_CRED[f] <= chapter], key=get_idx)
)


# find set of all extra-credit flag combinations we should test
extra_credit_combos: set[tuple] = {
# none (empty set)
(),
# all
all_implemented_extra_credit,
}

with open("./test_properties.json", encoding="utf-8") as f:
extra_cred_flags = json.load(f)["extra_credit_tests"]
for path, flags in extra_cred_flags.items():
# all keys in this dict start with chapter_N/
chapter_num = int(path.split("/")[0].removeprefix("chapter_"))
if chapter_num <= chapter:
extra_credit_combos.add(tuple(sorted(flags, key=get_idx)))


for flags in sorted(extra_credit_combos):
extra_cred_args: str = ""
if flags:
flag_str = " ".join("--" + f for f in flags)
test_compiler_arg = (
"--extra-credit" if flags == all_implemented_extra_credit else flag_str
)
extra_cred_args = f"{test_compiler_arg} -- {flag_str}"
print(f"Testing with extra credit features: {flag_str}")
for stage in stages:
print(f"Testing '{stage}' stage")
cmd = f"{prefix}./test_compiler {cc} --chapter {chapter} --stage {stage} {extra_cred_args}"
print(cmd)
subprocess.run(
cmd,
shell=True,
check=True,
)
97 changes: 97 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Build tagged versions of the reference implementation compiler
name: Build the compiler

on:
workflow_dispatch:
inputs:
chapter:
required: true
type: string
os:
required: true
default: ubuntu-latest

workflow_call:
inputs:
chapter:
required: true
type: number
os:
type: string
required: true
default: ubuntu-latest
branch:
type: string
required: false
default: main

env:
EXE_DIR: _build/default/bin/
EXE_PATH: _build/default/bin/main.exe

jobs:

build:
runs-on: ${{ inputs.os }}
env:
CHAPTER: ${{ inputs.chapter }}

steps:

# first check out at specified branch so we can find commit hash for this chapter
- uses: actions/checkout@v4
with:
repository: nlsandler/nqcc2
ref: ${{ inputs.branch }}
fetch-depth: 0 # need this to get commit history

# NOTE: for chapters with extra-credit features we have two commits:
# one for regular feature, one for extra credit. The '-n 1' options limits
# us to the first (i.e. later i.e. extra credit) commit hash
- name: Get commit hash
run: |
git log --grep "chapter $CHAPTER" -i --format='%H' -n 1
commit=$(git log --grep "chapter $CHAPTER" -i --format='%H' -n 1)
echo "commit=$commit" >> $GITHUB_ENV
- name: Check out NQCC at chapter ${{ inputs.chapter }}
uses: actions/checkout@v4
with:
repository: nlsandler/nqcc2
ref: ${{ env.commit }}

- name: Construct cache key
id: make-key
env:
runner_os: ${{ runner.os }}
run: |
commit=$(git rev-parse --short "$commit")
echo "cache-key=${runner_os}-${commit}-nqcc" >> $GITHUB_OUTPUT
- name: Cache build result
id: cache-nqcc
uses: actions/cache@v4
with:
path: ${{ env.EXE_PATH }}
key: ${{ steps.make-key.outputs.cache-key }}

# skip building if we get a cache hit
- name: Set up OCaml
if: steps.cache-nqcc.outputs.cache-hit != 'true'
uses: ocaml/setup-ocaml@v2
with:
ocaml-compiler: 4.14.x
# necessary to avoid random errors, see https://github.com/ocaml/setup-ocaml/issues/400
dune-cache: false

- name: Build it
if: steps.cache-nqcc.outputs.cache-hit != 'true'
run: |
opam install . --deps-only
opam exec -- dune build
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: nqcc-${{inputs.os}}-${{ inputs.chapter }}
path: ${{ env.EXE_PATH }}
59 changes: 59 additions & 0 deletions .github/workflows/build_and_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Build and test the compiler

on:
workflow_call:
inputs:
os:
required: true
type: string
chapter:
required: true
type: number
branch:
type: string
required: false
default: main

jobs:
build:
uses: ./.github/workflows/build.yaml
with:
chapter: ${{ inputs.chapter }}
os: ${{ inputs.os }}
branch: ${{ inputs.branch }}

test:
runs-on: ${{ inputs.os }}
needs: [build]
env:
CHAPTER: ${{ inputs.chapter }}
steps:

- name: Check out tests
uses: actions/checkout@v4
with:
repository: nlsandler/writing-a-c-compiler-tests
ref: complete-test-suite # TODO use main once this is merged

- name: Check out test runner script
uses: actions/checkout@v4
with:
repository: nlsandler/nqcc2
sparse-checkout: |
.github/run_tests.py
sparse-checkout-cone-mode: false
path: script

- name: Download the compiler
uses: actions/download-artifact@v4
with:
name: nqcc-${{inputs.os}}-${{ inputs.chapter }}
path: nqcc

# make NQCC executable
- run: chmod u+x nqcc/main.exe

# Invoke the run_tests.py script to test each intermediate stage
- name: Run the tests
run: ./script/.github/run_tests.py nqcc/main.exe "${CHAPTER}"

19 changes: 19 additions & 0 deletions .github/workflows/test_each_chapter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Build and test each chapter

on:
workflow_dispatch:

jobs:


build-and-test:
strategy:
fail-fast: true
matrix:
os: [macos-latest, ubuntu-latest]
chapter: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
uses: ./.github/workflows/build_and_test.yaml
with:
chapter: ${{ matrix.chapter }}
os: ${{ matrix.os }}
branch: ${{ github.ref_name }}
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# NQCC2, the Not-Quite-C Compiler

**CAUTION: FREQUENT FORCE PUSHES**

This is the reference implementation for the upcoming book [Writing a C Compiler](https://nostarch.com/writing-c-compiler), a hands-on guide to writing your own compiler for a big chunk of C.

Each commit corresponds to one chapter in the book, to illustrate what needs to change at each step. Because commits are structured this way, this repository sees frequent rebases and force pushes - fork with caution!

This implementation is still a work in progress - the functionality is complete, but needs more work on the readability front.

# Building the Compiler

This compiler is written in OCaml. Building it requires opam, the OCaml package manager (installation instructions [here](https://opam.ocaml.org/doc/Install.html)).

Then do:

```
git clone https://github.com/nlsandler/nqcc2.git
cd nqcc2
opam install . --deps-only
dune build
```

This puts the executable at `_build/default/bin/main.exe`.

# Usage Example

Assume we have this source file at ~/hello_world.c:
```c
int puts(char *c);

int main(void) {
puts("Hello, world!");
}
```
To compile and run it:
```
$ _build/default/bin/main.exe ~/hello_world.c
$ ~/hello_world
Hello, World!
```

0 comments on commit 813c4fd

Please sign in to comment.