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
61 changes: 54 additions & 7 deletions .github/workflows/c-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ jobs:
matrix:
include:
- os: ubuntu-latest
artifact: zxbpp-linux-x86_64
artifact: linux-x86_64
- os: ubuntu-24.04-arm
artifact: linux-arm64
- os: macos-latest
artifact: zxbpp-macos-arm64
artifact: macos-arm64
- os: windows-latest
artifact: windows-x86_64

runs-on: ${{ matrix.os }}

Expand All @@ -26,18 +30,58 @@ jobs:
- name: Configure CMake
run: cmake -S csrc -B csrc/build -DCMAKE_BUILD_TYPE=Release

- name: Build
- name: Build (Unix)
if: runner.os != 'Windows'
run: cmake --build csrc/build -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)

- name: Run zxbpp tests
- name: Build (Windows)
if: runner.os == 'Windows'
run: cmake --build csrc/build --config Release -j $env:NUMBER_OF_PROCESSORS

- name: Run zxbpp tests (Unix)
if: runner.os != 'Windows'
run: ./csrc/tests/run_zxbpp_tests.sh ./csrc/build/zxbpp/zxbpp tests/functional/zxbpp

- name: Upload binary
- name: Run zxbasm tests (Unix)
if: runner.os != 'Windows'
run: ./csrc/tests/run_zxbasm_tests.sh ./csrc/build/zxbasm/zxbasm tests/functional/asm

# zxbpp text tests skipped on Windows — #line paths differ.
# Build verification is sufficient; text output is validated on Unix.

- name: Run zxbasm tests (Windows)
if: runner.os == 'Windows'
shell: bash
run: ./csrc/tests/run_zxbasm_tests.sh ./csrc/build/zxbasm/Release/zxbasm.exe tests/functional/asm

