avr-gcc 툴체인을 사용하여 C의 AVR 마이크로 컨트롤러에 대한 연습으로 언어와 같은 간단한 BASIC에 대한 작은 인터프리터를 작성하고 있습니다. 그러나 어휘 분석기와 파서를 작성하는 데 도움이 될 수있는 오픈 소스 도구가 있는지 궁금합니다.
내 Linux 상자에서 실행되도록 작성하려면 flex / bison을 사용할 수 있습니다. 이제 8 비트 플랫폼으로 제한 했으므로 모든 작업을 수작업으로해야합니까?
avr-gcc 툴체인을 사용하여 C의 AVR 마이크로 컨트롤러에 대한 연습으로 언어와 같은 간단한 BASIC에 대한 작은 인터프리터를 작성하고 있습니다. 그러나 어휘 분석기와 파서를 작성하는 데 도움이 될 수있는 오픈 소스 도구가 있는지 궁금합니다.
내 Linux 상자에서 실행되도록 작성하려면 flex / bison을 사용할 수 있습니다. 이제 8 비트 플랫폼으로 제한 했으므로 모든 작업을 수작업으로해야합니까?
답변:
ATmega328p를 대상으로하는 간단한 명령 언어에 대한 파서를 구현했습니다 . 이 칩에는 32k ROM과 2k RAM 만 있습니다. RAM은 확실히 더 중요한 제한 사항입니다. 아직 특정 칩에 묶여 있지 않은 경우 가능한 한 많은 RAM이있는 것을 선택하십시오. 이것은 당신의 삶을 훨씬 더 쉽게 만들 것입니다.
처음에는 flex / bison 사용을 고려했습니다. 나는 두 가지 주요 이유로이 옵션에 반대하기로 결정했습니다.
Flex & Bison을 거부 한 후 다른 생성기 도구를 찾았습니다. 제가 고려한 몇 가지 사항은 다음과 같습니다.
Wikipedia의 비교를 살펴볼 수도 있습니다 .
궁극적으로 나는 어휘 분석기와 파서를 직접 코딩했습니다.
파싱을 위해 재귀 하강 파서를 사용했습니다. 내 생각 아이라 박스터는 이미이 주제를 다루는의 적절한 일을하고 있으며, 튜토리얼의 많은 온라인이 있습니다.
내 어휘 분석기를 위해 모든 터미널에 대한 정규식을 작성하고 동등한 상태 머신을 다이어그램으로 작성하고 상태 goto
간 점프를 위해 's를 사용하여 하나의 거대한 함수로 구현했습니다 . 이것은 지루했지만 결과는 훌륭했습니다. 여담으로, goto
상태 머신을 구현하기위한 훌륭한 도구입니다 - 당신의 모든 상태가 바로 옆에 관련 코드에 대한 명확한 라벨을 가질 수 있습니다, 거기에 어떤 함수 호출 또는 상태 변수의 오버 헤드가 없으며, 그것에 대해 최대한 빨리 얻을 수 있습니다. C는 실제로 정적 상태 머신을 구축하기위한 더 나은 구조를 가지고 있지 않습니다.
생각할 것 : 렉서는 파서의 전문 화일뿐입니다. 가장 큰 차이점은 일반 문법은 어휘 분석에 일반적으로 충분하지만 대부분의 프로그래밍 언어에는 (대부분) 문맥없는 문법이 있다는 것입니다. 따라서 렉서를 재귀 하강 파서로 구현하거나 파서 생성기를 사용하여 렉서를 작성하는 것을 막을 수는 없습니다. 일반적으로 더 전문화 된 도구를 사용하는 것만 큼 편리하지 않습니다.
파서를 코딩하는 쉬운 방법을 원하거나 공간이 부족한 경우 재귀 하강 파서를 직접 코딩해야합니다. 이들은 본질적으로 LL (1) 파서입니다. 이것은 Basic처럼 "단순"한 언어에 특히 효과적입니다. (나는 70 년대에이 중 몇 가지를했다!). 좋은 소식은 라이브러리 코드가 포함되어 있지 않다는 것입니다. 당신이 쓰는 그대로.
이미 문법이 있다면 코딩하기가 매우 쉽습니다. 먼저 왼쪽 재귀 규칙을 제거해야합니다 (예 : X = XY). 이것은 일반적으로 매우 쉽기 때문에 연습으로 남겨 둡니다. (목록 형성 규칙에 대해이 작업을 수행 할 필요는 없습니다. 아래 설명 참조).
다음 형식의 BNF 규칙이있는 경우 :
X = A B C ;
"해당 구문 구조를 보았다"라는 부울을 반환하는 규칙 (X, A, B, C)의 각 항목에 대한 서브 루틴을 만듭니다. X의 경우 코드 :
subroutine X()
if ~(A()) return false;
if ~(B()) { error(); return false; }
if ~(C()) { error(); return false; }
// insert semantic action here: generate code, do the work, ....
return true;
end X;
A, B, C도 마찬가지입니다.
토큰이 터미널 인 경우 터미널을 구성하는 문자열의 입력 스트림을 확인하는 코드를 작성하십시오. 예를 들어 숫자의 경우 입력 스트림에 숫자가 포함되어 있는지 확인하고 입력 스트림 커서를 숫자를지나 앞으로 이동합니다. 버퍼 스캔 포인터를 전진 시키거나 전진시키지 않음으로써 버퍼에서 파싱하는 경우 특히 쉽습니다 (BASIC의 경우 한 번에 한 줄씩 가져 오는 경향이 있음). 이 코드는 본질적으로 파서의 렉서 부분입니다.
BNF 규칙이 재귀 적이라면 ... 걱정하지 마십시오. 재귀 호출을 코딩하십시오. 이것은 다음과 같은 문법 규칙을 처리합니다.
T = '(' T ')' ;
다음과 같이 코딩 할 수 있습니다.
subroutine T()
if ~(left_paren()) return false;
if ~(T()) { error(); return false; }
if ~(right_paren()) { error(); return false; }
// insert semantic action here: generate code, do the work, ....
return true;
end T;
대안이있는 BNF 규칙이있는 경우 :
P = Q | R ;
그런 다음 대체 선택 사항으로 P를 코딩합니다.
subroutine P()
if ~(Q())
{if ~(R()) return false;
return true;
}
return true;
end P;
때로는 목록 형성 규칙을 접하게됩니다. 이들은 재귀 적으로 남겨지는 경향이 있으며이 경우는 쉽게 처리됩니다. 기본 아이디어는 재귀보다는 반복을 사용하는 것이며, 이는 "명백한"방식으로이 작업을 수행하는 무한 재귀를 피하는 것입니다. 예:
L = A | L A ;
반복을 사용하여 다음과 같이 코딩 할 수 있습니다.
subroutine L()
if ~(A()) then return false;
while (A()) do { /* loop */ }
return true;
end L;
이런 식으로 하루나 이틀 만에 수백 개의 문법 규칙을 코딩 할 수 있습니다. 채워야 할 세부 사항이 더 있지만 여기의 기본 사항은 충분합니다.
공간 이 정말 부족한 경우 이러한 아이디어를 구현하는 가상 머신을 구축 할 수 있습니다. 그것이 제가 70 년대에 8K 16 비트 단어가 당신이 얻을 수 있었던 일이었을 때 한 일입니다.
이것을 수동으로 코딩하고 싶지 않다면 본질적으로 동일한 것을 생성하는 메타 컴파일러 ( Meta II )로 자동화 할 수 있습니다 . 이것들은 엄청난 기술적 재미이며 큰 문법의 경우에도 실제로 모든 작업을 수행합니다.
2014 년 8 월 :
"파서로 AST를 빌드하는 방법"에 대한 많은 요청을받습니다. 본질적 으로이 답변을 자세히 설명하는 이에 대한 자세한 내용은 다른 SO 답변 https://stackoverflow.com/a/25106688/120163을 참조하십시오.
2015 년 7 월 :
간단한 표현 평가기를 작성하려는 사람들이 많이 있습니다. 위의 "AST 빌더"링크가 제안하는 것과 동일한 종류의 작업을 수행하여이를 수행 할 수 있습니다. 트리 노드를 구축하는 대신 산술을 수행하십시오. 다음은 이런 식으로 수행 된 표현식 평가 기 입니다.
GCC는 다양한 플랫폼으로 크로스 컴파일 할 수 있지만 컴파일러를 실행중인 플랫폼에서 flex 및 bison을 실행합니다. 컴파일러가 빌드하는 C 코드를 뱉어냅니다. 결과 실행 파일이 실제로 얼마나 큰지 테스트하십시오. libfl.a
타겟에 크로스 컴파일 해야하는 런타임 라이브러리 등이 있습니다.
바퀴를 다시 발명하는 대신 LUA : www.lua.org를 살펴 보십시오 . 다른 소프트웨어에 내장되고 내장 시스템과 같은 소규모 시스템에서 사용되는 해석 언어입니다. 내장 된 절차 적 구문 구문 분석 트리, 제어 로직, 수학 및 변수 지원-수천 명의 다른 사람들이 이미 디버깅하고 사용한 것을 재발 명 할 필요가 없습니다. 그리고 확장 가능합니다. 즉, 자체 C 함수를 추가하여 문법에 추가 할 수 있습니다.