Skip to content

CI workflow, native-recompile back end (--emit-c), runtime library + SDL2#2

Open
leag wants to merge 18 commits into
mainfrom
claude/claude-md-docs-mr8ssz
Open

CI workflow, native-recompile back end (--emit-c), runtime library + SDL2#2
leag wants to merge 18 commits into
mainfrom
claude/claude-md-docs-mr8ssz

Conversation

@leag

@leag leag commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Brings main up to date with everything since PR #1:

  • GitHub Actions CI (ruff, ty, pytest on Python 3.11–3.13) plus the README badge — merging this registers the workflow and makes the badge resolve (it 404s today because ci.yml has never been on the default branch).
  • Native-recompile back end (tbx --emit-c): lowers the decoded IR to a self-contained portable C translation unit (gcc/clang, POSIX or MinGW-w64). Grown over several commits to cover error trapping, SUB/CALL by-ref, file I/O, terminal control, graphics framebuffer, ON TIMER, PRINT USING.
  • Full corpus coverage: machine.c emulates the real-mode machine (PEEK/POKE/DEF SEG/BLOAD/BSAVE over a 1 MiB image, OUT/INP port latches, REG buffer, CHAIN via exec, CALL ABSOLUTE aborts at runtime) — all 564/564 corpus programs transpile.
  • Runtime as a library: make in tbx/c0_runtime/ builds libtbrt.a; --emit-c --no-runtime emits programs that link against it.
  • Optional SDL2 video backend (--emit-c --sdl / make SDL=1): real window for the graphics framebuffer, window keyboard feeding INKEY$/INSTAT.
  • Golden-gating sweep test, packaging polish, CLAUDE.md updates.

Tests: 1295 pass locally; ruff and ty clean. Windows runtime paths verified under Wine with llvm-mingw (18/18 checks).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CqBnSnCq2HwNA9uyzrUnEC

claude and others added 18 commits July 12, 2026 13:59
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJpGqftHVJEy28DqWTgVcr
- tests/tbx/test_goldens.py: sweep every corpus EXE against its committed
  ops and usercode golden (reusing dump_ops.canon so tool and test share
  one canonical format), plus coverage/orphan checks on the golden dirs.
- dump_user_code.py: skip compiler-flag fixtures (non-empty toggles) --
  they carry no golden by convention, but the tool used to write them,
  polluting the tree and silently widening the IR-snapshot sweep.
- CLI: add --version (from package metadata), with a test.
- Packaging: py.typed marker, [project.urls] in pyproject, CI badge in
  README; CLAUDE.md testing section updated to match the new gating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJpGqftHVJEy28DqWTgVcr
