And Brain said,
컴파일러(Compiler) 만들어보기 (with Rust) 본문
오늘은 Rust를 이용해 작고 귀여운 컴파일러를 하나 만들어 볼 것입니다.
컴파일러는 일반적으로 어떤 프로그래밍 언어로 작성된 소스 코드를 다른 프로그래밍 언어로 변환하는 프로그램을 말합니다. 가장 흔한 경우는 고급 언어로 작성된 소스 코드를 저급 언어(보통 기계어)로 변환하는 것입니다.
컴파일러는 일반적으로 다음과 같은 단계를 거치는데,
1. 어휘 분석(Lexical Analysis): 이 단계에서는 소스 코드가 토큰으로 분리됩니다. 토큰은 소스 코드에서 가장 작은 의미 단위로, 예를 들어 키워드, 식별자, 리터럴, 연산자 등이 될 수 있습니다.
2. 구문 분석(Syntax Analysis): 이 단계에서는 토큰들이 어떤 순서와 구조로 이루어져 있는지를 분석하여, 추상 구문 트리(Abstract Syntax Tree, AST)라는 중간 데이터 구조를 만듭니다.
3. 의미 분석(Semantic Analysis): 이 단계에서는 AST가 소스 언어의 의미를 정확하게 표현하고 있는지 검사합니다. 예를 들어, 이 단계에서는 모든 변수가 선언되기 전에 사용되지 않았는지, 모든 함수가 올바른 수의 인자로 호출되었는지 등을 확인합니다.
4. 중간 코드 생성(Intermediate Code Generation): 이 단계에서는 AST를 중간 표현으로 변환합니다. 중간 표현은 일반적으로 소스 코드 언어와 타겟 코드 언어 사이의 일종의 "중간" 언어입니다.
5. 코드 최적화(Code Optimization): 이 단계에서는 중간 표현이 효율적인 타겟 코드를 만들 수 있도록 최적화합니다. 예를 들어, 필요 없는 연산을 제거하거나, 반복문을 풀어내는 등의 변환을 수행할 수 있습니다.
6. 코드 생성(Code Generation): 이 마지막 단계에서는 최적화된 중간 표현을 실제 타겟 코드로 변환합니다.
우리는 오늘 단순히 Java의 System.out.println() 을 Javascript의 console.log() 로 변환하는 수준의 컴파일러를 만들 것입니다. 하여, 우리의 작고 귀여운 컴파일러에는 의미 분석과 중간 코드 생성 및 최적화 단계는 필요하지 않습니다.
먼저, 어휘 분석 단계부터 작성해봅시다.
// 토큰 타입을 나타내는 열거형
enum Token {
Keyword(String), // 키워드 (예: System, out, println)
OpenParenthesis, // 여는 괄호 (예: ()
CloseParenthesis, // 닫는 괄호 (예: ))
StringLiteral(String), // 문자열 리터럴 (예: "이리오너라!")
Semicolon, // 세미콜론 (예: ;)
}
// Java 코드 문자열을 입력으로 받아 토큰 벡터를 반환하는 함수
fn lexical_analysis(input: &str) -> Vec<Token> {
// 반환할 토큰 벡터를 초기화합니다.
let mut tokens = Vec::new();
// 입력 문자열에 대한 이터레이터를 생성하고 peekable로 변환합니다.
let mut chars = input.chars().peekable();
// 입력 문자열의 모든 문자를 순회합니다.
while let Some(c) = chars.next() {
// 현재 문자에 따라 적절한 토큰을 생성합니다.
let token = match c {
// 공백, 새 줄, 탭 등은 무시합니다.
' ' | '\n' | '\r' | '\t' => {
continue;
}
// 여는 괄호, 닫는 괄호, 세미콜론은 해당 토큰으로 변환합니다.
'(' => {
Token::OpenParenthesis
}
')' => {
Token::CloseParenthesis
}
';' => {
Token::Semicolon
}
// 따옴표로 시작하는 경우 문자열 리터럴로 간주합니다.
'"' => {
let mut string = String::new();
while let Some(c) = chars.next() {
if c == '"' {
break;
} else {
string.push(c);
}
}
Token::StringLiteral(string)
}
// 그 외의 경우 키워드로 간주합니다.
_ => {
let mut keyword = String::new();
keyword.push(c);
while let Some(&c) = chars.peek() {
if c.is_whitespace() || c == ';' || c == '(' || c == ')' || c == '"' {
break;
} else {
keyword.push(chars.next().unwrap());
}
}
Token::Keyword(keyword)
}
};
// 생성된 토큰을 벡터에 추가합니다.
tokens.push(token);
}
// 토큰 벡터를 반환합니다.
tokens
}
다음으로, 구문 분석 단계입니다.
// AST 열거형
enum AST {
PrintStatement(String),
}
// 토큰 벡터를 입력으로 받아 AST 벡터를 반환하는 함수
fn syntax_analysis(tokens: Vec<Token>) -> Vec<AST> {
// 반환할 AST 벡터를 초기화합니다.
let mut ast = Vec::new();
// 토큰 벡터에 대한 이터레이터를 생성하고 peekable로 변환합니다.
let mut tokens = tokens.into_iter().peekable();
// 토큰 벡터의 모든 토큰을 순회합니다.
while let Some(token) = tokens.next() {
// 토큰이 키워드인 경우에만 처리합니다.
if let Token::Keyword(keyword) = token {
// 키워드가 "System.out.println"인 경우에만 처리합니다.
if keyword == "System.out.println" {
// 다음 3개 토큰을 가져와 적절한 문장 구조인지 확인합니다.
if let Some(Token::OpenParenthesis) = tokens.next() {
if let Some(Token::StringLiteral(s)) = tokens.next() {
if let Some(Token::CloseParenthesis) = tokens.next() {
ast.push(AST::PrintStatement(s));
}
}
}
}
}
}
// AST 벡터를 반환합니다.
ast
}
이제, 코드 생성 단계를 작성해봅시다.
// 이 함수는 AST 벡터를 받아 JavaScript 코드를 문자열로 반환합니다.
fn generate_code(ast: Vec<AST>) -> String {
// 반환할 JavaScript 코드 문자열을 초기화합니다.
let mut js_code = String::new();
// AST 벡터를 순회하며 각 노드에 대한 JavaScript 코드를 생성합니다.
for node in ast {
match node {
// 노드가 PrintStatement인 경우
AST::PrintStatement(s) => {
// 해당 문자열을 출력하는 JavaScript 코드를 생성합니다.
// 예: `console.log("이리오너라!");`
js_code.push_str(&format!("console.log(\"{}\");\n", s));
},
}
}
// 생성된 JavaScript 코드를 반환합니다.
js_code
}
마지막으로, 각각의 컴파일 단계들을 실행할 컴파일 함수를 만들어봅시다.
// std::fs 모듈을 사용합니다.
use std::fs;
// compile 함수를 정의합니다.
// 이 함수는 Java 파일의 경로와 JavaScript 파일의 경로를 입력 받아,
// Java 코드를 JavaScript 코드로 컴파일하고 결과를 파일에 저장합니다.
fn compile(java_file_path: &str, js_file_path: &str) -> std::io::Result<()> {
// Java 파일을 읽습니다.
let java_code = fs::read_to_string(java_file_path)?;
// 읽어온 Java 코드에 대해 어휘 분석을 수행합니다.
let tokens = lexical_analysis(&java_code);
// 어휘 분석을 통해 얻은 토큰들에 대해 구문 분석을 수행합니다.
let ast = syntax_analysis(tokens);
// 구문 분석을 통해 얻은 추상 구문 트리를 바탕으로 JavaScript 코드를 생성합니다.
let js_code = generate_code(ast);
// 생성된 JavaScript 코드를 파일에 씁니다.
fs::write(js_file_path, js_code)?;
// 함수가 성공적으로 완료되었음을 나타내는 Ok(())를 반환합니다.
Ok(())
}
main 함수를 작성해주면 끝.
fn main() {
match compile("input.java", "output.js") {
Ok(()) => println!("Compilation successful!"),
Err(err) => println!("Error during compilation: {}", err),
}
}
// input.java
System.out.println("이리오너라!");
// output.js
console.log("이리오너라!");
output.js 파일이 생성되었음을 확인할 수 있습니다.
전체 코드는 아래에서 확인
https://github.com/TheMan1697/The-simple-compiler
GitHub - TheMan1697/The-simple-compiler: 작고 귀여운 컴파일러
작고 귀여운 컴파일러. Contribute to TheMan1697/The-simple-compiler development by creating an account on GitHub.
github.com
여기까지 아주 간단한 컴파일러를 만들어보았습니다. 하지만, 실제로 컴파일러는 엄청나게 복잡합니다. 자바 파일의 main 함수도 적지않고 그냥 System.out.println만 적었음에도 처리하는 코드가 굉장히 길어졌음을 알 수 있습니다. 컴파일러에 대해 한 발자국 다가가기 위한 예제였으니 이를 바탕으로 컴파일러가 무엇인지 조금 감이 잡히셨으면 합니다. 그럼,
Thanks for watching, Have a nice day.
'IT > Rust' 카테고리의 다른 글
OS with Rust, OS 개발기 - 2. 운영체제로 거듭나다(Reborn OS) (0) | 2023.06.07 |
---|---|
OS with Rust, OS 개발기 - 1. 운영체제를 넘어서(Beyond OS) (0) | 2023.06.04 |
Rust, 프로그래밍 언어의 코페르니쿠스적 전환 - 3 (0) | 2023.05.28 |
Rust, 프로그래밍 언어의 코페르니쿠스적 전환 - 2 (0) | 2023.05.21 |
Rust, 프로그래밍 언어의 코페르니쿠스적 전환 - 1 (3) | 2023.05.18 |