나는 프로그래밍 언어를 썼다. 방법도 있습니다.

지난 6 개월 동안 저는 Pinecone이라는 프로그래밍 언어를 연구하고 있습니다. 아직 성숙하지는 않지만 다음과 같이 사용할 수있는 기능이 이미 충분합니다.

  • 변수
  • 기능
  • 사용자 정의 구조

관심이 있다면 Pinecone의 방문 페이지 또는 GitHub 저장소를 확인하십시오.

나는 전문가가 아닙니다. 이 프로젝트를 시작했을 때, 내가하고있는 일에 대한 단서가 없었지만 여전히 그렇지 않습니다. 나는 언어 생성에 관한 수업을 전혀 들지 않고 온라인에서 조금만 읽었으며, 내가받은 많은 조언을 따르지 않았습니다.

그러나 나는 여전히 완전히 새로운 언어를 만들었습니다. 그리고 작동합니다. 그래서 나는 옳은 일을해야합니다.

이 글에서는 두루마리 아래에서 소스 코드를 마법으로 바꾸는 데 사용하는 Pinecone (및 기타 프로그래밍 언어) 파이프 라인을 보여 드리겠습니다.

또한 제가 한 일부 장단점과 내가 한 결정을 내린 이유에 대해서도 살펴 보겠습니다.

프로그래밍 언어 작성에 대한 완전한 자습서는 아니지만 언어 개발에 관심이있는 경우 좋은 출발점입니다.

시작하기

"어디서 시작해야할지 전혀 모른다"는 다른 개발자에게 언어를 쓰고 있다고 말할 때 많이 듣는 것입니다. 이것이 귀하의 반응 인 경우, 이제는 새로운 언어를 시작할 때 내려진 몇 가지 초기 결정과 단계를 거칩니다.

컴파일 및 해석

컴파일과 해석의 두 가지 주요 언어 유형이 있습니다.

  • 컴파일러는 프로그램이 수행 할 모든 작업을 파악하고이를 컴퓨터 코드 (컴퓨터가 실제로 빠르게 실행할 수있는 형식)로 바꾼 다음 나중에 실행할 수 있도록 저장합니다.
  • 통역사는 소스 코드를 한 줄씩 단계별로 진행하면서 진행 상황을 파악합니다.

기술적으로 모든 언어를 컴파일하거나 해석 할 수 있지만 일반적으로 특정 언어에 적합합니다. 일반적으로 해석은 더 융통성있는 반면 컴파일은 더 높은 성능을 갖는 경향이 있습니다. 그러나 이것은 매우 복잡한 주제의 표면을 긁는 것입니다.

나는 성능을 높이 평가하고, 고성능과 단순성을 모두 갖춘 프로그래밍 언어가 부족하다는 것을 알았으므로 Pinecone 용으로 컴파일했습니다.

이는 많은 언어 디자인 결정에 영향을 받기 때문에 조기에 결정해야하는 중요한 결정이었습니다 (예 : 정적 입력은 컴파일 된 언어에는 큰 이점이지만 해석 된 언어에는 그다지 중요하지 않습니다).

Pinecone은 컴파일을 염두에두고 설계되었다는 사실에도 불구하고 한동안 실행하는 유일한 방법 인 완전한 기능의 인터프리터를 가지고 있습니다. 이것에 대한 여러 가지 이유가 있는데, 나중에 설명하겠습니다.

언어 선택

나는 그것이 메타라는 것을 알고 있지만, 프로그래밍 언어 자체는 프로그램이기 때문에 언어로 작성해야합니다. 성능과 큰 기능 세트 때문에 C ++을 선택했습니다. 또한 실제로 C ++ 작업을 즐깁니다.

인터프리터 언어를 작성하는 경우, 인터프리터 및 인터프리터를 해석하는 인터프리터의 언어에서 성능이 저하되므로 컴파일 된 언어 (C, C ++ 또는 Swift와 같은)로 작성하는 것이 좋습니다.

컴파일 할 계획이라면 더 느린 언어 (예 : Python 또는 JavaScript)가 더 적합합니다. 컴파일 시간은 나쁠 수 있지만 제 생각에는 나쁜 런타임만큼 크지는 않습니다.

높은 수준의 디자인

