반응형
  • Generator는 목적 코드를 생성한다.
    • 목적코드는 기계가 읽을 수 있는 바이너리 형태의 코드이다. 
  • Parser에서 syntax tree를 만들었었다.
    • 트리 구조는 비선형이라 순회가 비교적 느리다.
    • 트리를 선형 구조로 만들어서 속도를 높인다. 
  • Generator는 비선형 구조인 syntax tree를 선형 구조로 표현하는 과정이다. 
  • 우리는 가상머신을 만들고 그 위에서 동작할 수 있는 목적코드를 생성할 것이다. 

 

명령어 만들기


Code.h

 

#include <any>
using std::any;

enum class Instruction
{
	Exit,
	Call, Alloca, Return,
	Jump, ConditionJump,
	Print,

	LogicalOr, LogicalAnd,
	Add, Subtract,
	Multiply, Divide, Modulo,
	Equal, NotEqual,

	LessThan, GreaterThan,
	LessOrEqual, GreaterOrEqual,
	Absolute, ReverseSign,

	GetElement, SetElement,
	GetGlobal, SetGlobal,
	GetLocal, SetLocal,

	PushNull, PushBoolean,
	PushNumber, PushString,
	PushArray, PushMap,
	PopOperand,
};

struct Code
{
	Instruction instruction;
	any operand;
};

Node.h

struct Statement {
    virtual auto generate() -> void = 0;
};
struct Expression {
    virtual auto generate() -> void = 0;
};

typedef struct Function : Statement {
    wstring name;
    vector<wstring> parameters;
    vector<Statement*> blocks;
    auto generate() -> void;
} Function;

struct Or : Expression {
    Expression* lhs;
    Expression* rhs;
    auto generate() -> void;
};
  • Statement와 Expression에 generate() 가상함수를 만든다.
  • Generator.cpp에 구현체 스켈레톤을 만들어준다.

 

Main.cpp

tuple<vector<Code>, map<string, size_t>> generate(Program* syntaxTree);
auto printObjectCode(tuple<vector<Code>, map<string, size_t>>) -> void;

int main() {
    string sourceCode = R"(
        함수 시작() {
            출력("안녕");
            출력("\n");
            출력(1 + 2 * 3);
            출력("\n");
        }
    )";

    vector<Token> tokenList = scan(sourceCode);
	Program* syntaxTree = parse(tokenList);
	auto objectCode = generate(syntaxTree);
    printObjectCode(objectCode);

    return 0;
}
  • generate 함수를 작성해주었다.

 

Generator.cpp

  • korlang에서는 시작() 이라는 함수가 시작점이다.
  • 함수를 호출하려면, 함수의 주소가 필요하다.
auto generate(Program* program) -> tuple<vector<Code>, map<wstring, size_t>>
{
    wstring main = L"시작";
    writeCode(Instruction::GetGlobal, main);
	//...
}
  • generate함수는 "시작()"함수의 주소를 가져오는 GetGlobal 코드를 생성함으로써 시작한다.
    • 지금의 과정은 프로그램을 해석하고 실행하는 것이 아니다.
    • 목적코드를 작성하는 것. 트리 형태의 프로그램을 선형으로 나타내는 것이다.
static vector<Code> codeList;

auto writeCode(Instruction instruction) -> size_t {

    codeList.push_back({ instruction });
    return codeList.size() - 1;

}

auto writeCode(Instruction instruction, any operand) -> size_t {

    codeList.push_back({ instruction, operand });
    return codeList.size() - 1;

}
  • writeCode()에서는 명령어를 만들어서 codeList에 삽입한다.
  • 반환하는 것은 목적 코드의 상대주소이다. 
auto generate(Program* program) -> tuple<vector<Code>, map<wstring, size_t>>
{
    wstring main = L"시작";
    writeCode(Instruction::GetGlobal, main);
    writeCode(Instruction::Call, static_cast<size_t>(0));
    writeCode(Instruction::Exit);

    for (auto& node : program->functions)
    {
        node->generate();
    }

    return {codeList, functionTable};
}
  • 그 다음으로는 함수를 호출하는 Call 명령을 작성한다.
    • GetGlobal로 가져온 "시작()" 함수를 호출한다는 뜻이다. 
    • 2번째 인자는 매개변수가 0개라는 의미이다. 
  • 그 다음으로 프로그램의 종료, 즉 "시작()" 함수의 종료를 뜻하는 Exit 명령을 작성한다. 
  • 그리고 코드 리스트와 함수 테이블을 반환한다.
"시작()"함수의 바디를 쓰기도 전에 EXIT을 먼저 쓰는 이유가 무엇일까?
  • 앞에서 말했듯이 Generator의 과정은 목적 코드를 생성하는 과정이다.
  • 선형으로 코드를 만든다.
  • 실행할 때는, 순차적으로 실행하는 것이 아닌, Goto(jump)을 통해서 주소를 이동하여 실행할 수 있다.
  • 그래서 "시작()" 함수 안에 모든 코드를 목적코드로 바꾼 다음에 Exit을 호출하는 것이 아닌 미리 적어놓을 수 있는 것이다. 
    • 종료할 때는 Exit 주소로 jump 하면 되기 때문이다. 

 

  • Exit까지 작성한 다음. functions의 노드들을 생성한다.
  • functions는 Program 구조체의 멤버로, Parser에서 생성한 syntax tree이다.
struct Program {
    vector<Function*> functions;
};

 

  • korlang은 최상위에 function만 올 수 있다. 
static map<wstring, size_t> functionTable;

auto Function::generate() -> void {
    functionTable[name] = codeList.size();

    for (auto& node : blocks)
    {
        node->generate();
    }
    writeCode(Instruction::Return);

}
  • generate에서 program의 functions를 생성했을 때는 Function::generate() 로 들어온다.
  • 이제 함수의 바디를 목적 코드로 생성한다.
  • 이 함수에서 첫번째로 생성하는 코드가 함수의 주소가 된다.
    • functionTable에 함수의 주소를 등록해준다.
      • 나중에 프로그램을 시작할 때, functionTable의 함수명과 매핑된 주소를 보고 codeList에서 주소를 찾아간다.
  • 그 다음 함수의 바디를 생성한다.

 

// Print
auto Print::generate() -> void {
    for (int i = arguments.size(); i >= 0; i--)
    {
        arguments[i - 1]->generate();
    }
    writeCode(Instruction::Print, arguments.size());
}
  • "출력()"함수는 이렇게 작성한다.
  • 인자들을 먼저 생성하고, 그 다음 Print 를 작성한다.
    • 2번째 인자로 Print 문 인지의 개수를 전달한다.
      • 이것의 크기만큼 피연산자 스택에서 꺼내서 콘솔에 출력한다.
// NumberLiteral
auto NumberLiteral::generate() -> void {
    writeCode(Instruction::PushNumber, value);
}

// StringLiteral
auto StringLiteral::generate() -> void {
    writeCode(Instruction::PushString, value);
}

