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>
This commit is contained in:
2026-07-30 23:53:56 -04:00
commit f8c15c2f2c
60 changed files with 10192 additions and 0 deletions

644
src/runtime_functions.c Normal file
View File

@@ -0,0 +1,644 @@
/**
* @file runtime_functions.c
* @brief The built-in function implementations.
*
* Ported from basicruntime_functions.go. The reference bootstraps its function
* table by running a BASIC program of DEF statements through the interpreter at
* startup and then nulling out the expressions so the native handlers take over
* -- except MOD, SPC and STR, which stay as BASIC expressions. None of that is
* reproduced: the signatures are data in the dispatch table and all three of
* those are ordinary native handlers here, which removes the need to run the
* interpreter before the interpreter is ready.
*/
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akbasic/convert.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include "verbs.h"
/*
* Every builtin starts the same way: find the argument list, evaluate the first
* argument, and hand back a fresh value to write the answer into. Arity is
* already guaranteed by the parser against the table's arity column.
*/
static akerr_ErrorContext *first_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, akbasic_ASTLeaf **argleaf, akbasic_Value **argvalue, akbasic_Value **out)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *arg = NULL;
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
arg = akbasic_leaf_first_argument(expr);
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
"%s expected an argument", fname);
if ( argvalue != NULL ) {
PASS(errctx, akbasic_runtime_evaluate(obj, arg, argvalue));
}
if ( out != NULL ) {
PASS(errctx, akbasic_environment_new_value(obj->environment, out));
PASS(errctx, akbasic_value_zero(*out));
}
if ( argleaf != NULL ) {
*argleaf = arg;
}
SUCCEED_RETURN(errctx);
}
/* Coerce an integer-or-float argument to double, the shape every trig call wants. */
static akerr_ErrorContext *arg_as_double(akbasic_Value *value, const char *fname, double *dest)
{
PREPARE_ERROR(errctx);
if ( value->valuetype == AKBASIC_TYPE_INTEGER ) {
*dest = (double)value->intval;
SUCCEED_RETURN(errctx);
}
if ( value->valuetype == AKBASIC_TYPE_FLOAT ) {
*dest = value->floatval;
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "%s expected integer or float", fname);
}
/* The trig family differs only by the libm call, so it is one shape. */
#define DEFINE_MATH_FUNCTION(__cname, __basicname, __call) \
akerr_ErrorContext *__cname(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, \
akbasic_Value *lval, akbasic_Value *rval, \
akbasic_Value **dest) \
{ \
PREPARE_ERROR(errctx); \
akbasic_Value *arg = NULL; \
akbasic_Value *out = NULL; \
double input = 0.0; \
\
(void)lval; (void)rval; \
PASS(errctx, first_arg(obj, expr, __basicname, NULL, &arg, &out)); \
PASS(errctx, arg_as_double(arg, __basicname, &input)); \
out->valuetype = AKBASIC_TYPE_FLOAT; \
out->floatval = __call(input); \
*dest = out; \
SUCCEED_RETURN(errctx); \
}
DEFINE_MATH_FUNCTION(akbasic_fn_atn, "ATN", atan)
DEFINE_MATH_FUNCTION(akbasic_fn_cos, "COS", cos)
DEFINE_MATH_FUNCTION(akbasic_fn_sin, "SIN", sin)
DEFINE_MATH_FUNCTION(akbasic_fn_tan, "TAN", tan)
DEFINE_MATH_FUNCTION(akbasic_fn_log, "LOG", log)
akerr_ErrorContext *akbasic_fn_abs(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "ABS", NULL, &arg, &out));
if ( arg->valuetype == AKBASIC_TYPE_INTEGER ) {
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (arg->intval < 0 ? -arg->intval : arg->intval);
} else if ( arg->valuetype == AKBASIC_TYPE_FLOAT ) {
out->valuetype = AKBASIC_TYPE_FLOAT;
out->floatval = fabs(arg->floatval);
} else {
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "ABS expected integer or float");
}
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_rad(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
double input = 0.0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "RAD", NULL, &arg, &out));
PASS(errctx, arg_as_double(arg, "RAD", &input));
out->valuetype = AKBASIC_TYPE_FLOAT;
out->floatval = input * (M_PI / 180.0);
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_sgn(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
double input = 0.0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "SGN", NULL, &arg, &out));
PASS(errctx, arg_as_double(arg, "SGN", &input));
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (input < 0.0 ? -1 : (input > 0.0 ? 1 : 0));
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_chr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
int64_t codepoint = 0;
int written = 0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "CHR", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"CHR expected an integer codepoint");
codepoint = arg->intval;
FAIL_ZERO_RETURN(errctx, (codepoint >= 0 && codepoint <= 0x10FFFF), AKBASIC_ERR_VALUE,
"CHR codepoint %" PRId64 " is outside Unicode", codepoint);
/* UTF-8 encode, matching the reference's string(rune(x)). */
out->valuetype = AKBASIC_TYPE_STRING;
if ( codepoint < 0x80 ) {
out->stringval[written++] = (char)codepoint;
} else if ( codepoint < 0x800 ) {
out->stringval[written++] = (char)(0xC0 | (codepoint >> 6));
out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
} else if ( codepoint < 0x10000 ) {
out->stringval[written++] = (char)(0xE0 | (codepoint >> 12));
out->stringval[written++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
} else {
out->stringval[written++] = (char)(0xF0 | (codepoint >> 18));
out->stringval[written++] = (char)(0x80 | ((codepoint >> 12) & 0x3F));
out->stringval[written++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
out->stringval[written++] = (char)(0x80 | (codepoint & 0x3F));
}
out->stringval[written] = '\0';
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "HEX", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"HEX expected an integer");
out->valuetype = AKBASIC_TYPE_STRING;
snprintf(out->stringval, sizeof(out->stringval), "%" PRIx64, arg->intval);
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_str(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "STR", NULL, &arg, &out));
/*
* The reference defines this as `"" + X#`, so it renders exactly the way
* string concatenation does -- an integer as %d and a float as %f.
*/
PASS(errctx, akbasic_value_to_string(arg, out->stringval, sizeof(out->stringval)));
out->valuetype = AKBASIC_TYPE_STRING;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_spc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
int64_t i = 0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "SPC", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"SPC expected an integer");
FAIL_ZERO_RETURN(errctx, (arg->intval >= 0 && arg->intval < AKBASIC_MAX_STRING_LENGTH),
AKBASIC_ERR_VALUE,
"SPC count %" PRId64 " is outside 0..%d",
arg->intval, AKBASIC_MAX_STRING_LENGTH - 1);
out->valuetype = AKBASIC_TYPE_STRING;
for ( i = 0; i < arg->intval; i++ ) {
out->stringval[i] = ' ';
}
out->stringval[arg->intval] = '\0';
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_val(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "VAL", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"VAL expected a string");
out->valuetype = AKBASIC_TYPE_FLOAT;
/*
* Through the strict converter, never aksl_atof: that family cannot report a
* failure, so VAL("garbage") would quietly answer 0.0 instead of raising.
* See TODO.md section 1.9.
*/
PASS(errctx, akbasic_str_to_double(arg->stringval, &out->floatval));
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *argleaf = NULL;
akbasic_Value *strval = NULL;
akbasic_Value *out = NULL;
akbasic_Variable *variable = NULL;
(void)lval; (void)rval;
/*
* LEN must not evaluate a non-string argument. `LEN(A#)` on a
* multi-dimensional array is legal and asks for the element count, but
* evaluating a bare identifier supplies the single subscript {0}, which a
* two-dimensional variable rightly rejects. So: inspect the leaf, and only
* evaluate when it is a string.
*/
PASS(errctx, first_arg(obj, expr, "LEN", &argleaf, NULL, NULL));
FAIL_ZERO_RETURN(errctx,
(akbasic_leaf_is_identifier(argleaf) || akbasic_leaf_is_literal(argleaf)),
AKBASIC_ERR_SYNTAX, "Expected identifier or string literal");
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER;
if ( argleaf->leaftype == AKBASIC_LEAF_LITERAL_STRING ||
argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ) {
PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &strval));
out->intval = (int64_t)strlen(strval->stringval);
} else {
PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
"Identifier %s is undefined", argleaf->identifier);
out->intval = variable->valuecount;
}
*dest = out;
SUCCEED_RETURN(errctx);
}
/* Fetch argument N (zero-based) of a call, already evaluated. */
static akerr_ErrorContext *nth_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int n, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *arg = NULL;
int i = 0;
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
arg = akbasic_leaf_first_argument(expr);
for ( i = 0; i < n && arg != NULL; i++ ) {
arg = arg->right;
}
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
"%s is missing argument %d", fname, n + 1);
PASS(errctx, akbasic_runtime_evaluate(obj, arg, dest));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_instr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *haystack = NULL;
akbasic_Value *needle = NULL;
akbasic_Value *out = NULL;
char *hit = NULL;
(void)lval; (void)rval;
PASS(errctx, nth_arg(obj, expr, "INSTR", 0, &haystack));
PASS(errctx, nth_arg(obj, expr, "INSTR", 1, &needle));
FAIL_ZERO_RETURN(errctx,
(haystack->valuetype == AKBASIC_TYPE_STRING &&
needle->valuetype == AKBASIC_TYPE_STRING),
AKBASIC_ERR_TYPE, "INSTR expected two strings");
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER;
hit = strstr(haystack->stringval, needle->stringval);
out->intval = (hit == NULL ? -1 : (int64_t)(hit - haystack->stringval));
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *source = NULL;
akbasic_Value *count = NULL;
akbasic_Value *out = NULL;
int64_t take = 0;
size_t sourcelen = 0;
(void)lval; (void)rval;
PASS(errctx, nth_arg(obj, expr, "LEFT", 0, &source));
PASS(errctx, nth_arg(obj, expr, "LEFT", 1, &count));
FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"LEFT expected a string");
FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"LEFT expected an integer count");
sourcelen = strlen(source->stringval);
take = count->intval;
if ( take < 0 ) {
take = 0;
}
if ( (size_t)take > sourcelen ) {
take = (int64_t)sourcelen; /* clamped to LEN, as the README says */
}
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval, (size_t)take);
out->stringval[take] = '\0';
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *source = NULL;
akbasic_Value *count = NULL;
akbasic_Value *out = NULL;
int64_t take = 0;
size_t sourcelen = 0;
(void)lval; (void)rval;
PASS(errctx, nth_arg(obj, expr, "RIGHT", 0, &source));
PASS(errctx, nth_arg(obj, expr, "RIGHT", 1, &count));
FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"RIGHT expected a string");
FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"RIGHT expected an integer count");
sourcelen = strlen(source->stringval);
take = count->intval;
if ( take < 0 ) {
take = 0;
}
if ( (size_t)take > sourcelen ) {
take = (int64_t)sourcelen;
}
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take), (size_t)take);
out->stringval[take] = '\0';
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *source = NULL;
akbasic_Value *startval = NULL;
akbasic_Value *lengthval = NULL;
akbasic_Value *out = NULL;
int64_t start = 0;
int64_t length = 0;
size_t sourcelen = 0;
(void)lval; (void)rval;
PASS(errctx, nth_arg(obj, expr, "MID", 0, &source));
PASS(errctx, nth_arg(obj, expr, "MID", 1, &startval));
PASS(errctx, nth_arg(obj, expr, "MID", 2, &lengthval));
FAIL_NONZERO_RETURN(errctx, (source->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"MID expected a string");
FAIL_ZERO_RETURN(errctx,
(startval->valuetype == AKBASIC_TYPE_INTEGER &&
lengthval->valuetype == AKBASIC_TYPE_INTEGER),
AKBASIC_ERR_TYPE, "MID expected integer start and length");
sourcelen = strlen(source->stringval);
start = startval->intval;
length = lengthval->intval;
FAIL_ZERO_RETURN(errctx, (start >= 0 && (size_t)start <= sourcelen), AKBASIC_ERR_BOUNDS,
"MID start %" PRId64 " is outside 0..%zu", start, sourcelen);
if ( length < 0 ) {
length = 0;
}
if ( (size_t)(start + length) > sourcelen ) {
length = (int64_t)sourcelen - start;
}
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval + start, (size_t)length);
out->stringval[length] = '\0';
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_mod(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *left = NULL;
akbasic_Value *right = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, nth_arg(obj, expr, "MOD", 0, &left));
PASS(errctx, nth_arg(obj, expr, "MOD", 1, &right));
FAIL_ZERO_RETURN(errctx,
(left->valuetype == AKBASIC_TYPE_INTEGER &&
right->valuetype == AKBASIC_TYPE_INTEGER),
AKBASIC_ERR_TYPE, "MOD expected two integers");
FAIL_ZERO_RETURN(errctx, (right->intval != 0), AKBASIC_ERR_VALUE, "DIVISION BY ZERO");
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER;
/*
* The reference defines MOD as `X% - (Y% * (X% / Y%))` in BASIC, which with
* truncating integer division is exactly C's %.
*/
out->intval = left->intval - (right->intval * (left->intval / right->intval));
*dest = out;
SUCCEED_RETURN(errctx);
}
/* SHL, SHR and XOR share a shape: two integers in, one integer out. */
static akerr_ErrorContext *two_integers(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int64_t *a, int64_t *b, akbasic_Value **out)
{
PREPARE_ERROR(errctx);
akbasic_Value *left = NULL;
akbasic_Value *right = NULL;
PASS(errctx, nth_arg(obj, expr, fname, 0, &left));
PASS(errctx, nth_arg(obj, expr, fname, 1, &right));
FAIL_ZERO_RETURN(errctx,
(left->valuetype == AKBASIC_TYPE_INTEGER &&
right->valuetype == AKBASIC_TYPE_INTEGER),
AKBASIC_ERR_TYPE, "%s expected two integers", fname);
*a = left->intval;
*b = right->intval;
PASS(errctx, akbasic_environment_new_value(obj->environment, out));
PASS(errctx, akbasic_value_zero(*out));
(*out)->valuetype = AKBASIC_TYPE_INTEGER;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_shl(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *out = NULL;
int64_t value = 0;
int64_t bits = 0;
(void)lval; (void)rval;
PASS(errctx, two_integers(obj, expr, "SHL", &value, &bits, &out));
FAIL_ZERO_RETURN(errctx, (bits >= 0 && bits < 64), AKBASIC_ERR_VALUE,
"SHL shift count %" PRId64 " is outside 0..63", bits);
out->intval = (int64_t)((uint64_t)value << bits);
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_shr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *out = NULL;
int64_t value = 0;
int64_t bits = 0;
(void)lval; (void)rval;
PASS(errctx, two_integers(obj, expr, "SHR", &value, &bits, &out));
FAIL_ZERO_RETURN(errctx, (bits >= 0 && bits < 64), AKBASIC_ERR_VALUE,
"SHR shift count %" PRId64 " is outside 0..63", bits);
out->intval = value >> bits;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_xor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *out = NULL;
int64_t left = 0;
int64_t right = 0;
(void)lval; (void)rval;
PASS(errctx, two_integers(obj, expr, "XOR", &left, &right, &out));
out->intval = left ^ right;
*dest = out;
SUCCEED_RETURN(errctx);
}
/*
* PEEK, POINTER and POINTERVAR reach real memory. In Go these went through
* unsafe.Pointer; in C they are ordinary casts and are simpler, not harder.
*
* They assume a little-endian host: tests/language/functions/pointer.bas sets
* A# = 255 and expects PEEK(POINTER(A#)) to be 255, which only holds if the low
* byte of the int64_t comes first.
*/
akerr_ErrorContext *akbasic_fn_peek(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *argleaf = NULL;
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
const uint8_t *source = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "PEEK", &argleaf, NULL, NULL));
FAIL_ZERO_RETURN(errctx,
(argleaf->leaftype == AKBASIC_LEAF_LITERAL_INT ||
argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_INT),
AKBASIC_ERR_TYPE, "PEEK expected INTEGER or INTEGER VARIABLE");
PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &arg));
FAIL_ZERO_RETURN(errctx, (arg->valuetype == AKBASIC_TYPE_INTEGER && arg->intval != 0),
AKBASIC_ERR_VALUE, "PEEK got NIL pointer or uninitialized variable");
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
source = (const uint8_t *)(uintptr_t)arg->intval;
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (int64_t)(*source);
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_pointer(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *argleaf = NULL;
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "POINTER", &argleaf, NULL, NULL));
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(argleaf), AKBASIC_ERR_TYPE,
"POINTER expected IDENTIFIER");
/* The address of the live storage, not of a copy of it. */
obj->eval_clone_identifiers = false;
ATTEMPT {
CATCH(errctx, akbasic_runtime_evaluate(obj, argleaf, &arg));
} CLEANUP {
obj->eval_clone_identifiers = true;
} PROCESS(errctx) {
} FINISH(errctx, true);
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER;
switch ( arg->valuetype ) {
case AKBASIC_TYPE_INTEGER:
out->intval = (int64_t)(uintptr_t)&arg->intval;
break;
case AKBASIC_TYPE_FLOAT:
out->intval = (int64_t)(uintptr_t)&arg->floatval;
break;
case AKBASIC_TYPE_STRING:
out->intval = (int64_t)(uintptr_t)&arg->stringval;
break;
default:
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "POINTER expects a INT, FLOAT or STRING variable");
}
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_pointervar(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *argleaf = NULL;
akbasic_Variable *variable = NULL;
akbasic_Value *out = NULL;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "POINTERVAR", &argleaf, NULL, NULL));
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(argleaf), AKBASIC_ERR_TYPE,
"POINTERVAR expected IDENTIFIER");
PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
"Identifier %s is undefined", argleaf->identifier);
PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (int64_t)(uintptr_t)variable;
*dest = out;
SUCCEED_RETURN(errctx);
}