컴파일러

컴퓨팅에서 컴파일러(compiler, 순화 용어: 해석기, 번역기)는 하나의 프로그래밍 언어(소스 언어)로 작성된 컴퓨터 코드를 다른 언어(타겟 언어)로 번역하는 소프트웨어이다. "컴파일러"라는 이름은 주로 고급 프로그래밍 언어의 소스 코드를 저급 프로그래밍 언어(예: 어셈블리어, 목적 파일, 또는 기계어)로 번역하여 실행 파일을 생성하는 프로그램에 사용된다. 원시 코드에서 목적 코드로 옮기는 과정을 컴파일(compile, 순화 용어: 옮김, 번역, )이라고 한다.

다양한 형태의 유용한 출력을 생성하는 여러 종류의 컴파일러가 있다. 크로스 컴파일러는 크로스 컴파일러 자체가 실행되는 CPU나 운영체제와는 다른 CPU나 운영체제를 위한 코드를 생성한다. 부트스트랩 컴파일러는 언어를 위한 더 영구적이거나 더 잘 최적화된 컴파일러를 컴파일하는 데 사용되는 임시 컴파일러인 경우가 많다.

관련 소프트웨어로는 저급 언어를 고급 언어로 번역하는 프로그램인 역컴파일러, 고급 언어 간에 번역하는 프로그램으로 주로 소스 대 소스 컴파일러 또는 트랜스파일러라고 불리는 프로그램, 언어 재작성기(일반적으로 언어 변경 없이 식의 형태를 번역하는 프로그램), 그리고 컴파일러 컴파일러 (종종 일반적이고 재사용 가능한 방식으로 컴파일러를 생성하여 다양한 컴파일러를 만들 수 있도록 하는 컴파일러 또는 그 일부를 생성하는 컴파일러)가 있다.

컴파일러는 다음과 같은 작업 중 일부 또는 전부를 수행할 수 있으며, 이들은 종종 단계라고 불린다: 전처리기, 낱말 분석, 구문 분석, 의미 분석 (구문 지향 번역), 입력 프로그램을 중간 표현으로 변환, 코드 최적화 및 기계별 코드 생성. 컴파일러는 일반적으로 이러한 단계를 모듈형 구성 요소로 구현하여 효율적인 설계와 소스 입력에서 대상 출력으로의 프로그램 변환의 정확성을 촉진한다. 잘못된 컴파일러 동작으로 인한 프로그램 오류는 추적하고 해결하기 매우 어려울 수 있으므로, 컴파일러 구현자들은 컴파일러 정확성을 보장하기 위해 상당한 노력을 기울인다.

 인터프리터와의 비교 

소스 코드를 실행 가능하게 만드는 측면에서 인터프리터는 컴파일러와 유사한 기능을 제공하지만, 다른 메커니즘을 통해 작동한다. 인터프리터는 코드를 기계어로 변환하지 않고 실행한다. 따라서 일부 인터프리터는 소스 코드를 직접 실행하는 반면, 다른 인터프리터는 바이트코드와 같은 중간 형태를 실행한다.

따라서 네이티브 코드로 컴파일된 프로그램은 인터프리트될 때보다 더 빠르게 실행되는 경향이 있다. 바이트코드 중간 형태를 사용하는 환경은 중간 속도를 보인다. 반면 JIT 컴파일은 한 번의 시작 처리 시간 비용으로 네이티브 실행 속도를 제공한다.

어셈블리어 및 C와 같은 저급 프로그래밍 언어의 경우, 특히 속도가 중요한 경우 크로스 플랫폼 지원보다는 컴파일되는 것이 일반적이다. 이러한 언어의 경우 소스 코드와 결과 기계어 사이에 일대일 대응이 많아 프로그래머가 하드웨어 사용을 더 쉽게 제어할 수 있다.

이론적으로 프로그래밍 언어는 컴파일러나 인터프리터 중 하나를 통해 사용될 수 있지만, 실제로는 각 언어가 둘 중 하나와 함께 사용되는 경향이 있다. 그럼에도 불구하고, 일반적으로 인터프리트되는 언어를 위한 컴파일러를 작성하는 것은 가능하다. 예를 들어, 커먼 리스프는 자바 바이트코드(그리고 자바 가상 머신에 의해 인터프리트됨), C 코드(그리고 네이티브 기계어로 컴파일됨), 또는 직접 네이티브 코드로 컴파일될 수 있다.

 역사 

upright=1.5|섬네일|일반적인 다중 언어, 다중 대상 컴파일러의 작동 다이어그램
과학자, 수학자, 엔지니어들이 개발한 이론적인 컴퓨팅 개념은 제2차 세계 대전 중 디지털 현대 컴퓨팅 개발의 기반을 형성했다. 디지털 장치는 1과 0, 그리고 기본 기계 아키텍처의 회로 패턴만 이해하기 때문에 원시적인 이진 언어가 발전했다. 1940년대 후반에는 컴퓨터 아키텍처에 대한 보다 실용적인 추상화를 제공하기 위해 어셈블리 언어가 만들어졌다. 초기 컴퓨터의 제한된 메모리 용량은 최초의 컴파일러가 설계될 때 상당한 기술적 문제를 야기했다. 따라서 컴파일 과정은 여러 개의 작은 프로그램으로 나뉘어야 했다. 프런트 엔드 프로그램은 백 엔드 프로그램이 타겟 코드를 생성하는 데 사용하는 분석 결과물을 생성한다. 컴퓨터 기술이 더 많은 자원을 제공함에 따라, 컴파일러 설계는 컴파일 과정과 더 잘 일치할 수 있었다.

프로그래머가 고급 언어를 사용하는 것이 일반적으로 더 생산적이므로, 디지털 컴퓨터가 제공하는 기능으로부터 고급 언어의 개발이 자연스럽게 이어졌다. 고급 언어는 구문과 의미론에 의해 엄격하게 정의되는 형식 언어로, 고급 언어 아키텍처를 형성한다. 이러한 형식 언어의 요소는 다음과 같다:
 알파벳, 모든 유한한 기호 집합;
 문자열, 유한한 기호 시퀀스;
 언어, 알파벳에 대한 모든 문자열 집합.

