GOAL P0-1: 니치 옵셔널이 포인터 대신 포인터가 든 자리를 넘겼다

?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.
This commit is contained in:
2026-08-17 15:56:48 +09:00
parent 51f555a830
commit dacf1e1b1b
3 changed files with 129 additions and 4 deletions
+93
View File
@@ -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;
}