Finish the language: every remaining verb group, and the defects that blocked them

Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.

Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.

Writing the tests turned up eight defects nobody had listed. Seven are fixed:

  IF A = 2 THEN was a parse error; only == worked
  IF ... AND ... was a parse error, because a condition parsed as one relation
  IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
  EXIT before any NEXT restarted the program and exhausted the variable pool
  READ never found a DATA line above it, and swallowed the lines between
  PRINT 2 + 2 at the prompt was filed as program text instead of answering
  a short read discarded its bytes, so COPY produced empty files
  every verb taking an argument list said "peek() returned nil token!" on none

The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.

Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.

Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.

94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 21:50:37 -04:00
parent 3aade4947a
commit 9845e77a5c
87 changed files with 11024 additions and 325 deletions

View File

@@ -29,28 +29,40 @@
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/renderer.h>
#include <akgl/sprite.h>
#include <akgl/version.h>
/*
* Everything in this target now depends on APIs that arrived in libakgl 0.3.0:
* akgl_render_bind2d(), akgl_audio_sweep(), and akgl_Keystroke with
* akgl_controller_poll_keystroke(). Refused here rather than at link time,
* because a missing symbol names a function and this names the release.
* The API floor is 0.3.0: akgl_render_bind2d(), akgl_audio_sweep(), and
* akgl_Keystroke with akgl_controller_poll_keystroke() all arrived there, and
* nothing in this target uses anything newer. The floor asserted below is
* 0.4.0 anyway, and the difference is ABI rather than API.
*
* The soname carries MAJOR.MINOR while the major is 0, so 0.2 and 0.3 are
* different ABIs and a mismatch is normally caught at load. This catches the
* case the soname cannot: a build against 0.2 headers that happens to find a
* 0.3 library, or the reverse.
* The soname carries MAJOR.MINOR while the major is 0, so 0.3 and 0.4 are
* different ABIs *by declaration* -- libakgl's versioning policy says a 0.x
* minor bump may break, and the soname is built to match. 0.4.0 in fact changed
* no public struct: it is leak and overread fixes inside src/. The floor moves
* anyway, because the alternative is deciding for ourselves which of libakgl's
* minor releases were really compatible, and that is exactly the judgement the
* soname exists to take away from us.
*
* What it catches is the case the soname cannot: a build against 0.3 headers
* that happens to find a 0.4 library, or the reverse. Refused here rather than
* at link time, because a missing symbol names a function and this names the
* release.
*/
#if !AKGL_VERSION_AT_LEAST(0, 3, 0)
#error "akbasic's libakgl adaptors require libakgl 0.3.0 or later"
#if !AKGL_VERSION_AT_LEAST(0, 4, 0)
#error "akbasic's libakgl adaptors require libakgl 0.4.0 or later"
#endif
#include <akbasic/audio.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
/**
* @brief One full cursor blink in milliseconds, half on and half off.
@@ -106,6 +118,15 @@ typedef struct
int y;
int width; /* pixel size of the text area */
int height;
/*
* The whole drawable area, kept so WINDOW can grow back out to it. Without
* it a window could only ever shrink, since `x`/`width` have by then been
* overwritten with the current window's.
*/
int fullx;
int fully;
int fullwidth;
int fullheight;
int cellw; /* one character cell, measured from the font */
int cellh;
int columns; /* the character grid the cell size works out to */
@@ -156,6 +177,35 @@ typedef struct
int shapecount;
} akbasic_AkglGraphics;
/**
* @brief State for the libakgl-backed sprite backend.
*
* One libakgl actor per BASIC sprite, plus the sheet, sprite and character each
* actor needs -- built by hand rather than loaded from a sprite document, since
* a Commodore sprite has no animation, no frame list and no state map to
* describe. Everything is pooled by libakgl; what is held here are the pointers
* and the one texture per slot that this backend owns and must destroy.
*
* `graphics` is the graphics backend's state, borrowed so `SPRSAV A$, n` can
* resolve a handle SSHAPE minted. The two devices are separate records on
* purpose -- a host may supply one and withhold the other -- so this is a
* pointer that may be NULL rather than an assumption that both exist.
*/
typedef struct
{
akgl_RenderBackend *renderer;
akbasic_AkglGraphics *graphics;
akgl_SpriteSheet *sheets[AKBASIC_MAX_SPRITES];
akgl_Sprite *sprites[AKBASIC_MAX_SPRITES];
akgl_Character *characters[AKBASIC_MAX_SPRITES];
akgl_Actor *actors[AKBASIC_MAX_SPRITES];
/** Per-axis expansion, which an actor's single `scale` cannot carry. */
bool xexpand[AKBASIC_MAX_SPRITES];
bool yexpand[AKBASIC_MAX_SPRITES];
} akbasic_AkglSprites;
/**
* @brief Point a text sink at a renderer and a font the host already has.
*
@@ -235,6 +285,53 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_set_pump(akbasic_TextSink *
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer);
/**
* @brief Point a sprite backend at a renderer the host already has.
*
* Requires akgl_heap_init() and akgl_registry_init() to have run -- every
* akgl_*_initialize ends in a registry write -- and requires the global `camera`
* to point somewhere, because akgl_actor_render() dereferences it without
* checking. akgl_game_init() does all three; a host that skips it, which is
* every host embedding this interpreter, does them itself.
* src/frontend_akgl.c is the worked example.
*
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the sprite pool; must outlive the backend.
* @param renderer The renderer the host already initialized; not created here.
* @param graphics The graphics backend's state, so SPRSAV can resolve an SSHAPE handle; may be NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj`, `state` or `renderer` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic_AkglSprites *state, akgl_RenderBackend *renderer, akbasic_AkglGraphics *graphics);
/**
* @brief Draw every defined, enabled sprite.
*
* Separate from the verbs for the same reason akbasic_sink_akgl_render() is: the
* interpreter does not own the frame. A host calls this when it is drawing.
*
* The actors are swept directly rather than through akgl_registry_iterate_actor(),
* which ends in FINISH_NORETURN -- an unhandled error inside it terminates the
* process, and goal 3 forbids anything in this library doing that. Sweeping our
* own eight also leaves a host's own actors alone.
*
* @param obj The backend to draw.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL or carries no state.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_akgl_render(akbasic_SpriteBackend *obj);
/**
* @brief Release every texture the sprite backend created.
*
* The actors, sprites, sheets and characters are libakgl's pooled objects and go
* back with akgl_heap_init(); the SDL textures behind the sheets are this
* backend's own and are not anybody else's to free.
*
* @param obj The backend to tear down. NULL is not an error.
*/
void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj);
/**
* @brief Wire an audio backend to libakgl's tone generator.
*

46
include/akbasic/args.h Normal file
View File

@@ -0,0 +1,46 @@
/**
* @file args.h
* @brief Collecting a verb's positional arguments, shared by the verb files.
*
* Every device verb group -- graphics, audio, and now sprites -- takes a short
* positional list with optional trailing members, so every handler wants the
* same two things: how many arguments arrived, and their numeric values. This
* was written twice before it was written here. src/runtime_audio.c's copy
* carried the rule for when to stop copying it:
*
* "If a third group wants it, that is the point at which it earns a home of
* its own."
*
* Group H is the third group.
*/
#ifndef _AKBASIC_ARGS_H_
#define _AKBASIC_ARGS_H_
#include <akerror.h>
#include <akbasic/grammar.h>
struct akbasic_Runtime;
/**
* @brief Collect a verb's arguments into an array, evaluating each one.
*
* Strings are refused here rather than in each verb. A verb that does take one
* -- SSHAPE, GSHAPE, SPRSAV -- reads its leaf directly instead, because what it
* wants from that argument is not a number and never was.
*
* @param obj Runtime to evaluate against.
* @param expr The command leaf.
* @param verb Name to use in a diagnostic; a verb that refuses has to say which.
* @param values Where to put the evaluated arguments.
* @param max Capacity of @p values.
* @param count Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument but `verb` is NULL.
* @throws AKBASIC_ERR_SYNTAX When more than `max` arguments were given.
* @throws AKBASIC_ERR_TYPE When an argument evaluated to a string.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_args_numbers(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, double *values, int max, int *count);
#endif // _AKBASIC_ARGS_H_

73
include/akbasic/console.h Normal file
View File

@@ -0,0 +1,73 @@
/**
* @file console.h
* @brief The group E console state: SLEEP, WAIT, KEY and the TI clock.
*
* All of it is the program's state rather than the device's, which is why it
* lives on the runtime beside the graphics, audio and sprite state. A host that
* swaps one output device for another does not expect a sleeping program to wake
* up or its function keys to be forgotten.
*/
#ifndef _AKBASIC_CONSOLE_H_
#define _AKBASIC_CONSOLE_H_
#include <akerror.h>
#include <akbasic/types.h>
/** @brief Function keys `KEY` can define. A C128 has eight. */
#define AKBASIC_MAX_FUNCTION_KEYS 8
/** @brief The state SLEEP, WAIT and KEY keep. */
typedef struct
{
bool sleeping; /** SLEEP is holding the step loop */
int64_t sleepuntilms; /** ...until the host clock reaches this */
bool waiting; /** WAIT is holding the step loop */
uintptr_t waitaddress; /** the byte it is polling */
uint8_t waitmask;
uint8_t waitxor;
char keys[AKBASIC_MAX_FUNCTION_KEYS][AKBASIC_MAX_STRING_LENGTH];
} akbasic_ConsoleState;
struct akbasic_Runtime;
/**
* @brief Reset the console state: nothing sleeping, nothing waiting, no macros.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_console_state_init(akbasic_ConsoleState *obj);
/**
* @brief Answer whether a SLEEP or a WAIT is still holding the step loop.
*
* Called once per akbasic_runtime_step(), beside akbasic_input_service() and for
* the same reason: waiting must not mean blocking. The step still returns, it
* simply does not advance the program.
*
* @param obj Object to initialize, inspect, or modify.
* @param blocked Output destination populated by the function; true while held.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either argument is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_console_service(struct akbasic_Runtime *obj, bool *blocked);
/**
* @brief Refresh `TI#` and `TI$` from the host's clock.
*
* Called once per step. They are ordinary global variables rather than
* pseudo-variables, because this dialect has no bare variable names -- see
* TODO.md section 5.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When no variable slot is free.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_console_update_clock(struct akbasic_Runtime *obj);
#endif // _AKBASIC_CONSOLE_H_

92
include/akbasic/data.h Normal file
View File

@@ -0,0 +1,92 @@
/**
* @file data.h
* @brief The `DATA` item list and the `READ` cursor `RESTORE` resets.
*
* The Go reference has neither. Its `READ` does not read: it records the
* identifiers it wants filled, sets the scope waiting for a `DATA` verb, and
* lets execution skip forward until one turns up. That has two consequences,
* both wrong and both fixed here:
*
* - **A `DATA` line before its `READ` is never found.** `10 DATA 1,2` /
* `20 READ A#` skipped to the end of the program in silence.
* - **There is no cursor**, so `RESTORE` had nothing to reset and could not be
* implemented at all.
*
* Every `DATA` statement is pre-scanned into one flat list before the program
* runs, the same way labels are (akbasic_runtime_scan_labels), and `READ` walks
* a cursor along it. Items are kept as their source text and converted when read,
* because what a `DATA` item means depends on the variable it is read into --
* `DATA 5` fills `A#` with an integer and `A$` with "5".
*/
#ifndef _AKBASIC_DATA_H_
#define _AKBASIC_DATA_H_
#include <akerror.h>
#include <akbasic/types.h>
/** @brief How many `DATA` items a program may hold. */
#define AKBASIC_MAX_DATA_ITEMS 512
/** @brief Longest single `DATA` item, terminator included. */
#define AKBASIC_MAX_DATA_ITEM_LENGTH 128
/** @brief One `DATA` item: its text, where it was written, and whether it was quoted. */
typedef struct
{
char text[AKBASIC_MAX_DATA_ITEM_LENGTH];
int64_t lineno; /** so RESTORE (line) can find it */
bool quoted; /** written with quotes, so it is a string */
} akbasic_DataItem;
/** @brief Every `DATA` item in the program, and how far `READ` has got. */
typedef struct
{
akbasic_DataItem items[AKBASIC_MAX_DATA_ITEMS];
int count;
int cursor;
} akbasic_DataState;
struct akbasic_Runtime;
/**
* @brief Reset the item list and the cursor.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_data_state_init(akbasic_DataState *obj);
/**
* @brief Collect every `DATA` item in the stored program, in source order.
*
* Called on every entry into AKBASIC_MODE_RUN, beside
* akbasic_runtime_scan_labels() and for the same reason: it is the one point
* `RUN`, `CONT`, akbasic_runtime_start() and the end of a RUNSTREAM load all
* pass through. Re-scanning is what picks up a program edited at the REPL.
*
* The scan is textual. `DATA` at the start of a statement, outside a string
* literal, then comma-separated items up to a `:` or the end of the line -- which
* is where a Commodore ends a `DATA` statement too.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the program holds more than AKBASIC_MAX_DATA_ITEMS items.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_data_scan(struct akbasic_Runtime *obj);
/**
* @brief Take the next `DATA` item and convert it for a variable of @p type.
*
* @param obj Object to initialize, inspect, or modify.
* @param type The type of the variable being filled.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
* @throws AKBASIC_ERR_STATE When there are no items left -- "OUT OF DATA".
* @throws AKBASIC_ERR_TYPE When a quoted item is read into a numeric variable.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_data_next(struct akbasic_Runtime *obj, akbasic_Type type, akbasic_Value *dest);
#endif // _AKBASIC_DATA_H_

99
include/akbasic/disk.h Normal file
View File

@@ -0,0 +1,99 @@
/**
* @file disk.h
* @brief The group F file channels: DOPEN, DCLOSE, PRINT#, INPUT#, GET#.
*
* A Commodore's disk verbs address a 1541 over the serial bus. There is no 1541,
* so what these mean here is the nearest thing a filesystem offers, and the
* verbs that mean nothing without the hardware are refused by name rather than
* faked -- see TODO.md section 5.
*
* Channels are the interpreter's, not the host's: a program that opens a file
* and is then stepped by a game's frame loop keeps its channel across frames,
* and a `NEW` closes the lot.
*/
#ifndef _AKBASIC_DISK_H_
#define _AKBASIC_DISK_H_
#include <stdio.h>
#include <akerror.h>
#include <akbasic/types.h>
/** @brief How many files may be open at once. A C128 allows ten; this allows the same. */
#define AKBASIC_MAX_CHANNELS 10
/** @brief One open file channel. */
typedef struct
{
FILE *fp;
char name[AKBASIC_MAX_STRING_LENGTH];
bool writing;
} akbasic_Channel;
/** @brief Every open channel. Indexed by the number a program writes after `#`. */
typedef struct
{
akbasic_Channel channels[AKBASIC_MAX_CHANNELS];
} akbasic_DiskState;
struct akbasic_Runtime;
/**
* @brief Mark every channel closed. Does not close anything; nothing is open yet.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_state_init(akbasic_DiskState *obj);
/**
* @brief Close every open channel.
*
* Called by `DCLOSE` with no argument and by `NEW`. A program that ends without
* closing its files leaves them to this -- and to the host, which owns the
* process and outlives the program.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_close_all(akbasic_DiskState *obj);
/**
* @brief Write one line to a channel, terminator included.
*
* What `PRINT #n, ...` reaches. A line rather than raw bytes, because that is
* what `INPUT #n` reads back and the pair has to agree.
*
* @param obj Object to initialize, inspect, or modify.
* @param number The channel number.
* @param text What to write.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `text` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the channel number is out of range.
* @throws AKBASIC_ERR_STATE When the channel is closed, or was opened for reading.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_write(struct akbasic_Runtime *obj, int64_t number, const char *text);
/**
* @brief Read one line from a channel, without its terminator.
*
* What `INPUT #n, VAR` reaches. End of file is reported through @p eof rather
* than raised, the same way the sink's `readline` reports it: running out of
* input is a state a program handles, not a failure.
*
* @param obj Object to initialize, inspect, or modify.
* @param number The channel number.
* @param dest Output destination populated by the function.
* @param len Capacity of @p dest.
* @param eof Output destination populated by the function; true at end of file.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any pointer argument is NULL.
* @throws AKBASIC_ERR_BOUNDS When the channel number is out of range.
* @throws AKBASIC_ERR_STATE When the channel is closed, or was opened for writing.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_readline(struct akbasic_Runtime *obj, int64_t number, char *dest, size_t len, bool *eof);
#endif // _AKBASIC_DISK_H_

View File

@@ -23,6 +23,13 @@
#include <akbasic/value.h>
#include <akbasic/variable.h>
/** @brief A DO or LOOP with no condition on that end. */
#define AKBASIC_LOOPCOND_NONE 0
/** @brief `WHILE c` -- keep looping while the condition is true. */
#define AKBASIC_LOOPCOND_WHILE 1
/** @brief `UNTIL c` -- keep looping until the condition becomes true. */
#define AKBASIC_LOOPCOND_UNTIL 2
struct akbasic_Runtime;
typedef struct akbasic_Environment
@@ -38,9 +45,32 @@ typedef struct akbasic_Environment
akbasic_Value forToValue;
akbasic_Variable *forNextVariable;
/*
* DO/LOOP state. The condition may sit on either end -- `DO WHILE c`,
* `LOOP UNTIL c`, both, or neither -- so each end keeps its own, and the
* leaf lives in this environment's pool because it is re-evaluated on every
* iteration long after the line that held it was scanned away.
*/
akbasic_ASTLeaf *doConditionLeaf;
/* The condition is re-evaluated every iteration, long after its line was scanned away. */
akbasic_ASTLeaf doLeafStorage[AKBASIC_MAX_CONDITION_LEAVES];
akbasic_LeafPool doLeafPool;
int doConditionKind; /** AKBASIC_LOOPCOND_* */
bool isDoLoop; /** distinguishes DO/LOOP from FOR/NEXT for EXIT */
/* Loop bounds */
int64_t loopFirstLine;
int64_t loopExitLine;
/**
* Set by `EXIT`, cleared by the `NEXT` that acts on it.
*
* `EXIT` cannot simply jump past the loop, because where the loop ends is
* not known until a `NEXT` has run at least once -- and an `EXIT` on the
* first pass is the normal case. So it skips forward to the `NEXT` using the
* same waitingForCommand machinery a zero-iteration loop body uses, and this
* is what tells that `NEXT` it is ending the loop rather than continuing it.
*/
bool exiting;
int64_t gosubReturnLine;