tbx/c0.py lowers the decoder's typed IR to a self-contained C translation
unit for modern platforms, with an embedded runtime following Turbo Basic
handbook semantics: GW-BASIC PRINT layout (sign-or-space + trailing space,
.5 without leading zero, 7 significant digits), CINT banker's rounding,
16-bit \ MOD AND OR XOR NOT, single-precision default variables, suffix
types (% & ! # $), static and runtime-bound DIM, DATA/READ/RESTORE,
GOSUB via labels-as-values (gcc/clang), flat FOR/WHILE/DO lowered to
test/jump label pairs so EXIT FOR/LOOP and cross-statement GOTO work.

Fail-loud like the decoder: constructs outside the implemented vocabulary
(file I/O, graphics, sound, traps, SUB/CALL, PRINT USING) raise instead of
mistranslating. 272/564 corpus fixtures transpile; all 272 compile clean
with gcc and run without crashes (the one timeout is a fixture whose
original program is an intentional infinite loop).

tests/tbx/test_c0.py pins recompiled stdout for seven fixtures end-to-end
(skipped without a host cc), a transpile-coverage floor, and the
fail-loud contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJpGqftHVJEy28DqWTgVcr
…trol

Coverage 272 -> 348 of 564 corpus fixtures; all 348 compile with gcc and
run crash-free (the sole timeout is the intentionally infinite zz_ginf).

- ON ERROR GOTO / ERR / ERL / RESUME [NEXT|line] / ERROR n via setjmp +
  computed goto; tb_erl is tracked always, so untrapped runtime errors
  abort with TB's error code and the emitted source's line number.
  Division (including \ and MOD) now raises error 11 on zero divisors.
- SUB/CALL: void C functions with pointer parameters -- variables and
  array elements pass by reference (zz_sub1 pins the semantics), other
  expressions by value copy; EXIT SUB returns. Non-parameter variables,
  arrays, and label-family statements inside SUB bodies stay fail-loud.
- Sequential file I/O: OPEN I/O/A, CLOSE, RESET, PRINT #n, WRITE [#n],
  INPUT #n, EOF; TB error codes for bad/unopened/missing files. PRINT
  columns are now per-channel, fixing a TAB(n)-into-file infinite loop.
- DIM bounds generalized to (lo:hi) pairs with OPTION BASE lower bounds
  (fixes the 'expression tuple' failure on t1_mix3); UBOUND/LBOUND.
- LOCATE/COLOR as ANSI escapes, KEY ON/OFF as a no-op, SHELL/CHDIR/
  ENVIRON/KILL/NAME via libc, BIN$, EOF, LPOS, long (&) variables.

tests: 16 pinned end-to-end runs including by-reference CALL, TAB layout
on console and into a file, and untrapped-error exit codes; coverage
floor raised to 330.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJpGqftHVJEy28DqWTgVcr
Coverage 348 -> 534 of 564 (95%); all 534 compile with gcc and run
crash-free. The 30 remaining fixtures all use machine access (PEEK/POKE/
OUT/WAIT/INP/REG/CALL ABSOLUTE/BLOAD/BSAVE/DEF SEG/CHAIN) and stay
fail-loud. The three run-forever cases (zz_ginf, t1_run x2) are faithful:
their originals are intentional infinite loops.

- Graphics: SCREEN modes 1/2/7-10 allocate an in-memory byte-per-pixel
  framebuffer (no display on a headless host; TB_SCREEN_PPM=file dumps
  the final image in the CGA palette). PSET/PRESET, LINE with box/fill/
  16-bit style patterns (Bresenham), CIRCLE with arcs and aspect, PAINT
  flood fill, GET/PUT blits (XOR/PSET/PRESET/AND/OR) clipped to the
  target array's capacity, VIEW/WINDOW coordinate transforms, DRAW's
  U D L R E F G H M B N C S command subset, POINT, PMAP, PALETTE.
- ON TIMER(n) GOSUB via monotonic-clock polling at statement boundaries
  (TB's own trap granularity); KEY/COM/PEN/PLAY handlers install as
  no-ops since those event sources don't exist on a modern host.
- Random-access files: OPEN "R" with the calibrated 128-byte records,
  FIELD registrations, LSET/RSET (field-aware and plain string
  justification), GET/PUT record I/O, SEEK; MKI$/MKL$/MKS$/MKD$ and
  CVI/CVL/CVS/CVD little-endian converters (embedded-NUL caveat noted).
- PRINT USING: the # . + numeric-field subset with GW field cycling and
  %-overflow; other format characters raise error 5.
- Multi-line DEF FN with EXIT DEF; comparisons in value position
  materialize TB's -1/0; CEIL/CLNG/RND/INSTAT/MTIMER/CSRLIN/INKEY$/
  DATE$/TIME$ set (process-local clock offset), PMAP, UBOUND/LBOUND,
  CLEAR/RUN via a generated whole-program reset, FILES, MKDIR/RMDIR,
  LPRINT-to-console, Palette-aware PPM dump.

tests: 21 pinned end-to-end runs (PRINT USING format cycling checked
against a file leg, multi-line DEF FN, CVI, POINT); floor raised to 510.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJpGqftHVJEy28DqWTgVcr
- The runtime prelude now lives as ordered fragment files (header, core,
  fileio, terminal, graphics, play, events) concatenated verbatim by an
  explicit manifest in c0.py; POSIX output is byte-identical to before.
- Add #ifdef _WIN32 paths (conio keyboard, VT console mode, Sleep,
  QueryPerformanceCounter, a DOS-wildcard fnmatch surrogate) so the
  output builds with MinGW-w64 gcc/clang: all 534 supported corpus
  programs cross-compile, and the test_c0-pinned programs run correctly
  under wine.
- Fix two portability bugs: mkdir's two-argument POSIX form (now a
  tb_mkdir wrapper), and clang rejecting RETURN's goto* when a dead
  ON COM/KEY/PEN/PLAY handler leaves main with no &&label expression.
