Files
akbasic/src/data.c
Logikoma f8cf198d35
Some checks failed
akbasic CI Build / cmake_build (push) Has been cancelled
akbasic CI Build / sanitizers (push) Has been cancelled
akbasic CI Build / coverage (push) Has been cancelled
akbasic CI Build / akgl_build (push) Has been cancelled
akbasic CI Build / mutation_test (push) Has been cancelled
Report prescan errors on their source lines
2026-08-06 12:47:31 -04:00

221 lines
6.2 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 <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;
int cmp = 0;
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 ) {
PASS(errctx, aksl_strncasecmp(cursor, "DATA", 4, &cmp));
if ( cmp == 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;
int64_t entry = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan");
FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
"Runtime has no environment; call akbasic_runtime_init() first");
entry = obj->environment->lineno;
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' ) {
/* Keep BASIC's error prefix on the source line being prescanned. */
obj->environment->lineno = i;
PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i));
}
}
obj->environment->lineno = entry;
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;
PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), 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);
}