From dacf1e1b1babf79c0a3f404be14064a225d7f93a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:56:48 +0900 Subject: [PATCH] =?UTF-8?q?GOAL=20P0-1:=20=EB=8B=88=EC=B9=98=20=EC=98=B5?= =?UTF-8?q?=EC=85=94=EB=84=90=EC=9D=B4=20=ED=8F=AC=EC=9D=B8=ED=84=B0=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=ED=8F=AC=EC=9D=B8=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=EB=93=A0=20=EC=9E=90=EB=A6=AC=EB=A5=BC=20=EB=84=98=EA=B2=BC?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?T 의 페이로드가 null 이 될 수 없으면 태그를 따로 두지 않고 그 불가능한 값을 null 로 쓴다. ?^T 와 ?&T 가 그렇다. 그런데 if let 이 그것을 풀 때 두 경우를 한 갈래로 처리하고 있었다. 바인딩이 참조인 이유가 둘이다. 페이로드가 값이면 바인딩은 그것이 래퍼 안에 앉은 자리를 가리켜야 하고(주소), 페이로드가 이미 포인터면 바인딩은 그 포인터여야 한다(값). 후자에 주소를 쓰면 포인터의 포인터가 되고, 프로그램은 값이 있어야 할 자리에서 주소를 읽는다. 컴파일도 되고 실행도 됐다. ?i32 5 (맞았음 -- 태그가 있어서 다른 길로 갔다) ?^i32 6125480 → 5 ?&i32 6125496 → 5 그리고 옵셔널을 null 과 비교하는 것이 lowering 되지 않았다 -- 래퍼 전체를 값으로 읽으려 해서 'cannot lower an aggregate as a value' 였다. 태그만 보면 되는 질문이다. optional/oknull.fe 가 검사만 하는 fixture 라 드러나지 않았다. exec/optref.fe 가 세 모양을 전부 고정한다: if let, orelse, .?, == null, 그리고 R7 관용구인 mem.replace(&mut box, null).? 로 소유자를 꺼내 놓는 것까지. 229/229, 33/33. --- fec/src/lowerexp.c | 22 ++++++++++ fec/src/lowerstm.c | 18 ++++++-- fec/tests/exec/optref.fe | 93 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 fec/tests/exec/optref.fe diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index 2b97662..e457fa7 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -78,6 +78,28 @@ Slot lower_expr_core(Lower *L, FeNode *n) if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) return lower_logical(L, n, !strcmp(n->text, "and")); op = binary_op(n->text, &is_cmp); + /* Comparing an optional with `null` asks about its tag, not about the + bytes of the whole wrapper -- which has no value form at all. */ + if ((op == FE_IR_EQ || op == FE_IR_NE) && n->a && n->b) { + FeNode *w = fe_m7_is_null(n->b) ? n->a : + (fe_m7_is_null(n->a) ? n->b : 0); + FeType *wt = w ? w->sem_type : 0; + if (wt && wt->kind == FE_TYPE_OPTIONAL) { + Slot s = lower_expr(L, w); + unsigned t0; + unsigned z; + if (!s.is_place) { + fail(L, "an optional with no place", w); + return slot_void(); + } + t0 = wrapper_tag(L, s, wt, w); + z = fe_ir_const(L->m, L->b, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, 0); + return slot_value(fe_ir_binary(L->m, L->b, op, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, t0, z, 1), + FE_IR_I8); + } + } operand = ir_type(n->a ? n->a->sem_type : 0); if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; a = as_value(L, lower_expr(L, n->a), n->a); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index cfc5f5d..33665e3 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -344,10 +344,20 @@ void lower_if_let(Lower *L, FeNode *n) FeType *bt = binding->sem_type; Slot payload = wrapper_payload(L, value, opt); unsigned local = declare_var(L, binding->cname, bt, binding->text); - if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), - as_address(L, payload, n), FE_IR_PTR); - else + if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) { + /* The binding is a reference either way, but for two different + reasons. When the payload is itself a single pointer (`^T`, + `&T`) the binding *is* that pointer, so it has to be read out. + When the payload is a value the binding points at where it sits + inside the wrapper, so the address is what is wanted. Taking the + address in the first case gives a pointer to the pointer, and + the program reads an address where it expects a value. */ + FeType *pl = opt ? (opt->kind == FE_TYPE_ERROR_UNION + ? opt->error_value : opt->elem) : 0; + unsigned p = pl && ir_type(pl) == FE_IR_PTR + ? as_value(L, payload, n) : as_address(L, payload, n); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), p, FE_IR_PTR); + } else store_into(L, fe_ir_at_local(local, 0), payload, n, ir_size(bt)); } lower_stmt(L, n->b); diff --git a/fec/tests/exec/optref.fe b/fec/tests/exec/optref.fe new file mode 100644 index 0000000..50725ae --- /dev/null +++ b/fec/tests/exec/optref.fe @@ -0,0 +1,93 @@ +// EXIT:0 +// OUTPUT:int 5 none 0 +// OUTPUT:own 5 absent -1 +// OUTPUT:ref 5 absent yes +// OUTPUT:cmp yes no +// OUTPUT:unwrap 5 +// OUTPUT:balanced +unit optref; + +import std.io; +import std.sys; +import std.mem; + +// An optional whose payload cannot be null keeps no separate tag: the payload's +// own impossible value is `null`. `?i32` carries a tag; `?^T` and `?&T` do not. +// +// Both shapes have to answer the same questions, and the pointer-shaped ones +// were answering with the address of the slot the pointer sits in rather than +// with the pointer -- one level of indirection too many, in code that compiled +// and ran. + +fn opt_int(on: bool) -> ?i32 { + if on { return 5; } + return null; +} + +/// A niche optional over an owner. The `if let` binding is `&i32` -- the +/// pointer itself, not where it is kept -- and `.?` moves the owner out so it +/// can be let go of. +fn take(o: ?^i32) -> i32 { + var box: ?^i32 = o; + var v: i32 = -1; + if let Some(p) = box { v = p.^; } + if box == null { return v; } + // SPEC 5 R7: a non-Copy value leaves a projection through `mem.replace`, + // which puts something valid back where it was. + let owner: ^i32 = mem.replace(&mut box, null).?; + mem.destroy(owner); + return v; +} + +fn make(v: i32) -> ?^i32 { + let p: ^i32 = mem.create(v) catch |e| { return null; }; + return p; +} + +struct Bag { + items: ^[]mut i32, + + /// SPEC 5 R8(a): derived from `self`, so the borrow is the caller's. + fn at(self: &Self, i: usize) -> ?&i32 { + if i >= self.items.^.n { return null; } + return &self.items.^[i]; + } +} + +fn run() -> !void { + // A tag in front of the payload. + var got: i32 = 0; + if let Some(a) = opt_int(true) { got = a; } + let none: i32 = opt_int(false) orelse 0; + @print("int {} none {}\n", got, none); + + // A niche over `^i32`. + @print("own {} absent {}\n", take(make(5)), take(null)); + + // A niche over `&i32`. + let room: ^[]mut i32 = try mem.alloc_slice(i32, 2); + room.^[0] = 5; + let b: Bag = Bag{ items: room }; + var seen: i32 = 0; + if let Some(r) = b.at(0) { seen = r.^; } + var past: bool = false; + if let Some(r) = b.at(9) { seen = seen; } else { past = true; } + @print("ref {} absent {}\n", seen, yesno(past)); + + // Comparing against null reads the tag, not the bytes of the wrapper. + @print("cmp {} {}\n", yesno(b.at(9) == null), yesno(b.at(0) == null)); + @print("unwrap {}\n", b.at(0).?.^); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +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; +}