197 lines
6.9 KiB
C
197 lines
6.9 KiB
C
/*
|
|
* test_scope.c — Tests for the chunked linked-frame CBMScope.
|
|
*
|
|
* Phase 0 of Python LSP integration: replaces the legacy fixed 64-binding
|
|
* array with growable per-scope chunks. Tests verify dynamic growth,
|
|
* binding-shadowing semantics, and the parent-chain lookup contract that
|
|
* Go and C/C++ LSP implementations rely on.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "cbm.h"
|
|
#include "lsp/scope.h"
|
|
#include "lsp/type_rep.h"
|
|
|
|
/* Build a NAMED type for a fixture string. Arena-allocated. */
|
|
static const CBMType *named_t(CBMArena *a, const char *qn) {
|
|
return cbm_type_named(a, qn);
|
|
}
|
|
|
|
/* ── Basic API ─────────────────────────────────────────────────── */
|
|
|
|
TEST(scope_push_returns_distinct_scope) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *root = cbm_scope_push(&a, NULL);
|
|
CBMScope *child = cbm_scope_push(&a, root);
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_NOT_NULL(child);
|
|
ASSERT(child != root);
|
|
ASSERT(child->parent == root);
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_pop_returns_parent) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *root = cbm_scope_push(&a, NULL);
|
|
CBMScope *child = cbm_scope_push(&a, root);
|
|
ASSERT(cbm_scope_pop(child) == root);
|
|
ASSERT(cbm_scope_pop(root) == NULL);
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_lookup_unbound_returns_unknown) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
const CBMType *t = cbm_scope_lookup(s, "missing");
|
|
ASSERT_NOT_NULL(t);
|
|
ASSERT(cbm_type_is_unknown(t));
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Binding semantics ─────────────────────────────────────────── */
|
|
|
|
TEST(scope_bind_then_lookup) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
cbm_scope_bind(s, "x", named_t(&a, "int"));
|
|
const CBMType *t = cbm_scope_lookup(s, "x");
|
|
ASSERT_NOT_NULL(t);
|
|
ASSERT(t->kind == CBM_TYPE_NAMED);
|
|
ASSERT_STR_EQ(t->data.named.qualified_name, "int");
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_rebind_overwrites_in_place) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
cbm_scope_bind(s, "x", named_t(&a, "int"));
|
|
cbm_scope_bind(s, "x", named_t(&a, "string"));
|
|
const CBMType *t = cbm_scope_lookup(s, "x");
|
|
ASSERT_STR_EQ(t->data.named.qualified_name, "string");
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_lookup_walks_parent_chain) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *root = cbm_scope_push(&a, NULL);
|
|
CBMScope *child = cbm_scope_push(&a, root);
|
|
cbm_scope_bind(root, "outer", named_t(&a, "Outer"));
|
|
cbm_scope_bind(child, "inner", named_t(&a, "Inner"));
|
|
const CBMType *outer_in_child = cbm_scope_lookup(child, "outer");
|
|
const CBMType *inner_in_root = cbm_scope_lookup(root, "inner");
|
|
ASSERT_STR_EQ(outer_in_child->data.named.qualified_name, "Outer");
|
|
ASSERT(cbm_type_is_unknown(inner_in_root));
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_child_shadows_parent) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *root = cbm_scope_push(&a, NULL);
|
|
CBMScope *child = cbm_scope_push(&a, root);
|
|
cbm_scope_bind(root, "x", named_t(&a, "ParentInt"));
|
|
cbm_scope_bind(child, "x", named_t(&a, "ChildStr"));
|
|
const CBMType *t = cbm_scope_lookup(child, "x");
|
|
ASSERT_STR_EQ(t->data.named.qualified_name, "ChildStr");
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Dynamic growth — the Phase 0 win ───────────────────────────── */
|
|
|
|
TEST(scope_dynamic_growth_300_bindings) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
char names[300][16];
|
|
for (int i = 0; i < 300; i++) {
|
|
snprintf(names[i], sizeof(names[0]), "v%d", i);
|
|
char qn[32];
|
|
snprintf(qn, sizeof(qn), "T%d", i);
|
|
cbm_scope_bind(s, names[i], named_t(&a, qn));
|
|
}
|
|
/* All 300 bindings retrievable — old 64-cap would have dropped 236 */
|
|
for (int i = 0; i < 300; i++) {
|
|
const CBMType *t = cbm_scope_lookup(s, names[i]);
|
|
ASSERT_NOT_NULL(t);
|
|
ASSERT(t->kind == CBM_TYPE_NAMED);
|
|
char expected[32];
|
|
snprintf(expected, sizeof(expected), "T%d", i);
|
|
ASSERT_STR_EQ(t->data.named.qualified_name, expected);
|
|
}
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_growth_chunk_boundary) {
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
char names[CBM_SCOPE_CHUNK_BINDINGS + 1][16];
|
|
/* Fill exactly one chunk + spillover: forces second chunk allocation */
|
|
for (int i = 0; i <= CBM_SCOPE_CHUNK_BINDINGS; i++) {
|
|
snprintf(names[i], sizeof(names[0]), "n%d", i);
|
|
char qn[16];
|
|
snprintf(qn, sizeof(qn), "T%d", i);
|
|
cbm_scope_bind(s, names[i], named_t(&a, qn));
|
|
}
|
|
/* First-chunk binding still retrievable */
|
|
const CBMType *first = cbm_scope_lookup(s, "n0");
|
|
ASSERT_STR_EQ(first->data.named.qualified_name, "T0");
|
|
/* Spillover binding (would have fallen off legacy 64-cap if cap < 17) */
|
|
char last_name[16];
|
|
snprintf(last_name, sizeof(last_name), "n%d", CBM_SCOPE_CHUNK_BINDINGS);
|
|
char last_qn[16];
|
|
snprintf(last_qn, sizeof(last_qn), "T%d", CBM_SCOPE_CHUNK_BINDINGS);
|
|
const CBMType *last = cbm_scope_lookup(s, last_name);
|
|
ASSERT_STR_EQ(last->data.named.qualified_name, last_qn);
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
TEST(scope_rebind_in_old_chunk) {
|
|
/* After many bindings have spilled into newer chunks, rebinding a name
|
|
* from the original chunk must overwrite that binding in place rather
|
|
* than create a duplicate in the head chunk. */
|
|
CBMArena a;
|
|
cbm_arena_init(&a);
|
|
CBMScope *s = cbm_scope_push(&a, NULL);
|
|
cbm_scope_bind(s, "early", named_t(&a, "EarlyV1"));
|
|
char names[100][16];
|
|
for (int i = 0; i < 100; i++) {
|
|
snprintf(names[i], sizeof(names[0]), "fill%d", i);
|
|
cbm_scope_bind(s, names[i], named_t(&a, "Filler"));
|
|
}
|
|
cbm_scope_bind(s, "early", named_t(&a, "EarlyV2"));
|
|
const CBMType *t = cbm_scope_lookup(s, "early");
|
|
ASSERT_STR_EQ(t->data.named.qualified_name, "EarlyV2");
|
|
cbm_arena_destroy(&a);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Suite registration ────────────────────────────────────────── */
|
|
|
|
SUITE(scope) {
|
|
RUN_TEST(scope_push_returns_distinct_scope);
|
|
RUN_TEST(scope_pop_returns_parent);
|
|
RUN_TEST(scope_lookup_unbound_returns_unknown);
|
|
RUN_TEST(scope_bind_then_lookup);
|
|
RUN_TEST(scope_rebind_overwrites_in_place);
|
|
RUN_TEST(scope_lookup_walks_parent_chain);
|
|
RUN_TEST(scope_child_shadows_parent);
|
|
RUN_TEST(scope_dynamic_growth_300_bindings);
|
|
RUN_TEST(scope_growth_chunk_boundary);
|
|
RUN_TEST(scope_rebind_in_old_chunk);
|
|
}
|