262 lines
10 KiB
C
262 lines
10 KiB
C
|
|
/**
|
||
|
|
* @file runtime_housekeeping.c
|
||
|
|
* @brief The housekeeping verbs: NEW, CLR, CONT, TRON, TROFF, SWAP and HELP.
|
||
|
|
*
|
||
|
|
* TODO.md section 4 group B. None of these is in the Go reference -- they are
|
||
|
|
* on its own "What Isn't Implemented" list -- so what they mean here is a
|
||
|
|
* decision rather than a transcription, and each decision is recorded at its
|
||
|
|
* verb. Where a C128's answer needs machine state this interpreter does not
|
||
|
|
* have, the verb does the part that means something and says so.
|
||
|
|
*
|
||
|
|
* None of them needs a device, and none is in the golden corpus's way: they all
|
||
|
|
* act on interpreter state a program can see through PRINT.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <inttypes.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#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 Drop every variable and function, and hand their storage back.
|
||
|
|
*
|
||
|
|
* Shared by NEW and CLR, which differ only in whether the program text goes
|
||
|
|
* too. The value pool is a bump allocator (see akbasic_ValuePool), so this is
|
||
|
|
* the only place anything it handed out ever comes back -- re-initializing the
|
||
|
|
* pool is what makes `CLR` in a loop viable where re-DIMming in one is not.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *clear_variables(akbasic_Runtime *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Environment *root = NULL;
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) {
|
||
|
|
obj->variables[i].used = false;
|
||
|
|
}
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_FUNCTIONS; i++ ) {
|
||
|
|
obj->functions[i].used = false;
|
||
|
|
obj->functions[i].environment = NULL;
|
||
|
|
}
|
||
|
|
PASS(errctx, akbasic_valuepool_init(&obj->valuepool));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Unwind to the root before emptying its symbol tables. A CLR inside a
|
||
|
|
* GOSUB would otherwise leave child environments holding pointers into the
|
||
|
|
* variable pool it just released, and the parent chain is the only handle
|
||
|
|
* on them.
|
||
|
|
*/
|
||
|
|
while ( obj->environment != NULL && obj->environment->parent != NULL ) {
|
||
|
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||
|
|
}
|
||
|
|
root = obj->environment;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (root != NULL), AKERR_NULLPOINTER, "Runtime has no root environment");
|
||
|
|
PASS(errctx, akbasic_symtab_init(&root->variables, AKBASIC_MAX_VARIABLES));
|
||
|
|
PASS(errctx, akbasic_symtab_init(&root->functions, AKBASIC_MAX_FUNCTIONS));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------------------------------- NEW --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_new(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int64_t i = 0;
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in NEW");
|
||
|
|
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
||
|
|
obj->source[i].code[0] = '\0';
|
||
|
|
obj->source[i].lineno = 0;
|
||
|
|
}
|
||
|
|
PASS(errctx, clear_variables(obj));
|
||
|
|
PASS(errctx, akbasic_symtab_init(&obj->environment->labels, AKBASIC_MAX_LABELS));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The line counter goes home too. Without this a NEW typed at the REPL
|
||
|
|
* leaves the next entered line filed under wherever the last program
|
||
|
|
* stopped, which is a surprising place for a fresh program to start.
|
||
|
|
*/
|
||
|
|
obj->environment->lineno = 0;
|
||
|
|
obj->environment->nextline = 0;
|
||
|
|
obj->stopped = false;
|
||
|
|
obj->stoppedline = 0;
|
||
|
|
obj->errorline = 0;
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------------------------------- CLR --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_clr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in CLR");
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Variables and functions, not the program. A C128's CLR also closes open
|
||
|
|
* files and empties the GOSUB and FOR stacks; the stacks *are* emptied here,
|
||
|
|
* because unwinding to the root is how the variable pool is made safe to
|
||
|
|
* release, and there are no open files to close -- DLOAD and DSAVE open,
|
||
|
|
* transfer and close within the verb.
|
||
|
|
*/
|
||
|
|
PASS(errctx, clear_variables(obj));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------- CONT --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_cont(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in CONT");
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Refused rather than treated as a RUN. "CAN'T CONTINUE" is what a C128
|
||
|
|
* says, and it says it for a reason: continuing a program that never
|
||
|
|
* started would run it from line zero with whatever variables happened to be
|
||
|
|
* lying about, which is not what anybody typing CONT wanted.
|
||
|
|
*/
|
||
|
|
FAIL_ZERO_RETURN(errctx, obj->stopped, AKBASIC_ERR_STATE, "CAN'T CONTINUE");
|
||
|
|
|
||
|
|
obj->environment->nextline = obj->stoppedline;
|
||
|
|
obj->stopped = false;
|
||
|
|
PASS(errctx, akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------------------- TRON / TROFF --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_tron(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in TRON");
|
||
|
|
obj->trace = true;
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_troff(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in TROFF");
|
||
|
|
obj->trace = false;
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------- SWAP --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_swap(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *first = NULL;
|
||
|
|
akbasic_ASTLeaf *second = NULL;
|
||
|
|
akbasic_Variable *a = NULL;
|
||
|
|
akbasic_Variable *b = NULL;
|
||
|
|
akbasic_Variable swap;
|
||
|
|
char namea[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
char nameb[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in SWAP");
|
||
|
|
|
||
|
|
first = akbasic_leaf_first_argument(expr);
|
||
|
|
second = (first != NULL ? first->next : NULL);
|
||
|
|
FAIL_ZERO_RETURN(errctx, (first != NULL && second != NULL), AKBASIC_ERR_SYNTAX,
|
||
|
|
"Expected SWAP VARIABLE, VARIABLE");
|
||
|
|
FAIL_ZERO_RETURN(errctx,
|
||
|
|
(akbasic_leaf_is_identifier(first) && akbasic_leaf_is_identifier(second)),
|
||
|
|
AKBASIC_ERR_SYNTAX, "Expected SWAP VARIABLE, VARIABLE");
|
||
|
|
/*
|
||
|
|
* Refused across types, which BASIC 7.0 also does. A# and B$ hold different
|
||
|
|
* things and swapping them would leave two variables whose names no longer
|
||
|
|
* describe their contents -- and the type suffix is the only type
|
||
|
|
* declaration this language has.
|
||
|
|
*/
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (first->leaftype != second->leaftype), AKBASIC_ERR_TYPE,
|
||
|
|
"SWAP needs two variables of the same type");
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_environment_get(obj->environment, first->identifier, &a));
|
||
|
|
PASS(errctx, akbasic_environment_get(obj->environment, second->identifier, &b));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (a != NULL && b != NULL), AKBASIC_ERR_UNDEFINED,
|
||
|
|
"SWAP could not reach %s or %s", first->identifier, second->identifier);
|
||
|
|
if ( a == b ) {
|
||
|
|
/* SWAP A#, A# is legal and does nothing, rather than corrupting itself. */
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The whole record except the name, so an array swaps its dimensions and its
|
||
|
|
* storage pointer along with its contents -- SWAP is documented as exchanging
|
||
|
|
* the *variables*, not their scalar values, and on a C128 it is O(1) for
|
||
|
|
* exactly this reason. The names stay put: they are what the symbol table
|
||
|
|
* points at.
|
||
|
|
*/
|
||
|
|
memcpy(namea, a->name, sizeof(namea));
|
||
|
|
memcpy(nameb, b->name, sizeof(nameb));
|
||
|
|
memcpy(&swap, a, sizeof(swap));
|
||
|
|
memcpy(a, b, sizeof(*a));
|
||
|
|
memcpy(b, &swap, sizeof(*b));
|
||
|
|
memcpy(a->name, namea, sizeof(a->name));
|
||
|
|
memcpy(b->name, nameb, sizeof(b->name));
|
||
|
|
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------- HELP --- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_help(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
char line[AKBASIC_MAX_LINE_LENGTH * 2];
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in HELP");
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A C128 re-lists the line the last error happened on and highlights the
|
||
|
|
* part it choked on. The line is reproduced here; the highlight is not,
|
||
|
|
* because the error is reported by the verb that raised it rather than by
|
||
|
|
* something holding a token offset, so there is nothing that knows which
|
||
|
|
* part to mark. Printing the line without pretending to know more is the
|
||
|
|
* honest half of the verb.
|
||
|
|
*/
|
||
|
|
if ( obj->errorline == 0 || obj->source[obj->errorline].code[0] == '\0' ) {
|
||
|
|
PASS(errctx, akbasic_runtime_println(obj, "NO ERROR TO HELP WITH"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
snprintf(line, sizeof(line), "%" PRId64 " %s",
|
||
|
|
obj->errorline, obj->source[obj->errorline].code);
|
||
|
|
PASS(errctx, akbasic_runtime_println(obj, line));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|