Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
22f1c3a
Start the Rust runtime: crate, error module, built beside the asm
jgarzik Oct 10, 2026
78cfddc
rt: the module layout, and the interfaces shared between modules
jgarzik Oct 10, 2026
39969ec
rt: port math.s -- RND, RANDOMIZE, TIMER, DATE$, TIME$, BEEP, CLS, LO…
jgarzik Oct 10, 2026
1ef4764
rt: port number formatting, VAL, the string helpers and DATA to Rust
jgarzik Oct 10, 2026
b7526e6
Merge the rt port of num, string and data
jgarzik Oct 10, 2026
ac4e30e
build.rs: keep core in rt.o, by taking the LTO member of the staticlib
jgarzik Oct 10, 2026
60120d7
rt: port files, console input, PRINT USING and platform init to Rust
jgarzik Oct 10, 2026
2b26a55
rt: declare the allocator as the C library does; link UCRT's stdio on…
jgarzik Oct 10, 2026
f281971
Merge the rt port of files, console input, PRINT USING and platform init
jgarzik Oct 10, 2026
d51c794
rt: FIELD overflow on a huge width; keep record buffers alive for FIE…
jgarzik Oct 10, 2026
3f5a752
Link the Rust runtime; delete the assembly runtime
jgarzik Oct 10, 2026
407c636
Merge the FIELD overflow and record-buffer lifetime fixes
jgarzik Oct 10, 2026
0fd1698
Test FIELD's widest width and a FIELD variable outliving its file
jgarzik Oct 10, 2026
6bc8122
Compile for aarch64: Linux and macOS arm64
jgarzik Oct 10, 2026
4ad0166
rt tests: leave no directories in /tmp
jgarzik Oct 10, 2026
45b2e8d
rt tests: run on Windows -- CRLF, and name an unexpected fatal error
jgarzik Oct 10, 2026
ad3d2ee
runtime test: remove its temp directory even when nm is missing
jgarzik Oct 10, 2026
fa5273b
rt: fix long numbers, -$ in PRINT USING, closed random files
jgarzik Oct 10, 2026
54ca2c3
rt: format numbers without per-digit calls or copies; print whole num…
jgarzik Oct 10, 2026
a3b9b10
Merge faster number printing
jgarzik Oct 10, 2026
ac81dd9
rt: keep 767 digits of a long number, so UCRT sees the sticky digit
jgarzik Oct 10, 2026
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
62 changes: 61 additions & 1 deletion .github/workflows/TestingCI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,67 @@ jobs:
- name: Run clippy
run: cargo clippy --release --all-targets -- -D warnings
# The examples are the only programs compiled and run end to end through
# the real driver -- shelling out to the assembler and linker -- rather
# the real driver -- shelling out to the linker -- rather
# than through the test harness.
- name: Compile and run the examples
shell: bash
run: |
set -euo pipefail
for f in examples/*.bas; do
echo "== $f"
./target/release/xbasic64 "$f" -o /tmp/example
/tmp/example </dev/null >/dev/null
done

linux-aarch64:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Fetch
run: cargo fetch
- name: Build
run: cargo build --release --verbose
- name: Run tests
run: cargo test --release --verbose
- name: Run fmt check
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --release --all-targets -- -D warnings
# The examples are the only programs compiled and run end to end through
# the real driver -- shelling out to the linker -- rather
# than through the test harness.
- name: Compile and run the examples
shell: bash
run: |
set -euo pipefail
for f in examples/*.bas; do
echo "== $f"
./target/release/xbasic64 "$f" -o /tmp/example
/tmp/example </dev/null >/dev/null
done

macos-arm64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Fetch
run: cargo fetch
- name: Build
run: cargo build --release --verbose
- name: Run tests
run: cargo test --release --verbose
- name: Run fmt check
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --release --all-targets -- -D warnings
# The examples are the only programs compiled and run end to end through
# the real driver -- shelling out to the linker -- rather
# than through the test harness.
- name: Compile and run the examples
shell: bash
Expand Down
81 changes: 45 additions & 36 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ cargo run -- -S program.bas # Cranelift IR to <output>.clif instead (no o

## Architecture

xbasic64 is a BASIC-to-x86_64 native code compiler. The AST is lowered straight
xbasic64 is a BASIC native code compiler for x86-64 (Linux, Windows) and
aarch64 (Linux, macOS). The AST is lowered straight
to Cranelift IR, and Cranelift writes the object file:

```
Expand All @@ -48,23 +49,38 @@ Source → Lexer → Parser → Sema → CodeGen → Object → Executable
(array descriptors read once per loop), `strings.rs`
(string ownership), `expr.rs`, `builtins.rs`, `stmt.rs`, `calls.rs`
(procedures and by-reference calls), `io.rs`, `trap.rs` (ON ERROR)
- **rtbuild.rs** - The hand-written x86-64 runtime library's trees
(`runtime/sysv`, `runtime/win64-native`: I/O, strings, math, using libc) and
how they become one assembly unit. Shared by `build.rs`, which assembles the
target's tree into `$OUT_DIR/runtime.o`, and by the crate's tests
- **runtime.rs** - The prebuilt runtime object, embedded; `main.rs` writes it
beside the program's object and links the two. Generated code reaches the
runtime's state and messages, and the runtime the program's `_data_*`, only
through `.globl` symbols
- **runtime.rs** - The prebuilt runtime object (from `rt/`), embedded;
`main.rs` writes it beside the program's object and links the two.
Generated code reaches the runtime's state and messages, and the runtime
the program's `_data_*`, only through exported symbols
- **rtsig.rs** - Signature of every runtime helper generated code calls,
whether it can raise an error, and whether its string result is fresh.
Every call goes through `FnGen::call`, which builds the call from it; tests
derive `fallible` from the assembly's control flow and check that every
`fresh` helper reaches `malloc`
read a call graph from `rt/src` and check that `fallible` is exactly "can
reach `raise_with_code`" and `fresh` exactly "can reach `alloc_str`"
- **main.rs** - CLI driver: reads source, runs the pipeline, writes the object
and the runtime's, and links them with `cc` (or `link.exe`)
- **build.rs** - Assembles the runtime when the compiler is built, so building
needs the target's assembler too
- **build.rs** - Builds the runtime (`rt/`) with rustc for the target when the
compiler is built: a `no_std` staticlib with fat LTO, of which only the
crate's one merged object is kept (`$OUT_DIR/rt.o`; the rest of the
archive is `compiler_builtins`). It reads the `ar` format itself

### The runtime (`rt/`)

A `no_std` Rust crate, one source for every target, calling the C library
through `rt/src/sys.rs`, where everything OS-specific lives (the standard
streams, errno, 64-bit seeks, locking, the clock). One module per area:
`error` (messages, trapping state, `_rt_error`), `num` (number formatting and
reading), `string`, `data`, `file` (files and the console as file 0), `input`,
`using`, `math`, `print`. Helpers are defined with `export!` under the names
and signatures `rtsig.rs` lists. Its unit tests run with std (`cargo test`
covers the workspace). In the runtime:

- **No `core::fmt`** outside tests: it adds tens of KB to every program.
Numbers are formatted with the C library's `snprintf`, as the asm runtime
did, so output is unchanged
- **Every string result that is fresh comes from `alloc_str`**, which raises
Out of memory; so every fresh helper is also fallible

### Test Structure (`tests/`)

Expand All @@ -82,8 +98,6 @@ Integration tests organized by feature area:
- `codegen/` - Assertions on the Cranelift IR the compiler generates (through
`common::compile_to_clif()`), and behavioural tests of code paths
with special cases: constant divisors, scalars in SSA values, huge functions
- `runtime/` - Structural checks on the two assembly runtimes: the same
`.globl` names, no `.equ` length constants, and the like
- Feature modules: `arithmetic/`, `arrays/`, `control/`, `data/`, `errors/`, `file_io/`, `input/`, `math/`, `print/`, `procedures/`, `strings/`, `types/`, `variables/`

### Key Design Decisions
Expand Down Expand Up @@ -111,8 +125,14 @@ Integration tests organized by feature area:
the backtracking one is superlinear with SSA values live across it; that
code runs about 2x slower
- **Every reference is colocated**: functions and data, the runtime's and libc's
included, are reached with `call rel32` and RIP-relative addresses; on Linux
the link is `-no-pie`
included, are reached PC-relative (`call rel32` and RIP-relative on x86-64,
`bl` and adrp+add on aarch64); on Linux the link is `-no-pie`, and macOS,
where executables are always PIE, needs nothing more
- **Native only, two ISAs**: Cranelift's `x86` and `arm64` backends are built
in, and the compiler targets the host it was built for. aarch64 Linux can be
checked on an x86-64 host under qemu-user: cross-build with
`aarch64-linux-gnu-gcc` as the linker, and put a `cc` that is that compiler
first in PATH for the compiler's own link step
- **GW-BASIC semantics**: Division (`/`) always returns Double; integer division uses `\`
- **Default type is Double**: Unsuffixed numeric variables are `#` (Double), not Single
- **Boolean -1/0**: Comparisons return -1 (true) or 0 (false) for bitwise compatibility
Expand All @@ -126,20 +146,6 @@ Integration tests organized by feature area:
reached through `Loc`; a scalar in an SSA value is a `Place::Var`
- **Runtime checks on by default**: subscripts, division by zero, overflow
and the rest; `--unsafe` removes them, and is refused with ON ERROR
- **Two runtimes in lockstep**: `src/runtime/sysv/` and `src/runtime/win64-native/` export
the same `.globl` names; a new helper must be added to both
- **Two assemblers, not just two runtimes**: `build.rs` assembles the runtime
with GNU `as` on Linux and `clang -c` on Windows, so the Win64 tree is the one built by the
assembler no test here runs. They disagree about `.equ len, . - label`: GNU
`as` folds it to an immediate, and an assembler that cannot prove the symbol
absolute assembles `mov reg, len` as a *load from that address* instead.
`CLS` did that on every Windows run. Bracket data with an `_end` label and
subtract at run time
- **A runtime helper should take at most four arguments**: Win64 passes only
four in registers, so a fifth is on the stack, where the hand-written Win64
helper must find it above the shadow space. Cranelift places it correctly,
but `_rt_instr` and `_rt_mid_assign` show what the runtime side costs; fold an
argument away where possible (see `_rt_file_open_random`)
- **Unsupported GW-BASIC names are refused, not ignored**: an unrecognised name
is otherwise just a new variable, so `UNSUPPORTED` in `sema.rs` names the
keywords this compiler does not provide and why
Expand All @@ -149,7 +155,7 @@ Integration tests organized by feature area:
- **Strings are owned and freed** (`codegen/strings.rs`): bit 63 of a string
slot's length word marks bytes the slot owns, which assignment frees; a
FIELD window owns nothing. A helper's result is a temporary to free only if
`rtsig` marks it `fresh` -- which a test checks reaches `malloc` -- so a new
`rtsig` marks it `fresh` -- which a test checks reaches `alloc_str` -- so a new
helper that allocates must say so, and one that returns a slice or a static
buffer must not. The runtime allocates every string with `malloc`, never
`HeapAlloc`, since generated code frees with `free`
Expand All @@ -170,10 +176,13 @@ Integration tests organized by feature area:
on `_err_resume_label`. By-reference parameters are real
references, so the handler sees what a procedure wrote. Only programs with
ON ERROR pay for any of this
- **Every helper follows its platform's standard ABI**: a double goes where the
ABI puts it -- by position on Win64, so `(file, value)` passes the value in
xmm1 there. `rtsig.rs` lists one signature that serves both ABIs, and codegen
builds each call's Cranelift signature from it
- **Runtime helpers use System V on x86-64, on every OS**: a string result
is two words returned in rax:rdx, as Cranelift returns two values, where
the Windows C convention would return it through a hidden pointer. So
`export!` defines each helper `extern "sysv64"` on x86-64 and codegen calls
it with `CallConv::SystemV` (`RT_CALL_CONV`); elsewhere both use the
platform's C convention, which returns two words in two registers.
`rtsig.rs` lists one signature, and codegen builds each call from it

## Language Reference

Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@ name = "xbasic64"
version = "1.0.2"
edition = "2024"
authors = ["Jeff Garzik <jgarzik@pobox.com>"]
description = "A BASIC-to-x86_64 native code compiler targeting 1980s-era BASIC dialects"
description = "A BASIC native code compiler (x86-64, aarch64) targeting 1980s-era BASIC dialects"
license = "MIT"
repository = "https://github.com/jgarzik/xbasic64"
homepage = "https://github.com/jgarzik/xbasic64"
documentation = "https://docs.rs/xbasic64"
readme = "README.md"
keywords = ["basic", "compiler", "x86-64", "retro", "programming-language"]
keywords = ["basic", "compiler", "x86-64", "aarch64", "retro"]
categories = ["compilers", "command-line-utilities", "development-tools"]
rust-version = "1.96"

# The runtime library is a member, so that clippy and its unit tests cover
# it; build.rs builds the copy programs link against.
[workspace]
members = ["rt"]
default-members = [".", "rt"]

[features]
default = []

[dependencies]
clap = { version = "4", features = ["derive"] }
# Pinned exactly: Cranelift's API changes between minor releases.
cranelift-codegen = { version = "=0.136.2", default-features = false, features = ["std", "x86", "unwind"] }
cranelift-codegen = { version = "=0.136.2", default-features = false, features = ["std", "x86", "arm64", "unwind"] }
cranelift-frontend = "=0.136.2"
cranelift-module = "=0.136.2"
cranelift-object = "=0.136.2"
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# xbasic64

A BASIC-to-x86_64 native code compiler.
A BASIC native code compiler for x86-64 and aarch64.

## Overview

xbasic64 compiles 1980s-era BASIC dialects (Tandy Color BASIC, GW-BASIC, QuickBASIC) directly to native x86-64 executables. No interpreter, no bytecode—just fast native binaries.
xbasic64 compiles 1980s-era BASIC dialects (Tandy Color BASIC, GW-BASIC, QuickBASIC) directly to native x86-64 and aarch64 executables. No interpreter, no bytecode—just fast native binaries.

**Why xbasic64?**

Expand Down Expand Up @@ -94,25 +94,25 @@ Source → Lexer → Parser → Semantic Analysis → Code Generator → Object
4. **Code Generator** - Lowers the AST to Cranelift IR; Cranelift does instruction
selection and register allocation and writes the object file

The runtime library provides I/O, string operations, and math functions as hand-written x86-64 assembly using libc for portability. It is assembled once, when the compiler is built, and linked beside each program.
The runtime library (I/O, strings, number formatting, math) is a `no_std` Rust crate in `rt/` that calls the C library. It is built once, when the compiler is built, and linked beside each program.

Key design choices:
- No IR of its own: the AST is lowered directly to Cranelift's
- Each platform's standard ABI, for libc interoperability
- A portable `no_std` Rust runtime over the C library
- GW-BASIC type semantics (division always returns Double)

## Requirements

- Rust 1.96 or later
- Linux: the system assembler (`as`) to build the compiler, which assembles
its runtime library; the C compiler/linker (`cc`) with libc to link programs
- Windows: Clang to build the compiler (it assembles the runtime), and the MSVC
linker (`link.exe`) to link programs
- Linux and macOS: the C compiler/linker (`cc`) with the C library, to link
programs
- Windows: the MSVC linker (`link.exe`) and its C runtime, to link programs

## Platforms

- Linux (x86-64)
- Linux (x86-64, aarch64)
- Windows (x86-64)
- macOS (arm64)

## License

Expand Down
Loading
Loading