검색/JavaScript를 출력하는 컴파일러와 빌드 시스템을 계층마다 테스트하며 고치기

JavaScript를 출력하는 컴파일러와 빌드 시스템을 계층마다 테스트하며 고치기

JavaScript를 출력하는 컴파일러 모노레포에서 AI에게 작업을 맡길 때 쓴다. 태스크 러너로 빌드하고, 동결된 구문 트리 정의는 건드리지 않으며, 손댄 계층마다 테스트를 추가하고, 새 빌드 옵션을 끝까지 연결하며, 속도는 린터가 아니라 실제 측정으로 판단한다.

이 AI 규칙을 사용하려면
## 태스크 러너로, 단계를 건너뛰지 말고 빌드하라 하위 빌드 도구를 직접 손으로 호출하지 말고 저장소의 태스크 러너를 사용한다. 기본 타깃은 플랫폼 툴체인을 빌드하고, 별도의 타깃들이 표준 라이브러리를 추가하고, 테스트 스위트를 실행하고, 코드를 포매팅하며, 코드를 다시 쓰지 않은 채 포매팅 상태만 검사한다. ### 타깃들은 서로 위에 쌓인다 JavaScript 도구 체계가 필요한 타깃이라면 의존성 설치가 자동으로 실행된다. 기본 빌드는 툴체인 바이너리 — 컴파일러 실행 파일과 별도의 빌드 시스템 바이너리 양쪽 — 를 만들어 내고 이를 플랫폼별 패키지 디렉터리로 복사한다. 표준 라이브러리 타깃은 그 툴체인 산출물을 사용해 런타임 소스를 컴파일한다. 테스트 타깃들은 그 위의 모든 것을 재사용한다. ### 단계를 건너뛰지 마라 각 타깃이 그 아래 타깃에 의존하기 때문에, 하위 도구를 직접 호출하면 낡은 바이너리가 그대로 남아 클린 빌드와 일치하지 않는 결과가 나올 수 있다. 빌드 결과가 있을 수 없는 모습으로 보인다면, 더 파고들기 전에 먼저 클린 타깃을 실행하고 맨 위에서부터 다시 빌드한다. ## 동결된 구문 트리 정의는 고치지 말고 변환 모듈을 고쳐라 일부 파일은 외부 파서 확장이 그 정확한 형태에 의존하기 때문에 의도적으로 동결되어 있다. 구문 트리 정의의 동결된 버전은 절대 수정하지 않는다. 라이브 구문 트리가 바뀌면, 대신 라이브 표현과 동결된 표현 사이를 변환하는 두 개의 매핑 모듈을 갱신한다. 그래야 확장이 계속 동작하면서도 메인 트리는 자유롭게 발전할 수 있다. ### 원본 포크와의 호환성은 제약이 아니다 이 컴파일러는 다른 컴파일러의 포크로 시작했지만, 프로젝트는 자체 구문 트리를 유지하며 원본에 대해 호환성을 깨는 변경을 해도 된다. 상위 프로젝트가 무엇을 하는지가 아니라, 이 컴파일 대상에 무엇이 가장 좋은지를 기준으로 판단한다. ### 새 키워드 예약을 피하라 새 예약어를 요구하지 않는 구현을 선호한다. 예약어를 하나 만들면 이미 그 이름을 사용하고 있는 기존 코드가 깨지기 때문이다. 새 키워드는 최후의 수단으로 다루고, 먼저 기존 구성 요소를 재사용하는 문법을 찾아본다. ## 경고를 숨기거나 임시 와일드카드를 쓰지 말고 원인을 고쳐라 컴파일러를 조용하게 만들려고 경고 억제 속성을 사용해서는 절대 안 된다. 대신 근본 문제를 고친다 — 억제된 경고는 바로 그 경고가 잡으라고 존재하는 종류의 실수를 감춘다. ### 와일드카드는 지름길이 아니라 결정이다 새 언어 기능의 자리표시자로 와일드카드 패턴을 쓰고 그대로 잊어버리는 일을 하지 않는다. 그 특정 경우에 값을 무시해야 한다고 확신할 때만 와일드카드를 쓰고, 모든 새 언어 기능이 모든 컴파일러 계층에서 완전히 처리되는지 확인한다. ### 결과는 명시적으로 버리고, 상태는 계속 이어 넘겨라 어떤 함수를 부수 효과 때문에만 호출한다면, 결과를 와일드카드에 묶지 말고 명시적인 무시 함수를 사용한다. 상태를 담은 결과는 절대 버리지 않는다 — 대신 호출 체인을 통해 계속 이어 넘긴다. ### 패턴을 정직하게 유지하는 작은 관례들 이미 필수 라벨 인자를 가진 함수에 마지막 유닛 매개변수를 덧붙이지 않는다. 라벨이 이미 우발적인 부분 적용을 막아준다. 레코드 패턴에서 마지막 와일드카드는, 그것이 겨냥하는 경고가 이 코드베이스에서 비활성화되어 있다면 생략한다. 서로 다른 중간 표현에 걸쳐 이름이 비슷한 생성자를 주의한다. 같은 배리언트 이름이 컴파일 단계마다 다른 것을 뜻할 수 있기 때문이다. ## 고치기 전에 어느 컴파일러 계층을 바꾸는지 파악하라 파이프라인은 파서에서 소스 텍스트를 표층 구문 트리로 바꾸고, 프런트엔드 변환을 적용하고, 타입 검사기에서 타입이 붙은 트리를 만들고, 람다 수준 중간 표현으로 낮추고, JavaScript 수준 중간 표현으로 변환한 뒤, 마지막으로 코드를 내보낸다. 무엇이든 건드리기 전에 자신이 편집하는 계층을 먼저 파악한다. ### 계층은 디렉터리에 대응한다 구문 디렉터리는 파싱과 표층 구문을 담당한다. 프런트엔드 디렉터리는 구문 트리 변환과 외부 함수 인터페이스 처리를 담당한다. 타입 검사기 디렉터리는 타입 검사와 타입이 붙은 트리의 변환을 담당한다. 코어 디렉터리는 람다 수준 모듈과 JavaScript 수준 모듈을 모두 담고 있으며, 둘은 파일 접두사로 구분된다. 별도의 디렉터리들이 확장 유틸리티와 자료 구조, 대상 생태계를 위한 타입 정의 생성, 그리고 언어 서버와 도구 체계를 담고 있다. ### 변경은 대개 계층을 가로지른다 새 언어 기능은 보통 파서 작업, 구문 트리 정의 갱신, 타입 검사 지원, 람다 수준 처리, JavaScript 생성, 그리고 테스트를 이 순서대로 요구한다. 한 계층을 건너뛰면 파싱은 깔끔하게 되지만 그다음 잘못 컴파일되는 기능이 만들어진다. ## 각 단계의 중간 표현을 출력해 버그를 찾아라 컴파일러 바이너리는 각 단계를 출력하는 플래그들을 받는다. 전처리된 소스, 파싱 후의 파스 트리, 타입 검사 후의 타입이 붙은 트리, 그리고 최적화되지 않은 람다 표현이다. 컴파일러 소스를 읽기 시작하기 전에, 각 단계의 덤프를 자신이 기대한 것과 비교한다. ### 증상을 그것을 담당하는 계층으로 보내라 출력 포매팅 문제는 프리티 프린터 모듈의 몫이다. 타입 오류는 타입 검사기 모듈들의 몫이다. 최적화 이후에만 나타나는 잘못된 결과는 람다 수준 분석 패스의 몫이다. 잘못된 형태로 생성된 코드는 JavaScript 생성 모듈들의 몫이다. ### 작게 재현하라 반복 작업 중에는 프로젝트 전체가 아니라 최소한의 파일 하나만 컴파일한다. 표현을 들여다봐야 할 때는 람다 프린터 모듈에 임시 출력을 넣고, 변경이 반영되기 전에 그 계측 코드를 제거한다. ## 계층을 바꿀 때마다 바로 테스트를 추가하라 모든 변경이 끝난 뒤가 아니라, 각 컴파일러 계층을 수정하는 즉시 테스트를 추가한다. 늦게 테스트하면 어느 계층이 회귀를 들여왔는지 감춰지고, 작은 이분 탐색이 변경 전체를 뒤지는 조사로 커진다. ### 반드시 테스트가 필요한 경우들 새 언어 기능과 버그 수정에는 항상 테스트가 필요하다. 분석 패스를 수정하거나 JavaScript 생성을 바꿀 때도 항상 테스트가 필요하다. 생성된 JavaScript는 정확성만이 아니라 품질까지 항상 확인한다. ### 알맞은 스위트를 커버하라 파서 테스트는 문법을 검증한다. 통합 테스트는 처음부터 끝까지의 동작을 다룬다. 단위 테스트는 개별 컴파일러 함수를 다룬다. 빌드 테스트는 오류 경우와 경계 경우를 다루며, 여기에는 단일 파일의 타입 오류와, 구현 파일과 인터페이스 파일을 따로 필요로 하는 모듈 간 오류가 포함된다. 왕복 변환, 생성된 타입, 분석, 도구 체계 스위트는 각각 자체 타깃을 가지며, 해당 영역이 바뀌면 실행해야 한다. ## 오류·경고 목록을 코드와 같은 변경에서 갱신하라 저장소는 컴파일러가 낼 수 있는 모든 오류와 경고를 모듈별 목록으로 유지하며, 각 항목은 픽스처에 매핑되거나 도달 불가능한 이유가 문서화되어 있다. 이 목록은 커버리지 구멍과 죽은 코드 제거 후보를 찾는 주된 도구이므로, 낡은 항목은 두 작업을 모두 어렵게 만든다. ### 코드와 같은 변경 안에서 목록을 갱신하라 오류 배리언트를 추가하거나 제거할 때는 해당 모듈 섹션의 행도 추가하거나 제거하고, 상태를 커버됨, 도달 불가, 또는 아직 해야 함으로 설정하며, 커버하는 픽스처를 연결하거나 그 배리언트에 도달할 수 없는 이유를 기록한다. 픽스처를 추가하거나 제거할 때는 영향을 받는 행의 픽스처 경로와 상태를 갱신해, 목록과 테스트 스위트가 절대 어긋나지 않게 한다. ## 컴파일 속도와 출력 JavaScript의 가독성을 지켜라 이 컴파일러는 빠른 피드백 루프를 위해 설계되었고 큰 코드베이스까지 확장되어야 하며, 생성된 출력은 사람이 읽는다. 두 속성 모두 나중에 생각할 것이 아니라 리뷰 기준이다. ### 생성된 코드를 읽기 좋게 유지하라 의미 없는 생성 심벌을 내보내지 말고 출력을 읽기 좋게 유지한다. 올바르지만 읽을 수 없는 출력을 만드는 변경은 완료된 것이 아니다. ### 전후를 측정하라 모든 변경의 컴파일 속도 영향을 고려하고, 람다 수준과 JavaScript 수준에서 적절한 최적화 패스를 사용하며, 성능을 목적으로 한 변경은 코드를 보고 추론하는 대신 전후로 프로파일링한다. ## 이름 규칙을 따르고, 사용자에게 보이는 변경은 체인지로그에 적어라 작성 중인 언어의 명명 관례에 맞춘다. 컴파일러의 구현 언어는 스네이크 케이스를 쓰고, 컴파일 대상 언어 자체로 작성된 소스는 카멜 케이스를 쓴다. 코드베이스의 기존 패턴을 따르고, 새로 만들기보다 기존 유틸리티 함수를 선호하며, 복잡한 알고리즘과 자명하지 않은 로직에 주석을 달고, 합리적인 범위에서 하위 호환성을 유지한다. ### 커밋에는 의무가 따른다 프로젝트가 요구하는 형식으로 커밋에 서명(sign off)하고, 모든 변경에 적절한 테스트를 포함하며, 커밋 전에 빌드가 통과하는지 확인한다. ### 사용자에게 보이는 변경을 기록하라 버그 수정이든, 기능이든, 호환성을 깨는 변경이든, 사용자에게 보이는 변경에는 체인지로그 항목을 추가한다. 현재 미출시 버전의 해당 섹션 아래에 넣고, 줄 끝을 기여로 이어지는 링크로 마무리한다. 제출물에는 이 항목이 포함되어 있을 것으로 기대된다. ## 새 빌드 시스템 옵션을 그것이 지나가는 모든 곳에 추가하라 시스템 언어로 작성된 동반 빌드 시스템은 빠른 증분 빌드와 더 나은 오류 보고를 제공한다. 그 모듈들은 깔끔하게 나뉜다. 빌드 상태와 모듈 레코드 같은 핵심 자료 구조, 컴파일과 컴파일러 인자 생성, 구문 트리 생성과 파서 인자, 패키지 탐색과 의존성 해석, 모듈 의존성 그래프와 순환 감지, 산출물 정리, 빌드 로깅과 오류 보고, 명령줄 인터페이스, 프로젝트 설정 파싱, 파일 감시기, 그리고 진입점이다. ### 옵션이 거쳐 가는 모든 자리에 추가하라 빌드 인자 타입과 감시 인자 타입 양쪽에 플래그를 선언하고 둘 사이의 변환을 갱신한다. 새 프로젝트 설정 필드가 있다면 설정 파서를 확장한다. 관련된 빌드 모듈을 수정한다. 그런 다음 진입점에서 빌드 함수로, 빌드 초기화로, 빌드 상태로, 그리고 감시 경로를 따라서도 매개변수를 전체 체인에 걸쳐 이어 넘긴다. 새 동작에 대한 단위 테스트를 추가한다. ### 우선순위와 오류 처리를 미리 정하라 명령줄 플래그가 프로젝트 설정 파일보다 우선한다. 패닉을 일으키는 대신 프로젝트의 표준 오류 래핑 결과 타입을 통해 오류를 전파하고, 개발용 진단에는 디버그 수준 로깅을 사용한다. ## 로컬 컴파일러를 가리키게 해서 빌드 시스템을 직접 실행하라 하위 프로젝트로 디렉터리를 옮기지 말고, 지속적 통합이 하는 방식대로 저장소 루트에서 빌드 시스템의 매니페스트 파일을 가리켜 그 자체의 툴체인 명령을 실행한다. 태스크 러너도 이를 빌드하고 통합 테스트를 실행하기 위한 단축 타깃을 제공한다. ### 개발용 바이너리가 로컬 툴체인을 가리키게 하라 그냥 직접 실행하면 바이너리는 설치된 릴리스를 집어 든다. 실행하기 전에, 컴파일러 실행 파일 경로와 런타임 경로에 대한 환경 변수를 현재 체크아웃 안의 절대 경로로 해석해 내보낸다. 개발용 바이너리는 도구 이름이 아니라 패키지 이름을 딸 수 있으니, 넘겨짚지 말고 실제로 생성된 파일 이름을 확인한다. ### 동작이 불분명하면 상세도를 올려라 빌드 시스템은 정보 수준과 디버그 수준 로깅을 위해 상세도를 높이는 플래그를 받는다. 감시 세션이 실제로 어떤 폴더를 감시하고 있는지 같은 세부 사항은 그렇게 확인한다. ### 트레이싱은 표준 명세를 따른다 분산 트레이싱은 전적으로 표준 텔레메트리 환경 변수 — 수집기 엔드포인트, 트레이스 전용 엔드포인트 재정의, 익스포터 헤더, 서비스 이름, 리소스 속성, 로그 수준 — 로만 설정되며, 프로젝트 고유의 조정 손잡이는 없다. 엔드포인트를 설정하는 것이 텔레메트리를 켜는 조건이며, 아무것도 설정하지 않으면 트레이싱은 아무 일도 하지 않는다. 이벤트가 트레이스에 나타나기를 원한다면 일반 로깅 매크로가 아니라 트레이싱 매크로로 내보낸다. 둘은 별개의 시스템을 쓰기 때문이다. ## CI 테스트는 고정 시간 대신 조건이 갖춰질 때까지 기다리게 하라 지속적 통합 러너는 개발 머신보다 느리고, 그래서 고정된 sleep은 불안정해진다. 추측한 시간만큼 자는 대신, 파일이 나타나기를 기다리는 것처럼 실제로 신경 쓰는 조건을 폴링한다. ### 비동기 종료는 완료가 아니다 감시기에게 종료를 알리는 헬퍼는 — 예를 들어 잠금 파일을 지우는 방식으로 — 신호를 보낼 뿐이고, 프로세스가 끝나기를 기다려 주지는 않는다. 종료 직전에 설정 변경 이벤트를 발생시키는 일은 피한다. 감시기가 예상하지 못한 동시 재빌드를 시작할 수 있기 때문이다. ### 플랫폼별 텍스트 편집을 직접 만들어 쓰지 마라 제자리 스트림 편집은 운영체제마다 다르게 동작하므로, 셸 테스트에서 편집기를 날것으로 호출하는 대신 저장소의 치환 헬퍼와 경로 정규화 헬퍼를 사용한다. 개별 통합 스크립트는 실행 파일 경로와 런타임 경로를 내보내고, 공유 헬퍼 파일을 source한 다음, 그 스크립트 하나만 호출해서 단독으로 실행할 수 있다. 전체 스위트를 돌리지 않고 반복 작업할 때 유용하다. ## 린터의 제안은 실제 성능으로 판단하라 린터의 제안은 입력이지 판결이 아니다. 제안된 리팩터링이 성능이나 명확성을 해칠 수 있다면, 기계적으로 적용하지 말고 그 득실을 명시적으로 따진다. ### 값을 하는 곳에서만 복잡성을 받아들여라 매개변수 구조체가 여러 개의 인자보다 자동으로 나은 것은 아니다. 정당하게 많은 매개변수가 필요하고 구조체가 형식만 늘릴 뿐인 함수에 대해서는 해당 린트를 국소적으로 허용한다. 복사는 작고, 한 번뿐이고, 소유권 규칙상 필요하며, 핫 패스 바깥에 있을 때 받아들일 만하다 — 그것을 피하려고 과하게 설계하지 않는다. ### 근거를 바탕으로 최적화하라 최적화하기 전에 프로파일링한다. 빌드 시스템의 성능 걱정거리는 대부분 실제 컴파일 시간에 비하면 무시할 만하다. 효율이 정말 중요한 곳에서는, 경계마다 소유 문자열 타입과 빌린 문자열 타입 사이를 변환하는 대신 호출 체인을 따라 매개변수 타입을 일관되게 유지한다. 그런 변환은 아무 이득 없이 할당만 늘리기 때문이다. 더러워진 모듈만 다시 컴파일하고, 독립적인 작업은 병렬화하고, 큰 프로젝트에서 빌드 상태의 메모리 사용량을 주시하며, 파일 시스템 연산을 최소화한다.