92
include/akbasic/format.h Normal file
View File

@@ -0,0 +1,92 @@
/**
* @file format.h
* @brief `PRINT USING` field formatting, and the `PUDEF` characters it fills with.
*
* Its own translation unit rather than a helper inside the PRINT handler,
* because the interesting part is a pure function of a format string and a
* value: it can be tested exhaustively without a runtime, a sink or a program,
* and field formatting has far more edge cases than a verb usually does.
*/
#ifndef _AKBASIC_FORMAT_H_
#define _AKBASIC_FORMAT_H_
#include <akerror.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
/** @brief How many characters `PUDEF` can redefine. */
#define AKBASIC_PUDEF_CHARS 4
/** @brief `PUDEF` position 1: what a leading blank in a numeric field is filled with. */
#define AKBASIC_PUDEF_BLANK 0
/** @brief `PUDEF` position 2: the thousands separator. */
#define AKBASIC_PUDEF_COMMA 1
/** @brief `PUDEF` position 3: the decimal point. */
#define AKBASIC_PUDEF_POINT 2
/** @brief `PUDEF` position 4: the currency sign. */
#define AKBASIC_PUDEF_DOLLAR 3
/**
* @brief The four characters `PUDEF` redefines.
*
* Lives on the runtime beside the other verb-group state: it is the program's,
* not the sink's, and a host that swaps one output device for another does not
* expect the program's punctuation to change.
*/
typedef struct
{
char chars[AKBASIC_PUDEF_CHARS];
} akbasic_FormatState;
/**
* @brief Reset the `PUDEF` characters to their defaults: space, comma, point, dollar.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_format_state_init(akbasic_FormatState *obj);
/**
* @brief Redefine the fill characters, up to four of them.
*
* Positions past the end of @p chars keep whatever they had, so `PUDEF "*"`
* changes only the leading-blank character. That is BASIC 7.0's rule.
*
* @param obj Object to initialize, inspect, or modify.
* @param chars Replacement characters, in PUDEF order; at most four are read.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either argument is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_format_pudef(akbasic_FormatState *obj, const char *chars);
/**
* @brief Render one value into one `PRINT USING` field.
*
* @p format is the whole format string; the *first* field in it is the one used,
* and any literal text around that field is copied through. That is how BASIC
* 7.0 works -- `PRINT USING "TOTAL: ###.##"; X` prints the label as well.
*
* Numeric fields are built from `#` digit positions, an optional `.` and
* embedded `,` separators, with an optional leading or trailing `$`, `+` or `-`.
* String fields are `=` to centre and `>` to right-justify, and a bare run of
* `#` left-justifies.
*
* **A value too wide for its field fills the field with `*`**, which is BASIC
* 7.0's answer and is deliberately loud: silently printing more digits than the
* field asked for would misalign every column after it.
*
* @param obj The PUDEF characters to fill with.
* @param format The format string.
* @param value The value to render.
* @param dest Output destination populated by the function.
* @param len Capacity of @p dest, terminator included.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
* @throws AKBASIC_ERR_BOUNDS When the result does not fit @p dest.
* @throws AKBASIC_ERR_SYNTAX When the format string contains no field at all.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_format_using(akbasic_FormatState *obj, const char *format, akbasic_Value *value, char *dest, size_t len);
#endif // _AKBASIC_FORMAT_H_

View File

@@ -84,6 +84,8 @@ typedef struct akbasic_AkglFrontend
akbasic_AkglGraphics graphicsstate;
akbasic_AudioBackend audio;
akbasic_InputBackend input;
akbasic_SpriteBackend sprites;
akbasic_AkglSprites spritesstate;
/** False once the window has been closed; the drive loop stops on it. */
bool running;