언어의 문장은 문법이라고 불리는 규칙 집합으로 정의될 수 있다.

배커스-나우르 표기법(BNF)은 언어의 "문장"의 구문을 설명한다. 이는 존 배커스가 개발했으며 알골 60의 구문에 사용되었다. 이 아이디어는 언어학자 노엄 촘스키의 문맥 자유 문법 개념에서 파생되었다. "BNF와 그 확장은 프로그래밍 표기법의 구문을 설명하는 표준 도구가 되었다. 많은 경우, 컴파일러의 일부는 BNF 설명에서 자동으로 생성된다."

1942년에서 1945년 사이에 콘라트 추제는 컴퓨터를 위한 최초의 (알고리즘) 프로그래밍 언어인 ("계획 계산")을 설계했다. 추제는 또한 ("계획 조립 장치")를 구상하여 프로그램의 수학적 공식을 기계가 읽을 수 있는 천공 필름으로 자동 번역했다. 1970년대까지 실제 구현은 이루어지지 않았지만, 1950년대 후반 켄 아이버슨이 설계한 APL에서 나중에 볼 수 있는 개념을 제시했다. APL은 수학적 계산을 위한 언어이다.

1949년부터 1951년까지 하인츠 루티샤우저는 수퍼플랜, 고급 언어 및 자동 번역기를 제안했다. 그의 아이디어는 나중에 프리드리히 L. 바우어와 클라우스 자멜손에 의해 정제되었다.

디지털 컴퓨팅 초기 단계의 고급 언어 설계는 다양한 응용 프로그램을 위한 유용한 프로그래밍 도구를 제공했다:
 포트란(Formula Translation)은 엔지니어링 및 과학 응용 프로그램을 위한 언어로, 실제로 구현된 최초의 고급 언어 중 하나이자 최초의 최적화 컴파일러로 간주된다.
 코볼(Common Business-Oriented Language)은 A-0 시스템과 FLOW-MATIC에서 발전하여 비즈니스 응용 프로그램을 위한 주요 고급 언어가 되었다.
 리스프(List Processor)는 기호 계산을 위한 언어이다.

컴파일러 기술은 고급 소스 프로그램을 디지털 컴퓨터를 위한 저급 대상 프로그램으로 엄격하게 정의된 변환의 필요성에서 발전했다. 컴파일러는 소스 코드 분석을 처리하는 프런트 엔드와 분석을 대상 코드로 합성하는 백 엔드로 볼 수 있었다. 프런트 엔드와 백 엔드 사이의 최적화는 더 효율적인 대상 코드를 생성할 수 있었다.

컴파일러 기술 개발의 몇 가지 초기 이정표:
 1952년 5월: 그레이스 호퍼의 레밍턴 랜드 팀은 A-0 시스템 프로그래밍 언어를 위한 컴파일러를 작성했으며(그리고 이를 설명하기 위해 컴파일러라는 용어를 만들었다), 비록 A-0 컴파일러는 현대적인 완전한 컴파일러의 개념보다는 로더 또는 링커에 더 가깝게 기능했지만.
 1952년 9월 이전: 맨체스터 마크 I 컴퓨터를 위해 맨체스터 대학교의 알릭 글레니가 개발한 오토코드 컴파일러는 일부 사람들에게는 최초의 컴파일된 프로그래밍 언어로 간주된다.
 1954–1957년: IBM의 존 배커스가 이끄는 팀은 최초의 고급 언어로 일반적으로 간주되는 FORTRAN을 개발했다. 1957년에 그들은 최초의 명확하게 완전한 컴파일러로 일반적으로 인정되는 FORTRAN 컴파일러를 완성했다.
 1959년: 데이터 시스템 언어 회의(CODASYL)는 코볼 개발을 시작했다. 코볼 디자인은 A-0와 FLOW-MATIC에 기반을 두었다. 1960년대 초까지 코볼은 여러 아키텍처에서 컴파일되었다.
 1958–1960년: 알골 58은 알골 60의 전신이었다. 이는 코드 블록을 도입하여 구조적 프로그래밍의 부상에 중요한 진전을 이루었다. 알골 60은 정적 영역 규칙을 갖는 중첩 함수 정의를 구현한 최초의 언어였다. 여기에는 재귀가 포함되었다. 그 구문은 BNF를 사용하여 정의되었다. 알골 60은 이후 많은 언어에 영감을 주었다. 토니 호어는 "이것은 이전 언어들뿐만 아니라 거의 모든 후속 언어들보다도 개선된 것이었다."라고 언급했다.
 1958–1962년: MIT의 존 매카시는 리스프를 설계했다. 기호 처리 기능은 인공지능 연구에 유용한 기능을 제공했다. 1962년 리스프 1.5 출시에서는 스티븐 러셀과 다니엘 J. 에드워즈가 작성한 인터프리터, 팀 하트와 마이크 레빈이 작성한 컴파일러 및 어셈블러와 같은 도구들이 언급되었다.

초기 운영체제와 소프트웨어는 어셈블리어로 작성되었다. 1960년대와 1970년대 초반에는 자원 제한으로 인해 시스템 프로그래밍에 고급 언어를 사용하는 것이 여전히 논란의 여지가 있었다. 그러나 BCPL, BLISS, B, C와 같은 고급 시스템 프로그래밍 언어로의 전환을 위한 여러 연구 및 산업 노력이 시작되었다.

BCPL(Basic Combined Programming Language)은 1966년 케임브리지 대학교의 마틴 리처즈가 컴파일러 작성 도구로 처음 개발했다. 여러 컴파일러가 구현되었으며, 리처즈의 책은 언어와 그 컴파일러에 대한 통찰력을 제공한다. BCPL은 여전히 연구에 사용되는 영향력 있는 시스템 프로그래밍 언어였을 뿐만 아니라 B 및 C 언어 설계의 기초를 제공했다.