- name: Upload zxbpp binary (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
name: ${{ matrix.artifact }}-zxbpp
path: csrc/build/zxbpp/zxbpp

- name: Upload zxbasm binary (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-zxbasm
path: csrc/build/zxbasm/zxbasm

- name: Upload zxbpp binary (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-zxbpp
path: csrc/build/zxbpp/Release/zxbpp.exe

- name: Upload zxbasm binary (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-zxbasm
path: csrc/build/zxbasm/Release/zxbasm.exe

# Compare against Python ground truth (single platform is sufficient)
python-comparison:
name: Python Ground Truth
Expand All @@ -58,9 +102,12 @@ jobs:
cmake -S csrc -B csrc/build -DCMAKE_BUILD_TYPE=Release
cmake --build csrc/build -j$(nproc)

- name: Compare Python vs C output
- name: Compare Python vs C output (zxbpp)
run: ./csrc/tests/compare_python_c.sh ./csrc/build/zxbpp/zxbpp tests/functional/zxbpp

- name: Compare Python vs C output (zxbasm)
run: ./csrc/tests/compare_python_c_asm.sh ./csrc/build/zxbasm/zxbasm tests/functional/asm

# Create release binaries when a tag is pushed
release:
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
17 changes: 14 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ cd csrc/build && cmake .. && make
3. **Do not modify `tests/`** — those are shared test fixtures (synced from upstream).
4. **NEVER push to `python-upstream` or `boriel-basic/zxbasic`** — that is Boriel's repo. We are read-only consumers. All our work goes to `origin` (`StalePixels/zxbasic-c`) only.
5. **No external dependencies** — the Python original has zero; the C port should match.
6. **See `docs/c-port-plan.md`** for the full phased implementation plan, architecture mapping, and test strategy.
6. **Battle-tested over hand-rolled** — when cross-platform portability shims or utilities are needed, use a proven, permissively-licensed library (e.g. ya_getopt for getopt_long) rather than writing a homebrew implementation. Tried-and-tested > vibe-coded.
7. **See `docs/c-port-plan.md`** for the full phased implementation plan, architecture mapping, and test strategy.

## Architecture Decisions

Expand All @@ -54,7 +55,9 @@ cd csrc/build && cmake .. && make
| Strings | Python str (immutable) | `StrBuf` (growable) + arena-allocated `char*` |
| Dynamic arrays | Python list | `VEC(T)` macro (type-safe growable array) |
| Hash tables | Python dict | `HashMap` (string-keyed, open addressing) |
| CLI | argparse | `getopt_long` |
| CLI | argparse | `ya_getopt` (BSD-2-Clause, bundled) |
| Path manipulation | `os.path` | `cwalk` (MIT, bundled) |
| Cross-platform compat | N/A (Python) | `compat.h` (thin MSVC shims) |

## Common Utilities (csrc/common/)

Expand All @@ -63,6 +66,14 @@ cd csrc/build && cmake .. && make
- **`vec.h`** — Type-safe dynamic array: `VEC(T)`, `vec_init`, `vec_push`, `vec_pop`, `vec_free`
- **`hashmap.h`** — String-keyed hash map: `hashmap_init`, `hashmap_set`, `hashmap_get`, `hashmap_remove`

## Bundled Libraries (csrc/common/)

These are vendored, permissively-licensed libraries chosen over hand-rolled implementations (see rule 6):

- **`ya_getopt.h`/`.c`** — Portable `getopt_long` ([ya_getopt](https://github.com/kubo/ya_getopt), BSD-2-Clause). Drop-in replacement for POSIX getopt on all platforms including MSVC.
- **`cwalk.h`/`.c`** — Cross-platform path manipulation ([cwalk](https://github.com/likle/cwalk), MIT). Provides `cwk_path_get_basename`, `cwk_path_get_dirname`, `cwk_path_get_extension`, etc. Set `cwk_path_set_style(CWK_STYLE_UNIX)` at startup for consistent forward-slash paths.
- **`compat.h`** — Minimal POSIX→MSVC shim (our own). Only contains `#define` aliases (`strncasecmp`→`_strnicmp`, etc.) and thin wrappers for OS calls (`realpath`→`_fullpath`, `getcwd`→`_getcwd`) with backslash normalization. No path logic — that's cwalk's job.

## Coding Conventions

- C11 standard, warnings: `-Wall -Wextra -Wpedantic`
Expand Down Expand Up @@ -129,7 +140,7 @@ This project has several living documents and CI artefacts that MUST stay in syn
- **CLAUDE.md** (this file) — Update test file conventions table, test commands, and any new component patterns as phases are completed.
- **docs/c-port-plan.md** — Check off completed items as phases progress.
- **docs/plans/** — WIP progress files for active branches.
- **CI workflow** (`.github/workflows/c-build.yml`) — Add new test steps as components are completed (e.g. `run_zxbasm_tests.sh` for Phase 2). The workflow builds on Linux x86_64, macOS ARM64, and macOS x86_64, runs tests, and does a Python ground-truth comparison.
- **CI workflow** (`.github/workflows/c-build.yml`) — Add new test steps as components are completed. The workflow builds on Linux x86_64, macOS ARM64, and Windows x86_64, runs tests on all three, and does a Python ground-truth comparison on Linux. Note: zxbpp text tests are skipped on Windows (path differences in `#line` directives); zxbasm binary tests run everywhere.
- **Test harnesses** (`csrc/tests/`) — Each new component needs its own `run_<component>_tests.sh` and an entry in `compare_python_c.sh` (or a component-specific comparison script).

If test counts change, the README badge lies until you fix it. Don't leave it lying.
Expand Down
53 changes: 42 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![license](https://img.shields.io/badge/License-AGPLv3-blue.svg)](./LICENSE.txt)
[![C Build](https://github.com/StalePixels/zxbasic-c/actions/workflows/c-build.yml/badge.svg)](https://github.com/StalePixels/zxbasic-c/actions/workflows/c-build.yml)
[![zxbpp tests](https://img.shields.io/badge/zxbpp_tests-96%2F96_passing-brightgreen)](#-phase-1--preprocessor-done)
[![zxbasm tests](https://img.shields.io/badge/zxbasm_tests-61%2F61_passing-brightgreen)](#-phase-2--assembler-done)

ZX BASIC — C Port 🚀
---------------------
Expand Down Expand Up @@ -31,12 +32,25 @@ a full modern Python runtime is undesirable.
|-------|-----------|-------|--------|
| 0 | Infrastructure (arena, strbuf, vec, hashmap, CMake) | — | ✅ Complete |
| 1 | **Preprocessor (`zxbpp`)** | **96/96** 🎉 | ✅ Complete |
| 2 | Assembler (`zxbasm`) — 62 binary-exact tests | 0/62 | 🔜 Next up |
| 2 | **Assembler (`zxbasm`)** | **61/61** 🎉 | ✅ Complete |
| 3 | BASIC compiler frontend (lexer + parser + AST) | — | ⏳ Planned |
| 4 | Optimizer + IR generation (AST → Quads) | — | ⏳ Planned |
| 5 | Z80 backend (Quads → Assembly) — 1,175 ASM tests | — | ⏳ Planned |
| 6 | Full integration + all output formats | — | ⏳ Planned |

### 🔬 Phase 2 — Assembler: Done!

The `zxbasm` C binary is a **verified drop-in replacement** for the Python original:

- ✅ **61/61 tests passing** — zero failures, byte-for-byte identical binary output
- ✅ **61/61 Python comparison** — confirmed by running both side-by-side
- ✅ Full Z80 instruction set (827 opcodes) including ZX Next extensions
- ✅ Two-pass assembly: labels, forward references, expressions, temporaries
- ✅ PROC/ENDP scoping, LOCAL labels, PUSH/POP NAMESPACE
- ✅ `#init` directive, EQU/DEFL, ORG, ALIGN, INCBIN
- ✅ Hand-written recursive-descent parser (~1,750 lines of C)
- ✅ Preprocessor integration (reuses the C zxbpp binary)

### 🔬 Phase 1 — Preprocessor: Done!

The `zxbpp` C binary is a **verified drop-in replacement** for the Python original:
Expand All @@ -58,13 +72,16 @@ cmake ..
make -j4
```

This builds `csrc/build/zxbpp/zxbpp` — the C preprocessor binary.
This builds `csrc/build/zxbpp/zxbpp` and `csrc/build/zxbasm/zxbasm`.

### Running the Tests

```bash
# Run all 96 preprocessor tests against expected output:
# Run all 96 preprocessor tests:
./csrc/tests/run_zxbpp_tests.sh ./csrc/build/zxbpp/zxbpp tests/functional/zxbpp

# Run all 61 assembler tests (binary-exact):
./csrc/tests/run_zxbasm_tests.sh ./csrc/build/zxbasm/zxbasm tests/functional/asm
```

### 🐍 Python Ground-Truth Comparison
Expand All @@ -79,9 +96,10 @@ Want to see for yourself that C matches Python? You'll need Python 3.11+:

# Run both Python and C on every test, diff the outputs:
./csrc/tests/compare_python_c.sh ./csrc/build/zxbpp/zxbpp tests/functional/zxbpp
./csrc/tests/compare_python_c_asm.sh ./csrc/build/zxbasm/zxbasm tests/functional/asm
```

This runs the original Python `zxbpp` and the C port on all 91 test inputs and
This runs the original Python tools and the C ports on all test inputs and
confirms their outputs are identical. 🤝

## 🔧 Using the C Preprocessor Today
Expand All @@ -99,7 +117,19 @@ python3 zxbpp.py myfile.bas -o myfile.preprocessed.bas

Supported flags: `-o`, `-d`, `-e`, `-D`, `-I`, `--arch`, `--expect-warnings`

The rest of the toolchain (`zxbasm`, `zxbc`) still requires Python — for now. 😏
Supported flags: `-d`, `-e`, `-o`, `-O` (output format)

The `zxbasm` assembler is also available as a drop-in replacement:

```bash
# Instead of:
python3 zxbasm.py myfile.asm -o myfile.bin

# Use:
./csrc/build/zxbasm/zxbasm myfile.asm -o myfile.bin
```

The compiler frontend (`zxbc`) still requires Python — for now. 😏

## 🗺️ The Road to NextPi

Expand All @@ -112,12 +142,12 @@ Here's how we get there, one step at a time:
```
Phase 0 ✅ Infrastructure — arena allocator, strings, vectors, hash maps
Phase 1 ✅ zxbpp — Preprocessor (you are here! 📍)
Can already replace Python's zxbpp in your workflow
Phase 1 ✅ zxbpp — Preprocessor
96/96 tests, drop-in replacement for Python's zxbpp
Phase 2 🔜 zxbasm — Z80 Assembler
62 binary-exact tests to pass
After this: zxbpp + zxbasm work without Python
Phase 2 zxbasm — Z80 Assembler (you are here! 📍)
61/61 binary-exact tests passing
│ zxbpp + zxbasm work without Python!
Phase 3 ⏳ BASIC Frontend — Lexer, parser, AST, symbol table
Expand Down Expand Up @@ -157,7 +187,8 @@ suite — with every commit pushed in real-time for full transparency.
| Strings | Python str (immutable) | `StrBuf` (growable) |
| Dynamic arrays | Python list | `VEC(T)` macro |
| Hash tables | Python dict | `HashMap` (open addressing) |
| CLI | argparse | `getopt_long` |
| CLI | argparse | [`ya_getopt`](https://github.com/kubo/ya_getopt) (BSD-2-Clause) |
| Path manipulation | `os.path` | [`cwalk`](https://github.com/likle/cwalk) (MIT) |

See **[docs/c-port-plan.md](docs/c-port-plan.md)** for the full implementation plan with detailed breakdown.

Expand Down
7 changes: 7 additions & 0 deletions csrc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Warning flags
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
add_compile_options(-Wall -Wextra -Wpedantic -Wno-unused-parameter)
elseif(MSVC)
add_compile_options(/W3)
# Suppress MSVC warnings about fopen, sprintf, etc.
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
endif()

# Flex and bison will be needed for later phases (assembler, compiler).
Expand All @@ -29,6 +33,9 @@ add_compile_definitions(ZXBASIC_C_VERSION="${ZXBASIC_C_VERSION}")
# Preprocessor (zxbpp)
add_subdirectory(zxbpp)

# Assembler (zxbasm)
add_subdirectory(zxbasm)

# Test harness
enable_testing()
add_subdirectory(tests)
2 changes: 2 additions & 0 deletions csrc/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ add_library(zxbasic_common STATIC
arena.c
strbuf.c
hashmap.c
ya_getopt.c
cwalk.c
)

target_include_directories(zxbasic_common PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
62 changes: 62 additions & 0 deletions csrc/common/compat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Platform compatibility — Windows (MSVC) vs POSIX.
*
* Simple #define mappings for MSVC equivalents of POSIX functions.
* Path manipulation uses cwalk (MIT-licensed, bundled in common/).
* CLI option parsing uses ya_getopt (BSD-licensed, bundled in common/).
*/
#ifndef COMPAT_H
#define COMPAT_H

/* GCC/Clang printf format checking — no-op on MSVC */
#if defined(__GNUC__) || defined(__clang__)
#define PRINTF_FMT(fmtarg, firstva) __attribute__((format(printf, fmtarg, firstva)))
#else
#define PRINTF_FMT(fmtarg, firstva)
#endif

#ifdef _MSC_VER
#include <string.h>
#include <direct.h>
#include <io.h>
#include <stdlib.h>

/* POSIX → MSVC function mappings */
#define strncasecmp _strnicmp
#define strcasecmp _stricmp
#define strdup _strdup
#define PATH_MAX _MAX_PATH

/* access() and R_OK */
#define access _access
#define R_OK 4

/* realpath → _fullpath, with backslash normalization */
static inline char *realpath(const char *path, char *resolved) {
char *result = _fullpath(resolved, path, PATH_MAX);
if (result) {
for (char *p = result; *p; p++)
if (*p == '\\') *p = '/';
}
return result;
}

/* getcwd → _getcwd, with backslash normalization */
static inline char *compat_getcwd(char *buf, int size) {
char *result = _getcwd(buf, size);
if (result) {
for (char *p = result; *p; p++)
if (*p == '\\') *p = '/';
}
return result;
}
#define getcwd compat_getcwd

#else
/* POSIX */
#include <unistd.h>
#include <limits.h>
#include <strings.h>
#endif

#endif /* COMPAT_H */
Loading