- Refresh the stale c0.py unsupported list, the README --emit-c section
  (file-device surrogates, Windows note), and CLAUDE.md; add .clangd to
  silence diagnostics on the non-standalone fragments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each c0_runtime/*.c now includes tb_runtime.h (system includes plus the
declarations of every cross-fragment / generated-code-facing symbol, now
non-static; fragment-internal helpers stay static) and compiles on its
own with gcc, clang, and MinGW-w64 at -Wall -Wextra clean, so the runtime
also builds as an ordinary C library. header.c is absorbed into the
header, and emit amalgamates header + fragments -- stripping the include
lines -- into the same self-contained generated file as before. The
repo-root .clangd suppression is no longer needed and is removed.

Verified: 1283 tests, ruff, ty; all 534 supported corpus programs still
cross-compile for Windows and the test_c0-pinned programs run correctly
under wine; the six units link as a library on Linux and Windows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…L2 backend

- machine.c: PEEK/POKE/DEF SEG/BLOAD/BSAVE against an emulated 1 MiB
  real-mode memory, OUT/INP as 64K port latches, WAIT returns at once,
  REG buffer with no-op CALL INTERRUPT, CHAIN execs the named file
  (error 53 if absent), CALL ABSOLUTE transpiles but aborts if reached.
  All 564/564 corpus programs now transpile (was 534).
- --no-runtime + c0_runtime/Makefile: emit #include "tb_runtime.h"
  instead of embedding the fragments and link many programs against one
  libtbrt.a.
- --sdl / -DTB_SDL: sdl.c presents the framebuffer in a real 4:3 SDL2
  window via a tb_present() hook on every drawing op (no-op stubs in
  graphics.c otherwise); window keys feed INKEY$/INSTAT; held at exit
  until a key (TB_SDL_HOLD=0 skips).
- tests: pin the machine round trips, BSAVE/BLOAD, CALL ABSOLUTE abort,
  CHAIN exec + error paths, the library link, and a headless SDL run
  (dummy driver); fail-loud witness is now a synthetic intrinsic.
  Windows paths verified under Wine with llvm-mingw (18/18 checks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqBnSnCq2HwNA9uyzrUnEC
… stdin

Phase 0+1 of the graduation plan:
- tbx/tools/oracle.py: bridge to the external headless Turbo Basic
  toolchain (TBX_ORACLE): compile_bas() with the real TB 1.1/1.0
  compiler, run_exe() with scripted keys + screen/file capture.
- tbx/tools/verify_fixture.py: automated byte-exact round trip
  (decompile -> emit -> oracle compile -> cmp). t1_print, t1_tab,
  zz_sc1, v10_t1_print all verify byte-identical.
- tbx/tools/dump_dos_output.py: capture what the ORIGINAL corpus EXE
  visibly does on the emulated machine into tests/fixtures/dosout/
  (screen text + files it wrote), with the key-script table shared
  with the test layer.
- test_c0.py test_dos_golden: recompiled native binaries must match the
  DOS goldens (lines rstripped, "Error N at pgm-ctr"/"in line" both
  normalize to "Error N", produced files CRLF-normalized; waivers name
  their surrogate -- t1_bsave's image carries live DGROUP memory).
- core.c: INPUT echoes the read line when stdin is not a tty, matching
  TB's own screen echo; t1_tab/t1_pr2 pinned outputs updated.

First 19 goldens captured and passing; the full-corpus sweep fills in
the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqBnSnCq2HwNA9uyzrUnEC
148 new DOS behavior goldens (tests/fixtures/dosout/) and the fixes they
witness:

- PEN(n) while PEN OFF raises error 5 (t1_penf); tb_pen_on tracks the
  event toggle
- CHDIR/RMDIR of a missing path raise 76/75 (t1_chdir, t1_rmdir), with
  DOS '\' separators translated via a shared tb_dospath
- KILL/NAME of a missing file raise 53 (t1_kill, t1_name)
- SEEK on a random-mode channel raises 54 (t1_seek)
- EOF() on a closed channel raises 52 (t1_filef)
- graphics statements without a SCREEN mode raise 5 (t1_circle, t1_draw)
  instead of implicitly entering mode 1; the SDL headless test now uses a
  synthetic SCREEN 1 program since no corpus fixture sets a mode
- LPRINT goes to a printer sink (TB_LPRINT_TXT or the null device), never
  the console (t1_lprint), with its own TAB/SPC column as channel 15
- KEY LIST prints the twelve empty soft keys (t1_keylist)
- INP of a never-latched port reads the floating bus, 255 (t1_inpf)
- PRINT expands non-integral numbers to 16 significant digits with a
  three-digit exponent (t1_fp); TB's own binary->decimal conversion
  carries ~1e-14 tail noise, so the golden comparison clips both sides
  to 13 significant digits
- waivers: t1_shellvar (SHELL '' starts the resident shell), t1_putfile
  (PUT of a never-FIELDed record exposes live DGROUP memory), t1_files
  (host directory listing is environmental)

docs/c0-graduation-plan.md records the phased plan for taking c0 out of
the experimental phase; CLAUDE.md documents the oracle workflow and the
dosout regression layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 item 4 of the graduation plan: Program.toggles is honored as
compiled, like TB itself. With Bounds ON every subscript range-checks
through tb_bix (TB error 9); with Overflow ON integer stores range-check
through tb_ichk/tb_lchk (error 6) instead of wrapping. Both default OFF,
and the flagged f*_ corpus fixtures now emit the checks while their
unflagged twins do not (test_options_toggles; the error branches are
pinned by synthetic programs since no corpus fixture trips them).

Triage of the still-running capture sweep's new goldens:
- WRITE numbers drop the leading sign-space and keep the trailing space
  on the console only ("1 ,2" t1_write; compact '1,"A"' t1_writefile)
- zz_cv_mkd/mkl/mks waived: MK*$ images embed NULs that the runtime's
  NUL-terminated strings truncate -- the known binary-strings divergence
  the phase-3 descriptor refactor resolves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep stragglers; all pass test_dos_golden unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Triage of the sweep's newest goldens:

- zz_x_ldiv/zz_x_lmod witness that \ and MOD convert operands to 16-bit
  with x87 FIST-store semantics: out-of-range values become the integer
  indefinite 0x8000, so 100000 \ 7 = -4681 and 100000 MOD 7 = -1
  (exactly -32768 \ 7). tb_idiv/tb_mod now convert through tb_fist16.

- zz_sc3 exposes a real TB 1.1 codegen bug: CASE IS compiles the bound
  compare against DS:0120 (a hidden, never-written slot) instead of the
  selector temp, then matches the arm iff the SELECTOR equals the
  materialized boolean -- so CASE IS > 10 with A=20 falls to CASE ELSE.
  Waived: c0 keeps the handbook semantics until phase-2 probes pin the
  hidden slot's behavior across layouts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full sweep captured 297/321 eligible stems (92.5%, above the >=90%
target). Every absence now has a SKIP entry in dump_dos_output with its
verified structural reason, so --missing reports only documented skips:
TRON EXEs stall in their trace hook on the real machine (witnessed on
t1_tron: prints [20], never returns to DOS), RUN and zz_ginf never exit,
SCREEN 1 loses the text screen, CALL ABSOLUTE has no code to run, bare
LINE INPUT gives the key script nothing to synchronize on, and
t1_scr/t1_shell complete on screen but the harness cannot confirm the
prompt returned. Plan doc updated with the checkpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Graduation plan phase 4, items 1 and 3:

- TB_RT_VERSION in tb_runtime.h and c0_runtime/README.md spelling out the
  surrogate contract (emulated machine, console, absent devices, graphics,
  file devices), the known divergences still open (NUL-terminated strings,
  RND, double vs x87, the CASE IS bug), and the toggle behavior. Changing
  a surrogate is now a reviewable event like golden regeneration.

- CI gains a c0 job matrix: Linux gcc + clang with SDL2 (the headless SDL
  test runs instead of skipping), macOS clang, and an experimental
  Windows MinGW lane (continue-on-error until proven). test_c0 honors
  $CC so the lanes actually select their compiler; clang verified green
  locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ive glob

The produced-file goldens carry DOS 8.3 names (t1_pr2.file.R.TXT), which
*.txt matches case-insensitively on Windows, so _dos_stems() invented
stems like t1_pr2.file.R and the job failed looking for their corpus EXEs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants