Gleam doesn't compile to Erlang source anymore
Gleam, 이제 Erlang 소스 코드로 컴파일하지 않습니다
Gleam 1.19.0은 Erlang 코드를 생성하던 방식을 바꿔 Erlang 컴파일러의 중간 표현인 abstract forms를 직접 출력합니다. 빌드 속도와 오류 위치 정보가 개선됐으며, JavaScript 코드 생성, TypeScript 타입 선언, 빌드 도구 연동과 언어 서버 기능도 함께 다듬었습니다.
- 주제
AI 요약
Gleam 1.19.0은 Erlang 가상 머신과 JavaScript 런타임을 대상으로 하는 언어 Gleam의 새 버전입니다. 가장 큰 변화는 Erlang 코드 생성기의 재작성입니다. Gleam은 이전처럼 Erlang 소스 코드를 만들지 않고, Erlang 컴파일러가 사용하는 중간 표현인 Erlang abstract forms를 직접 생성합니다.
Erlang 컴파일 경로 변경
Erlang abstract forms는 Erlang 문법을 나타내는 메타데이터 포함 트리입니다. 보통 Erlang 토크나이저와 파서가 소스 코드를 읽어 만들며, Erlang external term format으로 인코딩할 수 있습니다. Gleam은 이 형식의 바이너리를 직접 로드해 Erlang 컴파일러의 앞부분인 토큰화와 파싱 단계를 건너뜁니다.
새 코드 생성기는 Erlang 대상 프로젝트의 컴파일 시간을 줄입니다. 런타임에 전달하는 위치 정보도 Gleam 원본 코드 기준으로 정확해집니다. 따라서 BEAM 충돌 보고서와 스택 트레이스의 줄 번호가 생성된 Erlang 코드가 아니라 Gleam 코드의 위치를 가리킵니다. 이 메타데이터는 edb 같은 디버거에서 Gleam 지원을 구현하는 데도 활용할 수 있지만, 프로젝트 팀은 아직 그 작업을 진행하지 않았습니다. 재작성 과정에서는 오래된 코드 생성기를 현재 코드베이스의 규칙과 관례에 맞게 정비했습니다.
컴파일 속도 비교에는 José Valim의 langcompilebench를 사용했습니다. 각 언어에서 문자열 "hello world"를 반환하는 함수 100개를 포함한 모듈 100개를 컴파일하는 방식입니다. 글에서는 Gleam 1.17.0과 1.19.0의 캐시 없는 전체 빌드를 비교하며 상당한 개선을 보고합니다. 다만 이 테스트는 각 언어 기능의 일부만 다루는 인공적인 벤치마크입니다. 실제 프로젝트는 코드 형태와 기능 사용량이 달라 결과를 그대로 일반화할 수 없다고 설명합니다. Gleam 컴파일은 증분 방식이므로 일반적인 개발 중에는 전체 빌드보다 대기 시간이 짧습니다.
BEAM 바이트코드를 직접 만들지 않는 이유
Erlang abstract forms 대신 BEAM 바이트코드를 직접 생성하면 Erlang 컴파일러를 우회할 수 있습니다. 하지만 BEAM 바이트코드는 가상 머신 버전마다 바뀔 수 있습니다. Gleam 팀은 변경을 계속 따라가고 Erlang 유지보수자와 협력해야 하며, 새 가상 머신 버전에 맞춰 Gleam도 준비해야 합니다. 수십 년간 Erlang 컴파일러에 쌓인 최적화를 다시 구현하는 비용도 듭니다. 기업이나 학술 기관의 지원 없이 후원으로 운영되는 Gleam 프로젝트는 장기적인 유지 비용을 고려해야 합니다. 그래서 현재는 비용과 이점의 균형이 맞는 선택으로 abstract forms를 택했습니다. Elixir도 이 방식을 사용합니다.
JavaScript 생성 코드와 타입 개선
JavaScript 대상 컴파일러는 패턴 매칭을 중첩된 if 문으로 변환하던 방식을 개선했습니다. 예를 들어 Wibble의 두 필드가 각각 1과 2인지 확인하는 코드는 이전에 여러 단계의 조건문과 임시 변수를 만들었습니다. 이제는 타입 확인과 필드 검사를 하나의 조건으로 합칩니다. 압축 후 번들 크기는 거의 달라지지 않지만 JavaScript 엔진이 최적화해야 할 분기 수가 줄어듭니다.
짧은 Gleam 리스트 리터럴도 더 직접적인 코드로 변환합니다. 이전에는 JavaScript 배열을 만든 뒤 arrayToList 함수로 Gleam 리스트를 구성했습니다. 새 방식은 prepend를 이어 붙이고 마지막에 empty를 두는 형태입니다. 최신 JavaScript 엔진에서 성능이 좋아졌으며 Lustre처럼 짧은 리스트를 많이 사용하는 프로젝트에서 효과가 큽니다. 긴 리스트에서는 개선이 확인되지 않아 기존 배열 변환 방식을 유지합니다.
Gleam 사용자 정의 타입의 JavaScript API가 만드는 TypeScript 선언도 보완했습니다. 기존 타입 판별 함수 선언은 Box<number> 같은 값을 Full인지 확인할 때 내부 타입 매개변수를 unknown으로 일반화했습니다. 새 오버로드는 Box<I>를 받아 Full<I>로 좁히므로 기존 타입 정보를 보존합니다.
빌드 도구, 언어 서버, 오류 메시지
Elixir의 Mix와 Erlang의 rebar3에서 Gleam 패키지를 사용하기 쉽도록 빌드 명령도 개선했습니다. Gleam 컴파일러가 BEAM용 .app 리소스 파일을 생성하며, compile-package에는 개발 의존성을 제외하는 --no-dev 옵션이 추가됐습니다. 패키지 정보와 인터페이스를 표준 출력으로 내보낼 수 있고, JavaScript 및 TypeScript 프렐류드를 파일로 저장하는 기능도 마련했습니다.
언어 서버는 필드와 인수의 라벨에 대해 정의로 이동, 참조 찾기, 이름 바꾸기를 지원합니다. WebAssembly 컴파일러에는 브라우저에서 Gleam 코드를 포맷하는 format_source 함수가 추가됐습니다. 글은 이 기능을 향후 플레이그라운드에도 적용할 계획이라고 밝힙니다. 오류 처리도 손봤습니다. Git 병합 충돌 표식, 잘못 놓인 레코드 업데이트 구문, Gleam에 없는 +=와 *= 연산자, 잘못된 패턴 매칭의 | 사용 등에 맞춤 오류를 추가했습니다. 잘못된 타입 별칭이 뒤따르는 여러 오류를 일으키는 문제도 줄였습니다.
Lobsters 반응
- @crowdhailer — 제가 제대로 읽은 건가요? Gleam에서 Erlang abstract form으로 컴파일하는 편이 Erlang에서 Erlang abstract form으로 컴파일하는 것보다 빠른가요?
- @lpil — rebar3로 컴파일한 그 벤치마크 프로젝트에서는 그렇습니다.
- @mond — Gleam은 귀엽고 잘 설계된 언어라서, 시간이 더 있고 적당한 사용 사례가 있다면 써보고 싶습니다. 별개의 질문인데, Gleam 사용자는 use 표현식을 어떻게 생각하나요?
- @lpil — 인기가 있지만, Gleam을 배울 때 어려움을 겪는 사람에게는 가장 흔히 혼란을 주는 부분입니다.
- @eaon — lpil의 말에 동의합니다. use가 이해되기까지 시간이 걸리지만, 익숙해지면 코드를 읽기 쉽게 해줍니다. 언어에서 가장 유용한 요소 중 하나라고 해도 좋겠습니다.
- @janiczek — 연속 전달 스타일(continuation-style) API를 쓰도록 유도하나요? 꼭 그런 API가 필요하지 않은 경우에도요? Haskell의 do 표기법이 모나드 API를 권하는 것처럼요.
- @lpil — 대체로 그렇지 않습니다. 공개된 패키지를 꽤 많이 검토하는데, 그런 사례는 한 번밖에 떠오르지 않습니다. 과용 문제가 가장 많은 문법은 파이프 연산자입니다. 익숙해진 뒤 모든 코드를 파이프로 쓰려는 사람도 있습니다.
- @johnjoz — 제가 컴파일을 해온 지 38년쯤 됐는데, transpiler가 열등함을 뜻한다고 쓰이는 경우는 들어본 적이 없습니다. 오히려 활용(leverage)을 뜻한다고 생각했습니다.
원문: Gleam / 번역·요약: Trawling