프로그래밍 언어는 일반적으로 파이프 라인으로 구성됩니다. 즉, 여러 단계가 있습니다. 각 단계에는 구체적이고 잘 정의 된 방식으로 형식화 된 데이터가 있습니다. 각 단계에서 다음 단계로 데이터를 변환하는 기능도 있습니다.

첫 번째 단계는 전체 입력 소스 파일을 포함하는 문자열입니다. 마지막 단계는 실행할 수있는 것입니다. 우리가 단계별로 Pinecone 파이프 라인을 통과 할 때 이것은 모두 분명해질 것입니다.

렉싱

대부분의 프로그래밍 언어의 첫 번째 단계는 어휘 또는 토큰 화입니다. ‘Lex’는 어휘 분석의 줄임말로 많은 텍스트를 토큰으로 나누는 매우 멋진 단어입니다. '토큰 라이저'라는 단어가 훨씬 더 의미가 있지만 '어휘 분석기'는 어쨌든 그것을 사용한다는 것은 매우 재미 있습니다.

토큰

토큰은 언어의 작은 단위입니다. 토큰은 변수 또는 함수 이름 (일명 식별자), 연산자 또는 숫자 일 수 있습니다.

Lexer의 임무

어휘 분석기는 소스 파일의 전체 파일을 포함하는 문자열을 가져 와서 모든 토큰을 포함하는 목록을 내뱉습니다.

파이프 라인의 향후 단계에서는 원래 소스 코드를 참조하지 않으므로 렉서가 필요한 모든 정보를 생성해야합니다. 이 비교적 엄격한 파이프 라인 형식의 이유는 어휘 분석기가 주석을 제거하거나 무언가가 숫자 또는 식별자인지 감지하는 등의 작업을 수행 할 수 있기 때문입니다. 이 논리를 어휘 분석기 내부에 고정 시키려면 나머지 언어를 작성할 때 이러한 규칙에 대해 생각할 필요가 없으므로 이러한 유형의 구문을 한 곳에서 모두 변경할 수 있습니다.

굽힘

언어를 시작한 날 처음으로 쓴 것은 간단한 어휘 분석기였습니다. 얼마 후, 나는 어휘를 더 단순하고 덜 버그로 만드는 툴에 대해 배우기 시작했다.

이러한 도구는 주로 렉서를 생성하는 프로그램 인 Flex입니다. 언어 문법을 설명하는 특수 구문이있는 파일을 제공합니다. 그로부터 문자열을 작성하고 원하는 출력을 생성하는 C 프로그램을 생성합니다.

나의 결정

나는 한동안 내가 쓴 어휘 분석기를 유지하기로했다. 결국 Flex를 사용하면 종속성을 추가하고 빌드 프로세스를 복잡하게 만드는 데 충분하지 않은 상당한 이점을 얻지 못했습니다.

내 어휘 분석기는 수백 줄 밖에되지 않으며 거의 ​​문제가되지 않습니다. 내 자신의 렉서를 롤링하면 여러 파일을 편집하지 않고 언어에 연산자를 추가하는 기능과 같은 유연성이 향상됩니다.

파싱

파이프 라인의 두 번째 단계는 파서입니다. 파서는 토큰 목록을 노드 트리로 바꿉니다. 이 유형의 데이터를 저장하는 데 사용되는 트리를 AST (Abstract Syntax Tree)라고합니다. 적어도 Pinecone에서 AST에는 유형이나 식별자에 대한 정보가 없습니다. 단순히 구조화 된 토큰입니다.

파서 업무

파서는 렉서가 생성하는 순서화 된 토큰 목록에 구조를 추가합니다. 모호성을 막으려면 구문 분석기는 괄호와 연산 순서를 고려해야합니다. 단순히 구문 분석 연산자는 그리 어렵지 않지만 더 많은 언어 구조가 추가되면 구문 분석이 매우 복잡해질 수 있습니다.

바이슨

다시, 제 3 자 도서관과 관련하여 결정을 내 렸습니다. 주요 파싱 라이브러리는 Bison입니다. Bison은 Flex와 매우 유사하게 작동합니다. 문법 정보를 저장하는 사용자 정의 형식으로 파일을 작성한 다음, Bison은이를 사용하여 구문 분석을 수행 할 C 프로그램을 생성합니다. Bison을 사용하지 않았습니다.

관습이 더 나은 이유

