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>
194 lines
7.7 KiB
C
194 lines
7.7 KiB
C
/**
|
|
* @file runtime_trap.c
|
|
* @brief The group C verbs: TRAP, RESUME, and the ERR() message lookup.
|
|
*
|
|
* Error trapping, built on the interrupt machinery COLLISION already uses:
|
|
* #AKBASIC_INTERRUPT_ERROR was declared with it and armed by nothing until now.
|
|
* A trapped error is a GOSUB the program did not write, entered at a line
|
|
* boundary, and `RESUME` is its `RETURN`.
|
|
*
|
|
* **`ER` and `EL` are ordinary global variables here, spelled `ER#` and `EL#`.**
|
|
* A C128 exposes them as bare reserved names, which this dialect cannot do: an
|
|
* identifier carries its type in a suffix and a bare name is a *label*
|
|
* (`LABEL DONE` / `GOTO DONE`). Writing them into the global scope costs nothing,
|
|
* needs no new syntax, and `PRINT ER#` reads the way the rest of the language
|
|
* does. Recorded in TODO.md section 5.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
|
|
#include <akerror.h>
|
|
#include <akstdlib.h>
|
|
|
|
#include <akbasic/args.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "verbs.h"
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
do { \
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
} while ( 0 )
|
|
|
|
akerr_ErrorContext *akbasic_trap_set_error_variables(akbasic_Runtime *obj, int status, int64_t line)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Variable *variable = NULL;
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
"NULL runtime in set_error_variables");
|
|
/*
|
|
* The *global* scope, not the active one. A trapped error is reported from
|
|
* wherever it happened -- inside a GOSUB body, inside a loop -- and a
|
|
* handler that reads ER# has usually been entered through a scope of its
|
|
* own. akbasic_runtime_global() walks to the root, which is the only place
|
|
* both can see.
|
|
*/
|
|
PASS(errctx, akbasic_runtime_global(obj, "ER#", &variable));
|
|
PASS(errctx, akbasic_variable_set_integer(variable, (int64_t)status, zerosubscript, 1));
|
|
PASS(errctx, akbasic_runtime_global(obj, "EL#", &variable));
|
|
PASS(errctx, akbasic_variable_set_integer(variable, line, zerosubscript, 1));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ TRAP -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_trap(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *target = NULL;
|
|
akbasic_Value *value = NULL;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in TRAP");
|
|
|
|
target = (expr != NULL ? expr->right : NULL);
|
|
if ( target == NULL ) {
|
|
/* Bare TRAP disarms, which is how a C128 turns trapping back off. */
|
|
PASS(errctx, akbasic_runtime_disarm_interrupt(obj, AKBASIC_INTERRUPT_ERROR));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/*
|
|
* A label is taken by name and resolved when the trap fires, exactly as
|
|
* COLLISION's handler is: an error handler sits on a line normal flow does
|
|
* not fall into.
|
|
*/
|
|
if ( target->leaftype == AKBASIC_LEAF_IDENTIFIER &&
|
|
akbasic_leaf_first_subscript(target) == NULL ) {
|
|
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_ERROR,
|
|
0, target->identifier));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, target, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
|
"TRAP expected a line number or a label");
|
|
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_ERROR,
|
|
value->intval, NULL));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- RESUME -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *target = NULL;
|
|
akbasic_Value *value = NULL;
|
|
akbasic_Variable *variable = NULL;
|
|
int64_t zerosubscript[1] = { 0 };
|
|
int64_t resumeline = 0;
|
|
int cmp = 0;
|
|
bool isnext = false;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in RESUME");
|
|
FAIL_ZERO_RETURN(errctx, (obj->handlerenv != NULL), AKBASIC_ERR_STATE,
|
|
"RESUME outside the context of a TRAP handler");
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment == obj->handlerenv), AKBASIC_ERR_STATE,
|
|
"RESUME must return from the handler itself, not from a GOSUB inside it");
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
"RESUME from an orphaned environment");
|
|
|
|
/* Where the error happened, which EL# is holding. */
|
|
PASS(errctx, akbasic_runtime_global(obj, "EL#", &variable));
|
|
PASS(errctx, akbasic_variable_get_subscript(variable, zerosubscript, 1, &value));
|
|
resumeline = value->intval;
|
|
|
|
target = (expr != NULL ? expr->right : NULL);
|
|
if ( target != NULL && target->leaftype == AKBASIC_LEAF_COMMAND ) {
|
|
PASS(errctx, aksl_strcmp(target->identifier, "NEXT", &cmp));
|
|
isnext = (cmp == 0);
|
|
}
|
|
if ( target == NULL ) {
|
|
/*
|
|
* Bare RESUME retries the line that failed. That is a loop unless the
|
|
* handler fixed whatever was wrong, which is the program's business and
|
|
* is exactly what the verb is for.
|
|
*/
|
|
obj->environment->parent->nextline = resumeline;
|
|
} else if ( isnext ) {
|
|
/* RESUME NEXT carries on at the line after the one that failed. */
|
|
obj->environment->parent->nextline = resumeline + 1;
|
|
} else {
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, target, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_INTEGER),
|
|
AKBASIC_ERR_TYPE, "RESUME expected NEXT, a line number or a label");
|
|
obj->environment->parent->nextline = value->intval;
|
|
}
|
|
|
|
obj->handlerenv = NULL;
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- ERR -- */
|
|
|
|
akerr_ErrorContext *akbasic_fn_err(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Value *value = NULL;
|
|
akbasic_Value *out = NULL;
|
|
const char *name = NULL;
|
|
int64_t status = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in ERR");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "ERR expected an error number");
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"ERR expected a number");
|
|
status = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
|
? (int64_t)value->floatval : value->intval;
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
|
PASS(errctx, akbasic_value_zero(out));
|
|
out->valuetype = AKBASIC_TYPE_STRING;
|
|
|
|
/*
|
|
* The name libakerror registered for the code, which is where every status
|
|
* in this process is named -- akbasic's own band, libakgl's, and the errno
|
|
* values underneath. It answers for an unregistered code too, with "Unknown
|
|
* Error", so there is nothing to add here; the NULL guard is for a libakerror
|
|
* that ever decides otherwise.
|
|
*/
|
|
name = akerr_name_for_status((int)status, NULL);
|
|
PASS(errctx, aksl_strcpy(out->stringval, sizeof(out->stringval),
|
|
(name != NULL ? name : "Unknown Error")));
|
|
*dest = out;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|