You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

989 lines
40 KiB

'use strict';
/**
* TTTTTTTTTTTTTTTTTTTTTTTHHHHHHHHH HHHHHHHHHEEEEEEEEEEEEEEEEEEEEEE
* T:::::::::::::::::::::TH:::::::H H:::::::HE::::::::::::::::::::E
* T:::::::::::::::::::::TH:::::::H H:::::::HE::::::::::::::::::::E
* T:::::TT:::::::TT:::::THH::::::H H::::::HHEE::::::EEEEEEEEE::::E
* TTTTTT T:::::T TTTTTT H:::::H H:::::H E:::::E EEEEEE
* T:::::T H:::::H H:::::H E:::::E
* T:::::T H::::::HHHHH::::::H E::::::EEEEEEEEEE
* T:::::T H:::::::::::::::::H E:::::::::::::::E
* T:::::T H:::::::::::::::::H E:::::::::::::::E
* T:::::T H::::::HHHHH::::::H E::::::EEEEEEEEEE
* T:::::T H:::::H H:::::H E:::::E
* T:::::T H:::::H H:::::H E:::::E EEEEEE
* TT:::::::TT HH::::::H H::::::HHEE::::::EEEEEEEE:::::E
* T:::::::::T H:::::::H H:::::::HE::::::::::::::::::::E
* T:::::::::T H:::::::H H:::::::HE::::::::::::::::::::E
* TTTTTTTTTTT HHHHHHHHH HHHHHHHHHEEEEEEEEEEEEEEEEEEEEEE
*
* SSSSSSSSSSSSSSS UUUUUUUU UUUUUUUUPPPPPPPPPPPPPPPPP EEEEEEEEEEEEEEEEEEEEEERRRRRRRRRRRRRRRRR
* SS:::::::::::::::SU::::::U U::::::UP::::::::::::::::P E::::::::::::::::::::ER::::::::::::::::R
* S:::::SSSSSS::::::SU::::::U U::::::UP::::::PPPPPP:::::P E::::::::::::::::::::ER::::::RRRRRR:::::R
* S:::::S SSSSSSSUU:::::U U:::::UUPP:::::P P:::::PEE::::::EEEEEEEEE::::ERR:::::R R:::::R
* S:::::S U:::::U U:::::U P::::P P:::::P E:::::E EEEEEE R::::R R:::::R
* S:::::S U:::::U U:::::U P::::P P:::::P E:::::E R::::R R:::::R
* S::::SSSS U:::::U U:::::U P::::PPPPPP:::::P E::::::EEEEEEEEEE R::::RRRRRR:::::R
* SS::::::SSSSS U:::::U U:::::U P:::::::::::::PP E:::::::::::::::E R:::::::::::::RR
* SSS::::::::SS U:::::U U:::::U P::::PPPPPPPPP E:::::::::::::::E R::::RRRRRR:::::R
* SSSSSS::::S U:::::U U:::::U P::::P E::::::EEEEEEEEEE R::::R R:::::R
* S:::::S U:::::U U:::::U P::::P E:::::E R::::R R:::::R
* S:::::S U::::::U U::::::U P::::P E:::::E EEEEEE R::::R R:::::R
* SSSSSSS S:::::S U:::::::UUU:::::::U PP::::::PP EE::::::EEEEEEEE:::::ERR:::::R R:::::R
* S::::::SSSSSS:::::S UU:::::::::::::UU P::::::::P E::::::::::::::::::::ER::::::R R:::::R
* S:::::::::::::::SS UU:::::::::UU P::::::::P E::::::::::::::::::::ER::::::R R:::::R
* SSSSSSSSSSSSSSS UUUUUUUUU PPPPPPPPPP EEEEEEEEEEEEEEEEEEEEEERRRRRRRR RRRRRRR
*
* TTTTTTTTTTTTTTTTTTTTTTTIIIIIIIIIINNNNNNNN NNNNNNNNYYYYYYY YYYYYYY
* T:::::::::::::::::::::TI::::::::IN:::::::N N::::::NY:::::Y Y:::::Y
* T:::::::::::::::::::::TI::::::::IN::::::::N N::::::NY:::::Y Y:::::Y
* T:::::TT:::::::TT:::::TII::::::IIN:::::::::N N::::::NY::::::Y Y::::::Y
* TTTTTT T:::::T TTTTTT I::::I N::::::::::N N::::::NYYY:::::Y Y:::::YYY
* T:::::T I::::I N:::::::::::N N::::::N Y:::::Y Y:::::Y
* T:::::T I::::I N:::::::N::::N N::::::N Y:::::Y:::::Y
* T:::::T I::::I N::::::N N::::N N::::::N Y:::::::::Y
* T:::::T I::::I N::::::N N::::N:::::::N Y:::::::Y
* T:::::T I::::I N::::::N N:::::::::::N Y:::::Y
* T:::::T I::::I N::::::N N::::::::::N Y:::::Y
* T:::::T I::::I N::::::N N:::::::::N Y:::::Y
* TT:::::::TT II::::::IIN::::::N N::::::::N Y:::::Y
* T:::::::::T I::::::::IN::::::N N:::::::N YYYY:::::YYYY
* T:::::::::T I::::::::IN::::::N N::::::N Y:::::::::::Y
* TTTTTTTTTTT IIIIIIIIIINNNNNNNN NNNNNNN YYYYYYYYYYYYY
*
* CCCCCCCCCCCCC OOOOOOOOO MMMMMMMM MMMMMMMMPPPPPPPPPPPPPPPPP IIIIIIIIIILLLLLLLLLLL EEEEEEEEEEEEEEEEEEEEEERRRRRRRRRRRRRRRRR
* CCC::::::::::::C OO:::::::::OO M:::::::M M:::::::MP::::::::::::::::P I::::::::IL:::::::::L E::::::::::::::::::::ER::::::::::::::::R
* CC:::::::::::::::C OO:::::::::::::OO M::::::::M M::::::::MP::::::PPPPPP:::::P I::::::::IL:::::::::L E::::::::::::::::::::ER::::::RRRRRR:::::R
* C:::::CCCCCCCC::::CO:::::::OOO:::::::OM:::::::::M M:::::::::MPP:::::P P:::::PII::::::IILL:::::::LL EE::::::EEEEEEEEE::::ERR:::::R R:::::R
* C:::::C CCCCCCO::::::O O::::::OM::::::::::M M::::::::::M P::::P P:::::P I::::I L:::::L E:::::E EEEEEE R::::R R:::::R
* C:::::C O:::::O O:::::OM:::::::::::M M:::::::::::M P::::P P:::::P I::::I L:::::L E:::::E R::::R R:::::R
* C:::::C O:::::O O:::::OM:::::::M::::M M::::M:::::::M P::::PPPPPP:::::P I::::I L:::::L E::::::EEEEEEEEEE R::::RRRRRR:::::R
* C:::::C O:::::O O:::::OM::::::M M::::M M::::M M::::::M P:::::::::::::PP I::::I L:::::L E:::::::::::::::E R:::::::::::::RR
* C:::::C O:::::O O:::::OM::::::M M::::M::::M M::::::M P::::PPPPPPPPP I::::I L:::::L E:::::::::::::::E R::::RRRRRR:::::R
* C:::::C O:::::O O:::::OM::::::M M:::::::M M::::::M P::::P I::::I L:::::L E::::::EEEEEEEEEE R::::R R:::::R
* C:::::C O:::::O O:::::OM::::::M M:::::M M::::::M P::::P I::::I L:::::L E:::::E R::::R R:::::R
* C:::::C CCCCCCO::::::O O::::::OM::::::M MMMMM M::::::M P::::P I::::I L:::::L LLLLLL E:::::E EEEEEE R::::R R:::::R
* C:::::CCCCCCCC::::CO:::::::OOO:::::::OM::::::M M::::::MPP::::::PP II::::::IILL:::::::LLLLLLLLL:::::LEE::::::EEEEEEEE:::::ERR:::::R R:::::R
* CC:::::::::::::::C OO:::::::::::::OO M::::::M M::::::MP::::::::P I::::::::IL::::::::::::::::::::::LE::::::::::::::::::::ER::::::R R:::::R
* CCC::::::::::::C OO:::::::::OO M::::::M M::::::MP::::::::P I::::::::IL::::::::::::::::::::::LE::::::::::::::::::::ER::::::R R:::::R
* CCCCCCCCCCCCC OOOOOOOOO MMMMMMMM MMMMMMMMPPPPPPPPPP IIIIIIIIIILLLLLLLLLLLLLLLLLLLLLLLLEEEEEEEEEEEEEEEEEEEEEERRRRRRRR RRRRRRR
*
* =======================================================================================================================================================================
* =======================================================================================================================================================================
* =======================================================================================================================================================================
* =======================================================================================================================================================================
*/
/**
* 오늘 우리는 함께 컴파일러를 작성할 것입니다. 하지만 여러분이 알고있는 컴파일러가 아니라... 매우
* 기능이 간소하고 작은 컴파일러입니다! 너무 작아서 주석을 모두 제거하면 이 파일의 실제 코드는 200줄 밖에
* 되지 않습니다.
*
* 우리는 어떤 lisp과 같은 함수 호출을 C와 같은 함수호출로 컴파일 할 것입니다.
*
* 만약 여러분이 위 언어에 익숙하지 않을 수 있습니다. 그래서 간단하게 소개하도록 하겠습니다.
*
* 만약 `add`와 `subtract`라는 두 함수가 있다면 다음과 같이 작성될 것입니다:
*
* LISP C
*
* 2 + 2 (add 2 2) add(2, 2)
* 4 - 2 (subtract 4 2) subtract(4, 2)
* 2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))
*
* 참 쉽죠?
*
* 좋습니다. 이것들이 바로 오늘 우리가 컴파일 할 것들입니다. 위 함수들은 완전한 LISP이나 C 구문은
* 아니지만, 현대 컴파일러의 주요한 부분을 많이 증명하기엔 충분합니다.
*/
/**
* 대부분의 컴파일러들은 세가지 중요한 단계를 가지고 있습니다: Parsing, Transformation,
* 그리고 Code Generation 입니다.
*
* 1. *Parsing*은 원시 코드를 추상적인 코드로 바꿔주는 단계입니다.
*
* 2. *Transformation*은 이 추상적인 코드를 가지고, 컴파일러가 원하는 것은 무엇이든지 할 수
* 있도록 변형합니다.
*
* 3. *Code Generation*은 변형된 추상젹인 코드를 새로운 코드로 바꿉니다.
*/
/**
* Parsing
* -------
*
* 구문 분석은 일반적으로 두 단게로 분류됩니다: Lexical Analysis and Syntactic Analysis.
*
* 1. *Lexical Analysis*는 원시 코드를 가져다가 tokenizer(또는 lexer)라고 불리는 것에 의헤
* 토큰이라고 불리는 것으로 분리됩니다.
*
* 토큰은 구문의 분리된 부분을 설명하는 작은 물체들의 배열입니다. 숫자, 라벨, 구두점, 조작자,
* 무엇이든 될 수 있습니다.
*
* 2. *Syntactic Analysis*는 토큰을 구문의 각 부분과 그 관계를 설명하는 표현으로 재구성합니다.
* 이것은 중간 표현 또는 추상 구문 트리로 알려져 있습니다.
*
* 추상 구문 트리, 즉 줄여서 AST는 코드를 다루기 쉽고 우리에게 많은 정보를 알려주는 방식으로
* 표현된 중첩된 객체 입니다.
*
* 다음 구문은:
*
* (add 2 (subtract 4 2))
*
* 토큰을 이용하면 다음과 같이 보일지도 모릅니다:
*
* [
* { type: 'paren', value: '(' },
* { type: 'name', value: 'add' },
* { type: 'number', value: '2' },
* { type: 'paren', value: '(' },
* { type: 'name', value: 'subtract' },
* { type: 'number', value: '4' },
* { type: 'number', value: '2' },
* { type: 'paren', value: ')' },
* { type: 'paren', value: ')' },
* ]
*
* 그리고 추상 구문 트리 (AST)는 아마 아래와 같이 보일겁니다:
*
* {
* type: 'Program',
* body: [{
* type: 'CallExpression',
* name: 'add',
* params: [{
* type: 'NumberLiteral',
* value: '2',
* }, {
* type: 'CallExpression',
* name: 'subtract',
* params: [{
* type: 'NumberLiteral',
* value: '4',
* }, {
* type: 'NumberLiteral',
* value: '2',
* }]
* }]
* }]
* }
*/
/**
* Transformation
* --------------
*
* 컴파일러의 다음 스테이지 유형은 변환입니다. 다시 말하지만, 이건 그냥 마지막 단계에서 AST를 가지고
* 변경하는것 뿐입니다. 같은 언어로 AST를 조작하거나 완전히 새로운 언어로 바꿀 수도 있습니다.
*
* AST를 어떻게 변화시킬지 살펴보도록 할까요.
*
* 여러분은 AST의 구성 요소들이 매우 비슷하게 생겼다는 것을 눈치챘을 겁니다.
* 이 오브젝트들은 타입 속성을 가지고 있습니다. 이것들 각각은 AST 노드라고 알려져 있습니다. 이 노드들은
* 트리의 분리된 한 부분을 설명하는 속성들을 가지고 있습니다.
*
* 우리는 "NumberLiteral"를 위한 노드를 가질 수 있습니다:
*
* {
* type: 'NumberLiteral',
* value: '2',
* }
*
* 또는 "CallExpression"노드도 있을 수 있습니다:
*
* {
* type: 'CallExpression',
* name: 'subtract',
* params: [...nested nodes go here...],
* }
*
* AST를 변환할 때 우리는 속성을 추가/제거/교체하여 노드를 조작할 수 있고, 새로운 노드를 추가하거나,
* 노드를 제거하거나, 기존 AST를 그대로 두고 그것에 기반한 완전히 새로운 노드를 만들 수 있습니다.
*
* 우리는 새로운 언어를 목표로 하고 있기 때문에, 완전히 새로운 AST를 만드는 데 초점을 맞출 것입니다.
*
* Traversal
* ---------
*
* 모든 노드를 탐색하기 위해서는 우리는 노드들을 순회할 필요성이 있습니다. 이 순회 과정은 AST의 각각의
* 노드들을 깊이를 우선하여 탐색합니다.
*
* {
* type: 'Program',
* body: [{
* type: 'CallExpression',
* name: 'add',
* params: [{
* type: 'NumberLiteral',
* value: '2'
* }, {
* type: 'CallExpression',
* name: 'subtract',
* params: [{
* type: 'NumberLiteral',
* value: '4'
* }, {
* type: 'NumberLiteral',
* value: '2'
* }]
* }]
* }]
* }
*
* 위 AST를 위해 우리는:
*
* 1. Program - AST의 최상위 레벨에서 시작
* 2. CallExpression (add) - Program의 body에서 첫번째 요소로 이동
* 3. NumberLiteral (2) - CallExpression의 첫 번째 요소로 이동
* 4. CallExpression (subtract) - CallExpression의 두 번째 요소로 이동
* 5. NumberLiteral (4) - CallExpression의 첫 번째 요소로 이동
* 6. NumberLiteral (2) - CallExpression의 두 번째 요소로 이동
*
* 만약 우리가 이 AST를 직접 조작한다면, 별도의 AST를 만드는 대신에 다양한 종류의 추상화를 도입할 겁니다.
* 하지만 트리안에 있는 각각의 노드들을 방문하는 것만으로도 우리가 하려는 일을 할 수 있습니다.
*
* "방문"이라는 단어를 사용하는 이유는, 객체 구조의 요소에 대한 작업을 어떻게 표현해야 하는지에 대해
* 패턴이 있기 때문입니다.
*
* Visitors
* --------
*
* 기본적인 아이디어는 다른 노드 타입을 가지고 있는 방문자 객체를 만드는 것입니다.
*
* var visitor = {
* NumberLiteral() {},
* CallExpression() {},
* };
*
* 우리가 AST를 순회할 때, 매칭되는 타입의 노드에 방문했을 경우, 해당하는 메서드를 visitor에서 호출
* 할 것입니다.
*
* 이것을 유용하게 만들기 위해 우리는 노드와 상위 노드에 대한 참조를 전달할 것입니다.
*
* var visitor = {
* NumberLiteral(node, parent) {},
* CallExpression(node, parent) {},
* };
*
* 그러나, 우리는 "exit"를 호출할 가능성에 대해서도 고려해야합니다. 리스트로 부터 우리의 트리 구조에 대해
* 상상해 볼까요:
*
* - Program
* - CallExpression
* - NumberLiteral
* - CallExpression
* - NumberLiteral
* - NumberLiteral
*
* 계속 순회를 하다보면, 언젠가는 마지막 가지에 닿을 것입니다. 각 가지의 끝에 도달했을 때, 우리는 해당 트리에서
* "exit" 합니다. 그래서 각각의 트리를 내려가는것을 "enter", 다시 위로 올라가는 것을 "exit"라 합니다.
*
* -> Program (enter)
* -> CallExpression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Call Expression (enter)
* -> Number Literal (enter)
* <- Number Literal (exit)
* -> Number Literal (enter)
* <- Number Literal (exit)
* <- CallExpression (exit)
* <- CallExpression (exit)
* <- Program (exit)
*
* 이를 위해 visitor의 최종 형태는 다음과 같습니다:
*
* var visitor = {
* NumberLiteral: {
* enter(node, parent) {},
* exit(node, parent) {},
* }
* };
*/
/**
* Code Generation
* ---------------
*
* 컴파일러의 마지막 단계는 코드 생성입니다. 때때로 컴파일러는 변환과 겹치는 일을 하곤 하지만, 대부분의
* 부분 코드 생성은 우리의 AST와 문자열 식별 코드를 다시 빼내는 것을 의미합니다.
*
* 코드 제너레이터는 여러가지 다른 방식으로 작동하며, 어떤 컴파일러는 이전과 다른 토큰을 재사용할 것이고,
* 다른 컴파일러는 노드를 선형적으로 출력할 수 있도록 코드의 별도 표현을 만들 것입니다. 그러나 여기서
* 말할 수 있는 것은 우리가 방금 만든 것과 동일한 AST를 사용하게 될 것이며, 이것이 우리가 집중해야 할
* 부분 입니다.
*
* 효과적으로 우리의 코드 제너레이터는 AST의 모든 다양한 노드 유형을 "출력"하는 방법을 알게 될 것이며,
* 모든 것이 하나의 긴 코드 문자열로 인쇄될 때까지 중첩된 노드를 인쇄하기 위해 스스로를 재귀적으로 호출할
* 것입니다.
*/
/**
* 이게 다입니다! 이것들을 조합하면 컴파일러가 됩니다.
*
* 그렇다고 해서 모든 컴파일러들이 내가 여기서 말한 것과 똑같이 생겼다는 뜻은 아닙니다. 컴파일러는 여러 가지
* 목적을 가지고 있으며, 여기서 상세히 설명한 것보다 더 많은 단계가 필요할지도 모릅니다.
*
* 그러나 이제 여러분들은 대부분의 컴파일러가 어떻게 생겼는지 대해 일반사람들보다 높은 수준의 생각을
* 가져야 합니다.
*
* 이제 모든것을 설명했으니까, 여러분들은 여러분들만의 컴파일러를 작성할 수 있습니다. 그렇죠?
*
* 농담입니다, 그것이 바로 이글을 작성한 이유니까요 :P
*
* 그러면 시작해 볼까요...
*/
/**
* ============================================================================
* (/^▽^)/
* THE TOKENIZER!
* ============================================================================
*/
/**
* 우리는 첫번재 단계로 parsing, lexical analysis를 tokenizer를 이용하여 시작할 겁니다.
*
* 우리는 앞에서 보았던 코드를 가지고 토큰의 배열로 분해할 것입니다.
*
* (add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
*/
// 코드 문자열을 받는것으로 부터 시작합니다.
// 그리고 두가지 것들을 설정합니다.
function tokenizer(input) {
// `current` 변수는 위치를 추적합니다. 마치 커서처럼요.
let current = 0;
// `tokens` 배열은 토큰들이 들어가는 배열입니다.
let tokens = [];
// `current` 변수를 내부에서 원하는 만큼 증가시키도록 설정하는 "while" 반목문을 만드는 것으로
// 시작합니다.
//
// 토큰의 길이가 어느 정도 될 수 있으므로 하나의 반복문 안에서 여러 번 `current`를 증가시키고자
// 할 수도 있기 때문입니다.
while (current < input.length) {
// 현재 문자를 char에 저장합니다.
let char = input[current];
// 우리가 가장 먼저 확인하고 싶은 것은 개방된 괄호 입니다. 나중에 `CallExpression`에
// 쓰이겠지만 현재로스는 문자에만 신경을 쓰도록 하겠습니다.
//
// 개방된 괄호가 있는지 체크합니다:
if (char === '(') {
// 만약 if을 통과한다면 개방된 괄호는 `paren` type과 값을 새 토큰으로 정의합니다.
tokens.push({
type: 'paren',
value: '(',
});
// `current를 증가시킵니다`
current++;
// `continue`를 통해 새로운 반복문을 시작합니다.
continue;
}
// 다음으로 우리는 닫힌 괄호를 확인합니다. 우리는 이전과 같은 방법을 사용합니다: 닫힌 괄호를 확인하면
// 새로운 토큰을 추가하고 `current`를 증가시키고, `continue`를 실행합니다.
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 이제 공백에 대해 확인해 볼 겁니다. 우리는 공백을 문자 분리를 위해 사용하지만, 실제로 토큰으로
// 저장하는 것은 중요하지 않습니다. 여기서는 공백은 거릅니다.
//
// 따라서 여기서는 단지 공백인지 체크한 후, `continue`를 호출합니다.
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 다음은 숫자 토큰 타입입니다. 이것은 앞에서 보았던 것과는 다릅니다. 왜냐하면 숫자는 어떤 숫자의
// 문자가 될 수 있기 때문이고 우리는 모든 숫자들을 하나의 숫자로 저장하기를 원합니다.
//
// (add 123 456)
// ^^^ ^^^
// 오직 2개의 토큰으로 분리되기를 원합니다.
//
// 따라서 우리는 숫자의 가장 첫부분을 만났을 때부터 시작합니다.
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// `value`변수는 문자들을 저장합니다.
let value = '';
// 이제 문자가 숫자가 아닐때까지 계속해서 `value`에 더하며 `current`값을 증가시킵니다.
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 그 후, 완성된 `number` 토큰을 `tokens`배열에 넣습니다.
tokens.push({ type: 'number', value });
// 계속 진행합니다.
continue;
}
// 우리는 또한 큰따옴표(")로 둘러쌓여진 텍스트 문자열도 지원해야합니다.
//
// (concat "foo" "bar")
// ^^^ ^^^ 문자열 토큰들
//
// 따옴표로 시작하는지 체크합니다:
if (char === '"') {
// 문자열 토큰을 저장할 `value`변수를 만듭니다.
let value = '';
// 큰 따옴표는 무시합니다.
char = input[++current];
// 큰 따옴표가 나오기 전까지 문자들을 저장합니다.
while (char !== '"') {
value += char;
char = input[++current];
}
// 닫는 큰따옴표는 무시합니다.
char = input[++current];
// 완성된 `string` 토큰을 `tokens` 배열에 추가합니다.
tokens.push({ type: 'string', value });
continue;
}
// 마지막 토큰 타입은 `name` 입니다.
// 이것은 숫자 대신 글자의 순서, 즉 우리의 lisp 구문에 있는 함수의 이름입니다.
//
// (add 2 4)
// ^^^
// Name token
//
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
// 계속 반복하면서 value에 문자들을 저장합니다.
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 그리고 value를 `name` 타입으로 저장한 후 계속 진행합니다.
tokens.push({ type: 'name', value });
continue;
}
// 마지막으로, 매칭되지않는 문자들이 있다면, error를 던지고 완전히 종료합니다.
throw new TypeError('I dont know what this character is: ' + char);
}
// 그리고 `tokenizer`는 간단하게 tokens 배열을 반환하고 종료합니다.
return tokens;
}
/**
* ============================================================================
* ヽ/❀o ل͜ o\ノ
* THE PARSER!!!
* ============================================================================
*/
/**
* 우리들의 parser는, 토큰 배열을 가져다가 AST로 바꿉니다.
*
* [{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
*/
// 자 이제 `tokens` 배열을 인자로 받는 `parser`를 정의합시다.
function parser(tokens) {
// `current` 변수는 위치를 저장하는 커서로 사용됩니다.
let current = 0;
// 그러나 여기서는 `while` 반복문 대신 재귀를 사용합니다. 그래서 우리는 `walk` 함수를 정의할
// 겁니다.
function walk() {
// walk 함수 안에서 `current` 토큰을 가져와서 시작합니다.
let token = tokens[current];
// 우리는 `number` 토큰을 시작으로 각 토큰을 다른 코드 경로로 나눌 것입니다.
//
// `number` 토큰인지 확인합니다.
if (token.type === 'number') {
// 만약 우리가 그 코튼을 가지고 있다면 `current`를 증가시킵니다.
current++;
// 그리고 우리는 토큰 값과 `NumberLiteral`로 불리는 타입을 가진 새로운 AST노드를 반환
// 합니다.
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 만약 우리가 문자열을 가지고 있다면 `StringLiteral` 노드를 만듭니다.
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
// 다음으로 우리는 CallExpressions을 찾을 겁니다. 이 작업은 열린 괄호를 만났을 때 실행 됩니다.
if (
token.type === 'paren' &&
token.value === '('
) {
// AST에서 괄호를 생략하도록 `current`를 증가시킵니다.
token = tokens[++current];
// `CallExpression` 타입의 기본 노드를 만듭니다. 그리고 열린 괄호 뒤에 있는 토큰이
// 함수의 이름이기 때문에 현재 토큰의 값으로 설정합니다.
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// 이제 이름 토큰을 건너뛰기 위해서 `current`를 증가시킵니다.
token = tokens[++current];
// 그리고 이제 우리는 닫힌 괄호를 만날 때까지 각각의 코튼들이 `CallExpression`의 `params`이
// 되도록 순회하고자 한다.\
//
// 이제 재귀안으로 들어가 보자. 무한히 중첩될 수 있는 노드 집합을 분석하려고 시도하는 대신,
// 문제를 해결하기 위해 재귀 작업에 의존할 겁니다.
//
// 이것에 대해 설명하기 위해서, Lisp 코드를 가져와보자. 여러분들은 addd 내부에 포함된
// 숫자들과 `CallExpression`을 볼 수 있을 것입니다.
//
// (add 2 (subtract 4 2))
//
// 여러분들은 또한 여러개의 닫힌 괄호를 가지고 있다는 것도 눈치 챘을 것입니다.
//
// [
// { type: 'paren', value: '(' },
// { type: 'name', value: 'add' },
// { type: 'number', value: '2' },
// { type: 'paren', value: '(' },
// { type: 'name', value: 'subtract' },
// { type: 'number', value: '4' },
// { type: 'number', value: '2' },
// { type: 'paren', value: ')' }, <<< 닫힌 괄호
// { type: 'paren', value: ')' }, <<< 닫힌 괄호
// ]
//
//
// 우리는 중첩된 `walk` 함수에 의존하여 중첩된 CallExpression을 넘어 `current` 변수를
// 증가시킬 것입니다.
// 따라서 우리는 `paren` 타입과 닫힌 괄호의 `value`를 가진 토큰을 만날 때까지 반복되는
// `while` 반복문을 만들 것입니다.
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// `walk` 함수를 호출한 후 반횐되는 `node`를 `node.params`에 넣습니다.
node.params.push(walk());
token = tokens[current];
}
// 마지막으로 우리는 `current`를 증가시켜 닫힌 괄호를 생략합니다.
current++;
// 그리고 node를 반환합니다.
return node;
}
// 다시 말씀드리지만, 토큰의 종류를 인식하지 못했을 경우, error를 던집니다.
throw new TypeError(token.type);
}
// 이제, 우리들은 `Program`을 노드로 갖는 새로운 AST를 만들었습니다.
let ast = {
type: 'Program',
body: [],
};
// 그리고 우리는 `walk`함수를 이용하여 `ast.body` 함수를 빠르게 채워나갈 수 있습니다.
//
// 반복문 안에서 하는 이유는 우리 프로그램이 중첩이 아닌 `CallExpression`를 연달아 가질
// 수 있기 때문입니다.
//
// (add 2 2)
// (subtract 4 2)
//
while (current < tokens.length) {
ast.body.push(walk());
}
// 마지막으로 ast를 반환합니다.
return ast;
}
/**
* ============================================================================
* ⌒(❀>◞౪◟<❀)⌒
* THE TRAVERSER!!!
* ============================================================================
*/
/**
* 우리는 이제 AST를 가지고 visitor와 함께 다른 노드를 방문하려고 합니다. 우리는 일치하는 유형의
* 노드를 만날 때마다 visitor에 대해 메서드를 호출할 수 있어야 합니다.
*
* traverse(ast, {
* Program: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* CallExpression: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* NumberLiteral: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
* });
*/
// 따라서 우리는 AST를 순회하는 함수를 정의합니다. 이 함수 안에 두 함수를
// 정의할 것입니다.
function traverser(ast, visitor) {
// `traverseArray` 함수는 배열로 반복할 수 있는 함수이며, 다음에 정의될 함수를
// 호출합니다: `traverseNode`.
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// `traverseNode`는 `node`와 그것의 `perent`노드를 받습니다. 따라서 visitor 매서드들을 모두
// 넘길 수 있습니다.
function traverseNode(node, parent) {
// 우리는 먼저 visitor에 대한 메서드의 존재 여부를 'type'을 테스트하는것으로 시작합니다.
let methods = visitor[node.type];
// 이 노드 타입에 대한 `enter` 메서드가 있다면 `node`와 `parent`와 함께 호출할 것입니다.
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 다음으로 현재 노드들을 유형별로 구분합니다.
switch (node.type) {
// 우리는 최상위 경로인 `Program`에서 시작합니다. Program 노드들은 노드들의 배열을 가지고
// 있는 body라는 속성을 가지고 있기 때문에, 우리는 `traverseArray`를 호출하여 그들을
// 순회할 것입니다.
//
// (`traverseArray`는 `traverseNode`를 호출하므로, 따라서 우리는 트리를 재귀적으로 돌고
// 있다는 것을 기억해야합니다)
case 'Program':
traverseArray(node.body, node);
break;
// 다음으로 `CallExpression`는 `params`과 함께 똑같은 작업을 합니다.
case 'CallExpression':
traverseArray(node.params, node);
break;
// `NumberLiteral`과 `StringLiteral`의 경우 방문해야할 자식 노드가 없으므로 넘어
// 갑니다.
case 'NumberLiteral':
case 'StringLiteral':
break;
// 노드 타입을 인식할수 없다면, error를 던집니다.
default:
throw new TypeError(node.type);
}
// 만약 'exit' 메서드가 존재한다면, `node`와 `parent`와 함께 호출합니다.
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 마지막으로 traverser 함수는 `parent`가 없는 ast와 함쎄 `traverseNode`를 호출합니다.
// 왜냐하면 최상위 AST는 부모를 가지고 있지 않기 때문입니다.
traverseNode(ast, null);
}
/**
* ============================================================================
* ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽
* THE TRANSFORMER!!!
* ============================================================================
*/
/**
* 다음으로, transformer입니다. transformer는 AST를 가지고 visitor와 함께 새로운 ast를
* 만들 것입니다.
*
* ----------------------------------------------------------------------------
* Original AST | Transformed AST
* ----------------------------------------------------------------------------
* { | {
* type: 'Program', | type: 'Program',
* body: [{ | body: [{
* type: 'CallExpression', | type: 'ExpressionStatement',
* name: 'add', | expression: {
* params: [{ | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }, { | name: 'add'
* type: 'CallExpression', | },
* name: 'subtract', | arguments: [{
* params: [{ | type: 'NumberLiteral',
* type: 'NumberLiteral', | value: '2'
* value: '4' | }, {
* }, { | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }] | name: 'subtract'
* }] | },
* }] | arguments: [{
* } | type: 'NumberLiteral',
* | value: '4'
* ---------------------------------- | }, {
* | type: 'NumberLiteral',
* | value: '2'
* | }]
* (죄송하지만 너무 길어서 생략합니다. ) | }
* | }
* | }]
* | }
* ----------------------------------------------------------------------------
*/
// 따라서 transformer 함수는 lisp ast를 받습니다.
function transformer(ast) {
// 이전 AST와 마찬가지로 Program 노드를 갖는 newAst를 만들 것입니다.
let newAst = {
type: 'Program',
body: [],
};
// Next I'm going to cheat a little and create a bit of a hack. We're going to
// use a property named `context` on our parent nodes that we're going to push
// nodes to their parent's `context`. Normally you would have a better
// abstraction than this, but for our purposes this keeps things simple.
//
// Just take note that the context is a reference *from* the old ast *to* the
// new ast.
ast._context = newAst.body;
// 우리는 ast 그리고 visitor와 함께 traverser 함수를 호출하면서 시작합니다.
traverser(ast, {
// 첫번째 visitor 메서드는 `NumberLiteral` 입니다.
NumberLiteral: {
// 방문할때 메서드를 정의합니다.
enter(node, parent) {
// `NumberLiteral` 이름의 새 노드를 만든 후 부모의 context에 추가합니다.
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 다음으로 `StringLiteral` 입니다.
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 다음은 `CallExpression`.
CallExpression: {
enter(node, parent) {
// We start creating a new node `CallExpression` with a nested
// `Identifier`.
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// Next we're going to define a new context on the original
// `CallExpression` node that will reference the `expression`'s arguments
// so that we can push arguments.
node._context = expression.arguments;
// Then we're going to check if the parent node is a `CallExpression`.
// If it is not...
if (parent.type !== 'CallExpression') {
// We're going to wrap our `CallExpression` node with an
// `ExpressionStatement`. We do this because the top level
// `CallExpression` in JavaScript are actually statements.
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// Last, we push our (possibly wrapped) `CallExpression` to the `parent`'s
// `context`.
parent._context.push(expression);
},
}
});
// transformer 함수가 새로운 newAst를 반환함으로써 완성됩니다.
return newAst;
}
/**
* ============================================================================
* ヾ(〃^∇^)ノ♪
* THE CODE GENERATOR!!!!
* ============================================================================
*/
/**
* 이제 마지막 단계로 넘어갑시다: 코드 제너레이터.
*
* 코드제너레이터는 재귀적으로 호출하여 트리의 각 노드들을 하나의 거대한 문자열로 출력할 것입니다.
*/
function codeGenerator(node) {
// `node`의 타입에 따라 진행합니다.
switch (node.type) {
// `Program` 노드일 경우입니다. `body`의 각 노드를 맵으로 돌면서 코드 제너레이터를 거치면서
// 새로운 줄로 합칩니다.
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// `ExpressionStatement`의 경우, 중첩된 코드 제네레이터를 호출합니다. 그리고 세미콜론을
// 추가합니다...
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // << (...우리는 *올바른* 방식으로 코드화 하려하기 때문입니다)
);
// `CallExpression`의 경우, `callee`를 출력한 후 괄호를 열고, `arguments`를 맵으로
// 돌면서 코드 제네레이터를 통과시킨 후, 콤마와 함께 join한후 괄호를 닫습니다.
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// `Identifier`의 경우 `node`의 이름을 반환합니다.
case 'Identifier':
return node.name;
// `NumberLiteral`의 경우, `node`의 값을 반환합니다.
case 'NumberLiteral':
return node.value;
// `StringLiteral`의 경우, 큰 따옴표로 둘러쌓인 `node`의 값을 반환합니다.
case 'StringLiteral':
return '"' + node.value + '"';
// 그리고 노드로 인식하지 못한 결우에, error를 던집니다.
default:
throw new TypeError(node.type);
}
}
/**
* ============================================================================
* (۶* ‘ヮ’)۶”
* !!!!!!!!THE COMPILER!!!!!!!!
* ============================================================================
*/
/**
* 마침내! 우리는 '컴파일러' 함수를 만들 것입니다. 여기에 우리가 만든 모든 것들을 파이프라인으로
* 연결합니다.
*
* 1. input => tokenizer => tokens
* 2. tokens => parser => ast
* 3. ast => transformer => newAst
* 4. newAst => generator => output
*/
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
// 그리고 output을 리턴합니다
return output;
}
/**
* ============================================================================
* (๑˃̵ᴗ˂̵)و
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!여러분이 만든거에요!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* ============================================================================
*/
// 이제 모든것을 exports 합니다...
module.exports = {
tokenizer,
parser,
traverser,
transformer,
codeGenerator,
compiler,
};