// Arithmetic
auto Arithmetic::generate() -> void {
    map<Kind, Instruction> instructions = {
            {Kind::Add, Instruction::Add},
            {Kind::Subtract, Instruction::Subtract},
            {Kind::Multiply, Instruction::Multiply},
            {Kind::Divide, Instruction::Divide},
            {Kind::Modulo, Instruction::Modulo}
    };

    lhs->generate();
    rhs->generate();
    writeCode(instructions[kind]);
}
  • 이렇게 Instruction의 generate()함수도 작성해준다.

 

auto ExpressionStatement::generate() -> void {
    expression->generate();
}
  • ExpressionStatement는 이런 코드가 있을 때 생성된다.
함수 시작()
{
	1 + 2;
}
  • 계산식이지만, 그 자체로 하나의 문이다.

  • 위의 코드로 generate를 수행하면 목적코드가 이렇게 나온다. 
  • 이 상태에서 1+2의 결과인 3은 피연산자 스택에 남아있게 된다.
  • 피연산자 스택에서 같은 가져와 버리는 코드를 작성해야한다.
auto ExpressionStatement::generate() -> void {
    expression->generate();
    writeCode(Instruction::PopOperand);
}

 

변수 선언

  • 변수를 어떻게 관리할 수 있을까?
    • 변수의 이름과 값을 key와 value로 해서 map에 관리 할 수 있다.
    • 변수의 값이 위치한 주소를 사용할 수 있다.
      • 재귀 호출 등을 생각했을 때, 상대주소로 사용해야한다.
static list<map<wstring, size_t>> symbolStack;
static vector<size_t> offsetStack;
static size_t localSize = 0;
  • symbolStack에서 map은 변수이름이 key이고, 상대주소 offset이 value이다.
    • 맵을 리스트로 갖고 있는 이유는, 변수의 유효범위를 관리하기 위함이다.
  • offsetStack은 유효범위 별로 오프셋을 관리한다. 해당 유효범위에서 사용할 수 있는 가장 큰 오프셋 값을 저장해둔다.
  • offsetStack에 담고있는 최대 오프셋 값은 메모리 공간의 크기와 같다.
    • 함수의 실행이 끝나면 지역 변수들 값을 버린다.
    • 지역 변수를 버리기 위해 오프셋 크기를 갖고 있는 변수가 localSize이다.

 

auto patchOperand(size_t codeIndex, size_t operand) -> void
{
	codeList[codeIndex].operand = operand;
}

auto initBlock() -> void
{
    localSize = 0;
	offsetStack.push_back(0);
    symbolStack.emplace_front();
}

auto popBlock() -> void
{
	offsetStack.pop_back();
	symbolStack.pop_front();
}

// Function
auto Function::generate() -> void {
    functionTable[name] = codeList.size();

    auto temp = writeCode(Instruction::Alloca);
    initBlock();
    for (auto& node : blocks)
    {
        node->generate();
    }
    popBlock();
    patchOperand(temp, localSize);

    writeCode(Instruction::Return);
}
  • Function::generate()함수에 이렇게 추가해준다.
    1. Instruction::Alloca 코드를 추가해서 함수 실행 전에 지역변수 용 공간을 확보한다.
    2. initBlock에서는 symbolStack, offsetStack, localSize를 초기화 한다.
    3. 함수 바디를 생성하고 난 후에, popBlock()으로 스택을 정리한다.
    4. 그리고 이제 지역변수의 크기를 알았으므로, Instruction::Alloca 명령의 인자를 업데이트 해준다.
auto setLocal(wstring name) -> void
{
    symbolStack.front()[name] = offsetStack.back();
    offsetStack.back() += 1;
	localSize = max(localSize, offsetStack.back());
}

auto getLocal(wstring name) -> size_t
{
	for (auto& symbolTable : symbolStack)
	{
        if (symbolTable.count(name))
        {
            return symbolTable[name];
        }
	}
	return SIZE_MAX;
}

// Variable
auto Variable::generate() -> void {
    setLocal(name);
    expression->generate();
	writeCode(Instruction::SetLocal, getLocal(name));
    writeCode(Instruction::PopOperand);
}
  • Variable::generate()는 이렇게 작성한다.
  • setLocal에서는 symbolstack에 변수명으로 공간을 만들고, offset을 1 증가시킨다.
  • 값을 생성한 다음, SetLocal 명령어를 작성한다. getLocal함수로 변수의 값을 받아와서 인자로 사용한다.
  • 그리고 피연산자 스택을 비운다.

변수 참조

이 언어에서는 선언이 없는 변수의 참조는 전역변수로 간주한다.

getLocal 함수가 SIZE_MAX를 반환하면 로컬 참조가 없는 것이다.

// GetVariable
auto GetVariable::generate() -> void {
	if (getLocal(name) != SIZE_MAX)
	{
		writeCode(Instruction::GetLocal, getLocal(name));
	}
	else
	{
		writeCode(Instruction::GetGlobal, name);
	}
}

// SetVariable
auto SetVariable::generate() -> void {
    value->generate();
    if (getLocal(name) != SIZE_MAX)
    {
		writeCode(Instruction::SetLocal, getLocal(name));
    }
    else
    {
        writeCode(Instruction::SetGlobal, name);
    }
}

'반복' 문

반복문의 제어변수는 반복문 스코프 안에서만 유효하다.

// For
auto For::generate() -> void {
    pushBlock();
    variable->generate();
    auto jumpAddress = codeList.size();
	condition->generate();

    auto conditionJump = writeCode(Instruction::ConditionJump);

    // 참인 경우에 실행할 코드
    for (auto& node : blocks)
    {
		node->generate();
    }

	expression->generate();
	writeCode(Instruction::PopOperand);
	writeCode(Instruction::Jump, jumpAddress);
	patchAddress(conditionJump);
	popBlock();
}
  1. 유효 범위를 정해주기 위해, 시작과 끝에 pushBlock(), popBlock()을 해주었다.
  2. 제어 변수를 생성하고,
  3. 점프 주소를 기록한다.
    1. jumpAddress는 반복문 순회를 위해 기록한다.
  4. 조건문을 생성하고, 
  5. 거짓일 때 점프하는 명령어를 작성한다.
  6. 다음으로 참일 때, 실행할 코드를 생성한다.
  7. i++ 증감식을 생성한다.
  8. 지역 변수를 제거하고,
  9. 다시 jumpAddress 주소로 가서 조건식을 평가 할 수 있게, jump 코드를 작성한다.
  10. 그리고 조건식이 거짓일 때, jump할 주소를 업데이트 해준다.

 

'만약' 문

  • 만약문은 여러 개의 조건문 (if, elif)과 블럭을 가진다.
  • 하나의 본문을 실행한 뒤 끝으로 점프해야한다.
