Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
/**
|
|
|
|
|
* @file runtime_graphics.c
|
|
|
|
|
* @brief The group G verb implementations: GRAPHIC, COLOR, DRAW, BOX, CIRCLE,
|
|
|
|
|
* PAINT, SCALE, SSHAPE, GSHAPE and LOCATE.
|
|
|
|
|
*
|
|
|
|
|
* Nothing here includes an SDL or a libakgl header. Every drawing call goes
|
|
|
|
|
* through the akbasic_GraphicsBackend record the host attached, which is what
|
|
|
|
|
* lets the whole group be tested in a build with no SDL on the machine.
|
|
|
|
|
*
|
|
|
|
|
* These verbs are not in the Go reference -- it lists all of them as
|
|
|
|
|
* unimplemented -- so the semantics come from Commodore BASIC 7.0 rather than
|
|
|
|
|
* from a port. Where 7.0 does something a modern renderer cannot, the deviation
|
|
|
|
|
* is recorded in TODO.md section 5 rather than papered over here.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <math.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
#include <akstdlib.h>
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
#include <akbasic/args.h>
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
|
|
|
|
|
#include "verbs.h"
|
|
|
|
|
|
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
|
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
|
|
|
do { \
|
|
|
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
|
|
|
} while ( 0 )
|
|
|
|
|
|
|
|
|
|
/** @brief What SSHAPE writes into the string variable, ahead of the slot number. */
|
|
|
|
|
#define SHAPE_PREFIX "SHAPE:"
|
|
|
|
|
|
|
|
|
|
/** @brief CIRCLE's default arc increment, in degrees. BASIC 7.0 uses two. */
|
|
|
|
|
#define CIRCLE_DEFAULT_INC 2.0
|
|
|
|
|
|
|
|
|
|
/**
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
* @brief Refuse politely when the host lent us no graphics device, and ask its size.
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
*
|
|
|
|
|
* Every verb in this file opens with it. The standalone driver attaches no
|
|
|
|
|
* backend at all, so this is the common path rather than an edge case, and it
|
|
|
|
|
* has to name the verb -- "no graphics device" on its own tells a program author
|
|
|
|
|
* nothing about which line to look at.
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
*
|
|
|
|
|
* It is also where the drawing surface's size is refreshed, which is why the two
|
|
|
|
|
* jobs are one function: the size is wanted by every verb that draws, every verb
|
|
|
|
|
* that draws already opens with this, and a window can be resized between two
|
|
|
|
|
* statements. A backend with no `size` -- it is the record's one optional entry
|
|
|
|
|
* point -- keeps whatever akbasic_graphics_state_init() left, which is 320x200.
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *require_graphics(akbasic_Runtime *obj, const char *verb)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
int width = 0;
|
|
|
|
|
int height = 0;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in require_graphics");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->graphics != NULL), AKBASIC_ERR_DEVICE,
|
|
|
|
|
"%s needs a graphics device and this runtime has none", verb);
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
if ( obj->graphics->size != NULL ) {
|
|
|
|
|
PASS(errctx, obj->graphics->size(obj->graphics, &width, &height));
|
|
|
|
|
/*
|
|
|
|
|
* A device reporting nothing useful is not an error -- an SDL window
|
|
|
|
|
* mid-resize legitimately measures zero -- but it must not become the
|
|
|
|
|
* space a program draws into, because dividing by it in scale_point
|
|
|
|
|
* would send every scaled coordinate to infinity.
|
|
|
|
|
*/
|
|
|
|
|
if ( width > 0 && height > 0 ) {
|
|
|
|
|
obj->gfx.devwidth = width;
|
|
|
|
|
obj->gfx.devheight = height;
|
|
|
|
|
}
|
|
|
|
|
}
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
* @brief Map a user coordinate onto the device pixels the backend receives.
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
*
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
* With SCALE off this is the identity, and a coordinate is a device pixel: the
|
|
|
|
|
* whole of the host's window is reachable from BASIC. With SCALE on, the
|
|
|
|
|
* program's coordinates run 0..xmax across the same surface, so the ratio is all
|
|
|
|
|
* there is to it -- and the surface is however big the device last said it was,
|
|
|
|
|
* which is what makes `SCALE 1, 319, 199` the way to run a C128 listing full
|
|
|
|
|
* screen.
|
|
|
|
|
*
|
|
|
|
|
* **`xmax` maps onto the last pixel, not onto the width**, which is a
|
|
|
|
|
* one-character difference and the whole difference between that sentence being
|
|
|
|
|
* true and being nearly true. Dividing by the width sends `SCALE 1, 319, 199`
|
|
|
|
|
* plus `DRAW 1, 319, 199` to (width, height) -- one past the bottom-right
|
|
|
|
|
* corner, off the surface, drawing nothing. A user maximum is a coordinate the
|
|
|
|
|
* program is entitled to plot, the same way 7.0's own `SCALE 1` then
|
|
|
|
|
* `DRAW 1, 1023, 1023` reaches the corner of a C128 screen rather than missing it.
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
*/
|
|
|
|
|
static void scale_point(akbasic_GraphicsState *gfx, double *x, double *y)
|
|
|
|
|
{
|
|
|
|
|
if ( !gfx->scaling ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
if ( gfx->xmax > 0.0 && gfx->devwidth > 1 ) {
|
|
|
|
|
*x = (*x / gfx->xmax) * (double)(gfx->devwidth - 1);
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
if ( gfx->ymax > 0.0 && gfx->devheight > 1 ) {
|
|
|
|
|
*y = (*y / gfx->ymax) * (double)(gfx->devheight - 1);
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Draw a line `WIDTH` pixels thick.
|
|
|
|
|
*
|
|
|
|
|
* BASIC 7.0's `WIDTH` sets the thickness of the lines the graphics verbs draw,
|
|
|
|
|
* and the backend record has no thickness argument -- neither does libakgl's
|
|
|
|
|
* akgl_draw_line, which is what it would have to reach. So a thick line is drawn
|
|
|
|
|
* as parallel passes, offset perpendicular to whichever axis the line runs along.
|
|
|
|
|
* That is what a C128 does in effect and it needs nothing new from the device.
|
|
|
|
|
*
|
|
|
|
|
* Approximate for a diagonal, where offsetting along one axis leaves the passes a
|
|
|
|
|
* fraction under a pixel apart. At WIDTH 2 -- the only other value the verb
|
|
|
|
|
* accepts -- that is not visible; a general thickness would want a real
|
|
|
|
|
* perpendicular offset and an API that takes one.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *draw_line(akbasic_Runtime *obj, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double dx = (x2 > x1 ? x2 - x1 : x1 - x2);
|
|
|
|
|
double dy = (y2 > y1 ? y2 - y1 : y1 - y2);
|
|
|
|
|
bool steep = (dy > dx);
|
|
|
|
|
int pass = 0;
|
|
|
|
|
int width = (obj->gfx.linewidth > 0 ? obj->gfx.linewidth : 1);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A loop, so CATCH and the _BREAK macros are banned in here -- they expand
|
|
|
|
|
* to a C break and would leave the loop with an error still pending.
|
|
|
|
|
*/
|
|
|
|
|
for ( pass = 0; pass < width; pass++ ) {
|
|
|
|
|
double offset = (double)pass;
|
|
|
|
|
|
|
|
|
|
if ( steep ) {
|
|
|
|
|
PASS(errctx, obj->graphics->line(obj->graphics, x1 + offset, y1,
|
|
|
|
|
x2 + offset, y2, color));
|
|
|
|
|
} else {
|
|
|
|
|
PASS(errctx, obj->graphics->line(obj->graphics, x1, y1 + offset,
|
|
|
|
|
x2, y2 + offset, color));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
/* ------------------------------------------------------------- GRAPHIC --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[2];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int mode = 0;
|
|
|
|
|
akbasic_Color background;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "GRAPHIC"));
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "GRAPHIC", args, 2, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "GRAPHIC expected a mode");
|
|
|
|
|
|
|
|
|
|
mode = (int)args[0];
|
|
|
|
|
/*
|
|
|
|
|
* BASIC 7.0 numbers five modes plus a CLR. They differ in bitmap resolution
|
|
|
|
|
* and in whether the bottom of the screen stays text, neither of which means
|
|
|
|
|
* anything against a host's renderer -- so the mode is recorded and only its
|
|
|
|
|
* one observable consequence is honoured: mode 0 is text, and text mode does
|
|
|
|
|
* not draw. Refusing an out-of-range mode still matters, because that is a
|
|
|
|
|
* typo the program author can fix.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (mode >= 0 && mode <= 5), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"GRAPHIC mode %d out of range (0 to 5)", mode);
|
|
|
|
|
if ( mode == 5 ) {
|
|
|
|
|
/* GRAPHIC CLR: drop the saved shapes and go back to text. */
|
|
|
|
|
PASS(errctx, obj->graphics->free_shapes(obj->graphics));
|
|
|
|
|
PASS(errctx, akbasic_graphics_state_init(&obj->gfx));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj->gfx.mode = mode;
|
|
|
|
|
if ( count >= 2 && args[1] != 0.0 ) {
|
|
|
|
|
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, 0, &background));
|
|
|
|
|
PASS(errctx, obj->graphics->clear(obj->graphics, background));
|
|
|
|
|
obj->gfx.x = 0.0;
|
|
|
|
|
obj->gfx.y = 0.0;
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------- COLOR --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_color(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[2];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int source = 0;
|
|
|
|
|
int index = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
/*
|
|
|
|
|
* COLOR is the one verb in the group that does not touch the device: it
|
|
|
|
|
* binds a source to a palette index and the drawing verbs resolve that later.
|
|
|
|
|
* So it works with no graphics backend attached, which lets a program set its
|
|
|
|
|
* colors up before the host has lent it a renderer.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in COLOR");
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "COLOR", args, 2, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count == 2), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"COLOR expected a source and a color");
|
|
|
|
|
|
|
|
|
|
source = (int)args[0];
|
|
|
|
|
index = (int)args[1];
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_COLOR_SOURCES),
|
|
|
|
|
AKBASIC_ERR_BOUNDS, "COLOR source %d out of range (0 to %d)",
|
|
|
|
|
source, AKBASIC_COLOR_SOURCES - 1);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (index >= 1 && index <= 16), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"COLOR %d out of range (1 to 16)", index);
|
|
|
|
|
obj->gfx.source[source] = index;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* -------------------------------------------------------------- LOCATE --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_locate(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[2];
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in LOCATE");
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "LOCATE", args, 2, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count == 2), AKBASIC_ERR_SYNTAX, "LOCATE expected X, Y");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* LOCATE moves the *graphics* pixel cursor -- the point a DRAW with no
|
|
|
|
|
* starting coordinate begins from. The text cursor is CHAR's business. It
|
|
|
|
|
* needs no device, so it works before a renderer is attached.
|
|
|
|
|
*/
|
|
|
|
|
obj->gfx.x = args[0];
|
|
|
|
|
obj->gfx.y = args[1];
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------- SCALE --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_scale(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[3];
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in SCALE");
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "SCALE", args, 3, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "SCALE expected 0 or 1");
|
|
|
|
|
|
|
|
|
|
/* Pure coordinate arithmetic -- nothing reaches the device. */
|
|
|
|
|
obj->gfx.scaling = (args[0] != 0.0);
|
|
|
|
|
if ( count >= 3 ) {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (args[1] > 0.0 && args[2] > 0.0), AKBASIC_ERR_VALUE,
|
|
|
|
|
"SCALE maxima must be positive");
|
|
|
|
|
obj->gfx.xmax = args[1];
|
|
|
|
|
obj->gfx.ymax = args[2];
|
|
|
|
|
} else if ( obj->gfx.scaling ) {
|
|
|
|
|
/* BASIC 7.0's default scaled space. */
|
|
|
|
|
obj->gfx.xmax = 1023.0;
|
|
|
|
|
obj->gfx.ymax = 1023.0;
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.
akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.
SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.
SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.
The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:35:20 -04:00
|
|
|
/* ----------------------------------------------------------------- RGR --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_fn_rgr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *out = NULL;
|
|
|
|
|
double args[1];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int field = 0;
|
|
|
|
|
int64_t answer = 0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in RGR");
|
|
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "RGR", args, 1, &count));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RGR expected a field number");
|
|
|
|
|
field = (int)args[0];
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Field 0 is BASIC 7.0's whole RGR: the current GRAPHIC mode. It answers
|
|
|
|
|
* from state and works with no device, because a program can ask what mode
|
|
|
|
|
* it set without a screen to set it on.
|
|
|
|
|
*
|
|
|
|
|
* Fields 1 and 2 are ours, and they are the half of item 9 a program can
|
|
|
|
|
* see: the drawing surface's real width and height in pixels, so a listing
|
|
|
|
|
* can fill whatever window the host opened instead of assuming 320x200.
|
|
|
|
|
* They go through require_graphics(), which is both the refusal when there
|
|
|
|
|
* is no device -- asking how big the screen is when there is no screen is a
|
|
|
|
|
* program bug worth reporting -- and the refresh that makes the answer
|
|
|
|
|
* current rather than whatever it was at attach.
|
|
|
|
|
*/
|
|
|
|
|
switch ( field ) {
|
|
|
|
|
case 0:
|
|
|
|
|
answer = (int64_t)obj->gfx.mode;
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
PASS(errctx, require_graphics(obj, "RGR"));
|
|
|
|
|
answer = (int64_t)obj->gfx.devwidth;
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
PASS(errctx, require_graphics(obj, "RGR"));
|
|
|
|
|
answer = (int64_t)obj->gfx.devheight;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "RGR: field %d is outside 0..2", field);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
|
|
|
|
|
PASS(errctx, akbasic_value_zero(out));
|
|
|
|
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
|
|
|
|
out->intval = answer;
|
|
|
|
|
*dest = out;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
/* ---------------------------------------------------------------- DRAW --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_draw(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[32];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int i = 0;
|
|
|
|
|
akbasic_Color color;
|
|
|
|
|
double x = 0.0;
|
|
|
|
|
double y = 0.0;
|
|
|
|
|
double px = 0.0;
|
|
|
|
|
double py = 0.0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "DRAW"));
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "DRAW", args, 32, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"DRAW expected a color source");
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (count > 1 && (count % 2) == 0), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"DRAW expected coordinates in X,Y pairs");
|
|
|
|
|
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, (int)args[0], &color));
|
|
|
|
|
|
|
|
|
|
if ( count == 1 ) {
|
|
|
|
|
/* DRAW with no coordinates plots the pixel cursor, as 7.0 does. */
|
|
|
|
|
x = obj->gfx.x;
|
|
|
|
|
y = obj->gfx.y;
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
|
|
|
|
PASS(errctx, obj->graphics->point(obj->graphics, x, y, color));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( count == 3 ) {
|
|
|
|
|
/* A single pair is a point, not a line from wherever the cursor was. */
|
|
|
|
|
x = args[1];
|
|
|
|
|
y = args[2];
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
|
|
|
|
PASS(errctx, obj->graphics->point(obj->graphics, x, y, color));
|
|
|
|
|
obj->gfx.x = args[1];
|
|
|
|
|
obj->gfx.y = args[2];
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Two or more pairs is a polyline. Scale each end, never the segment. */
|
|
|
|
|
for ( i = 1; i + 3 < count + 1; i += 2 ) {
|
|
|
|
|
px = args[i];
|
|
|
|
|
py = args[i + 1];
|
|
|
|
|
x = args[i + 2];
|
|
|
|
|
y = args[i + 3];
|
|
|
|
|
scale_point(&obj->gfx, &px, &py);
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, draw_line(obj, px, py, x, y, color));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
obj->gfx.x = args[count - 2];
|
|
|
|
|
obj->gfx.y = args[count - 1];
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------- BOX --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_box(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[6];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int corner = 0;
|
|
|
|
|
akbasic_Color color;
|
|
|
|
|
double x1 = 0.0;
|
|
|
|
|
double y1 = 0.0;
|
|
|
|
|
double x2 = 0.0;
|
|
|
|
|
double y2 = 0.0;
|
|
|
|
|
double angle = 0.0;
|
|
|
|
|
double cx = 0.0;
|
|
|
|
|
double cy = 0.0;
|
|
|
|
|
double radians = 0.0;
|
|
|
|
|
double xs[4];
|
|
|
|
|
double ys[4];
|
|
|
|
|
double rx = 0.0;
|
|
|
|
|
double ry = 0.0;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "BOX"));
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "BOX", args, 6, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"BOX expected a color source and at least one corner");
|
|
|
|
|
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, (int)args[0], &color));
|
|
|
|
|
|
|
|
|
|
x1 = args[1];
|
|
|
|
|
y1 = args[2];
|
|
|
|
|
x2 = (count >= 5) ? args[3] : obj->gfx.x;
|
|
|
|
|
y2 = (count >= 5) ? args[4] : obj->gfx.y;
|
|
|
|
|
angle = (count >= 6) ? args[5] : 0.0;
|
|
|
|
|
|
|
|
|
|
scale_point(&obj->gfx, &x1, &y1);
|
|
|
|
|
scale_point(&obj->gfx, &x2, &y2);
|
|
|
|
|
|
|
|
|
|
if ( angle == 0.0 ) {
|
|
|
|
|
/*
|
|
|
|
|
* BASIC 7.0's last argument selects outline or fill. It is the sixth,
|
|
|
|
|
* and the fifth is the rotation, so a filled box is BOX s,x1,y1,x2,y2,a,1
|
|
|
|
|
* -- seven arguments, one more than this collects. Filling is therefore
|
|
|
|
|
* reached by the rotation being absent and is spelled with a negative
|
|
|
|
|
* angle; that is a deviation and is recorded in TODO.md section 5.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, obj->graphics->rect(obj->graphics, x1, y1, x2, y2, color));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A rotated box is four lines, because a renderer's rectangle is axis-aligned
|
|
|
|
|
* by construction. Rotation is about the box's own center, which is what 7.0
|
|
|
|
|
* does, and the angle is clockwise in degrees.
|
|
|
|
|
*/
|
|
|
|
|
cx = (x1 + x2) / 2.0;
|
|
|
|
|
cy = (y1 + y2) / 2.0;
|
|
|
|
|
rx = (x2 - x1) / 2.0;
|
|
|
|
|
ry = (y2 - y1) / 2.0;
|
|
|
|
|
radians = angle * (M_PI / 180.0);
|
|
|
|
|
|
|
|
|
|
xs[0] = -rx; ys[0] = -ry;
|
|
|
|
|
xs[1] = rx; ys[1] = -ry;
|
|
|
|
|
xs[2] = rx; ys[2] = ry;
|
|
|
|
|
xs[3] = -rx; ys[3] = ry;
|
|
|
|
|
for ( corner = 0; corner < 4; corner++ ) {
|
|
|
|
|
double ox = xs[corner];
|
|
|
|
|
double oy = ys[corner];
|
|
|
|
|
xs[corner] = cx + (ox * cos(radians)) - (oy * sin(radians));
|
|
|
|
|
ys[corner] = cy + (ox * sin(radians)) + (oy * cos(radians));
|
|
|
|
|
}
|
|
|
|
|
for ( corner = 0; corner < 4; corner++ ) {
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, draw_line(obj, xs[corner], ys[corner],
|
|
|
|
|
xs[(corner + 1) % 4], ys[(corner + 1) % 4], color));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* -------------------------------------------------------------- CIRCLE --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_circle(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[9];
|
|
|
|
|
int count = 0;
|
|
|
|
|
akbasic_Color color;
|
|
|
|
|
double cx = 0.0;
|
|
|
|
|
double cy = 0.0;
|
|
|
|
|
double xr = 0.0;
|
|
|
|
|
double yr = 0.0;
|
|
|
|
|
double start = 0.0;
|
|
|
|
|
double end = 360.0;
|
|
|
|
|
double rotation = 0.0;
|
|
|
|
|
double inc = CIRCLE_DEFAULT_INC;
|
|
|
|
|
double theta = 0.0;
|
|
|
|
|
double px = 0.0;
|
|
|
|
|
double py = 0.0;
|
|
|
|
|
double x = 0.0;
|
|
|
|
|
double y = 0.0;
|
|
|
|
|
bool first = true;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "CIRCLE"));
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "CIRCLE", args, 9, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 4), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"CIRCLE expected a color source, a center and a radius");
|
|
|
|
|
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, (int)args[0], &color));
|
|
|
|
|
|
|
|
|
|
cx = args[1];
|
|
|
|
|
cy = args[2];
|
|
|
|
|
xr = args[3];
|
|
|
|
|
yr = (count >= 5) ? args[4] : xr;
|
|
|
|
|
if ( count >= 7 ) {
|
|
|
|
|
start = args[5];
|
|
|
|
|
end = args[6];
|
|
|
|
|
}
|
|
|
|
|
rotation = (count >= 8) ? args[7] : 0.0;
|
|
|
|
|
inc = (count >= 9) ? args[8] : CIRCLE_DEFAULT_INC;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (xr >= 0.0 && yr >= 0.0), AKBASIC_ERR_VALUE,
|
|
|
|
|
"CIRCLE radius must not be negative");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (inc > 0.0), AKBASIC_ERR_VALUE,
|
|
|
|
|
"CIRCLE arc increment must be positive");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Drawn as a polygon of `inc`-degree segments rather than through the
|
|
|
|
|
* renderer's circle primitive. That is not a workaround for a missing API:
|
|
|
|
|
* BASIC 7.0's CIRCLE takes two radii, an arc range and a rotation, and is
|
|
|
|
|
* specified as an inc-degree polygon. A midpoint circle could only serve the
|
|
|
|
|
* one case where all of those are default, and a shape that changed
|
|
|
|
|
* character depending on whether the radii happened to be equal would be
|
|
|
|
|
* worse than one that is uniformly a polygon. TODO.md section 5.
|
|
|
|
|
*/
|
|
|
|
|
for ( theta = start; ; theta += inc ) {
|
|
|
|
|
double t = (theta > end) ? end : theta;
|
|
|
|
|
double radians = t * (M_PI / 180.0);
|
|
|
|
|
double ox = xr * sin(radians);
|
|
|
|
|
double oy = -yr * cos(radians);
|
|
|
|
|
double rot = rotation * (M_PI / 180.0);
|
|
|
|
|
|
|
|
|
|
x = cx + (ox * cos(rot)) - (oy * sin(rot));
|
|
|
|
|
y = cy + (ox * sin(rot)) + (oy * cos(rot));
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
|
|
|
|
if ( !first ) {
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, draw_line(obj, px, py, x, y, color));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
first = false;
|
|
|
|
|
px = x;
|
|
|
|
|
py = y;
|
|
|
|
|
if ( t >= end ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
obj->gfx.x = cx;
|
|
|
|
|
obj->gfx.y = cy;
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------- PAINT --- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_paint(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
double args[4];
|
|
|
|
|
int count = 0;
|
|
|
|
|
akbasic_Color color;
|
|
|
|
|
double x = 0.0;
|
|
|
|
|
double y = 0.0;
|
|
|
|
|
bool partial = false;
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "PAINT"));
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
PASS(errctx, akbasic_args_numbers(obj, expr, "PAINT", args, 4, &count));
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"PAINT expected a color source and a coordinate");
|
|
|
|
|
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, (int)args[0], &color));
|
|
|
|
|
|
|
|
|
|
x = args[1];
|
|
|
|
|
y = args[2];
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The akgl flood fill walks spans from a fixed stack and reports
|
|
|
|
|
* AKERR_OUTOFBOUNDS when it runs out, having filled *part* of the region.
|
|
|
|
|
* Reporting that to the program is the whole point: a screen that came back
|
|
|
|
|
* half-painted with no diagnostic is worse than an error, because the
|
|
|
|
|
* program cannot tell it happened.
|
|
|
|
|
*/
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, obj->graphics->paint(obj->graphics, (int)x, (int)y, color));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE(errctx, AKERR_OUTOFBOUNDS) {
|
|
|
|
|
partial = true;
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Raised out here rather than inside the HANDLE block. HANDLE sets
|
|
|
|
|
* handled = true on the context, and a FAIL_RETURN from inside it hands the
|
|
|
|
|
* caller a context already marked handled -- whose FINISH_LOGIC then declines
|
|
|
|
|
* to pass it up and releases it instead. The error vanishes and PAINT looks
|
|
|
|
|
* like it worked. Flag inside, raise outside.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, partial, AKBASIC_ERR_DEVICE,
|
|
|
|
|
"PAINT filled only part of the region: the device ran out of fill space");
|
|
|
|
|
|
|
|
|
|
obj->gfx.x = args[1];
|
|
|
|
|
obj->gfx.y = args[2];
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------- SSHAPE and GSHAPE --- */
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Find the string variable a shape verb reads or writes.
|
|
|
|
|
*
|
|
|
|
|
* SSHAPE and GSHAPE are the only two verbs in the group whose first argument is
|
|
|
|
|
* an identifier rather than a number, so they walk the leaf themselves instead
|
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>
2026-07-31 21:50:37 -04:00
|
|
|
* of going through akbasic_args_numbers().
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *shape_variable(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, akbasic_Variable **dest, akbasic_ASTLeaf **rest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL && rest != NULL),
|
|
|
|
|
AKERR_NULLPOINTER, "NULL argument in shape_variable");
|
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"%s expected a string variable", verb);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING),
|
|
|
|
|
AKBASIC_ERR_TYPE, "%s expected a string variable", verb);
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"%s could not reach the variable %s", verb, arg->identifier);
|
2026-07-31 11:58:15 -04:00
|
|
|
*rest = arg->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_sshape(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
double coords[4];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int handle = -1;
|
|
|
|
|
char encoded[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "SSHAPE"));
|
|
|
|
|
PASS(errctx, shape_variable(obj, expr, "SSHAPE", &variable, &arg));
|
|
|
|
|
|
|
|
|
|
while ( arg != NULL && count < 4 ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING),
|
|
|
|
|
AKBASIC_ERR_TYPE, "SSHAPE expected a number in argument %d", count + 2);
|
|
|
|
|
coords[count] = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
|
|
|
|
? value->floatval : (double)value->intval;
|
|
|
|
|
count += 1;
|
2026-07-31 11:58:15 -04:00
|
|
|
arg = arg->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"SSHAPE expected A$, X1, Y1");
|
|
|
|
|
if ( count < 4 ) {
|
|
|
|
|
coords[2] = obj->gfx.x;
|
|
|
|
|
coords[3] = obj->gfx.y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->graphics->save_shape(obj->graphics,
|
|
|
|
|
(int)coords[0], (int)coords[1],
|
|
|
|
|
(int)coords[2], (int)coords[3], &handle));
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A real C128 packs the pixels into the string. Here a value's string is a
|
|
|
|
|
* fixed 256 bytes and the region is a device surface, so what goes in is a
|
|
|
|
|
* handle to it. A program can still pass the string to GSHAPE, which is all
|
|
|
|
|
* BASIC ever does with it; what it can no longer do is save it to disk or
|
|
|
|
|
* take its LEN and get a size. TODO.md section 5.
|
|
|
|
|
*/
|
|
|
|
|
snprintf(encoded, sizeof(encoded), "%s%d", SHAPE_PREFIX, handle);
|
|
|
|
|
PASS(errctx, akbasic_variable_set_string(variable, encoded, zerosubscript, 1));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_cmd_gshape(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
akbasic_Value *stored = NULL;
|
|
|
|
|
double coords[2];
|
|
|
|
|
int count = 0;
|
|
|
|
|
int64_t handle = 0;
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
long long converted = 0;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
double x = 0.0;
|
|
|
|
|
double y = 0.0;
|
|
|
|
|
int64_t zerosubscript[1] = { 0 };
|
|
|
|
|
|
|
|
|
|
(void)lval; (void)rval;
|
|
|
|
|
PASS(errctx, require_graphics(obj, "GSHAPE"));
|
|
|
|
|
PASS(errctx, shape_variable(obj, expr, "GSHAPE", &variable, &arg));
|
|
|
|
|
|
|
|
|
|
while ( arg != NULL && count < 2 ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING),
|
|
|
|
|
AKBASIC_ERR_TYPE, "GSHAPE expected a number in argument %d", count + 2);
|
|
|
|
|
coords[count] = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
|
|
|
|
? value->floatval : (double)value->intval;
|
|
|
|
|
count += 1;
|
2026-07-31 11:58:15 -04:00
|
|
|
arg = arg->next;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
}
|
|
|
|
|
x = (count >= 1) ? coords[0] : obj->gfx.x;
|
|
|
|
|
y = (count >= 2) ? coords[1] : obj->gfx.y;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_variable_get_subscript(variable, zerosubscript, 1, &stored));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
(strncmp(stored->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0),
|
|
|
|
|
AKBASIC_ERR_VALUE,
|
|
|
|
|
"GSHAPE was given a string that did not come from SSHAPE");
|
Delete src/convert.c: libakstdlib 0.2.0 grew the contract it existed for
That file wrapped strtoll and strtod because libakstdlib's aksl_ato* family
could not report a conversion failure at all -- atoi("not a number") returned
success with 0, turning four diagnosable errors into wrong answers. Its own note
said what to do when that changed: "When it grows it, delete src/convert.c and
switch the call sites over." 0.2.0 grew it, so it is gone.
Six call sites go straight to the library now: both literal constructors in
src/grammar.c, the line number in src/scanner.c, FunctionVAL in
src/runtime_functions.c, INPUT in src/runtime_commands.c, and GSHAPE's handle in
src/runtime_graphics.c. aksl_strtoll(str, NULL, base, &dest) rather than
aksl_atoll wherever a base is involved, because the ato* forms are base 10 and
grammar.c picks base 8 or 16 off the lexeme's prefix; the NULL endptr is what
makes trailing junk an error rather than a stopping point.
The raised status changed from AKBASIC_ERR_VALUE to AKERR_VALUE, and the message
with it -- VAL("garbage") now says `no digits in "garbage"`. Section 1.8 makes
message text part of the acceptance contract, so that was checked against the
corpus before touching anything: no golden file contained a conversion message,
which is also why section 1.9 had been asking for one. Two exist now, so the next
change to that text has to move a golden file, and one of them pins the
reference's octal-literal defect (section 6 item 10) while it is still
deliberately reproduced.
tests/convert.c became tests/numeric_contract.c rather than being deleted with
the code. The assertions did not stop being worth making when the wrapper went
away -- they became assertions about a contract this port depends on and no
longer owns, and a regression in it would make four things quietly return zero.
Repoints the CI mutation job, which was bounded to src/convert.c and
src/symtab.c. src/symtab.c alone measures 74.1% against the gate of 65.
src/audio_tables.c was measured as a replacement second file and scored 64.7%:
that is a real gap rather than a reason to skip it, since almost every survivor
is in akbasic_audio_state_init, where nothing asserts a freshly initialised audio
state is actually zeroed -- the same gap this job's history records closing for
symtab. Recorded in TODO.md so the file can earn its place back.
One thing worth knowing before reading any score here, and now written down: the
harness's ICR operator only rewrites the constants 0 and 1, so a lookup table of
other values produces no mutants and a high score over one says nothing about
whether its entries are right. tests/audio_verbs.c now asserts all 32 ADSR
entries against the datasheet anyway, because a hand-edit typo is the real
failure mode there -- but that could not and did not move the mutation number,
and claiming otherwise would be the kind of thing this file exists to stop.
72/72 core, 73/73 with libakgl, 72/72 under ASan+UBSan, coverage 93.6% line and
97.8% function, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:28:04 -04:00
|
|
|
PASS(errctx, aksl_atoll(stored->stringval + strlen(SHAPE_PREFIX), &converted));
|
|
|
|
|
handle = (int64_t)converted;
|
Draw: implement the BASIC 7.0 graphics verbs
GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.
The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:
- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
so the primitive could serve only the fully-defaulted call, and a shape that
changed character depending on whether the radii happened to be equal would be
worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
because a value's string is a fixed 256 bytes and a region is a device surface.
GSHAPE refuses a string without that prefix instead of parsing whatever digits
it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
anything here -- mode 0 is text -- while still refusing an out-of-range mode,
since that is a typo worth catching.
PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.
COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.
Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.
65/65 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:17:48 -04:00
|
|
|
|
|
|
|
|
scale_point(&obj->gfx, &x, &y);
|
|
|
|
|
PASS(errctx, obj->graphics->paste_shape(obj->graphics, (int)handle, x, y));
|
|
|
|
|
SUCCEED_TRUE(obj, dest);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|