빌드 전체가 한 모듈이라 파일 이름도 하나였다. std.list 안에서 터진 경계 검사가
프로그램의 파일 이름을 대고 있었으니, 줄 번호는 맞는데 파일이 틀려서 엉뚱한 줄을
가리켰다 -- 이름을 안 대는 것보다 나쁘다.
모듈이 파일 표를 들고 트랩은 그 인덱스를 든다. 생성기는 파일마다 FE_FILE_n 을
한 번씩 찍는다.
before: index out of bounds at main.fe:2
after: index out of bounds at pick.fe:6
그리고 Parser.on 이 구조체 리터럴 안에서 다시 try 를 쓴다. 앞서 그것이 깨졌던
것은 try 때문이 아니라 Parser 가 1 바이트로 자리잡았기 때문이었다.
224/224, 31/31.
구조체도 필드를 n 이라 부를 수 있다. lowering 은 이름만 보고 슬라이스 길이
자리(포인터 다음 4바이트)를 읽어서, 그 자리에 있던 그럴듯한 숫자를 돌려주고
있었다. 체커는 제대로 필드로 풀고 있었으니 같은 함수 안에서 쓰기와 읽기가
어긋났다.
Box{ room: ^[]mut u8, n: usize, m: usize }
n 777 m 999 (전에는 n 12 -- room 의 길이)
배열/슬라이스/str 일 때만 길이로 읽는다. fieldn.fe 가 이것과, 옆의 슬라이스가
여전히 길이로 답하는 것을 함께 고정한다.
222/222, 30/30.
렉서 다음은 파서다. 노드는 한 배열에 살고 자식은 그 안의 인덱스다 -- 노드는
^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.
렉서가 알려준 것도 함께: R11 의 모양 -- 아레나가 소유하고 인덱스가 가리킨다 --
은 쓸 수 있다. 토큰이 from/len 을 들고 소스가 옆에서 같이 다니는 것은 장황하지만
막히지 않는다. 모든 함수가 src 를 하나 더 받는 것이 그 값이다.
파일 크기 규칙을 AGENTS 에 넣었다.
셀프호스팅에 손대기 전의 강제 함수다. 아픈 자리를 전부 건드린다: R4 아래의
토큰 구조체, 태그드 유니온, 진단 출력, 유닛 경계.
토큰은 자기가 나온 글자를 담지 않는다. R4 가 대여를 집합 저장소에서 막으므로,
어디서 시작해 얼마나 긴지를 적고 소스는 옆에서 같이 다닌다. 위치도 &mut usize
로 옆에서 다닌다 -- 슬라이스와 함께 구조체에 들어갈 수 없기 때문이다. 이것이
R11 이 말하는 모양이고, 쓸 수 있다.
first keyword unit @1 / number 42 @3 / text "hi" @3 / arrow -> @5
keyword 6 name 7 number 1 text 1 punct 15 / total 30
길에서 고친 것:
- binding.Type.Variant 가 안 풀렸다. 유닛 경계 이름 조회가 심볼만 보고 타입을
보지 않았다.
- 문자열 const 전역이 빈 슬라이스로 나갔다. 포인터는 링커만 아는 수라서 바이트에
구멍을 두고 링커가 채우게 한다.
- exec.py 가 OUTPUT 마커를 여러 개 적어도 마지막 하나만 검사했다. 고치자마자
readfile 의 낡은 기대가 드러났다.
run.py 217/217, exec.py 27/27.
임시값마다 스택 슬롯을 주던 것을 바꿨다. ebx, esi, edi 세 개를 나눠준다 --
eax/ecx/edx 는 이 방출기가 계산하는 자리이고, 남는 셋은 호출을 저장 없이
넘긴다.
IR 은 '임시값은 블록을 넘지 않는다'로 설계했지만 lowering 이 실제로는 넘기는
것을 만든다 -- 경계 검사가 인덱스를 계산한 자리와 쓰는 자리 사이에서 블록을
쪼갠다. 그래서 자격은 함수 전체를 보고 정하고, 블록 안의 선형 스캔은 살아남은
것만 다룬다.
그리고 목적지 레지스터에 직접 계산한다. 상수, 적재, 주소, 그리고 전폭 산술과
비교가 스크래치 레지스터를 거치지 않는다. 좁은 연산은 여전히 eax 를 거친다 --
esi 와 edi 에는 바이트 반쪽이 없다.
버그 둘:
- 프롤로그에 ebx 를 넣는 편집이 copy 의 push 까지 같이 바꿔서 pop 없는 push 가
생겼다. 스택이 어긋나 17개가 죽었다.
- load_place_base 가 load_temp 을 거치지 않고 슬롯을 직접 읽었다. 레지스터에
있는 포인터를 쓰지 않은 메모리에서 읽었다.
calc 4437 -> 3796 줄 (-14%)
wordfreq 6023 -> 5348 줄
214/214, 26/26.
SPEC R11 은 재귀·그래프 모양 데이터를 아레나가 값을 소유하고 정수 핸들이
가리키는 것으로 답한다. 그 답은 아레나가 실제로 있어야 쓸 수 있다.
핸들은 오프셋이라 아레나가 사는 동안 유효하고, 두 핸들을 비교하는 것은 두 수를
비교하는 것이다. 아레나는 몰래 자라지 않는다 -- 움직인 핸들은 더 이상 아무것도
가리키지 않기 때문이다.
길에서 고친 것 셋:
- Self 가 제네릭 인스턴스에서만 타입으로 묶여 있어서, 평범한 구조체의 Self{..}
가 안 풀렸다. 이제 모든 메서드에서 묶는다.
- binding.Type.method() 가 식 자리에서 해석되지 않았다.
- 다른 유닛의 비제네릭 구조체 메서드가 lowering 되지 않고 extern 으로만 나갔다.
파일을 나눌 때 그 가지가 빠졌다.
handles 0 4 8 / value 65 / full / reset 0 / balanced
exec.py 26/26.
lowering 의 변수·정리 목록·오류 이름 세 배열은 넘치면 오류가 아니라 넘친 것을
버리고 틀린 코드를 냈다. 한계에 닿는 방식 중 최악이다. 이제 자란다.
코드 생성기는 지역이 512개를 넘으면 함수를 아예 방출하지 않고 지나갔다. 이제
지역 수만큼 자리를 잡는다.
유닛 64 -> 256, 제네릭 인스턴스 512 -> 4096. 둘 다 원래 보고는 했지만 장난감을
기준으로 고른 숫자였다.
209 -> 213 fixture, exec 25/25.
AST 도 IR 도 본질이 태그드 유니온이다. 태그 비교만 되고 안을 꺼내지 못하면
검사 없이 필드를 읽어야 하고, 그러면 안전성 이야기가 통째로 무너진다.
무언가를 담는 변이는 메모리다: 태그가 앞, 페이로드가 뒤. 태그를 읽는 것은
어느 쪽이든 같은 질문이고 자리만 다르다. 페이로드는 가리키지 않고 복사한다 --
매치한 것의 소유권을 arm 이 가져가는 것이 보통이고, 그게 허용되는지는 검사기가
이미 판단했다.
레코드 변이의 필드 오프셋을 아무도 기록하지 않고 있었다. 레이아웃이 크기는
재면서 자리는 버렸다.
Enum.Variant{...} 와 Enum.Variant(x) 와 페이로드 없는 Enum.Variant 를 모두
만든다. 마지막 것은 페이로드를 가진 enum 안에서는 태그만 든 값이다.
some 7 / none / point 3 4 / empty
exec.py 25/25.
런타임에 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.
SPEC 6.3.1: 포매팅 빌트인은 가변 인자 함수가 아니다. 호출 하나가 리터럴
조각마다 쓰기 하나, 값마다 쓰기 하나로 펴진다. 언어에 가변 인자 호출 규약이
생기지 않고, 포맷 문자열은 실행 시점에 이미 사라져 있다.
verb: {} 십진, {x} 16진, {c} 한 바이트, 문자열은 포인터와 길이, bool 은
분기 두 개와 리터럴 두 개.
코드 생성기가 호출됐지만 정의되지 않은 이름을 스스로 extern 선언한다. lowering
이 런타임 호출을 직접 내므로, 손으로 관리해야 하는 목록 대신 호출 자체에서
이름을 모은다.
@print("n={} neg={} hex={x}\n", 42, 0-7, 255) -> n=42 neg=-7 hex=ff
check.c 3,937 줄, lower.c 1,913 줄이었다. 가장 큰 파일이 978 줄이 됐다.
check.c 679 스코프·심볼·흐름·소유권 접착
checkexp.c 739 포매팅 검사와 표현식
checkstm.c 635 문장, 함수, 메서드
checkgen.c 618 제네릭 실체화
checkcal.c 978 유닛 경계 호출과 옵셔널/에러 유니온
checkpro.c 184 선언 패스와 프로그램
lower.c 567 타입·슬롯·지역·블록·mem.*
lowerprn.c 238 포매팅 빌트인 전개
lowerexp.c 468 표현식
lowerstm.c 543 문장·함수·프로그램
줄 범위로 잘랐다. 주제별로 묶는 것보다 정확한데, 한 줄도 잃거나 겹치지
않기 때문이다. 파일 순서가 이미 단계를 따라가서 경계가 실제 이음매에 떨어진다.
모든 정의가 static 을 잃고 비공개 헤더에 프로토타입을 갖는다. 대안 --
static 을 유지하고 #include 로 텍스트만 나누는 것 -- 은 결합을 보여주는 대신
숨긴다.
두 스위트 그대로: 209/209, 21/21.
파이프라인이 끝에서 끝까지 도는 상태다. 검증이 두 스위트로 나뉜다 -- 컴파일러가
프로그램에 대해 뭐라고 하는가, 그리고 컴파일된 프로그램이 실제로 무엇을 하는가.
전자만 보면 진단은 옳은데 코드가 안 나오는 상태를 놓친다.
세션 중에 완화한 이동 규칙 두 곳을 TODO 맨 위에 사람의 판단을 기다리는
항목으로 적었다. 규칙을 건드리기 전에 프로그램 쪽을 먼저 고쳐보라는 것도
작업 흐름에 넣었다.
&mut T 를 &mut T 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는
동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면
배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해서 &mut 가 사실상 쓸 수
없었다 -- 재귀 하강 파서를 쓰다가 걸렸다.
페이로드 없는 enum 은 이름 붙은 수라서 수로 읽을 수 있다. 반대 방향은 안 된다:
임의의 수는 변이가 아니다.
calc 프로그램: 재귀 하강 수식 계산기. 우선순위, 괄호, 오류 전파.
1+2*3 = 7 (1+2)*3 = 9 2*(3+4)-5 = 9 10/3 = 3
1+ = error (1+2 = error
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 프로그램이 그 둘이 일치함을
실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부.
할당하는 내장 함수들이다. 평범한 호출이 아니라서 여기서 편다. create 는 값을
받아 그 복사본을 가리키는 소유 포인터를 주고, 할당이 실패할 수 있으므로 결과가
에러 유니온이다. 실패 코드는 OutOfMemory 이고, 소스 어디에도 그 이름이 적혀
있지 않지만 다른 이름과 같은 표에 들어간다.
갓 할당한 저장소는 통째로 소유하므로 쓸 수 있다 -- 방해할 사람이 없다. 그래서
alloc_slice 는 ^[]mut T 를 준다. 소유 슬라이스는 포인터와 길이가 값 자체라서
.^ 로 통과할 것이 없고, destroy 는 그 안의 포인터를 푼다.
heap 프로그램이 할당·try·defer 해제·for 순회를 한꺼번에 돈다: sum 4950
모노모피제이션이 실제로 코드를 만드는 자리가 여기다. 프론트엔드는 어떤
인스턴스가 존재하는지만 정했다. 검사기가 인스턴스마다 선언·바인딩·유닛·링크
이름을 기록하고, lowering 이 그 바인딩을 다시 걸고 같은 본문을 자기 이름으로
내린다. 제네릭 선언 자체는 코드가 없다.
comptime 인자는 값이 아니므로 호출에서 넘기지 않고 파라미터 자리도 잡지
않는다. 메서드 호출은 도달한 대상을 첫 인자로 넘긴다 -- self: Self 든
self: &Self 든 수신자의 주소로 같다. 구조체 메서드가 아예 lowering 되지
않고 있었다.
exec.py 13/13.
x[a..b] 는 인덱스가 아니라 포인터와 길이를 만든다. 양쪽 끝을 -- 서로에 대해,
그리고 있는 것에 대해 -- 검사한 뒤에 포인터를 만든다. 유효한 범위의 빈
슬라이스는 괜찮고 끝보다 늦게 시작하는 것은 아니다.
파서가 x[a] 와 x[a..] 를 구분하지 못했다. 둘 다 b 만 있고 c 가 없어서
모양이 같았다. '..' 가 있었으면 노드에 표시한다.
정수 출력이 된다: fizz 12345 -678
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 을 내린다.
링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는
괄호가 있는데 어셈블러가 받지 않는다.
전역은 정적 저장소다. 초기값이 컴파일타임 상수면 이미지에 박고 아니면 0이다.
문자열 리터럴은 바이트를 이미지에 두고 포인터와 길이를 값으로 만든다. 같은
글자는 같은 저장소를 쓴다 -- 읽기 전용이라 공유가 공짜다.
defers 프로그램이 defer 순서를 실행으로 고정한다. 등록 역순이고, 이른 return
과 끝까지 간 경로 양쪽 다 돈다.
옵셔널은 태그와 페이로드, 에러 유니온은 오류 코드와 페이로드다. 코드 0 이
'오류 없음'이다. 페이로드 위치 규칙을 types.c 로 옮겨서 레이아웃 패스와 코드
생성기가 같은 것을 본다.
try 는 분기다. 실패면 지금 함수의 에러 유니온을 그 코드로 만들어 나간다 --
그 전에 defer 를 돌린다. catch 와 orelse 는 오른쪽을 필요할 때만 평가하므로
역시 분기다.
for 는 세 형태를 공유한다: 세는 것, 원소를 도는 것, 위치까지 받는 것. 개수는
본문 전에 한 번 읽는다. 원소 바인딩은 참조다 -- 그래서 루프가 원본에 쓸 수
있다.
error.Name 은 빌드 전체에서 이름을 모아 철자 순으로 1부터 번호를 준다
(SPEC 4.6). 빌드 순서가 결과를 바꾸지 않는다.
run.py 197/197, exec.py 9/9.
인덱스는 부호 없는 비교와 트랩으로 펴진다. --no-checks 는 메시지가 아니라
비교와 분기 자체를 없앤다 -- 그게 그 플래그의 전부다.
버그 셋:
- 값으로 넘긴 구조체 파라미터는 주소로 도착하는데 lowering 이 그걸 몰라서
포인터를 구조체로 읽었다. 변수마다 by_address 를 기록한다.
- 덩어리 반환의 숨은 결과 인자를 지역이 아니라 임시값으로 다뤘다.
- store 에 폭이 없어서 1바이트 bool 을 4바이트로 썼다. 옆 지역변수가
뭉개졌고 logic 프로그램이 틀린 답을 냈다.
tests/exec.py 가 새 스위트다. run.py 는 컴파일러가 프로그램에 대해 뭐라고
하는지 보고, 이쪽은 프로그램이 실제로 무엇을 하는지 본다. 보고만 되고
방출되지 않는 경계검사는 저기서는 통과하고 여기서는 실패한다.
run.py 194/194, exec.py 6/6.
fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다.
레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를
고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은
코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 --
임시값이 어디 사는지만 바뀐다.
런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과
줄을 stderr 에 쓰고 3으로 끝낸다.
처음으로 Ferro 프로그램이 실행됐다:
1..10 합 -> 55
(7*6-2)/4 -> 10
루프+호출+분기 -> 1
tests/build.py 가 컴파일하고 링크하고 돌린다.
함수, 파라미터, 지역, 리터럴, 이름, 산술과 비교, 대입, if, while,
break/continue, 호출, 참조, 필드 접근, 포인터 역참조까지.
Slot 이 표현식의 결과다 -- 임시값에 든 값이거나 메모리 안의 자리다. 덩어리는
언제나 자리다. 임시값은 레지스터이고 덩어리는 거기 안 들어가기 때문이다.
and/or 는 연산이 아니라 제어 흐름으로 내린다. 오른쪽을 평가하지 않아야 하는
경우가 있어서다.
블록에 terminated 플래그를 뒀다. 없으면 분기 안의 return 이 join 으로 가는
점프에 덮인다.
--dump-ir 로 볼 수 있다. try/catch/defer/drop/for/배열/옵셔널/에러유니온과
제네릭 인스턴스는 아직이다.
명령 12개와 종결자 4개. 함수 단위 기본 블록이고 임시값은 블록을 넘지 않아
phi 노드가 없다 -- 블록을 넘겨야 하는 값은 지역을 경유한다. 코드가 조금 더
생기지만 레지스터 할당기를 블록 단위로 유지해 준다.
Ferro 타입은 여기서 사라진다. 구조체·슬라이스·옵셔널·에러 유니온이 전부
mem<N> 이고 필드는 lowering 이 계산한 바이트 오프셋이다. 모노모피제이션이
프론트엔드에서 끝나므로 IR 에 제네릭이라는 개념도 없다.
덩어리는 언제나 주소로 오간다. 크기 임계값이 없어서 ISA 마다 다른 구조체 전달
규칙을 통째로 피해간다. trap 은 이유와 줄 번호만 남기고 파일 이름 문자열은
유닛당 하나를 공유한다.
IR.md 가 설명이고 ir.h/ir.c 가 그 형태다. 아직 아무도 만들지 않는다.
세그먼트 주소 지정은 x86 리얼모드에만 있는 개념이고, 평평한 주소 공간을 가진
다른 32비트 프로세서에는 대응물이 없다. 영구 제외다.
usize/isize 는 타깃의 포인터 폭이며 특정 비트 수를 약속하지 않는다. 오늘
32비트지만 u32 와 자동 변환되지 않는다. bits16 에서 배울 것은 '32로 정하자'가
아니라 '폭이 언어 의미론으로 새어나가지 않게 하자'이고, 그것이 나중에 다른
폭의 타깃을 여는 유일한 장치다.
타깃이 하나이므로 레이아웃에서 pointer_bits 분기가 전부 사라졌다. 인터럽트
핸들러와 공유 상태는 v0.2 로 내렸다 -- 문법은 아직 파싱되지만 명세의 약속은
아니다.
타입 인자를 바인딩한 상태로 선언을 인스턴스마다 한 번씩 검사한다. 바인딩된
이름은 그냥 그 인자 타입이므로 본문, 필드 타입, 시그니처가 모두 같은 규칙으로
풀린다. 인스턴스 정체성은 선언 유닛 + 선언 + 인자 철자다.
찾은 버그 셋:
- 파서가 comptime 파라미터의 이름을 'comptime' 이라는 키워드에서 가져갔다.
타입 파라미터 이름이 전부 comptime 이 되어 아무것도 바인딩되지 않았다.
- 인스턴스 이름이 중첩마다 길어져서, 깊은 사슬에서 잘린 이름끼리 충돌해
재귀가 깊이 제한에 닿기 전에 조용히 멈췄다. 길어지면 인자를 일련번호로
적어 정체성을 유지한다.
- 순서 비교 연산자가 피연산자 타입을 보지 않아 구조체끼리 비교해도 통과했다.
제네릭과 무관한 기존 구멍이다.
fixture 셋이 명세와 어긋나 있어 명세를 따랐다. badbody 와 badop 은 호출 지점을
primary error 로 기대했지만 SPEC 9 는 본문의 연산이 primary 이고 호출에는
'instantiated here' note 를 붙이라고 한다. okscope 는 제네릭 본문이 호출자의
이름을 본다고 기대했지만 SPEC 9 는 정의 유닛에서 해석한다 -- badscope 로 옮기고
이유를 적었다.
188/188.
import 가 만든 binding 으로 다른 유닛의 선언에 닿는다. 호출, 구조체 리터럴,
값 참조 세 자리다. 시그니처의 타입은 그 시그니처가 쓰인 유닛에서 해석한다 --
호출한 쪽에서 해석하면 같은 이름이 다른 타입을 가리킨다.
pub 없는 선언과 필드는 자기 유닛 밖에서 보이지 않는다.
error.Name 은 구현된 적이 없었다. 기본 에러 집합 core.Error 의 멤버이고, 그
집합은 선언이 아니라 수집으로 채워지므로 변이 목록 없이 정체성만 갖는다.
units 34/34. dotpriv/main 은 위반이 있는 유닛을 import 하므로 받아들여질 수
없다 -- 마커를 붙이고 이유를 적었다.
유닛마다 FeCheck 를 새로 만들면 타입 문맥도 유닛마다 따로 생겨서 유닛 경계를
넘는 이름을 볼 수가 없었다. 검사기가 빌드 전체를 맡고, 모든 유닛의 선언을
등록한 뒤에 어느 본문이든 보기 시작한다.
스코프와 심볼과 타입은 현재 유닛보다 오래 살아야 하므로 AST 아레나가 아니라
검사기 자신의 아레나에서 잡는다. 이름이 같아도 유닛이 다르면 다른 타입이므로
nominal 타입은 선언한 유닛으로도 구분한다.
pub 은 파서가 버리고 있었다. 이제 FE_NODE_PUB 으로 남긴다.
보고서가 '애매'로 남긴 하나다. io.buf_writer 는 351e5db 가 Writer 핸들 enum
으로 교체하면서 없앤 콜백 방식 writer이고, 이 fixture 는 그것이 돌아오지 않게
막으려고 그 커밋에서 추가됐다. 파일만 봐서는 알 수 없어 주석으로 남긴다.
pin 94 -> 95. 마커 없는 fixture는 이제 없다.
마커가 없으면 러너는 '거부되기만 하면 통과'로 판정한다. 엉뚱한 이유로
거부돼도 초록이었다. fixture 별로 무엇을 검사하는지 읽고 지금 나오는 진단이
그 규칙을 짚는지 확인한 뒤 줄과 문구를 고정했다. 근거는 fixture-report.md에
있다.
pin 58 -> 94. 통과 수는 150/188 그대로다.
format 검사는 인자가 떨어진 자리에서 개수 불일치를 말하고 문자열을 다 훑은
뒤 같은 말을 또 했다. aggregate storage 검사는 M7 쪽이 optional 뒤의 참조를
보려고 도는 김에 평범한 &T 필드까지 잡아서, 뒤이어 도는 M6 검사와 겹쳤다.
fixture 전수 검사 결과 --check 경로에 중복 진단이 남아 있지 않다.
이동한 값을 쓰면 진단이 두 번 나왔다. 원인이 둘이다. 식별자를 읽으면
FE_OWN_READ 가 이미 보고하는데 mark_moved 가 FE_OWN_MOVE 로 같은 자리를 다시
보고했고, member lvalue 는 check_lvalue 가 base 를 검사한 뒤 check_lvalue_core
가 또 검사했다. M6/M7 두 검사기를 합칠 때 남은 자국이다.
러너는 진단의 첫 줄만 마커와 대조하므로 fixture 188개가 이것을 잡지 못했다.
철학 2의 근거가 사실과 달랐다. 전역 분석 금지만으로는 메모리가 줄지 않고,
현재 프론트엔드는 이미 그 예산을 한 자릿수 넘겼다 — FeBuild 하나가 26,892바이트
스택 지역 변수이고, 유닛 64개의 소스와 AST를 동시에 들고 있다.
전역 분석 금지는 국소적 진단과 작은 컴파일러라는 자체 근거로 유지한다.
컴파일러가 도는 곳을 §2.1로 분리했다. bits16 타깃은 그대로다 -- 8086용
프로그램을 만드는 것과 8086에서 컴파일러를 돌리는 것은 다른 일이다.
밑줄만 제거하고 충돌한 이름에 숫자를 붙인 결과라 이름이 무엇을 검사하는지
오히려 덜 드러낸다. own/badfld 와 types/badfld 가 서로 다른 것을 검사하는데
둘 다 badfmem 이 된 것이 그 증거다. 파일을 열어 판단하는 작업이므로
마커 판정과 함께 다시 한다.