TL;DR

  • 인터프리터의 런타임 객체와 라이브러리를 재사용해 동적 언어를 컴파일하는 방식은 BSDScheme에서 구현됐으며, 기존 기능을 공유하면서 컴파일러 백엔드를 단순화함.
  • BSDScheme은 기존 파서와 D 런타임을 활용해 Scheme 코드를 D로 변환하고, 표준 라이브러리 기능을 인터프리터와 컴파일러가 함께 사용함.
  • 이 접근 방식은 Python 코드를 C로 변환해 CPython 런타임에서 실행하는 Cython의 방식과 유사함.
  • Rust로 만든 JavaScript 컴파일러 jsc는 C++와 V8을 이용한 Node 애드온을 생성하며, 일반 재귀 피보나치에서는 Node보다 두 배 이상 느렸지만 꼬리 호출 최적화 적용 뒤에는 실행 시간이 비슷한 수준임.
  • jsc는 현재 지원 기능이 제한적이며, 임베디드 V8용 C++ 생성과 타입 추론 또는 타입 힌트 기반 비박싱 함수 생성이 후속 과제임.

BSDScheme

  • 지난 1년간 Scheme 구현체인 BSDScheme을 개발했으며, 바이트코드 컴파일러와 가상 머신 대신 추상 구문 트리(AST) 인터프리터로 시작함. [BSDScheme 초기 개발 과정]( )에 관한 상세 글도 소개함.
  • 언어의 객체와 연산을 추가하면서 원시 타입의 객체 표현, D 타입과의 변환, 기본 Scheme 연산자 +, -, car, cdr 등을 포함하는 D 런타임 코드가 축적됨.
  • 컴파일러 백엔드를 구현할 때는 파서가 이미 있었으므로 코드 생성만 필요했으며, 객체 표현과 표준 라이브러리 대부분도 준비된 상태였음.
  • 가장 단순한 구현으로 Scheme 코드를 D로 변환하고, 인터프리터를 위해 이미 작성한 객체와 함수를 재사용함.
  • 예를 들어 표준 라이브러리의 equals 함수는 인수 목록에서 두 값을 꺼내 정수, 문자, 문자열, 심벌, 함수, 불리언 타입별로 비교하고, 결과를 불리언 값으로 반환함.
  • 컴파일러가 Scheme 데이터를 Value 객체로 표현하면 기존 equals 함수와 표준 라이브러리 상당 부분을 인터프리터와 컴파일러가 공유할 수 있음.
  • 컴파일을 지원하는 데 필요한 핵심 요소는 D 함수 정의와 호출, if/else 등의 제어 구조임.
  • 예시 Scheme 프로그램은 재귀적인 거듭제곱 함수와 main 함수로 구성되며, 컴파일 결과는 D 코드에서 기존 equals, 뺄셈, 곱셈, 출력 함수를 호출하는 형태임.
  • 가져오는 함수는 모두 인터프리터용으로 이미 작성돼 있었으므로, 일부 코드를 D로 변환하고 기존 라이브러리를 가져와 호출하는 방식으로 Scheme 바이너리를 생성함.
  • 이 접근 방식은 Python 코드를 동등한 C 코드로 변환해 CPython 런타임 안에서 실행하면서 컴파일된 C의 속도와 C 라이브러리 직접 호출을 활용하는 Cython의 방식과 같음.
  • Cython 개요

jsc

  • 여러 프로그래밍 언어 연구용 언어를 다뤄본 뒤, BSDScheme 컴파일러에서 얻은 경험을 활용해 더 실용적인 JavaScript 컴파일러를 만들기로 함.
  • 백엔드는 가능한 한 간단한 대상으로 정했으며, V8 C++ 라이브러리를 사용하는 C++ 코드를 생성해 Node 애드온으로 빌드하는 구조임.
  • Node 애드온을 C++로 만드는 방법이 이미 잘 정립돼 있어, JavaScript 문자열을 C++의 v8::String으로, 숫자를 v8::Number로 변환하는 식으로 간단한 프로그램을 수작업 컴파일하며 시험함.
  • ML과 Python에 대한 경험을 바탕으로 컴파일러를 Rust로 작성했으며, Dave Herman의 JavaScript 파서를 사용함. 첫 번째 프로그램을 컴파일하는 과정이 jsc 개발에서 가장 어려운 단계였음.
  • 재귀 피보나치 예제는 fib(i)에서 i가 1 이하일 때 반환하고, 그렇지 않으면 fib(i - 1) + fib(i - 2)를 계산하는 구조임.
  • Node에서 예제를 직접 실행한 기준 시간은 사용자 CPU 시간 0.06초, 시스템 시간 0.02초, 총 0.083초이며 출력은 6765임.
  • jsc 설치에는 Rust, Cargo, Node, Node-GYP가 필요하며, 저장소를 복제한 뒤 빌드·설치하고 예제 파일을 컴파일하는 흐름임.
  • 저장소 복제 URL: https:/github.com/eatonphil/jsc
  • 컴파일 결과로 애드온을 불러 jsc_main()을 호출하는 JavaScript 진입점과 전체 프로그램을 나타내는 C++ 파일을 생성함.
  • 생성된 C++ 코드는 V8 API를 사용해 값을 검사·변환하고 조건과 함수 호출을 처리하며, jsc_main에서 피보나치 결과를 console.log로 출력함.
  • 컴파일된 버전을 Node에서 실행한 기준 시간은 사용자 CPU 시간 0.16초, 시스템 시간 0.03초, 총 0.175초이며 출력은 동일하게 6765임. 이 결과는 Node 직접 실행보다 두 배 이상 느린 수준임.
  • 피보나치 함수에 전달하는 수를 늘릴수록 컴파일된 프로그램의 완료 시간은 지수적으로 악화됐지만, Node의 실행 시간은 같은 양상으로 늘지 않음.
  • Node와 jsc의 성능 차이를 줄이기 위해 꼬리 호출 최적화를 검토함.
  • BSDScheme 인터프리터에서는 꼬리 호출 제거가 일어나지 않으면 빠져나오는 루프 안에 모든 함수를 넣는 방식으로 이를 구현하는 데 일주일이 걸렸으며, 해당 최적화를 컴파일러에는 적용하지 않았음.
  • jsc에서는 적용 가능한 꼬리 호출을 실제 호출 대신 레이블과 goto로 바꾸는 기본 꼬리 호출 제거를 두 시간 만에 추가함.
  • 최적화된 피보나치 프로그램은 누산기 a, b를 사용하고 fib(n - 1, b, a + b)로 재귀 호출하는 구조임.
  • 최적화 버전의 Node 직접 실행 시간은 총 0.080초이고, jsc 컴파일 버전은 총 0.087초이며 두 실행 모두 12586269025를 출력함.

jsc의 다음 단계

  • jsc의 전반적인 기능 지원은 매우 제한적임.
  • 현재 거의 모든 기본 숫자 연산과 동등·비동등 연산, 단위 테스트를 추가한 상태임.
  • 중첩 함수, 콜백, 클로저는 지원하지 않으며, while 루프는 지원하지만 for 루프는 아직 지원하지 않음.
  • else if 지원 여부는 확실하지 않으며, 배열과 객체는 물론 생성자와 프로토타입도 지원하지 않음. 이러한 기능 추가는 비교적 쉽게 착수할 수 있는 과제로 제시됨.
  • 그다음의 흥미로운 과제로는 Node 애드온만 대상으로 하는 대신 V8을 임베드하는 C++ 생성, 그리고 Cython·SBCL 방식의 비박싱 함수를 생성하기 위한 타입 추론 또는 타입 힌트가 제시됨.