Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m6s
akbasic CI Build / sanitizers (push) Failing after 3m59s
akbasic CI Build / coverage (push) Failing after 3m25s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Failing after 3m15s
0.6.0 and 0.7.0 break nothing here: 0.6.0 is three arcade-physics fixes and a physics.max_timestep property this port does not use, and 0.7.0 reports failures libakstdlib's wrappers were already catching and takes libakerror 2.0.1 so it can drop the exit-status trap its own suites needed. That is the same defect include/akbasic/error.h guards for this band, now fixed at the source rather than worked around in two places. The floor moves to 0.7.0 anyway. The soname carries MAJOR.MINOR while the major is 0, so deciding for ourselves which of libakgl's minor releases were really compatible is exactly the judgement it exists to take away. 0.7.0 is largely about a defect class worth checking for here rather than assuming past: ten unterminated strncpy calls into fixed-width name fields. Ours are clean -- every one is bounded to size - 1 and every buffer is either explicitly terminated afterwards or memset first, checked site by site. But two of them, both written last week in src/structtype.c, truncated an over-long name silently, and truncation is an error everywhere else in this interpreter. It matters more for a name than for a value: two long type names trimmed to the same 31 characters collide, and the second is then reported as redeclaring a type the program never wrote. Both are refused now, showing the name as written. The check was unreachable as first drafted, because the prescan read words into a buffer the size of the limit and so trimmed them before anything could look -- the scratch is twice the limit now, which is what makes the two cases distinguishable at all. tests/struct_types.c pins that it stays reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
277 lines
10 KiB
C
277 lines
10 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 COORD\n"
|
|
"20 X#\n"
|
|
"30 Y#\n"
|
|
"40 END TYPE\n"
|
|
"50 TYPE SEGMENT\n"
|
|
"60 SRC@ AS COORD\n"
|
|
"70 DEST@ AS COORD\n"
|
|
"80 NAME$\n"
|
|
"90 END TYPE\n"
|
|
"100 PRINT 1\n"));
|
|
TEST_REQUIRE_OK(akbasic_structtype_find(&HARNESS_RUNTIME.structtypes, "SEGMENT", &index));
|
|
TEST_REQUIRE(index >= 0, "SEGMENT should have been declared");
|
|
if ( index < 0 ) {
|
|
harness_stop();
|
|
return;
|
|
}
|
|
/* Two coordinates 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, "SRC@", &field));
|
|
TEST_REQUIRE_INT(field->offset, 0);
|
|
TEST_REQUIRE_INT(field->slotcount, 2);
|
|
TEST_REQUIRE_OK(akbasic_structtype_field(&HARNESS_RUNTIME.structtypes, index, "DEST@", &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");
|
|
if ( index < 0 ) {
|
|
harness_stop();
|
|
return;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief An over-long type or field name is refused, not silently trimmed.
|
|
*
|
|
* Truncation is an error everywhere else here, and it matters more for a name
|
|
* than for a value: two long names trimmed to the same 31 characters would
|
|
* collide, and the second would be reported as a redeclaration of a type the
|
|
* program never wrote.
|
|
*
|
|
* The check was unreachable when it was first written, because the prescan read
|
|
* words into a buffer the size of the limit and so trimmed them before anything
|
|
* could look. Pinned here so it stays reachable.
|
|
*/
|
|
static void test_long_names_are_refused(void)
|
|
{
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 TYPE AVERYVERYVERYLONGTYPENAMETHATGOESONANDON\n"
|
|
"20 W#\n"
|
|
"30 END TYPE\n"
|
|
"40 PRINT 1\n"));
|
|
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, "exceeds the 31 character limit") != NULL,
|
|
"an over-long type name should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
/* The whole name, not the trimmed one -- that is what makes it actionable. */
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GOESONANDON") != NULL,
|
|
"the refusal should show the name as written, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 TYPE OKNAME\n"
|
|
"20 AVERYVERYVERYLONGFIELDNAMETHATGOESON#\n"
|
|
"30 END TYPE\n"
|
|
"40 PRINT 1\n"));
|
|
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, "exceeds the 31 character limit") != NULL,
|
|
"an over-long field name should be refused, got \"%s\"", 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();
|
|
test_long_names_are_refused();
|
|
|
|
return akbasic_test_failures;
|
|
}
|