컴파일러 흉내내기

ㅎㄱㅎ·2020년 9월 6일
0

[ABC] : HELLO를 { ABC : "HELLO" }로 만들고 싶다.

https://ui.toast.com/weekly-pick/ko_20161107/ <-100% 참고 해서 일부 이름도 겹칩니다, 이 글을 보시는게 더 도움 될 수도 있습니다.

경고 이건 맛보기 입니다. 모든 상황에서 잘 되고, 에러도 내주고 하는 정교한 컴파일러를 만드는 목적이 아닙니다.

[ABC] : HELLO를 "빈 객체에 ABC라는 키를 생성하고 value로 HELLO를 세팅해줘" 라는 간단한 프로그래밍 구문이라고 생각하면 지금 하는 작업이 컴파일러 흉내(?)는 낼 수 있다고 생각 해 볼수도 있지 않을까요~?

[ABC] : HELLO 를 입력했다고 치면

[
  {type : "action" , value : "keyGen" }, //키 만들꼬야
  {type : "value" , value : "ABC", // 이걸로
  {type : "setValue", value : "setValue"}, //값 넣을꼬야
  {type : "value" , value : "HELLO"} // 이걸로
] (Tokenize..)

Tokenize 결과를 Parsing 하여 AST로 만들고,

그거에 따라 실제 Object를 만들어 낸다
의 과정 순으로 진행된다.

Tokenize해주는 lexer함수는 대강 이런 느낌이다(스트링을 쪼개서 처리 할 수 있게 꼬리표를 달아주는 거라고 생각하면 될 것 같습니다)

interface token {
    type: string;
    value : string;
}
function lexer (code:string) : Array<token>{    
    const trimCode = code.replace("/\s\r\n\t/g", "");  
    const codeArray = Array.from(trimCode); //요기 콜백에다가 해도 된다. (첫번째 map 함수)
    return codeArray.map((char, index)=>{
          if(char === "[" && index > 0) char = " " + char;
          else if(char === "]" && index < codeArray.length - 1) char = char + " ";
          else if(char === ":") char = char + " ";
          return char;
      })
    .join("")
    .split(/\s+/)
    .filter((t)=>{ return t.length > 0 })
    .map((t)=>{
        if(t.includes("[") || t.includes("]")){
            if(t.includes("[") && t.includes("]")){
                return [
                        {type : "action", value : "keyGen"}, 
                        {type: "value", value : t.slice(1, t.length - 1)}
                    ];
            }else if(!t.includes("[") || !t.includes("]")){      
                throw new Error("syntax error");
            }else if(t.includes(":")){
                throw new Error("syntax error");
            }
        }else if(t.includes(":")){
            return {type : "action", value : "setValue"};
        }
        return {type : "value", value : t};
    })
    .flat();
  }

목적대로 작성하면 되고, 꼬리표만 잘 달아주면 될 것 같다. 컴파일러를 새로 만드는게 아니고 필요한데 쓰는데 한번 맛보기로 응용해보는 것이니까.(초장부터 너무 멀리 생각하면 될 일도 안된다!) 결과는 아래와 같습니다.

action - value의 순서가 일관되게 tokenize를 하였다.
키 만들래 -> 이걸로 -> 값 넣을래 -> 이걸로 -> 키 만들래 -> 이걸로 -> .. -> ..
이 순서가 맞고, 이 순서에서 어긋났다면 잘못 입력한 것임을 알 수 있다 .

이제 꼬리표를 잘 달아줬으니 Parsing 함수를 작성해 봅시다.

function parser(tokens){
    var AST = {
        type : "createObject",
        body : []
    }

    while(tokens.length){
        var current = tokens.shift();

        if(current.type === "action"){
            switch (current.value){
                case "keyGen" : 
                    if(AST.body.length && AST.body[AST.body.length - 1].type !== "CallExpression") throw new Error("syntax Error")
                    var expression = {
                        type : "CallExpression",
                        name : "keyGen",
                        arguments : []
                    }

                    var argument = tokens.shift();
                    expression.arguments.push({
                        type : "StringLiteral",
                        value : argument.value
                    })

                    AST.body.push(expression);
                    break;
                case "setValue" :
                    if(AST.body.length && (AST.body[AST.body.length - 1].type !== "CallExpression" || AST.body[AST.body.length - 1].name !== "keyGen")) throw new Error("syntax Error")
                    var expression = {
                        type : "CallExpression",
                        name : "setValue",
                        arguments : []
                    }
                    var argument = tokens.shift();
                    if(argument.type !== "value"){
                      console.error(": expected value, check : ")
                    }
                    expression.arguments.push({
                        type : "StringLiteral",
                        value : argument.value
                    })
                    AST.body.push(expression);
                    break;                  
            }
        }else if(current.type === "value") throw new Error("syntax Error");
    }
    return AST;
}

이코드는 AST에 무조건 CallExpression만 오게 강제 하고 있습니다. 그렇게 함으로써
[] 다음에 : 가 무조건 오게 강제 하고 있습니다, 여기서 while문을 돌고 있지만, []나 : 가 오게 되면 shift로 token array에서 값을 빼게 되므로 (위에서 보았듯이 action value 순서가 보장되어 있다)
제대로 입력 했다면 이 while문 안에서 type이 value인 것을 만날 수 없기 때문입니다. (그래서 type이 value인게 있다고 하면 잘못 입력한 것이므로 throw Error 합니다)

다양한 스트링 입력 에러 상황에 맞춰 에러를 Throw해주는 코드도 넣습니다.
이렇게 함으로써 오직 [KEY] : VALUE [KEY] : VALUE ,,, 만 작동 되게 되며
[KEY : KEY]나 [KEY]VALUE나 [KEY:VALUE나 [KEY]:VALUE : VALUE와 같은 상황에서는 동작하지 않고 error를 Throw 해 줍니다.

결과는 다음과 같습니다.

keyGen이라는 action은 파라미터로 타입은 STringLiteral, value는 SCRAMBLE이다 ..
이런식으로 읽고 바로바로 동작을 수행할 수 있게 명령문? 같은 느낌으로 하달 합니다.

이거를 읽어서 적당히 코드를 작성하면(이 코드는 중요하지 않으니 그냥 패스)

이런 결과를 얻을 수 있습니다.

for를 loop로 바꾸고 if를 hoxy 로 바꾸려면
tokenize를 어떻게 하고 파싱을 어떤 규칙으로 하면 좋을까 ..
이런게 궁금해지네요 .. 이 때는 재귀로 AST를 탐색을 해야겠지요 ..

profile
dog발자

0개의 댓글