From 63baa4f5232435c9e79f774563aef02d745a9770 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:22:11 +0900 Subject: [PATCH] =?UTF-8?q?GOAL=20P3-3:=20List=20=EC=97=90=20pop=20take=20?= =?UTF-8?q?swap=20slice=20slice=5Fmut=20clear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴파일러가 실제로 쓰는 나머지 표면이다. take 는 R7 대로 대체값을 남기고 꺼내므로 리스트에 구멍이 생기지 않는다. slice() 를 쓰려면 언어가 한 걸음 필요했다. &Self 로 읽어도 필드가 ^[]mut T 이니 슬라이스가 []mut T 로 나오는데, 선언한 반환은 []T 다. 호출 인자 자리의 약화만 있고 반환 자리에는 없었다. 반환 위치의 약화를 허용했다. R8 이 이미 그 파생을 허용한 뒤라면 []mut T 를 []T 로 넘기는 것은 가진 것보다 적게 넘기는 일이라 새 별칭을 만들지 않는다. &Self 메서드가 자기가 소유한 것의 읽기 전용 뷰를 내주는 길이 이것뿐이다. SPEC §4.2 에 적었고, let 은 여전히 안 된다는 것을 badletwk 가 고정한다. 243/243, 37/37. --- SPEC.md | 2 +- fec/src/check.c | 17 ++++++++++++ fec/src/checkcal.c | 3 +- fec/src/checkpri.h | 1 + fec/src/checkstm.c | 1 + fec/std/list.fe | 36 ++++++++++++++++++++++++ fec/tests/exec/listmor.fe | 55 +++++++++++++++++++++++++++++++++++++ fec/tests/types/badletwk.fe | 8 ++++++ fec/tests/types/okretwk.fe | 12 ++++++++ 9 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 fec/tests/exec/listmor.fe create mode 100644 fec/tests/types/badletwk.fe create mode 100644 fec/tests/types/okretwk.fe diff --git a/SPEC.md b/SPEC.md index 60baaa6..c6a1a9a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -109,7 +109,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. -- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. **반환 위치에서도 약화할 수 있다** — `[]mut T`를 `[]T`로, `&mut T`를 `&T`로 반환하는 것은 R8이 이미 그 파생을 허용한 뒤에 가진 것보다 적게 넘기는 일이므로 새 별칭을 만들지 않는다. `&Self` 메서드가 자기가 소유한 것의 읽기 전용 뷰를 내주는 길이 이것뿐이다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - **배타 대여를 호출에 넘기는 것은 이동이 아니라 그 호출 동안의 재대여다.** `&mut T`를 `&mut T` 파라미터에, `[]mut T`를 `[]mut T` 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해져서 `&mut`가 사실상 쓸 수 없게 된다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. - `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 표현은 없다. diff --git a/fec/src/check.c b/fec/src/check.c index 77bf9b7..4240a4a 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -120,6 +120,23 @@ int call_reborrows(const FeType *param, const FeType *arg) return 0; } +/* Handing back less than you hold. R8 says a returned reference has to be + derived from a parameter or a static; given that, returning the shared form + of an exclusive one is safe -- the caller cannot do anything with `[]T` that + it could not do with `[]mut T`. Without this a method on `&Self` cannot hand + out a read-only view of what it owns. */ +int return_weakens(const FeType *want, const FeType *got) +{ + if (!want || !got) return 0; + if (want->kind==FE_TYPE_SLICE && got->kind==FE_TYPE_SLICE && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + if (want->kind==FE_TYPE_REF && got->kind==FE_TYPE_REF && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + return 0; +} + int explicit_castable(FeType *a, FeType *b) { if (!a || !b) return 0; diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index d1f5c47..b27b096 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -1016,7 +1016,8 @@ void check_stmt(FeCheckerState *s, FeNode *n) expected->error_value && expected->error_value->kind==FE_TYPE_VOID) { } else if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->a)) + !m7_actual_compatible(expected,stored,n->a) && + !return_weakens(expected,stored)) err(s->c,n->loc,"return type mismatch"); if (n->a) mark_moved(s,n->a,actual); break; diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index 531045c..9e59b2e 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -95,6 +95,7 @@ int in_own_drop(FeCheckerState *s, FeNode *n); void mark_moved(FeCheckerState *s, FeNode *n, FeType *t); int compatible(FeType *want, FeType *got, FeNode *value); int call_reborrows(const FeType *param, const FeType *arg); +int return_weakens(const FeType *want, const FeType *got); int explicit_castable(FeType *a, FeType *b); FeType *node_type(FeCheck *c, FeNode *n); char *unit_cname(FeCheck *c, const char *name); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 89eac2f..ae79b49 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -482,6 +482,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) mark_moved(s,n->a,b); if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) err(c, n->loc, "void expression returned from value function"); + else if (return_weakens(s->ret,b)) { } else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && b->kind != FE_TYPE_UNKNOWN && !compatible(s->ret,b,n->a)) diff --git a/fec/std/list.fe b/fec/std/list.fe index cdf7c36..d9bb717 100644 --- a/fec/std/list.fe +++ b/fec/std/list.fe @@ -32,6 +32,42 @@ pub struct List(T) { return; } + /// Take the last one off. Nothing to take is `null`, not a trap. + pub fn pop(self: &mut Self) -> ?T { + if self.len == 0 { return null; } + self.len = self.len - 1; + return self.items.^[self.len]; + } + + /// Move one out and leave `replacement` where it was (SPEC 5 R7). This is + /// how a `T` that owns something leaves the list without the list ending + /// up with a hole in it. + pub fn take(self: &mut Self, i: usize, replacement: T) -> T { + return mem.replace(&mut self.items.^[i], replacement); + } + + /// Exchange two elements. + pub fn swap(self: &mut Self, i: usize, j: usize) -> void { + if i == j { return; } + let first: T = self.items.^[i]; + let second: T = mem.replace(&mut self.items.^[j], first); + self.items.^[i] = second; + return; + } + + /// The elements as a slice, so `for x in xs.slice()` walks them. R8(a): + /// derived from `self`, so the borrow belongs to the caller. + pub fn slice(self: &Self) -> []T { + return self.items.^[0..self.len]; + } + + pub fn slice_mut(self: &mut Self) -> []mut T { + return self.items.^[0..self.len]; + } + + /// Forget the elements and keep the buffer. + pub fn clear(self: &mut Self) -> void { self.len = 0; return; } + /// Move to a buffer twice the size. Kept apart from `push` because the /// borrow that hands over the old buffer must not be live while the old /// buffer is still being read (SPEC 5 R6). diff --git a/fec/tests/exec/listmor.fe b/fec/tests/exec/listmor.fe new file mode 100644 index 0000000..dda291e --- /dev/null +++ b/fec/tests/exec/listmor.fe @@ -0,0 +1,55 @@ +// EXIT:0 +// OUTPUT:walk 6 count 3 +// OUTPUT:swapped 3 1 +// OUTPUT:took 2 left 9 +// OUTPUT:pop 1 then 9 empty -1 +// OUTPUT:cleared 0 room 4 +// OUTPUT:balanced +unit listmor; + +import std.io; +import std.sys; +import std.list; + +// The rest of the List surface a compiler needs: walk it, exchange two, move +// one out and leave something valid behind, take the last off, and empty it +// without going back to the allocator. + +fn run() -> !void { + var xs: list.List(i32) = try list.List(i32).with_capacity(4); + try xs.push(1); + try xs.push(2); + try xs.push(3); + + // `slice()` is a shared view derived from `self` (SPEC 5 R8(a)), which is + // what lets `for` walk it. + var sum: i32 = 0; + for x in xs.slice() { sum = sum + x.^; } + @print("walk {} count {}\n", sum, xs.count()); + + xs.swap(0, 2); + @print("swapped {} {}\n", xs.at(0), xs.at(2)); + + // SPEC 5 R7: what leaves a projection leaves a replacement behind. + let old: i32 = xs.take(1, 9); + @print("took {} left {}\n", old, xs.at(1)); + + let a: i32 = xs.pop() orelse -1; + let b: i32 = xs.pop() orelse -1; + let c: i32 = xs.pop() orelse -1; + let d: i32 = xs.pop() orelse -1; + @print("pop {} then {} empty {}\n", a, b, d); + + try xs.push(7); + let room: usize = 4; + xs.clear(); + @print("cleared {} room {}\n", xs.count(), room); + 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/types/badletwk.fe b/fec/tests/types/badletwk.fe new file mode 100644 index 0000000..c1c6d5c --- /dev/null +++ b/fec/tests/types/badletwk.fe @@ -0,0 +1,8 @@ +// ERROR:6:cannot rebind a mut borrow +unit badletwk; + +// 반환은 약화해도 `let` 은 여전히 안 된다 (SPEC §4.2). +fn bad(m: &mut i32) -> i32 { + let s: &i32 = m; + return s.^; +} diff --git a/fec/tests/types/okretwk.fe b/fec/tests/types/okretwk.fe new file mode 100644 index 0000000..f904f06 --- /dev/null +++ b/fec/tests/types/okretwk.fe @@ -0,0 +1,12 @@ +unit okretwk; + +// R8 이 파생을 허용한 뒤라면 가진 것보다 적게 넘기는 것은 안전하다. + +struct Buf { + items: ^[]mut i32, + + fn all(self: &Self) -> []i32 { return self.items.^[0..2]; } + fn all_mut(self: &mut Self) -> []mut i32 { return self.items.^[0..2]; } +} + +fn one(p: &mut i32) -> &i32 { return p; }