auto If::generate() -> void {
    vector<size_t> jumpList;
    for (int i = 0; i < conditions.size(); i++)
    {
        conditions[i]->generate();
		auto conditionJump = writeCode(Instruction::ConditionJump);
        pushBlock();
		for (auto& node : blocks[i])
		{
			node->generate();
		}
        popBlock();
        jumpList.push_back(writeCode(Instruction::Jump));
		patchAddress(conditionJump);

    }
    if (!elseBlock.empty())
    {
        pushBlock();
        for (auto& node : elseBlock)
        {
            node->generate();
        }
        popBlock();
    }

	for (auto& jump : jumpList)
	{
		patchAddress(jump);
	}
}
  1. 조건문마다 실행 후, 만약 문의 마지막으로 이동해야 한다. Jump 문을 작성하기 위해 list를 생성한다.
    1. 코드 작성 전에는 마지막 주소가 어딘지 모르기 때문에 jump 코드 주소를 다 저장해둔다.
  2. conditions부터 작성하고, conditionaljump를 작성한다.
    1. jump하는 주소는 body를 다 적고, patch 해준다.
  3. body를 작성한다. 
    1. 바디의 스코프 설정을 해줘야한다. pushblock()/popblock()
  4. 조건문을 다 적었으면, else 문 케이스에 대해 적어준다.
  5. 그리고 if 문에 대한 목적코드를 모두 작성했으므로, jumplist를 patch해준다.

 

계속하기 문

  • 계속하기(continue)문은 for문의 증감식 주소로 점프하는 명령이다.
static vector<vector<size_t>> continueStack;

// Continue
auto Continue::generate() -> void {
    if (continueStack.empty())
    {
        return;
    }

    auto jumpCode = writeCode(Instruction::Jump);
	continueStack.back().push_back(jumpCode);
}
  • continueStack은 jump 코드의 주소를 담는 리스트이다. 
    • 2중 리스트인 이유는
      • for문 안에 continue가 여러개 올 수 있고,
      • for문이 여러개 나올 수 있기 때문이다. 
  • Continue::generate에서는 코드 작성하고 리스트에만 넣어주고 끝이다.
// For
auto For::generate() -> void {
    continueStack.emplace_back();
    pushBlock();
    variable->generate();
    auto jumpAddress = codeList.size();
	condition->generate();

    auto conditionJump = writeCode(Instruction::ConditionJump);

    // 참인 경우에 실행할 코드
    for (auto& node : blocks)
    {
		node->generate();
    }

    auto continueAddress = codeList.size(); // 증감식 주소
	expression->generate();
	writeCode(Instruction::PopOperand);
	writeCode(Instruction::Jump, jumpAddress);
	patchAddress(conditionJump);
	popBlock();

    for (auto& jump : continueStack.back())
    {
		patchOperand(jump, continueAddress);
    }
    continueStack.pop_back();
}
  • for문도 업데이트 해주었다.
  • break문도 같은 논리 이므로 비슷하게 구현된다. 다만 증감식 위치로 가는 것이 아니라, for문 종료 위치로 patch 해줘야한다.

 

함수 호출

// Call
auto Call::generate() -> void {
	for (auto i = parameters.size(); i > 0; i--)
	{
		parameters[i - 1]->generate();
	}
	sub->generate();
	writeCode(Instruction::Call, parameters.size());

}

// Function
auto Function::generate() -> void {
    functionTable[name] = codeList.size();

    auto temp = writeCode(Instruction::Alloca);
    initBlock();
    for (auto& name : parameters)
    {
		setLocal(name);
    }

    for (auto& node : blocks)
    {
        node->generate();
    }
    popBlock();
    patchOperand(temp, localSize);

    writeCode(Instruction::Return);
}

// Return
auto Return::generate() -> void {
    expression->generate();
	writeCode(Instruction::Return);
}
  • Call
    1. 파라미터를 역순으로 생성한다 -> 스택구조이기 때문
    2. 피연산자식 코드를 생성한다.
      1. 피연산자는 함수를 뜻한다. 
    3. Call 명령을 작성한다.
  • Function
    • 함수 Call에서 파라미터를 참조할 수 있게, Function::generate에서 local 변수로 등록해준다.
  • Return
    1. 반환식을 생성하고
    2. Return 코드를 작성한다.

 

배열

// ArrayLiteral
auto ArrayLiteral::generate() -> void {
    for (int i = values.size(); i > 0; i--)
    {
		values[i - 1]->generate();
    }
	writeCode(Instruction::PushArray, values.size());
}
  • 피연산자 스택에 배열을 넣는 코드를 생성한다.
  • 스택을 사용하기 때문에 역순으로 생성한다.

 

원소값 참조

  • 배열이나 맵의 원소를 참조하는 목적 코드이다.
  • arr[1] 이런거
auto GetElement::generate() -> void {
	sub->generate();
	index->generate();
	writeCode(Instruction::GetElement);
}
  1. 피연산자 식(배열 명)을 먼저 생성하고
  2. 인덱스를 작성한다.
  3. 그리고 명령어를 작성한다.

 

원소값 수정

auto SetElement::generate() -> void {
    value->generate();
    sub->generate();
	index->generate();
	writeCode(Instruction::SetElement);
}
  • GetElement에서 대입식 생성하는 코드만 추가되었다.

 

나머지 코드들도 작성해준다.

728x90
반응형

이전글

 

[컴파일러 만들기] 1. Scanner 만들기

Scanner 만들기한글로 된 언어를 만들거라, Korlang 이라고 이름을 지었다.https://github.com/Yoon6/korlang GitHub - Yoon6/korlang: korlang compiler.korlang compiler. Contribute to Yoon6/korlang development by creating an account on GitH

yoonda.tistory.com

 

 

Parser 만들기


Parser란?

  • Parser에서는 syntax analysis를 수행한다.
  • 작성한 소스 코드의 구조를 분석하는 과정이다. 
    • 문법에 맞는지 검사하고,Parser Tree를 만든다.
    • 에러 처리를 한다.
    • symbol 테이블을 만든다.

https://www.geeksforgeeks.org/introduction-to-syntax-analysis-in-compiler-design/

 

Introduction to Syntax Analysis in Compiler Design - GeeksforGeeks

Syntax analysis, or parsing, is a crucial step in compiler design that verifies the grammatical structure of source code by interpreting tokens produced during lexical analysis and constructing a parse tree or abstract syntax tree while detecting syntax er

www.geeksforgeeks.org

  • CFG, LL Parser, LR Parser 등의 개념은 잘 모르겠다. 
    • 더 공부해서 추가 포스팅 올리기

 

문과 식

  • 문 -> Statement
  • 식 -> Expression

Expression

  • 평가가 가능해서 하나의 값으로 표현되는 '식'을 말한다.
// 계산식
1 + 2 * 3 
a + b 

// 배열 연산자
arr[1]

// 함수 콜
getValue()
  • 이런 애들을 Expression이라고 한다.

 

Statement

  • 실행가능한 코드 조각을 말한다.
    • Expression도 Statement라고 볼 수 있다.

  • 복합문과 단일문으로 구분되며, for/if 처럼 다른 문을 포함할 수 있는 문을 복합문, return/continue 처럼 다른 문을 포함할 수 없는 문을 단일문이라고 한다.

 

1. Parser Tree 의 Node 만들기

가장 먼저 root 노드를 만들어주자.

Node.h

