하나의 int, 네 개의 기계

빠진 bitwise 연산자를 겨눈 probe가 그 한 단 아래에 있는 것을 먼저 밟았다: 언어의 int가 backend마다 다른 폭이었다. interpreter는 63-bit, C backend는 말없이 32, LLVM과 wasm은 각각 별개. SHA-256의 round 상수 —— 20억 초과가 36개 —— 는 C backend에서 truncate되어 진단 제로로 자신만만한 잘못된 digest를 뱉었다. 한편 테스트가 도는 interpreter에서는 올바로 계산되고 있었다. bitwise 연산자가 의미를 갖기 전에, 언어는 「정수의 폭은 얼마인가」를 정해야 했다.

mereintegersbackendscorrectnesslanguage-design

이 Part는 앞 Part의 「싼 probe」 방법의 하루를, 싸지 않게 될 때까지 돌린 것이다. 네 개의 작은 프로그램에서 여섯 릴리스가 나왔다. 그 전부를 꿰뚫는 한 가닥 실은, 버그 보고처럼 들리지만 실은 설계 선언인 한 문장이다: 언어에 backend가 넷 있을 때, 진실은 넷이다. 그것들이 같은 진실이 되는 것은, 누군가 그렇게 했을 때뿐이다. 첫 probe는, 그렇게 되어 있지 않던 곳을 찾았다.

표적을 빗나간 probe

계획은 소박했다 —— 언어로 SHA-256을 써서, 빠진 bitwise 연산자가 얼마나 아픈지를 잰다. 모든 AND와 XOR를 나눗셈과 나머지로 위장해야 했기 때문이다. 위장은 추했지만 interpreter에서는 동작했고, NIST test vector를 전부 통과했다. 그런데 같은 프로그램을 C backend로 compile하니, 완전히 잘못된 digest를 뱉었다. 조금 다른 게 아니다 —— 무관하다. 그리고 아무 말도 하지 않는다: 경고 없음, 크래시 없음, 진단 없음. bit 위장은 옳았다. 문제는 한 단 아래였다. SHA-256의 64개 round 상수 중 36개가 20억 초과이고, C backend의 정수는 날 C int —— 32-bit —— 였다. 그 상수들은 쓰인 순간 말없이 음수로 wrap했다. 최소 재현은 적나라하다: 2147483647 + 1은 interpreter에서 2147483648, native에서 -2147483648를 인쇄한다. docs는 C backend가 어느 폭을 쓰는지 한 번도 쓰지 않았다. 테스트는 전부 interpreter에서 돌았고, 거기서 산술은 63-bit로 문제없었다. 이 프로젝트가 출하해 온 모든 compile된 프로그램은, 테스트한 정수 의미론과 다른 것으로 돌고 있었다 —— 그리고 20억 너머에 닿는 프로그램만이 그것을 알아챈다.

폭을 정한다

수정 자체는 미묘하지 않지만, 그 둘레의 규율은 미묘하다. C backend의 정수는 64-bit가 되었다 —— long long, 리터럴에는 폭 확장 전에 32-bit로 wrap하는 것을 막는 suffix를 붙이고, 인쇄와 parse 경로도 맞춰 확장. interpreter는 native의 63 그대로. wasm과 LLVM은 32 그대로지만, 그것에 대해 거짓말하기를 그만두었다: 범위에 안 들어가는 정수 리터럴은, silent하게 truncate되어 garbage나 하류 도구의 불평으로 나타나는 대신, 위치 정보 붙은 compile 에러가 되었다. 그리고 한 경계는 의도적으로 통일하지 않았다 —— foreign-function 경계, Mere의 extern fn이 libc나 POSIX 함수를 지목하는 곳에서는, int는 C의 32-bit int 그대로. 그것이 그 함수들의 시그니처가 실제로 쓰는 폭이기 때문이다. 여기서 확장하면 레지스터의 미정의 비트를 읽는다. 그래서 언어는 이제 정직한 표를 가진다: 4 backend, 세 개의 폭, 각각에 문서화된 규칙 하나, 그리고 차이가 우연이 아니라 의미를 지는 곳 하나. 이 이야기의 요점은 「버그가 고쳐졌다」가 아니다. 「정수의 폭은 얼마인가」는 언어가 가능한 한 어디서나 같게, 불가능한 곳에서는 보이는 형태로 답해야 하는 물음이라는 것 —— 그리고 이 프로젝트가 답을 찾은 방법은, 진실이 갈라지는 바로 그 수치를 쓰는 명세서를 베껴 쓰는 것이었다. 다음 이야기에서 드디어 probe가 필요로 하던 bitwise 연산자를 세우고, 그것이 얼마의 값어치였는지를 잰다.

← Back to Mere: 언어를 만들다