/** * @file data.c * @brief Implements the `DATA` item list and the `READ` cursor. */ #include #include #include #include #include #include #include #include #include #include #include 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); }