#ifndef KORLANG_NODE_H
#define KORLANG_NODE_H

#include "Kind.h"

struct Statement {};
struct Expression {};

typedef struct Function: Statement {
    wstring name;
    vector<wstring> parameters;
    vector<Statement> blocks;
} Function;

struct Program {
    vector<Function*> functions;
};

#endif //KORLANG_NODE_H
  • 책의 사양대로 따라했기 때문에, Program(Root Node)에서는 함수의 정의만 포함하고 있다.
    • 함수 호출은 Expression(식)이고, 함수 정의는 Statement(문)이다.

 

struct Return : Statement {
    Expression* expression;
};
  • 반환문은 반환식을 포함한다.

 

struct Variable : Statement {
    wstring name;
    Expression *expression;
};
  • 변수의 선언식이다.
  • 변수의 이름과 초기화식을 포함한다.

 

struct For : Statement {
    Variable *variable;
    Expression *condition;
    Expression *expression;
    vector<Statement *> blocks;
};
  • 변수 선언, 조건식, 증감식, 실행문 리스트를 포함한다.

 

이런 방식으로 언어에서 사용하는 Statement, Expression들을 추가해준다.

더보기
#ifndef KORLANG_NODE_H
#define KORLANG_NODE_H

#include "Kind.h"

struct Statement {};
struct Expression {};

typedef struct Function : Statement {
    wstring name;
    vector<wstring> parameters;
    vector<Statement> blocks;
} Function;

struct Program {
    vector<Function*> functions;
};

struct Return : Statement {
    Expression* expression;
};

struct Variable : Statement {
    wstring name;
    Expression* expression;
};

struct For : Statement {
    Variable* variable;
    Expression* condition;
    Expression* expression;
    vector<Statement*> blocks;
};

struct Break : Statement {};
struct Continue : Statement {};

struct If : Statement {
    vector<Expression*> conditions;
    vector<Statement*> blocks;
    vector<Statement*> elseBlock;
};

struct Print : Statement {
    vector<Expression*> arguments;
};

struct ExpressionStatement : Statement {
    Expression* expression;
};

struct Or : Expression {
    Expression* lhs;
    Expression* rhs;
};

struct And : Expression {
    Expression* lhs;
    Expression* rhs;
};

struct Relational : Expression {
    Kind kind;
    Expression* lhs;
    Expression* rhs;
};

struct Arithmetic : Expression {
    Kind kind;
    Expression* lhs;
    Expression* rhs;
};

struct Unary : Expression {
    Kind kind;
    Expression* sub;
};

struct Call : Expression {
    Expression* sub;
    vector<Expression*> arguments;
};

// 배열, map 원소의 참조
// arr[0]
struct GetElement : Expression {
    Expression* sub;
    Expression* index;
};

// arr[0] = 3; 
// 할당은 statement 아닌가?
struct SetElement : Expression {
    Expression* sub;
    Expression* index;
    Expression* value;
};

// 변수 참조
struct GetVariable : Expression {
    wstring name;
};

// 변수 수정
struct SetVariable : Expression {
    wstring name;
    Expression* value;
};

struct NullLiteral : Expression {};
struct BooleanLiteral : Expression {
    bool value = false;
};
struct NumberLiteral : Expression {
    double value = 0.0;
};
struct StringLiteral : Expression {
    wstring value;
};
struct ArrayLiteral : Expression {
    vector<Expression*> values;
};
struct MapLiteral : Expression {
    map<wstring, Expression*> values;
};
#endif //KORLANG_NODE_H

 

2. Parse 함수 만들기

main.cpp

vector<Token> scan(string sourceCode);
Program* parse(vector<Token> tokenList);
auto printTokenList(vector<Token>) -> void;
auto printSyntaxTree(Program*) -> void;

int main() {
    string sourceCode = R"(
        함수 시작() {
            출력("안녕");
            출력("\n");
            출력(1 + 2 * 3);
            출력("\n");
        }
    )";

    vector<Token> tokenList = scan(sourceCode);
	Program* syntaxTree = parse(tokenList);

    printSyntaxTree(syntaxTree);
    //printTokenList(tokenList);

    return 0;
}

main 함수에서 parsing을 하고, 트리를 출력해준다.

 

Parser.cpp

#include <vector>

#include "Node.h"
#include "Token.h"

static vector<Token>::iterator currentToken;

Program* parse(vector<Token> tokenList) {
	auto result = new Program();

	currentToken = tokenList.begin();

	while (currentToken->kind != Kind::EndOfLine)
	{
		switch (currentToken->kind)
		{
		case Kind::Function:
			result->functions.push_back(parseFunction());
			break;
		default:
			cout << *currentToken << " 잘못된 구문입니다.";
			exit(1);
		}
	}


	return result;
}
  • 현재 스펙 상, 프로그램에 포함될 수 있는 것은 함수 밖에 없다.
  • 그래서 parse 함수는 이걸로 끝이다.

 

bool skipCurrentIf(Kind kind)
{
	if (currentToken->kind != kind)
	{
		return false;
	}

	currentToken++;
	return true;
}

void skipCurrent(Kind kind)
{
	std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
	if (currentToken->kind != kind)
	{
		cout << converter.to_bytes(toString(kind)) << " 토큰이 필요합니다.";
		exit(1);
	}
	currentToken++;
}

vector<Statement*> parseBlock()
{

}

Function* parseFunction()
{
	auto result = new Function();
	skipCurrent(Kind::Function);

	// function name
	result->name = currentToken->string;
	skipCurrent(Kind::Identifier);

	// (
	skipCurrent(Kind::LeftParen);

	// parameters
	if (currentToken->kind != Kind::RightParen)
	{
		do
		{
			result->parameters.push_back(currentToken->string);
			skipCurrent(Kind::Identifier);
		}
		while (skipCurrentIf(Kind::Comma));
	}

	// )
	skipCurrent(Kind::RightParen);
	// {
	skipCurrent(Kind::LeftBrace);
	// body
	result->blocks = parseBlock();
	// }
	skipCurrent(Kind::RightBrace);

	return result;
}
  • parseFunction() 함수를 작성해주었다.

 

vector<Statement*> parseBlock()
{
	vector<Statement*> result;

	while (currentToken->kind != Kind::RightBrace)
	{
		switch (currentToken->kind)
		{
		case Kind::Variable:
			result.push_back(parseVariable());
			break;
		case Kind::For:
			result.push_back(parseFor());
			break;
		case Kind::If:
			result.push_back(parseIf());
			break;
		case Kind::Print:
			result.push_back(parsePrint());
			break;
		case Kind::Return:
			result.push_back(parseReturn());
			break;
		case Kind::Break:
			result.push_back(parseBreak());
			break;
		case Kind::Continue:
			result.push_back(parseContinue());
			break;
		case Kind::EndOfLine:
		case Kind::Unknown:
			cout << *currentToken << " 잘못된 구문입니다.";
			exit(1);
		}
	}

	return result;
}
  • parseBlock()을 만들어준다.
  • korlang 사양으로는 함수만 프로그램에 포함 가능하고, 모든 동작이 함수 안에서 일어나므로, parseBlock()만 이해하면 된다. 
  • Block 안에 있는 노드들은 모드 Statement(문) 이다.

