From 7165d6451e0d833f4c1de4812eaea495232b7eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Alperen=20=C3=87iftci?= Date: Thu, 29 Oct 2026 11:41:37 +0300 Subject: [PATCH] fix(compiler): check every clause under effect tags 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 ++++++++++++++----- ...9T120000Z-fixed-effect-tags-multiclause.md | 4 ++ .../effect_tags/reject_multiclause.ae | 7 ++++ .../reject_multiclause_transitive.ae | 9 +++++ .../effect_tags/test_effect_tags.sh | 4 +- 5 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 new_changelogs/20261029T120000Z-fixed-effect-tags-multiclause.md create mode 100644 tests/integration/effect_tags/reject_multiclause.ae create mode 100644 tests/integration/effect_tags/reject_multiclause_transitive.ae diff --git a/compiler/analysis/typechecker.c b/compiler/analysis/typechecker.c index 4ced9fbe8..6e0da962a 100644 --- a/compiler/analysis/typechecker.c +++ b/compiler/analysis/typechecker.c @@ -11781,17 +11781,27 @@ static const char* effect_scan(ASTNode* program, ASTNode* node, const char* forb } } { + ASTNode* def = effect_resolve_callee(program, node->value); + const char* callee = def && def->value ? def->value : node->value; int seen = 0; for (int i = 0; i < *nvisited; i++) - if (strcmp(visited[i], node->value) == 0) { seen = 1; break; } + if (strcmp(visited[i], callee) == 0) { seen = 1; break; } if (!seen && *nvisited < 512) { - ASTNode* def = effect_resolve_callee(program, node->value); if (def && def->child_count > 0) { - visited[(*nvisited)++] = node->value; - ASTNode* body = def->children[def->child_count - 1]; - const char* c = effect_scan(program, body, forbid, visited, - nvisited, outcall, outsz, depth + 1); - if (c) return c; + visited[(*nvisited)++] = callee; + for (int i = 0; i < program->child_count; i++) { + ASTNode* clause = program->children[i]; + if (!clause || + (clause->type != AST_FUNCTION_DEFINITION && + clause->type != AST_BUILDER_FUNCTION) || + !clause->value || strcmp(clause->value, callee) != 0 || + clause->child_count == 0) + continue; + ASTNode* body = clause->children[clause->child_count - 1]; + const char* c = effect_scan(program, body, forbid, visited, + nvisited, outcall, outsz, depth + 1); + if (c) return c; + } } } } @@ -11817,14 +11827,24 @@ static void check_effect_tags(ASTNode* program) { if (!eff) continue; const char* forbid = eff + 7; /* "fs,net,os" or a subset */ if (fn->child_count == 0) continue; - ASTNode* body = fn->children[fn->child_count - 1]; const char* visited[512]; int nv = 0; if (fn->value) visited[nv++] = fn->value; /* no self-recursion loop */ char offending[160]; offending[0] = '\0'; - const char* cap = effect_scan(program, body, forbid, visited, &nv, - offending, sizeof(offending), 0); + const char* cap = NULL; + for (int j = 0; j < program->child_count && !cap; j++) { + ASTNode* clause = program->children[j]; + if (!clause || + (clause->type != AST_FUNCTION_DEFINITION && + clause->type != AST_BUILDER_FUNCTION) || + !clause->value || !fn->value || strcmp(clause->value, fn->value) != 0 || + clause->child_count == 0) + continue; + ASTNode* body = clause->children[clause->child_count - 1]; + cap = effect_scan(program, body, forbid, visited, &nv, + offending, sizeof(offending), 0); + } if (cap) { /* #2729: named as the source spells it (`checks.shell`), not by * the merged C name (`checks_shell`). */ diff --git a/new_changelogs/20261029T120000Z-fixed-effect-tags-multiclause.md b/new_changelogs/20261029T120000Z-fixed-effect-tags-multiclause.md new file mode 100644 index 000000000..b8e047cc1 --- /dev/null +++ b/new_changelogs/20261029T120000Z-fixed-effect-tags-multiclause.md @@ -0,0 +1,4 @@ +- **Effect tags now cover every clause of a function.** `@pure`, `@no_fs`, + `@no_net`, and `@no_os` no longer accept a function when a later pattern + clause, or a later clause of a transitive callee, reaches a forbidden + capability. diff --git a/tests/integration/effect_tags/reject_multiclause.ae b/tests/integration/effect_tags/reject_multiclause.ae new file mode 100644 index 000000000..8da61264b --- /dev/null +++ b/tests/integration/effect_tags/reject_multiclause.ae @@ -0,0 +1,7 @@ +import std.os + +@no_os +status(0) -> 0 +status(n: int) -> os.system("echo hi") + n + +main() { status(1) } diff --git a/tests/integration/effect_tags/reject_multiclause_transitive.ae b/tests/integration/effect_tags/reject_multiclause_transitive.ae new file mode 100644 index 000000000..7b8f49e66 --- /dev/null +++ b/tests/integration/effect_tags/reject_multiclause_transitive.ae @@ -0,0 +1,9 @@ +import std.os + +helper(0) -> 0 +helper(n: int) -> os.system("echo hi") + n + +@pure +run() -> helper(1) + +main() { run() } diff --git a/tests/integration/effect_tags/test_effect_tags.sh b/tests/integration/effect_tags/test_effect_tags.sh index 3d4f489a1..0ed0b867d 100755 --- a/tests/integration/effect_tags/test_effect_tags.sh +++ b/tests/integration/effect_tags/test_effect_tags.sh @@ -25,6 +25,8 @@ check_rejected() { check_rejected reject_fs fs check_rejected reject_transitive os check_rejected reject_net net +check_rejected reject_multiclause os +check_rejected reject_multiclause_transitive os -echo " [PASS] effect_tags: @no_fs / @pure-transitive / @no_net violations all rejected" +echo " [PASS] effect_tags: direct, transitive, and multi-clause violations all rejected" exit 0