BLISS(Basic Language for Implementation of System Software)는 디지털 이큅먼트 코퍼레이션(DEC)의 PDP-10 컴퓨터를 위해 W. A. 울프의 카네기 멜런 대학교(CMU) 연구팀이 개발했다. CMU 팀은 1년 후인 1970년에 BLISS-11 컴파일러를 개발했다.

멀틱스(Multiplexed Information and Computing Service)는 시분할 운영체제 프로젝트로, MIT, 벨 연구소, 제너럴 일렉트릭(나중에 허니웰)이 참여했으며 MIT의 페르난도 코바토가 이끌었다. 멀틱스는 IBM과 IBM 사용자 그룹이 개발한 PL/I 언어로 작성되었다. IBM의 목표는 비즈니스, 과학 및 시스템 프로그래밍 요구 사항을 충족하는 것이었다. 고려될 수 있는 다른 언어들도 있었지만, PL/I는 구현되지 않았음에도 불구하고 가장 완벽한 솔루션을 제공했다. 멀틱스 프로젝트의 처음 몇 년 동안, 언어의 일부 하위 집합은 벨 연구소의 더그 맥일로이와 밥 모리스가 만든 Early PL/I(EPL) 컴파일러를 사용하여 어셈블리어로 컴파일될 수 있었다. EPL은 전체 PL/I를 위한 부트스트래핑 컴파일러가 개발될 때까지 프로젝트를 지원했다.

벨 연구소는 1969년에 멀틱스 프로젝트에서 탈퇴하고, BCPL 개념에 기반을 둔 시스템 프로그래밍 언어 B를 데니스 리치와 켄 톰프슨이 작성했다. 리치는 B를 위한 부트스트래핑 컴파일러를 만들고 PDP-7을 위한 Unics(Uniplexed Information and Computing Service) 운영체제를 B로 작성했다. Unics는 결국 Unix로 철자가 바뀌었다.

벨 연구소는 B와 BCPL을 기반으로 C의 개발 및 확장을 시작했다. BCPL 컴파일러는 벨 연구소에 의해 멀틱스로 이식되었고 BCPL은 벨 연구소에서 선호하는 언어였다. 처음에는 C 컴파일러가 개발되는 동안 벨 연구소의 B 컴파일러의 프런트 엔드 프로그램이 사용되었다. 1971년에 새로운 PDP-11은 B에 대한 확장을 정의하고 컴파일러를 다시 작성할 자원을 제공했다. 1973년까지 C 언어의 설계는 본질적으로 완료되었고 PDP-11을 위한 유닉스 커널은 C로 다시 작성되었다. 스티브 존슨은 새로운 기계에 C 컴파일러를 리타겟팅하는 것을 지원하기 위해 휴대용 C 컴파일러(PCC) 개발을 시작했다.

객체 지향 프로그래밍(OOP)은 응용 프로그램 개발 및 유지 보수에 대한 흥미로운 가능성을 제시했다. OOP 개념은 더 이전으로 거슬러 올라가지만 리스프 및 시뮬라 언어 과학의 일부였다. 벨 연구소는 C++ 개발과 함께 OOP에 관심을 갖게 되었다. C++는 1980년에 시스템 프로그래밍에 처음 사용되었다. 초기 디자인은 C 언어 시스템 프로그래밍 기능을 시뮬라 개념과 결합했다. 객체 지향 기능은 1983년에 추가되었다. Cfront 프로그램은 C84 언어 컴파일러를 위한 C++ 프런트 엔드를 구현했다. 이후 몇 년 동안 C++ 인기가 높아지면서 여러 C++ 컴파일러가 개발되었다.

많은 응용 분야에서 고급 언어를 사용하려는 아이디어가 빠르게 확산되었다. 새로운 프로그래밍 언어가 지원하는 기능이 확장되고 컴퓨터 아키텍처의 복잡성이 증가함에 따라 컴파일러는 더욱 복잡해졌다.

방위고등연구계획국(DARPA)은 1970년에 울프의 CMU 연구팀과 함께 컴파일러 프로젝트를 후원했다. 프로덕션 품질 컴파일러-컴파일러 PQCC 설계는 소스 언어 및 대상을 형식적으로 정의하여 프로덕션 품질 컴파일러(PQC)를 생성할 예정이었다. PQCC는 전통적인 의미의 구문 분석기 생성기(예: Yacc)를 넘어 컴파일러-컴파일러라는 용어를 확장하려 했지만 큰 성공을 거두지 못했다. PQCC는 더 적절하게 컴파일러 생성기로 불릴 수 있다.

PQCC는 코드 생성 프로세스에 대한 연구에서 진정으로 자동화된 컴파일러 작성 시스템을 구축하려고 했다. 이 노력은 PQC의 단계 구조를 발견하고 설계했다. BLISS-11 컴파일러는 초기 구조를 제공했다. 단계에는 분석(프런트 엔드), 가상 머신으로의 중간 번역(미들 엔드), 대상으로의 번역(백 엔드)이 포함되었다. TCOL은 PQCC 연구를 위해 개발되었으며 중간 표현에서 언어 특정 구문을 처리한다. TCOL의 변형은 다양한 언어를 지원했다. PQCC 프로젝트는 자동화된 컴파일러 구성 기술을 연구했다. 이 설계 개념은 최적화 컴파일러와 Ada (1995년부터 객체 지향) 프로그래밍 언어용 컴파일러에 유용함이 입증되었다.

에이다 STONEMAN 문서는 커널(KAPSE) 및 최소(MAPSE)와 함께 프로그램 지원 환경(APSE)을 공식화했다. 에이다 인터프리터 NYU/ED는 미국 국가 표준 협회(ANSI) 및 국제 표준화 기구(ISO)와의 개발 및 표준화 노력을 지원했다. 미국 군대의 초기 에이다 컴파일러 개발에는 STONEMAN 문서에 따라 완전한 통합 설계 환경 내의 컴파일러가 포함되었다. 육군과 해군은 DEC/VAX 아키텍처를 목표로 하는 에이다 언어 시스템(ALS) 프로젝트를 진행했으며, 공군은 IBM 370 시리즈를 목표로 하는 에이다 통합 환경(AIE)을 시작했다. 이 프로젝트들이 원하는 결과를 제공하지는 못했지만, 에이다 개발을 위한 전반적인 노력에 기여했다.

