자체 언어로 컴파일러 작성


204

직관적으로 언어의 컴파일러 Foo자체는 Foo로 작성할 수 없습니다. 보다 구체적으로, 언어 의 첫 번째 컴파일러 Foo는 Foo로 작성할 수 없지만 이후의 컴파일러는에 대해 작성할 수 있습니다 Foo.

그러나 이것이 사실입니까? 첫 번째 컴파일러가 "자체"로 작성된 언어에 대한 읽기에 대한 모호한 기억이 있습니다. 이것이 가능합니까? 그렇다면 어떻게됩니까?



이것은 매우 오래된 질문이지만 Java로 Foo 언어에 대한 인터프리터를 작성했다고 말합니다. 그런 다음 언어 foo를 사용하여 자체 통역사를 작성했습니다. Foo는 여전히 JRE가 필요합니까?
George Xavier

답변:


231

이것을 "부트 스트래핑"이라고합니다. 먼저 다른 언어 (보통 Java 또는 C)로 언어에 대한 컴파일러 (또는 인터프리터)를 빌드해야합니다. 완료되면 언어 Foo로 새 버전의 컴파일러를 작성할 수 있습니다. 첫 번째 부트 스트랩 컴파일러를 사용하여 컴파일러를 컴파일 한 다음이 컴파일 된 컴파일러를 사용하여 다른 모든 버전 (향후 버전 자체 포함)을 컴파일합니다.

대부분의 언어는 실제로 이런 방식으로 만들어집니다. 부분적으로 언어 디자이너는 자신이 만들고있는 언어를 사용하기를 원하며, 사소하지 않은 컴파일러는 종종 언어의 "완전한"정도에 대한 유용한 벤치 마크 역할을하기 때문입니다.

이에 대한 예는 스칼라입니다. 최초의 컴파일러는 Martin Odersky의 실험 언어 인 Pizza에서 만들어졌습니다. 버전 2.0부터 컴파일러는 Scala로 완전히 다시 작성되었습니다. 그 시점부터는 새로운 Scala 컴파일러를 사용하여 향후 반복을 위해 자체 컴파일 할 수 있기 때문에 기존 피자 컴파일러를 완전히 버릴 수 있습니다.


어리석은 질문 : 컴파일러를 다른 마이크로 프로세서 아키텍처로 이식하려면 부트 스트랩이 해당 아키텍처의 작동중인 컴파일러에서 다시 시작해야합니다. 이게 옳은 거니? 이것이 맞다면 이것은 컴파일러를 다른 아키텍처로 이식하는 데 유용 할 수 있으므로 첫 번째 컴파일러를 유지하는 것이 좋습니다 (특히 C와 같은 '범용 언어'로 작성된 경우)?
piertoni

2
@piertoni 일반적으로 컴파일러 백엔드를 새로운 마이크로 프로세서로 리 타게팅하는 것이 더 쉽다.
bstpierre

예를 들어 LLVM을 백엔드로 사용하십시오.

76

나는 듣고 기억합니다 소프트웨어 공학 라디오 팟 캐스트 딕 가브리엘은 LISP의 베어 본 버전을 작성하여 원래의 LISP 인터프리터를 부트 스트랩에 대해 언급 항에있어서, 종이에 손이 기계 코드로 조합. 그때부터 나머지 LISP 기능은 LISP로 작성되고 해석되었습니다.


모든 것이 많은 손으로 창세기 트랜지스터에서 부트 스트랩됩니다

47

이전 답변에 호기심 추가.

다음은 Linux From Scratch 매뉴얼 에서 인용 한 것입니다. 소스에서 GCC 컴파일러 빌드를 시작하는 단계입니다. (Linux From Scratch는 대상 시스템의 모든 단일 바이너리 를 컴파일해야한다는 점에서 배포판 설치와 근본적으로 다른 Linux를 설치하는 방법 입니다.)

make bootstrap

'bootstrap'대상은 GCC를 컴파일 할뿐만 아니라 여러 번 컴파일합니다. 첫 번째 라운드에서 컴파일 된 프로그램을 사용하여 두 번째로 컴파일 한 다음 다시 세 번 컴파일합니다. 그런 다음이 두 번째와 세 번째 컴파일을 비교하여 완벽하게 재생산 할 수 있도록합니다. 이것은 또한 올바르게 컴파일되었음을 의미합니다.

'bootstrap'타겟의 사용은 타겟 시스템의 툴체인을 빌드하기 위해 사용하는 컴파일러가 타겟 컴파일러의 버전과 동일하지 않을 수 있다는 사실에 의해 동기가 부여됩니다. 이런 식으로 진행하면 대상 시스템에서 자체 컴파일 할 수있는 컴파일러를 얻을 수 있습니다.


