210 lines
7.0 KiB
C
210 lines
7.0 KiB
C
|
|
/**
|
||
|
|
* @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 <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
#include <akbasic/structtype.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|