lower: 소유 값을 스코프 끝에서 자동으로 해제한다
defer 목록을 '스코프가 아직 갚아야 할 것' 목록으로 일반화했다. defer 블록과 소유 값 해제가 같은 목록에 쓰인 순서대로 들어가고, 모든 이탈 경로가 역순으로 갚는다. 해제에는 값 옆에 플래그를 둔다. 값이 저장될 때 세우고 넘겨줄 때 지운다. 값이 아직 여기 있는 경로에서만 해제되는데, 그건 코드의 모양만 봐서는 알 수 없는 것이다. 검사기가 소유권을 넘기는 사용을 이미 표시해두므로 그것을 읽는다. !void 함수의 빈 return 은 성공이다. 줄 값도 없고 오류도 없다는 뜻인데 프론트엔드가 타입 불일치로 거부하고 있었다. 런타임이 할당/해제 횟수를 센다. owndrop 프로그램이 그 둘이 일치함을 실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부.
This commit is contained in:
@@ -28,6 +28,8 @@ colon db ':',0
|
|||||||
newline db 13,10,0
|
newline db 13,10,0
|
||||||
numbuf db 16 dup(0)
|
numbuf db 16 dup(0)
|
||||||
written dd 0
|
written dd 0
|
||||||
|
allocs dd 0
|
||||||
|
frees dd 0
|
||||||
|
|
||||||
_DATA ends
|
_DATA ends
|
||||||
|
|
||||||
@@ -168,6 +170,7 @@ fe_rt_alloc proc near
|
|||||||
push 8 ; HEAP_ZERO_MEMORY
|
push 8 ; HEAP_ZERO_MEMORY
|
||||||
push eax
|
push eax
|
||||||
call _HeapAlloc@12
|
call _HeapAlloc@12
|
||||||
|
inc dword ptr [allocs]
|
||||||
mov esp, ebp
|
mov esp, ebp
|
||||||
pop ebp
|
pop ebp
|
||||||
ret
|
ret
|
||||||
@@ -186,12 +189,27 @@ fe_rt_free proc near
|
|||||||
push 0
|
push 0
|
||||||
push eax
|
push eax
|
||||||
call _HeapFree@12
|
call _HeapFree@12
|
||||||
|
inc dword ptr [frees]
|
||||||
free_done:
|
free_done:
|
||||||
mov esp, ebp
|
mov esp, ebp
|
||||||
pop ebp
|
pop ebp
|
||||||
ret
|
ret
|
||||||
fe_rt_free endp
|
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
|
; fe_rt_exit(code) -- never returns
|
||||||
public fe_rt_exit
|
public fe_rt_exit
|
||||||
fe_rt_exit proc near
|
fe_rt_exit proc near
|
||||||
|
|||||||
@@ -3615,6 +3615,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
|
|||||||
actual && actual->kind==FE_TYPE_ERROR_UNION &&
|
actual && actual->kind==FE_TYPE_ERROR_UNION &&
|
||||||
!fe_type_equal(expected,actual))
|
!fe_type_equal(expected,actual))
|
||||||
err(s->c,n->loc,"error result type mismatch");
|
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) &&
|
else if (!fe_type_equal(expected,stored) &&
|
||||||
!m7_actual_compatible(expected,stored,n->a))
|
!m7_actual_compatible(expected,stored,n->a))
|
||||||
err(s->c,n->loc,"return type mismatch");
|
err(s->c,n->loc,"return type mismatch");
|
||||||
|
|||||||
+127
-13
@@ -1,6 +1,7 @@
|
|||||||
#include "lower.h"
|
#include "lower.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "m7.h"
|
#include "m7.h"
|
||||||
|
#include "own.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
@@ -36,10 +37,21 @@ typedef struct Lower {
|
|||||||
unsigned break_target[32];
|
unsigned break_target[32];
|
||||||
unsigned continue_target[32];
|
unsigned continue_target[32];
|
||||||
unsigned loop_depth;
|
unsigned loop_depth;
|
||||||
/* `defer` blocks in the order they were written. Every exit path runs the
|
/* What a scope still owes when it ends: `defer` blocks to run and owned
|
||||||
ones that are live, last written first. */
|
values to release, in the order they were written. Every exit path runs
|
||||||
FeNode *deferred[32];
|
what is live, last first.
|
||||||
unsigned defer_count;
|
|
||||||
|
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.
|
/* 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
|
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
|
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 --- */
|
/* --------------------------------------------------------------- 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,
|
static unsigned declare_var(Lower *L, const char *cname, const FeType *t,
|
||||||
const char *name)
|
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->vars[L->var_count].by_address = 0;
|
||||||
++L->var_count;
|
++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;
|
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)
|
static LowerVar *find_var(Lower *L, const char *cname)
|
||||||
{
|
{
|
||||||
unsigned i;
|
unsigned i;
|
||||||
@@ -687,6 +729,16 @@ static Slot lower_expr(Lower *L, FeNode *n)
|
|||||||
Slot v;
|
Slot v;
|
||||||
if (!n || L->failed) return slot_void();
|
if (!n || L->failed) return slot_void();
|
||||||
v = lower_expr_core(L, n);
|
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;
|
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
|
/* Settle what a scope owes, most recent first. A `return` in the middle of a
|
||||||
middle of a function still owes them, so every exit path calls this. */
|
function still owes everything, so every exit path calls this. */
|
||||||
static void run_deferred(Lower *L, unsigned from)
|
static void run_deferred(Lower *L, unsigned from)
|
||||||
{
|
{
|
||||||
unsigned i;
|
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 -------- *
|
/* ------------------------------------------------------- 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)
|
static void lower_return(Lower *L, FeNode *n)
|
||||||
{
|
{
|
||||||
Slot v;
|
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
|
/* The value is computed before the deferred blocks run, because they may
|
||||||
destroy what it was read from. */
|
destroy what it was read from. */
|
||||||
v = lower_expr(L, n->a);
|
v = lower_expr(L, n->a);
|
||||||
@@ -1400,12 +1503,12 @@ static void lower_stmt(Lower *L, FeNode *n)
|
|||||||
if (!n || L->failed) return;
|
if (!n || L->failed) return;
|
||||||
switch (n->kind) {
|
switch (n->kind) {
|
||||||
case FE_N_BLOCK: {
|
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);
|
for (x = n->children; x; x = x->next) lower_stmt(L, x);
|
||||||
/* Leaving a block normally runs what it deferred. An exit that jumped
|
/* Leaving a block normally settles what it owes. An exit that jumped
|
||||||
away already ran them on its way out. */
|
away already settled on its way out. */
|
||||||
if (!L->b->terminated) run_deferred(L, outer);
|
if (!L->b->terminated) run_deferred(L, outer);
|
||||||
L->defer_count = outer;
|
L->owed_count = outer;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case FE_N_LET:
|
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);
|
unsigned local = declare_var(L, n->cname, n->sem_type, n->text);
|
||||||
if (n->b) {
|
if (n->b) {
|
||||||
Slot v = lower_expr(L, 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));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1448,7 +1556,13 @@ static void lower_stmt(Lower *L, FeNode *n)
|
|||||||
lower_stmt(L, n->a);
|
lower_stmt(L, n->a);
|
||||||
return;
|
return;
|
||||||
case FE_N_DEFER:
|
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;
|
return;
|
||||||
case FE_N_FOR:
|
case FE_N_FOR:
|
||||||
lower_for(L, n);
|
lower_for(L, n);
|
||||||
|
|||||||
@@ -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_alloc(n: usize) -> *u8;
|
||||||
extern "c" fn fe_rt_free(p: *u8);
|
extern "c" fn fe_rt_free(p: *u8);
|
||||||
extern "c" fn fe_rt_exit(code: i32);
|
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 {
|
pub fn exit(code: i32) -> void {
|
||||||
unsafe { fe_rt_exit(code); }
|
unsafe { fe_rt_exit(code); }
|
||||||
@@ -22,3 +24,8 @@ pub fn raw_alloc(n: usize) -> *u8 {
|
|||||||
pub fn raw_free(p: *u8) -> void {
|
pub fn raw_free(p: *u8) -> void {
|
||||||
unsafe { fe_rt_free(p); }
|
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(); } }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user