R11 이 데이터가 데이터를 가리킬 때 내놓는 답이다. 아레나가 값을 소유하고
핸들은 숫자라서 참조가 아니고, R4 가 반대할 것이 없다. mem.Arena 옆에 두되
다른 자료구조다 -- 저쪽은 크기가 제각각인 것을 오프셋으로 나눠주고, 이쪽은
슬롯마다 고정된 T 를 담고 슬롯을 되받는다.
핸들은 믿는 것이 아니라 검사받는다. 슬롯의 세대, 아레나의 epoch, 어느
아레나에서 왔는지를 8바이트에 담아서, 놓아준 뒤에 쓰거나 reset 뒤에 쓰거나
다른 아레나에 물어보면 옆값이 아니라 null 이 나온다. 여덟 바이트는 빌드마다
같다 -- --no-checks 는 비교를 건너뛸 뿐 배치를 바꾸지 않는다.
세대가 다 닳은 슬롯은 재사용 목록에 넣지 않고 버린다. 한 바퀴 돌면 옛 핸들이
다시 유효해지는데, 그것이 세대가 막으려던 바로 그 일이다.
get 은 사본을 돌려준다. 대여가 문장을 넘기지 않는 이유가 그것이고, 그래서
한 슬롯을 읽으면서 다른 슬롯에 쓸 수 있다. get_mut 은 두지 않았다 -- 아레나
하나를 통째로 잠그는 참조를 오래 들고 있게 하는 API 다.
길에서 고친 것: 메서드 호출이 시그니처를 호출자 유닛에서 풀고 있었다. 그래서
Arena(T) 안의 Handle(T) 를 부르는 쪽 유닛에서 찾다가 실패했다. 필드 타입에
쓰던 enter_declaring_unit 을 method_type 에도 물렸다.
241/241, 36/36.
ast.Node 가 이름이 무엇으로 해석됐는지 들고, Map 이 저장소를 유지한 채 키만
잊는다. 스코프가 끝날 때 표를 다음 스코프에 넘기는 것이 리졸버가 원하는
모양이다 -- 함수마다가 아니라 중첩 단계마다 표 하나.
bind 는 Name 노드의 남는 a 필드를 재활용할 수도 있었지만 명시적인 쪽을 골랐다.
노드가 32 에서 36 바이트가 되는 값으로 그 자리가 무엇인지 이름이 말한다.
clear 는 아무도 부르지 않는 채로 들어와 있었다. maps.fe 가 이제 부른다: 키가
사라지고, 방은 64 로 남고, 그 위에 다시 채워도 버퍼를 새로 잡지 않는다.
cleared 0 room 64 gone 0 / refilled 3 / balanced
GOAL.md 를 더했다. 외부 감사와, 그 항목들을 실제로 빌드해서 확인한 결과를
합친 P0~P4 다.
228/228, 32/32.
p.a 와 p.b 는 서로 다른 자리인데 한쪽을 대여하면 다른 쪽까지 잠겼다. 메서드가
하는 일의 대부분이 한 필드에 쓰면서 다른 필드를 읽는 것이라, std.map 의 keep 은
그것 때문에 함수 둘로 갈라져 있었고 파서도 같은 자리에서 걸렸다.
FeOwnState 가 필드별 칸을 넷 갖는다. 값으로 복사되는 구조체라 흐름 병합과
스냅샷은 손댈 것이 없었다. 전체를 대여하면 모든 필드와 충돌하고, 필드를
대여하면 전체 및 같은 필드와 충돌한다. 칸이 모자라면 전체 대여로 되돌아가
더 많이 보고할 뿐 놓치지 않는다.
읽기는 식별자에서 일어나는데 그 자리에서는 자기가 무엇의 밑동인지 알 수 없다.
그래서 투영이 내려가는 길에 어느 필드인지 적어두고 식별자가 그것을 집는다.
인덱스는 갈라지지 않는다 -- xs[i] 의 i 는 상수가 아닐 수 있고, 필드 이름은
상수다.
길에서 나온 것: mem.replace 가 목적지 대여를 가져가고 돌려주지 않았다. 일반
호출 인자는 문장 끝에 돌려주는데 intrinsic 경로에만 그것이 없었다. 전에는
그 자리가 어차피 거부돼서 드러나지 않았다.
var p = Pair{ a: 1, b: 2 };
let r = &mut p.a;
p.b = 3; // ok -- 전에는 에러
p.a = 3; // 에러
take(p); // 에러
SPEC §5 R6 을 고쳤고, 옛 규칙을 그대로 적어둔 문단과 예제를 갈아치웠다.
own/badrfld 는 이제 허용되는 코드였으므로 같은 필드를 건드리도록 다시 겨눴고
okrfld·badrall·badrsame·exec/fieldbrw 를 더했다.
228/228, 32/32.
렉서 다음은 파서다. 노드는 한 배열에 살고 자식은 그 안의 인덱스다 -- 노드는
^Node 를 들 수 없고(여럿이며 한 번씩 소유하지 않는다) &Node 도 들 수 없다(R4).
인덱스는 둘 다 아니다. 소스도 필드가 아니라 매 단계에 같이 다닌다.
unit demo / fn answer @2 / let n = (+ 1 (* 2 3)) / return n / balanced
전위 표기로 다시 찍는 것이 시험의 요점이다. 1 + 2 * 3 이 어떻게 묶였는지는
그렇게만 보인다.
쓰면서 나온 컴파일러 버그 넷:
1. 다른 유닛의 타입을 필드로 쓰면 그 필드 타입이 영영 UNKNOWN 이었다. 필드
해석이 유닛마다 선언 직후에 돌아서, 아직 선언되지 않은 유닛의 타입을 찾다
실패하고 그 답을 굳혔다. 이제 모든 유닛이 선언을 마친 뒤에 한 번 푼다.
2. 그리고 그 해석은 타입을 선언한 유닛에서 해야 한다. 필드 타입은 그 유닛의
import 로 쓰였는데 아무 유닛에서나 풀고 있었다. 타입 계층에 enter/leave
콜백을 두고 체커가 그 자리로 데려간다.
3. cycle_state 를 재귀 검사와 크기 계산이 같이 썼다. 첫 번째가 보는 중인 구조체
가 두 번째에게는 다 끝난 것으로 보여서, 필드가 하나뿐인 것처럼 1 바이트로
자리를 잡았다 -- Parser 가 그래서 자기 토큰을 밟았다. layout_state 로 나눴다.
4. 다른 유닛의 상수(ast.NONE)를 lowering 이 필드 접근으로 봤다. 체커가 이미
링크 이름을 붙여두었으니 그것이 있으면 전역이다.
그리고 R1 을 실제로 지키게 했다: 소유자를 놓으면 그것이 가진 것도 놓는다.
전에는 자기 drop 이 있거나 자기가 owned 일 때만이어서, drop 을 가진 타입을
필드로 담은 구조체는 그것을 놓을 방법이 없었다(drop 은 손으로 못 부른다).
이제 release_at 이 drop 을 부르고 필드로 내려간다. 그 덕에 List/Arena/Map 의
drop 이 전부 필요 없어져서 지웠다 -- 버퍼가 owned 이니 R1 이 알아서 한다.
221/221, 29/29.
컴파일러는 이름을 끊임없이 찾는데 리스트 선형 탐색은 그 모양이 아니다. 키는
맵이 소유하는 한 버퍼에 복사되고 슬롯은 그 안의 어디인지만 적는다 -- R11 이
말하는 아레나와 핸들 모양이고, 그래서 맵을 놓는 것이 엔트리마다 하나가 아니라
두 번의 해제다.
개방 주소법에 선형 탐사. 표는 2의 거듭제곱이라 나눗셈이 아니라 마스크이고,
탐사가 길어지는 것이 표가 차는 것보다 먼저라 3/4 에서 자란다.
길에서 고친 것 넷:
- 인스턴스를 만들 때 선언 유닛으로 전환하지 않아서, 필드 타입 Slot(V) 를
호출자 유닛에서 찾고 있었다.
- mem.alloc_slice 가 원소 타입으로 단순한 이름만 받았다. Slot(V) 같은
인스턴스도 받는다.
- 쓸 수 있는지를 바인딩이 아니라 소유된 것이 정한다. let p: ^[]mut T 는 p 를
고정하고 그것이 소유한 것은 쓸 수 있게 둔다. 슬라이스 인덱스도 마찬가지다.
- 메서드 인자에 자유 함수와 같은 호출 한정 약화가 없었다.
알게 된 것: 대여는 루트 단위라 self 의 한 필드에 쓰는 동안 다른 필드를 읽을 수
없다. 지역으로 빼거나 메서드를 나누면 되지만, 필드 단위 대여가 있으면 훨씬
편할 자리다.
count 5 / fn 2 let 3 missing 0 / grown 64 / after 40 / balanced
218/218, 28/28.
SPEC R11 은 재귀·그래프 모양 데이터를 아레나가 값을 소유하고 정수 핸들이
가리키는 것으로 답한다. 그 답은 아레나가 실제로 있어야 쓸 수 있다.
핸들은 오프셋이라 아레나가 사는 동안 유효하고, 두 핸들을 비교하는 것은 두 수를
비교하는 것이다. 아레나는 몰래 자라지 않는다 -- 움직인 핸들은 더 이상 아무것도
가리키지 않기 때문이다.
길에서 고친 것 셋:
- Self 가 제네릭 인스턴스에서만 타입으로 묶여 있어서, 평범한 구조체의 Self{..}
가 안 풀렸다. 이제 모든 메서드에서 묶는다.
- binding.Type.method() 가 식 자리에서 해석되지 않았다.
- 다른 유닛의 비제네릭 구조체 메서드가 lowering 되지 않고 extern 으로만 나갔다.
파일을 나눌 때 그 가지가 빠졌다.
handles 0 4 8 / value 65 / full / reset 0 / balanced
exec.py 26/26.
런타임에 open/read/close 와 명령줄을 넣었다. std.io 가 그 위에 파일 열기,
읽기, 쓰기, 그리고 명령줄을 조각으로 나누는 것을 얹는다. 인용부호 처리는
런타임이 알 일이 아니라 라이브러리가 할 일이다.
길에서 고친 것들:
- *T 가 타입 시스템에 실체가 없어서 덩어리로 취급됐다. 이제 진짜 종류다 --
주소일 뿐이고 추적할 대여도 실행할 drop 도 없는 Copy 타입. 그 결과 &u8 이
*u8 에 자동으로 맞지 않게 됐는데, 그게 맞다: R9 는 그 변환을 unsafe 안의
@ptr_cast 로만 허용한다.
- raw 포인터에 정수를 더하면 더 뒤의 주소다. 소유자나 대여에는 허용하지
않는다 -- 자기 자리가 있는 것에서 걸어나가는 것이 *T 의 용도다.
- @volatile_load / @volatile_store / @ptr_cast 를 내린다.
- undefined 가 선언된 타입을 따른다. 없으면 손으로 타이핑할 수 있는 것보다
큰 버퍼를 선언할 방법이 아예 없었다.
R8 이 정확히 동작하는 것도 확인했다: 참조성 파라미터가 둘인 함수는 슬라이스를
반환할 수 없다. 어디서 파생됐는지 시그니처가 말하지 않기 때문이다.
exec.py 24/24.
std.str 은 eq/starts_with/find/trim/parse_int 을 바이트 슬라이스 위에서 한다.
std.list 는 자라는 제네릭 시퀀스다 -- 버퍼를 소유하므로 리스트를 놓으면
원소도 놓인다. 성장은 두 배씩이라 push 당 복사량이 상수로 눌린다.
찾은 버그 넷:
- 메서드가 자기 타입의 유닛이 아니라 호출한 유닛에 속한 것으로 계산됐다.
다른 유닛의 제네릭 타입을 쓰면 필드가 전부 private 으로 보였다.
- 참조로 도달한 메서드를 찾지 못했다. self.grow() 가 안 됐다.
- 이미 참조인 수신자의 주소를 한 번 더 떠서 넘겼다. 포인터의 포인터를 받은
메서드가 그것을 구조체로 읽었다.
- 유닛으로 한정된 제네릭 타입(list.List(i32))이 타입 자리에서도 식 자리에서도
해석되지 않았다.
drop 을 가진 타입은 인스턴스마다 그 메서드가 존재해야 한다 -- 이름으로 부르는
사람이 없어도 스코프 정리가 부른다. 그리고 자기 drop 안에서는 필드를 꺼낼 수
있다. 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다.
run.py 207/207, exec.py 19/19.
defer 목록을 '스코프가 아직 갚아야 할 것' 목록으로 일반화했다. defer 블록과
소유 값 해제가 같은 목록에 쓰인 순서대로 들어가고, 모든 이탈 경로가 역순으로
갚는다.
해제에는 값 옆에 플래그를 둔다. 값이 저장될 때 세우고 넘겨줄 때 지운다. 값이
아직 여기 있는 경로에서만 해제되는데, 그건 코드의 모양만 봐서는 알 수 없는
것이다. 검사기가 소유권을 넘기는 사용을 이미 표시해두므로 그것을 읽는다.
!void 함수의 빈 return 은 성공이다. 줄 값도 없고 오류도 없다는 뜻인데
프론트엔드가 타입 불일치로 거부하고 있었다.
런타임이 할당/해제 횟수를 센다. owndrop 프로그램이 그 둘이 일치함을
실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부.
io.Writer 는 핸들 하나짜리 enum 이다. 참조도 컨텍스트 포인터도 담지 않으므로
Copy 이고 자유롭게 오간다 (SPEC 5 R8). fmt 는 sink 를 소유하지 않는다 --
호출자가 버퍼를 주고 앞에서 몇 바이트가 쓰였는지 돌려받는다.
lowering 에 추가: enum 변이 상수, match, 정수 캐스트, 문자열 이스케이프.
프론트엔드 정밀도 하나: 항상 빠져나가는 분기의 상태를 병합하지 않는다. 그
분기가 소비한 값이 그 분기를 지나지 않은 경로에서도 소비된 것처럼 보였다.
fmt_i32 가 이것 때문에 못 쓰였다.
extern "c" 이름은 유닛 접두사를 붙이지 않는다. 링커가 이미 아는 이름이라는
것이 그 선언의 요점이다.
run.py 199/199, exec.py 11/11.
std 는 예약된 이름이고 프로그램이 아니라 컴파일러와 함께 있으므로 자기 루트를
갖는다 (--std=). 본문 없는 선언은 링커가 찾을 것 -- 런타임이나 C 라이브러리 --
이므로 IR 에 extern 으로 나간다.
런타임에 write/alloc/free/exit 를 넣었다. 이것이 표준 라이브러리가 스스로
말할 수 없는 전부이고 나머지는 Ferro 로 쓴다.
@trap @unreachable @size_of @align_of @line 을 내린다.
링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는
괄호가 있는데 어셈블러가 받지 않는다.