Files
akbasic/CMakeLists.txt
Andrew Kesterson 631c70ce7d Share a host program's own C structures with a script
A BASIC TYPE and a host C struct are the same thing seen from two sides, so both
go in one type table and everything the language already does with a structure
works across the boundary with no second set of rules. The host describes its
struct once as a table of field descriptors and binds an instance:

    akbasic_host_bind(&SCRIPT, "FOE@", "ENEMY", &GOBLIN);

after which `FOE@.HP# = FOE@.HP# - 10` decrements GOBLIN.hp in place, with no
marshalling step the host has to remember to run.

The sharing is done with shadow slots: a binding takes a run from the same value
pool a DIMmed record uses, a field read refreshes its slot from host memory
first, and a write converts back and stores. So the script always sees current
values and its writes always land, while the rest of the interpreter goes on
seeing one storage model instead of two.

AKBASIC_HOST_FIELD takes the offset and the width from the same member, which is
the only reason it is a macro: writing offsetof and sizeof out by hand is two
chances to name the wrong member and no way to notice. A field name's suffix
must agree with the C type it describes, refused at registration -- a host
writing "HP%" over an int32_t has said two different things about one field and
the script would believe the suffix.

Conversion refuses rather than truncates. 200 into an int8_t, 70000 into an
int16_t, -1 into a uint8_t and thirteen characters into a char[8] are each an
error naming the field, because a silent wrap is found three frames later in
code that did nothing wrong. Each width is tested separately, since a range
check is exactly the thing that is right for int32_t and wrong for int8_t when
only one of them is covered.

The language's own distinction turns out to be the one a host needs, so there is
one API rather than two: assignment copies and gives a script a private
snapshot, POINT shares and lets it change the game, and which one happened is
visible in the listing.

Two things the work required. The prescan runs again on every RUN and used to
wipe the whole type table, unregistering the host's types the first time a
script ran; it now keeps what the host registered and drops only what the script
declared. And akbasic_runtime_start() did not rewind, which cost nothing while a
host started a script once and cost everything to a host running one per enemy
-- the second start began past the end and silently did nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:56:04 -04:00

629 lines
27 KiB
CMake

