Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
/**
|
|
|
|
|
* @file runtime.c
|
|
|
|
|
* @brief Implements the interpreter core: pools, evaluation and the step loop.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
#include <akbasic/parser.h>
|
|
|
|
|
#include <akbasic/runtime.h>
|
|
|
|
|
#include <akbasic/scanner.h>
|
|
|
|
|
#include <akbasic/verbs.h>
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ pools -- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in new_variable");
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) {
|
|
|
|
|
if ( !obj->variables[i].used ) {
|
|
|
|
|
memset(&obj->variables[i], 0, sizeof(obj->variables[i]));
|
|
|
|
|
obj->variables[i].used = true;
|
|
|
|
|
*dest = &obj->variables[i];
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "Maximum runtime variables reached");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in new_function");
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_FUNCTIONS; i++ ) {
|
|
|
|
|
if ( !obj->functions[i].used ) {
|
|
|
|
|
memset(&obj->functions[i], 0, sizeof(obj->functions[i]));
|
|
|
|
|
obj->functions[i].used = true;
|
|
|
|
|
obj->functions[i].leafpool.next = 0;
|
|
|
|
|
obj->functions[i].leafpool.capacity = AKBASIC_MAX_LEAVES * 2;
|
|
|
|
|
obj->functions[i].leafpool.leaves = obj->functions[i].leafstorage;
|
|
|
|
|
*dest = &obj->functions[i];
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "Maximum function definitions reached");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *env_acquire(akbasic_Runtime *obj, akbasic_Environment **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = 0; i < AKBASIC_MAX_ENVIRONMENTS; i++ ) {
|
|
|
|
|
if ( !obj->environments[i].used ) {
|
|
|
|
|
obj->environments[i].used = true;
|
|
|
|
|
*dest = &obj->environments[i];
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_ENVIRONMENT,
|
|
|
|
|
"Environment pool exhausted at line %" PRId64 " (%d in use)",
|
|
|
|
|
(obj->environment == NULL ? 0 : obj->environment->lineno),
|
|
|
|
|
AKBASIC_MAX_ENVIRONMENTS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_new_environment(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Environment *env = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in new_environment");
|
|
|
|
|
PASS(errctx, env_acquire(obj, &env));
|
|
|
|
|
PASS(errctx, akbasic_environment_init(env, obj, obj->environment));
|
|
|
|
|
obj->environment = env;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Environment *popped = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
|
|
|
|
"No previous environment to return to");
|
|
|
|
|
popped = obj->environment;
|
|
|
|
|
obj->environment = popped->parent;
|
|
|
|
|
/*
|
|
|
|
|
* Release it. The reference never does, which is a leak the GC papers over;
|
|
|
|
|
* here the pool is finite, so an unreleased environment is a bug that shows
|
|
|
|
|
* up as exhaustion a few thousand GOSUBs later.
|
|
|
|
|
*/
|
|
|
|
|
popped->used = false;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------- lifecycle -- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_zero(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in zero");
|
|
|
|
|
PASS(errctx, akbasic_environment_zero(obj->environment));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in init");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (sink != NULL), AKERR_NULLPOINTER, "NULL text sink in init");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Claim the status band before anything can raise one of our codes, or the
|
|
|
|
|
* first error out of this function prints "Unknown Error". Idempotent, so a
|
|
|
|
|
* host that already called it loses nothing.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_error_register());
|
|
|
|
|
|
|
|
|
|
memset(obj, 0, sizeof(*obj));
|
|
|
|
|
obj->sink = sink;
|
|
|
|
|
obj->environment = NULL;
|
|
|
|
|
obj->autoLineNumber = 0;
|
|
|
|
|
obj->eval_clone_identifiers = true;
|
|
|
|
|
obj->errclass = AKBASIC_ERRCLASS_NONE;
|
|
|
|
|
obj->mode = AKBASIC_MODE_REPL;
|
|
|
|
|
obj->run_finished_mode = AKBASIC_MODE_REPL;
|
|
|
|
|
obj->inputEof = false;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_valuepool_init(&obj->valuepool));
|
|
|
|
|
PASS(errctx, akbasic_value_zero(&obj->staticTrueValue));
|
|
|
|
|
PASS(errctx, akbasic_value_zero(&obj->staticFalseValue));
|
|
|
|
|
PASS(errctx, akbasic_value_set_bool(&obj->staticTrueValue, true));
|
|
|
|
|
PASS(errctx, akbasic_value_set_bool(&obj->staticFalseValue, false));
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
|
|
|
|
PASS(errctx, akbasic_runtime_zero(obj));
|
|
|
|
|
PASS(errctx, akbasic_scanner_zero(obj));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Reach hardware through backend records, and take the time from the host
Groups G, I and E are unblocked but cannot be written yet: the core library is
free of SDL and builds with no libakgl present, so a graphics verb cannot call
akgl_draw_* and a sound verb cannot call akgl_audio_*. This adds what they call
instead.
Three records of function pointers -- akbasic_GraphicsBackend, _AudioBackend and
_InputBackend -- in the same shape as akbasic_TextSink, and the same shape
libakgl uses for akgl_RenderBackend. akbasic_runtime_set_devices() attaches any
subset; all three may be NULL and that is the standalone driver's normal state,
so a runtime with no backends still comes up and still prints. A verb that needs
one it was not given raises the new AKBASIC_ERR_DEVICE rather than dereferencing
a NULL vtable.
Two decisions worth stating. The graphics record has no circle entry point:
BASIC 7.0's CIRCLE takes two radii, an arc range, a rotation and a degree
increment, which makes it a polygon by definition, so it will be built from line
calls rather than from akgl_draw_circle. And coordinates are double rather than
an integer pixel address, because SCALE makes them fractional and rounding at
each verb rather than once at the backend accumulates drift along a polyline.
akbasic_runtime_settime() is how SOUND, PLAY and TEMPO get a clock without the
library reading one. Section 1.6 forbids blocking or owning a loop, so the caller
that owns the frame owns the time -- which is what libakgl already does, since
akgl_actor_logic_changeframe takes curtimems as an argument. Left unset it is
zero and every duration expires immediately: audible, but never a hang.
AKBASIC_ERR_LAST is a sentinel rather than a status, so tests/error_codes.c walks
every code looking for an unnamed one without anybody remembering to widen the
loop when a code is added.
tests/mockdevice.h records every backend call as a formatted line. The graphics
and audio verbs emit nothing a golden file can compare, so that log is where
their assertions have to live -- and since it needs no SDL, the whole of groups
G, I and E stays testable in the default build.
62/62 ctest, clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:07:23 -04:00
|
|
|
akerr_ErrorContext *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL runtime in set_devices");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* No validation of the records themselves. A backend with a NULL entry point
|
|
|
|
|
* is caught at the call site by the verb that needs it, which can say which
|
|
|
|
|
* verb wanted what -- checking every pointer here would only be able to say
|
|
|
|
|
* that something, somewhere, was incomplete.
|
|
|
|
|
*/
|
|
|
|
|
obj->graphics = graphics;
|
|
|
|
|
obj->audio = audio;
|
|
|
|
|
obj->input = input;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL runtime in settime");
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Deliberately not rejecting a time that moves backwards. A host is free to
|
|
|
|
|
* drive this from a paused, scrubbed or replayed clock, and the only thing
|
|
|
|
|
* that happens is a note holding longer than it asked to.
|
|
|
|
|
*/
|
|
|
|
|
obj->timems = timems;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
/* ---------------------------------------------------------------- output -- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_write(akbasic_Runtime *obj, const char *text)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in write");
|
|
|
|
|
PASS(errctx, obj->sink->write(obj->sink, text));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_println(akbasic_Runtime *obj, const char *text)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in println");
|
|
|
|
|
PASS(errctx, obj->sink->writeln(obj->sink, text));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char *errclass_to_string(akbasic_ErrorClass errclass)
|
|
|
|
|
{
|
|
|
|
|
switch ( errclass ) {
|
|
|
|
|
case AKBASIC_ERRCLASS_IO: return "IO ERROR";
|
|
|
|
|
case AKBASIC_ERRCLASS_PARSE: return "PARSE ERROR";
|
|
|
|
|
case AKBASIC_ERRCLASS_RUNTIME: return "RUNTIME ERROR";
|
|
|
|
|
case AKBASIC_ERRCLASS_SYNTAX: return "SYNTAX ERROR";
|
|
|
|
|
default: return "UNDEF";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorClass errclass, const char *message)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char line[AKBASIC_MAX_LINE_LENGTH * 2];
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && message != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in runtime error");
|
|
|
|
|
obj->errclass = errclass;
|
|
|
|
|
/*
|
|
|
|
|
* The format, the trailing \n inside the string, and the second newline
|
|
|
|
|
* writeln adds are all part of the acceptance contract --
|
|
|
|
|
* tests/language/array_outofbounds.txt ends in 0a 0a. See TODO.md 1.8.
|
|
|
|
|
*/
|
|
|
|
|
snprintf(line, sizeof(line), "? %" PRId64 " : %s %s\n",
|
|
|
|
|
obj->environment->lineno, errclass_to_string(errclass), message);
|
|
|
|
|
PASS(errctx, akbasic_runtime_println(obj, line));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in set_mode");
|
|
|
|
|
obj->mode = mode;
|
|
|
|
|
if ( obj->mode == AKBASIC_MODE_REPL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_println(obj, "READY"));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------ evaluation -- */
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Report a runtime error carrying an akerr message, then re-raise. Used where
|
|
|
|
|
* the reference calls basicError() and returns the error: the BASIC-visible line
|
|
|
|
|
* goes to the sink and the context keeps propagating.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext *report_and_reraise(akbasic_Runtime *obj, akerr_ErrorContext *cause)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
|
|
|
int status = cause->status;
|
|
|
|
|
|
|
|
|
|
snprintf(message, sizeof(message), "%s", cause->message);
|
|
|
|
|
cause->handled = true;
|
|
|
|
|
IGNORE(akerr_release_error(cause));
|
|
|
|
|
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_RUNTIME, message));
|
|
|
|
|
FAIL_RETURN(errctx, status, "%s", message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *evaluate_identifier(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_ASTLeaf *texpr = NULL;
|
|
|
|
|
akbasic_Value *tval = NULL;
|
|
|
|
|
akbasic_Value *slot = NULL;
|
|
|
|
|
akbasic_Value *copy = NULL;
|
|
|
|
|
akbasic_Variable *variable = NULL;
|
|
|
|
|
int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH];
|
|
|
|
|
int subscriptcount = 0;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A .right hanging off an identifier is an array subscript only when it is
|
|
|
|
|
* an ARRAY_SUBSCRIPT argument list; anything else belongs to the enclosing
|
|
|
|
|
* expression and must not be followed.
|
|
|
|
|
*/
|
|
|
|
|
texpr = expr->right;
|
|
|
|
|
if ( texpr != NULL &&
|
|
|
|
|
texpr->leaftype == AKBASIC_LEAF_ARGUMENTLIST &&
|
|
|
|
|
texpr->operator_ == AKBASIC_TOK_ARRAY_SUBSCRIPT ) {
|
|
|
|
|
for ( texpr = texpr->right; texpr != NULL; texpr = texpr->right ) {
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (subscriptcount < AKBASIC_MAX_ARRAY_DEPTH),
|
|
|
|
|
AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"More than %d array subscripts", AKBASIC_MAX_ARRAY_DEPTH);
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, texpr, &tval));
|
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (tval->valuetype != AKBASIC_TYPE_INTEGER),
|
|
|
|
|
AKBASIC_ERR_TYPE,
|
|
|
|
|
"Array dimensions must evaluate to integer (C)");
|
|
|
|
|
subscripts[subscriptcount] = tval->intval;
|
|
|
|
|
subscriptcount += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( subscriptcount == 0 ) {
|
|
|
|
|
subscripts[0] = 0;
|
|
|
|
|
subscriptcount = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->identifier, &variable));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"Identifier %s is undefined", expr->identifier);
|
|
|
|
|
PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &slot));
|
|
|
|
|
|
|
|
|
|
if ( !obj->eval_clone_identifiers ) {
|
|
|
|
|
*dest = slot;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, ©));
|
|
|
|
|
PASS(errctx, akbasic_value_clone(slot, copy));
|
|
|
|
|
*dest = copy;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *evaluate_binary(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *lval = NULL;
|
|
|
|
|
akbasic_Value *rval = NULL;
|
|
|
|
|
akbasic_Value *scratch = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, &lval));
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &rval));
|
|
|
|
|
|
|
|
|
|
if ( expr->operator_ == AKBASIC_TOK_ASSIGNMENT ) {
|
|
|
|
|
PASS(errctx, akbasic_environment_assign(obj->environment, expr->left, rval, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &scratch));
|
|
|
|
|
switch ( expr->operator_ ) {
|
|
|
|
|
case AKBASIC_TOK_MINUS:
|
|
|
|
|
PASS(errctx, akbasic_value_math_minus(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_PLUS:
|
|
|
|
|
PASS(errctx, akbasic_value_math_plus(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_LEFT_SLASH:
|
|
|
|
|
PASS(errctx, akbasic_value_math_divide(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_STAR:
|
|
|
|
|
PASS(errctx, akbasic_value_math_multiply(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_AND:
|
|
|
|
|
PASS(errctx, akbasic_value_bitwise_and(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_OR:
|
|
|
|
|
PASS(errctx, akbasic_value_bitwise_or(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_LESS_THAN:
|
|
|
|
|
PASS(errctx, akbasic_value_less_than(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_LESS_THAN_EQUAL:
|
|
|
|
|
PASS(errctx, akbasic_value_less_than_equal(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_EQUAL:
|
|
|
|
|
PASS(errctx, akbasic_value_is_equal(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_NOT_EQUAL:
|
|
|
|
|
PASS(errctx, akbasic_value_is_not_equal(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_GREATER_THAN:
|
|
|
|
|
PASS(errctx, akbasic_value_greater_than(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_TOK_GREATER_THAN_EQUAL:
|
|
|
|
|
PASS(errctx, akbasic_value_greater_than_equal(lval, rval, scratch, dest));
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Don't know how to perform binary operation %d", (int)expr->operator_);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_Value *lval = NULL;
|
|
|
|
|
akbasic_Value *rval = NULL;
|
|
|
|
|
akbasic_Value *scratch = NULL;
|
|
|
|
|
const akbasic_Verb *verb = NULL;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in evaluate");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NULL expression in evaluate");
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &lval));
|
|
|
|
|
PASS(errctx, akbasic_value_zero(lval));
|
|
|
|
|
*dest = lval;
|
|
|
|
|
|
|
|
|
|
switch ( expr->leaftype ) {
|
|
|
|
|
case AKBASIC_LEAF_GROUPING:
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->expr, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_BRANCH:
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_runtime_evaluate(obj, expr->expr, &rval));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE_DEFAULT(errctx) {
|
|
|
|
|
PASS(errctx, report_and_reraise(obj, errctx));
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
if ( rval->boolvalue == AKBASIC_TRUE ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
if ( expr->right != NULL ) {
|
|
|
|
|
/* A false branch is optional for some branching operations. */
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER_INT:
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER_FLOAT:
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER_STRING:
|
|
|
|
|
PASS(errctx, evaluate_identifier(obj, expr, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_IDENTIFIER:
|
|
|
|
|
/* A bare identifier with no type suffix is a label. */
|
|
|
|
|
lval->valuetype = AKBASIC_TYPE_INTEGER;
|
|
|
|
|
PASS(errctx, akbasic_environment_get_label(obj->environment, expr->identifier, &lval->intval));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_LITERAL_INT:
|
|
|
|
|
lval->valuetype = AKBASIC_TYPE_INTEGER;
|
|
|
|
|
lval->intval = expr->literal_int;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_LITERAL_FLOAT:
|
|
|
|
|
lval->valuetype = AKBASIC_TYPE_FLOAT;
|
|
|
|
|
lval->floatval = expr->literal_float;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_LITERAL_STRING:
|
|
|
|
|
lval->valuetype = AKBASIC_TYPE_STRING;
|
|
|
|
|
memcpy(lval->stringval, expr->literal_string, sizeof(lval->stringval));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_UNARY:
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &rval));
|
|
|
|
|
PASS(errctx, akbasic_environment_new_value(obj->environment, &scratch));
|
|
|
|
|
if ( expr->operator_ == AKBASIC_TOK_MINUS ) {
|
|
|
|
|
PASS(errctx, akbasic_value_invert(rval, scratch, dest));
|
|
|
|
|
} else if ( expr->operator_ == AKBASIC_TOK_NOT ) {
|
|
|
|
|
PASS(errctx, akbasic_value_bitwise_not(rval, scratch, dest));
|
|
|
|
|
} else {
|
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX,
|
|
|
|
|
"Don't know how to perform operation %d on unary type %d",
|
|
|
|
|
(int)expr->operator_, (int)rval->valuetype);
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_FUNCTION:
|
|
|
|
|
PASS(errctx, akbasic_verb_lookup(expr->identifier, &verb));
|
|
|
|
|
if ( verb != NULL && verb->exec != NULL && verb->tokentype == AKBASIC_TOK_FUNCTION ) {
|
|
|
|
|
PASS(errctx, verb->exec(obj, expr, lval, rval, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_runtime_user_function(obj, expr, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_COMMAND_IMMEDIATE:
|
|
|
|
|
case AKBASIC_LEAF_COMMAND:
|
|
|
|
|
PASS(errctx, akbasic_verb_lookup(expr->identifier, &verb));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (verb != NULL && verb->exec != NULL), AKBASIC_ERR_UNDEFINED,
|
|
|
|
|
"Unknown command %s", expr->identifier);
|
|
|
|
|
PASS(errctx, verb->exec(obj, expr, lval, rval, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
case AKBASIC_LEAF_BINARY:
|
|
|
|
|
PASS(errctx, evaluate_binary(obj, expr, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in interpret");
|
|
|
|
|
/*
|
|
|
|
|
* While an environment is skipping forward to a verb, nothing runs but that
|
|
|
|
|
* verb. This is what keeps a zero-iteration FOR body from executing, given
|
|
|
|
|
* that the loop condition is evaluated at the bottom of the structure.
|
|
|
|
|
*/
|
|
|
|
|
if ( akbasic_environment_is_waiting_for_any(obj->environment) ) {
|
|
|
|
|
if ( expr->leaftype != AKBASIC_LEAF_COMMAND ||
|
|
|
|
|
!akbasic_environment_is_waiting_for(obj->environment, expr->identifier) ) {
|
|
|
|
|
*dest = &obj->staticTrueValue;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_runtime_evaluate(obj, expr, dest));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE_DEFAULT(errctx) {
|
|
|
|
|
PASS(errctx, report_and_reraise(obj, errctx));
|
|
|
|
|
} FINISH(errctx, true);
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_interpret_immediate(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in interpret_immediate");
|
|
|
|
|
*dest = NULL;
|
|
|
|
|
if ( expr->leaftype != AKBASIC_LEAF_COMMAND_IMMEDIATE ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr, dest));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_FunctionDef *fndef = NULL;
|
|
|
|
|
akbasic_Environment *targetenv = obj->environment;
|
|
|
|
|
akbasic_ASTLeaf *leafptr = NULL;
|
|
|
|
|
akbasic_ASTLeaf *argptr = NULL;
|
|
|
|
|
akbasic_Value *argvalue = NULL;
|
|
|
|
|
akbasic_Value *unused = NULL;
|
|
|
|
|
void *fnptr = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_environment_get_function(obj->environment, expr->identifier, &fnptr));
|
|
|
|
|
fndef = (akbasic_FunctionDef *)fnptr;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The function's environment is owned by the funcdef, not by the pool free
|
|
|
|
|
* list: it is reset on every call and outlives any single one. The reference
|
|
|
|
|
* holds it by value inside BasicFunctionDef for the same reason.
|
|
|
|
|
*/
|
|
|
|
|
if ( fndef->environment == NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
|
|
|
|
fndef->environment = obj->environment;
|
|
|
|
|
obj->environment = targetenv;
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akbasic_environment_init(fndef->environment, obj, obj->environment));
|
|
|
|
|
|
|
|
|
|
/* Bind arguments into the function's scope before entering it. */
|
|
|
|
|
leafptr = (expr->right != NULL ? expr->right->right : NULL);
|
|
|
|
|
argptr = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
|
|
|
|
|
while ( leafptr != NULL && argptr != NULL ) {
|
|
|
|
|
akbasic_Environment *callerenv = obj->environment;
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, leafptr, &argvalue));
|
|
|
|
|
obj->environment = fndef->environment;
|
|
|
|
|
PASS(errctx, akbasic_environment_assign(fndef->environment, argptr, argvalue, &unused));
|
|
|
|
|
obj->environment = callerenv;
|
|
|
|
|
leafptr = leafptr->right;
|
|
|
|
|
argptr = argptr->right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj->environment = fndef->environment;
|
|
|
|
|
|
|
|
|
|
if ( fndef->expression != NULL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, fndef->expression, dest));
|
|
|
|
|
obj->environment = obj->environment->parent;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A multi-line subroutine. Hand control to its environment and let the
|
|
|
|
|
* caller's step loop run it until RETURN pops back out. The result is the
|
|
|
|
|
* value RETURN parked in the child environment.
|
|
|
|
|
*/
|
|
|
|
|
obj->environment->gosubReturnLine = obj->environment->lineno + 1;
|
|
|
|
|
obj->environment->nextline = fndef->lineno;
|
|
|
|
|
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_process_line_run(obj));
|
|
|
|
|
}
|
|
|
|
|
*dest = &fndef->environment->returnValue;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------ line cycle -- */
|
|
|
|
|
|
|
|
|
|
int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
int64_t i = 0;
|
|
|
|
|
|
|
|
|
|
for ( i = obj->environment->lineno - 1; i > 0; i-- ) {
|
|
|
|
|
if ( obj->source[i].code[0] != '\0' ) {
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return obj->environment->lineno;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (lineno >= 0 && lineno < AKBASIC_MAX_SOURCE_LINES),
|
|
|
|
|
AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"Line number %" PRId64 " is outside 0..%d",
|
|
|
|
|
lineno, AKBASIC_MAX_SOURCE_LINES - 1);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (strlen(code) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"Source line exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1);
|
|
|
|
|
strncpy(obj->source[lineno].code, code, AKBASIC_MAX_LINE_LENGTH - 1);
|
|
|
|
|
obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0';
|
|
|
|
|
obj->source[lineno].lineno = lineno;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char buffer[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
char scanned[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
bool eof = false;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->sink->readline(obj->sink, buffer, sizeof(buffer), &eof));
|
|
|
|
|
if ( eof ) {
|
|
|
|
|
obj->environment->nextline = 0;
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* All this mode does is pick the line number off the front and file the
|
|
|
|
|
* source line under it. DLOAD reaches this from REPL mode, where the line
|
|
|
|
|
* numbers must be stripped the same way the REPL strips them.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
|
|
|
|
|
if ( obj->mode == AKBASIC_MODE_REPL ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
|
|
|
|
|
} else {
|
|
|
|
|
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, buffer));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char prompt[32];
|
|
|
|
|
char scanned[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
akbasic_Parser parser;
|
|
|
|
|
bool eof = false;
|
|
|
|
|
|
|
|
|
|
if ( obj->autoLineNumber > 0 ) {
|
|
|
|
|
snprintf(prompt, sizeof(prompt), "%" PRId64 " ",
|
|
|
|
|
obj->environment->lineno + obj->autoLineNumber);
|
|
|
|
|
PASS(errctx, akbasic_runtime_write(obj, prompt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, obj->sink->readline(obj->sink, obj->userline, sizeof(obj->userline), &eof));
|
|
|
|
|
if ( eof ) {
|
|
|
|
|
obj->inputEof = true;
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_QUIT));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
if ( obj->userline[0] == '\0' ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj->environment->lineno += obj->autoLineNumber;
|
|
|
|
|
PASS(errctx, akbasic_scanner_scan(obj, obj->userline, scanned, sizeof(scanned)));
|
|
|
|
|
PASS(errctx, akbasic_parser_init(&parser, obj));
|
|
|
|
|
|
|
|
|
|
while ( !akbasic_parser_is_at_end(&parser) ) {
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_parser_parse(&parser, &leaf));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE_DEFAULT(errctx) {
|
|
|
|
|
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
|
|
|
snprintf(message, sizeof(message), "%s", errctx->message);
|
|
|
|
|
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
|
|
|
|
|
} FINISH(errctx, false);
|
|
|
|
|
if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_interpret_immediate(obj, leaf, &value));
|
|
|
|
|
if ( value == NULL ) {
|
|
|
|
|
/* Not an immediate command, so it is program text: file it. */
|
|
|
|
|
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
|
|
|
|
|
} else if ( obj->autoLineNumber > 0 ) {
|
|
|
|
|
obj->environment->lineno = akbasic_runtime_find_previous_lineno(obj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char line[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
akbasic_ASTLeaf *leaf = NULL;
|
|
|
|
|
akbasic_Value *value = NULL;
|
|
|
|
|
akbasic_Parser parser;
|
|
|
|
|
|
|
|
|
|
if ( obj->environment->nextline >= AKBASIC_MAX_SOURCE_LINES ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strncpy(line, obj->source[obj->environment->nextline].code, sizeof(line) - 1);
|
|
|
|
|
line[sizeof(line) - 1] = '\0';
|
|
|
|
|
obj->environment->lineno = obj->environment->nextline;
|
|
|
|
|
obj->environment->nextline += 1;
|
|
|
|
|
if ( line[0] == '\0' ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_scanner_scan(obj, line, NULL, 0));
|
|
|
|
|
PASS(errctx, akbasic_parser_init(&parser, obj));
|
|
|
|
|
|
|
|
|
|
while ( !akbasic_parser_is_at_end(&parser) ) {
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_parser_parse(&parser, &leaf));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE_DEFAULT(errctx) {
|
|
|
|
|
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
|
|
|
snprintf(message, sizeof(message), "%s", errctx->message);
|
|
|
|
|
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
|
|
|
|
|
IGNORE(akbasic_runtime_set_mode(obj, obj->run_finished_mode));
|
|
|
|
|
} FINISH(errctx, false);
|
|
|
|
|
if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The reference discards both results here. An error has already been
|
|
|
|
|
* reported to the sink by interpret(); swallowing the context keeps a
|
|
|
|
|
* BASIC-level error from tearing down the host, which is the whole point
|
|
|
|
|
* of goal 3.
|
|
|
|
|
*/
|
|
|
|
|
ATTEMPT {
|
|
|
|
|
CATCH(errctx, akbasic_runtime_interpret(obj, leaf, &value));
|
|
|
|
|
} CLEANUP {
|
|
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE_DEFAULT(errctx) {
|
|
|
|
|
} FINISH(errctx, false);
|
|
|
|
|
if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------- step loop -- */
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_start(akbasic_Runtime *obj, int mode)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in start");
|
|
|
|
|
obj->run_finished_mode = (mode == AKBASIC_MODE_REPL ? AKBASIC_MODE_REPL : AKBASIC_MODE_QUIT);
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, mode));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the README from the Go version and document embedding
Carries the reference README across, adjusted where C changes the answer: cmake
instead of make, the ak* libraries instead of the go-sdl2 bindings, and the
limits table now includes the three ceilings the Go version did not need because
it called make().
The new "Embedding the interpreter" section is the point of the rewrite, so it
states the four rules the library holds to -- nothing terminates the process,
nothing calls malloc, no file-scope mutable state, the host owns the loop -- and
each was checked against the tree rather than asserted.
Adds akbasic_runtime_load(). Writing the section turned up a real gap: a host
usually already holds its script as a string and wants the sink reserved for
output, and the only path that existed was AKBASIC_MODE_RUNSTREAM reading the
program through the sink's readline, which forces a game to point its output
device at its source text. The alternative was reaching into the header's
"internal API" block for store_line. Neither is something to put in a README.
Adds examples/embed.c, which is the code the README quotes -- a custom sink, a
bounded per-frame run, and the PASS-not-CATCH rule for a loop inside an ATTEMPT.
It is built by every build and registered as a CTest case, so a signature change
breaks the build instead of rotting the document. The README's own snippet is
compiled separately as a check; both were run before committing.
The "What Isn't Implemented / Isn't Working" section leads with the eleven
inherited defects rather than burying them, because five of them were found by
this port and a reader deserves to know that 1 - 2 - 3 computes 1 - 2 before
they hit it. Corrected two claims while verifying: the runtime is 10.1MB rather
than the ~8MB first written, and its largest single cost is the environment pool
at 4.1MB, not the source table.
ctest 60/60; ASan+UBSan 60/60; no warnings under -Wall -Wextra.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:42:08 -04:00
|
|
|
akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *source)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
char line[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
char scanned[AKBASIC_MAX_LINE_LENGTH];
|
|
|
|
|
const char *cursor = NULL;
|
|
|
|
|
const char *eol = NULL;
|
|
|
|
|
size_t length = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (source != NULL), AKERR_NULLPOINTER, "NULL source in load");
|
|
|
|
|
|
|
|
|
|
for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) {
|
|
|
|
|
eol = strchr(cursor, '\n');
|
|
|
|
|
if ( eol == NULL ) {
|
|
|
|
|
eol = cursor + strlen(cursor);
|
|
|
|
|
}
|
|
|
|
|
length = (size_t)(eol - cursor);
|
|
|
|
|
if ( length > 0 && cursor[length - 1] == '\r' ) {
|
|
|
|
|
length -= 1;
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (length < sizeof(line)), AKBASIC_ERR_BOUNDS,
|
|
|
|
|
"Source line of %zu characters exceeds the %d character limit",
|
|
|
|
|
length, AKBASIC_MAX_LINE_LENGTH - 1);
|
|
|
|
|
memcpy(line, cursor, length);
|
|
|
|
|
line[length] = '\0';
|
|
|
|
|
if ( line[0] == '\0' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
* Scanning is what picks the line number off the front and rewrites the
|
|
|
|
|
* line to what follows it -- the same path RUNSTREAM takes, so a program
|
|
|
|
|
* loaded from memory and one read from a file are filed identically.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akbasic_runtime_zero(obj));
|
|
|
|
|
PASS(errctx, akbasic_scanner_zero(obj));
|
|
|
|
|
PASS(errctx, akbasic_scanner_scan(obj, line, scanned, sizeof(scanned)));
|
|
|
|
|
PASS(errctx, akbasic_runtime_store_line(obj, obj->environment->lineno, scanned));
|
|
|
|
|
}
|
|
|
|
|
obj->environment->nextline = 0;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
Port the BASIC interpreter from Go to C
Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.
The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.
Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.
Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.
src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.
Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.
Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.
The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.
ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:56 -04:00
|
|
|
akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in step");
|
|
|
|
|
|
|
|
|
|
if ( obj->mode == AKBASIC_MODE_QUIT ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PASS(errctx, akbasic_runtime_zero(obj));
|
|
|
|
|
PASS(errctx, akbasic_scanner_zero(obj));
|
|
|
|
|
|
|
|
|
|
switch ( obj->mode ) {
|
|
|
|
|
case AKBASIC_MODE_RUNSTREAM:
|
|
|
|
|
PASS(errctx, akbasic_runtime_process_line_runstream(obj));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_MODE_REPL:
|
|
|
|
|
PASS(errctx, akbasic_runtime_process_line_repl(obj));
|
|
|
|
|
break;
|
|
|
|
|
case AKBASIC_MODE_RUN:
|
|
|
|
|
PASS(errctx, akbasic_runtime_process_line_run(obj));
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The reference never clears runtime.errno, so the first BASIC-level error
|
|
|
|
|
* ends the program: in a file run, run_finished_mode is QUIT. Reproduced
|
|
|
|
|
* deliberately -- tests/language/array_outofbounds.txt depends on exactly one
|
|
|
|
|
* error line being printed and nothing after it.
|
|
|
|
|
*/
|
|
|
|
|
if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode));
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
int steps = 0;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in run");
|
|
|
|
|
while ( obj->mode != AKBASIC_MODE_QUIT ) {
|
|
|
|
|
PASS(errctx, akbasic_runtime_step(obj));
|
|
|
|
|
steps += 1;
|
|
|
|
|
if ( maxsteps > 0 && steps >= maxsteps ) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|