Files
akbasic/src/structtype.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

545 lines
19 KiB
C

/**
* @file structtype.c
* @brief Implements the `TYPE` table and the prescan that fills it.
*
* The prescan is textual and runs before the program does, exactly as the label
* and `DATA` prescans do and for the same reason: a declaration has to be in
* effect wherever control goes, including when a branch skips the lines that
* made it.
*
* **It is three passes, and each one exists for a case that cannot be done in
* the pass before it.** Pass 1 registers names and line ranges, so a field can
* name a type declared further down the program. Pass 2 parses field lists, now
* able to resolve every type reference. Pass 3 computes sizes and offsets by
* repeated resolution, because a type nested by value must know its own size
* before its container can -- and a group of types that never resolve is
* precisely a cycle of by-value containment, which is what makes "a TYPE cannot
* contain itself by value" a diagnosis rather than an assumption.
*/
#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/structtype.h>
#include "verbs.h"
/** @brief Step over spaces and tabs. */
static const char *skip_space(const char *cursor)
{
while ( *cursor != '\0' && isspace((unsigned char)*cursor) ) {
cursor += 1;
}
return cursor;
}
/**
* @brief Step over a stored line's own line number, if it still carries one.
*
* Every path inside the library files what the scanner already stripped, but
* akbasic_runtime_store_line() is public and a host may hand it a raw line, so
* both spellings are real and both are handled here -- the same allowance
* scan_line_labels() makes.
*/
static const char *skip_lineno(const char *cursor)
{
cursor = skip_space(cursor);
if ( isdigit((unsigned char)*cursor) ) {
while ( isdigit((unsigned char)*cursor) ) {
cursor += 1;
}
}
return skip_space(cursor);
}
/*
* Room to read a word that is *too long* and still see that it was.
*
* Reading into a buffer the size of the limit truncates before anything can
* check, so an over-long name would arrive already trimmed to fit and be
* accepted -- which is how the length check below was unreachable when it was
* first written. Twice the limit is enough to tell the two cases apart, and a
* word longer than this is still longer than the limit, so it is refused either
* way.
*/
#define STRUCT_WORD_SCRATCH (AKBASIC_MAX_STRUCT_NAME * 2)
/** @brief Copy the next whitespace-delimited word, uppercased. Empty when the line ends. */
static const char *next_word(const char *cursor, char *dest, size_t len)
{
size_t used = 0;
dest[0] = '\0';
cursor = skip_space(cursor);
while ( *cursor != '\0' && !isspace((unsigned char)*cursor) ) {
if ( used + 1 < len ) {
dest[used] = (char)toupper((unsigned char)*cursor);
used += 1;
}
cursor += 1;
}
dest[used] = '\0';
return cursor;
}
/**
* @brief Compare a word against a keyword. Verbs are case-insensitive here as everywhere.
*
* The answer leaves through @p dest rather than the return value because the
* comparison itself can fail, and a `bool` has nowhere to put that. See
* libakstdlib #38.
*/
static akerr_ErrorContext *word_is(const char *word, const char *keyword, bool *dest)
{
PREPARE_ERROR(errctx);
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL dest in word_is");
PASS(errctx, aksl_strcmp(word, keyword, &cmp));
*dest = (cmp == 0);
SUCCEED_RETURN(errctx);
}
/**
* @brief The type a field name's suffix declares.
*
* The same rule every other name in the language follows, which is the reason a
* field list needs no type column: `W#` is an integer because it says so.
*/
static akerr_ErrorContext *type_from_suffix(const char *name, akbasic_Type *dest)
{
PREPARE_ERROR(errctx);
size_t len = 0;
PASS(errctx, aksl_strlen(name, &len));
FAIL_ZERO_RETURN(errctx, (len >= 2), AKBASIC_ERR_SYNTAX,
"Field name \"%s\" carries no type suffix", name);
switch ( name[len - 1] ) {
case '#': *dest = AKBASIC_TYPE_INTEGER; break;
case '%': *dest = AKBASIC_TYPE_FLOAT; break;
case '$': *dest = AKBASIC_TYPE_STRING; break;
case '@': *dest = AKBASIC_TYPE_STRUCT; break;
default:
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX,
"Field name \"%s\" carries no type suffix (# integer, %% float, $ string, @ structure)",
name);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_structtype_table_init(akbasic_StructTypeTable *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL table in structtype init");
PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->count = 0;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_structtype_find(akbasic_StructTypeTable *obj, const char *name, int *dest)
{
PREPARE_ERROR(errctx);
int i = 0;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in structtype find");
*dest = -1;
for ( i = 0; i < obj->count; i++ ) {
if ( !obj->types[i].used ) {
continue;
}
PASS(errctx, aksl_strcmp(obj->types[i].name, name, &cmp));
if ( cmp == 0 ) {
*dest = i;
SUCCEED_RETURN(errctx);
}
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int typeindex, const char *name, akbasic_StructField **dest)
{
PREPARE_ERROR(errctx);
char known[AKBASIC_MAX_STRING_LENGTH];
const char *separator = NULL;
akbasic_StructType *type = NULL;
size_t used = 0;
size_t namelen = 0;
size_t seplen = 0;
int written = 0;
int i = 0;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in structtype field");
FAIL_ZERO_RETURN(errctx, (typeindex >= 0 && typeindex < obj->count), AKBASIC_ERR_BOUNDS,
"Structure type index %d is outside 0..%d", typeindex, obj->count - 1);
type = &obj->types[typeindex];
for ( i = 0; i < type->fieldcount; i++ ) {
PASS(errctx, aksl_strcmp(type->fields[i].name, name, &cmp));
if ( cmp == 0 ) {
*dest = &type->fields[i];
SUCCEED_RETURN(errctx);
}
}
/*
* List what the type does have. The only reason a misspelled field can be
* caught at all is that the set is closed and the program wrote it down, so
* the diagnostic may as well show it -- this is the message that turns "why
* is my total zero" into a one-line fix.
*/
known[0] = '\0';
for ( i = 0; i < type->fieldcount; i++ ) {
separator = (i == 0 ? "" : ", ");
PASS(errctx, aksl_strlen(separator, &seplen));
PASS(errctx, aksl_strlen(type->fields[i].name, &namelen));
/*
* The list is advisory, so a full buffer ends it rather than failing the
* lookup. aksl_snprintf treats truncation as an error, which is right
* everywhere the output matters -- so the fit is decided here, before the
* call, instead of being read back out of a return value afterwards.
*/
if ( used + seplen + namelen >= sizeof(known) ) {
break;
}
PASS(errctx, aksl_snprintf(&written, known + used, sizeof(known) - used, "%s%s",
separator, type->fields[i].name));
used += (size_t)written;
}
FAIL_RETURN(errctx, AKBASIC_ERR_UNDEFINED, "%s has no field %s (%s)",
type->name, name, (type->fieldcount > 0 ? known : "it has none"));
}
akerr_ErrorContext *akbasic_structtype_block_at(akbasic_StructTypeTable *obj, int64_t lineno, int *dest)
{
PREPARE_ERROR(errctx);
int i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in structtype block_at");
*dest = -1;
for ( i = 0; i < obj->count; i++ ) {
if ( obj->types[i].used && obj->types[i].firstline == lineno ) {
*dest = i;
SUCCEED_RETURN(errctx);
}
}
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- pass 1 -- */
/**
* @brief Register every type's name and line range.
*
* Names first and fields later, so a field may name a type declared further down
* the program. A linked list needs that even for itself -- `NEXT@ AS PTR TO
* NODE` inside `TYPE NODE` refers to a type that is not finished being read.
*/
static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
akbasic_StructTypeTable *table = &obj->structtypes;
char word[STRUCT_WORD_SCRATCH];
char name[STRUCT_WORD_SCRATCH];
const char *cursor = NULL;
int64_t i = 0;
int open = -1;
int existing = 0;
size_t namelen = 0;
bool matched = false;
const akbasic_Verb *verb = NULL;
int64_t entry = obj->environment->lineno;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
obj->environment->lineno = i;
cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word));
PASS(errctx, word_is(word, "END", &matched));
if ( matched ) {
(void)next_word(cursor, name, sizeof(name));
PASS(errctx, word_is(name, "TYPE", &matched));
if ( !matched ) {
continue; /* an ordinary END, not our terminator */
}
FAIL_ZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX,
"END TYPE on line %" PRId64 " closes nothing", i);
table->types[open].lastline = i;
open = -1;
continue;
}
PASS(errctx, word_is(word, "TYPE", &matched));
if ( !matched ) {
continue;
}
FAIL_ZERO_RETURN(errctx, (open < 0), AKBASIC_ERR_SYNTAX,
"TYPE on line %" PRId64 " opens inside %s, which is not closed",
i, table->types[open].name);
(void)next_word(cursor, name, sizeof(name));
FAIL_ZERO_RETURN(errctx, (name[0] != '\0'), AKBASIC_ERR_SYNTAX,
"TYPE on line %" PRId64 " has no name", i);
PASS(errctx, akbasic_structtype_find(table, name, &existing));
FAIL_NONZERO_RETURN(errctx, (existing >= 0), AKBASIC_ERR_VALUE,
"TYPE %s is declared twice", name);
/*
* A type name is a bare word, and so is every verb, so the two share a
* namespace whether we like it or not. Refused here with a message that
* says so -- left to the parser it comes out as "Expected expression or
* literal", pointing at the line rather than at the problem. This is the
* same check the scanner already makes for variable names.
*/
PASS(errctx, akbasic_verb_lookup(name, &verb));
FAIL_NONZERO_RETURN(errctx, (verb != NULL), AKBASIC_ERR_VALUE,
"TYPE %s: %s is a reserved word and cannot name a type", name, name);
FAIL_ZERO_RETURN(errctx, (table->count < AKBASIC_MAX_STRUCT_TYPES), AKBASIC_ERR_BOUNDS,
"More than %d TYPE declarations", AKBASIC_MAX_STRUCT_TYPES);
PASS(errctx, aksl_strlen(name, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS,
"TYPE name \"%s\" exceeds the %d character limit",
name, AKBASIC_MAX_STRUCT_NAME - 1);
open = table->count;
PASS(errctx, aksl_memset(&table->types[open], 0, sizeof(table->types[open])));
/* aksl_strcpy always terminates and refuses rather than truncates, which
is the behaviour this site already wanted -- two long names truncating
to the same prefix would collide silently. The length check above stays
because it names the limit in the message. */
PASS(errctx, aksl_strcpy(table->types[open].name, sizeof(table->types[open].name), name));
table->types[open].used = true;
table->types[open].firstline = i;
table->types[open].lastline = -1;
table->types[open].slotcount = -1; /* unresolved until pass 3 */
table->count += 1;
}
FAIL_NONZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX,
"TYPE %s is never closed with END TYPE", table->types[open >= 0 ? open : 0].name);
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- pass 2 -- */
/** @brief Parse one field declaration line into a descriptor. */
static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_StructType *type,
const char *line, int64_t lineno)
{
PREPARE_ERROR(errctx);
char fieldname[STRUCT_WORD_SCRATCH];
char word[STRUCT_WORD_SCRATCH];
const char *cursor = NULL;
akbasic_StructField *field = NULL;
akbasic_Type valuetype = AKBASIC_TYPE_UNDEFINED;
size_t namelen = 0;
bool matched = false;
int referenced = 0;
int cmp = 0;
int i = 0;
cursor = next_word(skip_lineno(line), fieldname, sizeof(fieldname));
if ( fieldname[0] == '\0' || fieldname[0] == '\'' ) {
SUCCEED_RETURN(errctx); /* a blank line inside the block */
}
PASS(errctx, word_is(fieldname, "REM", &matched));
if ( matched ) {
SUCCEED_RETURN(errctx);
}
FAIL_ZERO_RETURN(errctx, (type->fieldcount < AKBASIC_MAX_STRUCT_FIELDS), AKBASIC_ERR_BOUNDS,
"TYPE %s declares more than %d fields", type->name, AKBASIC_MAX_STRUCT_FIELDS);
for ( i = 0; i < type->fieldcount; i++ ) {
PASS(errctx, aksl_strcmp(type->fields[i].name, fieldname, &cmp));
FAIL_NONZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_VALUE,
"TYPE %s declares %s twice", type->name, fieldname);
}
PASS(errctx, type_from_suffix(fieldname, &valuetype));
PASS(errctx, aksl_strlen(fieldname, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS,
"Field name \"%s\" exceeds the %d character limit",
fieldname, AKBASIC_MAX_STRUCT_NAME - 1);
field = &type->fields[type->fieldcount];
PASS(errctx, aksl_memset(field, 0, sizeof(*field)));
/* Refused rather than truncated, for the same reason a type name is. */
PASS(errctx, aksl_strcpy(field->name, sizeof(field->name), fieldname));
field->valuetype = valuetype;
field->typeindex = -1;
field->offset = -1;
if ( valuetype != AKBASIC_TYPE_STRUCT ) {
/* A primitive needs no more words, and anything after it is a typo. */
cursor = next_word(cursor, word, sizeof(word));
FAIL_NONZERO_RETURN(errctx, (word[0] != '\0'), AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": %s is a %s field and takes no AS clause",
lineno, fieldname,
(valuetype == AKBASIC_TYPE_INTEGER ? "integer" :
valuetype == AKBASIC_TYPE_FLOAT ? "float" : "string"));
field->kind = AKBASIC_FIELD_PRIMITIVE;
field->slotcount = 1;
type->fieldcount += 1;
SUCCEED_RETURN(errctx);
}
/*
* An `@` field is a structure, and `@` alone does not say which -- three
* primitive types fit in three suffix characters and N declared types do
* not fit in one. So the declaration has to name it.
*/
cursor = next_word(cursor, word, sizeof(word));
PASS(errctx, word_is(word, "AS", &matched));
FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": %s must name its type -- %s AS TYPENAME, or %s AS PTR TO TYPENAME",
lineno, fieldname, fieldname, fieldname);
cursor = next_word(cursor, word, sizeof(word));
PASS(errctx, word_is(word, "PTR", &matched));
if ( matched ) {
cursor = next_word(cursor, word, sizeof(word));
PASS(errctx, word_is(word, "TO", &matched));
FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": expected PTR TO TYPENAME", lineno);
cursor = next_word(cursor, word, sizeof(word));
field->kind = AKBASIC_FIELD_POINTER;
field->valuetype = AKBASIC_TYPE_POINTER;
field->slotcount = 1; /* a reference, whatever it points at */
} else {
field->kind = AKBASIC_FIELD_STRUCT;
field->slotcount = -1; /* the nested type's size, in pass 3 */
}
FAIL_ZERO_RETURN(errctx, (word[0] != '\0'), AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": %s names no type", lineno, fieldname);
PASS(errctx, akbasic_structtype_find(table, word, &referenced));
FAIL_ZERO_RETURN(errctx, (referenced >= 0), AKBASIC_ERR_UNDEFINED,
"Line %" PRId64 ": %s names TYPE %s, which is not declared",
lineno, fieldname, word);
field->typeindex = referenced;
type->fieldcount += 1;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- pass 3 -- */
/**
* @brief Resolve sizes and offsets, and diagnose by-value recursion.
*
* A type whose fields are all sized can be sized. Repeating that until nothing
* changes resolves every legal arrangement however the declarations were
* ordered -- and whatever is left unresolved is a group of types that contain
* each other by value, which has no finite size. That is the diagnosis rather
* than a stack overflow later.
*/
static akerr_ErrorContext *resolve_sizes(akbasic_Runtime *runtime, akbasic_StructTypeTable *table)
{
PREPARE_ERROR(errctx);
bool progress = true;
int i = 0;
int f = 0;
while ( progress ) {
progress = false;
for ( i = 0; i < table->count; i++ ) {
akbasic_StructType *type = &table->types[i];
int offset = 0;
bool ready = true;
if ( type->slotcount >= 0 ) {
continue;
}
for ( f = 0; f < type->fieldcount; f++ ) {
akbasic_StructField *field = &type->fields[f];
if ( field->kind == AKBASIC_FIELD_STRUCT ) {
if ( table->types[field->typeindex].slotcount < 0 ) {
ready = false;
break;
}
field->slotcount = table->types[field->typeindex].slotcount;
}
field->offset = offset;
offset += field->slotcount;
}
if ( ready ) {
type->slotcount = offset;
progress = true;
}
}
}
for ( i = 0; i < table->count; i++ ) {
runtime->environment->lineno = table->types[i].firstline;
FAIL_NONZERO_RETURN(errctx, (table->types[i].slotcount < 0), AKBASIC_ERR_VALUE,
"TYPE %s contains itself by value, so it has no size. "
"A type may only refer to itself through PTR TO",
table->types[i].name);
FAIL_NONZERO_RETURN(errctx, (table->types[i].slotcount == 0), AKBASIC_ERR_VALUE,
"TYPE %s declares no fields", table->types[i].name);
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj)
{
PREPARE_ERROR(errctx);
akbasic_StructTypeTable *table = NULL;
int64_t i = 0;
int t = 0;
int64_t entry = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in structtype scan");
FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
"Runtime has no environment; call akbasic_runtime_init() first");
table = &obj->structtypes;
entry = obj->environment->lineno;
/*
* Drop what the *script* declared and keep what the *host* registered.
*
* This prescan runs again on every RUN, so wiping the table outright would
* unregister a host's types the first time a script was run -- and the host
* registered them before loading anything, with no way to know it had to do
* it again. Host types are registered before a program exists, so they are
* always a prefix; truncating to that prefix keeps every index a field
* already recorded pointing at the same type.
*/
for ( t = 0; t < table->count; t++ ) {
if ( !table->types[t].ishost ) {
break;
}
}
for ( i = t; i < table->count; i++ ) {
PASS(errctx, aksl_memset(&table->types[i], 0, sizeof(table->types[i])));
}
table->count = t;
PASS(errctx, scan_names(obj));
for ( t = 0; t < table->count; t++ ) {
akbasic_StructType *type = &table->types[t];
for ( i = type->firstline + 1; i < type->lastline; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
obj->environment->lineno = i;
PASS(errctx, parse_field(table, type, obj->source[i].code, i));
}
}
PASS(errctx, resolve_sizes(obj, table));
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx);
}