Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/setup-build-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ inputs:
runs:
using: composite
steps:
# graphviz and jq are what the Makefile checks for before configuring, and
# the snapshot tests run the IR through both.
- name: Install packages
shell: bash
env:
Expand All @@ -30,7 +28,9 @@ runs:
DEBIAN_FRONTEND: noninteractive
run: |
set -euo pipefail
packages=(build-essential graphviz jq)
# jq is used by the release-asset helper below. Graphviz is not needed
# by the build or test suite.
packages=(build-essential jq)
if [ "$ARCHITECTURE" = arm ] && [ "$(uname -m)" = aarch64 ]; then
# An Arm64 host runs the Arm output natively, through the armhf
# loader and libc rather than an emulator.
Expand Down
36 changes: 2 additions & 34 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ concurrency:
cancel-in-progress: true

jobs:
# Bootstrap each target and run the test suite against the stage 0 and stage
# 2 compilers. The snapshot and sanitizer checks used to be steps of this
# job; they are jobs of their own below, so that they run beside it rather
# than after it.
# Bootstrap each target and run the consolidated behavioral suite against
# the stage 0 and stage 2 compilers.
host-x86:
name: ${{ matrix.architecture }}/${{ matrix.link_mode }} (${{ matrix.compiler }})
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -61,39 +59,9 @@ jobs:
name: logs-${{ matrix.compiler }}-${{ matrix.architecture }}-${{ matrix.link_mode }}
path: |
out/*.log
out/tests/*.log
if-no-files-found: ignore
retention-days: 7

# The IR a target emits does not depend on which host compiler built shecc,
# so this dimension of the matrix above buys nothing here.
snapshots:
name: IR snapshots ${{ matrix.architecture }}/${{ matrix.link_mode }}
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
# SNAPSHOT_ARCHS in the Makefile: x64 carries no reference IR yet.
architecture: [arm, riscv]
link_mode: [static, dynamic]
env:
ARCH: ${{ matrix.architecture }}
DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up the build environment
uses: ./.github/actions/setup-build-env
with:
architecture: ${{ matrix.architecture }}
link-mode: ${{ matrix.link_mode }}
github-token: ${{ github.token }}
# Only the stage 0 compiler is involved, so this needs no emulator run
# and finishes well before the bootstrapping jobs do.
- name: IR regression tests
run: make check-snapshot ARCH="$ARCH" DYNLINK="$DYNLINK"

# A tree that has already been built cannot be turned into a sanitized one:
# the object files are up to date, so the link that follows reuses them and
# produces a stage 0 compiler with no sanitizer in it. That is what the
Expand Down
52 changes: 8 additions & 44 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ OUT ?= out
# Every architecture that can be selected as a build target. The first is the
# default when ARCH is not given.
ARCHS = arm riscv x64
# The subset carrying reference IR snapshots. x64 has none yet, so the snapshot
# targets skip it; it is still a fully supported build target.
SNAPSHOT_ARCHS = arm riscv
ARCH ?= $(firstword $(ARCHS))
SRCDIR := $(shell find src -type d)
LIBDIR := $(shell find lib -type d)

BUILTIN_LIBC_SOURCE ?= c.c
BUILTIN_LIBC_HEADER := c.h
# --dump-ir is what makes out/shecc-stage1.log the IR of the stage 1 build
# rather than an empty file. It is the only thing in the tree that exercises
# dump_insn()/dump_ph2_ir(), and it is what a failed CI run uploads.
STAGE0_FLAGS ?= --dump-ir
STAGE1_FLAGS ?=
DYNLINK ?= 0
Expand All @@ -61,9 +61,6 @@ endif
SRCS := $(wildcard $(patsubst %,%/main.c, $(SRCDIR)))
OBJS := $(SRCS:%.c=$(OUT)/%.o)
deps := $(OBJS:%.o=%.o.d)
TESTS := $(wildcard tests/*.c)
TESTBINS := $(TESTS:%.c=$(OUT)/%.elf)
SNAPSHOTS = $(foreach SNAPSHOT_ARCH,$(SNAPSHOT_ARCHS), $(foreach SNAPSHOT_MODE,static dynamic, $(patsubst tests/%.c, tests/snapshots/%-$(SNAPSHOT_ARCH)-$(SNAPSHOT_MODE).json, $(TESTS))))

all: config bootstrap

Expand All @@ -86,10 +83,9 @@ endif
# Naming "config" or "distclean" anywhere in the goals is that reconfigure: the
# record is about to be rewritten or removed, so the architecture it still holds
# does not apply and the check must not fire. Testing for their presence rather
# than filtering them out is what lets a goal list combine them with real work
# -- check-snapshots and update-snapshots recurse with exactly
# "distclean config check-snapshot ARCH=...". "clean" touches no generated
# config, so a mismatch cannot affect it either.
# than filtering them out is what lets a goal list combine them with real work,
# as "make distclean config check ARCH=riscv" does. "clean" touches no
# generated config, so a mismatch cannot affect it either.
CONFIGURED_ARCH := $(shell sed -n 's/^ARCH=//p' $(BUILD_SESSION) 2>/dev/null)
ifneq (,$(CONFIGURED_ARCH))
ifeq (,$(filter config distclean,$(MAKECMDGOALS)))
Expand Down Expand Up @@ -123,19 +119,13 @@ config:
$(VECHO) "Target machine code switch to %s\n" $(ARCH)
$(Q)$(CONFIG_CHECK_CMD)

$(OUT)/tests/%.elf: tests/%.c $(OUT)/$(STAGE0)
$(VECHO) " SHECC\t$@\n"
$(Q)$(OUT)/$(STAGE0) $(STAGE0_FLAGS) -o $@ $< > $(basename $@).log ; \
chmod +x $@ ; $(PRINTF) "Running $@ ...\n"
$(Q)$(TARGET_EXEC) $@ && $(call pass)

check: check-stage0 check-stage2 check-abi-stage0 check-abi-stage2

check-stage0: $(OUT)/$(STAGE0) $(TESTBINS) tests/driver.sh
check-stage0: $(OUT)/$(STAGE0) tests/driver.sh
$(VECHO) " TEST STAGE 0\n"
tests/driver.sh 0 $(DYNLINK)

check-stage2: $(OUT)/$(STAGE2) $(TESTBINS) tests/driver.sh
check-stage2: $(OUT)/$(STAGE2) tests/driver.sh
$(VECHO) " TEST STAGE 2\n"
tests/driver.sh 2 $(DYNLINK)

Expand All @@ -145,38 +135,12 @@ check-sanitizer: $(OUT)/$(STAGE0)-sanitizer tests/driver.sh
tests/driver.sh 0 $(DYNLINK)
$(Q)rm $(OUT)/shecc

check-snapshots: $(OUT)/$(STAGE0) $(SNAPSHOTS) tests/check-snapshots.sh
# static linking
$(Q)$(foreach SNAPSHOT_ARCH, $(SNAPSHOT_ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=0 --silent;)
# dynamic linking
$(Q)$(foreach SNAPSHOT_ARCH, $(SNAPSHOT_ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=1 --silent;)
$(VECHO) "Switching backend back to %s (DYNLINK=0)\n" arm
$(Q)$(MAKE) distclean config ARCH=arm DYNLINK=0 --silent

check-snapshot: $(OUT)/$(STAGE0) tests/check-snapshots.sh
$(VECHO) "Checking snapshot for %s (DYNLINK=%s)\n" $(ARCH) $(DYNLINK)
tests/check-snapshots.sh $(ARCH) $(DYNLINK)
$(VECHO) " OK\n"

check-abi-stage0: $(OUT)/$(STAGE0)
tests/$(ARCH)-abi.sh 0 $(DYNLINK);

check-abi-stage2: $(OUT)/$(STAGE2)
tests/$(ARCH)-abi.sh 2 $(DYNLINK);

update-snapshots: tests/update-snapshots.sh
# static linking
$(Q)$(foreach SNAPSHOT_ARCH, $(SNAPSHOT_ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=0 --silent;)
# dynamic linking
$(Q)$(foreach SNAPSHOT_ARCH, $(SNAPSHOT_ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=1 --silent;)
$(VECHO) "Switching backend back to %s (DYNLINK=0)\n" arm
$(Q)$(MAKE) distclean config ARCH=arm DYNLINK=0 --silent

update-snapshot: $(OUT)/$(STAGE0) tests/update-snapshots.sh
$(VECHO) "Updating snapshot for %s (DYNLINK=%s)\n" $(ARCH) $(DYNLINK)
tests/update-snapshots.sh $(ARCH) $(DYNLINK)
$(VECHO) " OK\n"

# Both prerequisites are order-only, and both exist because "make -j" would
# otherwise let a compile start beside the thing it reads. Selecting a target
# replaces src/codegen.c with "ln -sf", which unlinks before it relinks, so a
Expand Down
54 changes: 21 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,6 @@ It is still possible to build `shecc` on macOS or Microsoft Windows. However,
the second stage bootstrapping would fail due to `qemu-arm` absence, and the
`x64` target expects an x86-64 GNU/Linux host to execute its own output.

To execute the snapshot test, install the packages below:
```shell
$ sudo apt-get install graphviz jq
```

### Additional packages

The dynamic linking mode needs an ELF interpreter and the matching glibc for the
Expand Down Expand Up @@ -161,12 +156,13 @@ $ make check-sanitizer

File `out/shecc` is the first stage compiler. Its usage:
```shell
$ shecc [-o output] [+m] [--no-libc] [--dump-ir] [--dynlink] [-E] <infile.c>
$ shecc [-o output] [+m] [--dot] [--no-libc] [--dump-ir] [--dynlink] [-E] <infile.c>
```

Compiler options:
- `-o` : Specify output file name (default: `out.elf`)
- `+m` : Use hardware multiplication/division instructions (default: disabled)
- `--dot` : Write the SSA control-flow graph in Graphviz DOT format and stop
- `--no-libc` : Exclude embedded C library (default: embedded)
- `--dump-ir` : Dump intermediate representation (IR)
- `--dynlink` : Use dynamic linking (default: disabled)
Expand All @@ -191,37 +187,14 @@ $ chmod +x fib
$ qemu-arm -L /usr/arm-linux-gnueabihf fib
```

### IR Regression Tests

To ensure the consistency of frontend (lexer, parser) behavior when working on it, the snapshot test is introduced.
The snapshot test dumps IRs from the executable and compares the structural identity with the provided snapshots.

Verify the emitted IRs by specifying `check-snapshots` target when invoking `make`:
```shell
$ make check-snapshots
```

If the compiler frontend is updated, the emitted IRs might be changed.
Thus, you can update snapshots by specifying `update-snapshots` target when invoking `make`:
```shell
$ make update-snapshots
```

Notice that the above 2 targets will update all backend snapshots at once, to update/check current backend's snapshot,
use `update-snapshot` / `check-snapshot` instead.

Reference IRs exist for the Arm and RISC-V backends only. The x86-64 backend
carries none yet, so the snapshot targets skip it. `check-snapshots` and
`update-snapshots` reconfigure the tree as they walk the backends and leave it
configured for Arm, so re-run `make config ARCH=...` afterwards if you were
building another target.

### Unit Tests

`shecc` comes with a comprehensive test suite (400+ test cases). To run the tests:
`shecc` has one behavioral test flow. `make check` runs the executable and
compiler-error tests with both the host-built and self-hosted compilers, then
runs the selected target's ABI tests. To run it:
```shell
# Add 'DYNLINK=1' if using the dynamic linking mode.
$ make check # Run all tests (stage 0 and stage 2, plus the ABI suite)
$ make check # Consolidated suite: stage 0, stage 2, and ABI tests
$ make check-stage0 # Test stage 0 compiler only
$ make check-stage2 # Test stage 2 compiler only
$ make check-abi-stage0 # Check the target calling convention (stage 0)
Expand Down Expand Up @@ -263,6 +236,21 @@ For resetting architecture configurations, use the command `make distclean`.

## Intermediate Representation

To visualize the SSA control-flow graph, use the standalone `--dot` target.
It writes Graphviz DOT with one cluster per function and IR instruction nodes
grouped by basic block; no executable is generated. The graph is printed before
phi values are unwound into edge copies, so the phi nodes are still in it, and
functions the input cannot reach -- most of the embedded C library -- are
pruned first. The default output replaces the input suffix with `.dot`.

```shell
$ out/shecc --dot -o fib.dot tests/fib.c
$ dot -Tsvg fib.dot -o fib.svg
```

Graphviz is needed to render the result, but not to produce it, and nothing in
`make check` depends on it.

Once the option `--dump-ir` is passed to `shecc`, the intermediate representation (IR)
will be generated. Take the file `tests/fib.c` for example. It consists of a recursive
Fibonacci sequence function.
Expand Down
Loading