직관적으로 언어의 컴파일러 Foo
자체는 Foo로 작성할 수 없습니다. 보다 구체적으로, 언어 의 첫 번째 컴파일러 Foo
는 Foo로 작성할 수 없지만 이후의 컴파일러는에 대해 작성할 수 있습니다 Foo
.
그러나 이것이 사실입니까? 첫 번째 컴파일러가 "자체"로 작성된 언어에 대한 읽기에 대한 모호한 기억이 있습니다. 이것이 가능합니까? 그렇다면 어떻게됩니까?
직관적으로 언어의 컴파일러 Foo
자체는 Foo로 작성할 수 없습니다. 보다 구체적으로, 언어 의 첫 번째 컴파일러 Foo
는 Foo로 작성할 수 없지만 이후의 컴파일러는에 대해 작성할 수 있습니다 Foo
.
그러나 이것이 사실입니까? 첫 번째 컴파일러가 "자체"로 작성된 언어에 대한 읽기에 대한 모호한 기억이 있습니다. 이것이 가능합니까? 그렇다면 어떻게됩니까?
답변:
이것을 "부트 스트래핑"이라고합니다. 먼저 다른 언어 (보통 Java 또는 C)로 언어에 대한 컴파일러 (또는 인터프리터)를 빌드해야합니다. 완료되면 언어 Foo로 새 버전의 컴파일러를 작성할 수 있습니다. 첫 번째 부트 스트랩 컴파일러를 사용하여 컴파일러를 컴파일 한 다음이 컴파일 된 컴파일러를 사용하여 다른 모든 버전 (향후 버전 자체 포함)을 컴파일합니다.
대부분의 언어는 실제로 이런 방식으로 만들어집니다. 부분적으로 언어 디자이너는 자신이 만들고있는 언어를 사용하기를 원하며, 사소하지 않은 컴파일러는 종종 언어의 "완전한"정도에 대한 유용한 벤치 마크 역할을하기 때문입니다.
이에 대한 예는 스칼라입니다. 최초의 컴파일러는 Martin Odersky의 실험 언어 인 Pizza에서 만들어졌습니다. 버전 2.0부터 컴파일러는 Scala로 완전히 다시 작성되었습니다. 그 시점부터는 새로운 Scala 컴파일러를 사용하여 향후 반복을 위해 자체 컴파일 할 수 있기 때문에 기존 피자 컴파일러를 완전히 버릴 수 있습니다.
나는 듣고 기억합니다 소프트웨어 공학 라디오 팟 캐스트 딕 가브리엘은 LISP의 베어 본 버전을 작성하여 원래의 LISP 인터프리터를 부트 스트랩에 대해 언급 항에있어서, 종이에 손이 기계 코드로 조합. 그때부터 나머지 LISP 기능은 LISP로 작성되고 해석되었습니다.
이전 답변에 호기심 추가.
다음은 Linux From Scratch 매뉴얼 에서 인용 한 것입니다. 소스에서 GCC 컴파일러 빌드를 시작하는 단계입니다. (Linux From Scratch는 대상 시스템의 모든 단일 바이너리 를 컴파일해야한다는 점에서 배포판 설치와 근본적으로 다른 Linux를 설치하는 방법 입니다.)
make bootstrap
'bootstrap'대상은 GCC를 컴파일 할뿐만 아니라 여러 번 컴파일합니다. 첫 번째 라운드에서 컴파일 된 프로그램을 사용하여 두 번째로 컴파일 한 다음 다시 세 번 컴파일합니다. 그런 다음이 두 번째와 세 번째 컴파일을 비교하여 완벽하게 재생산 할 수 있도록합니다. 이것은 또한 올바르게 컴파일되었음을 의미합니다.
'bootstrap'타겟의 사용은 타겟 시스템의 툴체인을 빌드하기 위해 사용하는 컴파일러가 타겟 컴파일러의 버전과 동일하지 않을 수 있다는 사실에 의해 동기가 부여됩니다. 이런 식으로 진행하면 대상 시스템에서 자체 컴파일 할 수있는 컴파일러를 얻을 수 있습니다.
C에 대한 첫 번째 컴파일러를 작성할 때 다른 언어로 작성합니다. 이제 C 어셈블러에 대한 컴파일러가 있습니다. 결국, 문자열을 구문 분석 해야하는 곳, 특히 이스케이프 시퀀스를 찾아야합니다. \n
10 진수 코드 10 (및 \r
13 등)으로 문자 로 변환 하는 코드를 작성합니다 .
해당 컴파일러가 준비되면 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/
시작 소스 코드를 컴파일 할 것이 없기 때문에 컴파일러 자체를 작성할 수 없습니다. 이 문제를 해결하는 데는 두 가지 방법이 있습니다.
가장 덜 선호되는 것은 다음과 같습니다. 최소한의 언어 집합을 위해 최소 컴파일러를 어셈블러 (yuck)로 작성한 다음 해당 컴파일러를 사용하여 언어의 추가 기능을 구현합니다. 모든 언어 기능을 갖춘 컴파일러가 생길 때까지 길을 닦으십시오. 고통스러운 과정은 일반적으로 다른 선택이 없을 때만 수행됩니다.
선호되는 방법은 크로스 컴파일러를 사용하는 것입니다. 다른 머신에서 기존 컴파일러의 백엔드를 변경하여 대상 머신에서 실행되는 출력을 작성합니다. 그런 다음 대상 컴퓨터에서 멋진 컴파일러를 만들고 작업합니다. 교체 가능한 플러그 백엔드가있는 기존 컴파일러가 많이 있으므로 C 언어가 가장 많이 사용됩니다.
약간의 사실은 GNU C ++ 컴파일러가 C 서브 세트 만 사용하는 구현을 가지고 있다는 것입니다. 그 이유는 일반적으로 새로운 대상 시스템의 C 컴파일러를 쉽게 찾을 수 있기 때문에 전체 GNU C ++ 컴파일러를 빌드 할 수 있습니다. 이제 대상 머신에서 C ++ 컴파일러를 사용하도록 부팅했습니다.
일반적으로 컴파일러가 작동하는 (기본적 인 경우) 컷을 먼저 작동시켜야합니다. 그러면 자체 호스팅에 대해 생각할 수 있습니다. 이것은 실제로 일부 언어에서 중요한 이정표로 간주됩니다.
내가 "모노"에서 기억 한 바에 따르면, 그것들을 작동시키기 위해서는 몇 가지 사항을 반영해야 할 것입니다. 모노 팀은 어떤 것이 불가능하다고 지적합니다 Reflection.Emit
. 물론, MS 팀은 그들이 틀렸다는 것을 증명할 수 있습니다.
여기에는 몇 가지 실질적인 장점이 있습니다. 초보자에게는 상당히 좋은 단위 테스트입니다! 그리고 걱정해야 할 언어는 하나뿐입니다 (즉, C # 전문가가 C ++을 많이 알지 못할 수도 있지만 이제는 C # 컴파일러를 고칠 수 있습니다). 그러나 나는 직장에서 많은 전문적 자부심이 없는지 궁금합니다. 그들은 단순히 자체 호스팅이 되기를 원합니다 .
컴파일러는 아니지만 최근에 자체 호스팅 시스템에서 작업하고 있습니다. 코드 생성기는 코드 생성기를 생성하는 데 사용됩니다. 따라서 스키마가 변경되면 자체적으로 새 버전으로 실행합니다. 버그가 있으면 이전 버전으로 돌아가서 다시 시도하십시오. 매우 편리하고 유지 보수가 쉽습니다.
방금 PDC에서 Anders의 비디오 를 보았 으며 (약 1 시간) 그는 컴파일러로서의 서비스에 관한 훨씬 더 유효한 이유를 제시합니다. 기록만을 위해서.
GNU Ada 컴파일러 인 GNAT는 Ada 컴파일러가 완전히 구축되어야합니다. 쉽게 사용할 수있는 GNAT 바이너리가없는 플랫폼으로 포팅 할 때 문제가 될 수 있습니다.
SLIC (컴파일러 구현을위한 언어 시스템) 자체를 작성했습니다. 그런 다음 손으로 조립했습니다. SLIC에는 5 개의 하위 언어로 구성된 단일 컴파일러가 많았으므로 다음과 같이 많이 있습니다.
SLIC은 CWIC (컴파일러 작성 및 구현 용 컴파일러)에서 영감을 받았습니다. 대부분의 컴파일러 개발 패키지와 달리 SLIC 및 CWIC는 특수한 도메인 별 언어로 코드 생성을 해결했습니다. SLIC은 트리 크롤링 생성기 언어에서 대상 시스템 세부 사항을 분리하는 ISO, PSEUDO 및 MACHOP 하위 언어를 추가하여 CWIC 코드 생성을 확장합니다.
LISP 2 기반 생성기 언어의 동적 메모리 관리 시스템은 핵심 구성 요소입니다. 리스트는 대괄호로 묶인 언어로 표현되며, 그 구성 요소는 쉼표로 구분됩니다 (예 : 세 개의 요소 [a, b, c]리스트).
나무:
ADD
/ \
MPY 3
/ \
5 x
첫 번째 항목이 노드 객체 인 목록으로 표시됩니다.
[ADD,[MPY,5,x],3]
트리는 일반적으로 브랜치 앞에 노드가 분리되어 표시됩니다.
ADD[MPY[5,x],3]
생성기 함수는 이름이 지정된 (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 ++ 함수로 직접 컴파일 할 수 있습니다.
예, 해당 언어로 된 언어의 컴파일러를 작성할 수 있습니다. 아니요, 부트 스트랩하기 위해 해당 언어에 대한 첫 번째 컴파일러가 필요하지 않습니다.
부트 스트랩에 필요한 것은 언어의 구현입니다. 컴파일러 나 인터프리터 일 수 있습니다.
역사적으로 언어는 일반적으로 해석 된 언어 또는 컴파일 된 언어로 생각되었습니다. 통역사는 전자에 대해서만 작성되었고 컴파일러는 후자에 대해서만 작성되었습니다. 따라서 일반적으로 컴파일러가 언어로 작성된 경우 첫 번째 컴파일러는 부트 스트랩하기 위해 다른 언어로 작성된 다음 선택적으로 주제 언어에 맞게 컴파일러를 다시 작성합니다. 그러나 다른 언어로 통역사를 작성하는 것은 선택 사항입니다.
이것은 단지 이론적 인 것이 아닙니다. 나는 현재이 일을하고있다. 나는 내가 개발 한 언어 Salmon의 컴파일러를 연구하고 있습니다. 먼저 C에서 Salmon 컴파일러를 만들었고 이제 Salmon에서 컴파일러를 작성하고 있으므로 다른 언어로 작성된 Salmon 컴파일러를 사용하지 않고도 Salmon 컴파일러를 작동시킬 수 있습니다.