다른 에이다 컴파일러 개발 노력은 영국 요크 대학교와 독일 카를스루에 대학교에서 시작되었다. 미국에서는 Verdix(나중에 Rational에 인수됨)가 육군에 Verdix Ada Development System(VADS)를 제공했다. VADS는 컴파일러를 포함한 개발 도구 세트를 제공했다. Unix/VADS는 DEC Ultrix 및 육군 CECOM 평가에서 Motorola 68020을 대상으로 하는 Sun 3/60 Solaris와 같은 다양한 유닉스 플랫폼에서 호스팅될 수 있었다. 곧 에이다 유효성 검사 테스트를 통과한 많은 에이다 컴파일러가 출시되었다. 자유 소프트웨어 재단 GNU 프로젝트는 GNU 컴파일러 모음(GCC)을 개발했는데, 이는 여러 언어와 대상을 지원하는 핵심 기능을 제공한다. 에이다 버전 GNAT는 가장 널리 사용되는 에이다 컴파일러 중 하나이다. GNAT는 무료이지만 상업적 지원도 있다. 예를 들어, AdaCore는 1994년에 에이다를 위한 상업적 소프트웨어 솔루션을 제공하기 위해 설립되었다. GNAT Pro는 GNU GCC 기반 GNAT에 도구 모음을 포함하여 통합 개발 환경을 제공한다.

고급 언어는 컴파일러 연구 및 개발을 계속 주도했다. 최적화 및 자동 코드 생성이 주요 초점 분야였다. 프로그래밍 언어 및 개발 환경의 추세는 컴파일러 기술에 영향을 미쳤다. 더 많은 컴파일러가 언어 배포판(PERL, Java Development Kit)에 포함되거나 IDE(VADS, Eclipse, Ada Pro)의 구성 요소로 포함되었다. 기술 간의 상호 관계 및 상호 의존성이 증가했다. 웹 서비스의 출현은 웹 언어 및 스크립팅 언어의 성장을 촉진했다. 스크립트는 사용자가 시스템에 의해 실행될 명령을 입력할 수 있었던 명령 줄 인터페이스(CLI)의 초기 시대로 거슬러 올라간다. 사용자 셸 개념은 셸 프로그램을 작성하는 언어와 함께 개발되었다. 초기 Windows 디자인은 간단한 배치 프로그래밍 기능을 제공했다. 이러한 언어의 전통적인 변환은 인터프리터를 사용했다. 널리 사용되지는 않지만 Bash 및 배치 컴파일러가 작성되었다. 더 최근에는 정교한 인터프리트 언어가 개발자 도구 키트의 일부가 되었다. 현대 스크립팅 언어에는 PHP, Python, Ruby 및 Lua가 포함된다. (Lua는 게임 개발에 널리 사용된다.) 이들 모두 인터프리터 및 컴파일러 지원을 받는다.

"1950년대 후반 컴파일 분야가 시작되었을 때, 그 초점은 고급 언어 프로그램을 기계 코드로 번역하는 데 국한되었다... 컴파일러 분야는 컴퓨터 아키텍처, 프로그래밍 언어, 형식 방법, 소프트웨어 공학, 컴퓨터 보안을 포함한 다른 분야와 점점 더 밀접하게 얽혀 있다." "Compiler Research: The Next 50 Years" 기사는 객체 지향 언어와 자바의 중요성을 강조했다. 보안과 병렬 컴퓨팅이 미래 연구 목표로 언급되었다.

 컴파일러 구성 

컴파일러는 고급 소스 프로그램에서 저급 대상 프로그램으로의 형식적 변환을 구현한다. 컴파일러 설계는 종단 간 솔루션을 정의하거나, 전처리기, 어셈블러, 링커와 같은 다른 컴파일 도구와 인터페이스하는 정의된 하위 집합을 다룰 수 있다. 설계 요구사항에는 컴파일러 구성 요소 간의 내부적 인터페이스와 지원 도구 세트 간의 외부적 인터페이스가 엄격하게 정의되어야 한다.

초기에는 컴파일러 설계에 대한 접근 방식이 처리할 컴퓨터 언어의 복잡성, 설계하는 사람의 경험 및 사용 가능한 리소스에 직접적인 영향을 받았다. 리소스 제한으로 인해 소스 코드를 여러 번 통과해야 하는 필요성이 발생했다.

한 사람이 작성한 비교적 간단한 언어를 위한 컴파일러는 단일의 모놀리식 소프트웨어 조각일 수 있다. 그러나 소스 언어가 복잡해짐에 따라 설계는 여러 상호 의존적인 단계로 나뉠 수 있다. 개별 단계는 컴파일 과정의 기능에 개발을 집중시키는 설계 개선을 제공한다.

 원패스 컴파일러 대 다중 패스 컴파일러 
컴파일러를 패스 수로 분류하는 것은 컴퓨터의 하드웨어 리소스 제한에 배경을 둔다. 컴파일은 많은 작업을 수행하는 것을 포함하며, 초기 컴퓨터는 이 모든 작업을 수행하는 하나의 프로그램을 담을 만큼 충분한 메모리가 없었다. 결과적으로 컴파일러는 소스(또는 그 표현)를 한 번씩 통과하여 필요한 분석 및 변환의 일부를 수행하는 더 작은 프로그램으로 분할되었다.

단일 패스로 컴파일할 수 있는 능력은 컴파일러를 작성하는 작업을 단순화하고 원패스 컴파일러가 일반적으로 다중 패스 컴파일러보다 컴파일을 더 빠르게 수행하기 때문에 전통적으로 이점으로 여겨져 왔다. 따라서 초기 시스템의 리소스 제한에 부분적으로 기인하여 많은 초기 언어는 단일 패스로 컴파일될 수 있도록 특별히 설계되었다(예: 파스칼).

