Port onto libakstdlib 2b79aca and convert the eight bool predicates

akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.

Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.

verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.

Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.

DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.

Refs libakstdlib#26, libakstdlib#38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 15:41:49 -04:00
parent 330d731cfe
commit b434be1901
40 changed files with 1211 additions and 584 deletions

View File

@@ -9,7 +9,6 @@
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
@@ -96,7 +95,7 @@ akerr_ErrorContext *akbasic_cmd_print(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
*/
if ( (*dest)->valuetype == AKBASIC_TYPE_STRUCT || (*dest)->valuetype == AKBASIC_TYPE_POINTER ) {
if ( (*dest)->structbase == NULL ) {
snprintf(rendered, sizeof(rendered), "NOTHING");
PASS(errctx, aksl_strcpy(rendered, sizeof(rendered), "NOTHING"));
} else {
PASS(errctx, akbasic_struct_to_string(obj, (*dest)->structtype, (*dest)->structbase,
0, rendered, sizeof(rendered)));
@@ -149,13 +148,15 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
{
PREPARE_ERROR(errctx);
akbasic_Value *result = NULL;
bool waiting = false;
(void)lval; (void)rval;
/*
* A RETURN reached while skipping forward to one is the end of a DEF body,
* not a subroutine return. Stop waiting and carry on.
*/
if ( akbasic_environment_is_waiting_for(obj->environment, "RETURN") ) {
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "RETURN", &waiting));
if ( waiting ) {
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "RETURN"));
SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx);
@@ -445,6 +446,7 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char rendered[AKBASIC_MAX_STRING_LENGTH];
char buffer[AKBASIC_MAX_LINE_LENGTH];
long long converted = 0;
size_t len = 0;
bool eof = false;
(void)lval; (void)rval;
@@ -479,10 +481,11 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
break;
default:
entered->valuetype = AKBASIC_TYPE_STRING;
FAIL_ZERO_RETURN(errctx, (strlen(buffer) < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE,
PASS(errctx, aksl_strlen(buffer, &len));
FAIL_ZERO_RETURN(errctx, (len < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE,
"Input line exceeds the %d character limit", AKBASIC_MAX_STRING_LENGTH - 1);
strncpy(entered->stringval, buffer, AKBASIC_MAX_STRING_LENGTH - 1);
entered->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0';
/* aksl_strcpy always terminates, so the explicit terminator is gone. */
PASS(errctx, aksl_strcpy(entered->stringval, sizeof(entered->stringval), buffer));
break;
}
PASS(errctx, akbasic_environment_assign(obj->environment, identifier, entered, &unused));
@@ -539,6 +542,7 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
int64_t startidx = 0;
int64_t endidx = 0;
int64_t i = 0;
int written = 0;
(void)lval; (void)rval;
PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx));
@@ -552,7 +556,8 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
snprintf(line, sizeof(line), "%" PRId64 " %s", i, obj->source[i].code);
PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "%" PRId64 " %s",
i, obj->source[i].code));
PASS(errctx, akbasic_runtime_println(obj, line));
}
SUCCEED_TRUE(obj, dest);
@@ -588,6 +593,7 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe
{
PREPARE_ERROR(errctx);
akbasic_Value *value = NULL;
size_t namelen = 0;
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
"Expected a filename");
@@ -596,10 +602,107 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe
"Expected a filename string");
FAIL_ZERO_RETURN(errctx, (value->stringval[0] != '\0'), AKBASIC_ERR_VALUE,
"Filename must not be empty");
FAIL_ZERO_RETURN(errctx, (strlen(value->stringval) < len), AKBASIC_ERR_BOUNDS,
PASS(errctx, aksl_strlen(value->stringval, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < len), AKBASIC_ERR_BOUNDS,
"Filename exceeds the %zu character limit", len - 1);
strncpy(dest, value->stringval, len - 1);
dest[len - 1] = '\0';
/* aksl_strcpy always terminates, so the explicit terminator is gone. */
PASS(errctx, aksl_strcpy(dest, len, value->stringval));
SUCCEED_RETURN(errctx);
}
/**
* @brief Read one line, reporting end of stream rather than raising it.
*
* aksl_fgets raises AKERR_EOF where fgets(3) returned NULL, so the end of an
* ordinary file arrives as an error and has to be handled. It is handled here,
* in a function whose ATTEMPT block contains no loop at all, because a CATCH
* inside a loop expands to a break that leaves the loop rather than the
* ATTEMPT. The caller reads @p eof instead.
*/
static akerr_ErrorContext *dload_read_line(FILE *fp, char *buffer, size_t len, bool *eof)
{
PREPARE_ERROR(errctx);
size_t used = 0;
*eof = false;
ATTEMPT {
CATCH(errctx, aksl_fgets(buffer, len, fp, &used));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_EOF) {
*eof = true;
used = 0;
} FINISH(errctx, true);
while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) {
buffer[used - 1] = '\0';
used -= 1;
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a whole program in, one line at a time.
*
* Hoisted out of akbasic_cmd_dload()'s ATTEMPT block because a loop inside one
* can use neither CATCH -- which breaks the loop -- nor PASS, which returns
* past CLEANUP and leaves the file open. Out here PASS is correct, and the
* caller CATCHes this one call.
*/
static akerr_ErrorContext *dload_read_program(akbasic_Runtime *obj, FILE *fp,
char *buffer, size_t buflen,
char *scanned, size_t scanlen)
{
PREPARE_ERROR(errctx);
bool eof = false;
for ( ;; ) {
PASS(errctx, dload_read_line(fp, buffer, buflen, &eof));
if ( eof ) {
break;
}
if ( buffer[0] == '\0' ) {
continue;
}
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, scanlen));
PASS(errctx, akbasic_runtime_file_line(obj, scanned));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Write every stored line out, numbered.
*
* Hoisted out of akbasic_cmd_dsave()'s ATTEMPT block for the same reason
* dload_read_program() is.
*/
static akerr_ErrorContext *dsave_write_program(akbasic_Runtime *obj, FILE *fp, char *line, size_t len)
{
PREPARE_ERROR(errctx);
size_t written = 0;
int64_t i = 0;
int count = 0;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
/*
* aksl_snprintf's count is the length it wrote, and it treats
* truncation as an error rather than reporting a length it did not
* write -- so it is what goes to fwrite, and the strlen that used to
* recompute it is gone.
*/
PASS(errctx, aksl_snprintf(&count, line, len, "%" PRId64 " %s\n", i, obj->source[i].code));
/*
* The written count is required by libakstdlib 0.2.0 and discarded
* here on purpose: a short write is no longer something a caller has
* to notice for itself, because that release made it an AKERR_IO
* rather than a silent success. Before it, a DSAVE onto a full disk
* reported nothing.
*/
PASS(errctx, aksl_fwrite(line, 1, (size_t)count, fp, &written));
}
SUCCEED_RETURN(errctx);
}
@@ -610,7 +713,6 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char buffer[AKBASIC_MAX_LINE_LENGTH];
char scanned[AKBASIC_MAX_LINE_LENGTH];
FILE *fp = NULL;
size_t used = 0;
int64_t i = 0;
(void)lval; (void)rval;
@@ -632,23 +734,14 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
ATTEMPT {
CATCH(errctx, aksl_fopen(filename, "r", &fp));
while ( fgets(buffer, sizeof(buffer), fp) != NULL ) {
used = strlen(buffer);
while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) {
buffer[used - 1] = '\0';
used -= 1;
}
if ( buffer[0] == '\0' ) {
continue;
}
/*
* PASS inside the loop, never CATCH: CATCH expands to a break, which
* would leave this loop rather than the ATTEMPT and let the rest of
* the block run with an error pending.
*/
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
PASS(errctx, akbasic_runtime_file_line(obj, scanned));
}
/*
* The read is one CATCH of one call, because the loop it used to be
* lives in dload_read_program() now: inside this block a loop can use
* neither CATCH nor PASS without either escaping the loop or returning
* past the fclose below.
*/
CATCH(errctx, dload_read_program(obj, fp, buffer, sizeof(buffer),
scanned, sizeof(scanned)));
} CLEANUP {
if ( fp != NULL ) {
IGNORE(aksl_fclose(fp));
@@ -676,30 +769,13 @@ akerr_ErrorContext *akbasic_cmd_dsave(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char filename[AKBASIC_MAX_STRING_LENGTH];
char line[AKBASIC_MAX_LINE_LENGTH * 2];
FILE *fp = NULL;
int64_t i = 0;
int count = 0;
size_t written = 0;
(void)lval; (void)rval;
PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename)));
ATTEMPT {
CATCH(errctx, aksl_fopen(filename, "w", &fp));
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
snprintf(line, sizeof(line), "%" PRId64 " %s\n", i, obj->source[i].code);
/*
* The written count is required by libakstdlib 0.2.0 and discarded
* here on purpose: a short write is no longer something a caller has
* to notice for itself, because that release made it an AKERR_IO
* rather than a silent success. Before it, a DSAVE onto a full disk
* reported nothing.
*/
PASS(errctx, aksl_fwrite(line, 1, strlen(line), fp, &written));
count += 1;
}
CATCH(errctx, dsave_write_program(obj, fp, line, sizeof(line)));
} CLEANUP {
if ( fp != NULL ) {
IGNORE(aksl_fclose(fp));
@@ -807,6 +883,7 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
akbasic_Value *updated = NULL;
akbasic_Value scratch;
int64_t zerosubscript[1] = { 0 };
int cmp = 0;
bool met = false;
(void)lval; (void)rval;
@@ -842,7 +919,9 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
* A NEXT for someone else's loop variable: this environment is done, hand
* the line back to the parent and pop. That is how nested loops unwind.
*/
if ( strcmp(expr->right->identifier, obj->environment->forNextVariable->name) != 0 ) {
PASS(errctx, aksl_strcmp(expr->right->identifier,
obj->environment->forNextVariable->name, &cmp));
if ( cmp != 0 ) {
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
"NEXT in an orphaned environment");
obj->environment->parent->nextline = obj->environment->nextline;
@@ -951,7 +1030,8 @@ akerr_ErrorContext *akbasic_cmd_read(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_INT));
if ( value.valuetype == AKBASIC_TYPE_STRING ) {
literal.leaftype = AKBASIC_LEAF_LITERAL_STRING;
snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", value.stringval);
PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string),
value.stringval));
} else if ( value.valuetype == AKBASIC_TYPE_FLOAT ) {
literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT;
literal.literal_float = value.floatval;