Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/codegen/codegen_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -2500,8 +2500,37 @@ void codegen_node_single(ParserContext *ctx, ASTNode *node, FILE *out)
break;
}
default:
codegen_expression(ctx, node, out);
fprintf(out, ";\n");
{
// 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);
if (def && def->type_info) {
expr_has_drop = def->type_info->traits.has_drop;
} 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;
}
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);
Comment on lines +2525 to +2529
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

} else {
codegen_expression(ctx, node, out);
fprintf(out, ";\n");
Comment on lines +2504 to +2532
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Added regression test that verifies as a bare statement triggers drop cleanup. Test passes with the fix and would fail without it.

}
Comment on lines +2507 to +2533
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if (node->type == NODE_EXPR_CALL && node->call.callee && pending_closure_free_count > 0)
{
int is_thread_spawn = 0;
Expand All @@ -2524,6 +2553,8 @@ void codegen_node_single(ParserContext *ctx, ASTNode *node, FILE *out)
}
}
emit_pending_closure_frees(out);
break;
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions tests/runtime/memory/test_drop_unassigned.zc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Regression test for issue #406:
// Bare expressions returning Drop types should still call drop.
// Before the fix, `MyResource::new();` would leak memory because
// the return value was neither assigned nor dropped.

import "std/mem.zc"

let DROP_CALLED = 0;

struct MyResource {
id: int;
}

impl Drop for MyResource {
fn drop(self) {
DROP_CALLED = DROP_CALLED + 1;
}
}

impl MyResource {
fn new(id: int) -> MyResource {
return MyResource { id: id };
}
}

test "drop_unassigned_call" {
DROP_CALLED = 0;

// Bare call — return value is not assigned.
// The generated code must store the result in a temp and call _z_drop.
MyResource::new(42);

if (DROP_CALLED != 1) {
println "Error: Drop was not called for unassigned expression!";
println " Expected DROP_CALLED=1, got {DROP_CALLED}";
exit(1);
}
}
Loading