FENCE ( -- ) exposes the dict_fence_latest/dict_fence_here state FORGET already honored internally, letting callers (e.g. a future SDK capsule) raise the boundary after loading their own content -- no new VM fields, no policy logic beyond exposing existing state. Writing a direct test for it surfaced a real, severe, pre-existing bug in FORGET's relink logic, unrelated to FENCE itself and reproducible with the original boot-time fence alone: - Forgetting the single newest word incorrectly destroyed every other word back to the fence too, not just the target. - Forgetting an older word (correctly cascading to remove newer words too, per FORTH-79 semantics) crashed with SIGSEGV. Root cause: the relink code's target_prev pointer was, by construction, always inside the range the preceding loop had just freed whenever target wasn't vm->latest -- so writing through it was a use-after-free every time that branch executed. Fixed by removing the target_prev tracking and both branches entirely; vm->latest unconditionally becomes target_next (target's own captured, still-valid link) after the free loop, correct in every case. Added a FENCE test suite to dictionary_manipulation_words_test.c (Module 14) including the exact regression case (forgetting the newest word must not disturb an older one). Verified zero warnings and identical POST/dict_hash results across all three kernel architectures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
194 lines
7.7 KiB
C
194 lines
7.7 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
#include "../include/test_runner.h"
|
||
#include "../include/test_common.h"
|
||
|
||
/* Dictionary Manipulation Words Test Suites - Module 14 */
|
||
static WordTestSuite dict_manip_word_suites[] = {
|
||
{
|
||
"CREATE", {
|
||
{"basic", "CREATE test1 42 , test1 @ . CR", "Should create and store", TEST_NORMAL, 0, 1, {0}},
|
||
{"empty_name", "CREATE", "Should handle empty name", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{
|
||
"redefine_shadows", "CREATE T 1 , CREATE T 2 , T @ . CR", "Newest definition should win (prints 2)",
|
||
TEST_NORMAL, 0, 1, {0}
|
||
},
|
||
{"duplicate", "CREATE test2 CREATE test2", "Should allow redefinition (latest shadows)", TEST_NORMAL, 0, 1, {0}},
|
||
{"long_name", "CREATE abcdefghijklmnopqrstuvwxyz", "Should handle long name", TEST_EDGE_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
4, {0}
|
||
},
|
||
|
||
{
|
||
"FORGET", {
|
||
{"basic", "CREATE temp1 FORGET temp1", "Should forget word", TEST_NORMAL, 0, 1, {0}},
|
||
{"nonexistent", "FORGET nonexistent", "Should handle missing word", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{"protected", "FORGET FORGET", "Should protect system words", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
3, {0}
|
||
},
|
||
|
||
{
|
||
/* FENCE ( -- ): raises the FORGET boundary to the current dictionary
|
||
* top. Added alongside a real bug fix in FORGET itself, caught while
|
||
* building this word -- forgetting the single newest word used to
|
||
* incorrectly destroy every other word back to the fence too (a
|
||
* use-after-free in the relink logic, not just an off-by-one), a
|
||
* SIGSEGV in the worst case. "forget_latest_keeps_predecessor" below
|
||
* is exactly the regression case that fix addresses. */
|
||
"FENCE", {
|
||
{
|
||
"basic", "CREATE fnc1 FENCE CREATE fnc2 FORGET fnc2",
|
||
"Should forget a word defined after FENCE", TEST_NORMAL, 0, 1, {0}
|
||
},
|
||
{
|
||
"protects_older", "CREATE fnc3 CREATE fnc4 FENCE FORGET fnc3",
|
||
"Should refuse to forget a word defined before FENCE", TEST_ERROR_CASE, 1, 1, {0}
|
||
},
|
||
{
|
||
"forget_latest_keeps_predecessor",
|
||
"CREATE fnc5 FENCE CREATE fnc6 CREATE fnc7 FORGET fnc7 fnc6 DROP",
|
||
"Forgetting the newest word must not disturb an older one still above FENCE",
|
||
TEST_NORMAL, 0, 1, {0}
|
||
},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
3, {0}
|
||
},
|
||
|
||
{
|
||
"IMMEDIATE", {
|
||
{"basic", ": test3 42 ; IMMEDIATE test3 . CR", "Should execute immediately", TEST_NORMAL, 0, 1, {0}},
|
||
{
|
||
"already_immediate", ": test4 43 ; IMMEDIATE IMMEDIATE", "Should handle double immediate", TEST_NORMAL, 0, 1, {0}
|
||
},
|
||
/* TODO: IMMEDIATE outside definition - requires compile-state tracking in IMMEDIATE */
|
||
/* {"outside_def", "IMMEDIATE", "Should error outside definition", TEST_ERROR_CASE, 1, 1}, */
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
2, {0}
|
||
},
|
||
|
||
{
|
||
"FIND", {
|
||
{"existing", "FIND DUP . CR", "Should find system word", TEST_NORMAL, 0, 1, {0}},
|
||
{"user_word", ": test5 44 ; FIND test5 . CR", "Should find user word", TEST_NORMAL, 0, 1, {0}},
|
||
{"nonexistent", "FIND nonexistent . CR", "Should return 0", TEST_NORMAL, 0, 1, {0}},
|
||
{"empty", "FIND", "Should error (no token)", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
4, {0}
|
||
},
|
||
|
||
{
|
||
"DEFINITIONS", {
|
||
{"basic", "FORTH DEFINITIONS", "Should set current to context", TEST_NORMAL, 0, 1, {0}},
|
||
{"multiple", "FORTH DEFINITIONS DEFINITIONS", "Should be idempotent", TEST_NORMAL, 0, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
2, {0}
|
||
},
|
||
|
||
{
|
||
"SMUDGE", {
|
||
{"during_def", ": test6 SMUDGE 45 ;", "Should hide incomplete def", TEST_NORMAL, 0, 1, {0}},
|
||
{"outside_def", "SMUDGE", "Should error outside def", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
2, {0}
|
||
},
|
||
|
||
{
|
||
"LATEST", {
|
||
{"basic", "LATEST @ . CR", "Should show latest word", TEST_NORMAL, 0, 1, {0}},
|
||
{"after_def", ": test7 46 ; LATEST @ . CR", "Should update after def", TEST_NORMAL, 0, 1, {0}},
|
||
{
|
||
"after_forget", "CREATE test8 FORGET test8 LATEST @ . CR", "Should update after forget", TEST_NORMAL, 0,
|
||
1, {0}
|
||
},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
3, {0}
|
||
},
|
||
|
||
{
|
||
">BODY", {
|
||
{"basic", "CREATE test9 ' test9 >BODY . CR", "Should get param field", TEST_NORMAL, 0, 1, {0}},
|
||
{"variable", "VARIABLE var1 ' var1 >BODY . CR", "Should get var storage", TEST_NORMAL, 0, 1, {0}},
|
||
{"nonexistent", "' NONEXISTENT >BODY", "Should handle missing word", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
3, {0}
|
||
},
|
||
|
||
{
|
||
"HIDDEN", {
|
||
{"basic", ": test10 HIDDEN 47 ;", "Should hide word", TEST_NORMAL, 0, 1, {0}},
|
||
{"outside_def", "HIDDEN", "Should error outside def", TEST_ERROR_CASE, 1, 1, {0}},
|
||
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
|
||
},
|
||
2, {0}
|
||
},
|
||
|
||
/* End marker */
|
||
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
|
||
};
|
||
|
||
/**
|
||
* @brief Executes all dictionary manipulation words test suites
|
||
* @param vm Pointer to the Forth virtual machine instance
|
||
* @details Runs through all test cases for CREATE, FORGET, IMMEDIATE, FIND,
|
||
* DEFINITIONS, SMUDGE, LATEST, >BODY, and HIDDEN words
|
||
*/
|
||
void run_dictionary_manipulation_words_tests(VM *vm) {
|
||
log_message(LOG_INFO, "Running Dictionary Manipulation Words Tests (Module 14)...");
|
||
|
||
for (int i = 0; dict_manip_word_suites[i].word_name != NULL; i++) {
|
||
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
|
||
run_test_suite(vm, &dict_manip_word_suites[i]);
|
||
}
|
||
|
||
print_module_summary("Dictionary Manipulation Words", 0, 0, 0, 0);
|
||
} |