Files
libakgl/docs/04-errors.md

272 lines
17 KiB
Markdown
Raw Normal View History

Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
# 04. Errors and status codes
Every one of libakgl's exported functions returns `akerr_ErrorContext AKERR_NOIGNORE *`.
You cannot read a single example in this manual until you can read that return value, which
is why this chapter comes before any subsystem.
## The protocol is libakerror's
`ATTEMPT` / `CLEANUP` / `PROCESS` / `HANDLE` / `HANDLE_GROUP` / `FINISH`, and the `PASS`,
`CATCH`, `IGNORE`, `FAIL_*` and `SUCCEED_RETURN` macros, all belong to **libakerror**.
They are documented by the project that owns them, in `deps/libakerror/README.md`
"Library Architecture", "Lifecycle of an error in the AKError library", and "Exit status".
This manual does not restate them, because a copy here would be wrong the day libakerror
changes and nothing in this repository would notice.
What is genuinely libakgl's, and is written down nowhere else, is **which statuses these
Bump to 0.8.0, close Target 12, and record what is still missing Collision changed the collide slot's signature and grew four public structs, so this is an ABI break and the soname moves to libakgl.so.0.8. The manual's counts move with it: 195 functions across 23 headers, 183 of which return an error context. Target 12 -- "collision for 256 actors under 2 ms without the caller writing a broad phase" -- is met. A new benchmark times the whole step with a world attached, which is the number the target is actually about rather than a narrowphase call in isolation: 15.4 us at 64 actors, and 54.1 us at 256 in a purpose-built -DAKGL_MAX_HEAP_ACTOR=256 configuration. Over that 4x range the step grew 3.5x while the all-pairs control grew 15.6x, which is the n-squared the index exists to avoid. Plan item 7 is rewritten to record what shipped and why both partitioners exist; the Construct and Phaser citations stay. Three new TODO entries. Actor rotation, scoped to the five places it touches and the one place it does not -- collision_support is written so rotating `dir` in and the answer back is the whole narrowphase change, and rotational *response* is explicitly a different piece of work. The swept narrowphase FLAG_BULLET reserves, with the tunnelling arithmetic written out so a game can check its own numbers against 1280 px/s. And the ccd arena being single-threaded and sized by measurement at 7,264 bytes per GJK/EPA box pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 07:53:30 -04:00
195 functions raise and what each one means here**. That is the three tables below.
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
The house rules for *writing* code against the protocol — never a `*_RETURN` inside an
`ATTEMPT`, never a bare `return` out of a `HANDLE`, `CLEANUP` before `PROCESS`, an
`ATTEMPT` inside the loop rather than a `CATCH` inside one — are in `AGENTS.md` under
"Error-Handling Protocol", and two of them are enforced by the `error_protocol` test.
## Table 1 — libakgl's own status codes
libakerror reserves statuses 0255 for the host's `errno` values and its own `AKERR_*`
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
libakgl claims a band of six starting there, under the owner string `"libakgl"`:
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
```c excerpt=include/akgl/error.h
#define AKGL_ERR_OWNER "libakgl"
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
```
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 6`, one past the last
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
arm — it would catch nothing, and if a seventh real code is ever added it would silently
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
start catching that instead.
| Code | Value | Means | Raised by | What the caller does |
|---|---|---|---|---|
| `AKGL_ERR_SDL` | 256 | An SDL call failed; the message carries `SDL_GetError()` | Around forty sites — anything touching a window, texture, renderer, mixer or the state mutex | Usually fatal at startup. Check the driver and the asset path |
| `AKGL_ERR_REGISTRY` | 257 | A name lookup against a registry failed | **One site only**: `akgl_controller_default`, when `actorname` is not in `AKGL_REGISTRY_ACTOR` | Bind the control map *after* the actor is initialized |
| `AKGL_ERR_HEAP` | 258 | A pool has no free slot | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** See [Chapter 5](05-the-heap.md) before raising `AKGL_MAX_HEAP_*` |
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
| `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) |
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
six: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
`"Logic Interrupt"`, `"Collision Error"`. Those names are what a stack trace prints.
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
Two rows deserve more than a cell.
**`AKGL_ERR_REGISTRY` is rarer than its name suggests.** The obvious candidates do not
raise it. `akgl_actor_initialize` raises `AKERR_KEY` when the registry write fails, and
`akgl_actor_set_character` raises `AKERR_NULLPOINTER` when the named character is not in
`AKGL_REGISTRY_CHARACTER`. If you are writing a `HANDLE` arm for "that name was not
registered", `AKERR_NULLPOINTER` and `AKERR_KEY` are the statuses you will actually see.
**`AKGL_ERR_LOGICINTERRUPT` only works from one place.** `akgl_physics_simulate` wraps each
actor's step in an `ATTEMPT` and calls `actor->movementlogicfunc` through `CATCH`, with a
`HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT)` arm that does nothing — so a raise from your
movement logic skips that actor's gravity, drag and move for this step and the loop carries
on to the next actor. The backend's own `gravity` and `move` are called through `PASS` in
the same block, and `PASS` returns out of the function. **A backend hook that raises
`AKGL_ERR_LOGICINTERRUPT` aborts the whole physics step**, leaving every remaining actor
unsimulated and `gravity_time` unadvanced. Raise it from `movementlogicfunc` and nowhere
else.
## Table 2 — libakerror statuses libakgl raises, and what they mean here
The statuses are libakerror's; their libakgl meaning is not. This table was built by
grepping every `FAIL_*` and `HANDLE*` site in `src/`, not by reading header prose.
| Status | What it means when a libakgl function raises it |
|---|---|
| `AKERR_NULLPOINTER` | By a wide margin the most common — roughly six of every seven raise sites in `src/`. A required pointer argument was `NULL`*or* a required field was empty (`akgl_game.name`/`.version`/`.uri`), *or* a name was looked up and not found. `akgl_actor_set_character` and `akgl_get_property` both use it for "absent", and `registry_create` uses it for "SDL could not allocate the property set" |
| `AKERR_KEY` | **A key is absent, or a registry write was refused.** A missing JSON object member (every `akgl_get_json_*_value`), a character with no sprite for a state, a physics backend name that is neither `null` nor `arcade`, a failed `SDL_SetPointerProperty` in `akgl_actor_initialize`. Frequently a `HANDLE` arm rather than a failure |
| `AKERR_TYPE` | **A JSON value was present but the wrong type.** All of `src/json_helpers.c` and `akgl_get_json_tilemap_property`. This is the status that separates "you did not write that key" (`AKERR_KEY`) from "you wrote it as a string and it wants a number" |
| `AKERR_OUTOFBOUNDS` | A value did not fit its destination, or an index ran off the end. `aksl_strncpy` truncation, an array index past the end of a JSON array (`akgl_get_json_array_index_*`), a ninth child on an actor, a tile past `AKGL_TILEMAP_MAX_*`, a control map id out of range |
| `AKERR_VALUE` | A value parsed but was not usable: a version string that is not semver (`akgl_game_load_versioncmp`), two surfaces that differ (`akgl_compare_sdl_surfaces`), a zero-sized destination or an overlapping copy from the `aksl_` wrappers |
| `AKERR_RELATIONSHIP` | **One site**: `akgl_actor_add_child`, when the child already has a parent. Detach it first |
| `AKERR_API` | Two distinct meanings, so read the message. (1) **The function is not implemented**`akgl_render_2d_draw_mesh` does `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and it is reached by an ordinary-looking call. (2) **A savegame does not match this build**`akgl_game_load` and `akgl_game_load_versioncmp` |
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
| `AKERR_IO` | A read or write failed, or a savegame had trailing data after its name tables. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists |
| `AKERR_EOF` | End of input. Sometimes the *desired* outcome: `require_at_eof` in `src/game.c` handles it and treats anything else as a failure |
| `AKERR_INDEX` | **libakgl never raises this.** It appears once, as a `HANDLE_GROUP` arm in `akgl_get_json_with_default`, so that a caller whose *own* code raises it still gets a default. An out-of-range JSON array index arrives as `AKERR_OUTOFBOUNDS` |
Statuses raised by the libraries underneath also reach you unchanged. `aksl_fopen` reports
`ENOENT` and `EACCES` as themselves; `aksl_fclose` reports `ENOSPC` and `EDQUOT`.
`akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`,
`AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`.
[Chapter 22](22-appendix-limits.md) has the per-function cross-reference.
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
## Table 3 — the exit status trap
This is arithmetic, and it is silent.
| You write | Wait status the shell sees | Why |
|---|---|---|
| `exit(AKGL_ERR_SDL)` | **0 — a clean run** | An exit status is one byte. `AKGL_ERR_SDL` is 256, and 256 & 0xFF is 0 |
| `akerr_exit(status)` | 0 → 0, 1255 → itself, anything else → 125 | 125 is `AKERR_EXIT_STATUS_UNREPRESENTABLE` |
Every suite in `tests/` once reported success on the single most common failure a library
built on SDL can have — a renderer that would not come up. `tests/character.c` aborted at
its second of four tests and was green until 0.5.0.
**You do not normally need to do anything about this.** libakerror's default unhandled-error
handler already calls `akerr_exit()`, so a `main` that ends in `FINISH_NORETURN` exits
correctly. The trap is only live if you replace libakerror's
`akerr_handler_unhandled_error` pointer with your own handler, or call `exit(3)` on a
status yourself. If you do either, call `akerr_exit()`.
## Four traps that are libakgl's, not libakerror's
### `akgl_error_init()` must run before anything that can raise
`akgl_error_init` claims the status band *and* registers the six names. A code raised
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
before it runs has no name in the registry, so every stack trace carrying it prints
**"Unknown Error"** instead of "Heap Error" — and the status number, 258, means nothing to
a reader.
`akgl_game_init` calls it first, before anything else that can fail — that is why the
mutex creation and the `akgl_game.name` checks sit *after* it. A host with its own startup
path, such as one binding an existing renderer with `akgl_render_2d_bind` instead of
calling `akgl_game_init`, must call `akgl_error_init` itself. Repeat calls are a no-op, so
a program that cannot order its initialization precisely may simply call it more than once.
### The SDL callbacks end in `FINISH_NORETURN`, and that exits the process
`SDL_EnumerateProperties` takes a `void`-returning callback, so a failure inside one has
nowhere to go. All six of libakgl's therefore end in `FINISH_NORETURN`, which logs the
trace and hands the context to libakerror's unhandled-error handler — **and the default
handler exits the process.** They are `akgl_registry_iterate_actor` (`src/actor.c`),
`akgl_character_state_sprites_iterate` (`src/character.c`), and the four savegame name
iterators in `src/game.c`.
A reader meets this the first time a sprite name is wrong: an actor's `renderfunc` raises
`AKERR_KEY`, the callback cannot return it, and the game exits mid-frame with a stack trace
instead of returning an error you could have handled.
Two consequences worth planning for:
- A write error part-way through `akgl_game_save` terminates the game rather than reaching
`akgl_game_save`'s return value. `game.h` says so on `akgl_game_save_actors`.
- If you want a different outcome, point libakerror's `akerr_handler_unhandled_error`
function pointer at your own handler — and call `akerr_exit()` from it, per Table 3.
Note that `akgl_game_update` does **not** go through the callback. It walks
`akgl_heap_actors` directly and propagates through `PASS`, so an error from an actor's
`updatefunc` does reach you. The callback path is the render sweep and the savegame writer.
### `akgl_get_json_with_default` hands back the context you gave it
This is the one libakgl function whose ownership rule you have to know before you write
your first loader. It takes an incoming `akerr_ErrorContext *`, and when it does not handle
that status it returns **the same pointer**, because its `FINISH(e, true)` passes `e` up.
So the context has exactly one owner at a time, and it is never both yours and its. A
`CLEANUP` block that releases your original pointer *as well as* releasing what the call
returned releases the same slot twice, and a double-released context corrupts the failure
instead of reporting it. That is not hypothetical: it is how the first draft of the
`AKERR_OUTOFBOUNDS` test in `tests/json_helpers.c` passed against the unfixed library.
`AGENTS.md` documents the shape under "Testing Guidelines"; `TODO.md` item 18 records the
history.
The correct shape: take the result into a local, `NULL` your own pointer immediately, and
decide from the local.
It defaults on three statuses — `AKERR_KEY`, `AKERR_OUTOFBOUNDS` and `AKERR_INDEX` — so
both a missing object member and a short array give you the default. It does **not** default
on `AKERR_TYPE`: a key that is present with the wrong type is a mistake in the document, not
an absent value, and it propagates.
### libakerror 2.0.1 is a hard floor, checked at compile time
`include/akgl/error.h` carries two `#error` feature tests, because libakerror publishes no
version macro and each probes the narrowest thing that a required release introduced:
- `AKERR_FIRST_CONSUMER_STATUS` — added in 1.0.0 with the private status registry. Without
it, `AKGL_ERR_BASE` does not exist and the failure lands much later, inside `src/heap.c`.
- `AKERR_EXIT_STATUS_UNREPRESENTABLE` — added in 2.0.1 with `akerr_exit()`.
Both failures are an *installed header* being stale, not a build-tree problem. 2.0.0 is an
ABI break — `akerr_next_error()` returns a context that already holds its reference, and
`__akerr_last_ignored` is thread-local — and both expand at libakgl's own call sites through
`IGNORE` and the `FAIL_*` macros. The soname is `libakerror.so.2`, so the *libraries* cannot
be mixed by accident; the header can. Rebuild and reinstall libakerror.
## One worked example
A loader reading an optional integer out of a JSON document. It is the smallest piece of
real libakgl code that exercises both halves of the chapter: a status that means "absent
and that is fine", and the ownership rule on the function that turns it into a default.
```c
#include <jansson.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/json_helpers.h>
/* Read an optional integer out of a loaded document, defaulting to 4. */
akerr_ErrorContext AKERR_NOIGNORE *read_speed(json_t *doc, int *dest)
{
akerr_ErrorContext *keyerr = NULL;
akerr_ErrorContext *unhandled = NULL;
int def = 4;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, doc, AKERR_NULLPOINTER, "NULL document");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination");
keyerr = akgl_get_json_integer_value(doc, "speed", dest);
unhandled = akgl_get_json_with_default(keyerr, &def, dest, sizeof(int));
keyerr = NULL;
if ( unhandled != NULL ) {
return unhandled;
}
SUCCEED_RETURN(errctx);
}
int main(void)
{
json_t *doc = NULL;
int speed = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
doc = json_pack("{s:i}", "width", 32);
CATCH(errctx, read_speed(doc, &speed));
} CLEANUP {
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
```
Four things in there are libakgl-specific and worth naming:
1. **`akgl_error_init()` is the first call**, before anything that can raise.
2. **`keyerr = NULL;` on the line after the call.** Ownership passed either way — handled
or not — and the local is the only surviving reference. Nothing else in the function may
touch `keyerr` again.
3. **A `speed` key that is absent is not an error.** `akgl_get_json_integer_value` raises
`AKERR_KEY`, `akgl_get_json_with_default` handles it and writes 4. A `speed` key that is
present as a string *is* an error: `AKERR_TYPE` is not defaulted and comes back as
`unhandled`.
4. **`main` ends in `FINISH_NORETURN`.** That is what gets you `akerr_exit()` and a
truthful exit status without writing the trap from Table 3.
## Where to go next
- [Chapter 5](05-the-heap.md) — `AKGL_ERR_HEAP`, and why it is almost always a missing
release.
- [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and
`AKERR_NULLPOINTER`.
- [Chapter 22](22-appendix-limits.md) — which function raises what.
Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
- `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion.
- The generated Doxygen reference — every function's own `@throws` list.