View File

@@ -59,7 +59,14 @@ typedef enum
AKBASIC_TOK_ARRAY_SUBSCRIPT, /* 37 */
AKBASIC_TOK_FUNCTION_ARGUMENT, /* 38 */
AKBASIC_TOK_ATSYMBOL, /* 39 */
AKBASIC_TOK_IDENTIFIER_STRUCT /* 40 */
AKBASIC_TOK_IDENTIFIER_STRUCT, /* 40 */
/*
* 41, 42 -- MOVSPR's two separators, and nothing else in the language uses
* either. Appended rather than filed in with the other punctuation because
* the numbering is part of this enum's published shape.
*/
AKBASIC_TOK_SEMICOLON, /* 41 */
AKBASIC_TOK_HASHMARK /* 42 */
} akbasic_TokenType;
typedef enum
@@ -189,6 +196,18 @@ akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self);
*/
bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self);
/**
* @brief The value type an identifier leaf's suffix asks for.
*
* `A#` is an integer, `A%` a float, `A$` a string. A leaf that is not an
* identifier, or one with no suffix -- a label -- reports
* #AKBASIC_TYPE_UNDEFINED.
*
* @param self Leaf to inspect; NULL reports undefined.
* @return The type, or #AKBASIC_TYPE_UNDEFINED.
*/
akbasic_Type akbasic_leaf_identifier_type(akbasic_ASTLeaf *self);
/**
* @brief True when a leaf is an integer, float or string literal.
* @param self Leaf to inspect; NULL is not a literal.

View File

@@ -67,6 +67,7 @@ typedef struct
double x; /* pixel cursor: where LOCATE put it, or where */
double y; /* the last DRAW ended */
bool scaling; /* SCALE on */
int linewidth; /* WIDTH: 1 or 2 pixels per drawn line */
double xmax; /* user-coordinate maxima SCALE maps from */
double ymax;
} akbasic_GraphicsState;

