자체 프로그래밍 언어를 설계 할 때 gcc와 같은 기존 컴파일러를 사용하여 기계 코드로 끝날 수 있도록 소스 코드를 가져 와서 C 또는 C ++ 코드로 변환하는 변환기를 작성하는 것이 언제 합리적입니까? 이 접근법을 사용하는 프로젝트가 있습니까?
자체 프로그래밍 언어를 설계 할 때 gcc와 같은 기존 컴파일러를 사용하여 기계 코드로 끝날 수 있도록 소스 코드를 가져 와서 C 또는 C ++ 코드로 변환하는 변환기를 작성하는 것이 언제 합리적입니까? 이 접근법을 사용하는 프로젝트가 있습니까?
답변:
C 코드로 번역하는 것은 매우 잘 알려진 습관입니다. 클래스가있는 원래 C와 초기 C ++ 구현 ( Cfront 라고 함 )이 성공적으로 수행했습니다. Lisp 또는 Scheme의 몇 가지 구현, 예를 들어 Chicken Scheme , Scheme48 , Bigloo가 있습니다. 어떤 사람들은 Prolog를 C로 번역했습니다 . 그리고 Mozart의 일부 버전도 마찬가지였습니다 (그리고 Ocaml 바이트 코드를 C 로 컴파일하려고 시도했습니다 ). J.Pitrat의 인공 지능 CAIA 시스템 도 부트 스트랩되어 모든 C 코드를 생성합니다. Vala 는 GTK 관련 코드를 위해 C로도 변환합니다. Queinnec의 저술 Lisp In Small Pieces C 로의 번역에 관한 장이 있습니다.
C로 변환 할 때 발생하는 문제 중 하나는 꼬리 재귀 호출 입니다. C 표준은 (는 "인수 점프"에, 즉 C 컴파일러가 제대로 번역되어 있지 보증을하지 않고 의 경우에도 호출 스택을 먹고) 일부 의 경우, GCC (또는 연타의 / LLVM)의 최신 버전은 그 최적화를 할 .
또 다른 문제는 가비지 수집 입니다. 여러 구현에서는 Boehm 보수 가비지 수집기 ( C 친화적 인 ...)를 사용합니다. 악의적 일 수있는 코드를 가비지 수집 (예 : SBCL과 같은 여러 Lisp 구현과 같이)하려는 경우 ( dlclose
Posix에서 원합니다 ).
또 다른 문제는 일류 연속 과 call / cc 처리하는 것 입니다. 그러나 영리한 트릭이 가능합니다 (치킨 계획 내부를보십시오). 콜 스택에 액세스하려면 많은 트릭이 필요할 수 있습니다 (그러나 GNU backtrace 등 참조 ). 연속체 (즉, 스택 또는 스레드)의 직교 지속성 은 C에서 어렵다.
예외 처리는 종종 longjmp 등을 영리하게 호출하는 문제입니다 .
적절한 #line
지시문 을 생성 (방출 된 C 코드로) 할 수 있습니다. 이것은 지루하고 많은 작업이 필요합니다 (예 : 더 쉽게 gdb
디버그 가능한 코드를 생성하는 것이 좋습니다).
내 MELT Lisp 다운 도메인 특정 언어는 (사용자 정의하거나 확장하는 GCC를 ) (지금 ++ 실제로 가난한 C까지) C로 변환됩니다. 자체 생성 가비지 수집기가 있습니다. ( Qish 또는 Ravenbrook MPS에 관심이있을 수 있습니다 ). 실제로, 세대 별 GC는 수작업으로 작성된 C 코드보다 기계로 생성 된 C 코드에서 더 쉽습니다 (쓰기 장벽 및 GC 기계에 맞게 C 코드 생성기를 조정하기 때문에).
나는 진정한 C ++ 코드로 번역하는 언어 구현을 모른다 . 즉, 많은 "STL 템플릿을 사용하고 RAII 관용구를 존중하는 C ++ 코드를 생성하는"컴파일 타임 가비지 수집 "기술을 사용한다 . (알고 있다면 알려주세요).
오늘날 재미있는 것은 (현재 Linux 데스크톱에서) C 컴파일러는 C로 변환 된 대화식 최상위 읽기-판독-인쇄-루프 를 구현하기에 충분히 빠를 수 있다는 것입니다. 모든 사용자에서 C 코드 (수백 줄)를 방출합니다 상호 작용할 fork
때 공유 객체로 컴파일 한 다음 dlopen
. (MELT는 모든 것을 준비하고 있으며 일반적으로 충분히 빠릅니다). 이 모든 것이 수십 분의 1 초가 걸리고 최종 사용자가 수용 할 수 있습니다.
가능하면 C ++ 컴파일이 느리기 때문에 C ++가 아닌 C로 변환하는 것이 좋습니다.
언어를 구현하는 경우 libjit , GNU lightning , asmjit 또는 LLVM 또는 GCCJIT 와 같은 일부 JIT 라이브러리를 C 코드를 내보내는 대신 고려할 수도 있습니다 . C로 번역하고 싶다면 때때로 tinycc를 사용할 수 있습니다 . 생성 된 C 코드 (메모리에서도)를 매우 빠르게 컴파일하여 머신 코드 를 느리게 합니다. 그러나 일반적으로 GCC 와 같은 실제 C 컴파일러가 수행 하는 최적화를 활용하려고합니다.
C 언어로 번역하는 경우 생성 된 C 코드 의 전체 AST 를 메모리에 먼저 빌드해야합니다 (이렇게하면 모든 선언, 모든 정의 및 함수 코드를 먼저 생성하는 것이 더 쉬워집니다). 이런 식으로 일부 최적화 / 정규화를 수행 할 수 있습니다. 또한 여러 GCC 확장 (예 : 계산 된 gotos)에 관심이있을 수 있습니다 . C 컴파일러를 최적화하는 것은 매우 큰 C 함수에 실제로 불만족하기 때문에 거대한 C 함수 (예 : 수십만 줄의 생성 된 C)를 생성 하지 않으려 할 것입니다 (실제로 실험적으로gcc -O
큰 함수의 컴파일 시간은 함수 코드 크기의 제곱에 비례합니다). 따라서 생성 된 C 함수의 크기를 각각 수천 줄로 제한하십시오.
공지 사항이 모두 연타 (통해 LLVM 과) GCC (를 통해 libgccjit C & C는 ++ 컴파일러이 컴파일러에 적합 일부 내부 표현을 방출 할 수있는 방법을 제공하지만, 힘이 (여부) 열심히 C (또는 C ++) 코드를 방출보다 수 있도록하고) 각 컴파일러마다 다릅니다.
C로 번역 될 언어를 디자인하는 경우, 언어와 C의 혼합을 생성하는 몇 가지 트릭 (또는 구성)이 필요할 수 있습니다. DSL2011 논문 MELT : GCC 컴파일러에 포함 된 번역 된 도메인 특정 언어가 유용한 힌트를 제공해야합니다.
전체 머신 코드를 생성하는 시간이 C 컴파일러를 사용하여 "IL"을 머신 코드로 컴파일하는 중간 단계를 수행해야하는 불편 함을 능가하는 것이 합리적입니다.
일반적으로 도메인 별 언어는 이러한 방식으로 작성되며, 실행 가능한 파일 또는 dll로 컴파일되는 프로세스를 정의하거나 설명하는 데 매우 높은 수준의 시스템이 사용됩니다. 작업 / 양호한 어셈블리를 생성하는 데 걸리는 시간은 C를 생성하는 것보다 훨씬 길며 C는 성능을 위해 어셈블리 코드와 매우 유사하므로 C를 생성하고 C 컴파일러 작성자의 기술을 재사용하는 것이 좋습니다. gcc 또는 llvm을 작성하는 사람들은 최적화 된 기계어 코드를 만드는 데 많은 시간을 들였으므로 모든 노력을 다시 시도하는 것은 어려울 것입니다.
IIRC가 언어 중립적 인 LLVM의 컴파일러 백엔드를 재사용하는 것이 더 받아 들일 수 있으므로 C 코드 대신 LLVM 명령어를 생성합니다.
머신 코드를 생성하기 위해 컴파일러를 작성하는 것은 C를 생성하는 컴파일러를 작성하는 것 (어쩌면 더 쉬울 수 있음)을 작성하는 것보다 훨씬 어렵지 않지만 머신 코드를 생성하는 컴파일러는 특정 플랫폼에서 실행 가능한 프로그램 만 생성 할 수 있습니다. 그것은 쓰여졌다; 대조적으로 C 코드를 생성하는 컴파일러는 생성 된 코드가 지원하도록 설계된 C의 방언을 사용하는 모든 플랫폼에 대한 프로그램을 생성 할 수 있습니다. 많은 경우 C 표준으로 보장되지 않는 동작을 사용하지 않고 완전히 이식 가능하고 원하는대로 동작하는 C 코드를 작성할 수 있지만 플랫폼 보장 동작에 의존하는 코드는 훨씬 빠르게 실행될 수 있습니다. 그렇지 않은 코드보다 보장하는 플랫폼에서.
예를 들어, 언어가 빅 엔디안 방식으로 해석되어 UInt32
임의로 정렬 된 4 개의 연속 바이트 를 생성하는 기능을 지원한다고 가정합니다 UInt8[]
. 일부 컴파일러에서는 다음과 같이 코드를 작성할 수 있습니다.
uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));
컴파일러가 워드로드 작업을 수행 한 다음 워드 단위의 바이트 단위 명령을 생성하도록합니다. 그러나 일부 컴파일러는 __packed 수정자를 지원하지 않으며 부재시 작동하지 않는 코드를 생성합니다.
또는 코드를 다음과 같이 작성할 수 있습니다.
return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);
이러한 코드는 CHAR_BITS
8이 아닌 소스 플랫폼 의 소스 데이터의 각 8 진수가 별개의 배열 요소로 가정된다고 가정 하더라도 모든 플랫폼에서 작동해야 하지만 이러한 코드는 이식성이없는 것만 큼 빠르게 실행되지 않을 수 있습니다. 전자를 지원하는 플랫폼의 버전.
이식성은 종종 코드가 타입 캐스트 및 유사한 구조에서 매우 자유로울 것을 요구합니다. 예를 들어, 두 개의 32 비트 부호없는 정수를 곱하고 결과의 하위 32 비트를 생성하려는 코드는 이식성을 위해 다음과 같이 작성해야합니다.
uint32_t result = 1u*x*y;
그것 없이는 1u
INT_BITS가 33에서 64까지 인 시스템의 컴파일러는 x와 y의 곱이 2,147,483,647보다 크면 합법적으로 원하는 것을 수행 할 수 있으며 일부 컴파일러는 그러한 기회를 이용하기 쉽습니다.
위의 몇 가지 훌륭한 답변이 있지만, 한 의견에서 "왜 처음에 자신 만의 프로그래밍 언어를 만들고 싶습니까?"라는 질문에 대답했습니다. "주로 학습 목적이 될 것입니다." 다른 각도에서 대답하겠습니다.
어휘, 구문 및 언어에 대한 학습에 더 관심이있는 경우 소스 코드를 가져 와서 C 또는 C ++ 코드로 변환하는 변환기를 작성하여 gcc와 같은 기존 컴파일러를 사용하여 기계 코드로 끝낼 수 있습니다. 코드 생성 및 최적화에 대해 배우는 것보다 의미 분석!
자신의 머신 코드 생성기를 작성하는 것은 C 코드로 컴파일하여 주로 관심이없는 경우 피할 수있는 매우 중요한 작업입니다!
그러나 어셈블리 프로그램에 참여하여 코드를 가장 낮은 수준에서 최적화해야하는 문제에 매료되면 학습 경험을 위해 직접 코드 생성기를 작성하십시오!
Windows를 사용하는 경우 사용하는 운영 체제에 따라 코드를 중간 언어로 변환하여 기계 코드로 컴파일하는 데 시간이 걸리지 않는 Microsoft IL (중급 언어)이 있습니다. 또는 Linux를 사용하는 경우 별도의 컴파일러가 있습니다.
당신의 질문으로 되돌아가는 것은 당신이 당신의 언어를 디자인 할 때 기계가 높은 수준의 언어를 알지 못하기 때문에 별도의 컴파일러 나 인터프리터를 가져야합니다. 기계에 유용하도록 코드를 기계 코드로 컴파일해야합니다.
Your code should be compiled into machine code to make it useful for machine
-컴파일러가 c 코드를 출력으로 생성 한 경우 c 코드를 ac 컴파일러에 넣어 기계 코드를 생성 할 수 있습니다.