어떤 경우에는 언어 기능의 설계가 컴파일러에게 소스를 여러 번 통과하도록 요구할 수 있다. 예를 들어, 소스의 20행에 나타나는 선언이 10행에 나타나는 문장의 변환에 영향을 미치는 경우를 고려해 보자. 이 경우 첫 번째 패스는 문장 이후에 나타나는 선언에 대한 정보를 수집해야 하며, 실제 변환은 후속 패스 중에 발생한다.

단일 패스로 컴파일하는 것의 단점은 고품질 코드를 생성하는 데 필요한 많은 정교한 최적화를 수행할 수 없다는 것이다. 최적화 컴파일러가 몇 번의 패스를 수행하는지 정확히 세는 것은 어려울 수 있다. 예를 들어, 최적화의 다른 단계는 하나의 표현식을 여러 번 분석하지만 다른 표현식은 한 번만 분석할 수 있다.

컴파일러를 작은 프로그램으로 나누는 것은 입증 가능한 올바른 컴파일러를 생산하는 데 관심이 있는 연구자들이 사용하는 기술이다. 작은 프로그램 세트의 정확성을 증명하는 것은 더 크고 단일이며 동등한 프로그램의 정확성을 증명하는 것보다 종종 노력이 덜 필요하다.

 3단계 컴파일러 구조 
섬네일|center|upright=2.5|컴파일러 설계
컴파일러 설계의 정확한 단계 수와 관계없이 단계는 세 가지 단계 중 하나에 할당될 수 있다. 이 단계에는 프런트 엔드, 미들 엔드, 백 엔드가 포함된다.
 프런트 엔드는 특정 소스 언어에 따라 입력을 스캔하고 구문과 의미를 확인한다. 정적 자료형 언어의 경우 자료형 정보를 수집하여 형식 검사를 수행한다. 입력 프로그램에 구문 오류나 자료형 오류가 있으면 오류 및 경고 메시지를 생성하며, 일반적으로 문제가 감지된 소스 코드의 위치를 식별한다. 경우에 따라 실제 오류는 프로그램의 (훨씬) 이전에 있을 수 있다. 프런트 엔드의 측면에는 낱말 분석, 구문 분석, 의미 분석이 포함된다. 프런트 엔드는 입력 프로그램을 중간 표현 (IR)으로 변환하여 미들 엔드에서 추가 처리한다. 이 IR은 일반적으로 소스 코드에 비해 프로그램의 더 낮은 수준의 표현이다.
 미들 엔드는 대상이 되는 CPU 아키텍처와 독립적인 IR에 대한 최적화를 수행한다. 이 소스 코드/기계 코드 독립성은 다른 언어 및 대상 프로세서를 지원하는 컴파일러 버전 간에 일반적인 최적화를 공유할 수 있도록 의도되었다. 미들 엔드 최적화의 예로는 쓸모없거나(불필요한 코드 제거) 도달 불가능한 코드 제거(도달 가능성 분석), 상수 값 발견 및 전파(상수 전파), 계산을 덜 자주 실행되는 위치(예: 루프 외부)로 재배치하거나, 컨텍스트에 기반한 계산의 특수화 등이 있으며, 결국 백 엔드에서 사용되는 "최적화된" IR을 생성한다.
 백 엔드는 미들 엔드에서 최적화된 IR을 가져온다. 이는 대상 CPU 아키텍처에 특정한 추가 분석, 변환 및 최적화를 수행할 수 있다. 백 엔드는 레지스터 할당을 수행하면서 대상 종속 어셈블리 코드를 생성한다. 백 엔드는 명령어 스케줄링을 수행하여 지연 슬롯을 채워 병렬 실행 장치를 계속 바쁘게 만들기 위해 명령어를 재정렬한다. 대부분의 최적화 문제는 NP-난해이지만, 이를 해결하기 위한 휴리스틱 기술은 잘 개발되어 생산 품질 컴파일러에 구현되어 있다. 일반적으로 백 엔드의 출력은 특정 프로세서 및 운영 체제에 특화된 기계어이다.

이 프런트/미들/백 엔드 접근 방식은 서로 다른 언어의 프런트 엔드를 서로 다른 CPU의 백 엔드와 결합하면서 미들 엔드의 최적화를 공유할 수 있게 한다. 이 접근 방식의 실제 예로는 GNU 컴파일러 모음, 클랭 (LLVM 기반 C/C++ 컴파일러) 및 암스테르담 컴파일러 키트가 있으며, 이들은 여러 프런트 엔드, 공유 최적화 및 여러 백 엔드를 가지고 있다.

 프런트 엔드 
섬네일|렉서와 파서의 C 예시. 문자 시퀀스 "if(net>0.0)total+=net*(1.0+tax/100.0);"에서 시작하여 스캐너는 토큰 시퀀스를 구성하고 각 토큰을 예를 들어 , , 또는 로 분류한다. 후자의 시퀀스는 파서에 의해 구문 트리로 변환된 다음 나머지 컴파일러 단계에 의해 처리된다. 스캐너와 파서는 정규 문법과 적절히 문맥 자유 문법의 C 문법 부분을 각각 처리한다.

프런트 엔드는 소스 코드를 분석하여 프로그램의 내부 표현(IR)을 구축한다. 또한 심볼 테이블을 관리하는데, 이는 소스 코드의 각 심볼을 위치, 자료형, 스코프와 같은 관련 정보에 매핑하는 자료 구조이다.