나머지 statement parse 함수들도 작성해주자.

더보기
Variable* parseVariable()
{
	auto result = new Variable();
	// `변수` 키워드를 건너뛴다.
	skipCurrent(Kind::Variable);

	result->name = currentToken->string;
	// skip variable name
	skipCurrent(Kind::Identifier);

	// skip =
	skipCurrent(Kind::Assignment);

	result->expression = parseExpression();

	// skip ;
	skipCurrent(Kind::Semicolon);

	return result;
}

For* parseFor()
{
	auto result = new For();

	// skip '반복'
	skipCurrent(Kind::For);

	result->variable = new Variable();
	result->variable->name = currentToken->string;

	// skip variable name
	skipCurrent(Kind::Identifier);
	// skip =
	skipCurrent(Kind::Assignment);
	result->variable->expression = parseExpression();

	if (result->variable->expression == nullptr)
	{
		cout << "for문에 초기화 식이 없습니다.";
		exit(1);
	}

	// skip ;
	skipCurrent(Kind::Semicolon);
	result->condition = parseExpression();
	if (result->condition == nullptr)
	{
		cout << "for문에 조건식이 없습니다.";
		exit(1);
	}

	// skip ;
	skipCurrent(Kind::Semicolon);
	result->expression = parseExpression();
	if (result->expression == nullptr)
	{
		cout << "for문에 증감식이 없습니다.";
		exit(1);
	}

	// skip {
	skipCurrent(Kind::LeftBrace);
	result->blocks = parseBlock();
	// skip }
	skipCurrent(Kind::RightBrace);

	return result;
}

If* parseIf()
{
	auto result = new If();
	
	// skip '만약'
	skipCurrent(Kind::If);
	do
	{
		auto condition = parseExpression();
		if (condition == nullptr)
		{
			cout << "if문에 조건식이 없습니다.";
			exit(1);
		}
		result->conditions.push_back(condition);
		// skip {
		skipCurrent(Kind::LeftBrace);
		result->blocks.push_back(parseBlock());
		// skip }
		skipCurrent(Kind::RightBrace);
	}
	while (skipCurrentIf(Kind::Elif));

	if (skipCurrentIf(Kind::Else))
	{
		// skip {
		skipCurrent(Kind::LeftBrace);
		result->elseBlock = parseBlock();
		// skip }
		skipCurrent(Kind::RightBrace);
	}

	return result;
}

Print* parsePrint()
{
	auto result = new Print();
	// skip '출력'
	skipCurrent(Kind::Print);
	// skip (
	skipCurrent(Kind::LeftParen);

	// 하나의 식만 출력 가능
	result->arguments.push_back(parseExpression());

	// skip )
	skipCurrent(Kind::RightParen);
	// skip ;
	skipCurrent(Kind::Semicolon);


	return result;
}

Return* parseReturn()
{
	auto result = new Return();

	// skip '반환'
	skipCurrent(Kind::Return);

	result->expression = parseExpression();
	if (result->expression == nullptr)
	{
		cout << "반환문에 반환값이 없습니다.";
		exit(1);
	}

	// skip ;
	skipCurrent(Kind::Semicolon);

	return result;
}

Break* parseBreak()
{
	auto result = new Break();
	// skip '끊기'
	skipCurrent(Kind::Break);
	// skip ;
	skipCurrent(Kind::Semicolon);

	return result;
}

Continue* parseContinue()
{
	auto result = new Continue();
	// skip '계속하기'
	skipCurrent(Kind::Continue);
	// skip ;
	skipCurrent(Kind::Semicolon);

	return result;
}

ExpressionStatement* parseExpressionStatement()
{
	auto result = new ExpressionStatement();
	result->expression = parseExpression();
	skipCurrent(Kind::Semicolon);
	return result;
}
  • 이렇게 모든 Statement의 parse 함수를 작어하였다.
  • 이제 expression을 파싱해보자.

 

Expression

  • Expression 안에는
    • 연산자
      • 피연산자
      • 연산자
        • 단항 연산자
        • 이항 연산자
    • 피연산자

가 있다.

 

연산자에는 우선순위가 있는데 다음과 같다. 

우선순위가 높은 연산자가 가장 먼저 연산된다.

* 높다 -> 1위가 가장 높음. 6위가 가장 낮음.

우선순위 연산자 종류 결합 방향
1 후위 () [] 연산자 단항연산자 왼쪽
2 전위 + - 연산자 단항연산자 오른쪽
3 산술 * / % 연산자 이항연산자 왼쪽
4 산술 + - 연산자 이항연산자 왼쪽
5 관계 연산자 이항연산자 왼쪽
6 논리 AND 연산자 이항연산자 왼쪽
7 논리 OR 연산자 이항연산자 왼쪽
8 대입 연산자 이항연산자 오른쪽

구문트리를 만들 때는, 말단의 노드부터 차례대로 연산하기 때문에(bottom-up) 우선순위가 낮은 연산자부터 계산해야한다.

 

 

Expression* parseAssignment()
{
	// = 대입 연산자
	// 1. 왼쪽 피연산자부터 파싱
	auto result = parseOr();

	if (currentToken->kind != Kind::Assignment)
	{
		return result;
	}

	// skip =
	skipCurrent(Kind::Assignment);

	// GetVariable -> 참조 변수 타입
	GetVariable* variable = dynamic_cast<GetVariable*>(result);
	if (variable != nullptr)
	{
		SetVariable* result = new SetVariable();
		result->name = variable->name;
		result->value = parseAssignment();
		return result;
	}

	// GetElement -> 배열, map 원소 참조 타입
	GetElement* element = dynamic_cast<GetElement*>(result);
	if (element != nullptr)
	{
		SetElement* result = new SetElement();
		result->sub = element->sub;
		result->index = element->index;
		result->value = parseAssignment();
		return result;
	}

	cout << "대입 연산자의 왼쪽 피연산자가 변수나 배열, map 원소 참조가 아닙니다.";
	exit(1);
}

Expression* parseExpression()
{
	return parseAssignment();
}
  • parseExpression()에서는 순위가 가장 낮은 대입연산자 parseAssignment 처리 부터 해준다.
  • parseAssignment에서는 그 다음으로 순위가 낮은 OR연산자 parseOr을 수행한다.
  • 왼쪽 피연산자를 알아내고, GetVariable(변수), GetElement(배열, 맵)인지 확인한다.
    • 그리고 재귀적으로 parseAssignment를 호출한다.
      • 재귀적으로 호출하는 이유 -> 대입 연산자는 우측 결합 연산자이기 때문.
        • a = arr[1] = b = 3; 
          • 이런 식이 있을 때, b = 3이 가장 먼저 연산되고, arr[1] = b, a = arr[1] 순서대로 연산되기 때문이다.
