Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3a1cbc6
wip: start feature/phase3-zxbc — init progress tracker
Xalior Mar 7, 2026
cdaa84d
Pedantry
Xalior Mar 7, 2026
ab28fe0
feat: add zxbc skeleton — types, AST, symbol table, options, errmsg, CLI
Xalior Mar 7, 2026
c2b8a8a
feat: add BASIC lexer with all states and ZX Spectrum string escapes
Xalior Mar 7, 2026
e1f5ef9
feat: add BASIC parser — hand-written recursive descent with Pratt ex…
Xalior Mar 7, 2026
166bdf7
fix: parser improvements — 984/1036 tests pass (95%)
Xalior Mar 7, 2026
7ecfc80
docs: update WIP progress — parser at 95% (984/1036 tests)
Xalior Mar 7, 2026
2c9a253
fix: parser improvements — 1020/1036 tests pass (98.5%)
Xalior Mar 7, 2026
e1f3fc8
docs: update WIP — 1020/1036 tests (98.5%)
Xalior Mar 7, 2026
480537f
fix: parser quality — proper gfx attributes, no voided values (1030/1…
Xalior Mar 7, 2026
14e3528
feat: integrate zxbpp preprocessor into zxbc — 1036/1036 tests pass (…
Xalior Mar 7, 2026
d9385ba
feat: add -W/--disable-warning and +W/--enable-warning flags
Xalior Mar 7, 2026
fbd13a0
docs: add full test inventory to CLAUDE.md — cmdline, API, symbols, a…
Xalior Mar 7, 2026
4673483
chore: add .claude/ to gitignore
Xalior Mar 7, 2026
b97de37
wip: start phase 3 test coverage — init progress tracker
Xalior Mar 7, 2026
38da3b2
feat: fix --org storage, add parse_int, config file loading, parse_on…
Xalior Mar 7, 2026
f4d7aef
test: add unit tests matching Python test suites — 56 tests, all pass
Xalior Mar 7, 2026
956eee3
ci: add unit tests and cmdline tests to CI workflow
Xalior Mar 7, 2026
7a74f74
fix: Windows build — add compat.h for strcasecmp, fix test_config por…
Xalior Mar 7, 2026
ee654b3
feat: extract args parsing, add cmdline value tests matching Python
Xalior Mar 7, 2026
1720ed4
feat: add full symbol table API + check module, matching Python test …
Xalior Mar 7, 2026
484dc65
feat: expand AST tests to cover all 19 symbol node types (61 tests)
Xalior Mar 7, 2026
edd6684
docs: update all docs for release 1.18.7+c3, bump VERSION
Xalior Mar 7, 2026
1d6178d
docs: merge Phase 3 plans into single unified document
Xalior Mar 7, 2026
25a9797
feat: add semantic analysis infrastructure — check predicates, make_t…
Xalior Mar 7, 2026
d541e05
feat: wire symbol resolution into parser — access_var, access_call
Xalior Mar 7, 2026
ef76a1f
docs: correct test metric — exit-code parity with Python, not syntax-…
Xalior Mar 7, 2026
75d7ca6
feat: semantic fixes — suffix handling, access_var/call, typecast saf…
Xalior Mar 7, 2026
b8abea9
feat: function scope, pragma handling, access_id fix, builtin precede…
Xalior Mar 7, 2026
0bc9aaa
feat: strict mode, pragma handling, check_is_declared error reporting…
Xalior Mar 7, 2026
012a978
feat: SUB/CONST class checks, function class mismatch detection (934/…
Xalior Mar 7, 2026
ca40862
feat: lvalue checks, IF THEN: fix, expr-context callable validation (…
Xalior Mar 7, 2026
fe2326b
feat: global labels, check_pending_labels for GOTO targets (953/1036)
Xalior Mar 7, 2026
782cada
feat: GOSUB-in-func check, duplicate labels, function_level tracking …
Xalior Mar 7, 2026
519caa3
feat: DATA-in-func, duplicate var DIM, GOSUB-in-func checks (958/1036)
Xalior Mar 7, 2026
6bc0458
feat: string array init check, DIM AT+init error, suffix type inferen…
Xalior Mar 7, 2026
1837bce
feat: detect duplicate function/sub definitions (964/1036 tests pass)
Xalior Mar 7, 2026
a58d0de
feat: add parse-only baselines for accurate zxbc test comparison
Xalior Mar 7, 2026
5abb899
fix: proper DECLARE handling — forward declarations skip body parsing
Xalior Mar 7, 2026
9fdda6e
fix: explicit mode checks — report undeclared variables in #pragma ex…
Xalior Mar 7, 2026
ef41b95
feat: add check_pending_calls, fix DIM suffix stripping (967/1036, 0 FP)
Xalior Mar 7, 2026
ebcdb88
feat: function forward-decl validation — type/param mismatch, SUB-as-…
Xalior Mar 7, 2026
5b7a2e7
feat: DATA/READ validation, mandatory-after-optional param check (976…
Xalior Mar 7, 2026
48b0d24
feat: READ target validation — array/lvalue checks (980/1036, 0 FP)
Xalior Mar 7, 2026
757516f
docs: update Phase 3 progress — 980/1036 (94.6%), categorize remainin…
Xalior Mar 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 31 additions & 0 deletions .github/workflows/c-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ jobs:
if: runner.os != 'Windows'
run: ./csrc/tests/run_zxbasm_tests.sh ./csrc/build/zxbasm/zxbasm tests/functional/asm

- name: Run unit tests (Unix)
if: runner.os != 'Windows'
run: |
cd csrc/build
./tests/test_utils
./tests/test_config
./tests/test_types
./tests/test_ast
./tests/test_symboltable
./tests/test_check
cd ../..
./csrc/build/tests/test_cmdline

- name: Run cmdline tests (Unix)
if: runner.os != 'Windows'
run: ./csrc/tests/run_cmdline_tests.sh ./csrc/build/zxbc/zxbc tests/cmdline

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

Expand All @@ -54,6 +71,20 @@ jobs:
shell: bash
run: ./csrc/tests/run_zxbasm_tests.sh ./csrc/build/zxbasm/Release/zxbasm.exe tests/functional/asm

- name: Run unit tests (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
cd csrc/build
./tests/Release/test_utils.exe
./tests/Release/test_config.exe
./tests/Release/test_types.exe
./tests/Release/test_ast.exe
./tests/Release/test_symboltable.exe
./tests/Release/test_check.exe
cd ../..
./csrc/build/tests/Release/test_cmdline.exe

- name: Upload zxbpp binary (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ build/
venv/
.pypi-token
.coverage.*
.claude/
22 changes: 20 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ cd csrc/build && cmake .. && make
5. **No external dependencies** — the Python original has zero; the C port should match.
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.
8. **Never discard, void, or stub out parsed values.** This is a byte-for-byte compiler port. Every language construct must produce correct AST output — no `(void)result`, no token-skipping loops, no "consume until newline" shortcuts. If the Python builds an AST node from a parsed value, the C must too. If you don't know how to handle a construct, stop and study the Python — don't guess, don't skip, don't stub. A parse that silently drops data is worse than a parse that fails loudly.
9. **No speculative or guess-based parsing.** Don't invent grammar rules or function signatures. Every parser handler must correspond to an actual Python grammar production. Read `src/zxbc/zxbparser.py` (or the relevant Python source) and implement exactly what it does — including which AST nodes are created, what children they have, and what names/tags they use.

## Architecture Decisions

| Aspect | Python Original | C Approach |
|--------|----------------|------------|
| Parsing (zxbpp) | PLY lex/yacc | Hand-written recursive-descent |
| Parsing (zxbasm, zxbc) | PLY lex/yacc | flex + bison |
| Parsing (zxbasm, zxbc) | PLY lex/yacc | Hand-written recursive-descent |
| AST nodes | 50+ classes with inheritance | Tagged union structs with common header |
| Memory | Python GC | Arena allocator (allocate during compilation, free all at end) |
| Strings | Python str (immutable) | `StrBuf` (growable) + arena-allocated `char*` |
Expand Down Expand Up @@ -110,6 +112,8 @@ Each component gets two test harnesses in `csrc/tests/`:

Always validate against Python when adding features — don't trust assumptions.

**What "matching" means:** A test "matches" when C and Python produce the **same exit code** for the same input file and flags. Not "C exits 0" — that only measures syntax parsing. Python's `--parse-only` runs full semantic analysis, post-parse validation, and AST visitors before returning. A file that Python rejects with exit code 1 (semantic error) must also be rejected by C with exit code 1.

```bash
# Build and quick test:
cd csrc/build && cmake .. && make -j4 && cd ../..
Expand All @@ -127,11 +131,25 @@ Each component has its own input/output file types:
|-----------|-------|-----------------|----------|
| zxbpp | `.bi` | `.out` (stdout), `.err` (errors) | `tests/functional/zxbpp/` |
| zxbasm | `.asm` | `.bin` (binary) | `tests/functional/asm/` |
| zxbc | `.bas` | varies (`.asm`, `.bin`, `.tap`, etc.) | `tests/functional/arch/` |
| zxbc | `.bas` | `.asm` (assembly output) | `tests/functional/arch/zx48k/` |

- C binaries must accept **identical CLI flags** as the Python originals
- Python test runner: `tests/functional/test.py` (used by pytest via `test_prepro.py`, `test_asm.py`, `test_basic.py`)

### Beyond Functional Tests — Full Test Inventory

The Python project has unit and integration tests beyond the functional `.bas`/`.bi`/`.asm` files. **All of these must be matched by the C port.** Don't assume functional tests are sufficient.

| Suite | Location | What it tests | Files |
|-------|----------|---------------|-------|
| CLI / flags | `tests/cmdline/` | `--parse-only`, `--org` hex, `-F` config file, cmdline-overrides-config | `test_zxb.py` + fixtures |
| API / config | `tests/api/` | arg parser defaults, type checking, symbol table, config, utils | 5 test files |
| AST nodes | `tests/symbols/` | Node construction for all 20 AST node types | 20 test files |
| Backend | `tests/arch/zx48k/backend/` | Memory cell operations | 1 test file |
| Optimizer | `tests/arch/zx48k/optimizer/` | Basic blocks, CPU state, optimizer passes | 6 test files |
| Peephole | `tests/arch/zx48k/peephole/` | Pattern matching, evaluation, templates | 4 test files |
| Compiler | `tests/zxbc/` | Parser table generation | 1 test file |

## Keeping Things Up To Date

This project has several living documents and CI artefacts that MUST stay in sync with the code. When you add features, fix bugs, or complete phases:
Expand Down
81 changes: 65 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
[![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)
[![zxbc parse-only](https://img.shields.io/badge/zxbc_parse--only-914%2F1036_matching_Python-yellow)](#-phase-3--compiler-frontend-in-progress)
[![C unit tests](https://img.shields.io/badge/C_unit_tests-132_passing-blue)](#c-unit-test-suite)

ZX BASIC — C Port 🚀
---------------------
Expand All @@ -23,8 +25,9 @@ The toolchain being ported — `zxbc` (compiler), `zxbasm` (assembler), and `zxb
suite of 1,285+ functional tests.

The practical end-goal: a C implementation of the compiler suitable for **embedding on
[NextPi](https://www.specnext.com/)** and similar resource-constrained platforms where
a full modern Python runtime is undesirable.
[NextPi](https://www.specnext.com/)** and similar resource-constrained platforms. The
NextPi ships with a lightweight Python 2 install, but ZX BASIC requires Python 3.11+ —
far too heavy for the hardware. Native C binaries sidestep the problem entirely.

## 📊 Current Status

Expand All @@ -33,11 +36,45 @@ a full modern Python runtime is undesirable.
| 0 | Infrastructure (arena, strbuf, vec, hashmap, CMake) | — | ✅ Complete |
| 1 | **Preprocessor (`zxbpp`)** | **96/96** 🎉 | ✅ Complete |
| 2 | **Assembler (`zxbasm`)** | **61/61** 🎉 | ✅ Complete |
| 3 | BASIC compiler frontend (lexer + parser + AST) | — | ⏳ Planned |
| 3 | **Compiler frontend (`zxbc`)** | **914/1036** matching Python | 🔨 In Progress |
| 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 3 — Compiler Frontend: In Progress

The `zxbc` frontend is measured by **exit-code parity with Python** — for every `.bas` test file, does the C binary produce the same exit code as the Python original?

- ✅ **914/1036 matching Python** (88%) — C and Python agree on pass/fail
- ❌ **122 mismatches** — all cases where Python catches a semantic error but C doesn't yet
- ✅ **0 false positives** — C never errors on a file that Python accepts
- ✅ Hand-written recursive-descent lexer + parser (all 1036 files parse syntactically)
- ✅ Full AST with tagged-union node types (30 node kinds)
- ✅ Symbol table with lexical scoping, type registry, basic types
- ✅ Type system: all ZX BASIC types (`byte` through `string`), aliases, refs
- ✅ Semantic infrastructure: type coercion, constant folding, symbol resolution
- ✅ CLI with all `zxbc` flags, config file loading, `--parse-only`
- ✅ Preprocessor integration (reuses C zxbpp via static library)
- 🔨 Remaining semantic analysis: function scope, post-parse validation, AST visitors

#### C Unit Test Suite

Beyond matching Python's functional tests, the C port has its own unit test suite
verifying internal APIs match the Python test suites (`tests/api/`, `tests/symbols/`,
`tests/cmdline/`):

| Test Program | Tests | Matches Python |
|-------------|-------|----------------|
| `test_utils` | 14 | `tests/api/test_utils.py` |
| `test_config` | 6 | `tests/api/test_config.py` + `test_arg_parser.py` |
| `test_types` | 10 | `tests/symbols/test_symbolBASICTYPE.py` |
| `test_ast` | 61 | All 19 `tests/symbols/test_symbol*.py` files |
| `test_symboltable` | 22 | `tests/api/test_symbolTable.py` (18 + 4 C extras) |
| `test_check` | 4 | `tests/api/test_check.py` |
| `test_cmdline` | 15 | `tests/cmdline/test_zxb.py` + `test_arg_parser.py` |
| `run_cmdline_tests.sh` | 4 | `tests/cmdline/test_zxb.py` (exit-code) |
| **Total** | **136** | |

### 🔬 Phase 2 — Assembler: Done!

The `zxbasm` C binary is a **verified drop-in replacement** for the Python original:
Expand Down Expand Up @@ -72,7 +109,7 @@ cmake ..
make -j4
```

This builds `csrc/build/zxbpp/zxbpp` and `csrc/build/zxbasm/zxbasm`.
This builds `csrc/build/zxbpp/zxbpp`, `csrc/build/zxbasm/zxbasm`, and `csrc/build/zxbc/zxbc`.

### Running the Tests

Expand All @@ -82,6 +119,14 @@ This builds `csrc/build/zxbpp/zxbpp` and `csrc/build/zxbasm/zxbasm`.

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

# Run 132 C unit tests:
cd csrc/build && ./tests/test_utils && ./tests/test_config && ./tests/test_types \
&& ./tests/test_ast && ./tests/test_symboltable && ./tests/test_check && cd ../..
./csrc/build/tests/test_cmdline

# Run 4 zxbc command-line tests:
./csrc/tests/run_cmdline_tests.sh ./csrc/build/zxbc/zxbc tests/cmdline
```

### 🐍 Python Ground-Truth Comparison
Expand Down Expand Up @@ -117,25 +162,28 @@ python3 zxbpp.py myfile.bas -o myfile.preprocessed.bas

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

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. 😏
Supported flags: `-d`, `-e`, `-o`, `-O` (output format)

The compiler frontend (`zxbc`) can already parse all BASIC sources:

```bash
./csrc/build/zxbc/zxbc --parse-only myfile.bas
```

Full compilation (code generation) is coming next. 😏

## 🗺️ The Road to NextPi

The big picture: a fully native C compiler toolchain that runs on the
[NextPi](https://www.specnext.com/) — a Raspberry Pi accelerator board for the
ZX Spectrum Next. No Python runtime needed, just a single binary.
ZX Spectrum Next. The NextPi has a lightweight Python 2, but ZX BASIC needs
Python 3.11+ which is impractical on that hardware. Native C binaries solve this.

Here's how we get there, one step at a time:

Expand All @@ -145,11 +193,12 @@ Here's how we get there, one step at a time:
Phase 1 ✅ zxbpp — Preprocessor
│ 96/96 tests, drop-in replacement for Python's zxbpp
Phase 2 ✅ zxbasm — Z80 Assembler (you are here! 📍)
Phase 2 ✅ zxbasm — Z80 Assembler
│ 61/61 binary-exact tests passing
│ zxbpp + zxbasm work without Python!
Phase 3 ⏳ BASIC Frontend — Lexer, parser, AST, symbol table
Phase 3 🔨 BASIC Frontend (you are here! 📍)
│ 1036/1036 parse-only + 132 unit tests
Phase 4 ⏳ Optimizer + IR — AST → Quads intermediate code
Expand All @@ -159,7 +208,7 @@ Here's how we get there, one step at a time:
Phase 6 ⏳ Integration — All output formats (.tap, .tzx, .sna, .z80)
│ Full CLI compatibility with zxbc
🏁 Single static binary: zxbasic for NextPi and embedded platforms
🏁 Native binaries for NextPi and embedded platforms — no Python needed
```

Each phase is independently useful — you don't have to wait for the whole thing.
Expand All @@ -181,7 +230,7 @@ suite — with every commit pushed in real-time for full transparency.
| Aspect | Python Original | C Port |
|--------|----------------|--------|
| Parsing (zxbpp) | PLY lex/yacc | Hand-written recursive-descent |
| Parsing (zxbasm, zxbc) | PLY lex/yacc | flex + bison |
| Parsing (zxbasm, zxbc) | PLY lex/yacc | Hand-written recursive-descent |
| AST nodes | 50+ classes with inheritance | Tagged union structs |
| Memory | Python GC | Arena allocator |
| Strings | Python str (immutable) | `StrBuf` (growable) |
Expand Down
3 changes: 3 additions & 0 deletions csrc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ add_subdirectory(zxbpp)
# Assembler (zxbasm)
add_subdirectory(zxbasm)

# Compiler (zxbc)
add_subdirectory(zxbc)

# Test harness
enable_testing()
add_subdirectory(tests)
2 changes: 1 addition & 1 deletion csrc/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.18.7+c1
1.18.7+c3
2 changes: 2 additions & 0 deletions csrc/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ add_library(zxbasic_common STATIC
hashmap.c
ya_getopt.c
cwalk.c
utils.c
config_file.c
)

target_include_directories(zxbasic_common PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
82 changes: 82 additions & 0 deletions csrc/common/config_file.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* config_file.c — Simple .ini config file reader
*
* Ported from Python's configparser usage in src/api/config.py.
*/
#include "config_file.h"
#include "hashmap.h"
#include "compat.h"

#include <ctype.h>
#include <stdio.h>
#include <string.h>

/* Trim leading and trailing whitespace in-place, return pointer to trimmed start */
static char *trim(char *s) {
while (*s && isspace((unsigned char)*s)) s++;
size_t len = strlen(s);
while (len > 0 && isspace((unsigned char)s[len - 1])) s[--len] = '\0';
return s;
}

int config_load_section(const char *filename, const char *section, config_callback cb, void *userdata) {
FILE *f = fopen(filename, "r");
if (!f) return -1;

char line[1024];
bool in_section = false;
bool section_found = false;
HashMap seen_keys;
hashmap_init(&seen_keys);

int result = 0;

while (fgets(line, sizeof(line), f)) {
char *p = trim(line);

/* Skip empty lines and comments */
if (!*p || *p == '#' || *p == ';') continue;

/* Section header */
if (*p == '[') {
char *end = strchr(p, ']');
if (!end) continue;
*end = '\0';
char *sec_name = trim(p + 1);

if (strcasecmp(sec_name, section) == 0) {
in_section = true;
section_found = true;
} else {
if (in_section) break; /* left our section */
}
continue;
}

if (!in_section) continue;

/* key = value */
char *eq = strchr(p, '=');
if (!eq) continue;
*eq = '\0';
char *key = trim(p);
char *value = trim(eq + 1);

/* Check for duplicate keys (Python configparser raises on duplicates) */
if (hashmap_get(&seen_keys, key)) {
result = -2;
break;
}
hashmap_set(&seen_keys, key, (void *)1);

if (!cb(key, value, userdata)) {
break;
}
}

hashmap_free(&seen_keys);
fclose(f);

if (result < 0) return result;
return section_found ? 1 : 0;
}
27 changes: 27 additions & 0 deletions csrc/common/config_file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* config_file.h — Simple .ini config file reader
*
* Ported from Python's configparser usage in src/api/config.py.
* Supports [section] headers and key = value pairs.
*/
#ifndef ZXBC_CONFIG_FILE_H
#define ZXBC_CONFIG_FILE_H

#include <stdbool.h>

/* Callback invoked for each key=value pair found in the target section.
* Return true to continue, false to abort parsing. */
typedef bool (*config_callback)(const char *key, const char *value, void *userdata);

/*
* Load a config file and invoke callback for each key=value in the given section.
*
* Returns:
* 1 on success (section found and parsed)
* 0 if section not found
* -1 on file open error
* -2 on parse error (e.g. duplicate keys)
*/
int config_load_section(const char *filename, const char *section, config_callback cb, void *userdata);

#endif /* ZXBC_CONFIG_FILE_H */
Loading
Loading