Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
Some checks failed
akbasic CI Build / cmake_build (push) Successful in 2m52s
akbasic CI Build / sanitizers (push) Successful in 3m9s
akbasic CI Build / coverage (push) Failing after 3m12s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Successful in 7m27s

That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.

Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.

The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.

tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.

Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.

One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.

72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 10:28:04 -04:00
parent c70f8e7727
commit ecdc6b60ef
19 changed files with 245 additions and 248 deletions

View File

@@ -58,16 +58,41 @@ static void test_tables(void)
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(65536, &hz), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_audio_register_to_hz(-1, &hz), AKBASIC_ERR_BOUNDS);
/* Attack and decay are different tables, and decay is three times attack. */
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(0, true, &ms));
TEST_REQUIRE_INT(ms, 2);
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(0, false, &ms));
TEST_REQUIRE_INT(ms, 6);
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(15, true, &ms));
TEST_REQUIRE_INT(ms, 8000);
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(15, false, &ms));
TEST_REQUIRE_INT(ms, 24000);
/*
* Every entry of both ADSR tables, not just the ends.
*
* Mutation testing is why: checking only rate 0 and rate 15 left the twelve
* entries between them free to be any number at all, and the failure mode of
* this file is a constant that is plausibly wrong rather than obviously
* wrong. A corrupted middle entry makes one envelope decay at the wrong
* speed, which nothing else in the suite would notice and no listener could
* attribute.
*
* The values are the 6581/8580 datasheet's, transcribed here independently
* of src/audio_tables.c rather than read back from it.
*/
{
static const int attack[16] = {
2, 8, 16, 24, 38, 56, 68, 80,
100, 250, 500, 800, 1000, 3000, 5000, 8000
};
int rate = 0;
for ( rate = 0; rate < 16; rate++ ) {
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, true, &ms));
TEST_REQUIRE_INT(ms, attack[rate]);
/*
* Decay and release are exactly three times attack. That is a
* property of the chip's envelope generator rather than a
* coincidence, so it is asserted as the relationship it is.
*/
TEST_REQUIRE_OK(akbasic_audio_rate_to_ms(rate, false, &ms));
TEST_REQUIRE_INT(ms, attack[rate] * 3);
}
}
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(16, true, &ms), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(-1, false, &ms), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_audio_rate_to_ms(0, true, NULL), AKERR_NULLPOINTER);
/*
* Equal temperament, checked at the three points anybody would know: A4 is

View File

@@ -1,72 +0,0 @@
/**
* @file convert.c
* @brief Tests the strict string-to-number converters.
*
* These exist because libakstdlib's aksl_ato* family cannot report a conversion
* failure. Every assertion here is one that family would get wrong.
*/
#include <errno.h>
#include <akbasic/convert.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
int64_t i = 0;
double d = 0.0;
TEST_REQUIRE_OK(akbasic_error_register());
/* Valid decimal, hex and negative. */
TEST_REQUIRE_OK(akbasic_str_to_int64("1234", 10, &i));
TEST_REQUIRE_INT(i, 1234);
TEST_REQUIRE_OK(akbasic_str_to_int64("0xff", 16, &i));
TEST_REQUIRE_INT(i, 255);
TEST_REQUIRE_OK(akbasic_str_to_int64("-256", 10, &i));
TEST_REQUIRE_INT(i, -256);
/*
* The whole point: aksl_atoi returns success with 0 for each of these.
*/
TEST_REQUIRE_STATUS(akbasic_str_to_int64("", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("not a number", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("12abc", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("12 ", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("99999999999999999999999", 10, &i), ERANGE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64(NULL, 10, &i), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("1", 10, NULL), AKERR_NULLPOINTER);
/* Floats. */
TEST_REQUIRE_OK(akbasic_str_to_double("123.456", &d));
TEST_REQUIRE_FEQ(d, 123.456);
TEST_REQUIRE_OK(akbasic_str_to_double("-2.5", &d));
TEST_REQUIRE_FEQ(d, -2.5);
TEST_REQUIRE_OK(akbasic_str_to_double("32", &d));
TEST_REQUIRE_FEQ(d, 32.0);
TEST_REQUIRE_STATUS(akbasic_str_to_double("", &d), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_double("garbage", &d), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_double("1.5x", &d), AKBASIC_ERR_VALUE);
/*
* errno hygiene. strtoll and strtod do not clear errno on success, so a
* stale ERANGE left by some earlier call would make a perfectly valid
* conversion raise. That is what the `errno = 0` before each call is for.
*
* Found by mutation testing: deleting that line, or setting errno to
* something non-zero, survived the suite until these four assertions
* existed.
*/
errno = ERANGE;
TEST_REQUIRE_OK(akbasic_str_to_int64("42", 10, &i));
TEST_REQUIRE_INT(i, 42);
errno = ERANGE;
TEST_REQUIRE_OK(akbasic_str_to_double("2.5", &d));
TEST_REQUIRE_FEQ(d, 2.5);
return akbasic_test_failures;
}

View File

@@ -0,0 +1,6 @@
10 REM The reference selects base 8 for any lexeme starting with 0, so 010 is 8
20 REM and 08 is a parse error. Commodore BASIC has no octal literals -- this is
30 REM TODO.md section 6 item 10, reproduced deliberately until it is fixed on
40 REM purpose. Pinned here so the eventual fix has to change a golden file.
50 PRINT 010
60 PRINT 08

View File

@@ -0,0 +1,3 @@
8
? 60 : PARSE ERROR trailing junk "8" after the number in "08"

View File

@@ -0,0 +1,6 @@
10 REM VAL on a non-numeric string must raise rather than quietly return 0.
20 REM There was no golden coverage of this until the converters moved onto
30 REM libakstdlib 0.2.0, which is when the message became that library's.
40 PRINT VAL("32")
50 PRINT VAL("garbage")
60 PRINT "UNREACHABLE"

View File

@@ -0,0 +1,3 @@
32.000000
? 50 : RUNTIME ERROR no digits in "garbage"

102
tests/numeric_contract.c Normal file
View File

@@ -0,0 +1,102 @@
/**
* @file numeric_contract.c
* @brief Pins the string-to-number contract this port depends on libakstdlib for.
*
* `src/convert.c` used to live here in spirit: it wrapped `strtoll`/`strtod`
* because `libakstdlib`'s `aksl_ato*` family could not report a conversion
* failure at all, and every assertion below is one that family used to get
* wrong. As of `libakstdlib` 0.2.0 it gets them right, so the wrapper is gone
* and the call sites go straight to the library.
*
* The assertions did not go with it. Four things in this interpreter turn a
* string into a number -- an integer literal, a float literal, a line number in
* the scanner, and `VAL()` at runtime -- and every one of them relies on a bad
* string being *refused* rather than quietly becoming zero. That is a contract
* belonging to another library now, which is exactly why it is worth a test on
* this side: if it ever regresses, `VAL("garbage")` starts returning 0 and
* nothing else in the suite would say so.
*
* Same reasoning `libakstdlib`'s own `tests/test_status_registry.c` gives for
* pinning that it reserves no status range: a contract you depend on and do not
* own is worth asserting where you depend on it.
*/
#include <errno.h>
#include <akstdlib.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
long long i = 0;
double d = 0.0;
TEST_REQUIRE_OK(akbasic_error_register());
/* The shapes the scanner and the parser actually produce. */
TEST_REQUIRE_OK(aksl_atoll("1234", &i));
TEST_REQUIRE_INT(i, 1234);
TEST_REQUIRE_OK(aksl_atoll("-256", &i));
TEST_REQUIRE_INT(i, -256);
/*
* Hex goes through aksl_strtoll with an explicit base, because the ato*
* forms are base 10 and would stop at the 'x'. src/grammar.c picks the base
* from the lexeme's prefix and passes it here, so both halves matter: base
* 16 must accept the 0x, and base 8 must read a leading zero as octal --
* which is the reference's behaviour that TODO.md section 6 item 10 records
* as a defect and section 5 says to reproduce until it is fixed on purpose.
*/
TEST_REQUIRE_OK(aksl_strtoll("0xff", NULL, 16, &i));
TEST_REQUIRE_INT(i, 255);
TEST_REQUIRE_OK(aksl_strtoll("010", NULL, 8, &i));
TEST_REQUIRE_INT(i, 8);
/*
* The refusals. Every one of these returned success with 0 before
* libakstdlib 0.2.0, which is the whole reason src/convert.c existed: a
* silent 0 turns four diagnosable errors into wrong answers.
*/
TEST_REQUIRE_STATUS(aksl_atoll("", &i), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atoll("not a number", &i), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atoll("12abc", &i), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atoll("12 ", &i), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atoll("99999999999999999999999", &i), ERANGE);
/* A NULL endptr is what makes trailing junk an error rather than a stop. */
TEST_REQUIRE_STATUS(aksl_strtoll("12abc", NULL, 10, &i), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atoll(NULL, &i), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(aksl_atoll("1", NULL), AKERR_NULLPOINTER);
/* The float half, which is what VAL() and a float literal both reach. */
TEST_REQUIRE_OK(aksl_atof("123.456", &d));
TEST_REQUIRE_FEQ(d, 123.456);
TEST_REQUIRE_OK(aksl_atof("-2.5", &d));
TEST_REQUIRE_FEQ(d, -2.5);
TEST_REQUIRE_OK(aksl_atof("32", &d));
TEST_REQUIRE_FEQ(d, 32.0);
TEST_REQUIRE_STATUS(aksl_atof("", &d), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atof("garbage", &d), AKERR_VALUE);
TEST_REQUIRE_STATUS(aksl_atof("1.5x", &d), AKERR_VALUE);
/*
* errno hygiene, kept from the wrapper's own tests because the hazard did
* not move: strtoll and strtod do not clear errno on success, so a stale
* ERANGE left by an earlier call must not make a valid conversion raise.
* Mutation testing found this when the wrapper owned it; asserting it here
* is what says libakstdlib carries the same guarantee.
*/
errno = ERANGE;
TEST_REQUIRE_OK(aksl_atoll("42", &i));
TEST_REQUIRE_INT(i, 42);
errno = ERANGE;
TEST_REQUIRE_OK(aksl_atof("2.5", &d));
TEST_REQUIRE_FEQ(d, 2.5);
return akbasic_test_failures;
}