프런트 엔드는 스캐너리스 파서처럼 단일 모놀리식 함수나 프로그램일 수 있지만, 전통적으로는 순차적으로 또는 동시에 실행될 수 있는 여러 단계로 구현되고 분석되었다. 이 방법은 모듈성과 관심사 분리 때문에 선호된다. 가장 일반적으로 프런트 엔드는 낱말 분석 (렉싱 또는 스캐닝이라고도 함), 구문 분석 (스캐닝 또는 파싱이라고도 함), 그리고 의미 분석의 세 단계로 나뉜다. 렉싱과 파싱은 구문 분석(각각 단어 구문과 구절 구문)을 구성하며, 간단한 경우 이러한 모듈(렉서와 파서)은 언어의 문법에서 자동으로 생성될 수 있지만, 더 복잡한 경우에는 수동 수정이 필요하다. 어휘 문법과 구문 문법은 일반적으로 문맥 자유 문법이며, 이는 분석을 크게 단순화하며, 문맥 민감성은 의미 분석 단계에서 처리된다. 의미 분석 단계는 일반적으로 더 복잡하고 수동으로 작성되지만, 속성 문법을 사용하여 부분적으로 또는 완전히 자동화될 수 있다. 이 단계들은 다시 더 세분화될 수 있다: 스캐닝 및 평가로서의 렉싱, 그리고 구체 구문 트리 (CST, 파스 트리)를 구축한 다음 이를 추상 구문 트리 (AST, 구문 트리)로 변환하는 것으로서의 파싱. 어떤 경우에는 라인 재구성 및 전처리(preprocessing)와 같은 추가 단계가 사용되지만, 이는 드물다.

프런트 엔드의 주요 단계는 다음과 같다:
 은 입력 문자 시퀀스를 파서가 처리할 수 있는 정규 형식으로 변환한다. 키워드를 스트로핑하거나 식별자 내에 임의의 공백을 허용하는 언어는 이 단계가 필요하다. 1960년대에 사용된 하향식, 재귀 하향, 테이블 구동 파서는 일반적으로 소스를 한 번에 한 문자씩 읽었으며 별도의 토큰화 단계가 필요하지 않았다. 아틀라스 오토코드와 임프 (및 일부 알골 및 코랄 66 구현)는 컴파일러가 라인 재구성 단계를 가질 스트로핑된 언어의 예이다.
 전처리는 매크로 대체 및 조건부 컴파일을 지원한다. 일반적으로 전처리 단계는 구문 또는 의미 분석 이전에 발생한다. 예를 들어 C의 경우, 전처리기는 구문 형태가 아닌 어휘 토큰을 조작한다. 그러나 스킴과 같은 일부 언어는 구문 형태에 기반한 매크로 대체를 지원한다.
 낱말 분석 (렉싱 또는 토큰화라고도 함)은 소스 코드 텍스트를 어휘 토큰이라는 작은 조각들의 시퀀스로 분해한다. 이 단계는 두 가지 단계로 나눌 수 있다: 입력 텍스트를 렉셈이라고 하는 구문 단위로 분할하고 범주를 할당하는 스캐닝; 그리고 렉셈을 처리된 값으로 변환하는 평가. 토큰은 토큰 이름과 선택적 토큰 값으로 구성된 쌍이다. 일반적인 토큰 범주에는 식별자, 키워드, 구분자, 연산자, 리터럴 및 주석이 포함될 수 있지만, 토큰 범주의 집합은 프로그래밍 언어마다 다르다. 렉셈 구문은 일반적으로 정규 언어이므로 정규 표현식에서 구성된 유한 상태 자동자를 사용하여 인식할 수 있다. 낱말 분석을 수행하는 소프트웨어를 낱말 분석기라고 한다. 이것은 별도의 단계가 아닐 수 있다. 스캐너리스 파싱에서 파싱 단계와 결합될 수 있으며, 이 경우 파싱은 토큰 수준이 아니라 문자 수준에서 수행된다.
 구문 분석 (파싱이라고도 함)은 토큰 시퀀스를 구문 분석하여 프로그램의 구문 구조를 식별하는 것을 포함한다. 이 단계는 일반적으로 언어의 구문을 정의하는 형식 문법 규칙에 따라 구축된 트리 구조로 선형 토큰 시퀀스를 대체하는 파스 트리를 구축한다. 파스 트리는 종종 컴파일러의 후반 단계에서 분석, 증강 및 변환된다.
 의미 분석은 파스 트리에 의미론적 정보를 추가하고 심볼 테이블을 구축한다. 이 단계는 타입 검사 (타입 오류 확인), 객체 바인딩 (변수 및 함수 참조를 해당 정의와 연결), 또는 확정 할당 (모든 지역 변수가 사용 전에 초기화되어야 함)과 같은 의미론적 검사를 수행하여 잘못된 프로그램을 거부하거나 경고를 발행한다. 의미 분석은 일반적으로 완전한 파스 트리를 필요로 하며, 이는 이 단계가 파싱 단계 다음에 논리적으로 오고, 코드 생성 단계에 앞서 논리적으로 오지만, 컴파일러 구현에서 여러 단계를 코드에 대한 한 번의 패스로 통합하는 것이 종종 가능하다.

 미들 엔드 
미들 엔드, 또는 최적화 도구는 생성된 기계 코드의 성능과 품질을 향상시키기 위해 중간 표현에 대한 최적화를 수행한다. 미들 엔드는 대상 CPU 아키텍처와 독립적인 최적화를 포함한다.

미들 엔드의 주요 단계는 다음과 같다:
 분석: 이것은 입력에서 파생된 중간 표현으로부터 프로그램 정보를 수집하는 단계이다. 데이터 흐름 분석은 사용-정의 체인을 구축하는 데 사용되며, 의존성 분석, 별칭 분석, 포인터 분석, 이스케이프 분석 등과 함께 사용된다. 정확한 분석은 모든 컴파일러 최적화의 기초이다. 컴파일된 모든 함수의 제어 흐름 그래프와 프로그램의 호출 그래프도 일반적으로 분석 단계에서 구축된다.
 최적화: 중간 언어 표현이 기능적으로 동등하지만 더 빠르거나 (또는 더 작거나) 다른 형태로 변환된다. 인기 있는 최적화는 인라인 확장, 불필요한 코드 제거, 상수 전파, 루프 변환 및 심지어 자동 병렬화이다.
컴파일러 분석은 모든 컴파일러 최적화의 전제 조건이며, 이들은 긴밀하게 협력한다. 예를 들어, 의존성 분석은 루프 변환에 중요하다.

