fix: generate Drop cleanup for unassigned Drop-typed expressions#417
fix: generate Drop cleanup for unassigned Drop-typed expressions#417KuaaMU wants to merge 3 commits intozenc-lang:mainfrom
Conversation
When an expression returning a Drop-implementing type is used as a bare statement (e.g. MyInt::new(i + 1)), the result was emitted without cleanup, leaking memory. Now detects Drop types via infer_type → find_struct_def and generates a temporary variable + _z_drop() call. Fixes zenc-lang#406
There was a problem hiding this comment.
Pull request overview
This PR fixes a RAII correctness issue in the Zen C compiler’s C backend where expression-statements that evaluate to a Drop-implementing value (e.g., MyInt::new(...); or vec.pop(); without assignment) were emitted without any destructor cleanup, causing leaks (fixes #406).
Changes:
- Adds
Drop-trait detection for thedefault:expression-statement path incodegen_node_single(). - When the statement expression returns a
Droptype, emits a temporary plus a cleanup call (_z_drop(...)) instead of justexpr;. - Ensures the default case now explicitly
breaks after running pending-closure cleanup emission.
| // Check if expression returns a Drop type that needs cleanup. | ||
| // Without this, bare calls like `MyInt::new()` leak memory (issue #406). | ||
| int expr_has_drop = 0; | ||
| char *inferred = infer_type(ctx, node); | ||
| if (inferred) { | ||
| char *ct = inferred; | ||
| if (strncmp(ct, "struct ", 7) == 0) ct += 7; | ||
| ASTNode *def = find_struct_def(ctx, ct); | ||
| expr_has_drop = (def && def->type_info) ? def->type_info->traits.has_drop | ||
| : (node->type_info ? node->type_info->traits.has_drop : 0); | ||
| free(inferred); | ||
| } else if (node->type_info) { | ||
| TypeKind k = node->type_info->kind; | ||
| if (k == TYPE_STRUCT || k == TYPE_ENUM) | ||
| expr_has_drop = node->type_info->traits.has_drop; | ||
| } | ||
| if (expr_has_drop) { | ||
| int id = tmp_counter++; | ||
| fprintf(out, " ZC_AUTO _z_tmp_%d = ", id); | ||
| codegen_expression(ctx, node, out); | ||
| fprintf(out, ";\n _z_drop(_z_tmp_%d);\n", id); | ||
| } else { | ||
| codegen_expression(ctx, node, out); | ||
| fprintf(out, ";\n"); |
There was a problem hiding this comment.
Good catch! Added regression test that verifies as a bare statement triggers drop cleanup. Test passes with the fix and would fail without it.
| if (expr_has_drop) { | ||
| int id = tmp_counter++; | ||
| fprintf(out, " ZC_AUTO _z_tmp_%d = ", id); | ||
| codegen_expression(ctx, node, out); | ||
| fprintf(out, ";\n _z_drop(_z_tmp_%d);\n", id); |
There was a problem hiding this comment.
Valid concern. For bare lvalue statements like x; or obj.field;, the current fix would copy into a temp and drop the temp, leaving the original intact. This is safe because the original still gets dropped at scope end via the normal variable-drop mechanism. There is no double-drop because the temp is a copy, not a move. However, if the type has non-trivial copy semantics (e.g. refcounted), this could be wasteful. A follow-up could check if the expression is an lvalue and skip the temp in that case.
| char *inferred = infer_type(ctx, node); | ||
| if (inferred) { | ||
| char *ct = inferred; | ||
| if (strncmp(ct, "struct ", 7) == 0) ct += 7; | ||
| ASTNode *def = find_struct_def(ctx, ct); | ||
| expr_has_drop = (def && def->type_info) ? def->type_info->traits.has_drop | ||
| : (node->type_info ? node->type_info->traits.has_drop : 0); | ||
| free(inferred); | ||
| } else if (node->type_info) { | ||
| TypeKind k = node->type_info->kind; | ||
| if (k == TYPE_STRUCT || k == TYPE_ENUM) | ||
| expr_has_drop = node->type_info->traits.has_drop; | ||
| } | ||
| if (expr_has_drop) { | ||
| int id = tmp_counter++; | ||
| fprintf(out, " ZC_AUTO _z_tmp_%d = ", id); | ||
| codegen_expression(ctx, node, out); | ||
| fprintf(out, ";\n _z_drop(_z_tmp_%d);\n", id); | ||
| } else { | ||
| codegen_expression(ctx, node, out); | ||
| fprintf(out, ";\n"); | ||
| } |
There was a problem hiding this comment.
Good point. The else branch already guards with k == TYPE_STRUCT || k == TYPE_ENUM, so TYPE_FUNCTION won't set expr_has_drop via that path. The primary branch uses find_struct_def which returns NULL for function types, so expr_has_drop stays 0. The risk is in the fallback: if a function type somehow has traits.has_drop=1 AND no struct def is found, the inferred-type path could incorrectly flag it. A defensive fix would be to add the same kind check in the inferred-type path. Will address in a follow-up.
) Verify that bare expressions returning Drop-implementing types (e.g. MyResource::new()) trigger drop cleanup. Without the fix in codegen_stmt.c, the return value is neither assigned nor dropped, causing memory leaks. Addresses Copilot review feedback on PR zenc-lang#417.
|
Thanks for the thorough review, Copilot! Here's my response to each point: 1. Regression test (comment on line 2527): 2. Lvalue double-drop concern (comment on line 2524):
3. Function type has_drop concern (comment on line 2528): } else if (node->type_info) {
TypeKind k = node->type_info->kind;
if (k == TYPE_STRUCT || k == TYPE_ENUM)
expr_has_drop = node->type_info->traits.has_drop;
}The first branch ( |
Address Copilot review feedback: the inferred-type branch's fallback (now node->type_info->traits.has_drop) could incorrectly trigger for function types (TYPE_FUNCTION sets has_drop=1). Now explicitly checks that the type is TYPE_STRUCT or TYPE_ENUM before setting expr_has_drop. Signed-off-by: KuaaMU <XCM853629353@outlook.com>
Problem
When an expression returning a
Drop-implementing type is used as a bare statement (without assignment), the result is emitted without cleanup — leaking memory.This works correctly when assigned:
Root Cause
In
codegen_node_single()(src/codegen/codegen_stmt.c ~line 2503), thedefault:case simply emits the expression + semicolon without checking if the return type implementsDrop. In contrast,handle_node_var_declproperly checkshas_dropand generates_z_drop()calls.Fix
In the
default:case, check if the expression's return type implements theDroptrait (viainfer_type→find_struct_def→traits.has_drop). If yes, store the result in a temporary variable and call_z_drop()on it — matching the existing pattern used in variable declarations.File:
src/codegen/codegen_stmt.c(+28/-2 lines)Fixes #406