Expression* parseOr()
{
	auto result = parseAnd();

	while (skipCurrentIf(Kind::LogicalOr))
	{
		auto temp = new Or();
		temp->lhs = result;
		temp->rhs = parseAnd();
		result = temp; // { { parseAnd() || parseAnd() } || parseAnd() } || parseAnd() ...
	}

	return result;
}
  • parseOr()는 이렇게 만든다. 
  • OR연산자가 나올 때 까지 누적해서 Or expression을 만든다.
  • parseAssignment()와 달리 반복문으로 하는 이유
    • OR연산은 왼쪽 결합 연산자이기 때문이다.
      • a 또는 b 또는 c
      • 라는 식이 있을 때, a 또는 b부터 계산이 되어야한다.

 

이렇게 나머지 Expression parsing 함수도 작성해보자.

더보기
Expression* parseElement(Expression* sub)
{
	auto result = new GetElement();
	result->sub = sub;

	// skip [
	skipCurrent(Kind::LeftBraket);

	// index 계산
	result->index = parseExpression();

	// skip ]
	skipCurrent(Kind::RightBraket);

	return result;
}

Expression* parseCall(Expression* sub)
{
	auto result = new Call();
	result->sub = sub;

	// skip (
	skipCurrent(Kind::LeftParen);

	// 파라미터 추가
	if (currentToken->kind != Kind::RightParen)
	{
		do
		{
			result->parameters.push_back(parseExpression());
		} while (skipCurrentIf(Kind::Comma));
	}
	// skip )
	skipCurrent(Kind::RightParen);
	return result;
}

Expression* parsePostfix(Expression* sub)
{
	while (true)
	{
		switch (currentToken->kind)
		{
		case Kind::LeftParen:
			sub = parseCall(sub);
			break;
		case Kind::LeftBraket:
			sub = parseElement(sub);
			break;
		default:
			return sub;
		}
	}
}


Expression* parseNullLiteral()
{
	skipCurrent(Kind::NullLiteral);
	auto result = new NullLiteral();
	return result;
}

Expression* parseBooleanLiteral()
{
	auto result = new BooleanLiteral();
	result->value = currentToken->kind == Kind::TrueLiteral;
	skipCurrent(currentToken->kind);
	return result;
}

Expression* parseNumberLiteral() 
{
	auto result = new NumberLiteral();
	result->value = stod(currentToken->string);
	skipCurrent(Kind::NumberLiteral);
	return result;
}

Expression* parseStringLiteral()
{
	auto result = new StringLiteral();
	result->value = currentToken->string;
	skipCurrent(Kind::StringLiteral);
	return result;
}

Expression* parseListLiteral()
{
	auto result = new ArrayLiteral();
	// skip [
	skipCurrent(Kind::LeftBraket);

	if (currentToken->kind != Kind::RightBraket)
	{
		do
		{
			result->values.push_back(parseExpression());
		} while (skipCurrentIf(Kind::Comma));
	}

	// skip ]
	skipCurrent(Kind::RightBraket);
	return result;
}

Expression* parseMapLiteral()
{
	auto result = new MapLiteral();
	// skip {
	skipCurrent(Kind::LeftBrace);

	if (currentToken->kind != Kind::RightBrace)
	{
		do
		{
			wstring name = currentToken->string;
			skipCurrent(Kind::StringLiteral);
			skipCurrent(Kind::Colon);

			auto value = parseExpression();
			result->values[name] = value;
		} while (skipCurrentIf(Kind::Comma));
	}

	// skip }
	skipCurrent(Kind::RightBrace);
	return result;
}

Expression* parseIdentifier() 
{
	auto result = new GetVariable();
	result->name = currentToken->string;
	skipCurrent(Kind::Identifier);
	return result;
}

Expression* parseInnerExpression()
{
	skipCurrent(Kind::LeftParen);
	auto result = parseExpression();
	skipCurrent(Kind::RightParen);
	return result;
}


Expression* parseOperand()
{
	Expression* result = nullptr;
	switch (currentToken->kind)
	{
	case Kind::NullLiteral:   result = parseNullLiteral();      break;
	case Kind::TrueLiteral:
	case Kind::FalseLiteral:  result = parseBooleanLiteral();   break;
	case Kind::NumberLiteral: result = parseNumberLiteral();    break;
	case Kind::StringLiteral: result = parseStringLiteral();    break;
	case Kind::LeftBraket:    result = parseListLiteral();      break;
	case Kind::LeftBrace:     result = parseMapLiteral();       break;
	case Kind::Identifier:    result = parseIdentifier();       break;
	case Kind::LeftParen:     result = parseInnerExpression();  break;
	default:                  cout << "잘못된 식입니다.";       exit(1);
	}

	return parsePostfix(result);
}

Expression* parseUnary()
{
	set<Kind> operators = {
		Kind::Add,
		Kind::Subtract,
	};
	while (operators.count(currentToken->kind))
	{
		auto temp = new Unary();
		temp->kind = currentToken->kind;
		skipCurrent(currentToken->kind);
		temp->sub = parseUnary();
		return temp;
	}

	return parseOperand();
}

Expression* parseArithmetic2()
{
	set<Kind> operators = {
	Kind::Multiply,
	Kind::Divide,
	Kind::Modulo,
	};
	auto result = parseUnary();

	while (operators.count(currentToken->kind))
	{
		auto temp = new Arithmetic();
		temp->kind = currentToken->kind;
		skipCurrent(currentToken->kind);
		temp->lhs = result;
		temp->rhs = parseUnary();
		result = temp;
	}

	return result;
}

Expression* parseArithmetic1()
{
	set<Kind> operators = {
		Kind::Add,
		Kind::Subtract,
	};
	auto result = parseArithmetic2();

	while (operators.count(currentToken->kind))
	{
		auto temp = new Arithmetic();
		temp->kind = currentToken->kind;
		skipCurrent(currentToken->kind);
		temp->lhs = result;
		temp->rhs = parseArithmetic2();
		result = temp;
	}

	return result;
}

Expression* parseRelational() // <, >, == ...
{
	set<Kind> operators = {
		Kind::Equal,
		Kind::NotEqual,
		Kind::LessThan,
		Kind::GreaterThan,
		Kind::LessOrEqual,
		Kind::GreaterOrEqual,
	};
	auto result = parseArithmetic1();

	while (operators.count(currentToken->kind))
	{
		auto temp = new Relational();
		temp->kind = currentToken->kind;
		skipCurrent(currentToken->kind);
		temp->lhs = result;
		temp->rhs = parseArithmetic1();
		result = temp;
	}

	return result;
}

Expression* parseAnd()
{
	auto result = parseRelational();

	while (skipCurrentIf(Kind::LogicalAnd)) 
	{
		auto temp = new And();
		temp->lhs = result;
		temp->rhs = parseRelational();
		result = temp; // { { parseEquality() && parseEquality() } && parseEquality() } && parseEquality() ...
	}

	return result;

}

 

Print 함수를 만들어서 출력하면 아래와 같이 나온다.

  • 소스코드
함수 시작() {
    출력("안녕");
    출력("\n");
    출력(1 + 2 * 3);
    출력("\n");
}
  • 결과
