212 lines
5.8 KiB
C
212 lines
5.8 KiB
C
|
|
/**
|
||
|
|
* @file data.c
|
||
|
|
* @brief Implements the `DATA` item list and the `READ` cursor.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <ctype.h>
|
||
|
|
#include <inttypes.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <strings.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
|
||
|
|
#include <akbasic/data.h>
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_data_state_init(akbasic_DataState *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL data state in init");
|
||
|
|
obj->count = 0;
|
||
|
|
obj->cursor = 0;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Collect the items of one `DATA` statement.
|
||
|
|
*
|
||
|
|
* @param obj The list to append to.
|
||
|
|
* @param cursor Points just past the `DATA` keyword; advanced past what is read.
|
||
|
|
* @param lineno The line these items were written on.
|
||
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||
|
|
* @throws AKBASIC_ERR_BOUNDS When the list is full or an item is too long.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *collect_items(akbasic_DataState *obj, const char **cursor, int64_t lineno)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
const char *p = *cursor;
|
||
|
|
|
||
|
|
for ( ;; ) {
|
||
|
|
akbasic_DataItem *item = NULL;
|
||
|
|
size_t used = 0;
|
||
|
|
|
||
|
|
while ( *p == ' ' || *p == '\t' ) {
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
if ( *p == '\0' || *p == ':' ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj->count < AKBASIC_MAX_DATA_ITEMS), AKBASIC_ERR_BOUNDS,
|
||
|
|
"The program holds more than %d DATA items", AKBASIC_MAX_DATA_ITEMS);
|
||
|
|
item = &obj->items[obj->count];
|
||
|
|
item->lineno = lineno;
|
||
|
|
item->quoted = false;
|
||
|
|
|
||
|
|
if ( *p == '"' ) {
|
||
|
|
/*
|
||
|
|
* A quoted item runs to its closing quote and may hold anything --
|
||
|
|
* commas, colons, spaces. That is the only way to put a comma in a
|
||
|
|
* DATA item, and it is why the scan has to understand quotes at all.
|
||
|
|
*/
|
||
|
|
item->quoted = true;
|
||
|
|
p += 1;
|
||
|
|
while ( *p != '\0' && *p != '"' && used < sizeof(item->text) - 1 ) {
|
||
|
|
item->text[used] = *p;
|
||
|
|
used += 1;
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
if ( *p == '"' ) {
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
while ( *p != '\0' && *p != ',' && *p != ':' && used < sizeof(item->text) - 1 ) {
|
||
|
|
item->text[used] = *p;
|
||
|
|
used += 1;
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
/* Trailing spaces are not part of an unquoted item. */
|
||
|
|
while ( used > 0 && (item->text[used - 1] == ' ' || item->text[used - 1] == '\t') ) {
|
||
|
|
used -= 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
item->text[used] = '\0';
|
||
|
|
obj->count += 1;
|
||
|
|
|
||
|
|
while ( *p == ' ' || *p == '\t' ) {
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
if ( *p != ',' ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
p += 1;
|
||
|
|
}
|
||
|
|
*cursor = p;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Scan one source line for `DATA` statements.
|
||
|
|
*
|
||
|
|
* Walks statement by statement, the way scan_line_labels() does, because `DATA`
|
||
|
|
* is a verb and a verb starts a statement -- and a string literal is the only
|
||
|
|
* thing that can hide a separator.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, int64_t lineno)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
const char *cursor = code;
|
||
|
|
bool statementstart = true;
|
||
|
|
bool instring = false;
|
||
|
|
|
||
|
|
while ( *cursor != '\0' ) {
|
||
|
|
if ( instring ) {
|
||
|
|
instring = (*cursor != '"');
|
||
|
|
cursor += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( *cursor == '"' ) {
|
||
|
|
instring = true;
|
||
|
|
statementstart = false;
|
||
|
|
cursor += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( *cursor == ':' ) {
|
||
|
|
statementstart = true;
|
||
|
|
cursor += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( isspace((unsigned char)*cursor) ) {
|
||
|
|
cursor += 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
/* A stored line may still carry its own number; see scan_line_labels(). */
|
||
|
|
if ( statementstart && isdigit((unsigned char)*cursor) ) {
|
||
|
|
while ( isdigit((unsigned char)*cursor) ) {
|
||
|
|
cursor += 1;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ( statementstart && strncasecmp(cursor, "DATA", 4) == 0
|
||
|
|
&& !isalnum((unsigned char)cursor[4]) ) {
|
||
|
|
cursor += 4;
|
||
|
|
PASS(errctx, collect_items(obj, &cursor, lineno));
|
||
|
|
statementstart = false;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
statementstart = false;
|
||
|
|
cursor += 1;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_data_scan(akbasic_Runtime *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int64_t i = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan");
|
||
|
|
PASS(errctx, akbasic_data_state_init(&obj->data_state));
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
|
||
|
|
if ( obj->source[i].code[0] != '\0' ) {
|
||
|
|
PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_data_next(akbasic_Runtime *obj, akbasic_Type type, akbasic_Value *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_DataItem *item = NULL;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in data_next");
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj->data_state.cursor < obj->data_state.count),
|
||
|
|
AKBASIC_ERR_STATE, "OUT OF DATA");
|
||
|
|
item = &obj->data_state.items[obj->data_state.cursor];
|
||
|
|
obj->data_state.cursor += 1;
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_value_zero(dest));
|
||
|
|
if ( type == AKBASIC_TYPE_STRING ) {
|
||
|
|
dest->valuetype = AKBASIC_TYPE_STRING;
|
||
|
|
snprintf(dest->stringval, sizeof(dest->stringval), "%s", item->text);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A quoted item is a string and reading it into a number is a mistake worth
|
||
|
|
* reporting -- `READ A#` against `DATA "TEXT"` would otherwise quietly yield
|
||
|
|
* zero. An *unquoted* item that is not a number is caught the same way, by
|
||
|
|
* the conversion refusing it.
|
||
|
|
*/
|
||
|
|
FAIL_NONZERO_RETURN(errctx, item->quoted, AKBASIC_ERR_TYPE,
|
||
|
|
"READ: DATA item \"%s\" on line %" PRId64 " is a string",
|
||
|
|
item->text, item->lineno);
|
||
|
|
if ( type == AKBASIC_TYPE_FLOAT ) {
|
||
|
|
dest->valuetype = AKBASIC_TYPE_FLOAT;
|
||
|
|
PASS(errctx, aksl_atof(item->text, &dest->floatval));
|
||
|
|
} else {
|
||
|
|
long long converted = 0;
|
||
|
|
|
||
|
|
dest->valuetype = AKBASIC_TYPE_INTEGER;
|
||
|
|
PASS(errctx, aksl_atoll(item->text, &converted));
|
||
|
|
dest->intval = (int64_t)converted;
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|