View File

@@ -31,6 +31,19 @@
typedef struct akbasic_Parser
{
akbasic_Runtime *runtime;
/**
* True while parsing something that cannot possibly be an assignment, which
* makes a lone `=` an equality test rather than one.
*
* The scanner reads `==` as EQUAL and a lone `=` as ASSIGNMENT, because at
* that point it cannot know whether it is looking at a statement or a
* condition. A condition is the one place the question has an answer: BASIC
* has no assignment expression, so `IF A# = 2 THEN` can only be a
* comparison -- and that is how every BASIC manual writes it. Set around the
* condition and cleared afterwards, because `FOR I# = 1 TO 5` runs through
* the same expression parser and there `=` really is an assignment.
*/
bool comparing;
} akbasic_Parser;
/**

View File

@@ -19,11 +19,17 @@
#include <akerror.h>
#include <akbasic/audio.h>
#include <akbasic/console.h>
#include <akbasic/data.h>
#include <akbasic/disk.h>
#include <akbasic/environment.h>
#include <akbasic/format.h>
#include <akbasic/grammar.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
#include <akbasic/variable.h>
@@ -44,6 +50,39 @@ typedef struct
int64_t lineno;
} akbasic_SourceLine;
/**
* @brief What can interrupt a running program and send it into a handler.
*
* The numbering is COLLISION's own argument minus one, so `COLLISION 1` arms
* slot 0. The two collision types this interpreter refuses still hold slots:
* dropping them would make the arithmetic a lookup table for no gain, and a
* later release that implements them wants the same numbers.
*/
typedef enum
{
AKBASIC_INTERRUPT_SPRITE = 0, /** COLLISION 1 -- sprite met sprite */
AKBASIC_INTERRUPT_BACKGROUND, /** COLLISION 2 -- sprite met background; refused */
AKBASIC_INTERRUPT_LIGHTPEN, /** COLLISION 3 -- light pen; refused */
AKBASIC_INTERRUPT_ERROR, /** TRAP -- a BASIC error; group C, not yet armed by anything */
AKBASIC_MAX_INTERRUPTS
} akbasic_InterruptSource;
/**
* @brief One armed interrupt: where its handler is, and whether it is due.
*
* The target is held as a line number *or* a label name, and a label is resolved
* when the interrupt fires rather than when it is armed. A label may be re-filed
* by its own LABEL statement at any point, so resolving late is what lets the
* later spelling win.
*/
typedef struct
{
bool armed;
bool pending;
int64_t line; /* the numeric target; 0 when a label was given */
char label[AKBASIC_SYMTAB_MAX_KEY]; /* the label target; empty when a number was given */
} akbasic_Interrupt;
/** @brief A user-defined subroutine or single-expression function. */
typedef struct
{
@@ -95,6 +134,7 @@ typedef struct akbasic_Runtime
akbasic_GraphicsBackend *graphics;
akbasic_AudioBackend *audio;
akbasic_InputBackend *input;
akbasic_SpriteBackend *sprites;
/*
* The graphics verbs' own state -- mode, color-source bindings, pixel cursor
@@ -114,6 +154,59 @@ typedef struct akbasic_Runtime
/* GETKEY's hold on the step loop. Same reasoning again: it is the program's. */
akbasic_InputState input_state;
/*
* The eight sprites, their positions and their collision bits. Same
* reasoning as gfx and audio_state, and one more: RSPPOS and RSPRITE read
* this rather than asking the device, so they answer correctly with no
* device attached at all.
*/
akbasic_SpriteState sprite_state;
/* PUDEF's fill characters, which PRINT USING pads and punctuates with. */
akbasic_FormatState format_state;
/* SLEEP's and WAIT's hold on the step loop, and KEY's macros. */
akbasic_ConsoleState console_state;
/* Every DATA item in the program, and how far READ has got along it. */
akbasic_DataState data_state;
/* The open file channels DOPEN hands out. */
akbasic_DiskState disk_state;
/*
* Where the running program was loaded from, or empty when it did not come
* from a file -- a REPL session, or a host that handed over a string.
*
* Only one thing needs it today: SPRSAV resolving an image path that does
* not exist relative to the working directory, which is what makes a `.bas`
* beside its art work from anywhere. Group F's disk verbs will want the
* same, which is why it is on the runtime rather than in the sprite state.
*/
char sourcepath[AKBASIC_MAX_LINE_LENGTH];
/*
* The armed interrupts, and the environment the one currently running was
* entered through.
*
* `handlerenv` is doing two jobs at once. It is the "a handler is running"
* flag -- an interrupt does not interrupt an interrupt, which is what keeps a
* collision that persists across the handler from recursing until the
* environment pool is gone. And it is the identity RETURN compares against,
* so the flag clears on the RETURN that leaves *this* handler rather than on
* the first RETURN of any GOSUB the handler itself makes.
*/
akbasic_Interrupt interrupts[AKBASIC_MAX_INTERRUPTS];
akbasic_Environment *handlerenv;
/*
* The status code of the error being reported, for `ER#` to carry into a
* TRAP handler. Set by the reporting path, which is the only place that
* still has the error context; akbasic_runtime_error() sees a message and a
* class, both of which have already lost the number.
*/
int lasterrorstatus;
/*
* 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
@@ -164,6 +257,17 @@ typedef struct akbasic_Runtime
/* REPL line assembly */
char userline[AKBASIC_MAX_LINE_LENGTH];
/*
* Set by the scanner when the line it just read began with a line number.
*
* It is what tells the REPL apart from a program. A line typed *with* a
* number is program text and is filed; a line typed *without* one is a
* direct-mode statement and runs now. Without this the REPL could only run
* the handful of verbs marked immediate, so `PRINT 2 + 2` at the prompt was
* silently stored rather than answered -- see TODO.md section 5.
*/
bool hadlinenumber;
/* Scanner state */
char line[AKBASIC_MAX_LINE_LENGTH];
int current;
@@ -193,10 +297,33 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, ak
* @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.
* @param sprites Where SPRITE, MOVSPR and SPRSAV land; 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);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input, akbasic_SpriteBackend *sprites);
/**
* @brief Tell the interpreter where the program it is running came from.
*
* An asset path a program writes -- `SPRSAV "ship.png", 1` -- is tried against
* the process working directory first and against this directory second, so a
* program stored beside its art works whether it was launched from its own
* directory or from somewhere else. Exactly what libakgl does for a sprite
* document naming its spritesheet.
*
* @p path is the program *file*, not its directory; the directory is taken from
* it. Passing NULL, or never calling this, leaves only the working directory,
* which is the right answer for a REPL session and for a host that handed the
* interpreter a string.
*
* @param obj Object to initialize, inspect, or modify.
* @param path Path to the program file, or NULL for none.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);
/**
* @brief Tell the interpreter what time the host thinks it is.
@@ -218,6 +345,153 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_devices(akbasic_Runtime *
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems);
/**
* @brief Move every line onto a new number, rewriting every reference to one.
*
* Moving lines is a permutation of `source[]`. Rewriting `GOTO`, `GOSUB`, `RUN`,
* `RESTORE`, `TRAP` and `COLLISION` targets to match is the work, and doing it on
* source text means telling a line number apart from digits inside a string
* literal -- `PRINT "GOTO 10"` must survive.
*
* A target naming a line that does not exist is left alone rather than
* rewritten: `GOTO 9999` in a program with no line 9999 is already broken, and
* inventing a destination for it would hide that. A `GOTO` to a *label* needs no
* rewriting at all, which is the strongest argument for writing programs that
* way.
*
* @param obj Object to initialize, inspect, or modify.
* @param newstart The number the first renumbered line takes.
* @param increment How far apart the new numbers are; must be positive.
* @param oldstart Renumber from this line on, leaving anything before it alone.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_VALUE When the increment is not positive, or a renumbered line would land on a kept one.
* @throws AKBASIC_ERR_BOUNDS When a new number would exceed AKBASIC_MAX_SOURCE_LINES, or a rewritten line would not fit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int64_t increment, int64_t oldstart);
/**
* @brief File every `LABEL` in the stored program before any of it runs.
*
* Without this a label exists only from the moment its `LABEL` statement
* executes, so `GOTO` and `GOSUB` reach backwards and never forwards -- and an
* interrupt handler, which by definition sits on a line normal flow does not
* fall into, could not be named by label at all. That is the case this exists
* for; forward `GOTO` is the improvement that comes with it.
*
* The scan is textual rather than a parse: `LABEL` at the start of a statement,
* outside a string literal, followed by an identifier. Parsing every line up
* front would raise on lines the program would never have reached, which is a
* worse trade than a scanner that understands one keyword.
*
* `LABEL` still executes normally and still files itself, so a label re-filed at
* run time wins. A name that appears twice therefore resolves to the last one in
* the source until one of them runs.
*
* Called by akbasic_runtime_set_mode() on every entry into AKBASIC_MODE_RUN,
* which is the one point `RUN`, `CONT`, akbasic_runtime_start() and the end of a
* RUNSTREAM load all pass through. A host has no reason to call it directly.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL or the runtime has no environment.
* @throws AKBASIC_ERR_BOUNDS When the program holds more labels than AKBASIC_MAX_LABELS.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_scan_labels(akbasic_Runtime *obj);
/**
* @brief Point an interrupt source at a handler, replacing whatever it had.
*
* Exactly one of @p line and @p label carries the target. A label is stored by
* name and resolved when the interrupt fires; see #akbasic_Interrupt for why.
*
* Arming does not clear a pending event. A program that disarms and re-arms
* around a critical section still sees the collision that happened inside it,
* which is the behaviour that loses no events.
*
* @param obj Object to initialize, inspect, or modify.
* @param source Which interrupt to arm.
* @param line Line number to enter, or 0 when @p label carries the target.
* @param label Label to enter, or NULL when @p line carries the target.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `source` is out of range or the label is too long.
* @throws AKBASIC_ERR_VALUE When neither or both of `line` and `label` name a target.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source, int64_t line, const char *label);
/**
* @brief Stop an interrupt source from entering a handler.
*
* Any event already pending on it is dropped: a program that has said it no
* longer cares should not be sent into a handler it has just taken down.
*
* @param obj Object to initialize, inspect, or modify.
* @param source Which interrupt to disarm.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `source` is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_disarm_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source);
/**
* @brief Record that an interrupt source fired.
*
* Cheap and safe to call every frame from a device backend, whether or not
* anything is armed: an unarmed source records nothing, so a host does not have
* to ask what the script has subscribed to. The handler is entered later, by
* akbasic_runtime_step(), at a line boundary -- never from inside the code that
* raised it.
*
* @param obj Object to initialize, inspect, or modify.
* @param source Which interrupt fired.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `source` is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_raise_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source);
/**
* @brief Enter the handler of the first pending interrupt, if one is due.
*
* Called by akbasic_runtime_step() between source lines, which is the only place
* it is safe: a handler entered mid-statement would return into the middle of a
* line, and the parser holds no state that could resume there. That granularity
* is the same one block skipping already works at -- see TODO.md.
*
* Entering a handler is a GOSUB the program did not write: a scope is pushed,
* its return line is the line that was about to run, and the handler's RETURN
* pops back to it. So a handler must end in RETURN, exactly as on a C128.
*
* Sources are tried in enum order, and only one is entered per call. Nothing is
* entered while a handler is already running.
*
* @param obj Object to initialize, inspect, or modify.
* @param entered Output destination populated by the function; true when a handler was entered. May be NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_UNDEFINED When the handler's label names nothing in the program.
* @throws AKBASIC_ERR_ENVIRONMENT When the environment pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_service_interrupts(akbasic_Runtime *obj, bool *entered);
/**
* @brief Publish `ER#` and `EL#` into the global scope for a TRAP handler.
*
* A C128 spells these `ER` and `EL` as bare reserved names. This dialect has no
* bare variable names -- an identifier carries its type in a suffix, and a name
* without one is a label -- so they are ordinary global integers. See TODO.md
* section 5.
*
* @param obj Object to initialize, inspect, or modify.
* @param status The status code of the error.
* @param line The line it was reported on.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When no variable slot is free.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_trap_set_error_variables(akbasic_Runtime *obj, int status, int64_t line);
/**
* @brief Reset the per-line state without disturbing the program or variables.
* @param obj Object to initialize, inspect, or modify.

View File

@@ -33,6 +33,34 @@ typedef struct akbasic_TextSink
*/
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
/**
* Put the cursor at a character cell, or NULL when the sink has no cursor.
*
* `CHAR` is the only verb that needs it, and a stdio sink genuinely cannot
* do it -- a terminal's cursor is not this library's to move, and a pipe has
* no cursor at all. So it is optional, like the audio backend's `sweep`, and
* `CHAR` refuses by name against a sink that leaves it NULL rather than
* printing in the wrong place.
*
* @param self The sink.
* @param col Zero-based column.
* @param row Zero-based row.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
/**
* Constrain the sink to a rectangle of character cells, or NULL when it has
* no grid to constrain. `WINDOW` is the only caller. Coordinates are
* inclusive, as BASIC 7.0 writes them.
*
* @param self The sink.
* @param left Leftmost column, zero-based.
* @param top Topmost row, zero-based.
* @param right Rightmost column, inclusive.
* @param bottom Bottommost row, inclusive.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
} akbasic_TextSink;
/** @brief State for the stdio-backed sink. */