12
"대상 시스템의 모든 단일 바이너리를 컴파일해야하지만"소스가 컴파일 할 수 없기 때문에 어딘가에서 얻은 gcc 바이너리로 시작해야합니다. 각 연속 gcc를 다시 컴파일하는 데 사용 된 각 gcc 바이너리의 계보를 추적했는지 궁금합니다 .K & R의 원래 C 컴파일러로 다시 돌아가시겠습니까?
robru

43

C에 대한 첫 번째 컴파일러를 작성할 때 다른 언어로 작성합니다. 이제 C 어셈블러에 대한 컴파일러가 있습니다. 결국, 문자열을 구문 분석 해야하는 곳, 특히 이스케이프 시퀀스를 찾아야합니다. \n10 진수 코드 10 (및 \r13 등)으로 문자 로 변환 하는 코드를 작성합니다 .

해당 컴파일러가 준비되면 C로 다시 구현하기 시작합니다.이 프로세스를 " 부트 스트래핑 "이라고합니다.

문자열 파싱 코드는 다음과 같습니다.

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

컴파일 할 때 '\ n'을 이해하는 바이너리가 있습니다. 즉, 소스 코드를 변경할 수 있습니다.

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

그렇다면 '\ n'이 13의 코드라는 정보는 어디에 있습니까? 바이너리에 있습니다! DNA와 같습니다.이 바이너리로 C 소스 코드를 컴파일하면이 정보가 상속됩니다. 컴파일러가 스스로 컴파일하면이 지식을 자손에게 전달합니다. 이 시점부터는 소스만으로 컴파일러가 수행 할 작업을 볼 수있는 방법이 없습니다.

일부 프로그램의 소스에서 바이러스를 숨기려면 다음과 같이 수행하십시오. 컴파일러의 소스를 가져 와서 함수를 컴파일하는 함수를 찾아서 다음과 같이 바꾸십시오.

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

흥미로운 부분은 A와 B입니다. A는 compileFunction바이러스 를 포함 하는 소스 코드 이며 어떤 방식으로 암호화되어 있으므로 결과 바이너리를 검색하는 것이 분명하지 않습니다. 따라서 컴파일러 자체를 컴파일하면 바이러스 주입 코드가 보존됩니다.

B는 바이러스로 대체하려는 기능과 동일합니다. 예를 들어, 소스 파일 "login.c"의 "login"함수는 Linux 커널에서 가져온 것일 수 있습니다. 일반 비밀번호 외에 루트 계정의 비밀번호 "joshua"를 허용하는 버전으로 바꿀 수 있습니다.

컴파일하고 바이너리로 확산 시키면 소스를보고 바이러스를 찾을 수있는 방법이 없습니다.

아이디어의 원천 : https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/


1
바이러스 감염 컴파일러를 작성하는 것에 대한 후반의 요점은 무엇입니까? :)
mhvelplund

3
@mhvelplund 부트 스트랩이 어떻게 당신을 죽일 수 있는지에 대한 지식을 퍼 뜨리기.
Aaron Digulla

19

시작 소스 코드를 컴파일 할 것이 없기 때문에 컴파일러 자체를 작성할 수 없습니다. 이 문제를 해결하는 데는 두 가지 방법이 있습니다.

가장 덜 선호되는 것은 다음과 같습니다. 최소한의 언어 집합을 위해 최소 컴파일러를 어셈블러 (yuck)로 작성한 다음 해당 컴파일러를 사용하여 언어의 추가 기능을 구현합니다. 모든 언어 기능을 갖춘 컴파일러가 생길 때까지 길을 닦으십시오. 고통스러운 과정은 일반적으로 다른 선택이 없을 때만 수행됩니다.

선호되는 방법은 크로스 컴파일러를 사용하는 것입니다. 다른 머신에서 기존 컴파일러의 백엔드를 변경하여 대상 머신에서 실행되는 출력을 작성합니다. 그런 다음 대상 컴퓨터에서 멋진 컴파일러를 만들고 작업합니다. 교체 가능한 플러그 백엔드가있는 기존 컴파일러가 많이 있으므로 C 언어가 가장 많이 사용됩니다.

약간의 사실은 GNU C ++ 컴파일러가 C 서브 세트 만 사용하는 구현을 가지고 있다는 것입니다. 그 이유는 일반적으로 새로운 대상 시스템의 C 컴파일러를 쉽게 찾을 수 있기 때문에 전체 GNU C ++ 컴파일러를 빌드 할 수 있습니다. 이제 대상 머신에서 C ++ 컴파일러를 사용하도록 부팅했습니다.


