2.0.0 makes the error pool and the status registry thread safe, and it is an ABI break carrying the soname to libakerror.so.2. The break is a quiet one: __akerr_last_ignored became thread-local and akerr_next_error() now returns a context that already holds a reference, so objects compiled against a 1.x header and linked against 2.x count every reference twice and never give a slot back. Nothing about that fails to link, which is exactly what a guard is for -- include/akbasic/error.h feature-tests AKERR_THREAD_SAFE instead of AKERR_FIRST_CONSUMER_STATUS, which 2.0.0 also still defines and which therefore no longer distinguishes anything. 2.0.1 is the release this band needed most. The default unhandled-error handler ended in exit(errctx->status), and a process exit status is one byte: AKBASIC_ERR_BASE is 512, and 512 truncates to 0, so an unhandled AKBASIC_ERR_SYNTAX reported success to anything watching $?. Every other code in the band came out as some unrelated error's number. akerr_exit() substitutes 125 for anything a byte cannot carry, and a probe raising AKBASIC_ERR_DEVICE through FINISH_NORETURN now exits 125 rather than 7. It was latent here rather than live -- src/main.c handles the context and returns EXIT_FAILURE, and every test with a top-level ATTEMPT carries a HANDLE_DEFAULT -- but "no caller relies on it today" is not a property a header can keep true. tests/version_check.c asserts the mapping and fails if AKBASIC_ERR_BASE ever stops truncating to zero, because that is the day this stops being about our base. Chapter 10 gains a threading section: libakerror is safe from any thread now, and this interpreter is not and has no lock anywhere in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
100 lines
5.0 KiB
C
100 lines
5.0 KiB
C
/**
|
|
* @file error.h
|
|
* @brief Declares akbasic's status codes and the registry claim for them.
|
|
*/
|
|
|
|
#ifndef _AKBASIC_ERROR_H_
|
|
#define _AKBASIC_ERROR_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
/*
|
|
* libakerror 2.0.0 is the floor, raised from 1.0.0 because 2.0.0 is an ABI break
|
|
* that a compile against the wrong header cannot survive quietly:
|
|
*
|
|
* - `__akerr_last_ignored` became thread-local. `IGNORE` expands at *our* call
|
|
* site, so our objects reference that symbol under whichever storage model
|
|
* the header on the include path declared.
|
|
* - `akerr_next_error()` now returns a context that already holds a reference,
|
|
* and `ENSURE_ERROR_READY` no longer increments. Objects compiled against a
|
|
* 1.x header count every reference twice and never give a slot back.
|
|
*
|
|
* Neither is a compile error. Both are a pool that leaks or a use-after-free,
|
|
* which is exactly the class of mismatch a guard is for.
|
|
*
|
|
* libakerror still publishes no version macro, so this feature-tests on
|
|
* AKERR_THREAD_SAFE, which 2.0.0 introduced and writes into the generated header
|
|
* as 1 or 0 -- so `#ifndef` is the right test and `#if` is not. It replaces the
|
|
* AKERR_FIRST_CONSUMER_STATUS test this carried for 1.0.0, which 2.0.0 also
|
|
* still defines and which therefore no longer distinguishes anything.
|
|
*/
|
|
#ifndef AKERR_THREAD_SAFE
|
|
#error "libakbasic requires libakerror >= 2.0.0: the akerror.h on the include path predates the thread-safe error pool. Rebuild and reinstall libakerror."
|
|
#endif
|
|
|
|
/*
|
|
* 2.0.1 additionally fixes an exit status this band made worse than most.
|
|
* `akerr_default_handler_unhandled_error()` used to end in `exit(errctx->status)`
|
|
* and a process exit status is one byte, so a consumer status came out truncated
|
|
* -- and **AKBASIC_ERR_BASE is 512, which truncates to 0**. An unhandled
|
|
* `AKBASIC_ERR_SYNTAX` reported success to whatever was watching `$?`. It exits
|
|
* AKERR_EXIT_STATUS_UNREPRESENTABLE (125) now.
|
|
*
|
|
* Nothing here calls `exit()` on a status -- `src/main.c` handles the context and
|
|
* returns EXIT_FAILURE, and every test installs a HANDLE_DEFAULT -- so the hazard
|
|
* was latent rather than live. It is guarded anyway, because "no caller relies on
|
|
* it today" is not a property a header can keep true.
|
|
*/
|
|
#ifndef AKERR_EXIT_STATUS_UNREPRESENTABLE
|
|
#error "libakbasic requires libakerror >= 2.0.1: an unhandled status in akbasic's band would exit 0. Rebuild and reinstall libakerror."
|
|
#endif
|
|
|
|
/*
|
|
* libakerror reserves 0-255 for the host's errno values and its own AKERR_*
|
|
* codes. libakgl claims 256-260. akbasic claims 512-767, leaving 261-511 as
|
|
* headroom for libakgl to grow into -- see MAINTENANCE.md for the coordinated
|
|
* range map, which is the only coordination there is: libakerror can enumerate
|
|
* its consumers no better than we can.
|
|
*
|
|
* These are absolute constants, never offsets from AKERR_LAST_ERRNO_VALUE, so a
|
|
* libc that grows an errno cannot move them. An enum rather than a chain of
|
|
* #defines so the values stay compile-time integer constants -- HANDLE expands
|
|
* to case labels, which require that.
|
|
*
|
|
* Add a code here and you must name it in akbasic_error_register(), or it prints
|
|
* as "Unknown Error" in every stack trace that carries it.
|
|
*/
|
|
#define AKBASIC_OWNER "akbasic"
|
|
|
|
enum {
|
|
AKBASIC_ERR_BASE = 512, /** Start of akbasic's reserved status range */
|
|
AKBASIC_ERR_SYNTAX = AKBASIC_ERR_BASE,
|
|
/** Parse-time grammar violation */
|
|
AKBASIC_ERR_TYPE, /** Incompatible types in an operation */
|
|
AKBASIC_ERR_UNDEFINED, /** Reference to an undefined verb, function or label */
|
|
AKBASIC_ERR_BOUNDS, /** Array subscript or pool index out of range */
|
|
AKBASIC_ERR_ENVIRONMENT, /** Environment pool exhausted or orphaned environment */
|
|
AKBASIC_ERR_VALUE, /** A value was malformed, truncated or unconvertible */
|
|
AKBASIC_ERR_STATE, /** A verb ran outside the block structure it requires */
|
|
AKBASIC_ERR_DEVICE, /** A verb needs a graphics, audio or input backend the runtime has not been given, or one that cannot do what was asked */
|
|
AKBASIC_ERR_LAST, /** One past the last real code. Not a status; tests/error_codes.c walks up to it so a new code is checked for a name without anybody remembering to widen the loop. */
|
|
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
|
|
};
|
|
|
|
/**
|
|
* @brief Claim the akbasic status band and register a name for every code in it.
|
|
*
|
|
* Reserves the whole 256-value range in one call -- libakerror treats only an
|
|
* identical reservation as a repeat, so a subset or superset raises -- and then
|
|
* names each code through the owned entry point. Call it before anything else in
|
|
* the library. Repeating the call is a no-op.
|
|
*
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
|
|
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
|
|
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_error_register(void);
|
|
|
|
#endif // _AKBASIC_ERROR_H_
|