lexer를 사용하면 내 코드를 사용하기로 결정한 것이 분명했습니다. 어휘 분석기는 나만의 '왼쪽 패드'를 쓰지 않는 것만큼이나 어리석은 느낌을주는 사소한 프로그램입니다.

파서는 다른 문제입니다. 내 Pinecone 파서의 길이는 현재 750 줄이며 처음 두 개는 휴지통이므로 세 개를 작성했습니다.

나는 원래 여러 가지 이유로 내 결정을 내 렸으며, 그 결정이 순조롭게 진행되지는 않았지만 대부분은 사실입니다. 주요 내용은 다음과 같습니다.

  • 워크 플로에서 컨텍스트 전환 최소화 : C ++과 Pinecone 간의 컨텍스트 전환은 Bison의 문법 문법을 포기하지 않으면 충분하지 않습니다.
  • 빌드를 단순하게 유지 : 빌드 전에 문법 변경이있을 때마다 Bison을 실행해야합니다. 이것은 자동화 될 수 있지만 빌드 시스템 간 전환시 어려움이됩니다.
  • 나는 멋진 똥을 만드는 것을 좋아합니다. Pinecone은 쉽지 않을 것이라고 생각했기 때문에 만들지 않았습니다. 왜 내가 스스로 할 수있을 때 중심적인 역할을 위임할까요? 사용자 정의 파서는 사소한 것이 아니지만 완전히 가능합니다.

처음에 나는 가능한 길을 가고 있는지 완전히 확신하지 못했지만 Walter Bright (C ++의 초기 버전 개발자 및 D 언어 제작자)가 말한 것에 대해 확신을 얻었습니다. 이야기:

“좀 더 논란의 여지가 있지만, 나는 어휘 분석기 나 파서 생성기와 소위“컴파일러 컴파일러”로 시간을 낭비하지 않을 것이다. 시간 낭비이다. 렉서 및 파서를 작성하는 것은 컴파일러 작성 작업의 작은 비율입니다. 생성기를 사용하면 직접 작성하는 것보다 시간이 오래 걸리고 생성기와 결혼하게됩니다 (컴파일러를 새 플랫폼으로 이식 할 때 중요 함). 그리고 발전기는 불행히도 오류가 많은 오류 메시지를내는 것으로 유명합니다.”

액션 트리

우리는 이제 보편적이고 보편적 인 용어의 영역을 떠났거나 적어도 나는 그 용어가 무엇인지 모릅니다. 내가 이해 한 바에 따르면,‘액션 트리’는 LLVM의 IR (중간 표현)과 가장 유사합니다.

액션 트리와 추상 구문 트리 사이에는 미묘하지만 매우 중요한 차이가 있습니다. 그들 사이에 차이가 있어야한다는 것을 알아내는 데 꽤 오랜 시간이 걸렸습니다 (파서의 다시 작성의 필요성에 기여했습니다).

액션 트리와 AST

간단히 말해서 액션 트리는 컨텍스트가있는 AST입니다. 해당 컨텍스트는 함수가 반환하는 유형과 같은 정보이거나 변수가 사용되는 두 곳이 실제로 동일한 변수를 사용한다는 것입니다. 이 모든 컨텍스트를 파악하고 기억해야하기 때문에 작업 트리를 생성하는 코드에는 많은 네임 스페이스 조회 테이블과 기타 항목이 필요합니다.

액션 트리 실행

액션 트리가 있으면 코드를 실행하는 것이 쉽습니다. 각 액션 노드에는 '실행'기능이 있습니다.이 함수는 일부 입력을 받고, 액션 (하위 액션 호출 가능)을 포함 해 액션의 출력을 반환합니다. 이것은 통역사입니다.

컴파일 옵션

“잠깐만 요!”“Pinecone이 컴파일 된 것이 아닙니까?”라고 말하는 것을 들었습니다. 그렇습니다. 그러나 컴파일하는 것은 해석하는 것보다 어렵습니다. 몇 가지 가능한 접근 방식이 있습니다.

내 컴파일러 만들기

처음에는 좋은 생각 인 것 같습니다. 나는 스스로 물건을 만드는 것을 좋아하고, 변명에 대한 변명이 변명하고 있습니다.

불행히도 이식 가능한 컴파일러를 작성하는 것은 각 언어 요소에 대한 일부 기계 코드를 작성하는 것만 큼 쉽지 않습니다. 많은 아키텍처와 운영 체제로 인해 개인이 크로스 플랫폼 컴파일러 백엔드를 작성하는 것은 비현실적입니다.

