자바스크립트로 컴파일러 만들기 - ChoDragon9/posts GitHub Wiki
이 글은 TOAST UI | 자바스크립트로 컴파일러 만들기에 작성된 내용을 학습하면서 작성된 글이다.
입력을 토큰으로 분리하는 작업이다. 렉서 함수를 통해 코드 문자열을 작고 의미있는 토큰으로 분리한다.
const lexer = code => {
return code
.split(/\s+/)
.map(token => {
return isNaN(token)
? {type: 'word', value: token}
: {type: 'number', value: token};
});
};
const input = 'Paper 100';
const output = lexer(input);
// [
// { type: 'word', value: 'Paper' },
// { type: 'number', value: '100' }
// ]
토큰들 사이의 관계를 찾는다. 연관된 토큰 끼리 그룹화 한다. 파서 함수를 통해 각 토큰들의 문법적 정보를 찾고, AST(추상 구문 트리)라 부르는 객체를 만든다.
AST는 일반화된 형식이 있는 게 아니다. 필요에 따라 구조를 만들어 사용한다. 예를 들어 Vue는 파서 함수 baseParse의 반환값 AST는 RootNode다.
const AST_TYPE = {
CALL_EXPRESSION: 'CallExpression',
NUMBER_LITERAL: 'NumberLiteral',
};
const parser = tokens => {
const body = [];
while (tokens.length > 0) {
const currentToken = tokens.shift();
if (currentToken.type === 'word') {
switch (currentToken.value) {
case 'Paper' :
const expression = {
type: AST_TYPE.CALL_EXPRESSION,
name: 'Paper',
arguments: []
};
const argument = tokens.shift();
if (argument.type === 'number') {
expression.arguments.push({
type: AST_TYPE.NUMBER_LITERAL,
value: argument.value
});
body.push(expression)
}
break;
}
}
}
const AST = {
body,
type: 'Drawing',
};
return AST
};
const input = [
{type: 'word', value: 'Paper'},
{type: 'number', value: '100'}
];
const output = parser(input);
// {
// body: [
// {
// type: 'CallExpression',
// name: 'Paper',
// arguments: [
// {type: 'NumberLiteral', value: '100'}
// ]
// }
// ],
// type: 'Drawing'
// }
변형 함수를 통해 컴파일러 결과물의 도메인에 유사한 AST로 변환한다.
const transformer = ast => {
const body = ast.body.map((node) => {
switch (node.name) {
case 'Paper' :
const paperColor = 100 - Number(node.arguments[0].value);
return {
tag: 'rect',
attr: {
x: 0,
y: 0,
width: 100,
height: 100,
fill: `rgb(${paperColor}%,${paperColor}%,${paperColor}%)`
}
}
}
});
const svgAst = {
tag: 'svg',
attr: {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 100 100',
version: '1.1',
width: 100,
height: 100,
},
body,
};
return svgAst
};
const input = {
body: [
{
type: 'CallExpression',
name: 'Paper',
arguments: [
{type: 'NumberLiteral', value: '100'}
]
}
],
type: 'Drawing'
};
const output = transformer(input);
// {
// tag: 'svg',
// attr: {
// xmlns: 'http://www.w3.org/2000/svg',
// viewBox: '0 0 100 100',
// version: '1.1',
// width: 100,
// height: 100
// },
// body: [
// {
// tag: 'rect',
// attr: {
// x: 0, y: 0,
// width: 100, height: 100,
// fill: 'rgb(0%,0%,0%)'
// }
// }
// ]
// }
컴파일러의 마지막 단계이다. 생성 함수를 통해 이전 단계에서 만든 AST 기반으로 결과물을 만들어낸다.
const generateAttrString = attr => {
return Object
.entries(attr)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')
};
const generateElementString = body => {
return body
.map(node => `<${node.tag} ${generateAttrString(node.attr)}></${node.tag}>`)
.join(`\n\t`);
};
const generator = svgAst => {
const svgAttr = generateAttrString(svgAst.attr);
const elements = generateElementString(svgAst.body);
return `<svg ${svgAttr}>\n${elements}\n</svg>`
};
const input = {
tag: 'svg',
attr: {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 100 100',
version: '1.1',
width: 100,
height: 100
},
body: [
{
tag: 'rect',
attr: {
x: 0, y: 0,
width: 100, height: 100,
fill: 'rgb(0%,0%,0%)'
}
}
]
};
const output = generator(input);
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" version="1.1" width="100" height="100">
// <rect x="0" y="0" width="100" height="100" fill="rgb(0%,0%,0%)"></rect>
// </svg>
위에서 만든 함수들을 병합하면 컴파일러가 된다.
import {lexer} from './lexer';
import {parser} from './parser';
import {transformer} from './transformer';
import {generator} from './generator';
const compiler = (code) => {
const tokens = lexer(code);
const ast = parser(tokens);
const svgAst = transformer(ast);
const svg = generator(svgAst);
return svg;
};
const input = 'Paper 100';
const output = compiler(input);
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" version="1.1" width="100" height="100">
// <rect x="0" y="0" width="100" height="100" fill="rgb(0%,0%,0%)"></rect>
// </svg>