From 3a01cb4c51ca2cde2be897c98062be51ba59ea05 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:32:15 +0900 Subject: [PATCH] =?UTF-8?q?lower:=20=EC=86=8C=EC=9C=A0=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=8A=A4=EC=BD=94=ED=94=84=20=EB=81=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defer 목록을 '스코프가 아직 갚아야 할 것' 목록으로 일반화했다. defer 블록과 소유 값 해제가 같은 목록에 쓰인 순서대로 들어가고, 모든 이탈 경로가 역순으로 갚는다. 해제에는 값 옆에 플래그를 둔다. 값이 저장될 때 세우고 넘겨줄 때 지운다. 값이 아직 여기 있는 경로에서만 해제되는데, 그건 코드의 모양만 봐서는 알 수 없는 것이다. 검사기가 소유권을 넘기는 사용을 이미 표시해두므로 그것을 읽는다. !void 함수의 빈 return 은 성공이다. 줄 값도 없고 오류도 없다는 뜻인데 프론트엔드가 타입 불일치로 거부하고 있었다. 런타임이 할당/해제 횟수를 센다. owndrop 프로그램이 그 둘이 일치함을 실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부. --- fec/rt/start.asm | 18 +++++ fec/src/check.c | 5 ++ fec/src/lower.c | 140 ++++++++++++++++++++++++++++++++++---- fec/std/sys.fe | 7 ++ fec/tests/exec/owndrop.fe | 28 ++++++++ 5 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 fec/tests/exec/owndrop.fe diff --git a/fec/rt/start.asm b/fec/rt/start.asm index 340020f..45ead57 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -28,6 +28,8 @@ colon db ':',0 newline db 13,10,0 numbuf db 16 dup(0) written dd 0 +allocs dd 0 +frees dd 0 _DATA ends @@ -168,6 +170,7 @@ fe_rt_alloc proc near push 8 ; HEAP_ZERO_MEMORY push eax call _HeapAlloc@12 + inc dword ptr [allocs] mov esp, ebp pop ebp ret @@ -186,12 +189,27 @@ fe_rt_free proc near push 0 push eax call _HeapFree@12 + inc dword ptr [frees] free_done: mov esp, ebp pop ebp ret fe_rt_free endp +; fe_rt_allocs() / fe_rt_frees() -- what the allocator has been asked to do, +; so that a test can insist every allocation was released. +public fe_rt_allocs +fe_rt_allocs proc near + mov eax, [allocs] + ret +fe_rt_allocs endp + +public fe_rt_frees +fe_rt_frees proc near + mov eax, [frees] + ret +fe_rt_frees endp + ; fe_rt_exit(code) -- never returns public fe_rt_exit fe_rt_exit proc near diff --git a/fec/src/check.c b/fec/src/check.c index a86cd34..4c6ae41 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -3615,6 +3615,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n) actual && actual->kind==FE_TYPE_ERROR_UNION && !fe_type_equal(expected,actual)) err(s->c,n->loc,"error result type mismatch"); + /* A bare `return` in a function returning `!void` is the success case: + there is no value to give, and no error either. */ + else if (!n->a && expected && expected->kind==FE_TYPE_ERROR_UNION && + expected->error_value && + expected->error_value->kind==FE_TYPE_VOID) { } else if (!fe_type_equal(expected,stored) && !m7_actual_compatible(expected,stored,n->a)) err(s->c,n->loc,"return type mismatch"); diff --git a/fec/src/lower.c b/fec/src/lower.c index ae90687..e6ad699 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -1,6 +1,7 @@ #include "lower.h" #include #include "m7.h" +#include "own.h" #include /* ------------------------------------------------------------------------- * @@ -36,10 +37,21 @@ typedef struct Lower { unsigned break_target[32]; unsigned continue_target[32]; unsigned loop_depth; - /* `defer` blocks in the order they were written. Every exit path runs the - ones that are live, last written first. */ - FeNode *deferred[32]; - unsigned defer_count; + /* What a scope still owes when it ends: `defer` blocks to run and owned + values to release, in the order they were written. Every exit path runs + what is live, last first. + + A drop carries a flag beside the value. The flag is set when the value + is stored and cleared wherever it is moved away, so the release happens + exactly on the paths where the value is still there -- which is not + something the shape of the code can tell you on its own. */ + struct { + FeNode *block; /* a `defer`, when set */ + unsigned local; /* the owned value, otherwise */ + unsigned flag; + FeType *type; + } owed[64]; + unsigned owed_count; /* Every `error.Name` used anywhere in the build, sorted, numbered from one. SPEC 4.6: the names are collected rather than declared, and the order is fixed by the spelling so that the same program always gets the same @@ -194,6 +206,14 @@ static unsigned as_address(Lower *L, Slot s, FeNode *n) /* --------------------------------------------------------------- locals --- */ +/* Does letting go of this type have to do something? */ +static int needs_release(const FeType *t) +{ + if (!t) return 0; + if (t->kind == FE_TYPE_OWNED) return 1; + return t->has_drop != 0; +} + static unsigned declare_var(Lower *L, const char *cname, const FeType *t, const char *name) { @@ -205,9 +225,31 @@ static unsigned declare_var(Lower *L, const char *cname, const FeType *t, L->vars[L->var_count].by_address = 0; ++L->var_count; } + if (needs_release(t) && L->owed_count < 64) { + unsigned flag = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "live"); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + L->owed[L->owed_count].block = 0; + L->owed[L->owed_count].local = local; + L->owed[L->owed_count].flag = flag; + L->owed[L->owed_count].type = (FeType *)t; + ++L->owed_count; + } return local; } +/* The liveness flag beside a local, or none. */ +static int release_flag(Lower *L, unsigned local, unsigned *flag) +{ + unsigned i; + for (i = L->owed_count; i > 0; --i) + if (!L->owed[i - 1].block && L->owed[i - 1].local == local) { + *flag = L->owed[i - 1].flag; + return 1; + } + return 0; +} + static LowerVar *find_var(Lower *L, const char *cname) { unsigned i; @@ -687,6 +729,16 @@ static Slot lower_expr(Lower *L, FeNode *n) Slot v; if (!n || L->failed) return slot_void(); v = lower_expr_core(L, n); + /* The checker marked the uses that hand ownership away. Where one names a + local we track, the value is no longer ours to release. */ + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->kind == FE_N_IDENT) { + LowerVar *var = find_var(L, n->cname); + unsigned flag; + if (var && release_flag(L, var->local, &flag)) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + } + } return n->sem_context ? wrap_context(L, v, n) : v; } @@ -938,12 +990,40 @@ static Slot lower_expr_core(Lower *L, FeNode *n) } } -/* Run the `defer` blocks that are live, most recent first. A `return` in the - middle of a function still owes them, so every exit path calls this. */ +/* Settle what a scope owes, most recent first. A `return` in the middle of a + function still owes everything, so every exit path calls this. */ static void run_deferred(Lower *L, unsigned from) { unsigned i; - for (i = L->defer_count; i > from; --i) lower_stmt(L, L->deferred[i - 1]); + for (i = L->owed_count; i > from; --i) { + if (L->owed[i - 1].block) { + lower_stmt(L, L->owed[i - 1].block); + continue; + } + { + /* Release only where the value is still here. */ + unsigned live = fe_ir_load(L->m, L->b, FE_IR_I8, + fe_ir_at_local(L->owed[i - 1].flag, 0)); + FeIrBlock *doit = new_block(L); + FeIrBlock *skip = new_block(L); + unsigned args[1]; + FeType *t = L->owed[i - 1].type; + fe_ir_br(L->b, live, doit->id, skip->id); + L->b = doit; + if (t && t->kind == FE_TYPE_OWNED && t->elem && + t->elem->kind == FE_TYPE_SLICE) { + FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local, + SLICE_PTR_OFFSET); + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + } else { + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->owed[i - 1].local, 0)); + } + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + fe_ir_jmp(L->b, skip->id); + L->b = skip; + } + } } /* ------------------------------------------------------- wrappers -------- * @@ -1116,7 +1196,30 @@ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, static void lower_return(Lower *L, FeNode *n) { Slot v; - if (!n->a) { run_deferred(L, 0); fe_ir_ret(L->b, 0, 0); return; } + if (!n->a) { + /* A bare return from a `!void` function still has to say that nothing + went wrong. */ + if (L->ret_type && L->ret_type->kind == FE_TYPE_ERROR_UNION) { + unsigned local = scratch(L, L->ret_type, "success"); + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), + fe_ir_at_local(local, 0), ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(L->ret_type), + fe_ir_at_local(local, 0)), 1); + return; + } + run_deferred(L, 0); + fe_ir_ret(L->b, 0, 0); + return; + } /* The value is computed before the deferred blocks run, because they may destroy what it was read from. */ v = lower_expr(L, n->a); @@ -1400,12 +1503,12 @@ static void lower_stmt(Lower *L, FeNode *n) if (!n || L->failed) return; switch (n->kind) { case FE_N_BLOCK: { - unsigned outer = L->defer_count; + unsigned outer = L->owed_count; for (x = n->children; x; x = x->next) lower_stmt(L, x); - /* Leaving a block normally runs what it deferred. An exit that jumped - away already ran them on its way out. */ + /* Leaving a block normally settles what it owes. An exit that jumped + away already settled on its way out. */ if (!L->b->terminated) run_deferred(L, outer); - L->defer_count = outer; + L->owed_count = outer; return; } case FE_N_LET: @@ -1414,7 +1517,12 @@ static void lower_stmt(Lower *L, FeNode *n) unsigned local = declare_var(L, n->cname, n->sem_type, n->text); if (n->b) { Slot v = lower_expr(L, n->b); + unsigned flag; store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); + if (release_flag(L, local, &flag)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), one, FE_IR_I8); + } } return; } @@ -1448,7 +1556,13 @@ static void lower_stmt(Lower *L, FeNode *n) lower_stmt(L, n->a); return; case FE_N_DEFER: - if (L->defer_count < 32) L->deferred[L->defer_count++] = n->a; + if (L->owed_count < 64) { + L->owed[L->owed_count].block = n->a; + L->owed[L->owed_count].local = 0; + L->owed[L->owed_count].flag = 0; + L->owed[L->owed_count].type = 0; + ++L->owed_count; + } return; case FE_N_FOR: lower_for(L, n); diff --git a/fec/std/sys.fe b/fec/std/sys.fe index 18088b8..c2c91f9 100644 --- a/fec/std/sys.fe +++ b/fec/std/sys.fe @@ -6,6 +6,8 @@ extern "c" fn fe_rt_write(handle: i32, bytes: *u8, len: usize) -> i32; extern "c" fn fe_rt_alloc(n: usize) -> *u8; extern "c" fn fe_rt_free(p: *u8); extern "c" fn fe_rt_exit(code: i32); +extern "c" fn fe_rt_allocs() -> i32; +extern "c" fn fe_rt_frees() -> i32; pub fn exit(code: i32) -> void { unsafe { fe_rt_exit(code); } @@ -22,3 +24,8 @@ pub fn raw_alloc(n: usize) -> *u8 { pub fn raw_free(p: *u8) -> void { unsafe { fe_rt_free(p); } } + +// How many times the allocator was asked to hand out memory, and to take it +// back. A test can insist the two agree; nothing else should care. +pub fn allocs() -> i32 { unsafe { return fe_rt_allocs(); } } +pub fn frees() -> i32 { unsafe { return fe_rt_frees(); } } diff --git a/fec/tests/exec/owndrop.fe b/fec/tests/exec/owndrop.fe new file mode 100644 index 0000000..f12cabd --- /dev/null +++ b/fec/tests/exec/owndrop.fe @@ -0,0 +1,28 @@ +// EXIT:0 +// OUTPUT:balanced +unit owndrop; +import std.io; +import std.sys; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn scoped() -> !void { + let a: ^i32 = try mem.create(1); + let b: ^i32 = try mem.create(2); + take(b); +} + +fn early(flag: bool) -> !void { + let p: ^i32 = try mem.create(3); + if flag { return; } + take(p); +} + +fn main() -> i32 { + scoped() catch |e| { return 1; }; + early(true) catch |e| { return 2; }; + early(false) catch |e| { return 3; }; + if sys.allocs() == sys.frees() { io.print("balanced\n"); return 0; } + io.print("leaked\n"); + return 4; +}