Reach hardware through backend records, and take the time from the host
Groups G, I and E are unblocked but cannot be written yet: the core library is free of SDL and builds with no libakgl present, so a graphics verb cannot call akgl_draw_* and a sound verb cannot call akgl_audio_*. This adds what they call instead. Three records of function pointers -- akbasic_GraphicsBackend, _AudioBackend and _InputBackend -- in the same shape as akbasic_TextSink, and the same shape libakgl uses for akgl_RenderBackend. akbasic_runtime_set_devices() attaches any subset; all three may be NULL and that is the standalone driver's normal state, so a runtime with no backends still comes up and still prints. A verb that needs one it was not given raises the new AKBASIC_ERR_DEVICE rather than dereferencing a NULL vtable. Two decisions worth stating. The graphics record has no circle entry point: BASIC 7.0's CIRCLE takes two radii, an arc range, a rotation and a degree increment, which makes it a polygon by definition, so it will be built from line calls rather than from akgl_draw_circle. And coordinates are double rather than an integer pixel address, because SCALE makes them fractional and rounding at each verb rather than once at the backend accumulates drift along a polyline. akbasic_runtime_settime() is how SOUND, PLAY and TEMPO get a clock without the library reading one. Section 1.6 forbids blocking or owning a loop, so the caller that owns the frame owns the time -- which is what libakgl already does, since akgl_actor_logic_changeframe takes curtimems as an argument. Left unset it is zero and every duration expires immediately: audible, but never a hang. AKBASIC_ERR_LAST is a sentinel rather than a status, so tests/error_codes.c walks every code looking for an unnamed one without anybody remembering to widen the loop when a code is added. tests/mockdevice.h records every backend call as a formatted line. The graphics and audio verbs emit nothing a golden file can compare, so that log is where their assertions have to live -- and since it needs no SDL, the whole of groups G, I and E stays testable in the default build. 62/62 ctest, clean under -Wall -Wextra, doxygen clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
@@ -18,8 +18,11 @@
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/environment.h>
|
||||
#include <akbasic/grammar.h>
|
||||
#include <akbasic/graphics.h>
|
||||
#include <akbasic/input.h>
|
||||
#include <akbasic/sink.h>
|
||||
#include <akbasic/types.h>
|
||||
#include <akbasic/value.h>
|
||||
@@ -84,6 +87,27 @@ typedef struct akbasic_Runtime
|
||||
akbasic_Environment *environment;
|
||||
akbasic_TextSink *sink;
|
||||
|
||||
/*
|
||||
* The device backends, any of which may be NULL. The standalone driver
|
||||
* supplies none of them, so a PRINT-only program must keep working; every
|
||||
* verb that needs one refuses with AKBASIC_ERR_DEVICE instead.
|
||||
*/
|
||||
akbasic_GraphicsBackend *graphics;
|
||||
akbasic_AudioBackend *audio;
|
||||
akbasic_InputBackend *input;
|
||||
|
||||
/*
|
||||
* The host's clock, in milliseconds, as of its last akbasic_runtime_settime()
|
||||
* call. The interpreter does not read a clock: it owns no loop and must not
|
||||
* block, so the caller that owns the frame owns the time. libakgl does the
|
||||
* same thing -- akgl_actor_logic_changeframe() takes curtimems as an
|
||||
* argument rather than asking the system for it.
|
||||
*
|
||||
* Left at zero, every duration expires on the step after it starts. That is
|
||||
* wrong but never a hang, which is the right way for it to fail.
|
||||
*/
|
||||
int64_t timems;
|
||||
|
||||
/* REPL line assembly */
|
||||
char userline[AKBASIC_MAX_LINE_LENGTH];
|
||||
|
||||
@@ -104,6 +128,43 @@ typedef struct akbasic_Runtime
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);
|
||||
|
||||
/**
|
||||
* @brief Attach the device backends a host is willing to lend the interpreter.
|
||||
*
|
||||
* Any argument may be NULL, which is how a host withholds a capability: a script
|
||||
* given no audio backend gets an error from SOUND rather than silence. Call it
|
||||
* after akbasic_runtime_init() and before akbasic_runtime_start(); calling it
|
||||
* again mid-run is allowed and takes effect on the next verb.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param graphics Where DRAW, BOX, CIRCLE and PAINT land; may be NULL.
|
||||
* @param audio Where SOUND, PLAY and VOL land; may be NULL.
|
||||
* @param input Where GET and GETKEY read; may be NULL.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input);
|
||||
|
||||
/**
|
||||
* @brief Tell the interpreter what time the host thinks it is.
|
||||
*
|
||||
* SOUND durations, PLAY note lengths and TEMPO are all time-based, and section
|
||||
* 1.6 forbids the library blocking or reading a clock of its own. A host calls
|
||||
* this once a frame before akbasic_runtime_run(); the standalone driver calls it
|
||||
* from a monotonic clock each step.
|
||||
*
|
||||
* Time is only ever compared, never differenced against a wall clock, so any
|
||||
* monotonic millisecond source will do. It is not required to advance, and a
|
||||
* host that never calls this leaves it at zero -- durations then expire
|
||||
* immediately, which is audible but never a hang.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param timems The host's current time in milliseconds.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems);
|
||||
|
||||
/**
|
||||
* @brief Reset the per-line state without disturbing the program or variables.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
|
||||
Reference in New Issue
Block a user