FUNCTION 시작:
  BLOCK:
    PRINT:
      "안녕"
    PRINT:
      "\n"
    PRINT:
      +:
        LHS:
          1
        RHS:
          *:
            LHS:
              2
            RHS:
              3
    PRINT:
      "\n"

 

 

참고 자료

https://shoark7.github.io/programming/knowledge/expression-vs-statement

 

 

 

 

728x90
반응형

Scanner 만들기


 

GitHub - Yoon6/korlang: korlang compiler.

korlang compiler. Contribute to Yoon6/korlang development by creating an account on GitHub.

github.com

 

scanner란?

  • lexical analysis 라고도 한다.
  • 소스 코드를 토큰으로 분리하는 과정이다.
  • 주의해야할 점은 "의미있는" 토큰으로 분리해야한다는 것이다.
만약(변수1 > 변수2)
{
	출력(123);
}
  • 이런 코드가 있을 때, 
    • '만', '약', (, '변', '수', 1, >, '변', '수', 2, ), ...
    • 이런 식으로 분리한다면 아무 의미가 없다.
  • '만약', (, '변수1', > '변수2', ), {, '출력', (, 123, ), ;, } 
    • 이렇게 토큰화 되어야한다.
  • 토큰으로 올 수 있는 요소들은 아래와 같다. 
    • 키워드, 식별자, 연산자, 구분자, 숫자 리터럴, 문자열 리터럴
토큰 예시
키워드 if, for, function
식별자 변수나 함수의 이름처럼 사용자가 정의한 어휘
연산자 +,-,*,/,%
구분자 (),{}.[],;
숫자 리터럴 123, 1.2 같은 정수 혹은 실수
문자열 리터럴 "Hello", "World" 같이 따옴표로 둘러싸인 문자

 

1. 토큰 정의하기

Kind.h

#ifndef KORLANG_KIND_H
#define KORLANG_KIND_H
#include <string>

using namespace std;

enum class Kind {
    Unknown, EndOfLine,

    NullLiteral,
    TrueLiteral, FalseLiteral,
    NumberLiteral, StringLiteral,
    Identifier,

    Function, Return,
    Variable,
    For, Break, Continue,
    If, Elif, Else,
    Print,

    LogicalAnd, LogicalOr,
    Assignment,
    Add, Subtract,
    Multiply, Divide, Modulo,
    Equal, NotEqual,
    LessThen, GreaterThan,
    LessOrEqual, GreaterOrEqaul,

    Comma, Colon, Semicolon,
    LeftParen, RightParen,
    LeftBrace, RightBrace,
    LeftBraket, RightBraket,
};
#endif //KORLANG_KIND_H
  • 위와 같이 소스코드 안에서 사용가능한 어휘들을 만들어주었다.

 

Kind.cpp

#include "Kind.h"
#include <map>

static map<string, Kind> stringToToken = {
        {"#unknown",    Kind::Unknown},
        {"#끝", Kind::EndOfLine},

        {"빈값",        Kind::NullLiteral},
        {"참",        Kind::TrueLiteral},
        {"거짓",       Kind::FalseLiteral},
        {"#숫자-리터럴",     Kind::NumberLiteral},
        {"#문자열-리터럴",     Kind::StringLiteral},
        {"#식별자",    Kind::Identifier},

        {"함수",    Kind::Function},
        {"반환",      Kind::Return},
        {"변수",         Kind::Variable},
        {"반복",         Kind::For},
        {"끊기",       Kind::Break},
        {"계속하기",    Kind::Continue},
        {"만약",          Kind::If},
        {"그게아니라",        Kind::Elif},
        {"아니면",        Kind::Else},
        {"출력",       Kind::Print},

        {"그리고",         Kind::LogicalAnd},
        {"또는",          Kind::LogicalOr},

        {"=",           Kind::Assignment},

        {"+",           Kind::Add},
        {"-",           Kind::Subtract},
        {"*",           Kind::Multiply},
        {"/",           Kind::Divide},
        {"%",           Kind::Modulo},

        {"==",          Kind::Equal},
        {"!=",          Kind::NotEqual},
        {"<",           Kind::LessThan},
        {">",           Kind::GreaterThan},
        {"<=",          Kind::LessOrEqual},
        {">=",          Kind::GreaterOrEqual},

        {",",           Kind::Comma},
        {":",           Kind::Colon},
        {";",           Kind::Semicolon},
        {"(",           Kind::LeftParen},
        {")",           Kind::RightParen},
        {"{",           Kind::LeftBrace},
        {"}",           Kind::RightBrace},
        {"[",           Kind::LeftBraket},
        {"]",           Kind::RightBraket},
};
  • 소스 코드 문자열과 열거형 멤버를 매핑시켜준다.

 

Token.h

#ifndef KORLANG_TOKEN_H
#define KORLANG_TOKEN_H
#include "Kind.h"

struct Token {
    Kind kind = Kind::Unknown;
    string string;
};

#endif //KORLANG_TOKEN_H
  • Scanning 결과로 반환할 Token 구조체이다.
    • 변수의 이름의 경우
    • kind = Kind:Identifier, string = "변수1" 이렇게 될 것이다.

 

Main.cpp

vector<Token> scan(string sourceCode);
  • Scanner는 입력으로 소스코드를 받고, 출력으로 토큰 리스트를 출력한다.

 

int main() {
    string sourceCode = R"(
        함수 시작() {
            출력("안녕");
            출력("\n");
            출력(1 + 2 * 3);
            출력("\n");
        }
    )";

    vector<Token> tokenList = scan(sourceCode);

    return 0;
}
  • 이런 sourceCode를 lexical analyze 해보자.

 

 

Scanner.cpp

#include <vector>
#include "Token.h"

static wstring::iterator current;

enum class CharType {
    Unknown,
    WhiteSpace,
    NumberLiteral,
    StringLiteral,
    IdentifierAndKeyword,
    OperatorAndPunctuator
};

bool isHangul(wchar_t wc) {
    return (wc >= 0xAC00 && wc <= 0xD7A3);
}

CharType getCharType(wchar_t c) {
    if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
        return CharType::WhiteSpace;
    }
    if ('0' <= c && c <= '9') {
        return CharType::NumberLiteral;
    }
    if (c == '\"') {
        return CharType::StringLiteral;
    }
    if ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || isHangul(c)) {
        return CharType::IdentifierAndKeyword;
    }
    if ('!' <= c && c <= '/' ||
        ':' <= c && c <= '@' ||
        '[' <= c && c <= '`' ||
        '{' <= c && c <= '~') {
        return CharType::OperatorAndPunctuator;
    }
    return CharType::Unknown;
}

bool isCharType(wchar_t wc, CharType type) {
    switch (type) {
        case CharType::NumberLiteral:
            return '0' <= wc && wc <= '9';
        case CharType::StringLiteral:
            return (33 <= wc && wc <= 126 && wc != '\"') || isHangul(wc);
        case CharType::IdentifierAndKeyword:
            return '0' <= wc && wc <= '9' || 'a' <= wc && wc <= 'z' || 'A' <= wc && wc <= 'Z' || isHangul(wc);
        case CharType::OperatorAndPunctuator:
            return '!' <= wc && wc <= '/' || ':' <= wc && wc <= '@' || '[' <= wc && wc <= '`' || '{' <= wc && wc <= '~';
        default:
            return false;
    }
}

