Lobsters

How I Wrote a Forth (Without Knowing How)

Forth를 모르고 Forth를 만든 이야기 — Lua C API 위에서 MoonForth 구현하기

Forth 구현 경험이 없던 개발자가 Lua의 C API 위에서 C로 작성한 Forth 구현체 MoonForth의 개발 과정을 회고한 글입니다. JonesForth의 소스를 참조하되 어셈블리 대신 Lua 스택을 직접 조작하는 C 함수로 코어 워드를 구성했고, 함수 포인터 테이블로 컴파일을 구현해 일주일 만에 조건문과 루프까지 갖춘 실사용 가능한 Forth를 완성했습니다.

본문

Forth를 한 번도 구현해 본 적 없는 저자가 Lua의 C API를 기반으로 자신만의 Forth인 MoonForth를 완성하기까지의 과정을 기록한 회고입니다. 저자는 과정을 제대로 기록하지 않았다고 밝히면서도, Forth 작성이 '놀라울 정도로 직선적인' 과정이었기에 기억만으로도 충분히 재구성할 수 있었다고 말합니다.

JonesForth에서 출발하되 우회 전략을 택하다

좋은 Forth 구현 안내서를 찾지 못한 저자는 Forth 커뮤니티의 일반적인 권고에 따라 x86 어셈블리로 작성된 JonesForth의 소스 코드를 참조했습니다. 다만 목표가 C로 작성하는 것이었기에 어셈블리 코드가 직접 옮겨지지 않았고, 인터프리터와 컴파일러 부분은 이해하기 어려워 일단 건너뛰고 다른 부분부터 진행했습니다.

Lua의 스택을 Forth의 스택처럼 다루기

돌파구는 Lua가 내부적으로 함수 간 값 전달에 스택을 사용한다는 점이었습니다. 저자는 Lua의 C API 위에서 이 스택을 직접 조작하는 C 함수들을 작성했고, 이를 Forth의 코어 워드로 간주했습니다. 단 몇 시간 만에 스택 조작 함수들이 갖춰졌고, 이후 복잡한 워드를 구현할 때도 이미 만든 자신의 워드들을 최대한 재사용하는 방식으로 Forth 특유의 '팩토링' 개념을 거꾸로 적용했습니다. JonesForth의 하드코딩된 워드들의 동작을 관찰한 뒤 자기 방식으로 재구현하는 식이었습니다.

컴파일 모델: 함수 포인터의 테이블

결정적 아이디어는 C 함수를 포인터로 만들어 Lua에 유저데이터로 푸시할 수 있다는 깨달음이었습니다. Forth 워드가 다른 워드들의 나열에 불과하므로, 사용자 정의 워드를 C 함수 포인터의 테이블로 저장할 수 있었습니다. Lua 테이블이 항상 참조이므로 다른 워드 정의를 담은 테이블도 중첩해 저장 가능했고, 새 워드가 기존 워드를 덮어써도 이미 컴파일된 워드에 영향을 주지 않는 일반적인 Forth의 재정의 의미론이 자연스럽게 성립했습니다. 문자열과 숫자 리터럴도 워드 정의 안에 함께 저장해 인터프리터가 각 항목의 타입을 읽어 처리하게 했습니다.

인터프리터, 컴파일러, 그리고 흐름 제어

인터프리터는 시행착오는 있었지만 print 문으로 실행을 추적해 결함을 하나씩 제거하는 방식으로 완성됐고, Forth 작업을 결심한 지 사흘 만에 첫 코드가 실행됐습니다. 컴파일러는 곧이어 완성됐고 immediate word 개념도 쉽게 구현됐습니다. 흐름 제어는 JonesForth처럼 더 단순한 워드로부터 Forth 자체로 구현했는데, 워드 정의에 직접 접근할 수 없는 제약은 분기 명령을 나중에 백필하는 워드를 만들어 해결했습니다. 이로써 if/else/then과 루프를 갖추게 됐고, 시작한 지 일주일 만에 JonesForth 참조 탭을 닫을 수 있었습니다.

'스스로를 쓰는 언어'의 실감

저자는 Dave Gauer의 유명한 글에서 말한 'Forth는 스스로를 쓰는 언어'라는 주장을 이제야 이해했다고 씁니다. Forth가 어떻게 동작해야 하는지 대략의 감만 있으면, 다음에 만들어야 할 것이 항상 눈앞에 보이고, 부족한 것은 프로그램을 쓰다 막히는 지점에서 자연스럽게 드러나기 때문입니다. 문제를 알고 해법의 형태를 알며 소스 데이터를 원하는 결과로 조작할 줄 안다면, 언어가 작고 자신이 직접 만들었을 때 확장은 쉬운 일이라는 것입니다.

Luarth와의 차이와 설계 선택

Lua에서 Forth를 부트스트랩하는 Eduardo Ochs의 2008년 글과 여러 Lua 기반 Forth 구현이 이미 존재하며, 같은 목표의 Luarth라는 프로젝트도 있었습니다. 그러나 Luarth는 사용자 정의 워드조차 만들 수 없는 미완성 상태였습니다. 결정적 차이는 Luarth가 내장 워드를 완전한 Lua 함수로 구현한 반면 MoonForth는 평범한 C 함수로 구현해 포인터 역참조로 호출한다는 점입니다. Lua 호출 스택을 거치지 않으므로 MoonForth의 워드는 내부 Lua 상태와 충돌 없이 스택을 자유롭게 조작할 수 있고, 저자는 이것이 Luarth가 멈춘 지점을 자신이 넘을 수 있었던 이유일지 모른다고 추측합니다.

왜 Forth이고 왜 Lua인가

저자는 윤리적 문제가 있는 소프트웨어 공급자에 대한 의존에서 벗어나려는 맥락에서 Forth에 관심을 가졌다고 밝힙니다. Forth는 도구가 거의 없이도 개인이 쉽게 만들 수 있어 대안적 컴퓨팅 생태계 실험에 적합하기 때문입니다. Lua를 선택한 이유로는 유지보수자들이 패치를 거의 받아들이지 않고 직접 개발해온 역사, 코드베이스가 오래 비대해지지 않은 점, 외부 변경 압력이 적은 점을 들어 AI 생성 코드의 유입 가능성이 낮은 생태계라고 평가합니다. 또한 MoonForth의 유일한 의존성은 Lua 자체이므로 Lua가 있는 곳이면 어디든 이식 가능하며, Lua 5.1과의 호환을 통해 LuaJIT 등 대안 구현체와의 상호운용성도 확보했습니다. 실제로 간단한 GNOME 앱과 LÖVE 게임 엔진용 개념증명 프로그램을 MoonForth로 작성했으며, 다음 단계는 본격적인 소프트웨어를 만들어 보는 것이라고 글을 맺습니다.

원문: vtrlx.ca / 번역·요약: Trawling