float 를 달려 본 적 없는 backend

raytracer —— 구 세 개, 거울 바운스, 딱딱한 그림자, 벡터 연산은 모두 float 의 tuple —— 는 인터프리터와 C backend 에서 동일하게 렌더링되고, WebAssembly 의 어셈블러에서 죽었다. local.set 은 i32 를 기대했는데 f64 가 왔다. 원인은 기능의 결여보다 기묘했다. Wasm 의 float 는 box 되고, boxing 에는 raw f64 임시 변수가 필요하며, 그 임시 변수를 올바른 형으로 선언하는 기계는 존재했고 동작했다 —— 그러나 읽고 있던 것은 top-level 의 emitter 뿐. 함수의 emitter 셋은 모든 local 을 무조건 i32 로 선언하고 있었다. top-level 의 float 식은 늘 동작했다. 이름 있는 함수 안의 float 임시 변수는 한 번도 어셈블러를 통과한 적이 없었다. 테스트 스위트는 emit 된 텍스트를 assert 하고 어셈블하는 것은 정수 프로그램뿐이어서, 이 구멍은 프로그램이 신경 쓸 때까지 보이지 않았다.

merewasmfloatscodegendogfood

probe 목록의 마지막 미측정 축은 WebAssembly backend 의 float 였다. C backend 는 float probe 를 이미 마쳤다 —— Mandelbrot, 발견된 것은 문서의 구멍이었고 churn 은 없었다 —— 그러나 Wasm 은 float 를 다른 형태로 표현한다. box 되는 것이다. heap 에 확보한 f64 로의 i32 포인터로, 연산마다 unbox 하고 끝나면 re-box 한다. backend 의 “모든 것은 i32” 라는 균일한 값 모델을 지키기 위해. raytracer 는 자연스러운 forcing program 이다. float 연산이 거의 전부, 픽셀 하나에 수천 번의 연산, 그 하나하나가 box. 거대한 지면 구 위에 구 세 개, 평행 광원, Lambert 와 경면 셰이딩, 딱딱한 그림자, 거울 바운스 한 번 —— 그리고 모든 backend 가 같은 checksum 을 print 하고 같은 이미지 파일을 써야 한다는 설계상의 제약. 어디선가 어긋나면 숨을 곳이 없다.

두 backend 에서 동일, 셋째에서 죽다

인터프리터는 렌더링했다. C backend 도 렌더링했다. 바이트 단위로 같았고 이미지는 올바르게 보였다 —— 거울 구에 비치는 반사, 구 아래 고이는 그림자. Wasm 빌드는 한 명령도 달리기 전에 실패했다. 어셈블러가 local.set expected [i32] but got [f64] 로 모듈을 거부한 것이다. 실행시의 오답이 아니다 —— 형으로서 불법인 모듈. 컴파일러는 WebAssembly 가 “그것은 WebAssembly 가 아니다” 라고 말하는 WebAssembly 를 emit 하고 있었다.

하나의 emitter 만 읽고 있던 기계

실패하던 함수는 픽셀 양자화기였고, 실패하던 명령은 리터럴 255.999 의 boxing 이었다. float 의 boxing 은 알로케이터가 8 바이트를 잘라내는 동안 raw f64 를 임시 local 에 쥐고 있는 것을 뜻한다 —— 그리고 Wasm 의 local 은 형이 있으므로 그 임시 변수는 f64 로 선언되어야 한다. 기묘한 것은 여기다. 이를 위한 기계는 존재했고 올발랐다. mint 된 모든 임시 변수의 형을 추적하는 목록이 있었고, 거기 기록하는 f64 용 헬퍼가 있었으며, top-level 프로그램 emitter 는 local 을 선언할 때 충실히 그 목록을 읽고 있었다. 그러나 함수의 emitter 셋 —— top-level 함수, 들어 올려진 내부 함수, closure adapter —— 은 한 번도 상담하지 않았다. 각자가 여분의 local 을 하드코드된 i32 로, 카운터가 말하는 개수만큼, 형을 무시하고 선언하고 있었다.

즉 분업이 버그를 완벽하게 숨기고 있었다. 프로그램 top-level 의 float 식은 main-body emitter 에 떨어져 동작한다. 같은 식이 이름 있는 함수 안에 있으면 함수 emitter 에 떨어져 불법 모듈을 낳는다. 테스트 스위트의 float 테스트는 emit 된 텍스트를 assert 하고 있었다 —— 출력에 f64.add 가 있는가 —— 어셈블은 하지 않고. 그리고 어셈블해서 달리는 테스트는 셀프호스트 계열로, 그것은 컴파일러이고, 끝에서 끝까지 정수와 문자열이다. 모든 float 테스트는 통과했고 float 함수는 한 번도 어셈블러를 통과한 적이 없었다. 이 backend 는, 정확한 의미에서, 실제 코드의 형태를 한 float 프로그램을 한 번도 달려 본 적이 없었다.

세 개의 emitter, 하나의 목록

수정은 함수 emitter 셋에게 main emitter 가 예전부터 하던 일을 시키는 것뿐이었다. 형 목록을 save 하고, 함수마다 reset 하고, 여분의 local 을 기록된 형으로 선언한다 —— closure adapter 에는 한 가지 비틀림이 있어, 그 앞머리 몇 개의 local 은 capture 의 전개로 늘 i32, 추적된 임시 변수 앞에 놓는다. 정수만의 프로그램은 i32 임시 변수만 mint 하므로 출력은 이전과 바이트 단위로 동일하다 —— 이것이 중요한 것은 셀프호스트의 fixpoint 가 emit 된 WAT 를 문자열로 비교하고, 셀프호스트된 컴파일러가 정수 프로그램이기 때문이다. fixpoint 는 미동도 하지 않았다. 선언이 고쳐지자 raytracer 는 어셈블 되고, 달리고, 인터프리터나 C 와 같은 checksum 을 print 하고, 바이트까지 같은 이미지 파일을 썼다. 세 개의 backend, 한 장의 그림. 그것을 렌더링하는 데 무엇이 지불되었는가 —— 그 측정은 독립된 한 편에 값한다.

← Back to Mere: 언어를 만들다