Compile, link and run every example in the documentation

libakgl exports 157 functions and the only user-facing documentation was a FAQ
in README.md whose examples did not compile: two unbalanced `PASS()` calls, a
`sprite->frameids = [0, 1, 2, 3];` that is not C in any dialect, a stray `9`
inside a bitmask expression, an `int screenwidth = NULL`, and -- in the first
snippet a reader ever saw -- the exact `strncpy` call AGENTS.md forbids.

That is not a reader routing around typos. It is what happens to samples that
nothing executes. This is akbasic's documentation harness with the interpreter
taken out and the ability to link and run put in.

The contract is a set of fence info strings, so an example is ordinary markdown
that still highlights on the forge:

  ```c                  compiled with -fsyntax-only -Wall -Werror
  ```c wrap=NAME        the same, wrapped in tests/docs_preludes/NAME.pre/.post
  ```c run=NAME         linked against akgl and run headless
  ```c excerpt=PATH     must still appear verbatim in PATH
  ```c screenshot=NAME  also the source of docs/images/NAME.png
  ```json kind=KIND     loaded through the real akgl_*_load_json
  ```output             the exact stdout of the runnable block above it

`run=` is why this links at all: -fsyntax-only proves a call typechecks, not
that the startup order works or that an ATTEMPT block gives back what it took.
`json kind=` exists because the asset formats are documented in prose and read
by four loaders with nothing tying the two together -- util/assets/littleguy.json
is already invalid against the loader it ships with.

A fence with no info string is a hard error, and so is an unknown one. The
failure mode this exists to prevent is passing because it quietly ran nothing,
so an unannotated block is a missing decision rather than a default. Exit status
is the number of failed examples; 2 for a usage error, which is a different
thing and has to be distinguishable.

Proven to fail on all ten of its failure modes -- untagged fence, non-compiling
snippet, stale excerpt, orphan output block, unknown info string, run= output
mismatch, invalid JSON, missing figure, a -Werror warning, and a dangling
preload= -- because a check that has never failed has not been tested.

`-Werror` here although AKGL_WERROR stays off for the library: that option is
off so a new compiler's diagnostic cannot break a consumer's build, and a doc
snippet is not a consumer. A sample that warns is a sample that teaches the
warning.

Registered as `docs_examples` and `docs_screenshots`. No docs-path filter in CI,
deliberately: documentation goes stale because the code moved, not because
somebody edited a chapter.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 20:58:11 -04:00
parent 2766372f37
commit 8ac291d2dd
19 changed files with 2271 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
SUCCEED_RETURN(errctx);
}
/**
* Set in HANDLE_DEFAULT and read after FINISH. Returning from inside a HANDLE
* block leaves before RELEASE_ERROR and leaks the context's pool slot; AGENTS.md
* spells that out, and a documentation example is a bad place to teach it wrong.
*/
static int docs_failed = 0;
int main(void)
{
PREPARE_ERROR(errctx);
/*
* Set here as well as in the environment, so the program answers the same
* whether the harness ran it or a reader did.
*/
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT {
CATCH(errctx, akgl_error_init());
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_BREAK(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
FAIL_ZERO_BREAK(
errctx,
SDL_CreateWindowAndRenderer(
"net/aklabs/libakgl/docs", 320, 240, 0,
&akgl_window, &akgl_renderer->sdl_renderer),
AKGL_ERR_SDL,
"Couldn't create window/renderer: %s", SDL_GetError());
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
akgl_camera = &akgl_default_camera;
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = 320.0f;
akgl_camera->h = 240.0f;
CATCH(errctx, docs_example());
} CLEANUP {
if ( akgl_window != NULL ) {
SDL_DestroyWindow(akgl_window);
akgl_window = NULL;
}
SDL_Quit();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "the documented example failed");
docs_failed = 1;
/*
* FINISH_NORETURN rather than FINISH: FINISH expands a
* `return __err_context` that an int-returning function cannot compile,
* even where the branch is unreachable. src/main.c in akbasic and
* tools/docs_screenshot.c do the same and say so.
*/
} FINISH_NORETURN(errctx);
return docs_failed;
}

View File

@@ -0,0 +1,24 @@
/*
* akglapp -- a complete program, brought up headless and run.
*
* This is the prelude behind `c run=akglapp`, and it is the reason the harness
* links at all. -fsyntax-only proves a call typechecks; it does not prove the
* startup order works, that the sprite loads, or that an ATTEMPT block gives
* back what it took. A run= block proves those, and an `output` block below it
* pins what the reader will actually see.
*
* The block supplies the body. Anything it prints on stdout is the example's
* output; libakgl's own registry chatter goes to stderr and is not compared.
*
* The startup here is the same sequence tests/sprite.c and tools/docs_screenshot.c
* use, and it is the sequence chapter 07 documents: error registry, renderer
* backend, SDL, window, bind, heap, registries, camera. A host that owns its own
* window does exactly this rather than calling akgl_game_init().
*/
#include <docs_prelude.h>
akerr_ErrorContext AKERR_NOIGNORE *docs_example(void);
akerr_ErrorContext *docs_example(void)
{
PREPARE_ERROR(errctx);

View File

@@ -0,0 +1,2 @@
SUCCEED_RETURN(errctx);
}

View File

@@ -0,0 +1,22 @@
/*
* akglbody -- the inside of a function that returns an error context.
*
* The overwhelmingly common shape of a libakgl example: a few locals, an
* ATTEMPT block, and calls that are checked. Written out in full, every one of
* those blocks would open with the same four lines and close with the same two,
* and a reader would learn the protocol six times instead of the call once.
*
* The block supplies everything between `PREPARE_ERROR` and the closing brace.
* `errctx` is the local error context, per the house parameter names.
*
* Nothing here declares an akgl_ function. A prelude that did would let an
* example calling it with the wrong arguments compile, which is the one thing
* this whole harness exists to catch.
*/
#include <docs_prelude.h>
akerr_ErrorContext AKERR_NOIGNORE *docs_example(void);
akerr_ErrorContext *docs_example(void)
{
PREPARE_ERROR(errctx);

View File

@@ -0,0 +1,11 @@
/*
* akglfile -- file scope.
*
* The block is a run of top-level declarations: a movementlogicfunc, a backend
* vtable, a static table. All this prelude supplies is the include set, so the
* chapter does not have to spend six lines on `#include` before showing the one
* function it is about.
*
* There is no akglfile.post. Nothing has to be closed.
*/
#include <docs_prelude.h>