컴파일러 분석 및 최적화의 범위는 크게 다르다. 그 범위는 기본 블록 내에서 작동하는 것부터 전체 프로시저, 심지어 전체 프로그램까지 다양할 수 있다. 최적화의 세분성과 컴파일 비용 사이에는 상충 관계가 존재한다. 예를 들어, 핍홀 최적화는 컴파일 중에 빠르게 수행되지만 코드의 작은 지역 부분에만 영향을 미치며, 코드 조각이 나타나는 컨텍스트와 독립적으로 수행될 수 있다. 이와 대조적으로, 프로시저 간 최적화는 더 많은 컴파일 시간과 메모리 공간을 필요로 하지만, 여러 함수의 동작을 동시에 고려함으로써만 가능한 최적화를 가능하게 한다.

현대 상업용 컴파일러인 HP, IBM, SGI, 인텔, 마이크로소프트, 그리고 썬 마이크로시스템즈에서는 프로시저 간 분석 및 최적화가 일반적이다. 자유 소프트웨어인 GCC는 강력한 프로시저 간 최적화가 부족하다는 비판을 오랫동안 받아왔지만, 이 점에 있어서 변화하고 있다. 완전한 분석 및 최적화 인프라를 갖춘 또 다른 오픈 소스 컴파일러는 오픈64이며, 이는 많은 조직에서 연구 및 상업적 목적으로 사용된다.

컴파일러 분석 및 최적화에 필요한 추가 시간과 공간 때문에 일부 컴파일러는 기본적으로 이를 건너뛴다. 사용자는 어떤 최적화를 활성화할지 컴파일러에 명시적으로 알리기 위해 컴파일 옵션을 사용해야 한다.

 백 엔드 
백 엔드는 CPU 아키텍처별 최적화와 코드 생성을 담당한다.

백 엔드의 주요 단계는 다음과 같다:
 기계 종속 최적화: 컴파일러가 목표로 하는 CPU 아키텍처의 세부 사항에 의존하는 최적화이다. 대표적인 예는 핍홀 최적화로, 짧은 어셈블러 명령어 시퀀스를 더 효율적인 명령어로 재작성한다.
 코드 생성: 변환된 중간 언어가 출력 언어로 번역되는데, 일반적으로 시스템의 기본 기계어이다. 여기에는 레지스터와 메모리에 어떤 변수를 배치할지 결정하고, 적절한 기계 명령어와 관련 주소 지정 방식을 명령어 선택 및 명령어 스케줄링하는 등 리소스 및 저장소 결정이 포함된다 (또한 세티-울만 알고리즘 참조). 디버그를 용이하게 하기 위해 디버그 데이터도 생성해야 할 수 있다.

 컴파일러 정확성 

컴파일러 정확성은 컴파일러가 언어 사양에 따라 작동함을 입증하려는 소프트웨어 공학 분야이다. 기술에는 정형 기법을 사용하여 컴파일러를 개발하고 기존 컴파일러에 대한 엄격한 테스트(종종 컴파일러 검증이라고 함)를 사용하는 것이 포함된다.

 컴파일 언어 대 인터프리트 언어 

고급 프로그래밍 언어는 일반적으로 특정 번역 방식을 염두에 두고 나타난다. 즉, 컴파일 언어 또는 인터프리트 언어로 설계된다. 그러나 실제로는 언어가 전적으로 컴파일되거나 전적으로 인터프리트되어야 하는 경우는 거의 없지만, 런타임에 재해석에 의존하는 언어를 설계하는 것은 가능하다. 이러한 분류는 일반적으로 해당 언어의 가장 인기 있거나 널리 사용되는 구현을 반영한다. 예를 들어, 베이직은 때때로 인터프리트 언어라고 불리고, C는 컴파일 언어라고 불리지만, 베이직 컴파일러와 C 인터프리터의 존재에도 불구하고 그렇다.

인터프리팅이 컴파일을 완전히 대체하지는 않는다. 이는 사용자로부터 컴파일을 숨기고 점진적으로 만든다. 인터프리터 자체를 인터프리트할 수 있더라도, 실행 스택의 맨 아래 어딘가에는 직접 실행되는 기계어 명령어 세트가 필요하다 (기계어 참조).

더 나아가, 최적화를 위해 컴파일러는 인터프리터 기능을 포함할 수 있고, 인터프리터는 선행 컴파일 기술을 포함할 수 있다. 예를 들어, 컴파일 중에 표현식이 실행될 수 있고 그 결과가 출력 프로그램에 삽입될 수 있다면, 프로그램이 실행될 때마다 재계산될 필요가 없어 최종 프로그램을 크게 가속화할 수 있다. JIT 컴파일과 바이트코드 인터프리팅을 향한 현대적인 경향은 컴파일러와 인터프리터의 전통적인 분류를 더욱 모호하게 만들기도 한다. 메타 트레이싱은 이를 더욱 발전시킨 자동화된 컴파일러 합성 접근 방식으로, 언어 인터프리터에서 컴파일러를 합성하는 데 사용될 수 있다.

일부 언어 사양은 구현에 컴파일 기능을 포함해야 한다고 명시한다. 예를 들어, 커먼 리스프가 그렇다. 그러나 커먼 리스프의 정의 자체에 인터프리트되는 것을 막는 내재된 것은 없다. 다른 언어들은 인터프리터에서 구현하기는 매우 쉽지만, 컴파일러를 작성하기는 훨씬 어렵게 만드는 특징을 가지고 있다. 예를 들어, APL, 스노볼4, 그리고 많은 스크립팅 언어는 프로그램이 런타임에 일반 문자열 연산을 사용하여 임의의 소스 코드를 구성한 다음, 이를 특별한 평가 함수에 전달하여 해당 코드를 실행하도록 허용한다. 컴파일 언어에서 이러한 기능을 구현하려면 일반적으로 컴파일러 자체의 버전을 포함하는 런타임 라이브러리와 함께 프로그램이 제공되어야 한다.

 종류 
컴파일러의 한 분류는 생성된 코드가 실행되는 플랫폼에 따른 것이다. 이를 대상 플랫폼이라고 한다.