cmake_minimum_required(VERSION 3.10)
# The single source of truth for the version, in the shape libakstdlib and
# libakgl both use. It flows into the library SOVERSION and nothing else spells
# a version number.
#
# 0.x on purpose: TODO.md section 12 records eleven defects carried over from the
# Go reference that are deliberately reproduced and not yet fixed, so the
# language surface is not being promised yet.
project(akbasic VERSION 0.1.0 LANGUAGES C)
# Pre-1.0 the ABI may break on a minor bump, so the soname carries MAJOR.MINOR.
# At 1.0 this becomes ${PROJECT_VERSION_MAJOR} alone.
if(PROJECT_VERSION_MAJOR EQUAL 0)
set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
else()
set(AKBASIC_SOVERSION "${PROJECT_VERSION_MAJOR}")
endif()
include(CTest)
include(GNUInstallDirs)
option(AKBASIC_WITH_AKGL "Build the libakgl-backed text sink and link SDL3" OFF)
option(AKBASIC_BUILD_EXAMPLES "Build the embedding example in examples/" ON)
option(AKBASIC_COVERAGE "Instrument the build with gcov coverage counters" OFF)
option(AKBASIC_SANITIZE "Build with ASan + UBSan" OFF)
# ---------------------------------------------------------------------------
# Dependencies
#
# libakgl vendors its own copies of libakerror and libakstdlib and guards every
# dependency with if(NOT TARGET ...), so akerror::akerror and akstdlib::akstdlib
# must exist *before* add_subdirectory(deps/libakgl) or the targets are declared
# twice.
#
# All three dependencies register their CTest tests unconditionally. Pulled in
# EXCLUDE_FROM_ALL their test binaries are never built, so each one would land in
# our suite as "Not Run" and fail. CMake has no way to un-register a test and
# set_tests_properties cannot reach across directory scopes, so add_test() and
# set_tests_properties() are shadowed for the duration of the add_subdirectory()
# calls. Note libakstdlib carries the same shadow but only arms it when *it* is
# top-level, so it does nothing for us -- this one has to wrap all three.
#
# libakerror additionally namespaces its `mutation` target when embedded but not
# its `coverage` target (deps/libakerror/CMakeLists.txt:194 vs :172), so a
# coverage build collides on the `coverage` target and fails to configure at all.
# Rename the dependency's on the way past. Remove this once libakerror applies
# the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to
# `mutation` -- filed in deps/libakstdlib/TODO.md section 2.3.
#
# **Only one project in a tree may shadow add_test(), and this is that project.**
# CMake exposes an overridden command as `_name` and chains exactly one level: a
# second override rebinds `_add_test` to the first override and the builtin
# becomes unreachable to everyone, so our own registrations recurse until CMake
# stops at "Maximum recursion depth of 1000 exceeded". libakgl 0.3.0 briefly
# shadowed it unconditionally and this is exactly what happened; the top-level
# guard it used to have is back, and libakstdlib has always had one.
# ---------------------------------------------------------------------------
set(AKBASIC_SUPPRESS_ADD_TEST TRUE)
function(add_test)
if(NOT AKBASIC_SUPPRESS_ADD_TEST)
_add_test(${ARGV})
endif()
endfunction()
function(set_tests_properties)
if(NOT AKBASIC_SUPPRESS_ADD_TEST)
_set_tests_properties(${ARGV})
endif()
endfunction()
function(add_custom_target _name)
if(AKBASIC_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
_add_custom_target(akerror_coverage ${ARGN})
else()
_add_custom_target(${ARGV})
endif()
endfunction()
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
if(AKBASIC_WITH_AKGL)
# libakgl 0.3.0 adds its own vendored SDL, SDL_image, SDL_mixer, SDL_ttf and
# jansson whether or not it is top-level, so there is nothing to do here but
# add it. Before that it went down a find_package path when embedded and
# required all five installed on the system, and this block declared them by
# hand first -- filed as libakgl API-gap item 5 and resolved there.
#
# akerror::akerror and akstdlib::akstdlib still have to exist first: libakgl
# guards every dependency with if(NOT TARGET ...), which is what stops its
# vendored copies of those two being declared a second time.
add_subdirectory(deps/libakgl EXCLUDE_FROM_ALL)
endif()
set(AKBASIC_SUPPRESS_ADD_TEST FALSE)
# ---------------------------------------------------------------------------
# Instrumentation, applied per target so it never leaks into an exported
# interface.
# ---------------------------------------------------------------------------
function(akbasic_instrument _target)
if(AKBASIC_COVERAGE)
target_compile_options(${_target} PRIVATE --coverage -O0 -g)
target_link_options(${_target} PRIVATE --coverage)
endif()
if(AKBASIC_SANITIZE)
target_compile_options(${_target} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
target_link_options(${_target} PRIVATE -fsanitize=address,undefined)
endif()
endfunction()
# ---------------------------------------------------------------------------
# The interpreter library. Everything here must be free of SDL, free of any
# process-terminating call, and buildable with no libakgl present.
# ---------------------------------------------------------------------------
set(AKBASIC_SOURCES
src/args.c
src/audio_tables.c
src/data.c
src/environment.c
src/error.c
src/grammar.c
src/format.c
src/graphics_tables.c
src/parser.c
src/parser_commands.c
src/play.c
src/runtime.c
src/runtime_audio.c
src/runtime_commands.c
src/runtime_console.c
src/runtime_disk.c
src/runtime_format.c
src/runtime_functions.c
src/runtime_graphics.c
src/runtime_housekeeping.c
src/runtime_machine.c
src/renumber.c
src/runtime_input.c
src/runtime_sprite.c
src/runtime_struct.c
src/runtime_structure.c
src/runtime_trap.c
src/scanner.c
src/sink_stdio.c
src/sink_tee.c
src/sprite_tables.c
src/host.c
src/structtype.c
src/symtab.c
src/value.c
src/variable.c
src/verbs.c
)
add_library(akbasic ${AKBASIC_SOURCES})
add_library(akbasic::akbasic ALIAS akbasic)
target_include_directories(akbasic PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)
target_compile_options(akbasic PRIVATE -Wall -Wextra)
target_link_libraries(akbasic PUBLIC akstdlib::akstdlib akerror::akerror)
target_link_libraries(akbasic PRIVATE m)
set_target_properties(akbasic PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${AKBASIC_SOVERSION}
)
akbasic_instrument(akbasic)
# The libakgl-backed text sink, kept in its own target so the core library and
# the whole golden suite build with no SDL present.
if(AKBASIC_WITH_AKGL)
add_library(akbasic_akgl
src/audio_akgl.c
src/graphics_akgl.c
src/input_akgl.c
src/sink_akgl.c
src/sprite_akgl.c
)
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)
# The documentation's figure generator: a second, much smaller host that draws
# one program onto an offscreen target and writes a PNG. Not instrumented and
# not a test -- it produces a build artifact rather than an answer, and folding
# it into the coverage figure would credit the interpreter for lines only a
# documentation build runs.
add_executable(akbasic_screenshot tools/screenshot.c)
target_compile_options(akbasic_screenshot PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_screenshot PRIVATE akbasic_akgl SDL3_image::SDL3_image)
# Regenerating every figure in docs/ is a deliberate act, never part of a
# build: the PNGs are checked in, and a rebuild that silently rewrote them
# would put a binary diff in front of anybody who happened to run `make`.
add_custom_target(docs_screenshots
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh"
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--tool "$<TARGET_FILE:akbasic_screenshot>"
DEPENDS akbasic_screenshot
COMMENT "Regenerating the documentation figures in docs/images"
VERBATIM)
endif()
# ---------------------------------------------------------------------------
# The standalone driver. This is the only place FINISH_NORETURN may appear.
# ---------------------------------------------------------------------------
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_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. Vendored into assets/fonts/
# rather than reached for in the submodule -- see assets/fonts/PROVENANCE.md,
# which also carries the licence question that came with it.
target_compile_definitions(basic PRIVATE
AKBASIC_HAVE_AKGL=1
AKBASIC_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
endif()
akbasic_instrument(basic)
# The embedding example. It is built by default and registered as a test so the
# code README.md quotes cannot rot: a signature change breaks the build.
if(AKBASIC_BUILD_EXAMPLES)
foreach(_example IN ITEMS embed hostvars hoststruct)
add_executable(akbasic_example_${_example} examples/${_example}.c)
target_compile_options(akbasic_example_${_example} PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_example_${_example} PRIVATE akbasic)
akbasic_instrument(akbasic_example_${_example})
_add_test(NAME example_${_example} COMMAND akbasic_example_${_example})
endforeach()
endif()
# ---------------------------------------------------------------------------
# Tests.
#
# Three lists, and two of them invert the meaning of "Passed", exactly as
# libakstdlib does:
# AKBASIC_TESTS must exit 0
# AKBASIC_WILL_FAIL_TESTS abort by design
# AKBASIC_KNOWN_FAILING_TESTS assert the *correct* contract for a defect
# recorded in TODO.md and are expected to fail.
# When one starts passing CTest reports
# "unexpectedly passed" -- that is the cue to move
# it into AKBASIC_TESTS along with the fix.
#
# Test executables are named akbasic_test_<name>, never test_<name>: a
# dependency's targets are created even under EXCLUDE_FROM_ALL, and libakstdlib
# already ships test_version. The CTest name stays bare, so `ctest -R value`
# still selects it.
# ---------------------------------------------------------------------------
set(AKBASIC_TESTS
audio_verbs
console_verbs
devices
disk_verbs
environment_scope
error_codes
for_next
format_verbs
grammar_leaves
graphics_verbs
hoststruct
hostvars
housekeeping_verbs
input_verbs
interrupts
machine_verbs
numeric_contract
parser_commands
parser_expressions
read_data
renumber
runtime_evaluate
runtime_verbs
scanner_tokens
sink_stdio
sink_tee
sprite_verbs
structure_verbs
struct_pointers
struct_types
trap_verbs
symtab
value_arithmetic
value_bitwise
value_compare
variable_subscript
verbs_table
version_check
)
set(AKBASIC_WILL_FAIL_TESTS
)
# Empty, and worth keeping declared rather than deleted -- libakstdlib left its
# AKSL_KNOWN_FAILING_TESTS in place for the same reason when 0.2.0 cleared it.
# The next defect that has to be reproduced before it can be fixed goes here, and
# the machinery to do that should not have to be reinvented.
set(AKBASIC_KNOWN_FAILING_TESTS
for_semantics
)
foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILING_TESTS)
add_executable(akbasic_test_${_test} tests/${_test}.c)
target_compile_options(akbasic_test_${_test} PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_test_${_test} PRIVATE akbasic)
target_include_directories(akbasic_test_${_test} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
akbasic_instrument(akbasic_test_${_test})
_add_test(NAME ${_test} COMMAND akbasic_test_${_test})
endforeach()
# The akgl-backed suite. Its own list because it is the only test here that links
# SDL, and because it can only exist when AKBASIC_WITH_AKGL is on -- the whole
# point of the backend records is that everything else builds without it.
#
# It runs under the dummy video and audio drivers with a software renderer,
# following deps/libakgl/tests/draw.c: a 128x128 target read back with
# SDL_RenderReadPixels, so it needs no display and no offscreen harness.
if(AKBASIC_WITH_AKGL)
add_executable(akbasic_test_akgl_backends tests/akgl_backends.c)
target_compile_options(akbasic_test_akgl_backends PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_test_akgl_backends PRIVATE akbasic_akgl)
target_include_directories(akbasic_test_akgl_backends PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
# libakgl's monospaced fixture font, borrowed rather than copied: a second copy
# of a 10 KB binary is a second thing to keep in step for no benefit.
target_compile_definitions(akbasic_test_akgl_backends PRIVATE
AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
akbasic_instrument(akbasic_test_akgl_backends)
_add_test(NAME akgl_backends COMMAND akbasic_test_akgl_backends)
_set_tests_properties(akgl_backends PROPERTIES
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}/assets/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")
# The keyboard test, and the only one in the repository that uses a real X
# server rather than the dummy driver. Note the deliberate absence of an
# ENVIRONMENT line: forcing the dummy driver here would defeat the whole point.
#
# Everything else synthesises SDL events, which tests the code *downstream* of
# SDL and cannot test the code upstream of it. That gap shipped a bug -- the
# frontend never called SDL_StartTextInput(), so SDL emitted no text-input
# events, the line editor dropped every keystroke, and the suite stayed green
# because it was pushing those events itself. This one drives xdotool at a
# focused window, so X11, SDL's composition and the window manager are all in
# the path.
#
# Needs xdotool and script(1), and it **steals keyboard focus** for a few
# seconds. Skipped rather than failed when it cannot run -- headless, no
# xdotool, no window manager -- because none of those means the answer is no.
# AKBASIC_SKIP_INTERACTIVE=1 skips it while you are using the machine.
find_program(XDOTOOL_EXECUTABLE xdotool)
if(NOT XDOTOOL_EXECUTABLE)
message(STATUS "xdotool not found: the akgl_typing test will skip. "
"Install it to cover the real keyboard path.")
endif()
_add_test(NAME akgl_typing
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/akgl_typing.sh $<TARGET_FILE:basic>)
_set_tests_properties(akgl_typing PROPERTIES
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
TIMEOUT 180
SKIP_RETURN_CODE 77)
# Every figure in docs/ re-rendered and byte-compared against the checked-in
# copy. docs_examples only asks whether the file *exists*, which catches a
# figure that was never generated and not one that stopped being of the code
# beside it -- and that second failure is the one this whole arrangement is
# against.
#
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
# tests/reference/ already makes about golden output: that the dummy video
# driver and the software renderer are reproducible. They are, run to run and
# build to build. What is untested is an SDL upgrade that shifts one pixel of
# a diagonal, and the answer to that is to regenerate the figures in the same
# commit as the bump -- not to weaken this to a size check, which would pass
# for every wrong picture that happened to be 320x200.
_add_test(NAME docs_screenshots
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--tool $<TARGET_FILE:akbasic_screenshot>
--check)
_set_tests_properties(docs_screenshots PROPERTIES
TIMEOUT 120
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif()
# ---------------------------------------------------------------------------
# The documentation's own examples.
#
# docs/ and README.md are full of programs, transcripts, C snippets and shell
# commands, and every one of them was checked by hand exactly once -- when it
# was written. Three were already wrong the day this was added, one of them a
# struct that had grown two members hours earlier. Documentation goes stale
# because the *code* moved, so this is an ordinary test case rather than
# something a docs-only CI job runs: it fails when the interpreter changes under
# the chapter, which is the case a path filter would miss.
#
# tests/docs_examples.sh reads the fence info strings; MAINTENANCE.md documents
# them.
#
# The C snippets compile against the real include path, which is transitive
# through akerror, akstdlib and -- in this configuration -- akgl. Writing it out
# for the script to read keeps that one source of truth: a hardcoded -I list
# here would rot exactly the way the documentation does.
set(AKBASIC_DOCS_CFLAGS_FILE "${CMAKE_CURRENT_BINARY_DIR}/docs_cflags.txt")
if(AKBASIC_WITH_AKGL)
set(AKBASIC_DOCS_CFLAGS_TARGET akbasic_akgl)
else()
set(AKBASIC_DOCS_CFLAGS_TARGET akbasic)
endif()
file(GENERATE
OUTPUT "${AKBASIC_DOCS_CFLAGS_FILE}"
CONTENT "-I$<JOIN:$<TARGET_PROPERTY:${AKBASIC_DOCS_CFLAGS_TARGET},INCLUDE_DIRECTORIES>,\n-I>\n"
)
# Spelled out with if() rather than $<$<BOOL:...>:--akgl>, because a
# generator expression that evaluates to nothing still contributes an *empty
# argument*. The script read that empty string as the first filename, checked
# no documents at all, and passed -- caught only because it reports what it ran.
set(AKBASIC_DOCS_ARGS
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--basic $<TARGET_FILE:basic>
--cflags-file "${AKBASIC_DOCS_CFLAGS_FILE}")
if(AKBASIC_WITH_AKGL)
list(APPEND AKBASIC_DOCS_ARGS --akgl)
endif()
_add_test(NAME docs_examples
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/docs_examples.sh ${AKBASIC_DOCS_ARGS})
_set_tests_properties(docs_examples PROPERTIES
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
TIMEOUT 300)
if(AKBASIC_WITH_AKGL)
# Same reason as the golden cases: an AKGL `basic` opens a window, and the
# chapters on graphics, sound and sprites are most of what runs here.
_set_tests_properties(docs_examples PROPERTIES
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)
_set_tests_properties(
${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
)
endif()
if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
_set_tests_properties(
${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
PROPERTIES WILL_FAIL TRUE
)
endif()
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
# case per .bas so a failure names the file.
#
# It used to be driven in place out of deps/basicinterpret, on the reasoning that
# copying a submodule's corpus guarantees drift. That reasoning was sound and it
# has been overruled deliberately: the Go dependency is being deprecated, and a
# build that cannot run its acceptance suite without cloning the implementation
# it replaced is not finished. The copy is byte-identical to
# basicinterpreter@d76162c and tests/reference/README.md records
# where it came from and what the drift now costs.
file(GLOB_RECURSE AKBASIC_GOLDEN_CASES
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/*.bas"
)
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
string(REGEX REPLACE "^tests/" "" _name "${_case}")
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
string(REPLACE "/" "_" _name "${_name}")
_add_test(
NAME golden_${_name}
COMMAND ${CMAKE_COMMAND}
-DBASIC=$<TARGET_FILE:basic>
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/${_case}
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
)
endforeach()
if(AKBASIC_GOLDEN_CASES)
set(AKBASIC_GOLDEN_NAMES)
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
string(REGEX REPLACE "^tests/" "" _name "${_case}")
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
string(REPLACE "/" "_" _name "${_name}")
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.
#
# Still separate now that the reference's corpus lives in this repository too,
# and the reason changed rather than went away: tests/reference/ is a *record* of
# what the Go implementation did and nothing in it should ever be edited to suit
# this one, while tests/language/ is ours to change. Registered under local_ so a
# failure says at a glance which of the two it came from -- and so a diff that
# touches tests/reference/ stands out as the thing it is.
#
# Note what this can and cannot cover. The graphics and sound verbs draw and play
# rather than print, so what a golden file sees of them is their *refusals* and
# whatever a program can PRINT about the state they changed. The behaviour that
# reaches a device is asserted against tests/mockdevice.h instead.
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests"
"${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}")
_add_test(
NAME local_${_name}
COMMAND ${CMAKE_COMMAND}
-DBASIC=$<TARGET_FILE:basic>
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/${_case}
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
)
list(APPEND AKBASIC_LOCAL_NAMES local_${_name})
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()
# ---------------------------------------------------------------------------
# Mutation testing.
#
# Coverage says which lines ran; mutation testing says whether anything would
# have noticed if they were wrong. That distinction matters more here than in an
# ordinary C library: the akerror control-flow macros expand at their call sites,
# so gcov attributes ATTEMPT/CATCH/PASS to the caller and cannot really see them.
# Mutation testing is the only thing that checks them at all.
#
# This target runs the whole akbasic-owned src/ tree and is slow -- upwards of an
# hour. CI runs a narrower, faster set with a --threshold gate; see
# .gitea/workflows/ci.yaml.
#
# Namespaced when embedded in another project, for the same reason the coverage
# targets in the dependencies are: a sibling may well ship a `mutation` target.
find_package(Python3 COMPONENTS Interpreter)
if(Python3_FOUND)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKBASIC_MUTATION_TARGET mutation)
else()
set(AKBASIC_MUTATION_TARGET akbasic_mutation)
endif()
add_custom_target(${AKBASIC_MUTATION_TARGET}
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
USES_TERMINAL
COMMENT "Running mutation tests (breaks the library, expects tests to fail)"
)
endif()
install(TARGETS akbasic basic
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/akbasic DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})