TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 27 READS

포스와 리스프, 서로를 비추는 두 언어의 메타프로그래밍

더글러스 호이트의 저서 『Let Over Lambda』의 한 장은 리스프 매크로 기법을 총동원해 또 다른 언어인 포스(Forth)를 리스프 위에 구현하는 실험을 담고 있다. 목표는 실용적인 포스 처리계를 만드는 것이 아니라, 리스프 사용자에게 포스의 메타프로그래밍 개념을 가르치고 '매크로를 통한 구문의 이중성(duality of syntax)'이라는 주제를 설명하는 데 있다. 언뜻 무관해 보이는 두 언어를 나란히 놓고 보면, 각각이 왜 그런 낯선 모습을 하게 되었는지가 오히려 선명해진다.

조직 없이 자라난 언어

포스의 출발점은 다른 주류 언어와 결이 다르다. 리스프가 MIT와 이후 DARPA의 후원 아래 성장했고 포트란은 IBM, C는 AT&T의 유닉스에서 나온 것과 달리, 포스는 1968년경 척 무어(Chuck Moore)가 천문학과 하드웨어 설계 등 자신의 계산 필요를 해결하려고 독자적으로 만든 언어다. 강력한 정부·학계·기업 후원 없이 성공한 사실상 첫 언어이며, 이후 열정적인 풀뿌리 사용자 공동체를 통해 배포되고 개선되어 왔다. 이 태생은 포스의 성격을 규정한다. 포스는 스타일이 아니라 구현 용이성과 자원 효율을 위해 설계되었고, 그래서 오늘날에도 자원이 극도로 제한된 임베디드 환경에서 가장 흔히 쓰인다. 프로그래밍이 가능한 거의 모든 컴퓨터에 통째로 구현될 수 있다는 점이 그 설계의 증거다. 실제로 스택 기반 언어를 직접 만들어보는 일은 언어 설계에 관심 있는 프로그래머의 통과의례처럼 여겨지며, 포스에 뿌리를 둔 포스트스크립트(PostScript)나 조이(Joy) 같은 언어가 그 계보에 있다.

두 개의 스택과 사전

포스의 특징적인 요소는 스택을 프로그램이 직접 만질 수 있다는 점이다. 대부분의 언어는 스택 조작 자체를 허용하지 않고, C 구현에서는 함수의 인자와 반환 주소가 하나의 가변 스택 프레임에 함께 담긴다. 반면 포스는 이 두 용도를 파라미터 스택(pstack)과 리턴 스택(rstack)이라는 별개의 스택으로 분리한다. 호이트의 리스프 구현에서는 이 스택을 배열 대신 콘스 셀 연결 리스트로 만들고, 커먼 리스프의 push·pop 매크로로 다룬다. 또 다른 축은 사전(dictionary)이다. 포스 사전은 리스프 함수와 비슷하지만 실제로는 함수라기보다 절차(procedure)에 가까운 '워드(word)'들의 단일 연결 리스트다. 흥미롭게도 사전은 알파벳순이 아니라 시간순으로 저장되어, 새 워드를 뒤에 붙이고 탐색할 때는 최근 정의된 워드를 먼저 살핀다. 조회는 마지막 워드에서 시작해 prev 포인터를 따라 거슬러 올라가며, 비교에는 eq가 아니라 eql을 쓴다. 포스는 리스프와 달리 숫자 등 심벌이 아닌 값으로도 워드에 이름을 붙일 수 있기 때문이다.

스레디드 코드라는 진짜 정체성