Token scanNumberLiteral() {
    wstring string;
    while (isCharType(*current, CharType::NumberLiteral)) {
        string += *(current++);
    }
    if (*current == '.') {
        string += *(current++);
        while (isCharType(*current, CharType::NumberLiteral)) {
            string += *(current++);
        }
    }
    return Token{Kind::NumberLiteral, string};
}

Token scanStringLiteral() {
    wstring string;
    current++;
    while (isCharType(*current, CharType::StringLiteral)) {
        string += *(current++);
    }
    if (*current != '\"') {
        cout << "문자열 종료 문자가 없습니다.";
        exit(1);
    }
    current++;
    return Token{Kind::StringLiteral, string};
}

Token scanIdentifierAndKeyword() {
    wstring string;
    while (isCharType(*current, CharType::IdentifierAndKeyword)) {
        string += *(current++);
    }
    auto kind = toKind(string);
    if (kind == Kind::Unknown) {
        kind = Kind::Identifier;
    }
    return Token{kind, string};
}

Token scanOperatorAndPunctuator() {
    wstring string;
    while (isCharType(*current, CharType::OperatorAndPunctuator))
    {
        string += *(current++);
    }
    while (!string.empty() && toKind(string) == Kind::Unknown)
    {
        string.pop_back();
        current--;
    }
    if (string.empty())
    {
        cout << *current << " 사용할 수 없는 문자입니다.";
        exit(1);
    }
    return Token{toKind(string), string};

}

vector<Token> scan(string sourceCode) {
    vector<Token> result;
    sourceCode += '\0'; // Add null character
    wstring_convert<codecvt_utf8_utf16<wchar_t>> converter;
    std::wstring wSourceCode = converter.from_bytes(sourceCode);

    current = wSourceCode.begin();

    while (*current != '\0') {
        switch (getCharType(*current)) {
            case CharType::WhiteSpace:
                current += 1;
                break;
            case CharType::NumberLiteral:
                result.push_back(scanNumberLiteral());
                break;
            case CharType::StringLiteral:
                result.push_back(scanStringLiteral());
                break;
            case CharType::IdentifierAndKeyword:
                result.push_back(scanIdentifierAndKeyword());
                break;
            case CharType::OperatorAndPunctuator:
                result.push_back(scanOperatorAndPunctuator());
                break;
            default:
                cout << *current << " 사용할 수 없는 문자입니다.";
                exit(1);
        }
    }

    result.push_back({Kind::EndOfLine});
    return result;
}
  • 한글로 된 언어이기 때문에 wstring으로 변환해주었다.

 

Token.h

#include <locale>
#include <codecvt>
auto operator<<(ostream&, Token&)->ostream&;
  • 추가

Kind.h

auto toKind(const wstring&) -> Kind;
auto toString(Kind)->wstring;
  • 추가

Kind.cpp

auto toKind(const wstring& string) -> Kind {
    if (stringToKind.count(string))
    {
        return stringToKind.at(string);
    }
    return Kind::Unknown;
}

static auto kindToString = [] {
    map<Kind, wstring> result;
    for (auto& [key, value] : stringToKind)
        result[value] = key;
    return result;
}();

auto toString(Kind type)->wstring {
    if (kindToString.count(type))
        return kindToString.at(type);
    return L"";
}
  • 추가

Main.cpp

auto printTokenList(vector<Token> tokenList) -> void {
    cout << setw(12) << left << "KIND" << "STRING" << endl;
    cout << string(23, '-') << endl;
    for (auto &token: tokenList)
        cout << token << endl;
}

auto operator<<(ostream &stream, Token &token) -> ostream & {
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return stream << setw(12) << left << converter.to_bytes(toString(token.kind)) << setw(12) << right << '\t' << converter.to_bytes(token.string);
}

 

출력

  • 위와 같이 작성하면, 출력이 이렇게 나오게 된다.
KIND        STRING
-----------------------
함수                 	함수
#식별자             	시작
(                      	(
)                      	)
{                      	{
출력                 	출력
(                      	(
#문자열-리터럴           	안녕
)                      	)
;                      	;
출력                 	출력
(                      	(
#문자열-리터럴           	\n
)                      	)
;                      	;
출력                 	출력
(                      	(
#숫자-리터럴           	1
+                      	+
#숫자-리터럴           	2
*                      	*
#숫자-리터럴           	3
)                      	)
;                      	;
출력                 	출력
(                      	(
#문자열-리터럴           	\n
)                      	)
;                      	;
}                      	}
#끝
  • 한글도 잘 토큰화 하는 것을 볼 수 있다.
  • 이렇게 되면 lexical analysis 이후에 토큰 리스트를 뽑아내었다.
728x90
반응형

C++로 컴파일러 만들어보기


https://www.yes24.com/Product/Goods/103153057

 

컴파일러 만들기 - 예스24

현대 소프트웨어는 하드웨어의 성능 발전에 힘입어 많은 부분에서 추성화된 덕택에 코딩에 입문하기도 쉬워졌고 원하는 프로그램을 만들기도 쉬워졌다. 하지만 컴퓨터를 더 잘 이해하고 싶고

www.yes24.com

 

  • "컴파일러 만들기" - 유종원 지음
  • 책을 보고 학습한 내용 정리 및 실습을 할 것이다.

 

나의 목표

  1. 책 내용 정리
  2. 나만의 프로그래밍 언어 만든 후 컴파일러 제작.
    1. Github까지 배포해보자.

 

  • 프로그래밍 언어는 한글로 만들 것이다. 
    • 책에서는 저자가 만든 Yulang이라는 형태를 사용한다.
function main() {
	printLine '123';
}
  • 대략 이런 형태임.
    • C++ 로 컴파일러를 작성하면서 작성 저 코드를 scan/parse 하다 보니까 이게 C++인지 Yulang인지 헷갈림.

 

함수 메인() {
	변수 이름 = "머윤";
	출력(1+2);
    출력줄바꿈("Hello");
    출력(이름);
}
  • 이런식으로 한글로 써서 C++과 확실히 구분되게 하겠다 (한글을 써서 좋은 점인가..?)
  • 스펙은 우선 책에 나와있는 최소 스펙으로 만들것임.
    • 변수/배열/map/함수/콘솔출력 정도

 

컴파일러란

코드를 입력받아 코드를 출력하는 프로그램이다. 

소스코드를 받아 목적코드를 만들어낸다.

출처 : https://pintokarsanda.blog.binusian.org/tag/phases-of-compiler/

이 책에서는 크게 3가지 단계를 수행한다. 

1. 어휘분석(Scanner)

2. 구분분석(Parser)

3. 코드생성(Generator)

 

자 이제 시작해보자.

728x90

+ Recent posts