From abafe87c7aeed72388a05fd57ab04a232c1d8ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Alperen=20=C3=87iftci?= Date: Thu, 29 Oct 2026 11:57:44 +0300 Subject: [PATCH] fix(compiler): retain enum prefixes for containment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adil Alperen Çiftci --- compiler/analysis/typechecker.c | 40 ++++++++++- ...9T124500Z-fixed-enum-containment-prefix.md | 3 + .../enum_containment/test_enum_containment.sh | 71 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 new_changelogs/20261029T124500Z-fixed-enum-containment-prefix.md create mode 100755 tests/integration/enum_containment/test_enum_containment.sh diff --git a/compiler/analysis/typechecker.c b/compiler/analysis/typechecker.c index 4ced9fbe8..50bda3aff 100644 --- a/compiler/analysis/typechecker.c +++ b/compiler/analysis/typechecker.c @@ -12351,6 +12351,26 @@ static void enum_rewrite_ast(ASTNode* nd, const char** names, int n) { enum_rewrite_ast(nd->children[i], names, n); } +/* A qualified enum member is lowered to one identifier before lexical scopes + * are checked. Preserve the prefix the source wrote so hide/seal decisions use + * `Color`, not the generated `Color_Red` constant name. */ +static int enum_member_source_prefix(const ASTNode* nd, char* out, size_t outsz) { + static const char marker[] = "enum_prefix:"; + if (!nd || !nd->annotation || !out || outsz == 0) return 0; + size_t marker_len = sizeof(marker) - 1; + for (const char* p = nd->annotation; (p = strstr(p, marker)) != NULL; p += marker_len) { + if (p != nd->annotation && p[-1] != ';') continue; + const char* value = p + marker_len; + const char* end = strchr(value, ';'); + size_t len = end ? (size_t)(end - value) : strlen(value); + if (len == 0 || len >= outsz) return 0; + memcpy(out, value, len); + out[len] = '\0'; + return 1; + } + return 0; +} + // #1044: rewrite `EnumName.Member` member-access nodes into a plain identifier // naming the C enum constant `EnumName_Member`, with the enum's type. Done up // front (in the resolve pass) so the identifier is in place before the @@ -12370,6 +12390,9 @@ static void enum_rewrite_member_access(ASTNode* nd) { } else { char qualified[512]; snprintf(qualified, sizeof(qualified), "%s_%s", ename, nd->value); + char marker[512]; + snprintf(marker, sizeof(marker), "enum_prefix:%s", ename); + nd->annotation = annotation_add_marker(nd->annotation, marker); Type* et = create_type(TYPE_ENUM); et->struct_name = strdup(ename); free(nd->value); @@ -16749,7 +16772,22 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { return typecheck_function_call(expr, table); case AST_IDENTIFIER: { - Symbol* symbol = lookup_symbol(table, expr->value); + char enum_prefix[512]; + int qualified_enum = enum_member_source_prefix(expr, enum_prefix, + sizeof(enum_prefix)); + if (qualified_enum && qualified_prefix_blocked(table, enum_prefix)) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), + "'%s' is hidden in this scope by `hide` or `seal except`", + enum_prefix); + type_error(error_msg, expr->line, expr->column); + return 0; + } + /* The source prefix passed containment. Resolve the generated + * constant without applying hide/seal a second time to its C name; + * `seal except Color` grants `Color.Red`, not `Color_Red`. */ + Symbol* symbol = qualified_enum ? lookup_member_symbol(table, expr->value) + : lookup_symbol(table, expr->value); if (!symbol) { char error_msg[256]; if (expr->value && name_blocked_by_hide(table, expr->value)) { diff --git a/new_changelogs/20261029T124500Z-fixed-enum-containment-prefix.md b/new_changelogs/20261029T124500Z-fixed-enum-containment-prefix.md new file mode 100644 index 000000000..71f811fb4 --- /dev/null +++ b/new_changelogs/20261029T124500Z-fixed-enum-containment-prefix.md @@ -0,0 +1,3 @@ +- **Qualified enum members now retain their lexical prefix.** `hide Color` + blocks `Color.Red`, while `seal except Color` permits it; containment no + longer checks the generated `Color_Red` constant name. diff --git a/tests/integration/enum_containment/test_enum_containment.sh b/tests/integration/enum_containment/test_enum_containment.sh new file mode 100755 index 000000000..d8a1a0452 --- /dev/null +++ b/tests/integration/enum_containment/test_enum_containment.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# Qualified enum members retain their written prefix through enum lowering so +# lexical hide/seal checks see the same name the source used. +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +AE="$ROOT/build/ae" +[ -x "$AE" ] || { echo " [SKIP] enum_containment: $AE not built"; exit 0; } + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +cat >"$TMPDIR/hide.ae" <<'AE' +enum Color { Red, Blue } +main() { + { + hide Color + c = Color.Red + } +} +AE + +cat >"$TMPDIR/seal.ae" <<'AE' +enum Color { Red, Blue } +main() { + keep = 1 + { + seal except keep + c = Color.Red + } +} +AE + +cat >"$TMPDIR/allowed.ae" <<'AE' +enum Color { Red, Blue } +main() { + { + seal except Color + c = Color.Red + } +} +AE + +failed=0 +reject() { + name="$1" + if "$AE" build "$TMPDIR/$name.ae" -o "$TMPDIR/$name" >"$TMPDIR/$name.log" 2>&1; then + echo " [FAIL] enum_containment: $name accepted a blocked enum prefix" + failed=1 + return + fi + if ! grep -q "'Color' is hidden in this scope" "$TMPDIR/$name.log"; then + echo " [FAIL] enum_containment: $name failed without the containment diagnostic" + sed 's/^/ /' "$TMPDIR/$name.log" | head -10 + failed=1 + fi +} + +reject hide +reject seal + +if ! "$AE" build "$TMPDIR/allowed.ae" -o "$TMPDIR/allowed" >"$TMPDIR/allowed.log" 2>&1; then + echo " [FAIL] enum_containment: whitelisted enum prefix was rejected" + sed 's/^/ /' "$TMPDIR/allowed.log" | head -10 + failed=1 +fi + +[ "$failed" -eq 0 ] || exit 1 + +echo " [PASS] enum_containment: hidden and sealed prefixes reject; whitelist accepts"