/** * @file struct_pointers.c * @brief Tests strict pointers: `PTR TO`, `POINT ... AT`, and `->`. * * The assertions worth having here are the *refusals*. A pointer that works is * visible in tests/language/structures/; a pointer that should not have worked * is not, because the failure mode of a missing check is a program that keeps * going and gives the wrong answer later. * * "Strict" means the two operators do not stand in for one another: `.` needs a * structure and `->` needs a pointer, so a reader always knows from the spelling * whether the thing on the left is their own copy or somebody else's data. */ #include #include #include #include #include "harness.h" #include "testutil.h" /** @brief A RECT, an instance, and a pointer already aimed at it. */ static const char *PRELUDE = "10 TYPE RECT\n" "20 W#\n" "30 H#\n" "40 END TYPE\n" "50 DIM A@ AS RECT\n" "60 DIM P@ AS PTR TO RECT\n" "70 A@.W# = 3\n" "80 A@.H# = 4\n" "90 POINT P@ AT A@\n"; /** @brief Run the prelude plus @p tail, and leave the output in HARNESS_OUTPUT. */ static akerr_ErrorContext AKERR_NOIGNORE *run_with(const char *tail) { PREPARE_ERROR(errctx); char source[2048]; snprintf(source, sizeof(source), "%s%s", PRELUDE, tail); PASS(errctx, harness_start(NULL)); PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source)); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); SUCCEED_RETURN(errctx); } /** @brief Writing through a pointer changes the thing it points at. */ static void test_pointer_writes_through(void) { TEST_REQUIRE_OK(run_with("100 P@->W# = 99\n110 PRINT A@.W#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n"); harness_stop(); } /** * @brief Assignment still copies, even with a pointer in the room. * * The property the whole design rests on: `POINT` is the only way to share, so * a program that never writes it can never be surprised by aliasing. */ static void test_assignment_still_copies(void) { TEST_REQUIRE_OK(run_with("100 DIM B@ AS RECT\n" "110 B@ = A@\n" "120 P@->W# = 7\n" "130 PRINT A@.W#\n" "140 PRINT B@.W#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n3\n"); harness_stop(); } /** * @brief The two operators do not stand in for one another. * * Both messages name the other operator, because the mistake is always one of * the two and saying which is most of the fix. */ static void test_wrong_operator_refused(void) { TEST_REQUIRE_OK(run_with("100 PRINT P@.W#\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "use -> to reach a field through a pointer") != NULL, "'.' on a pointer should say to use ->, got \"%s\"", HARNESS_OUTPUT); harness_stop(); TEST_REQUIRE_OK(run_with("100 PRINT A@->W#\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "use . to reach a field of a structure") != NULL, "'->' on a structure should say to use ., got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief A pointer that has never been POINTed is not a null dereference. */ static void test_unbound_pointer_refused(void) { TEST_REQUIRE_OK(run_with("100 DIM Q@ AS PTR TO RECT\n110 PRINT Q@->W#\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not pointing at anything") != NULL, "an unbound pointer should refuse by name, got \"%s\"", HARNESS_OUTPUT); harness_stop(); /* And it renders as NOTHING rather than as a crash or an empty line. */ TEST_REQUIRE_OK(run_with("100 DIM Q@ AS PTR TO RECT\n110 PRINT Q@\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "NOTHING\n"); harness_stop(); } /** @brief A pointer is to one type and will not be aimed at another. */ static void test_pointer_type_is_checked(void) { TEST_REQUIRE_OK(run_with("100 TYPE OTHER\n" "110 N#\n" "120 END TYPE\n" "130 DIM O@ AS OTHER\n" "140 POINT P@ AT O@\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "cannot be pointed at") != NULL, "a pointer to RECT should refuse an OTHER, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief A structure cannot be assigned to a pointer, and the message says why. * * The two are spelled differently on purpose. Allowing `P@ = A@` to mean "point * at" would make assignment sometimes copy and sometimes alias, which is the one * ambiguity this design exists to avoid. */ static void test_structure_not_assignable_to_pointer(void) { TEST_REQUIRE_OK(run_with("100 P@ = A@\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "POINT it AT") != NULL, "assigning a structure to a pointer should say to POINT it, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief A list is built, walked and rendered -- the reason pointers exist. * * Walked with reassignment rather than recursion, deliberately: a recursive * multi-line DEF does not return in this interpreter, which is recorded in * TODO.md and is not this feature's to fix. */ static void test_linked_list(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 PTR TO NODE\n" "40 END TYPE\n" "50 DIM N1@ AS NODE\n" "60 DIM N2@ AS NODE\n" "70 N1@.COUNT# = 10\n" "80 N2@.COUNT# = 20\n" "90 POINT N1@.TAIL@ AT N2@\n" "100 DIM WALK@ AS PTR TO NODE\n" "110 POINT WALK@ AT N1@\n" "120 PRINT WALK@->COUNT#\n" "130 WALK@ = WALK@->TAIL@\n" "140 PRINT WALK@->COUNT#\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); TEST_REQUIRE_STR(HARNESS_OUTPUT, "10\n20\n"); harness_stop(); } /** * @brief A cycle renders to a bounded depth instead of forever. * * Copy cannot follow a pointer, so no *copy* can loop -- but rendering must * follow one or a list would not be worth printing, and two nodes pointing at * each other is easy to write by accident. */ static void test_cycle_renders_bounded(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 PTR TO NODE\n" "40 END TYPE\n" "50 DIM A@ AS NODE\n" "60 DIM B@ AS NODE\n" "70 POINT A@.TAIL@ AT B@\n" "80 POINT B@.TAIL@ AT A@\n" "90 PRINT A@\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "(...)") != NULL, "a cycle should stop at the depth bound, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } int main(void) { TEST_REQUIRE_OK(akbasic_error_register()); test_pointer_writes_through(); test_assignment_still_copies(); test_wrong_operator_refused(); test_unbound_pointer_refused(); test_pointer_type_is_checked(); test_structure_not_assignable_to_pointer(); test_linked_list(); test_cycle_renders_bounded(); return akbasic_test_failures; }