[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를 탐색을 해야겠지요 ..