14

일반적으로 컴파일러가 작동하는 (기본적 인 경우) 컷을 먼저 작동시켜야합니다. 그러면 자체 호스팅에 대해 생각할 수 있습니다. 이것은 실제로 일부 언어에서 중요한 이정표로 간주됩니다.

내가 "모노"에서 기억 한 바에 따르면, 그것들을 작동시키기 위해서는 몇 가지 사항을 반영해야 할 것입니다. 모노 팀은 어떤 것이 불가능하다고 지적합니다 Reflection.Emit. 물론, MS 팀은 그들이 틀렸다는 것을 증명할 수 있습니다.

여기에는 몇 가지 실질적인 장점이 있습니다. 초보자에게는 상당히 좋은 단위 테스트입니다! 그리고 걱정해야 할 언어는 하나뿐입니다 (즉, C # 전문가가 C ++을 많이 알지 못할 수도 있지만 이제는 C # 컴파일러를 고칠 수 있습니다). 그러나 나는 직장에서 많은 전문적 자부심이 없는지 궁금합니다. 그들은 단순히 자체 호스팅이 되기를 원합니다 .

컴파일러는 아니지만 최근에 자체 호스팅 시스템에서 작업하고 있습니다. 코드 생성기는 코드 생성기를 생성하는 데 사용됩니다. 따라서 스키마가 변경되면 자체적으로 새 버전으로 실행합니다. 버그가 있으면 이전 버전으로 돌아가서 다시 시도하십시오. 매우 편리하고 유지 보수가 쉽습니다.


업데이트 1

방금 PDC에서 Anders의 비디오 를 보았 으며 (약 1 시간) 그는 컴파일러로서의 서비스에 관한 훨씬 더 유효한 이유를 제시합니다. 기록만을 위해서.


4

덤프는 다음과 같습니다 (실제로 검색하기 어려운 주제).

이것은 PyPyRubinius 의 아이디어이기도합니다 .

(이것은 Forth 에도 적용될 수 있다고 생각 하지만 Forth 에 대해서는 아무것도 모릅니다.)


스몰 토크 관련 기사로 연결되는 첫 번째 링크는 현재 유용하고 즉각적인 정보가없는 페이지를 가리키고 있습니다.
nbro

1

GNU Ada 컴파일러 인 GNAT는 Ada 컴파일러가 완전히 구축되어야합니다. 쉽게 사용할 수있는 GNAT 바이너리가없는 플랫폼으로 포팅 할 때 문제가 될 수 있습니다.


1
왜 그런지 모르겠습니까? 부트 스트랩을 여러 번 부트 스트랩해야하는 규칙은 없으며 (새 플랫폼마다), 현재 플랫폼과 크로스 컴파일 할 수도 있습니다.
Marco van de Voort

1

실제로 대부분의 컴파일러는 위에서 언급 한 이유로 컴파일 언어로 작성됩니다.

첫 번째 부트 스트랩 컴파일러는 일반적으로 C, C ++ 또는 Assembly로 작성됩니다.


1

Mono 프로젝트 C # 컴파일러는 오랫동안 "자체 호스팅"되어 왔으며, 이는 C # 자체로 작성된 것입니다.

내가 아는 것은 컴파일러가 순수한 C 코드로 시작되었지만 ECMA의 "기본"기능이 구현되면 컴파일러를 C #으로 다시 작성하기 시작했습니다.

동일한 언어로 컴파일러를 작성하면 얻을 수있는 이점을 알지 못하지만 적어도 언어 자체가 제공 할 수있는 기능과 관련이 있다고 확신합니다 (예 : C는 객체 지향 프로그래밍을 지원하지 않습니다) .

자세한 내용은 여기를 참조 하십시오 .


1

SLIC (컴파일러 구현을위한 언어 시스템) 자체를 작성했습니다. 그런 다음 손으로 조립했습니다. SLIC에는 5 개의 하위 언어로 구성된 단일 컴파일러가 많았으므로 다음과 같이 많이 있습니다.

  • SYNTAX 파서 프로그래밍 언어 PPL
  • 생성기 LISP 2 기반 트리 크롤링 PSEUDO 코드 생성 언어
  • ISO In Sequence, PSEUDO 코드, 최적화 언어
  • 어셈블리 코드 생성 언어와 같은 PSEUDO 매크로
  • MACHOP 어셈블리 머신 명령어 정의 언어.

SLIC은 CWIC (컴파일러 작성 및 구현 용 컴파일러)에서 영감을 받았습니다. 대부분의 컴파일러 개발 패키지와 달리 SLIC 및 CWIC는 특수한 도메인 별 언어로 코드 생성을 해결했습니다. SLIC은 트리 크롤링 생성기 언어에서 대상 시스템 세부 사항을 분리하는 ISO, PSEUDO 및 MACHOP 하위 언어를 추가하여 CWIC 코드 생성을 확장합니다.

LISP 2 트리 및 목록

LISP 2 기반 생성기 언어의 동적 메모리 관리 시스템은 핵심 구성 요소입니다. 리스트는 대괄호로 묶인 언어로 표현되며, 그 구성 요소는 쉼표로 구분됩니다 (예 : 세 개의 요소 [a, b, c]리스트).

나무:

     ADD
    /   \
  MPY     3
 /   \
5     x

첫 번째 항목이 노드 객체 인 목록으로 표시됩니다.

[ADD,[MPY,5,x],3]

트리는 일반적으로 브랜치 앞에 노드가 분리되어 표시됩니다.

ADD[MPY[5,x],3]

LISP 2 기반 생성기 기능으로 분석

생성기 함수는 이름이 지정된 (unparse) => action> 쌍입니다 ...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

구문 분석되지 않은 표현식은 트리 패턴 및 / 또는 객체 유형을 구분하여이를 분리하고 해당 부분을 로컬 변수에 지정하여 절차 적 조치로 처리되도록하는 테스트입니다. 다른 인수 유형을 취하는 오버로드 된 함수와 비슷합니다. () => ...을 제외하고 테스트는 코딩 된 순서대로 시도됩니다. 첫 번째는 해당 조치를 실행하여 구문 분석을 완료합니다. 해석되지 않은 표현은 분해 테스트입니다. ADD [x, y]는 분기를 지역 변수 x 및 y에 할당하는 두 분기 ADD 트리와 일치합니다. 작업은 간단한식이거나 .BEGIN ... .END 바운드 코드 블록 일 수 있습니다. 나는 오늘 c 스타일 {...} 블록을 사용할 것입니다. 트리 일치, [] 구문 분석 규칙은 생성 된 결과를 액션에 전달하는 생성자를 호출 할 수 있습니다.

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

특히 위의 expr_gen unparse는 두 가지 분기 ADD 트리와 일치합니다. 테스트 패턴 내에서 트리 분기에 배치 된 단일 인수 생성기가 해당 분기와 함께 호출됩니다. 인수 목록은 반환 된 객체에 할당 된 로컬 변수입니다. 비 분석 위의 두 분기는 ADD 트리 분해, 각 분기를 재귀 적으로 눌러 expr_gen으로 지정합니다. 왼쪽 분기 리턴은 로컬 변수 x에 배치됩니다. 마찬가지로 오른쪽 분기는 y 반환 객체와 함께 expr_gen에 전달되었습니다. 위의 수치 식 평가 기의 일부가 될 수 있습니다. 노드 문자열 대신 위의 벡터라는 벡터라는 바로 가기 기능이있었습니다. 노드의 벡터는 해당 동작의 벡터와 함께 사용할 수 있습니다.

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

위의보다 완전한 표현식 평가 기는 expr_gen 왼쪽 분기에서 x로, 오른쪽 분기에서 y 로의 리턴을 지정합니다. x 및 y에서 수행 된 해당 동작 벡터가 반환되었습니다. 마지막 unparse => action 쌍은 숫자 및 기호 객체와 일치합니다.

심볼 및 심볼 속성

기호에는 이름이 지정된 속성이있을 수 있습니다. val : (x) x에 포함 된 심볼 객체의 val 속성에 액세스합니다. 일반화 된 기호 테이블 스택은 SLIC의 일부입니다. SYMBOL 테이블은 기능에 대한 지역 기호를 제공하여 밀리고 터질 수 있습니다. 새로 생성 된 심볼은 상단 심볼 테이블에 카탈로그됩니다. 심볼 룩업은 맨 위 테이블에서 먼저 심볼 테이블 스택을 검색합니다.

기계 독립적 인 코드 생성

SLIC의 생성기 언어는 PSEUDO 명령 객체를 생성하여 섹션 코드 목록에 추가합니다. .FLUSH는 PSEUDO 코드 목록을 실행하여 목록에서 각 PSEUDO 명령을 제거하고 호출합니다. 실행 후 PSEUDO 객체 메모리가 해제됩니다. PSEUDO 및 GENERATOR 조치의 절차 본문은 기본적으로 출력을 제외하고 동일한 언어입니다. PSEUDO는 기계 독립적 인 코드 순차를 제공하는 어셈블리 매크로의 역할을합니다. 트리 크롤링 생성기 언어에서 특정 대상 시스템을 분리합니다. PSEUDO는 MACHOP 기능을 호출하여 기계 코드를 출력합니다. MACHOP는 벡터와 같은 항목을 사용하여 어셈블리 의사 연산 (예 : dc, 상수 정의 등) 및 기계 명령어 또는 유사한 형식의 명령어 제품군을 정의하는 데 사용됩니다. 그들은 단순히 매개 변수를 명령을 구성하는 일련의 비트 필드로 변환합니다. MACHOP 호출은 어셈블리처럼 보이고 어셈블리가 컴파일 목록에 표시 될 때 필드의 인쇄 형식을 제공합니다. 예제 코드에서 나는 쉽게 추가 할 수는 있지만 원래 언어가 아닌 c 스타일 주석을 사용하고 있습니다. MACHOP은 코드를 비트 주소 지정 가능 메모리로 생성합니다. SLIC 링커는 컴파일러의 출력을 처리합니다. 벡터 입력을 사용하는 DEC-10 사용자 모드 명령어를위한 MACHOP : MACHOP은 코드를 비트 주소 지정 가능 메모리로 생성합니다. SLIC 링커는 컴파일러의 출력을 처리합니다. 벡터 입력을 사용하는 DEC-10 사용자 모드 명령어를위한 MACHOP : MACHOP은 코드를 비트 주소 지정 가능 메모리로 생성합니다. SLIC 링커는 컴파일러의 출력을 처리합니다. 벡터 입력을 사용하는 DEC-10 사용자 모드 명령어를위한 MACHOP :

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

.MORG 36, O (18) : $ / 36; 위치를 8 비트로 18 비트의 $ / 36 워드 주소를 인쇄하는 36 비트 경계에 정렬합니다. 9 비트 opcd, 4 비트 레지스터, 간접 비트 및 4 비트 인덱스 레지스터는 단일 18 비트 필드처럼 결합되어 인쇄됩니다. 18 비트 주소 / 36 또는 즉시 값이 8 진수로 출력 및 인쇄됩니다. MOVEI 예제는 r1 = 1 및 r2 = 2로 출력됩니다.

400020 201082 000005            MOVEI r1,5(r2)

컴파일러 어셈블리 옵션을 사용하면 컴파일 목록에 생성 된 어셈블리 코드가 표시됩니다.

함께 연결

SLIC 링커는 링크 및 심볼 해상도를 처리하는 라이브러리로 제공됩니다. 그러나 대상 머신에 대해 대상 특정 출력로드 파일 형식을 작성하고 링커 라이브러리 라이브러리와 링크해야합니다.

생성기 언어는 트리를 파일에 쓰고이를 읽고 멀티 패스 컴파일러를 구현할 수 있습니다.

짧은 여름 생성 코드 및 출처

SLIC이 진정한 컴파일러 컴파일러라는 것을 이해하기 위해 코드 생성을 먼저 살펴 보았습니다. SLIC은 1960 년대 후반에 Systems Development Corporation에서 개발 한 CWIC (작성 및 구현 용 컴파일러)에서 영감을 받았습니다. CWIC에는 GENERATOR 언어에서 숫자 바이트 코드를 생성하는 SYNTAX 및 GENERATOR 언어 만 있습니다. 바이트 코드는 이름이 지정된 섹션과 연관된 메모리 버퍼에 배치되거나 (CWIC 문서에 사용 된 용어) .FLUSH 문으로 작성되었습니다. CWIC의 ACM 용지는 ACM 아카이브에서 구할 수 있습니다.

주요 프로그래밍 언어를 성공적으로 구현

1970 년대 후반 SLIC은 COBOL 크로스 컴파일러를 작성하는 데 사용되었습니다. 대부분 단일 프로그래머가 약 3 개월 안에 완료했습니다. 필요에 따라 프로그래머와 약간 협력했습니다. 다른 프로그래머가 대상 TI-990 미니 컴퓨터에 대한 런타임 라이브러리 및 MACHOP를 작성했습니다. 이 COBOL 컴파일러는 어셈블리에서 작성된 DEC-10 기본 COBOL 컴파일러보다 초당 훨씬 많은 라인을 컴파일했습니다.

컴파일러에 대한 더 많은 이야기는 보통

처음부터 컴파일러를 작성하는 데있어 가장 큰 부분은 런타임 라이브러리입니다. 심볼 테이블이 필요합니다. 입력과 출력이 필요합니다. 동적 메모리 관리 등. 컴파일러의 런타임 라이브러리를 작성하고 컴파일러를 작성하는 것이 더 많은 작업 일 수 있습니다. 그러나 SLIC에서 런타임 라이브러리는 SLIC에서 개발 된 모든 컴파일러에 공통적입니다. 두 개의 런타임 라이브러리가 있습니다. 언어의 대상 시스템 (예 : COBOL)을위한 것입니다. 다른 하나는 컴파일러 컴파일러 런타임 라이브러리입니다.

나는 이것이 파서 생성기가 아님을 확립했다고 생각한다. 백엔드를 조금 이해하면 파서 프로그래밍 언어를 설명 할 수 있습니다.

파서 프로그래밍 언어

파서는 간단한 방정식의 형태로 작성된 공식을 사용하여 작성됩니다.

<name> <formula type operator> <expression> ;

가장 낮은 수준의 언어 요소는 문자입니다. 토큰은 언어 문자의 하위 집합으로 구성됩니다. 문자 클래스는 문자 하위 집합의 이름을 지정하고 정의하는 데 사용됩니다. 문자 클래스 정의 연산자는 콜론 (:) 문자입니다. 클래스의 멤버 인 문자는 정의의 오른쪽에 코딩됩니다. 인쇄 가능한 문자는 소수의 단일 문자열로 묶습니다. 비 인쇄 및 특수 문자는 숫자 서수로 표시 될 수 있습니다. 반원은 대안으로 분리 | 운영자. 클래스 수식은 세미콜론으로 끝납니다. 문자 클래스에는 이전에 정의 된 클래스가 포함될 수 있습니다.

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

skip_class 0b00000001은 미리 정의되어 있지만 skip_class를 정의하는 오버로드 일 수 있습니다.

요약 : 문자 클래스는 문자 상수, 문자 서수 또는 이전에 정의 된 문자 클래스 만 가능한 대체 목록입니다. 문자 클래스를 구현함에 따라 클래스 수식에 클래스 비트 마스크가 할당됩니다. (위의 주석에 표시됨) 문자 리터럴 또는 서수를 가진 클래스 수식은 클래스 비트를 할당합니다. 마스크는 포함 된 클래스의 클래스 마스크를 할당 된 비트 (있는 경우)와 함께 정렬하여 만듭니다. 클래스 테이블은 문자 클래스에서 작성됩니다. 문자 서수로 색인 된 항목에는 문자의 클래스 멤버십을 나타내는 비트가 포함됩니다. 수업 테스트는 인라인으로 이루어집니다. eax에서 문자 서 수가있는 IA-86 코드 예제는 클래스 테스트를 보여줍니다.

test    byte ptr [eax+_classmap],dgt

뒤에 :

jne      <success>

또는

je       <failure>

IA-86 명령어는 오늘날 널리 알려져 있다고 생각하기 때문에 IA-86 명령어 코드 예제가 사용됩니다. 클래스 마스크로 평가되는 클래스 이름은 문자 서수 (eax)로 인덱스 된 클래스 테이블과 함께 비파괴 적으로 AND됩니다. 0이 아닌 결과는 클래스 멤버십을 나타냅니다. (EAX는 문자를 포함하는 al (하위 8 비트 EAX) 제외)는 0입니다.

이 오래된 컴파일러에서는 토큰이 약간 달랐습니다. 키워드는 토큰으로 설명되지 않았습니다. 그것들은 단순히 파서 언어에서 인용 된 문자열 상수와 일치합니다. 인용 된 문자열은 일반적으로 유지되지 않습니다. 개질제가 사용될 수있다. A +는 문자열을 일치시킵니다. (예 : + '-'는 성공하면 문자를 유지하는 문자와 일치합니다.), 연산 (예 : 'E')은 문자열을 토큰에 삽입합니다. 공백은 첫 번째 일치 항목이 작성 될 때까지 선행 SKIP_CLASS 문자를 건너 뛰는 토큰 공식으로 처리됩니다. 명시 적 skip_class 문자 일치는 skip_class 문자로 시작하는 토큰 허용 건너 뛰기를 중지합니다. 문자열 토큰 수식은 작은 따옴표 문자 또는 큰 따옴표 문자열과 일치하는 선행 skip_class 문자를 건너 뜁니다. "인용 된 문자열 내에서"문자를 일치시키는 것이 중요합니다.

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

첫 번째 대안은 작은 따옴표로 묶은 문자와 일치합니다. 올바른 대안은 큰 따옴표로 묶인 문자열과 일치하며 작은 따옴표로 묶인 두 개의 "문자를 사용하는 큰 따옴표 문자를 포함 할 수 있습니다. 이 공식은 자체 정의에 사용되는 문자열을 정의합니다. 내부 오른쪽 대안 ' "'$ (-" "" ".ANY |" "" "" "," "" ") '"'는 큰 따옴표로 묶인 문자열과 일치합니다. 큰 따옴표 문자와 일치시키기 위해 작은 따옴표 문자를 사용할 수 있지만 큰 문자 따옴표로 묶인 문자열 안에 "문자를 사용하려면 두 문자를 사용하여 하나를 가져와야합니다. 예를 들어 따옴표를 제외한 모든 문자와 일치하는 왼쪽 내부 대안에서 :

-"""" .ANY

음수 미리보기- "" ""는 성공할 때 ( "문자와 일치하지 않음") .ANY 문자와 일치합니다 ( "-" "" "로 인해 해당 문자를 제거 할 수 없음). 올바른 대안은 "" "" "문자와 일치하고 실패한 것이 올바른 대안입니다.

"""""",""""

하나의 "문자를 삽입하기 위해 두 개의"문자를 단일 더블 "", "" "" "로 바꾸려고 시도합니다. 닫는 문자열 따옴표 문자에 실패하는 두 가지 내부 대안이 일치하고 MAKSTR []이 문자열 오브젝트를 작성하도록 호출됩니다. sequence, loop while successful, 연산자는 시퀀스 일치에 사용됩니다 토큰 수식 건너 뛰기 건너 뛰기 클래스 문자 건너 뛰기 공백 (부울 공백) skip_class 건너 뛰기가 비활성화되면 []를 사용하여 다른 언어로 프로그래밍 된 함수를 호출 할 수 있습니다. [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] 및 MAKINT []는 일치하는 토큰 문자열을 유형이 지정된 객체로 변환하는 라이브러리 함수로 제공됩니다.

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

위의 숫자 토큰 공식은 정수 및 부동 소수점 숫자를 인식합니다. 대안은 항상 성공합니다. 수치 객체는 계산에 사용될 수 있습니다. 수식이 성공하면 토큰 객체가 구문 분석 스택으로 푸시됩니다. (+ 'E'| 'e', ​​'E')의 지수 리드는 흥미 롭습니다. 우리는 항상 MAKEFLOAT []에 대문자 E를 갖기를 원합니다. 그러나 우리는 소문자 'e'를 사용하여 ','를 사용하여 대체합니다.

문자 클래스와 토큰 공식의 일관성을 발견했을 수 있습니다. 파싱 ​​공식은 역 추적 대안과 트리 구성 연산자를 계속 추가합니다. 역 추적 및 비역 추적 대체 연산자는 식 수준 내에서 혼합 될 수 없습니다. 역 추적을하지 않는 (a | b \ c) | 역 추적 대안이 있습니다. (a \ b \ c), (a | b | c) 및 ((a | b) \ c)는 유효합니다. 역 추적 대안은 왼쪽 대안을 시도하기 전에 구문 분석 상태를 저장하고 실패시 올바른 대안을 시도하기 전에 구문 분석 상태를 복원합니다. 일련의 대안들에서 첫 번째 성공적인 대안은 그룹을 만족시킨다. 다른 대안은 시도되지 않습니다. 팩토링 및 그룹화는 지속적인 진행 구문 분석을 제공합니다. 역 추적 대안은 왼쪽 대안을 시도하기 전에 구문 분석의 저장된 상태를 만듭니다. 구문 분석이 부분적으로 일치 한 후 실패 할 경우 역 추적이 필요합니다.

(a b | c d)\ e

위의 반환 오류가 발생하면 대체 CD가 시도됩니다. c가 실패를 반환하면 역 추적 대안이 시도됩니다. a가 성공하고 b가 실패하면 구문 분석 파일이 역 추적되고 e 시도됩니다. 마찬가지로 실패 c가 성공하고 실패 b가 역 추적되고 대안 e가 취해집니다. 역 추적은 공식 내로 제한되지 않습니다. 구문 분석 수식이 언제든지 부분적으로 일치하고 실패하면 구문 분석이 맨 위 역 추적으로 재설정되고 그 대안이 사용됩니다. 백 트랙이 생성 된 코드를 출력 감지 한 경우 컴파일 오류가 발생할 수 있습니다. 역 추적은 컴파일을 시작하기 전에 설정됩니다. 오류 반환 또는 역 추적은 컴파일러 오류입니다. 역 추적이 쌓입니다. 음수와 양수를 사용할 수 있습니까? 구문 분석을 진행하지 않고 테스트 할 운영자를 엿봄 /보고 있습니다. 문자열 테스트는 입력 상태를 저장하고 재설정하기 만하면됩니다. 미리보기는 실패하기 전에 부분적으로 일치하는 구문 분석 표현식입니다. 미리보기는 역 추적을 사용하여 구현됩니다.

파서 언어는 LL 또는 LR 파서가 아닙니다. 그러나 트리 생성을 프로그래밍하는 재귀 괜찮은 파서를 작성하기위한 프로그래밍 언어 :

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

일반적으로 사용되는 구문 분석 예제는 산술 표현식입니다.

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

루프를 사용하는 Exp와 Term은 왼손잡이 트리를 만듭니다. 오른쪽 재귀를 사용하는 인수는 오른쪽 트리를 만듭니다.

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

다음은 c 스타일 주석으로 업데이트 된 SLIC 버전의 cc 컴파일러입니다. 함수 유형 (문법, 토큰, 문자 클래스, 생성기, PSEUDO 또는 MACHOP은 ID 다음에 오는 초기 구문에 의해 결정됩니다. 이러한 하향식 구문 분석기를 사용하면 공식을 정의하는 프로그램으로 시작합니다.

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

// 트리를 만들 때 id가 어떻게 해제되고 나중에 결합되는지 확인합니다.

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

파서 언어가 주석 처리 및 오류 복구를 처리하는 방법에 주목하십시오.

나는 그 질문에 대답했다고 생각합니다. SLIC의 후계자 인 cc 언어 자체의 많은 부분을 작성했습니다. 아직 컴파일러가 없습니다. 그러나 어셈블리 코드, 벌거 벗은 asm c 또는 c ++ 함수로 직접 컴파일 할 수 있습니다.


0

예, 해당 언어로 된 언어의 컴파일러를 작성할 수 있습니다. 아니요, 부트 스트랩하기 위해 해당 언어에 대한 첫 번째 컴파일러가 필요하지 않습니다.

부트 스트랩에 필요한 것은 언어의 구현입니다. 컴파일러 나 인터프리터 일 수 있습니다.

역사적으로 언어는 일반적으로 해석 된 언어 또는 컴파일 된 언어로 생각되었습니다. 통역사는 전자에 대해서만 작성되었고 컴파일러는 후자에 대해서만 작성되었습니다. 따라서 일반적으로 컴파일러가 언어로 작성된 경우 첫 번째 컴파일러는 부트 스트랩하기 위해 다른 언어로 작성된 다음 선택적으로 주제 언어에 맞게 컴파일러를 다시 작성합니다. 그러나 다른 언어로 통역사를 작성하는 것은 선택 사항입니다.

이것은 단지 이론적 인 것이 아닙니다. 나는 현재이 일을하고있다. 나는 내가 개발 한 언어 Salmon의 컴파일러를 연구하고 있습니다. 먼저 C에서 Salmon 컴파일러를 만들었고 이제 Salmon에서 컴파일러를 작성하고 있으므로 다른 언어로 작성된 Salmon 컴파일러를 사용하지 않고도 Salmon 컴파일러를 작동시킬 수 있습니다.


-1

BNF를 설명 하는 BNF 를 작성할 수 있습니다 .


4
실제로는 어렵지 않지만 실제 응용 프로그램은 파서 생성기에 있습니다.
Daniel Spiewak

실제로 나는 그 방법을 사용하여 LIME 파서 생성기를 생성했습니다. 메타 문법의 제한적이고 단순화 된 표 형식 표현은 간단한 재귀 하강 파서를 통과합니다. 그런 다음 LIME은 문법 언어에 대한 구문 분석기를 생성 한 다음 해당 구문 분석기를 사용하여 누군가가 실제로 구문 분석기를 생성하는 데 관심이있는 문법을 읽습니다. 이것은 내가 방금 쓴 것을 쓰는 법을 알 필요가 없다는 것을 의미합니다. 마술 같아요
Ian

BNF가 스스로 설명 할 수 없기 때문에 실제로는 할 수 없습니다. 비 터미널 기호가 인용되지 않은 yacc 에서 사용되는 것과 같은 변형이 필요합니다 .
Lorne의 후작

1
<>를 인식 할 수 없으므로 bnf를 사용하여 bnf를 정의 할 수 없습니다. EBNF는 언어의 상수 문자열 토큰을 인용하여 수정했습니다.
GK
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.