Swift, Rust 및 Clang의 팀조차도 자체적으로 모든 것을 귀찮게하고 싶지 않으므로 대신 모두 사용합니다 ...

LLVM

LLVM은 컴파일러 도구 모음입니다. 기본적으로 언어를 컴파일 된 실행 가능한 바이너리로 변환하는 라이브러리입니다. 완벽한 선택 인 것 같아서 바로 뛰어 들었습니다. 슬프게도 나는 물이 얼마나 깊었는지 확인하지 않고 즉시 익사했습니다.

LLVM은 어셈블리 언어는 어렵지 않지만 복잡한 복잡한 라이브러리는 어렵습니다. 사용이 불가능하지는 않지만 훌륭한 자습서가 있지만 Pinecone 컴파일러를 완전히 구현할 준비를하기 전에 연습을해야한다는 것을 깨달았습니다.

트랜스 필링

나는 일종의 컴파일 된 Pinecone을 원했고 그것을 빨리 원했기 때문에 일을 할 수있는 한 가지 방법 인 transpiling을 사용했습니다.

Pinecone to C ++ transpiler를 작성하고 GCC로 출력 소스를 자동으로 컴파일하는 기능을 추가했습니다. 이것은 현재 거의 모든 Pinecone 프로그램에서 작동합니다 (단, 몇 가지 경우가 있습니다). 특히 휴대용 또는 확장 가능한 솔루션은 아니지만 당분간 작동합니다.

미래

Pinecone을 계속 개발한다고 가정하면 조만간 LLVM 컴파일 지원이 제공됩니다. 나는 그것이 얼마나 많은 일을 하는가를 의심하지 않으며, 트랜스 필러는 결코 완전히 안정되지 않으며 LLVM의 이점은 엄청납니다. LLVM에서 샘플 프로젝트를 만들어 중단해야 할 때가 있습니다.

그때까지 통역사는 사소한 프로그램에 적합하며 C ++ 트랜스 파일링은 더 많은 성능이 필요한 대부분의 작업에 적합합니다.

결론

프로그래밍 언어를 조금 덜 신비롭게 만들었기를 바랍니다. 직접 만들고 싶다면 강력히 추천합니다. 알아 내야 할 구현 세부 사항이 많이 있지만 여기에 개요가 있으면 충분합니다.

다음은 시작에 대한 고급 조언입니다 (내가하는 일을 실제로 알지 못하므로 소금 한알과 함께 섭취하십시오).

  • 의심스러운 경우 해석하십시오. 해석 된 언어는 일반적으로 설계, 제작 및 학습이 더 쉽습니다. 나는 당신이하고 싶은 일을 알고 있다면 컴파일 된 것을 쓰지 말 것을 권장하지 않지만, 당신이 울타리에 있다면 나는 해석 할 것입니다.
  • 렉서 및 파서에 관해서는 원하는대로하십시오. 자신의 글을 쓰는 것에 반대하는 주장이 있습니다. 결국 디자인을 생각하고 모든 것을 현명한 방법으로 구현하면 실제로 중요하지 않습니다.
  • 내가 끝낸 파이프 라인에서 배우십시오. 현재 시행중인 파이프 라인을 설계하는 데 많은 시행 착오가있었습니다. 나는 AST, 액션 트리로 바뀌는 AST 및 기타 끔찍한 아이디어를 제거하려고 시도했습니다. 이 파이프 라인은 작동하므로 실제로 좋은 아이디어가 없으면 변경하지 마십시오.
  • 복잡한 범용 언어를 구현할 시간이나 동기가 없다면 Brainfuck과 같은 난해한 언어를 구현해보십시오. 이 통역사들은 몇 백 줄 정도로 짧을 수 있습니다.

Pinecone 개발에 관해서는 후회가 거의 없습니다. 나는 길을 따라 많은 나쁜 선택을했지만, 그러한 실수의 영향을받는 대부분의 코드를 다시 작성했습니다.

현재 Pinecone의 상태가 양호하고 기능이 향상되어 쉽게 개선 될 수 있습니다. 쓰기 Pinecone은 저에게 매우 교육적이고 즐거운 경험이었으며 이제 막 시작되었습니다.