네이티브 또는 호스티드 컴파일러는 컴파일러 자체가 실행되는 것과 동일한 유형의 컴퓨터 및 운영 체제에서 출력이 직접 실행되도록 의도된 것이다. 크로스 컴파일러의 출력은 다른 플랫폼에서 실행되도록 설계되었다. 크로스 컴파일러는 소프트웨어 개발 환경을 지원하지 않도록 의도된 임베디드 시스템용 소프트웨어를 개발할 때 자주 사용된다.

가상 머신 (VM)용 코드를 생성하는 컴파일러의 출력은 해당 컴파일러를 생성한 것과 동일한 플랫폼에서 실행될 수도 있고 아닐 수도 있다. 이러한 이유로 이러한 컴파일러는 일반적으로 네이티브 또는 크로스 컴파일러로 분류되지 않는다.

컴파일러의 대상이 되는 저급 언어 자체가 고급 프로그래밍 언어일 수 있다. 일부는 휴대용 어셈블리어의 일종으로 보는 C는 종종 이러한 컴파일러의 대상 언어이다. 예를 들어, C++의 초기 컴파일러인 C프론트는 C를 대상 언어로 사용했다. 이러한 컴파일러가 생성하는 C 코드는 일반적으로 사람이 읽고 유지 보수하기 위한 것이 아니므로, 들여쓰기 스타일과 예쁜 C 중간 코드 생성은 무시된다. C가 좋은 대상 언어가 되게 하는 기능 중 일부는 컴파일러가 원래 소스 디버그를 지원하기 위해 생성할 수 있는 `#line` 지시어와 C 컴파일러에서 사용할 수 있는 광범위한 플랫폼 지원을 포함한다.

일반적인 컴파일러 유형은 기계어를 출력하지만, 다른 많은 유형이 있다:
 소스 대 소스 컴파일러는 고급 언어를 입력으로 받아 고급 언어를 출력하는 컴파일러 유형이다. 예를 들어, 자동 병렬화 컴파일러는 종종 고급 언어 프로그램을 입력으로 받아 코드를 변환하고 병렬 코드 주석(예: OpenMP) 또는 언어 구성 요소(예: 포트란의 `DOALL` 문)를 추가한다. 소스 대 소스 컴파일러의 다른 용어로는 트랜스컴파일러 또는 트랜스파일러가 있다.
 바이트코드 컴파일러는 일부 프롤로그 구현과 같이 이론적인 기계의 어셈블리어로 컴파일한다.
 이 프롤로그 기계는 워런 추상 기계 (WAM)로도 알려져 있다.
 자바, 파이썬용 바이트코드 컴파일러도 이 범주의 예이다.
 JIT 컴파일러 (JIT 컴파일러)는 런타임까지 컴파일을 연기한다. JIT 컴파일러는 파이썬, 자바스크립트, 스몰토크, 자바, 마이크로소프트 .NET의 공통 중간 언어 (CIL) 등 많은 현대 언어에서 존재한다. JIT 컴파일러는 일반적으로 인터프리터 내에서 실행된다. 인터프리터가 코드 경로가 "핫"하다는 것을 감지하면, 즉 자주 실행될 때, JIT 컴파일러가 호출되어 "핫" 코드를 컴파일하여 성능을 향상시킨다.
 자바와 같은 일부 언어의 경우, 애플리케이션은 먼저 바이트코드 컴파일러를 사용하여 컴파일되고 기계 독립적인 중간 표현으로 제공된다. 바이트코드 인터프리터는 바이트코드를 실행하지만, 성능 향상이 필요할 때 JIT 컴파일러는 바이트코드를 기계어로 번역한다.
 하드웨어 컴파일러 (합성 도구라고도 함)는 하드웨어 기술 언어를 입력으로 받고 넷리스트 또는 다른 형태로 하드웨어 구성에 대한 설명을 출력하는 컴파일러이다.
 이 컴파일러의 출력은 매우 낮은 수준의 컴퓨터 하드웨어를 대상으로 하며, 예를 들어 FPGA 또는 구조화된 응용 주문형 집적회로이다. 이러한 컴파일러는 하드웨어 컴파일러라고 불리는데, 컴파일하는 소스 코드가 하드웨어의 최종 구성과 작동 방식을 효과적으로 제어하기 때문이다. 컴파일의 출력은 트랜지스터 또는 순람표의 상호 연결일 뿐이다.
 하드웨어 컴파일러의 예로는 FPGA 구성을 위해 사용되는 Xilinx Synthesis Tool인 XST가 있다. 유사한 도구는 알테라, Synplicity, Synopsys 및 기타 하드웨어 공급업체에서 제공된다.
 연구 시스템은 파이썬이나 C++와 같은 고급 직렬 언어의 하위 집합을 병렬 디지털 로직으로 직접 컴파일한다. 이는 일반적으로 함수형 언어 또는 다중 패러다임 언어의 함수형 하위 집합에 대해 더 쉽게 수행할 수 있다.
 저급 언어에서 고급 언어로 번역하는 프로그램은 역컴파일러이다.
 컴파일 머신에서 지원되지 않는 목적 파일 형식으로 번역하는 프로그램은 크로스 컴파일러라고 불리며, 임베디드 소프트웨어 애플리케이션에서 실행하기 위한 코드를 준비하는 데 일반적으로 사용된다.
 최적화 및 변환을 적용하면서 목적 파일을 동일한 유형의 목적 파일로 다시 작성하는 프로그램은 바이너리 재컴파일러이다.

사람이 읽을 수 있는 어셈블리어를 하드웨어가 실행하는 기계어 명령어로 번역하는 어셈블러는 컴파일러로 간주되지 않는다. (기계어를 어셈블리어로 번역하는 역 프로그램은 역어셈블러라고 한다.)

 참고 자료 
 Cooper, Keith D. & Linda Torczon, Engineering a compiler, Morgan Kaufmann, 2004, pp 1-8.

 각주 

 내용주

 외부 링크 
 

 
분류:미국의 발명품
분류:프로그래밍 언어 구현
분류:유틸리티 소프트웨어 종류
분류:컴파일러 구성