Port the standalone SDL frontend, and give the sink a line editor

An AKGL build of `basic` was a terminal program with unused SDL linked into
it. It now opens the reference's 800x600 window, draws BASIC output in the
Commodore font, pumps events, lets you type at it, and still mirrors every
byte to stdout.

The stdout mirror is a composing sink rather than a second write inside the
interpreter, and lives in the core library where it needs no SDL. The line
editor waits for a typed line by borrowing one frame at a time from the host,
so nothing blocks and nothing owns an event loop it should not.

The whole golden corpus now runs through the SDL binary as well as the stdio
one, byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:21:39 -04:00
parent 7897a49fb5
commit f24201fdef
14 changed files with 1816 additions and 106 deletions

View File

@@ -133,6 +133,7 @@ set(AKBASIC_SOURCES
src/runtime_input.c
src/scanner.c
src/sink_stdio.c
src/sink_tee.c
src/symtab.c
src/value.c
src/variable.c
@@ -170,6 +171,16 @@ if(AKBASIC_WITH_AKGL)
target_compile_options(akbasic_akgl PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_akgl PUBLIC akbasic akgl)
akbasic_instrument(akbasic_akgl)
# The SDL host, in its own target so the separation goal 3 rests on is visible
# in the build graph and not only in a comment: akbasic is the interpreter,
# akbasic_akgl is the adaptors that draw through somebody else's renderer, and
# this is the one thing in the repository that creates a window. A game
# embedding the interpreter links the first two and not this.
add_library(akbasic_frontend src/frontend_akgl.c)
target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl)
akbasic_instrument(akbasic_frontend)
endif()
# ---------------------------------------------------------------------------
@@ -179,8 +190,14 @@ add_executable(basic src/main.c)
target_compile_options(basic PRIVATE -Wall -Wextra)
target_link_libraries(basic PRIVATE akbasic)
if(AKBASIC_WITH_AKGL)
target_link_libraries(basic PRIVATE akbasic_akgl)
target_compile_definitions(basic PRIVATE AKBASIC_HAVE_AKGL=1)
target_link_libraries(basic PRIVATE akbasic_frontend)
# The reference's own font, at the reference's own size. Its main.go opens
# "./fonts/C64_Pro_Mono-STYLE.ttf", which only ever resolved from its own
# source directory; compiled in, it resolves from anywhere, and AKBASIC_FONT
# overrides it at runtime for an installed copy.
target_compile_definitions(basic PRIVATE
AKBASIC_HAVE_AKGL=1
AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/fonts/C64_Pro_Mono-STYLE.ttf")
endif()
akbasic_instrument(basic)
@@ -229,6 +246,7 @@ set(AKBASIC_TESTS
runtime_verbs
scanner_tokens
sink_stdio
sink_tee
symtab
value_arithmetic
value_bitwise
@@ -276,6 +294,24 @@ if(AKBASIC_WITH_AKGL)
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
TIMEOUT 60
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy")
# The frontend suite. Separate from the one above because it tests the *host*
# rather than the adaptors -- it is the only test that links akbasic_frontend,
# and the only one that opens a window and pumps events. It uses the
# reference's own Commodore font, because proving the drawn text is in that
# font is part of what TODO.md section 3 asks for.
add_executable(akbasic_test_akgl_frontend tests/akgl_frontend.c)
target_compile_options(akbasic_test_akgl_frontend PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_test_akgl_frontend PRIVATE akbasic_frontend)
target_include_directories(akbasic_test_akgl_frontend PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
target_compile_definitions(akbasic_test_akgl_frontend PRIVATE
AKBASIC_TEST_C64_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/basicinterpret/fonts/C64_Pro_Mono-STYLE.ttf")
akbasic_instrument(akbasic_test_akgl_frontend)
_add_test(NAME akgl_frontend COMMAND akbasic_test_akgl_frontend)
_set_tests_properties(akgl_frontend PROPERTIES
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
TIMEOUT 60
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()
if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
@@ -323,6 +359,13 @@ if(AKBASIC_GOLDEN_CASES)
list(APPEND AKBASIC_GOLDEN_NAMES golden_${_name})
endforeach()
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES TIMEOUT 30)
# An AKGL build of `basic` opens a window, and forty-one of them is not what
# anybody running the suite wanted. The dummy driver produces the same stdout,
# which is the only thing a golden case compares.
if(AKBASIC_WITH_AKGL)
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()
endif()
# The local golden corpus, for verbs the reference never implemented.
@@ -341,8 +384,21 @@ file(GLOB_RECURSE AKBASIC_LOCAL_CASES
"${CMAKE_CURRENT_SOURCE_DIR}/tests/language/*.bas"
)
#
# One family of local cases is driver-specific and has to be. A case named
# no_device.bas asserts that a verb needing a device refuses politely when it was
# given none, which is true of the stdio driver and deliberately false of the
# AKGL one -- attaching the three device backends is the whole point of
# AKBASIC_WITH_AKGL, and TODO.md section 8 item 1 asks for exactly that change in
# observable behaviour. Skipping them there is not a coverage hole: the refusal
# path is asserted directly against the backend records in tests/devices.c, which
# both configurations build.
#
set(AKBASIC_LOCAL_NAMES)
foreach(_case IN LISTS AKBASIC_LOCAL_CASES)
if(AKBASIC_WITH_AKGL AND _case MATCHES "no_device\\.bas$")
continue()
endif()
string(REGEX REPLACE "^language/" "" _name "${_case}")
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
string(REPLACE "/" "_" _name "${_name}")
@@ -358,6 +414,10 @@ endforeach()
if(AKBASIC_LOCAL_NAMES)
_set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES TIMEOUT 30)
if(AKBASIC_WITH_AKGL)
_set_tests_properties(${AKBASIC_LOCAL_NAMES} PROPERTIES
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()
endif()
# ---------------------------------------------------------------------------