253
include/akbasic/sprite.h Normal file
View File

@@ -0,0 +1,253 @@
/**
* @file sprite.h
* @brief Declares the sprite backend: where SPRITE, MOVSPR and SPRSAV land.
*
* The same shape as akbasic/graphics.h and for the same reason -- the core
* library builds with no SDL and no libakgl present, so a sprite verb calls
* through a record of function pointers rather than reaching for akgl_actor_*.
* akbasic_sprite_init_akgl() lives in the separate akbasic_akgl target.
*
* **A sprite is a Commodore sprite, not a libakgl sprite.** libakgl's has a
* sheet, a frame list, an animation speed and a state-to-sprite map, all of
* which arrive from a JSON document; BASIC 7.0 has a word for none of it. What
* BASIC has is eight numbered slots, each holding one still image with a
* position, a colour, a visibility flag and two expansion bits. So the JSON
* layer is bypassed rather than reached through: the adaptor builds the same
* structures directly and hands the interpreter this much smaller surface.
*
* The one place the two overlap is loading an image from a file, which is why
* `define_file` is here: a program that has art on disk should be able to say so,
* and libakgl already resolves a relative asset path and decodes the image. That
* path is not bypassed -- it is exactly akgl_spritesheet_initialize().
*
* A runtime with no sprite backend is the normal case, so every verb that needs
* one refuses with AKBASIC_ERR_DEVICE. The verbs that only touch interpreter
* state -- SPRCOLOR, and the RSP* readbacks -- work regardless, which is what
* makes them testable without a device.
*/
#ifndef _AKBASIC_SPRITE_H_
#define _AKBASIC_SPRITE_H_
#include <akerror.h>
#include <akbasic/graphics.h>
#include <akbasic/types.h>
/** @brief Sprites a program has. Eight, as on a C128; the verbs number them 1-8. */
#define AKBASIC_MAX_SPRITES 8
/** @brief Width of a Commodore sprite in pixels. */
#define AKBASIC_SPRITE_WIDTH 24
/** @brief Height of a Commodore sprite in pixels. */
#define AKBASIC_SPRITE_HEIGHT 21
/**
* @brief Bytes in a sprite pattern: three per row, twenty-one rows.
*
* The C128 documents SPRSAV's string as the SSHAPE data format at a fixed 24x21,
* which is 63 bytes of bitmap plus a four-byte header. Only the bitmap is
* carried here -- the header records a width and height that are constants for a
* sprite -- so a DIMmed integer array of this many elements is a whole pattern.
*/
#define AKBASIC_SPRITE_PATTERN_BYTES 63
/**
* @brief Fastest MOVSPR speed, and how far one unit of it travels.
*
* `MOVSPR n, angle # speed` takes 0-15 on a C128 and the reference manual does
* not say what a unit is in pixels; it says 15 is fastest and 0 stops. So the
* mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND pixels
* per second in BASIC's 320x200 space, which puts speed 15 at four seconds to
* cross the screen. Recorded as a deviation in TODO.md section 5 rather than
* presented as fidelity.
*/
#define AKBASIC_SPRITE_MAX_SPEED 15
/** @brief Pixels per second one unit of MOVSPR speed is worth. See #AKBASIC_SPRITE_MAX_SPEED. */
#define AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND 5.0
/**
* @brief One sprite, as BASIC sees it.
*
* Every field here is something a BASIC verb sets and an RSP* function reads
* back, which is why the whole thing lives on the runtime rather than on the
* device: a host that swaps one renderer for another does not expect the
* program's sprite positions to go with it. The device is told about changes; it
* is never asked what they were.
*/
typedef struct
{
bool defined; /* SPRSAV has given this slot an image */
bool enabled; /* SPRITE n, 1 */
bool behind; /* priority: drawn behind the drawing surface */
bool xexpand; /* SPRITE's x-expand bit */
bool yexpand;
bool multicolour;
int colorindex; /* 1-16, the sprite's own colour */
double x; /* position in BASIC's 320x200 space */
double y;
double angle; /* MOVSPR's continuous motion: degrees */
int speed; /* clockwise from vertical, and 0-15 */
} akbasic_Sprite;
/**
* @brief The sprite verbs' own state, which lives on the runtime.
*
* `bumped` accumulates rather than reporting an instant. A collision that
* happens between two BUMP() calls is still news when the second one asks, and a
* program polling once a frame would otherwise miss anything that started and
* ended inside a frame. BUMP() clears what it reports, which is what a C128 does
* and what makes "has this sprite hit anything since I last looked" answerable.
*/
typedef struct
{
akbasic_Sprite sprites[AKBASIC_MAX_SPRITES];
int sharedcolor1; /* SPRCOLOR's two multicolour registers, 1-16 */
int sharedcolor2;
uint16_t bumped; /* bit n-1 set when sprite n has collided */
int64_t lastservicems; /* when MOVSPR's continuous motion last moved */
} akbasic_SpriteState;
/**
* @brief Where the sprite verbs land.
*
* Sprites are numbered 1 through #AKBASIC_MAX_SPRITES at this boundary, the same
* way a BASIC program numbers them. Translating to a zero-based slot is the
* backend's business, and doing it here would mean two numbering schemes in one
* file for no gain.
*
* There is deliberately no entry point for the RSP* readbacks. Everything they
* report is in #akbasic_SpriteState, which the interpreter owns, so asking the
* device would be asking it to repeat what it was told.
*/
typedef struct akbasic_SpriteBackend
{
void *self;
/**
* Install a pattern into sprite @p n from packed bitmap bytes.
*
* Three bytes per row, most significant bit leftmost, twenty-one rows --
* the layout a C128 keeps in its sprite block, and what a type-in listing's
* DATA statements hold. @p fg is what a set bit draws and @p bg what a clear
* one does; a clear bit is normally transparent, which is @p bg with a zero
* alpha.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define)(struct akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg);
/**
* Install a pattern into sprite @p n from a region SSHAPE saved.
*
* The handle is the graphics backend's, not this one's -- SSHAPE puts it in
* a BASIC string and SPRSAV hands it straight back. An adaptor that serves
* both devices resolves it; one that does not may refuse with
* AKBASIC_ERR_DEVICE.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define_shape)(struct akbasic_SpriteBackend *self, int n, int handle);
/**
* Install a pattern into sprite @p n by loading an image file.
*
* @p path is what the program wrote, resolved by the backend rather than
* here: the interpreter has no idea what a path means to the host, and the
* akgl adaptor resolves it exactly as libakgl resolves a spritesheet named
* by a sprite document. @p root is the directory to fall back to when the
* path does not resolve against the working directory, normally the
* directory the running program was loaded from; it may be NULL.
*
* Unlike the other two, this one does not force 24x21: the image's own size
* is the sprite's size. A modern PC has no reason to throw away art that
* does not happen to be sprite-shaped.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*define_file)(struct akbasic_SpriteBackend *self, int n, const char *path, const char *root);
/** Show or hide sprite @p n. */
akerr_ErrorContext AKERR_NOIGNORE *(*show)(struct akbasic_SpriteBackend *self, int n, bool visible);
/** Put sprite @p n at (@p x, @p y) in BASIC's 320x200 space. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akbasic_SpriteBackend *self, int n, double x, double y);
/**
* Set sprite @p n's own colour, its expansion bits and its priority.
*
* One call rather than four because `SPRITE` sets them together and a
* backend that has to rebuild a texture to change a colour would otherwise
* do it four times for one statement.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*configure)(struct akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind);
/** Set the two multicolour registers every multicolour sprite shares. */
akerr_ErrorContext AKERR_NOIGNORE *(*shared_colors)(struct akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2);
/**
* Report which sprites are currently overlapping another sprite.
*
* Bit n-1 of @p mask is set when sprite n overlaps at least one other. The
* interpreter calls this once per step, ORs the result into
* akbasic_SpriteState::bumped, and raises the collision interrupt when
* anything is set -- so a backend answers about *now* and does not have to
* remember anything.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*collisions)(struct akbasic_SpriteBackend *self, uint16_t *mask);
} akbasic_SpriteBackend;
/**
* @brief Reset the sprite state to its power-on values.
*
* Eight undefined, hidden sprites at the origin in white, the two shared
* multicolour registers at their C128 defaults, and nothing bumped. NEW comes
* back through here.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_state_init(akbasic_SpriteState *obj);
/**
* @brief Unpack 63 bytes of sprite bitmap into one pixel per byte.
*
* Three bytes per row, most significant bit leftmost. @p dest receives
* #AKBASIC_SPRITE_WIDTH * #AKBASIC_SPRITE_HEIGHT entries, each 0 or 1. Shared by
* every backend that has to turn a pattern into pixels, and separate from them
* so the bit order is asserted once rather than trusted eight times.
*
* @param pattern Packed bitmap; at least @p bytes readable.
* @param bytes How many bytes @p pattern holds; must be #AKBASIC_SPRITE_PATTERN_BYTES.
* @param dest Output destination populated by the function; 504 entries.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either pointer is NULL.
* @throws AKBASIC_ERR_BOUNDS When `bytes` is not #AKBASIC_SPRITE_PATTERN_BYTES.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_unpack(const uint8_t *pattern, int bytes, uint8_t *dest);
struct akbasic_Runtime;
/**
* @brief Move every sprite MOVSPR set in continuous motion.
*
* Called once per akbasic_runtime_step(), before the line runs, and paced off
* akbasic_runtime_settime() -- exactly the way the PLAY queue is paced, and for
* the same reason: this library owns no loop and must not block, so the caller
* that owns the frame owns the clock.
*
* A host that never sets the time leaves it at zero and nothing moves, which is
* a still picture rather than a hang.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sprite_service(struct akbasic_Runtime *obj);
/**
* @brief Ask the device what is overlapping, and raise the collision interrupt.
*
* Also called once per step. What it finds is ORed into
* akbasic_SpriteState::bumped for BUMP() to report, and raised as
* #AKBASIC_INTERRUPT_SPRITE -- unconditionally, because an unarmed interrupt
* records nothing and there is therefore nothing to ask first.
*
* A runtime with no sprite device, or one whose backend withholds `collisions`,
* does nothing here rather than refusing: a step is not a statement and has no
* program line to blame.
*
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_collision_service(struct akbasic_Runtime *obj);
#endif // _AKBASIC_SPRITE_H_

View File

@@ -31,6 +31,13 @@
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */
#define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */
/*
* Leaves a DO/LOOP condition may use. Its own small pool rather than a second
* AKBASIC_MAX_LEAVES one, because a leaf carries two 256-byte strings and this
* is per environment: `DO WHILE A# = 1 AND B# = 2` is six leaves, and sixteen
* leaves nobody uses on every one of 32 scopes is 250 KB of nothing.
*/
#define AKBASIC_MAX_CONDITION_LEAVES 16
/* Commodore convention: true is -1, not 1. */
#define AKBASIC_TRUE -1

View File

@@ -76,6 +76,19 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_set_bool(akbasic_Value *obj, bo
/** @brief True when the value is a boolean holding AKBASIC_TRUE. */
bool akbasic_value_is_true(akbasic_Value *self);
/**
* @brief True when the value is nonzero, whatever numeric type it carries.
*
* What a condition tests. Commodore BASIC has no boolean type -- a comparison
* yields -1 or 0, `AND` and `OR` are the bitwise operators, and `IF A THEN` is
* legal for any numeric A -- so "is this a boolean holding true" is the wrong
* question for a branch to ask. Strings are never true.
*
* @param self Value to test; NULL is not true.
* @return `true` when the value is a nonzero number or a true truth value.
*/
bool akbasic_value_is_truthy(akbasic_Value *self);
/**
* @brief Negate a numeric value.
*