From d715bc062547f5c07751b28c5caf61330c110730 Mon Sep 17 00:00:00 2001 From: Logikoma Date: Wed, 5 Aug 2026 06:40:14 -0400 Subject: [PATCH] Report prescan errors on their source lines --- docs/16-structures.md | 2 +- src/data.c | 7 +++++ src/runtime.c | 5 ++++ src/structtype.c | 14 +++++++-- tests/interrupts.c | 25 ++++++++++++++++ tests/language/structures/reserved_names.txt | 2 +- tests/read_data.c | 30 ++++++++++++++++++++ tests/struct_types.c | 17 +++++++++++ 8 files changed, 98 insertions(+), 4 deletions(-) diff --git a/docs/16-structures.md b/docs/16-structures.md index 0343040..eff3f6c 100644 --- a/docs/16-structures.md +++ b/docs/16-structures.md @@ -50,7 +50,7 @@ nothing would say which type it is. ``` ```output -? 40 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type +? 10 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type ``` diff --git a/src/data.c b/src/data.c index 2893bfa..4a08bcc 100644 --- a/src/data.c +++ b/src/data.c @@ -160,14 +160,21 @@ akerr_ErrorContext *akbasic_data_scan(akbasic_Runtime *obj) { PREPARE_ERROR(errctx); int64_t i = 0; + int64_t entry = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan"); + FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER, + "Runtime has no environment; call akbasic_runtime_init() first"); + entry = obj->environment->lineno; PASS(errctx, akbasic_data_state_init(&obj->data_state)); for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { if ( obj->source[i].code[0] != '\0' ) { + /* Keep BASIC's error prefix on the source line being prescanned. */ + obj->environment->lineno = i; PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i)); } } + obj->environment->lineno = entry; SUCCEED_RETURN(errctx); } diff --git a/src/runtime.c b/src/runtime.c index 27a0a29..a451660 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -1576,17 +1576,22 @@ akerr_ErrorContext *akbasic_runtime_scan_labels(akbasic_Runtime *obj) PREPARE_ERROR(errctx); akbasic_Environment *root = NULL; int64_t i = 0; + int64_t entry = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan_labels"); FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER, "Runtime has no environment; call akbasic_runtime_init() first"); for ( root = obj->environment; root->parent != NULL; root = root->parent ) { } + entry = obj->environment->lineno; for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { if ( obj->source[i].code[0] != '\0' ) { + /* Keep BASIC's error prefix on the source line being prescanned. */ + obj->environment->lineno = i; PASS(errctx, scan_line_labels(root, obj->source[i].code, i)); } } + obj->environment->lineno = entry; SUCCEED_RETURN(errctx); } diff --git a/src/structtype.c b/src/structtype.c index 435bc15..ee80f9d 100644 --- a/src/structtype.c +++ b/src/structtype.c @@ -260,11 +260,13 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) size_t namelen = 0; bool matched = false; const akbasic_Verb *verb = NULL; + int64_t entry = obj->environment->lineno; for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { if ( obj->source[i].code[0] == '\0' ) { continue; } + obj->environment->lineno = i; cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word)); PASS(errctx, word_is(word, "END", &matched)); @@ -327,6 +329,7 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj) FAIL_NONZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX, "TYPE %s is never closed with END TYPE", table->types[open >= 0 ? open : 0].name); + obj->environment->lineno = entry; SUCCEED_RETURN(errctx); } @@ -440,7 +443,7 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S * each other by value, which has no finite size. That is the diagnosis rather * than a stack overflow later. */ -static akerr_ErrorContext *resolve_sizes(akbasic_StructTypeTable *table) +static akerr_ErrorContext *resolve_sizes(akbasic_Runtime *runtime, akbasic_StructTypeTable *table) { PREPARE_ERROR(errctx); bool progress = true; @@ -477,6 +480,7 @@ static akerr_ErrorContext *resolve_sizes(akbasic_StructTypeTable *table) } for ( i = 0; i < table->count; i++ ) { + runtime->environment->lineno = table->types[i].firstline; FAIL_NONZERO_RETURN(errctx, (table->types[i].slotcount < 0), AKBASIC_ERR_VALUE, "TYPE %s contains itself by value, so it has no size. " "A type may only refer to itself through PTR TO", @@ -493,9 +497,13 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj) akbasic_StructTypeTable *table = NULL; int64_t i = 0; int t = 0; + int64_t entry = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in structtype scan"); + FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER, + "Runtime has no environment; call akbasic_runtime_init() first"); table = &obj->structtypes; + entry = obj->environment->lineno; /* * Drop what the *script* declared and keep what the *host* registered. @@ -525,10 +533,12 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj) if ( obj->source[i].code[0] == '\0' ) { continue; } + obj->environment->lineno = i; PASS(errctx, parse_field(table, type, obj->source[i].code, i)); } } - PASS(errctx, resolve_sizes(table)); + PASS(errctx, resolve_sizes(obj, table)); + obj->environment->lineno = entry; SUCCEED_RETURN(errctx); } diff --git a/tests/interrupts.c b/tests/interrupts.c index 9ee7d30..2b1f6eb 100644 --- a/tests/interrupts.c +++ b/tests/interrupts.c @@ -16,6 +16,8 @@ * below. */ +#include + #include "harness.h" /** @@ -341,6 +343,28 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_prescan_boundaries(void) SUCCEED_RETURN(errctx); } +/** @brief A full label table reports the line whose label could not be filed. */ +static void test_label_prescan_error_line(void) +{ + char source[4096] = ""; + size_t used = 0; + int i = 0; + + for ( i = 1; i <= AKBASIC_MAX_LABELS + 1; i++ ) { + used += (size_t)snprintf(source + used, sizeof(source) - used, + "%d LABEL L%d\n", i, i); + } + (void)snprintf(source + used, sizeof(source) - used, "100 PRINT 1\n"); + + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source)); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 65 : PARSE ERROR") != NULL, + "a full label table should report its source line, got \"%s\"", + HARNESS_OUTPUT); + harness_stop(); +} + int main(void) { PREPARE_ERROR(errctx); @@ -355,6 +379,7 @@ int main(void) CATCH(errctx, test_undefined_label_is_reported()); CATCH(errctx, test_arm_refusals()); CATCH(errctx, test_prescan_boundaries()); + test_label_prescan_error_line(); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { diff --git a/tests/language/structures/reserved_names.txt b/tests/language/structures/reserved_names.txt index b25f440..571b60d 100644 --- a/tests/language/structures/reserved_names.txt +++ b/tests/language/structures/reserved_names.txt @@ -1,2 +1,2 @@ -? 90 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type +? 60 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type diff --git a/tests/read_data.c b/tests/read_data.c index b861725..3f85475 100644 --- a/tests/read_data.c +++ b/tests/read_data.c @@ -8,6 +8,7 @@ * TODO.md section 6. */ +#include #include #include @@ -174,6 +175,34 @@ static void test_colon_ends_data(void) harness_stop(); } +/** @brief DATA overflow reports the line where the item limit was crossed. */ +static void test_data_prescan_error_line(void) +{ + char source[4096] = ""; + size_t used = 0; + int line = 0; + int item = 0; + + for ( line = 1; line <= 34; line++ ) { + used += (size_t)snprintf(source + used, sizeof(source) - used, "%d DATA ", line); + for ( item = 0; item < 15; item++ ) { + used += (size_t)snprintf(source + used, sizeof(source) - used, + "%s1", (item == 0 ? "" : ",")); + } + used += (size_t)snprintf(source + used, sizeof(source) - used, "\n"); + } + used += (size_t)snprintf(source + used, sizeof(source) - used, "100 DATA 1,1\n"); + (void)snprintf(source + used, sizeof(source) - used, "101 DATA 1\n200 PRINT 1\n"); + + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source)); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 101 : PARSE ERROR") != NULL, + "DATA overflow should report its source line, got \"%s\"", + HARNESS_OUTPUT); + harness_stop(); +} + /** @brief A float item fills a float variable with its fractional part intact. */ static void test_float_items(void) { @@ -227,6 +256,7 @@ int main(void) test_type_mismatch(); test_quoted_items(); test_colon_ends_data(); + test_data_prescan_error_line(); test_float_items(); test_negative_items(); return akbasic_test_failures; diff --git a/tests/struct_types.c b/tests/struct_types.c index 16c5243..3607ff9 100644 --- a/tests/struct_types.c +++ b/tests/struct_types.c @@ -218,6 +218,22 @@ static void test_declaration_errors_are_basic_errors(void) } } +/** @brief A field declaration error reports the field's source line. */ +static void test_type_prescan_error_line(void) +{ + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, + "10 TYPE RECT\n" + "20 W# EXTRA\n" + "30 END TYPE\n" + "40 PRINT 1\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 20 : PARSE ERROR") != NULL, + "TYPE prescan should report its source line, got \"%s\"", + HARNESS_OUTPUT); + harness_stop(); +} + /** * @brief An over-long type or field name is refused, not silently trimmed. * @@ -270,6 +286,7 @@ int main(void) test_missing_field_lists_the_others(); test_self_by_value_refused(); test_declaration_errors_are_basic_errors(); + test_type_prescan_error_line(); test_long_names_are_refused(); return akbasic_test_failures;