/** * @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 #include #include #include #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; }