View File

@@ -0,0 +1,2 @@
SUCCEED_RETURN(errctx);
}

View File

@@ -0,0 +1,17 @@
/*
* akglframe -- one frame of drawing, for a figure.
*
* `c screenshot=NAME` blocks are wrapped in this and linked against
* tools/docs_screenshot.c, which owns main(), brings libakgl up against an
* offscreen target of the stated size, calls docs_frame(), reads the target
* back and writes docs/images/NAME.png.
*
* The block draws and returns. It must print nothing: tools/docs_screenshots.sh
* fails a figure whose program wrote to stdout, because an image of a blank
* screen is worse than no image at all.
*/
#include <docs_prelude.h>
akerr_ErrorContext *docs_frame(void)
{
PREPARE_ERROR(errctx);

View File

@@ -0,0 +1,64 @@
/**
* @file docs_prelude.h
* @brief The include set every `c wrap=`, `c run=` and `c screenshot=` block gets.
*
* The four preludes in this directory all begin with this file, so the list of
* headers a wrapped example can rely on lives in exactly one place. It is not
* reachable from a `c` block with no `wrap=`: an unwrapped block is a whole
* translation unit and has to show its own `#include` lines, which is usually
* what a chapter wants a reader to see.
*
* Every public header, deliberately. The `headers` suite already proves each one
* is self-contained, so there is no ordering to get right, and an example that
* calls one subsystem while illustrating another does not have to grow an
* include line that is noise in the chapter.
*
* `docs_frame()` is declared here rather than in tools/docs_screenshot.c so the
* host and the akglframe prelude cannot drift apart on the signature.
*/
#ifndef _AKGL_DOCS_PRELUDE_H_
#define _AKGL_DOCS_PRELUDE_H_
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/assets.h>
#include <akgl/audio.h>
#include <akgl/character.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/iterator.h>
#include <akgl/json_helpers.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/staticstring.h>
#include <akgl/text.h>
#include <akgl/tilemap.h>
#include <akgl/types.h>
#include <akgl/util.h>
/**
* @brief One frame of drawing, supplied by a `c screenshot=NAME` block.
*
* tools/docs_screenshot.c brings libakgl up against an offscreen target, calls
* this, and writes what it drew to `docs/images/NAME.png`. The body is the
* chapter's own listing, wrapped by tests/docs_preludes/akglframe.pre.
*/
akerr_ErrorContext AKERR_NOIGNORE *docs_frame(void);
#endif // _AKGL_DOCS_PRELUDE_H_