Repository navigation
Conversation
The runtime is to move from the two hand-written x86-64 assembly trees to one no_std Rust crate, rt/, so that it is one source for every OS and can follow the compiler to aarch64. This is its first step; nothing links it yet, and every program still runs on the assembly. - rt/ is a workspace member (and a default member, so clippy and cargo test cover it). Its unit tests run with std. - build.rs builds it with rustc for the target: a staticlib with fat LTO, emitted as the one object that makes (/rt.o, 14 KB, needing only libc). The staticlib archive itself would carry 4 MB of compiler_builtins the runtime never references. - export! defines each helper under its symbol name with the System V convention on x86-64 -- on Windows too, so that a string result comes back in rax:rdx as Cranelift's two-value return does -- and the C convention elsewhere. - sys holds the libc declarations and what differs by OS: the standard streams (glibc, macOS, UCRT) and the line ending. - error is error.s in Rust: the messages and their GW-BASIC numbers, the four trapping words generated code reads by symbol, _rt_error, _rt_error_num, _rt_fatal, _rt_err, _rt_erl, _rt_stop and _rt_end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One module per assembly file. num and string declare what the others call -- scan_number, fmt_double, fmt_basic and strdup -- so that they can be ported side by side; their bodies are still todo!(). Nothing links rt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CATE, COLOR, POS The xorshift64 generator and its splitmix64 seeding, bit for bit (unit tests hold the sequence the asm runtime prints); TIMER from one clock reading (gettimeofday, or _ftime64 on Windows) in local time; DATE$ and TIME$ through strftime, as fresh strings; the console escapes formatted with snprintf and written to the C library's stdout on every OS. sys.rs gains a marked block for the clock and struct tm. file.rs gets a marked stub of console_column / set_console_column, which the file module's own replaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
num.rs: fmt_double/fmt_basic (print.s), scan_number and _rt_val. string.rs: every helper in string.s, slices staying slices and every fresh result from alloc_str, so a failed allocation now raises Out of memory. data.rs: READ, RESTORE and array autodim, with the program's DATA symbols replaced by a table of the tests' own under cfg(test). The code has no path that can panic: rt.o is emitted before LTO merges in core, so a failing bounds check would leave core's panic functions as undefined symbols. sys.rs gains strtod. Expected text in the tests is what the asm runtime printed. Errors that are not trapped are tested in a child process, which keeps the trapping state out of the tests running in parallel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a foundational, platform-specific no_std runtime with new FFI and a new per-build rustc step whose cross-platform (Linux/macOS/Windows) correctness cannot be fully verified here, warranting human review.
1 open finding
What changed in this PR
This PR begins porting xbasic64's hand-written assembly runtime to a portable no_std Rust crate (rt/). The new crate is added as a workspace member (so clippy and unit tests cover it) and is compiled by build.rs with rustc into $OUT_DIR/rt.o for the target, but is deliberately not yet linked into compiled programs — the assembly runtime (runtime::OBJECT) remains in use until the port is complete and switched over in one step. The first ported areas are errors (error.rs) and the math/console group RND/RANDOMIZE/TIMER/DATE$/TIME$/BEEP/CLS/LOCATE/COLOR/POS (math.rs), backed by a C-library/platform abstraction (sys.rs) and core primitives (lib.rs); the remaining modules are stubs with todo!().
Changes:
- Add the
rt/no_stdruntime crate withStr/Word/State/alloc_strprimitives, theexport!ABI macro (sysv64 on x86_64), and portederror/math/sysmodules plus unit tests; other modules are placeholders. - Wire
rt/into the Cargo workspace (Cargo.toml,Cargo.lock,rt/Cargo.toml). - Extend
build.rsto buildrt/as a fat-LTO staticlib emitted as a single object (rt.o), not yet linked.
| File | Description |
|---|---|
| Cargo.toml | Declares the workspace with rt as a member and default member. |
| Cargo.lock | Records the new xbasic64-rt package entry. |
| build.rs | Adds build_rt to compile rt/ via rustc into $OUT_DIR/rt.o; watches rt/src. |
| rt/Cargo.toml | New runtime crate manifest (lib-only, no doctests, publish = false). |
| rt/src/lib.rs | Crate root: export! macro, Str/Word/State, cstr, alloc_str, panic handler. |
| rt/src/sys.rs | C-library FFI and per-OS stream/clock/Tm abstractions. |
| rt/src/error.rs | Error messages, GW-BASIC error codes, raise/trap/fatal, ERR/ERL/ERROR/STOP/END. |
| rt/src/math.rs | RND/RANDOMIZE/TIMER/DATE$/TIME$/BEEP/CLS/LOCATE/COLOR/POS with ANSI escapes and tests. |
| rt/src/num.rs | Number formatting/scanning API with todo!() stubs. |
| rt/src/string.rs | strdup stub. |
| rt/src/file.rs | Temporary console-column stub consumed by math.rs. |
| rt/src/data.rs, input.rs, print.rs, using.rs | Placeholder module headers. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--emit obj writes the crate's object without the parts of core fat LTO would merge into it, so any bounds check left core's panic functions as undefined symbols. rustc now builds the staticlib, whose first member is that merged object, and build.rs keeps only it -- read straight from the ar archive (GNU, BSD and COFF long names), so no ar tool is needed -- and drops the rest, compiler_builtins. rt.o: 12.7 KB of text; it needs only the C library and the program's _data_* symbols. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.s, input.s and using.s become file.rs, input.rs and using.rs, and print.s's _rt_platform_init becomes print.rs's, one source for every target. Every file, and the console as file 0, is a C stdio stream in binary mode; sys.rs gains the OS differences in one marked block: stdin, errno, 64-bit seek, record locking, the directory check (the fstat system call on Linux, so old glibc still links), and why an OPEN failed. Nothing links it yet; unit tests cover the field readers, the INPUT line parser, the USING format parser and renderer, and file round trips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Windows Two CI failures in the runtime crate's own build: - Linux: newer Rust's suspicious_runtime_symbol_definitions lint wants malloc, calloc, realloc and free declared with *mut c_void, the signatures std itself relies on. Callers cast. - Windows: UCRT's snprintf and fprintf are inline functions in its headers, so the unit-test binary did not link. The extern block now names legacy_stdio_definitions, which compiled programs already link. Also notes, in build.rs and rt/Cargo.toml, that the runtime's edition is stated in both and must change together (review feedback). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # rt/src/file.rs # rt/src/sys.rs
…LD windows _rt_field_next tested `off + w > reclen`. A width near 2^63 wrapped the sum negative, passed, and handed out a window of length ~2^63 at the buffer, with the next field's offset far below it: LSET through either wrote out of bounds. It now compares w against the room left. The assembly runtime had the same flaw. A FIELD variable is a non-owned window onto its number's record buffer, and outlives the OPEN that made it. Re-OPENing the number (sequential or RANDOM) freed the buffer, as the assembly runtime also did, so a later LSET wrote freed memory and PRINT read it -- glibc aborted the unit test with a corrupted tcache. Each number's buffer is now MAX_RECLEN bytes, allocated on its first OPEN FOR RANDOM and never freed or moved; later OPENs FOR RANDOM reuse it, and the number's mode alone says whether it is a random file. At most 15 such buffers exist. LOCK's range and GET/PUT's next record now wrap explicitly, as the assembly's imul and inc did, rather than relying on release overflow semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every program now links rt/ -- one no_std Rust source over the C library -- instead of the two hand-written x86-64 assembly trees, which are gone (src/runtime/, src/rtbuild.rs: about 11,400 lines). - build.rs builds only rt/; building the compiler no longer needs an assembler (GNU as, or clang on Windows). - Codegen calls runtime helpers with the System V convention on x86-64, Windows included (RT_CALL_CONV), as export! defines them: a string result comes back in rax:rdx, as Cranelift returns two values, where the Windows C convention would use a hidden pointer. Elsewhere both use the platform's C convention. - Every fresh helper is now fallible: alloc_str raises Out of memory, a trappable error, where the asm never checked. - The asm-structure tests (tests/runtime, the fall-through test, the assembles-both test) are gone. rtsig's tests now read a call graph from rt/src: fallible must be exactly "reaches raise_with_code" and fresh exactly "reaches alloc_str", for every helper. A unit test checks rt.o needs nothing but the C library and the program's _data_* symbols. Checked against the assembly runtime (78cfddc's build): 646 of 649 examples and corpus programs give identical stdout, stderr and exit codes; the 3 others use RND seeded by TIMER, as always. Valgrind: 649 programs, 0 errors. Deliberate differences: a string's NUL bytes are written (glibc fprintf %.*s stopped at one); Out of memory is raised; Windows stdout is now CRT-buffered (flushed before input, stderr and exit). Measured, ASM -> Rust runtime (best of 3; text+data+bss): strings, 3M iterations 0.35s -> 0.35s 4118+1650+9768 -> 7255+2312+112 PRINT numbers, 2M lines 0.71s -> 0.78s 3416+1626+9720 -> 5805+2224+56 file write+INPUT #, 500k 0.24s -> 0.20s 6350+1706+9752 -> 12624+2320+5216 PRINT "hi" executable 21680 -> 17696 bytes on disk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
End-to-end versions of the two record-buffer fixes, now that programs link the Rust runtime. A FIELD variable used after its number is closed and reopened wrote freed memory (valgrind: invalid writes) before d51c794; the width test pins FIELD overflow at the largest LONG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the runtime in portable Rust, the compiler itself was all that tied programs to x86-64. - Cranelift's arm64 backend is built in beside x86; the compiler targets its host, as before. has_sse41 is set only on x86-64 (an aarch64 ISA rejects it, so every compile failed). - macOS links with cc ... -Wl,-dead_strip: executables are always PIE there, which colocated references already are (adrp+add, bl through ld64's stubs); no -no-pie, no --gc-sections, no -lm. - CI gains ubuntu-24.04-arm and macos-latest jobs, the same steps as Linux. - The two ulimit -v memory tests run on Linux only; macOS ignores the limit. The runtime's nm test likewise, as a module. - Docs: README platforms, CLAUDE.md (targets, how to check aarch64 under qemu), the CLI's description; DEF SEG and VARSEG no longer say x86-64. Checked on aarch64 Linux under qemu-user (aarch64-linux-gnu-gcc to link): cargo test passes in full (126 + 589 + 57), and the aarch64 compiler's programs match the x86-64 compiler's on 646 of 649 examples and corpus programs, the 3 others being RND/TIMER as always. macOS is checked by clippy only here, and by its CI job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file and PRINT USING unit tests made /tmp/xbrt-file-<pid> and /tmp/xbrt-using-<pid> and never removed them, one more per test run. They now use tempfile directories, dropped at the end of each test; a test run in a child process, which may exit on a fatal error without unwinding, uses a directory its parent made and removes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows CI died inside the runtime's unit-test binary: a fatal error
("?Permission denied in 30") exited the whole process, naming no test.
Running the tests under Wine (windows-gnu) showed the rest:
- A test that expects a fatal error compared the child's stderr with LF;
Windows writes CRLF, and the child's stderr, not yet in binary mode as a
program's is, made it CR CR LF. expect_fatal now normalizes line ends.
- sequential_round_trip expected an LF from PRINT #'s newline, and a LOF
of 51; both now follow sys::NEWLINE.
- Under cfg(test), a fatal error outside a child process now panics with
its message, so the harness names the test. Whatever raised Permission
denied on Windows will say where on the next run.
- The error test clears _err_line, which it left at 30 for every later
test's report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It returned early, before removing the directory, on a host without nm; a tempfile guard removes it on every path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scan_number kept at most 120 characters, so VAL of a 130-digit number lost its magnitude. Significant digits now go to strtod as an integer and an exponent: leading zeros take no room, digits past 800 only move the point (with one sticky digit, so rounding is still exact), and the exponent is parsed and saturated here. consumed is unchanged; ordinary numbers round exactly as before. - PRINT USING put a negative number's sign after the $ of a $$ or **$ field ($-12.50); it goes before, as GW-BASIC writes -$12.50. - A RANDOM file that was CLOSEd kept its mode, so FIELD carved the old buffer and LOC reported the old record. random_buf now needs the stream open: FIELD on a closed number is Bad file number (52). - OPEN FOR RANDOM allocates the record buffer before opening the file, so Out of memory no longer leaves #n open and unusable. - x86-64 macOS links fstat$INODE64, whose struct stat is_directory reads. - strftime_now uses string::strdup; error.rs shares code_of. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bers without snprintf
Number printing had become 7-12% slower than the asm runtime: every
digit went through a non-inlined NumText::push, fmt_basic copied the
text again to prepend its blank, and print_number a third time to
append PRINT's.
- fmt_double writes through a local cursor, runs of digits and zeros
as one slice copy each (per-byte loops unrolled into kilobytes)
- NumText is buf[start..end]; fmt_double leaves the byte before the
text free, so fmt_basic only stores the blank, and print_number
appends its blank in place (with_trailing_blank)
- A whole number below 10^P (P = 7 SINGLE, 16 DOUBLE) prints exactly
its integer's digits, so it is converted directly, skipping
snprintf("%.*e"). Integer variables reach PRINT and STR$ as doubles,
so this is their path too. A test checks it against the snprintf
path near zero and both limits, plus explicit boundary strings
(-0, 1E15, 1E16, 9999999/1E7 SINGLE, .5, 16777216 SINGLE)
Best of 5, 2M iterations, output to /dev/null, output byte-identical:
master(asm) before after
PRINT I&/3 0.33 0.38 0.24
A$ = STR$(I&/3) 0.32 0.37 0.24
PRINT I&; I&*1.5#; I&/3 0.94 1.05 0.46
PRINT I& 0.30 0.30 0.03
PRINT X! (SINGLE) 0.25 0.25 0.22
PRINT (I&+.5#)/3 (never whole)
0.34 0.38 0.34
rt.o text 30139 -> 30098; a PRINT I& program's text 5569 -> 6028.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scan_number kept 800 significant digits and stood for any nonzero digit past them with one sticky 1, as the 801st. UCRT's strtod reads only 768 significant digits, so on Windows it never saw the sticky digit: a number a hair above a halfway point between two DOUBLEs (2^53+1 followed by a nonzero digit 1000 places on) rounded to even, down, and the rt test scan_reads_a_number_of_any_length failed there. 767 digits are still exact -- no DOUBLE, nor any halfway point, has more than 767 significant digits -- and the sticky digit is now the 768th. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

No description provided.