BASIC 7.0 has no records at all, so none of this is a port. The `@` suffix was not invented either: the Go reference reserved IDENTIFIER_STRUCT and never used it, and src/grammar.c rendered such a leaf as "NOT IMPLEMENTED" until now. Declaring the type is what buys the storage model. A TYPE states its fields, so an instance has a known slot count and is laid out exactly as an array is -- one contiguous run from the same value pool DIM already draws from, with field access as offset arithmetic. No new pool holds data; the only new table holds descriptors. A nested value flattens into its container's run, which is why LINE with two POINTs and a string is five slots rather than three. Each field takes its type from its own suffix, the same rule every other name here follows, so a field list needs no type column. An `@` field is the exception and has to name its type, because three primitive types fit in three suffix characters and N declared types do not fit in one. The declaration is prescanned before the program runs, like labels and DATA and for the same reason: it has to be in effect wherever control goes. Three passes, each for a case the one before cannot do -- names first so a field can refer to a type declared later, then field lists, then sizes by repeated resolution. What never resolves is a cycle of by-value containment, so "a TYPE cannot contain itself by value" is a diagnosis rather than an assumption, and the message says to use PTR TO instead. Assignment copies. That interception is the whole feature and it cannot live in akbasic_value_clone(), which copies one slot -- and one slot holds a *reference* to an instance rather than the instance, so going through it would alias. A structure is intercepted before that path and its slots are copied one at a time, walking the descriptor rather than memcpy-ing the run, because a pointer field must copy its reference where a value field must copy its slots. Two smaller things the work required. All three prescans now sit inside one ATTEMPT: a malformed declaration is the program's mistake, and it was printing a stack trace and taking the driver with it, which is the boundary goal 3 exists to draw. And a fresh variable's structtype is -1 rather than the 0 a memset leaves, because 0 is a valid type index and every new variable was claiming to be the first type declared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
226 lines
8.3 KiB
C
226 lines
8.3 KiB
C
/**
|
|
* @file struct_types.c
|
|
* @brief Tests `TYPE` declarations, their layout, and records by value.
|
|
*
|
|
* The assertions here are about the *descriptor* -- what the prescan read out of
|
|
* the source and how it laid an instance out -- because that is the part a BASIC
|
|
* program cannot see and therefore cannot pin. What a program can see is pinned
|
|
* by tests/language/structures/ instead.
|
|
*
|
|
* The layout assertions matter more than they look. A field's offset is the only
|
|
* thing standing between `R@.H#` and `R@.W#`, and an off-by-one there reads the
|
|
* neighbouring field rather than failing -- so it would pass every test that
|
|
* only ever set one field and read it back.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
#include <akbasic/structtype.h>
|
|
|
|
#include "harness.h"
|
|
#include "testutil.h"
|
|
|
|
/** @brief Load a program and start it, which is what runs the prescan. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *declare(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static const char *RECT =
|
|
"10 TYPE RECT\n"
|
|
"20 W#\n"
|
|
"30 H#\n"
|
|
"40 END TYPE\n"
|
|
"50 PRINT 1\n";
|
|
|
|
/** @brief A declaration becomes a descriptor with names, types and offsets. */
|
|
static void test_layout(void)
|
|
{
|
|
akbasic_StructField *field = NULL;
|
|
int index = -1;
|
|
|
|
TEST_REQUIRE_OK(declare(RECT));
|
|
TEST_REQUIRE_OK(akbasic_structtype_find(&HARNESS_RUNTIME.structtypes, "RECT", &index));
|
|
TEST_REQUIRE(index >= 0, "RECT should have been declared");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.structtypes.types[index].fieldcount, 2);
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.structtypes.types[index].slotcount, 2);
|
|
|
|
/*
|
|
* Offsets, in declaration order. This is the assertion an off-by-one hides
|
|
* from: swapping these two would still let a program set W# and read W#.
|
|
*/
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "W#", &field));
|
|
TEST_REQUIRE_INT(field->offset, 0);
|
|
TEST_REQUIRE_INT(field->valuetype, AKBASIC_TYPE_INTEGER);
|
|
TEST_REQUIRE_INT(field->slotcount, 1);
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "H#", &field));
|
|
TEST_REQUIRE_INT(field->offset, 1);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A nested value flattens into its container's run.
|
|
*
|
|
* The property that lets an instance be one contiguous slice of the value pool,
|
|
* exactly as an array is, and so needs no pool of its own.
|
|
*/
|
|
static void test_nesting_flattens(void)
|
|
{
|
|
akbasic_StructField *field = NULL;
|
|
int index = -1;
|
|
|
|
TEST_REQUIRE_OK(declare("10 TYPE POINT\n"
|
|
"20 X#\n"
|
|
"30 Y#\n"
|
|
"40 END TYPE\n"
|
|
"50 TYPE LINE\n"
|
|
"60 FROM@ AS POINT\n"
|
|
"70 TO@ AS POINT\n"
|
|
"80 NAME$\n"
|
|
"90 END TYPE\n"
|
|
"100 PRINT 1\n"));
|
|
TEST_REQUIRE_OK(akbasic_structtype_find(&HARNESS_RUNTIME.structtypes, "LINE", &index));
|
|
TEST_REQUIRE(index >= 0, "LINE should have been declared");
|
|
/* Two points of two slots each, plus one string: five, not three. */
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.structtypes.types[index].slotcount, 5);
|
|
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "FROM@", &field));
|
|
TEST_REQUIRE_INT(field->offset, 0);
|
|
TEST_REQUIRE_INT(field->slotcount, 2);
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "TO@", &field));
|
|
TEST_REQUIRE_INT(field->offset, 2);
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "NAME$", &field));
|
|
TEST_REQUIRE_INT(field->offset, 4);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A type may be named before it is declared.
|
|
*
|
|
* Which is why the prescan registers every name before it reads any field list.
|
|
* A linked list needs it even for itself, and this is the case that pins it.
|
|
*/
|
|
static void test_forward_reference(void)
|
|
{
|
|
int index = -1;
|
|
|
|
TEST_REQUIRE_OK(declare("10 TYPE OUTER\n"
|
|
"20 INNER@ AS INNERT\n"
|
|
"30 END TYPE\n"
|
|
"40 TYPE INNERT\n"
|
|
"50 N#\n"
|
|
"60 END TYPE\n"
|
|
"70 PRINT 1\n"));
|
|
TEST_REQUIRE_OK(akbasic_structtype_find(&HARNESS_RUNTIME.structtypes, "OUTER", &index));
|
|
TEST_REQUIRE(index >= 0, "OUTER should have been declared");
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.structtypes.types[index].slotcount, 1);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief The field-missing refusal names the type and lists what it does have. */
|
|
static void test_missing_field_lists_the_others(void)
|
|
{
|
|
akbasic_StructField *field = NULL;
|
|
akerr_ErrorContext *raised = NULL;
|
|
int index = -1;
|
|
|
|
TEST_REQUIRE_OK(declare(RECT));
|
|
TEST_REQUIRE_OK(akbasic_structtype_find(&HARNESS_RUNTIME.structtypes, "RECT", &index));
|
|
|
|
raised = akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "NOPE#", &field);
|
|
TEST_REQUIRE(raised != NULL, "a field the type does not declare must be refused");
|
|
TEST_REQUIRE_INT(raised->status, AKBASIC_ERR_UNDEFINED);
|
|
/*
|
|
* The list is the point. A misspelled field is catchable only because the
|
|
* set is closed and the program wrote it down, so the message shows it --
|
|
* that is what turns "why is my total zero" into a one-line fix.
|
|
*/
|
|
TEST_REQUIRE(strstr(raised->message, "W#") != NULL && strstr(raised->message, "H#") != NULL,
|
|
"the refusal should list the fields RECT does have, got \"%s\"", raised->message);
|
|
test_discard_error(raised);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A type containing itself by value is refused, by name.
|
|
*
|
|
* It has no finite size, so the prescan's size resolution never completes for
|
|
* it -- and "the set of types that never resolved" is exactly the set that
|
|
* contains itself. Diagnosed there rather than discovered when the pool runs
|
|
* out, and the message says what to do instead.
|
|
*/
|
|
static void test_self_by_value_refused(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 TYPE NODE\n"
|
|
"20 COUNT#\n"
|
|
"30 TAIL@ AS NODE\n"
|
|
"40 END TYPE\n"
|
|
"50 PRINT 1\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
/*
|
|
* Reported as a BASIC error rather than raised at the host. A malformed
|
|
* declaration is the program's mistake, and goal 3 says a script's mistakes
|
|
* do not reach the embedding program.
|
|
*/
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "contains itself by value") != NULL,
|
|
"self-containment should be refused by name, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PTR TO") != NULL,
|
|
"the refusal should say what to do instead, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* And through a pointer it is legal, which is how a list is built at all. */
|
|
TEST_REQUIRE_OK(declare("10 TYPE NODE\n"
|
|
"20 COUNT#\n"
|
|
"30 TAIL@ AS PTR TO NODE\n"
|
|
"40 END TYPE\n"
|
|
"50 PRINT 1\n"));
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief A declaration error reaches the program, not the host. */
|
|
static void test_declaration_errors_are_basic_errors(void)
|
|
{
|
|
static const char *CASES[] = {
|
|
"10 TYPE RECT\n20 W\n30 END TYPE\n40 PRINT 1\n",
|
|
"10 TYPE RECT\n20 W#\n30 PRINT 1\n",
|
|
"10 TYPE RECT\n20 W#\n30 END TYPE\n40 TYPE RECT\n50 H#\n60 END TYPE\n70 PRINT 1\n",
|
|
"10 TYPE RECT\n20 W#\n30 W#\n40 END TYPE\n50 PRINT 1\n",
|
|
"10 TYPE RECT\n20 INNER@\n30 END TYPE\n40 PRINT 1\n"
|
|
};
|
|
size_t i = 0;
|
|
|
|
for ( i = 0; i < sizeof(CASES) / sizeof(CASES[0]); i++ ) {
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, CASES[i]));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PARSE ERROR") != NULL,
|
|
"case %zu should report a parse error, got \"%s\"", i, HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
TEST_REQUIRE_OK(akbasic_error_register());
|
|
|
|
test_layout();
|
|
test_nesting_flattens();
|
|
test_forward_reference();
|
|
test_missing_field_lists_the_others();
|
|
test_self_by_value_refused();
|
|
test_declaration_errors_are_basic_errors();
|
|
|
|
return akbasic_test_failures;
|
|
}
|