diff --git a/SPEC.md b/SPEC.md index 0bd8ba1..150c909 100644 --- a/SPEC.md +++ b/SPEC.md @@ -251,18 +251,20 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. -v0.1의 대여 상태는 **root local/parameter 단위**로 추적한다. projection은 place의 root를 찾는 데만 쓰며 field-sensitive/index-sensitive 독립성을 증명하지 않는다. 따라서 `&mut p.a`는 `p` 전체를 배타 대여하고 그동안 `p.b`의 읽기·쓰기·대여도 금지한다. `&mut xs[0]`과 `&mut xs[1]`도 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다. 이는 compiler A/B의 함수-local 상태 기계를 작고 결정적으로 유지하기 위한 v0.1의 의도적인 보수성이다. +여기서 `x`는 변수가 아니라 **place**다. `p.a`와 `p.b`는 서로 다른 place이므로 한쪽을 대여해도 다른 쪽은 그대로 읽고 쓸 수 있다. 대여가 필드 단위로 갈라지는 것은 루트 변수의 직속 필드 한 겹까지이며, 그 아래(`p.a.b`)와 인덱스(`arr[i]`)·역참조(`p.^`)는 전체를 대여한 것으로 본다. 전체를 대여하면 모든 필드와 충돌하고, 필드를 대여하면 전체 및 같은 필드와 충돌한다. 한 값에서 동시에 갈라둘 수 있는 필드 수에는 구현 상한이 있고, 넘으면 전체 대여로 되돌아간다 — 더 많이 보고할 뿐 놓치지는 않는다. + +대여 상태는 **root local/parameter 와 그 직속 필드** 단위로 추적한다. `&mut xs[0]`과 `&mut xs[1]`은 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다 — index는 상수가 아닐 수 있고, 그것을 따지는 것은 함수-local 상태 기계가 감당할 일이 아니다. 필드 이름은 상수라 그 문제가 없으므로 갈라진다. ```fe var p = Pair{ a: 1, b: 2 }; let r = &mut p.a; -p.b = 3; // 에러: root p가 배타 대여 중 +p.b = 3; // ok: p.b는 다른 place +p.a = 3; // 에러: p.a가 배타 대여 중 +take(p); // 에러: 전체는 대여된 필드를 포함한다 r.^ = 4; let a = &mut xs[0]; let b = &mut xs[1]; // 에러: 둘 다 root xs를 대여 -a.^ = 1; -b.^ = 2; ``` 참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. diff --git a/TODO.md b/TODO.md index d3e546f..99bdcea 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | **Ferro 렉서를 Ferro 로** | 강제 함수. 돌아간다 | | `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | | **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | +| 필드 단위 대여 | `p.a` 와 `p.b` 는 다른 place 다. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | 파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). @@ -38,8 +39,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 1 | 심볼 표와 이름 해석을 Ferro 로 | 중 | `std.map` 이 준비됐다 | | 2 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | | 3 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | -| 4 | 필드 단위 대여 | 중 | 지금은 루트 단위라 `self` 의 한 필드에 쓰는 동안 다른 필드를 못 읽는다 | -| 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | +| 4 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | ## 언어에 남은 구멍 @@ -69,6 +69,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | | 배타 대여를 호출에 넘기기 | **이동이 아니라 호출 동안의 재대여** (SPEC §4.2, §5 R6). 없으면 배타 파라미터를 함수당 한 번만 넘길 수 있어 재귀 하강 파서를 못 쓴다 | | 자기 `drop` 안의 부분 이동 | **허용** (SPEC §5 R7 예외). 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | +| 대여 단위 | **place 단위** (SPEC §5 R6). 루트 변수의 직속 필드 한 겹까지 갈라지고, 그 아래와 인덱스·역참조는 전체 대여다. 한 값당 4 필드까지, 넘으면 전체로 되돌아간다 | --- diff --git a/fec/src/check.c b/fec/src/check.c index 57ed1cf..357b955 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -260,6 +260,7 @@ FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->decl = decl; fe_own_state_init(&sym->own, initialized); sym->borrow_root = 0; + sym->borrow_field = 0; sym->borrow_mut = 0; sym->borrow_defer = 0; sym->owner = scope; @@ -506,9 +507,47 @@ int own_is_global(FeCheckerState *s, FeSym *sym) return 0; } +/* Strip the `&`/`&mut` off an expression; the place underneath is what is + being reached. */ +static FeNode *own_strip_ref(FeNode *e) +{ + while (e && e->kind==FE_N_UNARY && e->text && + (strcmp(e->text,"&")==0 || strcmp(e->text,"&mut")==0)) + e=e->a; + return e; +} + +/* The first field projected off the root of `expr`, and that root. + `self.bytes.^[i]` projects `bytes` off `self`. An index (`arr[i]`) and a + dereference (`p.^`) name no field, so they answer for the whole value -- + which is what the checker did for everything before. */ +const char *own_projected_field(FeNode *expr, FeNode **root_out) +{ + FeNode *inner; + FeNode *outer=0; + if (root_out) *root_out=0; + inner=own_strip_ref(expr); + while (inner && (inner->kind==FE_N_MEMBER || inner->kind==FE_N_INDEX)) { + outer=inner; + inner=own_strip_ref(inner->a); + } + if (!inner || inner->kind!=FE_N_IDENT || !outer) return 0; + if (outer->kind!=FE_N_MEMBER) return 0; + /* A member node's own text is the token the postfix chain started at, not + the operator, so the spelling of the projection is what to look at: + `.?` carries nothing on the right and `.^` carries a caret. */ + if (outer->text && (strcmp(outer->text,".?")==0 || + strcmp(outer->text,".^")==0)) return 0; + if (!outer->b || !outer->b->text) return 0; + if (strcmp(outer->b->text,"^")==0) return 0; + if (root_out) *root_out=inner; + return outer->b->text; +} + void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) { FeSym *root=own_root_symbol(s,expr); + const char *field=own_projected_field(expr,0); if (!root) return; if (mutable && root->type && root->type->kind==FE_TYPE_REF && !root->type->ref_mut) { @@ -521,9 +560,9 @@ void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) err(s->c,expr->loc,"cannot borrow a mutable global"); return; } - fe_own_access(s->c->diags,&root->own, - mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - expr->loc); + fe_own_access_field(s->c->diags,&root->own,field, + mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + expr->loc); } void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) @@ -533,8 +572,12 @@ void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) if (strcmp(expr->text,"&")!=0 && strcmp(expr->text,"&mut")!=0) return; root=own_root_symbol(s,expr->a); if (!root) return; - if (strcmp(expr->text,"&mut")==0) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); + { + const char *field=own_projected_field(expr->a,0); + if (strcmp(expr->text,"&mut")==0) + fe_own_release_exclusive_field(&root->own,field); + else fe_own_release_shared_field(&root->own,field); + } } /* Return-reference provenance is represented at call sites by retaining a @@ -577,6 +620,7 @@ void own_bind_derived_call(FeCheckerState *s, FeSym *binding, else fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_SHARED,value->loc); binding->borrow_root=root; + binding->borrow_field=0; binding->borrow_mut=value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut; } @@ -631,9 +675,13 @@ void own_release_after_stmt(FeCheckerState *s, FeScope *scope, ref->decl && ref->decl->text ? ref->decl->text : ref->name); if (!scope_end && (ref->borrow_defer || !last || last->defer_extended || !own_contains_node(stmt,last->last_node))) continue; - if (ref->borrow_mut) fe_own_release_exclusive(&ref->borrow_root->own); - else fe_own_release_shared(&ref->borrow_root->own); + if (ref->borrow_mut) + fe_own_release_exclusive_field(&ref->borrow_root->own, + ref->borrow_field); + else fe_own_release_shared_field(&ref->borrow_root->own, + ref->borrow_field); ref->borrow_root=0; + ref->borrow_field=0; } } @@ -687,6 +735,7 @@ void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, if (!states) return; for (i=0;iborrow_root; + states[i].field=slots[i].sym->borrow_field; states[i].mutable=slots[i].sym->borrow_mut; } } @@ -698,6 +747,7 @@ void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, if (!states) return; for (i=0;iborrow_root=states[i].root; + slots[i].sym->borrow_field=states[i].field; slots[i].sym->borrow_mut=states[i].mutable; } } @@ -709,6 +759,7 @@ void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, if (!left || !right) return; for (i=0;iborrow_root=left[i].root ? left[i].root : right[i].root; + slots[i].sym->borrow_field=left[i].root ? left[i].field : right[i].field; slots[i].sym->borrow_mut=left[i].mutable || right[i].mutable; } } diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index f24f9e8..8454791 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -139,6 +139,7 @@ FeType *check_call(FeCheckerState *s, FeNode *n) !m7_actual_compatible(expected,b,value)) err(c,value->loc,"mem.replace value type mismatch"); mark_moved(s,value,value->sem_type ? value->sem_type : b); + own_release_temporary_borrow(s,arg); n->sem_type=expected ? expected : unknown(c); fe_type_require_replace(&c->types,n->sem_type); return n->sem_type; @@ -351,7 +352,24 @@ FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, return payload; } +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n); + +/* Every expression goes through here, which is where a projection can tell + the identifier underneath it which field is actually being reached. */ FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_expr_dispatch(s,n); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n) { FeType *a; FeType *b; @@ -486,7 +504,24 @@ FeType *check_expr(FeCheckerState *s, FeNode *n) return check_expr_core(s,n); } +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read); + +/* An assignment target is a projection too, so it leaves the same word for the + identifier underneath: `self.room = x` reaches `room` and nothing else. */ FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_lvalue_dispatch(s,n,read); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read) { FeType *base=0; FeFieldType *field; @@ -786,6 +821,7 @@ void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth!=0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index a1b3bb5..3554e31 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -308,7 +308,10 @@ FeType *check_identifier(FeCheckerState *s, FeNode *n) n->cname = sym->cname; n->sem_type = sym->type; if (!sym->fn) { - fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + /* When this identifier is the base of a projection, the read reaches + one field and not the whole value. The chain above left word. */ + const char *field = s->proj_base==n ? s->proj_field : 0; + fe_own_access_field(s->c->diags,&sym->own,field,FE_OWN_READ,n->loc); sym->moved=sym->own.move; } return sym->type; @@ -482,6 +485,10 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) err(c,value->loc,"mem.replace value type mismatch"); if(value) mark_moved(s,value,b); + /* The destination is lent for the length of the call, the same + as any other argument. Without this the borrow stays live to + the end of the function and the place can never be read. */ + own_release_temporary_borrow(s,arg); n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); fe_type_require_replace(&c->types,n->sem_type); return n->sem_type; diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index fa75c95..531045c 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -30,6 +30,8 @@ struct FeSym { release the root borrow without a separate alias engine. */ FeOwnState own; FeSym *borrow_root; + /* Which field of the root this binding borrowed, or null for all of it. */ + const char *borrow_field; int borrow_mut; int borrow_defer; FeScope *owner; @@ -52,6 +54,11 @@ typedef struct FeCheckerState { unsigned defer_depth; FeOwnLiveness liveness; FeNode *fn_node; + /* While a projection is being checked, which field of which base it + reaches. The read happens down at the identifier, which cannot see the + chain above it, so the chain leaves word here on the way down. */ + const char *proj_field; + FeNode *proj_base; } FeCheckerState; /* The type bindings in force, saved across a nested instantiation. */ @@ -70,6 +77,7 @@ typedef struct FeFlowSlot { typedef struct FeFlowBorrow { FeSym *root; + const char *field; int mutable; } FeFlowBorrow; @@ -114,6 +122,7 @@ void flow_restore(FeFlowSlot *slots, unsigned count); void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, unsigned count); FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr); +const char *own_projected_field(FeNode *expr, FeNode **root_out); int own_is_global(FeCheckerState *s, FeSym *sym); void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable); void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 4d28c1e..98f4d01 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -272,6 +272,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth != 0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); @@ -297,6 +298,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth != 0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); diff --git a/fec/src/own.c b/fec/src/own.c index 8b51c37..7217289 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -92,6 +92,7 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) void fe_own_state_init(FeOwnState *state, int initialized) { + unsigned i; if (!state) return; state->move = FE_OWN_AVAILABLE; state->initialized = initialized != 0; @@ -100,6 +101,12 @@ void fe_own_state_init(FeOwnState *state, int initialized) state->borrow_conflict = 0; state->move_loc = fe_own_no_loc(); state->borrow_loc = fe_own_no_loc(); + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + state->fields[i].name = 0; + state->fields[i].shared = 0; + state->fields[i].exclusive = 0; + state->fields[i].loc = fe_own_no_loc(); + } } static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) @@ -130,8 +137,187 @@ static int fe_own_require_stable_borrow(FeDiags *diags, FeOwnState *state, return 0; } +/* Whole-value state only: what a field access has to get past before it looks + at its own entry. `check` reports and decides; `apply` also records. */ +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); + +/* The entry for this field, or null. `make` asks for one to be created. */ +static FeOwnField *fe_own_field_slot(FeOwnState *state, const char *field, + int make) +{ + unsigned i; + unsigned free_slot = FE_OWN_FIELD_MAX; + if (!state || !field) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + if (state->fields[i].name && + strcmp(state->fields[i].name, field) == 0) return &state->fields[i]; + if (!state->fields[i].name && free_slot == FE_OWN_FIELD_MAX) + free_slot = i; + } + if (!make || free_slot == FE_OWN_FIELD_MAX) return 0; + state->fields[free_slot].name = field; + state->fields[free_slot].shared = 0; + state->fields[free_slot].exclusive = 0; + state->fields[free_slot].loc = fe_own_no_loc(); + return &state->fields[free_slot]; +} + +/* A live borrow of some field, for the accesses that reach the whole value. */ +static const FeOwnField *fe_own_field_live(const FeOwnState *state, + int mut_only) +{ + unsigned i; + if (!state) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *f = &state->fields[i]; + if (!f->name) continue; + if (f->exclusive) return f; + if (!mut_only && f->shared) return f; + } + return 0; +} + +void fe_own_release_shared_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->shared) { fe_own_release_shared(state); return; } + --f->shared; + if (!f->shared && !f->exclusive) f->name = 0; +} + +void fe_own_release_exclusive_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->exclusive) { fe_own_release_exclusive(state); return; } + f->exclusive = 0; + if (!f->shared) f->name = 0; +} + int fe_own_access(FeDiags *diags, FeOwnState *state, FeOwnAccessKind access, FeLoc loc) +{ + return fe_own_access_field(diags, state, 0, access, loc); +} + +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc) +{ + FeOwnField *f; + const FeOwnField *other; + if (!state) return 0; + if (access == FE_OWN_PROJECTION) return 1; + if (!field) { + /* Reaching the whole value: a borrow of any part of it is in the way. + A shared borrow of a field still lets the whole be read. */ + other = fe_own_field_live(state, access == FE_OWN_READ); + if (other) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" : + access == FE_OWN_MOVE ? "cannot move while value is borrowed" : + access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot borrow while a field of the value is borrowed", + other->loc, "borrow originated here"); + return 0; + } + return fe_own_access_whole(diags, state, access, loc); + } + /* Reaching one field: a borrow of the whole value is in the way, and so is + a borrow of this same field. A borrow of a different field is not. */ + if (!fe_own_access_whole_check(diags, state, access, loc)) return 0; + f = fe_own_field_slot(state, field, + access == FE_OWN_BORROW_SHARED || + access == FE_OWN_BORROW_MUT); + if (!f) { + /* No room left in the table, so this borrow covers the whole value. + That reports more than it has to and never less. */ + if (access == FE_OWN_BORROW_SHARED || access == FE_OWN_BORROW_MUT) + return fe_own_access_whole(diags, state, access, loc); + return 1; + } + switch (access) { + case FE_OWN_READ: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot read directly while value is mutably borrowed", + f->loc, "mutable borrow originated here"); + return 0; + } + return 1; + case FE_OWN_WRITE: + case FE_OWN_MOVE: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" + : "cannot move while value is borrowed", + f->loc, "borrow originated here"); + return 0; + } + return 1; + case FE_OWN_BORROW_SHARED: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create shared borrow while mutable borrow is live", + f->loc, "mutable borrow originated here"); + return 0; + } + if (!f->shared) f->loc = loc; + ++f->shared; + return 1; + case FE_OWN_BORROW_MUT: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create mutable borrow while another borrow is live", + f->loc, "existing borrow originated here"); + return 0; + } + f->exclusive = 1; + f->loc = loc; + return 1; + default: + break; + } + return 1; +} + +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + if (access == FE_OWN_WRITE) { + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot write while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + return 1; + } + if (!fe_own_require_value(diags, state, loc)) return 0; + if (access == FE_OWN_READ || access == FE_OWN_BORROW_SHARED) { + if (state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot create shared borrow while mutable borrow is live", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + return 1; + } + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_MOVE ? + "cannot move while value is borrowed" : + "cannot create mutable borrow while another borrow is live", + state->borrow_loc, "existing borrow originated here"); + return 0; + } + return 1; +} + +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) { if (!state) return 0; if (access == FE_OWN_PROJECTION) return 1; @@ -225,9 +411,38 @@ void fe_own_release_exclusive(FeOwnState *state) if (!state->shared) state->borrow_loc = fe_own_no_loc(); } +/* Merging two paths through the code: a borrow that is live on either side is + live after, because the checker cannot know which side ran. */ +static void fe_own_merge_fields(FeOwnState *out, const FeOwnState *left, + const FeOwnState *right) +{ + unsigned i; + unsigned j; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) out->fields[i] = left->fields[i]; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *r = &right->fields[i]; + if (!r->name) continue; + for (j = 0; j < FE_OWN_FIELD_MAX; ++j) { + if (out->fields[j].name && + strcmp(out->fields[j].name, r->name) != 0) continue; + if (!out->fields[j].name) out->fields[j] = *r; + else { + if (r->shared > out->fields[j].shared) + out->fields[j].shared = r->shared; + if (r->exclusive && !out->fields[j].exclusive) { + out->fields[j].exclusive = 1; + out->fields[j].loc = r->loc; + } + } + break; + } + } +} + FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) { FeOwnState out; + fe_own_merge_fields(&out, &left, &right); out.move = left.move == right.move ? left.move : fe_own_merge_move(left.move, right.move); out.initialized = left.initialized && right.initialized; @@ -246,6 +461,17 @@ FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right) { if (!left || !right) return 0; + { + unsigned i; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *a = &left->fields[i]; + const FeOwnField *b = &right->fields[i]; + if (!a->name != !b->name) return 0; + if (a->name && strcmp(a->name, b->name) != 0) return 0; + if (a->shared != b->shared || a->exclusive != b->exclusive) + return 0; + } + } return left->move == right->move && left->initialized == right->initialized && left->shared == right->shared && diff --git a/fec/src/own.h b/fec/src/own.h index 927ff97..a591b6d 100644 --- a/fec/src/own.h +++ b/fec/src/own.h @@ -34,6 +34,22 @@ typedef struct FeOwnPlace { int projected; } FeOwnPlace; +/* How many distinct fields of one value can be borrowed at once. Past this + a borrow falls back to covering the whole value, which reports more than it + has to but never less. */ +#define FE_OWN_FIELD_MAX 4 + +/* A borrow of one field rather than of the whole value. `self.bytes` and + `self.used_bytes` are different places, so borrowing one has to leave the + other readable -- otherwise a method cannot write through one field while + reading another, which is most of what a method does. */ +typedef struct FeOwnField { + const char *name; + unsigned shared; + int exclusive; + FeLoc loc; +} FeOwnField; + typedef struct FeOwnState { int move; int initialized; @@ -42,6 +58,10 @@ typedef struct FeOwnState { int borrow_conflict; FeLoc move_loc; FeLoc borrow_loc; + /* Whole-value state is above; these cover one field each. A whole-value + borrow conflicts with every field, and a field borrow conflicts with + the whole value and with itself. */ + FeOwnField fields[FE_OWN_FIELD_MAX]; } FeOwnState; typedef struct FeOwnProvenance { @@ -72,9 +92,15 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place); void fe_own_state_init(FeOwnState *state, int initialized); int fe_own_access(FeDiags *diags, FeOwnState *state, FeOwnAccessKind access, FeLoc loc); +/* The same, but reaching only one field of the value. A null `field` is the + whole value and behaves exactly as `fe_own_access`. */ +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc); int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc); void fe_own_release_shared(FeOwnState *state); void fe_own_release_exclusive(FeOwnState *state); +void fe_own_release_shared_field(FeOwnState *state, const char *field); +void fe_own_release_exclusive_field(FeOwnState *state, const char *field); FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right); int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right); int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, diff --git a/fec/std/map.fe b/fec/std/map.fe index 3b50ec7..d598bb3 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -92,33 +92,22 @@ pub struct Map(V) { return; } - /// Make sure `need` bytes fit, moving to a bigger buffer if they do not. - /// - /// Kept apart from `keep` because the borrow that swaps the buffer in is - /// a borrow of the whole of `self` -- borrowing is tracked at the root -- - /// and it has to be over before any field is read again. - fn ensure_room(self: &mut Self, need: usize) -> !void { - let have: usize = self.bytes.^.n; - if need <= have { return; } - var room: usize = have; - while room < need { room = room * 2; } - let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); - var k: usize = 0; - let filled: usize = self.used_bytes; - while k < filled { - bigger.^[k] = self.bytes.^[k]; - k = k + 1; - } - let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); - mem.destroy(old); - return; - } - - /// Copy `key` into the byte buffer and point the slot at it. + /// Copy `key` into the byte buffer and point the slot at it, moving to a + /// bigger buffer first if it does not fit. fn keep(self: &mut Self, key: []u8, slot: usize) -> !void { let at: usize = self.used_bytes; - let need: usize = at + key.n; - try self.ensure_room(need); + if at + key.n > self.bytes.^.n { + var room: usize = self.bytes.^.n; + while room < at + key.n { room = room * 2; } + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); + var k: usize = 0; + while k < at { + bigger.^[k] = self.bytes.^[k]; + k = k + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + } var i: usize = 0; while i < key.n { self.bytes.^[at + i] = key[i]; diff --git a/fec/tests/exec/fieldbrw.fe b/fec/tests/exec/fieldbrw.fe new file mode 100644 index 0000000..c512a74 --- /dev/null +++ b/fec/tests/exec/fieldbrw.fe @@ -0,0 +1,81 @@ +// EXIT:0 +// OUTPUT:a 40 b 3 +// OUTPUT:used 5 room 8 +// OUTPUT:first 9 +// OUTPUT:balanced +unit fieldbrw; + +import std.io; +import std.sys; +import std.mem; + +// Borrowing is per field. `p.a` and `p.b` are different places, so lending one +// out has to leave the other readable -- otherwise a method cannot write +// through one field while reading another, which is most of what a method +// does. + +struct Pair { a: i32, b: i32, } + +fn scale(v: &mut i32, by: i32) -> void { + v.^ = v.^ * by; + return; +} + +struct Box { + bytes: ^[]mut u8, + used: usize, + room: usize, + + fn with_capacity(n: usize) -> !Self { + let room: ^[]mut u8 = try mem.alloc_slice(u8, n); + return Self{ bytes: room, used: 0, room: n }; + } + + /// Move to a bigger buffer, then go on reading the other fields. The + /// borrow that hands over the buffer covers `bytes` and nothing else. + fn grow(self: &mut Self, want: usize) -> !void { + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, want); + var i: usize = 0; + while i < self.used { + bigger.^[i] = self.bytes.^[i]; + i = i + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + self.room = want; + return; + } + + fn push(self: &mut Self, v: u8) -> !void { + if self.used == self.room { try self.grow(self.room * 2); } + self.bytes.^[self.used] = v; + self.used = self.used + 1; + return; + } +} + +fn run() -> !void { + var p: Pair = Pair{ a: 4, b: 2 }; + let left: &mut i32 = &mut p.a; + // Writing another field while `a` is lent out. + p.b = 3; + scale(left, 10); + @print("a {} b {}\n", p.a, p.b); + + var box: Box = try Box.with_capacity(4); + try box.push(9); + try box.push(8); + try box.push(7); + try box.push(6); + try box.push(5); + @print("used {} room {}\n", box.used, box.room); + @print("first {}\n", box.bytes.^[0]); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/own/badrall.fe b/fec/tests/own/badrall.fe new file mode 100644 index 0000000..630a374 --- /dev/null +++ b/fec/tests/own/badrall.fe @@ -0,0 +1,14 @@ +// ERROR:11:borrow +unit badrall; + +struct Pair { a: i32, b: i32, } + +fn take(q: Pair) -> i32 { return q.a; } + +fn bad() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + let n = take(p); + left.^ = 4; + return n; +} diff --git a/fec/tests/own/badrfld.fe b/fec/tests/own/badrfld.fe index 65cb8b0..90a16e3 100644 --- a/fec/tests/own/badrfld.fe +++ b/fec/tests/own/badrfld.fe @@ -6,6 +6,6 @@ struct Pair { a: i32, b: i32, } fn bad() -> void { var p = Pair{ a: 1, b: 2 }; let left = &mut p.a; - p.b = 3; + p.a = 3; left.^ = 4; } diff --git a/fec/tests/own/badrsame.fe b/fec/tests/own/badrsame.fe new file mode 100644 index 0000000..b2e6342 --- /dev/null +++ b/fec/tests/own/badrsame.fe @@ -0,0 +1,12 @@ +// ERROR:9:borrow +unit badrsame; + +struct Pair { a: i32, b: i32, } + +fn bad() -> void { + var p = Pair{ a: 1, b: 2 }; + let one = &mut p.a; + let two = &mut p.a; + one.^ = 1; + two.^ = 2; +} diff --git a/fec/tests/own/okrfld.fe b/fec/tests/own/okrfld.fe new file mode 100644 index 0000000..5d68c28 --- /dev/null +++ b/fec/tests/own/okrfld.fe @@ -0,0 +1,14 @@ +unit okrfld; + +// Borrowing one field leaves the others alone. `p.a` and `p.b` are different +// places, so a borrow of one says nothing about the other. + +struct Pair { a: i32, b: i32, } + +fn ok() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + p.b = 3; + left.^ = 4; + return p.a + p.b; +}