evocation - Call forth the blue-green flame of computation from the universe, weave its energies into a fabric, that we may share our blood with it
evocation — 청록빛 계산의 불꽃을 불러내 에너지의 직물로 엮는 언어
Evocation은 Forth에서 파생한 64비트 amd64용 언어이자 자기 호스팅 컴파일러입니다. 주석이 달린 hex dump로 바이너리 생성 과정을 감사할 수 있는 자기 부트스트래핑 구조를 갖추고 있으며, 현재 컴파일에는 최대 10분이 걸립니다.
- 주제
AI 요약
Evocation은 Irenes가 만든 Forth dialect입니다. 언어 설계뿐 아니라 parse theory, type theory, database 같은 주제를 실험하는 기반을 목표로 합니다. 실용적인 용도도 있지만, 제작자는 이 언어가 존재해야 한다고 느꼈고 가장 즐거운 방식으로 만들었다는 점을 주된 동기로 설명합니다. differentiable neural network의 도움 없이 전부 작성했으며 앞으로도 그 방식을 유지한다고 밝힙니다. 현재 지원하는 아키텍처는 64비트 amd64 하나입니다.
자기 호스팅과 자기 부트스트래핑
Evocation은 self-hosting compiler입니다. 일단 실행 가능한 Evocation을 확보하면 자기 자신으로 새 컴파일러를 빌드합니다. 다만 처음부터 바이너리를 배포하지 않으므로 최초 실행본을 별도로 준비해야 합니다. 저장소에 들어 있는 evoke.hex를 작은 프로그램 hex에 넣으면 evoke 바이너리가 생성됩니다.
``sh
./hex < evoke.hex > evoke
chmod 755 evoke
``
hex는 유일한 바이너리 구성 요소이며 480바이트로 컴파일됩니다. 저장소에는 이 바이너리와 함께 hex.hex도 들어 있습니다. hex.hex는 hex의 모든 바이트가 어떤 목적과 출처를 갖는지 설명하는 주석付き hex dump입니다. 따라서 raw binary를 사람이 직접 감사할 수 있는 크기로 유지하고, 주석이 달린 표현을 다시 실행 파일로 변환하는 구조를 root of trust로 삼습니다.
제작자가 말하는 self-bootstrapping은 컴파일러가 일반 바이너리 대신 자신의 생성 과정을 설명하는 주석付き hex dump를 출력하는 데서 출발합니다. 출력에는 각 바이트의 provenance와 검증 방법이 포함됩니다. mescc와 Guix 개발자에게서 얻은 통찰, 즉 소스 코드와 바이너리의 차이는 주석이라는 관점을 기반으로 합니다. 제작자는 고급 언어로 구현된 컴파일러가 자기 자신의 hex dump를 생성하는 첫 사례라고 설명합니다. 이 방식은 Ken Thompson의 “Reflections on Trusting Trust”가 다룬 부트스트래핑 문제와도 연결됩니다.
언어와 실행 모델
대화형 모드에서는 다음처럼 문자열을 출력하고 산술을 실행합니다.
``forth
.\" Hi, Irenes!\"
6 7 * . newline
bye
``
Forth답게 모든 토큰을 공백으로 구분합니다. 문자열 리터럴은 s\" ...\" 형식이며, s와 따옴표 사이 및 따옴표 뒤에 공백이 필요합니다. .와 따옴표를 사용하는 출력 문법도 같은 규칙을 따릅니다. s\" 같은 단어가 뒤따르는 문자열을 직접 읽으므로 lexer 자체는 매우 짧습니다. 일반 언어처럼 구두점이 단어를 끊도록 만들지 않았고, 장차 더 복잡한 문법과 parsing formalism을 상위 계층에서 구현할 계획입니다. 그 formalism은 아직 존재하지 않습니다.
고수준 제어 흐름으로 if, unless, if-else, forever, while을 제공합니다. 문법은 postfix 연산과 중괄호 블록을 조합합니다.
``forth
: count 10 0 { 2dup < } { space dup . 1+ } while 2drop newline ;
count
``
이 기능은 컴파일된 코드에서만 동작합니다. Evocation은 범용 memory management를 제공하지 않고 log를 사용합니다. 할당은 쉽지만 해제는 어렵기 때문에, 반복에 필요한 코드 블록을 메모리에 배치하는 동작을 대화형 환경에서 일반화하지 않았습니다.
소스 구조와 transformation facility
최상위 컴파일 진입점은 evoke.e입니다. 다른 파일을 어떤 순서로 불러오고 처리하는지 짧게 나열합니다. execution.e와 core.e는 실행 기반을 구성하고, amd64.e는 assembly instruction을 구현합니다. labels.e는 주소에 이름을 붙이는 label을 처리합니다. Linux 시스템 호출과 실행 파일 출력은 각각 linux.e, elf.e에 들어 있습니다. 입력과 출력은 input.e, output.e, 단어 정의는 dynamic.e, 문법은 interpret.e, 제어 흐름은 flow-control.e가 담당합니다.
가장 독특한 부분은 transform.e의 transformation facility입니다. Forth 코드를 standalone executable로 컴파일하는 과정에서 변환을 적용합니다. evoke.e의 label-transform 호출과 execution.e의 log-load-transform 호출이 컴파일을 이 시설에 넘기는 지점입니다. 저장소에는 더 작은 예제로 자기 소스를 출력하는 quine.e, Evocation assembly로 작성한 hello.e와 hex.e도 포함됩니다.
컴파일러를 수정할 때는 기존 evoke로 새 버전을 만들어 evoke2를 실행합니다. 이어서 새 바이너리로 다시 컴파일해 evoke3를 만들고, evoke2와 evoke3가 bytewise identical인지 확인해야 합니다. 같은 소스에서 생성한 결과가 매번 달라지지 않는 성질을 계속 유지하려는 절차입니다.
Hex transform과 현재 성능
hex transform은 일반적으로 실행 파일을 출력하는 전체 컴파일 과정을 주석付き hex dump 생성 과정으로 바꿉니다. 컴파일 과정의 주석과 call-stack 정보를 결과에 전달해 바이너리 내부와 각 바이트의 목적을 설명합니다. hex는 Evocation 문법 안의 주석을 처리하고 ASCII hexadecimal을 raw binary로 바꿉니다.
같은 방식을 Evocation 자체에도 적용할 수 있습니다. 소스와 메타데이터를 준비한 뒤 hex-transform을 실행하면 evoke.hex가 생성됩니다. 이후 ./hex < evoke.hex를 실행하면 원래 evoke와 byte-for-byte 동일한 바이너리가 나옵니다. 현재 이 작업은 완료까지 최대 10분이 걸립니다. 메타데이터 출력 버퍼가 여러 연산에서 선형 순회를 요구하는 점이 가장 큰 비용입니다. 이 부분을 고치면 여러 dictionary에서 linked list를 사용하는 비용이 다음 개선 대상으로 남습니다. 전체 실행 시간의 대부분은 log-load transform이 차지하며, 해당 변환과 관계없는 작업에서는 이 호출을 임시로 drop 두 번으로 바꾸면 더 빨리 끝나지만 정상적인 컴파일러는 생성되지 않습니다.
Lobsters 반응
- @Irene — 안녕하세요. 네, 제가 저자입니다. 이 글을 올릴지 말지 확신하지 못했는데, 여기 계신 분들이 접근 방식이나 언어에 관해 의견을 주신다면 기쁘게 듣겠습니다.
- @doug-moen — 죄송하지만 게시물 제목에 조금 정신이 팔렸습니다. 언어를 만든 영적·미학적 동기를 표현하려고 시적으로 쓴 제목이라고 이해했습니다. 그런데 “우리가 그것과 피를 나눌 수 있도록”이라는 말은 본인에게 어떤 의미인가요?
원문: code.irenes.space / 번역·요약: Trawling