GOAL — 부트스트랩까지 남은 작업
외부 스펙 감사(v0.1.8)와, 그 항목들을 실제로 빌드해서 확인한 결과를 합친 실행
계획이다. 감사는 문서를 읽었고 여기 적힌 것은 컴파일러에 물어본 답이다.
검증 게이트: 매 항목마다 두 스위트 모두. P0-1 과 P1-1 은 run.py 만으로는
잡히지 않는다 -- 조용히 틀린 값을 내는 종류라 exec.py fixture 로 동작을
고정해야 한다.
순서 근거: P0 가 P3 를 막는다 (stdlib 표면 전체가 ?&T 위에 얹힌다).
P1 은 P0-1 을 뺀 나머지 전부의 기준이다. P2 는 독립이라 언제 해도 된다.
P0 — 조용히 틀린 것
| # |
항목 |
무엇 |
왜 지금 |
규모 |
| 0-1 |
?&T lowering |
옵셔널 참조를 니치로 표현한다 (참조는 null 이 될 수 없으므로 포인터 0 이 none). == null, .?, if let, orelse 전부 |
if let Some(r) 이 컴파일되고 쓰레기 값을 반환한다. == null 은 internal: cannot lower an aggregate as a value. SPEC §5 R8 이 명시적으로 허용하는 타입인데 백엔드가 없다 |
중 |
| 0-2 |
제네릭 인스턴스 리터럴 |
Handle(Node){ raw: 7 } 를 파싱한다 |
지금은 Self{...} 나 생성자 함수로만 인스턴스를 만들 수 있다. 타입 인자를 명시한 리터럴은 expected ';' 로 죽는다 |
소 |
P1 — 스펙의 빈칸
반나절짜리이고 이후 모든 결정의 기준이 된다.
| # |
항목 |
결정 |
규모 |
| 1-1 |
정수 리터럴 기본 타입 |
expected type 이 있으면 그 타입, 없으면 i32. 값이 대상 타입 범위를 벗어나면 컴파일 에러 (let b: u8 = 300;). null 이 이미 같은 방식이라 일관된다 |
중 |
| 1-2 |
phantom 타입 파라미터 |
§9 에 한 문장: "generic 타입 파라미터는 본문에서 사용되지 않아도 된다. Handle(Node) 와 Handle(Type) 은 서로 다른 nominal 인스턴스다." 구현은 이미 그렇게 동작한다 -- 미래의 구현자가 깨뜨리지 못하게 적어두는 것 |
소 |
| 1-3 |
오버플로 정의 |
§7.4 에 "--no-checks 에서 오버플로 결과는 랩어라운드로 정의된다". i386 add 의 실제 동작이다 |
소 |
| 1-4 |
잔챙이 일곱 |
아래 표 |
중 |
1-4 세부
| 항목 |
결정 |
정수·char match arm |
_ 필수 |
enum payload 에 ^T/drop 있는 타입 |
허용. drop 은 활성 배리언트만 |
for x in slice 순회 중 원본 |
순회 동안 원본 root 는 대여 상태 |
defer 안에서 return |
컴파일 에러 |
undefined 배열 |
슬라이스로 넘겨 채우는 것만 허용 |
| 외부 유닛에서 private 필드가 있는 struct 리터럴 |
컴파일 에러. 생성자 함수 강제 |
by-value self 에서 부분 이동 |
mem.replace 필요. 예외는 자기 drop 안뿐 (§5 R7) |
P2 — SPEC 에서 죽은 백엔드 제거
C 백엔드는 없다. 파이프라인은 .fe → i386 asm → wasm → wlink → .exe 다.
그런데 SPEC.md 에 그 흔적이 열 군데 남아 있어서, 감사가 문서를 충실히 읽고
존재하지 않는 문제(C 의 부호 있는 오버플로 UB)를 보고했다.
| # |
항목 |
무엇 |
규모 |
| 2-1 |
C 백엔드 잔재 |
.fei, --emit-c, fe_errors.h, "호스트 C 방출", "C 방출 시 static inline" 열 군데 |
소 |
| 2-2 |
오류 코드 절 재작성 |
.fei 기반 증분 빌드 서술을 실제대로 -- 드라이버가 빌드 전체에서 모아 철자 순으로 1부터 |
소 |
P3 — stdlib
| # |
항목 |
표면 |
규모 |
| 3-1 |
Handle(T) |
8바이트 고정. index 32 / slot_gen 16 / epoch 8 / arena_id 8. --no-checks 는 비교만 생략하고 레이아웃은 그대로 둔다. 슬롯 gen 이 넘치면 그 슬롯은 영구 폐기 -- 랩어라운드로 stale 핸들이 되살아나는 것을 막는다 |
소 |
| 3-2 |
Arena(T) |
짧은 대여만: alloc, get_copy, set, take, swap, free, reset, len, drop. reset 은 슬롯별 gen 이 아니라 epoch 를 올린다 |
중 |
| 3-3 |
List(T) 보강 |
pop, take, swap, slice, slice_mut |
소 |
| 3-4 |
StringInterner |
StrId{ raw: u32 }. intern, eq, eq_ids, hash, len, write. str 을 꺼내는 API 는 두지 않는다 -- 꺼내면 그 문자열이 사는 동안 interner 전체가 잠긴다 |
중 |
| 3-5 |
std.map 판정 |
이미 []u8 키라 별도 IntMap 이 필요 없다. StrId 를 4바이트 키로 쓰는지 확인만 |
소 |
P4 — 측정
| # |
항목 |
무엇 |
규모 |
| 4-1 |
--report-unsafe |
유닛별 unsafe 블록 수, *T 출현 수, *_unchecked 호출 수. 목표는 std.mem/std.sys 밖 0 개. CI 에서 회귀 검사 -- 늘어나면 실패 |
소 |
| 4-2 |
--report-instances |
제네릭 인스턴스 수와 추정 크기 |
소 |
채택하지 않는 것
| 감사 항목 |
판정 |
근거 |
참조 튜플 -> (&mut T, &mut T) |
보류 |
struct 는 필드 단위 대여로 이미 풀렸다 (swap2(&mut p.a, &mut p.b) 동작 확인). 컨테이너의 두 원소만 남는데 렉서·파서를 쓰면서 필요했던 자리가 0 번이다 |
Arena.get_mut -> ?&mut T |
거부 |
감사 자신의 "참조를 오래 들고 있지 마라" 와 모순이다. 한 arena 안에서는 여전히 전체가 잠긴다 |
fmt.fmt_strid |
거부 |
@print 는 컴파일 단계에서 타입으로 fmt_* 를 고르는데, StrId 를 찍으려면 interner 인스턴스 가 필요하고 R10 이 가변 전역 대여를 금지한다. @print("{}", interner.text(id)) 로 간다 |
별도 IntMap(V) |
불필요 |
std.map 이 []u8 키라 이미 포괄한다 |
| C 오버플로 방출 규칙 |
대체 |
C 백엔드가 없다. 결론(랩어라운드 정의)만 1-3 으로 흡수 |
| R4 완화 |
영구 제외 |
이걸 풀면 언어의 존재 이유가 없어진다 |
이미 끝난 것
| 감사 항목 |
상태 |
| depth-1 field-sensitive 대여 |
f7e6676. std.map 의 keep 이 다시 함수 하나가 됐다 |
| Copy AST 노드 |
ast.Node 는 정수와 핸들뿐이라 자연히 Copy |
같은 struct 의 두 &mut |
필드 단위 대여로 풀림 |
| phantom 파라미터 동작 |
구현은 이미 지원. 문장만 P1-2 |
| Ferro 렉서·파서를 Ferro 로 |
fec/tests/exec/lexer/ |
| R4, R10, R9, 블록 표현식 배제, 오류 번호표, Copy handle enum |
손대지 않는다 |