포스에서 말하는 '스레드(thread)'는 동시성과 아무 관련이 없다. 선점형 스케줄링이나 공유 메모리 프로세스가 아니라, 코드를 메모리에 엮어 넣는 컴파일과 메타프로그래밍의 틀을 가리킨다. 리스프가 프로그램이 컴파일되는 심벌 트리에 접근하게 해준다면, 포스는 심벌 조작 대신 코드를 메모리에 배치하는 그 과정 자체에 접근하게 해준다. 겉으로 드러나는 스택과 후위 표기법보다도, 이 스레드야말로 포스를 포스답게 만든다. 호이트의 비유를 빌리면 포스가 스택을 다루는 방식은 리스프가 리스트를 다루는 방식과 같아서, 둘 다 결국 메타프로그래밍에 가장 알맞은 자료구조를 골랐을 뿐이다. 고전적인 방식은 간접 스레디드 코드이고 현대 포스는 대개 직접 스레디드 코드를 쓰며, 그 차이는 한 단계의 간접 참조다. 메모리상에서는 포인터를 나타내는 픽스넘 셀들이 나란히 놓이고, 프로세서에 맞춰 최적화된 짧은 기계어인 내부 인터프리터가 이 포인터들을 따라가며 의미를 해석한다. 셀을 만나면 현재 프로그램 카운터를 리턴 스택에 넣고 셀이 가리키는 곳으로 이동했다가, 스레드 끝에서 리턴 스택을 꺼내 원래 자리로 돌아온다.

이런 저장 방식 덕분에 컴파일된 포스 워드는 대부분 다른 워드를 가리키는 픽스넘 배열이 되어 프로그램이 극히 작아진다. 스레딩이 투명하게 드러나 있어 실행 속도와 프로그램 크기라는 대표적 트레이드오프를 정밀하게 조절할 수 있다는 것이 포스의 오랜 장점이다. 크기와 속도의 양극단에는 각각 토큰 스레딩과 서브루틴 스레딩이 있는데, 토큰 스레딩은 포인터보다 작은 픽스넘으로 간접 참조를 한 단계 더 두고, 서브루틴 스레딩은 포인터 대신 인라인 기계어 호출 명령을 직접 심는다. 최신 포스 컴파일러들은 서브루틴 스레딩을 부분적으로 채택하는 추세다.

즉시 워드, 그리고 실무자에게 남는 것

포스에는 리스프 매크로에 대응하는 개념도 있다. 워드에는 immediate 플래그가 있어, 이 플래그가 켜진 '즉시 워드'는 실행 시점이 아니라 컴파일 시점에 실행된다. 커먼 리스프의 매크로 시스템이 어떤 언어보다 강력한 것은 사실이지만, 포스 역시 언어 확장 능력에서 대다수 언어를 능가한다. 그 바탕에는 리스프와 공유하는 철학이 있다. '언어 구현자에게 충분히 좋은 것이라면 응용 프로그래머에게도 충분히 좋다'는 태도, 즉 원시 요소를 미리 정해두기보다 메타 원시 요소를 조합해 원하는 언어를 직접 짓게 하는 방식이다. 두 언어 모두 사용자가 스스로를 단순 이용자가 아니라 구현자로 여기게 만든다.

한국의 실무자에게 이 실험이 주는 의미는 명확하다. 여기서 만든 포스는 '리스프스러운(lispy)' 변형이며, 저자 스스로 밝히듯 교육과 논의를 위한 플랫폼이지 성능 최적화된 임베디드용 포스가 아니다. 실무에 그대로 이식할 코드로 읽으면 초점을 놓친다. 대신 얻을 것은 관점이다. 언어의 제약을 주어진 것으로 받아들이는 대신 '척 무어가 리스프 머신을 가졌다면 포스는 어떤 모습이었을까'라고 되묻는 태도, 그리고 스택이든 트리든 특정 자료구조에 대한 집착이 실은 메타프로그래밍이라는 목적에서 파생된 선택이라는 인식이다. 다만 스레딩 방식별 저수준 효율은 프로세서에 크게 의존하므로 원문도 세부 수치는 다루지 않는다는 점, 이 구현이 실측 벤치마크가 아니라 개념 설명이라는 점은 분명한 한계로 기억해둘 만하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://letoverlambda.com/textmode.cl/guest/chap8.html
SHARE
처리 중...