자바스크립트로 컴파일러 만들기 - ChoDragon9/posts GitHub Wiki

이 글은 TOAST UI | 자바스크립트로 컴파일러 만들기에 작성된 내용을 학습하면서 작성된 글이다.

컴파일러 동작 과정

1. 어휘 분석(Lexical Analysis, Tokenization)

입력을 토큰으로 분리하는 작업이다. 렉서 함수를 통해 코드 문자열을 작고 의미있는 토큰으로 분리한다.

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' }
// ]

2. 파싱(Parsing, Syntactical Analysis)

토큰들 사이의 관계를 찾는다. 연관된 토큰 끼리 그룹화 한다. 파서 함수를 통해 각 토큰들의 문법적 정보를 찾고, 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'
// }

3. 변형(Transformation)

변형 함수를 통해 컴파일러 결과물의 도메인에 유사한 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%)'
//       }
//     }
//   ]
// }

4. 코드 생성(Code Generate)

컴파일러의 마지막 단계이다. 생성 함수를 통해 이전 단계에서 만든 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>

5. 함수 병합

위에서 만든 함수들을 병합하면 컴파일러가 된다.

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>
⚠️ **GitHub.com Fallback** ⚠️