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

AST-grep는 왜 Tree-sitter를 러스트로 다시 썼나: 파서보다 중요했던 것

코드 검색 도구 ast-grep이 그 심장부인 Tree-sitter의 C 코어를 러스트로 다시 작성했다. 흥미로운 점은 그 코드를 사람이 직접 타이핑한 것이 아니라 ChatGPT가 지시를 받아 작성했다는 사실이다. 결과물은 파싱 속도, 완성된 트리를 읽는 속도, 그리고 ast-grep 전체 실행 속도 모두에서 기존보다 빨라졌다. 제목에 등장하는 '30%'는 파서 단독 성능 향상 수치이며, 실제 애플리케이션을 끝에서 끝까지 돌렸을 때는 약 22% 빨라졌다.

배경을 짚어두는 편이 좋겠다. ast-grep은 텍스트가 아니라 구문(syntax) 단위로 코드를 검색하는 도구여서, 다루는 모든 파일을 먼저 구문 트리로 변환해야 한다. 그 트리를 만드는 것이 Tree-sitter다. 문법 정의를 넣으면 해당 언어의 빠른 파서를 생성해 주는 프레임워크로, 원래 에디터 세계에서 태어나 지금은 방대한 문법·도구 생태계를 떠받치고 있다. ast-grep의 성능을 파고들면 규칙 자체는 캐싱하거나 불필요한 구문 방문을 건너뛰어 얼마든지 빠르게 만들 수 있었지만, 모든 파일이 트리가 되어야 한다는 조건은 바뀌지 않았다. 파서는 토대이자 동시에 천장이었다.

한 사람이 감당할 수 없던 일이 가능해진 이유

저자는 이 파서를 다시 쓰는 일을 수년간 꿈만 꿨다고 말한다. 성숙한 C 구현, 바이너리 호환성, 외부 스캐너, 오류 복구, 증분(incremental) 파싱, 모호한 문법, 여러 언어 바인딩, 그리고 그 위에 쌓인 거대한 생태계를 깨지 않아야 한다는 부담까지. 한 사람에게는 주말 프로젝트가 아니었다. 상황을 바꾼 것은 Bun, pgrust, Roc 등에서 잇따라 등장한 AI 기반 재작성 시도였다. 이들은 Tree-sitter 재작성이 현명하다는 것을 증명하지도, 런타임을 작게 만들지도 않았지만, 그 실험이 개인이 시도할 만큼 저렴해졌다는 사실은 보여 주었다. 저자는 그 지렛대를 빌려 '무리한 질문'을 던졌다.

작업은 순탄한 직선이 아니었다. 처음에는 호환성을 최우선으로 두고 C 코드를 한 부분씩 러스트로 옮겼다. 기본 유틸리티, 트리 저장, 렉싱, 파서 스택, 트리 탐색, 마지막으로 파서 루프 순서였다. 에이전트가 코드를 읽고 패치를 쓰고 컴파일 오류를 고치고 테스트를 돌리는 동안, 사람은 목표와 제약, 반박과 결정을 제공했다. 기존 구현과 테스트 스위트가 정답지 역할을 했다. C 코어가 러스트가 되었고, 컴파일되었고, 테스트를 통과했으며, 기존 문법이 그대로 동작했다.

빠른 파서가 오히려 느려지는 순간

곧바로 '더 빠르게'를 요청한 것이 함정이었다. 프로파일링을 제대로 쓰고 알고리즘 수준의 변화를 찾으라고 지시한 결과 벤치마크는 20% 향상 선을 넘었지만, 저자는 코드를 열고도 흐름을 따라갈 수 없었다. 기계적으로 번역된 층 위에 AI가 만든 최적화가 겹겹이 쌓였고, 얼마 지나지 않아 파서는 러스트다운 친절한 패닉도 없이 그냥 세그폴트로 사라지기 시작했다. 20% 빠르면서 이따금 프로세스가 증발하는 파서는 최적화가 아니다. 저자는 최적화 작업 전체를 되돌렸고 20%도 함께 사라졌다. 이후 도달한 성능은 '더 빠르게'가 아니라 '설명 가능하게' 만들라는 방향 전환에서 나왔다.

다음 최적화 기법은 뜻밖에도 '삭제'였다. 대상 워크로드를 다시 정의하자 질문이 달라졌다. Tree-sitter의 증분 파싱은 사용자가 한 글자씩 편집하는 에디터에서 이전 트리를 재사용해 변경된 영역만 다시 만드는, 대단히 유용한 기능이다. 그러나 ast-grep과 AI 코딩 에이전트가 다루는 세계는 완성된 파일 스냅숏이다. 에이전트가 파일을 읽고 분석·수정한 뒤 새 스냅숏을 넘기는 이 흐름에서 새로 파싱하는 것은 성능 저하가 아니라 정상 동작이다. 그래서 이전 트리 재사용 로직을 제거했다. 호환을 위해 공개 파라미터는 남겼지만 런타임은 항상 새로 파싱한다. Wasm으로 컴파일된 문법을 런타임에 네이티브로 로드하는 기능도 함께 걷어냈다(브라우저용 Wasm 빌드는 유지). 다만 이는 상호작용형 에디터로 돌아가면 재검토해야 하는, 좁은 워크로드를 위한 제품적 결정이라고 못 박는다.

삭제 뒤에는 정돈이 이어졌다. 로컬에서 수명이 증명되는 원시 포인터는 참조나 슬라이스로, '노드 없음'을 뜻하던 센티널 포인터는 정직한 Option으로, 불필요한 출력 파라미터는 반환값으로 승격했다. 반면 다른 바이너리가 이미 그 모양에 의존하고 있는 생성된 언어 레이아웃과 내보내는 함수는 일부러 C의 형태 그대로 '못생기게' 남겨 두었다. 목적은 예쁜 문법이 아니라, 세그폴트나 의심스러운 할당이 아키텍처 어디에서 발생했는지 주소를 짚을 수 있는 구조였다. 이후 파서가 끊임없이 수행하는 reduce 연산에 집중해, 일반화 LR 파서에 관한 학술 자료까지 조사한 끝에 '입력이 실제로 필요로 하기 전에는 일반 구조를 만들지 말라'는 오래된 교훈에 도달했다. 선형 스택으로 그래프 관리 비용을 피하고, 아레나로 할당자 호출을 줄인 것이 그 실천이었다.

한국 실무자에게 남는 것

메모리는 분명한 트레이드오프다. 러스트 빌드는 ast-grep 실행에서 약 8 MiB를 더 썼다. 다만 TypeScript 컴파일러 저장소의 테스트 기준 트리라는 극한의 부하 테스트에서 최대 91.2 MiB를 기록했는데, 이는 프로젝트 초기 같은 코퍼스가 1 GiB를 넘겼던 것에 비하면 성취에 가깝다. 이 작업이 주는 실무적 함의는 세 가지다. 첫째, 대규모 C 런타임 재작성이 이제 개인이 AI 에이전트를 지휘해 시도할 만한 일이 되었다는 것. 둘째, 최적화의 첫 수단이 종종 '더 빠른 코드'가 아니라 '내 워크로드에 필요 없는 기능의 삭제'라는 것. 셋째, 파서 벤치마크의 승리가 곧 애플리케이션의 승리는 아니라는 점이다. 이 결과물은 상류 Tree-sitter의 1:1 대체가 아니라 파일 단위 분석과 에이전트 도구를 위한 좁은 런타임이며, 성능 목표를 자기 워크로드로 명확히 규정한 덕분에 얻은 것이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://astgrep.com/blog/tree-sitter-rust-rewrite
SHARE
처리 중...