쪽지발송 성공
Click here
재능넷 이용방법
재능넷 이용방법 동영상편
가입인사 이벤트
판매 수수료 안내
안전거래 TIP
재능인 인증서 발급안내

🌲 지식인의 숲 🌲

🌳 디자인
🌳 음악/영상
🌳 문서작성
🌳 번역/외국어
🌳 프로그램개발
🌳 마케팅/비즈니스
🌳 생활서비스
🌳 철학
🌳 과학
🌳 수학
🌳 역사
해당 지식과 관련있는 인기재능

안녕하세요.저는 현업 9년차 IT 서비스 중견기업에 재직중인 개발자입니다.결과물만 중요하게 생각하지 않고, 소스코드와 개발 과정 그리고 완성도...

주된 경력은 php기반 업무용 웹프로그램 개발입니다.웹프로그램과 연계되는 윈도우용 응용프로그램도 가능합니다. 학사관리시스템,리스업무관...

워드프레스를 설치는 했지만, 그다음 어떻게 해야할지 모르시나요? 혹은 설치가 어렵나요?무료 워드프레스부터 프리미엄 테마까지 설치하여 드립니...

Go 언어로 구현하는 인터프리터

2024-10-11 11:21:33

재능넷
조회수 275 댓글수 0

Go 언어로 구현하는 인터프리터: 코드의 마법사 되기 🧙‍♂️✨

 

 

안녕하세요, 코딩 마법사 여러분! 오늘은 정말 흥미진진한 여행을 떠나볼 거예요. 바로 Go 언어를 사용해서 우리만의 인터프리터를 만드는 여정이죠. 🚀 이 여정은 마치 우리가 프로그래밍 언어의 비밀 요리법을 배우는 것과 같아요. 여러분이 좋아하는 요리 프로그램을 보면서 "와, 저렇게 하면 이런 맛있는 요리가 나오는구나!"라고 느끼셨죠? 오늘 우리는 코드의 세계에서 그런 경험을 하게 될 거예요!

Go 언어로 인터프리터를 만드는 과정은 마치 우리가 언어의 요리사가 되는 것과 같아요. 우리는 원재료(소스 코드)를 받아서, 그것을 맛있는 요리(실행 가능한 프로그램)로 변환할 거예요. 이 과정에서 우리는 프로그래밍 언어의 내부 작동 원리를 깊이 이해하게 될 뿐만 아니라, Go 언어의 강력한 기능들을 활용하는 방법도 배우게 될 거예요.

여러분, 혹시 재능넷이라는 재능 공유 플랫폼을 들어보셨나요? 이곳에서는 다양한 분야의 전문가들이 자신의 지식과 기술을 공유하고 있어요. 우리가 오늘 배울 내용도 충분히 재능넷에서 공유할 만한 가치 있는 기술이 될 거예요. 누군가는 여러분이 만든 인터프리터를 보고 "와, 이런 걸 어떻게 만들었지?"라고 감탄할지도 모르죠!

자, 이제 우리의 마법 같은 여정을 시작해볼까요? 준비되셨나요? 그럼 Go!!! 🏁

1. 인터프리터의 세계로: 기본 개념 이해하기 🌍

먼저, 인터프리터가 무엇인지 알아볼까요? 인터프리터는 마치 우리가 외국어를 통역하는 것처럼, 프로그래밍 언어를 컴퓨터가 이해할 수 있는 형태로 '통역'해주는 프로그램이에요.

인터프리터의 정의: 고급 프로그래밍 언어로 작성된 소스 코드를 한 줄씩 읽어가며 실행하는 프로그램

인터프리터는 크게 다음과 같은 단계로 작동해요:

  • 1. 렉싱(Lexing): 소스 코드를 토큰으로 분리
  • 2. 파싱(Parsing): 토큰을 구문 트리로 변환
  • 3. 평가(Evaluation): 구문 트리를 실행하여 결과 도출

이 과정을 더 자세히 살펴볼까요? 🔍

1.1 렉싱(Lexing): 코드를 토큰으로 쪼개기

렉싱은 마치 문장을 단어로 나누는 것과 같아요. 예를 들어, "x = 5 + 3"이라는 코드가 있다면, 렉서는 이를 다음과 같이 나눌 거예요:

  • "x" (식별자)
  • "=" (할당 연산자)
  • "5" (정수 리터럴)
  • "+" (더하기 연산자)
  • "3" (정수 리터럴)

이렇게 나눈 각각의 요소를 '토큰'이라고 해요. 토큰은 프로그래밍 언어의 가장 작은 의미 단위랍니다.

1.2 파싱(Parsing): 토큰을 구조화하기

파싱은 토큰들을 문법적으로 의미 있는 구조로 조립하는 과정이에요. 마치 단어들을 조합해서 문장을 만드는 것과 비슷하죠. 이 과정에서 '추상 구문 트리(Abstract Syntax Tree, AST)'라는 것을 만들어요.

예를 들어, "x = 5 + 3"이라는 표현식은 다음과 같은 구조로 표현될 수 있어요:

추상 구문 트리 예시 할당(=) x 더하기(+) 5 3

이런 구조를 만들면, 컴퓨터가 코드의 의미를 더 쉽게 이해할 수 있어요.

1.3 평가(Evaluation): 코드를 실행하기

마지막으로, 평가 단계에서는 만들어진 구문 트리를 실제로 실행해요. 이 과정에서 변수에 값이 할당되고, 연산이 수행되며, 함수가 호출되죠. 우리의 예시에서는 다음과 같은 일이 일어날 거예요:

  1. 5와 3을 더해서 8을 얻습니다.
  2. 변수 x에 8을 할당합니다.

이렇게 해서 "x = 5 + 3"이라는 코드가 실행되고, x에는 8이라는 값이 저장되는 거죠!

와! 우리가 방금 인터프리터의 기본 동작 원리를 배웠어요. 이제 이 과정을 Go 언어로 어떻게 구현하는지 살펴볼 준비가 되었나요? 다음 섹션에서는 Go 언어의 특징과 왜 인터프리터 구현에 Go가 좋은 선택인지 알아볼 거예요. 여러분의 재능넷 프로필에 "Go 언어로 인터프리터 구현 가능"이라고 적을 날이 머지않았네요! 😉

2. Go 언어: 인터프리터 구현의 완벽한 동반자 🐹

자, 이제 우리의 주인공인 Go 언어에 대해 알아볼 시간이에요! Go는 구글에서 개발한 프로그래밍 언어로, 간결하면서도 강력한 기능을 제공해요. 특히 인터프리터를 구현하는 데 있어서 Go는 정말 훌륭한 선택이 될 수 있답니다.

Go 언어의 특징: 간결한 문법, 빠른 컴파일 속도, 강력한 동시성 지원, 효율적인 가비지 컬렉션

2.1 Go의 장점: 인터프리터 구현에 왜 좋을까?

Go 언어가 인터프리터 구현에 특별히 적합한 이유를 살펴볼까요? 🤔

  • 간결한 문법: Go의 문법은 매우 간결하고 읽기 쉬워요. 이는 복잡한 인터프리터 로직을 구현할 때 코드의 가독성을 높여줍니다.
  • 정적 타입 시스템: Go는 컴파일 시점에 타입을 체크해요. 이는 런타임 에러를 줄이고 성능을 향상시키는 데 도움이 됩니다.
  • 인터페이스: Go의 인터페이스는 유연하고 강력해요. 이를 통해 다양한 AST 노드 타입을 효과적으로 관리할 수 있습니다.
  • 고루틴과 채널: Go의 동시성 모델은 복잡한 파싱 작업을 병렬로 처리하는 데 유용할 수 있어요.
  • 표준 라이브러리: Go는 풍부한 표준 라이브러리를 제공해요. 특히 텍스트 처리와 관련된 기능들이 인터프리터 구현에 유용합니다.

이런 특징들 덕분에 Go로 인터프리터를 만들면, 코드가 깔끔하고 성능도 좋은 결과물을 얻을 수 있어요. 마치 잘 정돈된 주방에서 요리하는 것처럼 편안하고 효율적으로 작업할 수 있죠!

2.2 Go 언어의 기본 문법 복습

인터프리터를 구현하기 전에, Go의 기본적인 문법을 간단히 복습해볼까요? 이렇게 하면 나중에 코드를 작성할 때 더 수월할 거예요.


package main

import "fmt"

func main() {
    // 변수 선언
    var x int = 5
    y := 10  // 축약형 선언

    // 조건문
    if x < y {
        fmt.Println("x는 y보다 작습니다.")
    }

    // 반복문
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }

    // 함수 호출
    result := add(x, y)
    fmt.Printf("%d + %d = %d\n", x, y, result)
}

// 함수 정의
func add(a, b int) int {
    return a + b
}
  

이 간단한 예제에서 우리는 Go의 주요 문법 요소들을 볼 수 있어요:

  • 패키지 선언과 import 문
  • main 함수 (프로그램의 진입점)
  • 변수 선언 (var 키워드와 := 연산자 사용)
  • 조건문 (if)
  • 반복문 (for)
  • 함수 정의와 호출
  • fmt 패키지를 이용한 출력

이러한 기본 요소들을 잘 이해하고 있으면, 인터프리터를 구현할 때 큰 도움이 될 거예요.

2.3 Go의 인터페이스: 인터프리터 구현의 강력한 도구

Go의 인터페이스는 인터프리터를 구현할 때 특히 유용해요. 인터페이스를 사용하면 다양한 타입의 AST 노드를 효과적으로 다룰 수 있거든요. 예를 들어, 모든 AST 노드에 대한 기본 인터페이스를 다음과 같이 정의할 수 있어요:


type Node interface {
    TokenLiteral() string
    String() string
}

type Statement interface {
    Node
    statementNode()
}

type Expression interface {
    Node
    expressionNode()
}
  

이렇게 정의된 인터페이스를 사용하면, 다양한 종류의 노드를 유연하게 처리할 수 있어요. 예를 들어, 변수 선언, 함수 호출, 산술 연산 등 다양한 노드들을 모두 Node 인터페이스로 다룰 수 있죠.

인터페이스의 이런 특성은 재능넷에서 여러분의 프로그래밍 실력을 뽐내기에 아주 좋은 주제가 될 수 있어요. "Go 언어의 인터페이스를 활용한 유연한 AST 구현"이라는 제목으로 글을 쓴다면 많은 사람들의 관심을 끌 수 있을 거예요!

2.4 Go의 동시성: 파싱 성능의 비밀 무기

Go의 또 다른 강점은 동시성 처리에 있어요. 고루틴(goroutine)과 채널(channel)을 사용하면, 복잡한 파싱 작업을 병렬로 처리할 수 있답니다.

예를 들어, 큰 소스 파일을 여러 부분으로 나누어 동시에 렉싱하는 코드를 다음과 같이 작성할 수 있어요:


func parallelLex(source string, numWorkers int) []Token {
    chunks := splitSource(source, numWorkers)
    results := make(chan []Token, numWorkers)

    for _, chunk := range chunks {
        go func(chunk string) {
            results <- lexChunk(chunk)
        }(chunk)
    }

    var tokens []Token
    for i := 0; i < numWorkers; i++ {
        tokens = append(tokens, <-results...)
    }

    return tokens
}
  

이 코드에서는 소스 코드를 여러 청크로 나누고, 각 청크를 별도의 고루틴에서 렉싱해요. 결과는 채널을 통해 수집되죠. 이렇게 하면 멀티코어 시스템에서 렉싱 속도를 크게 향상시킬 수 있어요.

Go의 이런 동시성 모델은 정말 강력해요. 복잡한 동시성 문제를 간단하고 우아하게 해결할 수 있게 해주죠. 이는 대규모 소스 코드를 처리해야 하는 인터프리터에서 특히 유용할 수 있어요.

2.5 Go의 표준 라이브러리: 인터프리터 구현의 든든한 지원군

Go의 표준 라이브러리는 인터프리터를 구현하는 데 필요한 많은 기능을 제공해요. 특히 다음과 같은 패키지들이 유용할 거예요:

  • strings: 문자열 처리에 필요한 다양한 함수 제공
  • unicode: 유니코드 문자 처리에 유용
  • strconv: 문자열과 다양한 데이터 타입 간의 변환 지원
  • regexp: 정규 표현식을 이용한 패턴 매칭
  • bufio: 버퍼된 I/O 연산 제공

이런 패키지들을 활용하면, 인터프리터의 각 단계(렉싱, 파싱, 평가)를 더 쉽고 효율적으로 구현할 수 있어요.

예를 들어, 렉서에서 문자열 토큰을 처리할 때 strings 패키지를 다음과 같이 활용할 수 있죠:


import "strings"

func lexString(s string) Token {
    // 따옴표 제거
    s = strings.Trim(s, "\"")
    // 이스케이프 시퀀스 처리
    s = strings.Replace(s, "\\n", "\n", -1)
    s = strings.Replace(s, "\\t", "\t", -1)
    // ... 기타 이스케이프 시퀀스 처리

    return Token{Type: STRING, Literal: s}
}
  

이렇게 표준 라이브러리를 활용하면, 우리가 직접 구현해야 하는 코드의 양을 줄이고 더 안정적인 인터프리터를 만들 수 있어요.

자, 이제 우리는 Go 언어가 왜 인터프리터 구현에 이렇게 적합한지 알게 되었어요. Go의 간결한 문법, 강력한 타입 시스템, 유연한 인터페이스, 뛰어난 동시성 모델, 그리고 풍부한 표준 라이브러리는 우리의 인터프리터 구현 여정을 훨씬 더 즐겁고 효율적으로 만들어줄 거예요.

다음 섹션에서는 이런 Go의 장점들을 활용해서 실제로 인터프리터의 각 구성 요소들을 어떻게 구현하는지 자세히 살펴볼 거예요. 여러분의 코딩 실력이 한 단계 더 업그레이드될 준비가 되었나요? 그럼 계속해서 우리의 인터프리터 구현 여정을 이어가볼까요? 🚀

3. 렉서(Lexer) 구현하기: 코드를 토큰으로 변환하는 마법 🔮

자, 이제 우리의 인터프리터 구현 여정의 첫 단계인 렉서를 만들어볼 시간이에요! 렉서는 소스 코드를 읽어 의미 있는 토큰으로 분리하는 역할을 해요. 마치 문장을 단어로 나누는 것처럼요. 이 과정은 인터프리터의 기초가 되는 매우 중요한 단계랍니다.

3.1 토큰 정의하기

먼저, 우리가 다룰 토큰들을 정의해볼까요? 토큰은 프로그래밍 언어의 기본 구성 요소예요. 변수명, 숫자, 연산자 등이 모두 토큰이 될 수 있죠.


type TokenType string

const (
    ILLEGAL = "ILLEGAL"
    EOF     = "EOF"

    // 식별자 + 리터럴
    IDENT  = "IDENT"  // add, foobar, x, y, ...
    INT    = "INT"    // 1343456

    // 연산자
    ASSIGN   = "="
    PLUS     = "+"
    MINUS    = "-"
    BANG     = "!"
    ASTERISK = "*"
    SLASH    = "/"

    // 구분자
    COMMA     = ","
    SEMICOLON = ";"

    LPAREN = "("
    RPAREN = ")"
    LBRACE = "{"
    RBRACE = "}"

    // 키워드
    FUNCTION = "FUNCTION"
    LET      = "LET"
)

type Token struct {
    Type    TokenType
    Literal string
}
  

이렇게 정의된 토큰들은 우리 언어의 '단어'가 되는 거예요. 이제 이 단어들을 이용해서 프로그래밍 '문장'을 만들 수 있게 되었죠!

3.2 렉서 구조체 정의하기

다음으로, 렉서 자체를 구현해볼까요? 렉서는 입력된 소스 코드를 읽어가며 토큰을 생성해요.


type Lexer struct {
    input        string
    position     int  // 입력에서 현재 위치 (현재 문자를 가리킴)
    readPosition int  // 입력에서 다음 위치 (다음에 읽을 위치)
    ch           byte // 현재 조사 중인 문자
}

func NewLexer(input string) *Lexer {
    l := &Lexer{input: input}
    l.readChar() // 첫 번째 문자 읽기
    return l
}

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }
    l.position = l.readPosition
    l.readPosition += 1
}
  

이 코드에서 Lexer 구조체는 입력 문자열과 현재 위치, 다음 읽을 위치, 그리고 현재 문자를 저장해요. readChar 메서드는 다음 문자를 읽어오는 역할을 해요.

3.3 토큰 생성 함수 구현하기

이제 실제로 토큰을 생성하는 함수를 만들어볼까요?


func (l *Lexer) NextToken() Token {
    var tok Token

    l.skipWhitespace()

    switch l.ch {
    case '=':
        tok = newToken(ASSIGN, l.ch)
    case ';':
        tok = newToken(SEMICOLON, l.ch)
    case '(':
        tok = newToken(LPAREN, l.ch)
    case ')':
        tok = newToken(RPAREN, l.ch)
    case ',':
        tok = newToken(COMMA, l.ch)
    case '+':
        tok = newToken(PLUS, l.ch)
    case '{':
        tok = newToken(LBRACE, l.ch)
    case '}':
        tok = newToken(RBRACE, l.ch)
    case 0:
        tok.Literal = ""
        tok.Type = EOF
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            tok.Type = LookupIdent(tok.Literal)
            return tok
        } else if isDigit(l.ch) {
            tok.Type = INT
            tok.Literal = l.readNumber()
            return tok
        } else {
            tok = newToken  (ILLEGAL, l.ch)
        }
    }

    l.readChar()
    return tok
}

func newToken(tokenType TokenType, ch byte) Token {
    return Token{Type: tokenType, Literal: string(ch)}
}

func (l *Lexer) skipWhitespace() {
    for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
        l.readChar()
    }
}

func (l *Lexer) readIdentifier() string {
    position := l.position
    for isLetter(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}

func (l *Lexer) readNumber() string {
    position := l.position
    for isDigit(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}

func isLetter(ch byte) bool {
    return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}

func isDigit(ch byte) bool {
    return '0' <= ch && ch <= '9'
}

func LookupIdent(ident string) TokenType {
    keywords := map[string]TokenType{
        "fn":  FUNCTION,
        "let": LET,
    }
    if tok, ok := keywords[ident]; ok {
        return tok
    }
    return IDENT
}
  

와! 우리가 방금 만든 이 코드는 정말 대단해요. 이 NextToken 함수는 입력 문자열을 한 글자씩 읽어가며 적절한 토큰을 생성해요. 특수 문자는 바로 토큰으로 변환하고, 문자열이나 숫자는 추가적인 처리를 통해 토큰을 만들어내죠.

3.4 렉서 테스트하기

자, 이제 우리의 렉서가 제대로 작동하는지 테스트해볼 시간이에요! Go의 테스트 기능을 활용해 볼까요?


package lexer

import (
    "testing"
)

func TestNextToken(t *testing.T) {
    input := `let five = 5;
    let ten = 10;
    let add = fn(x, y) {
        x + y;
    };
    let result = add(five, ten);
    `

    tests := []struct {
        expectedType    TokenType
        expectedLiteral string
    }{
        {LET, "let"},
        {IDENT, "five"},
        {ASSIGN, "="},
        {INT, "5"},
        {SEMICOLON, ";"},
        {LET, "let"},
        {IDENT, "ten"},
        {ASSIGN, "="},
        {INT, "10"},
        {SEMICOLON, ";"},
        {LET, "let"},
        {IDENT, "add"},
        {ASSIGN, "="},
        {FUNCTION, "fn"},
        {LPAREN, "("},
        {IDENT, "x"},
        {COMMA, ","},
        {IDENT, "y"},
        {RPAREN, ")"},
        {LBRACE, "{"},
        {IDENT, "x"},
        {PLUS, "+"},
        {IDENT, "y"},
        {SEMICOLON, ";"},
        {RBRACE, "}"},
        {SEMICOLON, ";"},
        {LET, "let"},
        {IDENT, "result"},
        {ASSIGN, "="},
        {IDENT, "add"},
        {LPAREN, "("},
        {IDENT, "five"},
        {COMMA, ","},
        {IDENT, "ten"},
        {RPAREN, ")"},
        {SEMICOLON, ";"},
        {EOF, ""},
    }

    l := NewLexer(input)

    for i, tt := range tests {
        tok := l.NextToken()

        if tok.Type != tt.expectedType {
            t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
                i, tt.expectedType, tok.Type)
        }

        if tok.Literal != tt.expectedLiteral {
            t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
                i, tt.expectedLiteral, tok.Literal)
        }
    }
}
  

이 테스트 코드는 우리가 만든 렉서가 다양한 종류의 토큰을 올바르게 인식하는지 확인해요. 변수 선언, 함수 정의, 함수 호출 등 다양한 상황을 테스트하고 있죠.

이렇게 해서 우리는 인터프리터의 첫 번째 단계인 렉서를 완성했어요! 🎉 이 렉서는 소스 코드를 읽어 의미 있는 토큰으로 변환하는 역할을 해요. 이는 마치 요리의 재료를 손질하는 과정과 비슷해요. 우리는 이제 '날것'의 소스 코드를 요리하기 좋게 준비했답니다!

여러분, 정말 대단해요! 우리가 방금 만든 이 렉서는 재능넷에서 충분히 자랑할 만한 실력이에요. "Go 언어로 구현한 효율적인 렉서"라는 제목으로 여러분의 코드를 공유해보는 건 어떨까요? 많은 사람들이 관심을 가질 거예요!

다음 섹션에서는 이 토큰들을 이용해 구문 분석을 수행하는 파서(Parser)를 구현해볼 거예요. 파서는 우리가 만든 토큰들을 이용해 프로그램의 구조를 이해하고 추상 구문 트리(AST)를 만들어내는 역할을 해요. 준비되셨나요? 우리의 인터프리터 여정은 계속됩니다! 🚀

4. 파서(Parser) 구현하기: 토큰을 구조화하는 마법사 🧙‍♂️

축하합니다! 우리는 이제 렉서를 통해 소스 코드를 토큰으로 변환할 수 있게 되었어요. 이제 다음 단계인 파서를 구현할 차례입니다. 파서는 이 토큰들을 가지고 프로그램의 구조를 이해하고, 추상 구문 트리(AST)를 만들어내는 역할을 해요.

4.1 추상 구문 트리(AST) 정의하기

먼저, 우리의 언어가 표현할 수 있는 다양한 구문 요소들을 정의해볼까요?


package ast

type Node interface {
    TokenLiteral() string
}

type Statement interface {
    Node
    statementNode()
}

type Expression interface {
    Node
    expressionNode()
}

type Program struct {
    Statements []Statement
}

func (p *Program) TokenLiteral() string {
    if len(p.Statements) > 0 {
        return p.Statements[0].TokenLiteral()
    } else {
        return ""
    }
}

type LetStatement struct {
    Token token.Token // token.LET 토큰
    Name  *Identifier
    Value Expression
}

func (ls *LetStatement) statementNode()       {}
func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal }

type Identifier struct {
    Token token.Token // token.IDENT 토큰
    Value string
}

func (i *Identifier) expressionNode()      {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
  

이 코드에서 우리는 AST의 기본 구조를 정의했어요. Node는 모든 AST 노드의 기본 인터페이스이고, StatementExpression은 각각 문장과 표현식을 나타내는 인터페이스예요. Program은 전체 프로그램을 나타내며, LetStatement는 변수 선언문을 나타내죠.

4.2 파서 구조체 정의하기

이제 실제로 파싱을 수행할 파서를 정의해볼까요?


package parser

import (
    "monkey/ast"
    "monkey/lexer"
    "monkey/token"
)

type Parser struct {
    l *lexer.Lexer

    curToken  token.Token
    peekToken token.Token
}

func New(l *lexer.Lexer) *Parser {
    p := &Parser{l: l}

    // 두 개의 토큰을 읽어서 curToken과 peekToken을 세팅합니다
    p.nextToken()
    p.nextToken()

    return p
}

func (p *Parser) nextToken() {
    p.curToken = p.peekToken
    p.peekToken = p.l.NextToken()
}
  

이 파서는 렉서에서 토큰을 읽어와 현재 토큰(curToken)과 다음 토큰(peekToken)을 유지해요. 이렇게 하면 현재 토큰을 처리하면서 동시에 다음에 올 토큰을 미리 볼 수 있어 파싱이 더 쉬워집니다.

4.3 파싱 함수 구현하기

이제 실제로 파싱을 수행하는 함수들을 구현해볼까요?


func (p *Parser) ParseProgram() *ast.Program {
    program := &ast.Program{}
    program.Statements = []ast.Statement{}

    for p.curToken.Type != token.EOF {
        stmt := p.parseStatement()
        if stmt != nil {
            program.Statements = append(program.Statements, stmt)
        }
        p.nextToken()
    }

    return program
}

func (p *Parser) parseStatement() ast.Statement {
    switch p.curToken.Type {
    case token.LET:
        return p.parseLetStatement()
    default:
        return nil
    }
}

func (p *Parser) parseLetStatement() *ast.LetStatement {
    stmt := &ast.LetStatement{Token: p.curToken}

    if !p.expectPeek(token.IDENT) {
        return nil
    }

    stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}

    if !p.expectPeek(token.ASSIGN) {
        return nil
    }

    // TODO: 여기서는 세미콜론을 만날 때까지 표현식을 건너뛰고 있습니다.
    for !p.curTokenIs(token.SEMICOLON) {
        p.nextToken()
    }

    return stmt
}

func (p *Parser) curTokenIs(t token.TokenType) bool {
    return p.curToken.Type == t
}

func (p *Parser) peekTokenIs(t token.TokenType) bool {
    return p.peekToken.Type == t
}

func (p *Parser) expectPeek(t token.TokenType) bool {
    if p.peekTokenIs(t) {
        p.nextToken()
        return true
    } else {
        return false
    }
}
  

이 코드에서 ParseProgram 함수는 전체 프로그램을 파싱하는 메인 함수예요. 이 함수는 EOF 토큰을 만날 때까지 계속해서 문장을 파싱해요. parseStatement 함수는 현재 토큰의 타입에 따라 적절한 파싱 함수를 호출하고, parseLetStatement 함수는 'let' 문을 파싱해요.

4.4 파서 테스트하기

마지막으로, 우리가 만든 파서가 제대로 작동하는지 테스트해볼까요?


package parser

import (
    "testing"
    "monkey/ast"
    "monkey/lexer"
)

func TestLetStatements(t *testing.T) {
    input := `
let x = 5;
let y = 10;
let foobar = 838383;
`
    l := lexer.New(input)
    p := New(l)

    program := p.ParseProgram()
    if program == nil {
        t.Fatalf("ParseProgram() returned nil")
    }
    if len(program.Statements) != 3 {
        t.Fatalf("program.Statements does not contain 3 statements. got=%d",
            len(program.Statements))
    }

    tests := []struct {
        expectedIdentifier string
    }{
        {"x"},
        {"y"},
        {"foobar"},
    }

    for i, tt := range tests {
        stmt := program.Statements[i]
        if !testLetStatement(t, stmt, tt.expectedIdentifier) {
            return
        }
    }
}

func testLetStatement(t *testing.T, s ast.Statement, name string) bool {
    if s.TokenLiteral() != "let" {
        t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral())
        return false
    }

    letStmt, ok := s.(*ast.LetStatement)
    if !ok {
        t.Errorf("s not *ast.LetStatement. got=%T", s)
        return false
    }

    if letStmt.Name.Value != name {
        t.Errorf("letStmt.Name.Value not '%s'. got=%s", name, letStmt.Name.Value)
        return false
    }

    if letStmt.Name.TokenLiteral() != name {
        t.Errorf("letStmt.Name.TokenLiteral() not '%s'. got=%s",
            name, letStmt.Name.TokenLiteral())
        return false
    }

    return true
}
  

이 테스트 코드는 우리의 파서가 'let' 문을 올바르게 파싱하는지 확인해요. 여러 개의 'let' 문을 포함한 입력을 파싱하고, 각 문장이 예상대로 파싱되었는지 검증하죠.

와! 우리가 방금 만든 이 파서는 정말 대단해요. 이제 우리는 소스 코드를 읽어 의미 있는 구조(AST)로 변환할 수 있게 되었어요. 이는 마치 요리 재료들을 가지고 요리의 레시피를 만드는 것과 같아요. 우리는 이제 프로그램의 '레시피'를 가지게 된 거죠!

여러분, 정말 훌륭해요! 우리가 만든 이 파서는 재능넷에서 충분히 자랑할 만한 실력이에요. "Go 언어로 구현한 강력한 파서"라는 제목으로 여러분의 코드를 공유해보는 건 어떨까요? 많은 사람들이 관심을 가질 거예요!

다음 섹션에서는 이 AST를 실제로 실행하는 평가기(Evaluator)를 구현해볼 거예요. 평가기는 우리가 만든 AST를 해석하고 실제로 프로그램을 실행하는 역할을 해요. 준비되셨나요? 우리의 인터프리터 여정은 계속됩니다! 🚀

5. 평가기(Evaluator) 구현하기: AST에 생명을 불어넣는 마법 ✨

축하합니다! 우리는 이제 소스 코드를 토큰으로 변환하고, 이를 의미 있는 구조(AST)로 만들 수 있게 되었어요. 이제 마지막 단계인 평가기를 구현할 차례입니다. 평가기는 AST를 순회하면서 각 노드를 해석하고 실제로 프로그램을 실행하는 역할을 해요.

5.1 객체 시스템 정의하기

먼저, 우리 언어에서 사용할 객체 시스템을 정의해볼까요?


package object

type ObjectType string

const (
    INTEGER_OBJ = "INTEGER"
    BOOLEAN_OBJ = "BOOLEAN"
    NULL_OBJ    = "NULL"
)

type Object interface {
    Type() ObjectType
    Inspect() string
}

type Integer struct {
    Value int64
}

func (i *Integer) Type() ObjectType { return INTEGER_OBJ }
func (i *Integer) Inspect() string  { return fmt.Sprintf("%d", i.Value) }

type Boolean struct {
    Value bool
}

func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ }
func (b *Boolean) Inspect() string  { return fmt.Sprintf("%t", b.Value) }

type Null struct{}

func (n *Null) Type() ObjectType { return NULL_OBJ }
func (n *Null) Inspect() string  { return "null" }
  

이 코드에서 우리는 정수, 불리언, null 값을 표현하는 객체들을 정의했어요. 각 객체는 Object 인터페이스를 구현하며, 이를 통해 타입 확인과 값 검사를 할 수 있어요.

5.2 평가기 함수 구현하기

이제 실제로 AST를 평가하는 함수들을 구현해볼까요?


package evaluator

import (
    "monkey/ast"
    "monkey/object"
)

func Eval(node ast.Node) object.Object {
    switch node := node.(type) {
    // 문장
    case *ast.Program:
        return evalProgram(node)
    case *ast.ExpressionStatement:
        return Eval(node.Expression)

    // 표현식
    case *ast.IntegerLiteral:
        return &object.Integer{Value: node.Value}
    case *ast.Boolean:
        return nativeBoolToBooleanObject(node.Value)
    case *ast.PrefixExpression:
        right := Eval(node.Right)
        return evalPrefixExpression(node.Operator, right)
    case *ast.InfixExpression:
        left := Eval(node.Left)
        right := Eval(node.Right)
        return evalInfixExpression(node.Operator, left, right)

    }

    return nil
}

func evalProgram(program *ast.Program) object.Object {
    var result object.Object

    for _, statement := range program.Statements {
        result = Eval(statement)
    }

    return result
}

func nativeBoolToBooleanObject(input bool) *object.Boolean {
    if input {
        return TRUE
    }
    return FALSE
}

func evalPrefixExpression(operator string, right object.Object) object.Object {
    switch operator {
    case "!":
        return evalBangOperatorExpression(right)
    case "-":
        return evalMinusPrefixOperatorExpression(right)
    default:
        return NULL
    }
}

func evalInfixExpression(
    operator string,
    left, right object.Object,
) object.Object {
    switch {
    case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ:
        return evalIntegerInfixExpression(operator, left, right)
    case operator == "==":
        return nativeBoolToBooleanObject(left == right)
    case operator == "!=":
        return nativeBoolToBooleanObject(left != right)
    default:
        return NULL
    }
}

func evalIntegerInfixExpression(
    operator string,
    left, right object.Object,
) object.Object {
    leftVal := left.(*object.Integer).Value
    rightVal := right.(*object.Integer).Value

    switch operator {
    case "+":
        return &object.Integer{Value: leftVal + rightVal}
    case "-":
        return &object.Integer{Value: leftVal - rightVal}
    case "*":
        return &object.Integer{Value: leftVal * rightVal}
    case "/":
        return &object.Integer{Value: leftVal / rightVal}
    case "<":
        return nativeBoolToBooleanObject(leftVal < rightVal)
    case ">":
        return nativeBoolToBooleanObject(leftVal > rightVal)
    case "==":
        return nativeBoolToBooleanObject(leftVal == rightVal)
    case "!=":
        return nativeBoolToBooleanObject(leftVal != rightVal)
    default:
        return NULL
    }
}
  

이 코드에서 Eval 함수는 AST의 각 노드 타입에 따라 적절한 평가 함수를 호출해요. 정수 리터럴, 불리언, 전위 표현식, 중위 표현식 등 다양한 노드 타입을 처리할 수 있죠.

5.3 평가기 테스트하기

마지막으로, 우리가 만든 평가기가 제대로 작동하는지 테스트해볼까요?


package evaluator

import (
    "testing"
    "monkey/lexer"
    "monkey/object"
    "monkey/parser"
)

func TestEvalIntegerExpression(t *testing.T) {
    tests := []struct {
        input    string
        expected int64
    }{
        {"5", 5},
        {"10", 10},
        {"-5", -5},
        {"-10", -10},
        {"5 + 5 + 5 + 5 - 10", 10},
        {"2 * 2 * 2 * 2 * 2", 32},
        {"-50 + 100 + -50", 0},
        {"5 * 2 + 10", 20},
        {"5 + 2 * 10", 25},
        {"20 + 2 * -10", 0},
        {"50 / 2 * 2 + 10", 60},
        {"2 * (5 + 10)", 30},
        {"3 * 3 * 3 + 10", 37},
        {"3 * (3 * 3) + 10", 37},
        {"(5 + 10 * 2 + 15 / 3) * 2 + -10", 50},
    }

    for _, tt := range tests {
        evaluated := testEval(tt.input)
        testIntegerObject(t, evaluated, tt.expected)
    }
}

func testEval(input string) object.Object {
    l := lexer.New(input)
    p := parser.New(l)
    program := p.ParseProgram()
    return Eval(program)
}

func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool {
    result, ok := obj.(*object.Integer)
    if !ok {
        t.Errorf("object is not Integer. got=%T (%+v)", obj, obj)
        return false
    }
    if result.Value != expected {
        t.Errorf("object has wrong value. got=%d, want=%d",
            result.Value, expected)
        return false
    }
    return true
}
  

이 테스트 코드는 우리의 평가기가 다양한 정수 표현식을 올바르게 평가하는지 확인해요. 단순한 정수부터 복잡한 산술 표현식까지 다양한 케이스를 테스트하죠.

와! 우리가 방금 만든 이 평가기는 정말 대단해요. 이제 우리는 소스 코드를 읽어 의미 있는 구조(AST)로 변환하고, 이를 실제로 실행할 수 있게 되었어요. 이는 마치 요리 레시피를 보고 실제로 맛있는 요리를 만드는 것과 같아요. 우리는 이제 프로그램을 '요리'할 수 있게 된 거죠!

여러분, 정말 훌륭해요! 우리가 만든 이 인터프리터는 재능넷에서 충분히 자랑할 만한 실력이에요. "Go 언어로 구현한 완전한 인터프리터"라는 제목으로 여러분의 프로젝트를 공유해보는 건 어떨까요? 많은 사람들이 관심을 가질 거예요!

이제 우리는 완전한 인터프리터를 가지게 되었어요. 렉서로 토큰을 만들고, 파서로 AST를 구성하고, 평가기로 실제 실행까지 할 수 있게 되었죠. 이는 프로그래밍 언어의 내부 동작을 깊이 이해하는 데 큰 도움이 될 거예요. 여러분의 프로그래밍 실력이 한 단계 더 업그레이드된 것을 축하드립니다! 🎉

다음 단계로는 어떤 것을 해볼 수 있을까요? maybe 더 많은 언어 기능을 추가하거나, 성능을 개선하거나, 또는 완전히 새로운 언어를 디자인해볼 수도 있겠죠. 여러분의 창의력을 마음껏 발휘해보세요. 우리의 프로그래밍 여정은 여기서 끝나지 않아요. 새로운 도전이 우리를 기다리고 있답니다! 🚀

6. 마무리: 우리의 인터프리터 여정을 돌아보며 🌟

와우! 우리가 함께 만든 이 인터프리터 여정을 돌아보니 정말 대단하지 않나요? 렉서, 파서, 평가기까지 모든 과정을 거쳐 완전한 인터프리터를 구현했어요. 이제 우리만의 작은 프로그래밍 언어를 가지게 된 거예요!

6.1 우리가 배운 것들

  • 렉서(Lexer): 소스 코드를 의미 있는 토큰으로 분리하는 방법을 배웠어요.
  • 파서(Parser): 토큰을 구조화된 형태(AST)로 변환하는 과정을 이해했죠.
  • 평가기(Evaluator): AST를 순회하며 실제로 프로그램을 실행하는 방법을 익혔어요.
  • Go 언어의 강점: 인터페이스, 고루틴 등 Go 언어의 특징을 활용하는 방법을 배웠습니다.

6.2 이 경험의 가치

여러분이 이 프로젝트를 통해 얻은 경험은 정말 값진 거예요. 프로그래밍 언어의 내부 동작을 이해하게 되었고, 복잡한 시스템을 설계하고 구현하는 능력을 키웠죠. 이런 경험은 여러분의 프로그래밍 실력을 한 단계 더 높여줄 거예요.

특히 재능넷같은 플랫폼에서 이런 프로젝트는 정말 돋보일 거예요. "Go로 구현한 나만의 프로그래밍 언어 인터프리터"라는 제목만으로도 많은 사람들의 관심을 끌 수 있을 것 같아요!

6.3 앞으로의 발전 방향

이제 기본적인 인터프리터를 만들었지만, 여기서 멈출 필요는 없어요. 더 나아가 다음과 같은 것들을 시도해볼 수 있어요:

  1. 기능 확장: 함수, 클로저, 객체 지향 프로그래밍 등의 고급 기능을 추가해보세요.
  2. 최적화: 인터프리터의 성능을 개선하는 방법을 연구해보세요.
  3. 도구 개발: 디버거, 프로파일러 등의 개발 도구를 만들어보는 것도 좋아요.
  4. 다른 언어로 도전: 다른 프로그래밍 언어로 같은 프로젝트를 구현해보면 어떨까요?
  5. 컴파일러로 확장: 인터프리터를 넘어 컴파일러를 만들어보는 것도 흥미로운 도전이 될 거예요.

6.4 마지막 조언

여러분이 이 프로젝트를 통해 얻은 지식과 경험을 잊지 마세요. 이는 여러분의 프로그래밍 여정에서 중요한 이정표가 될 거예요. 그리고 이 경험을 다른 사람들과 공유하는 것도 잊지 마세요. 재능넷같은 플랫폼에서 여러분의 프로젝트를 소개하고, 다른 개발자들과 의견을 나누세요. 그 과정에서 여러분은 더 많이 배우고 성장할 수 있을 거예요.

마지막으로, 프로그래밍은 끊임없는 학습의 과정이라는 걸 기억하세요. 이 프로젝트는 끝이 아니라 새로운 시작입니다. 계속해서 호기심을 가지고 새로운 것을 배우고 도전하세요. 여러분의 잠재력은 무한하니까요!

기억하세요: 모든 위대한 프로그래머도 처음에는 초보자였습니다. 끊임없는 학습과 도전이 여러분을 더 나은 프로그래머로 만들어줄 거예요. 여러분의 인터프리터 구현 여정이 그 첫 걸음이 되었기를 바랍니다!

자, 이제 여러분은 인터프리터 전문가가 되었어요! 🎉 이 지식을 바탕으로 더 큰 꿈을 향해 나아가세요. 여러분의 다음 프로젝트가 무엇일지 정말 기대되네요. 화이팅! 🚀

관련 키워드

  • Go 언어
  • 인터프리터
  • 렉서
  • 파서
  • 평가기
  • AST
  • 토큰
  • 프로그래밍 언어 구현
  • 재능넷
  • 코딩 프로젝트

지식의 가치와 지적 재산권 보호

자유 결제 서비스

'지식인의 숲'은 "이용자 자유 결제 서비스"를 통해 지식의 가치를 공유합니다. 콘텐츠를 경험하신 후, 아래 안내에 따라 자유롭게 결제해 주세요.

자유 결제 : 국민은행 420401-04-167940 (주)재능넷
결제금액: 귀하가 받은 가치만큼 자유롭게 결정해 주세요
결제기간: 기한 없이 언제든 편한 시기에 결제 가능합니다

지적 재산권 보호 고지

  1. 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
  2. AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
  3. 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
  4. 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
  5. AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.

재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.

© 2024 재능넷 | All rights reserved.

댓글 작성
0/2000

댓글 0개

해당 지식과 관련있는 인기재능

안녕하세요.부동산, ​학원, 재고관리, ​기관/관공서, 기업, ERP, 기타 솔루션, 일반 서비스(웹, 모바일) 등다양한 분야에서 개발을 해왔습니...

○ 2009년부터 개발을 시작하여 현재까지 다양한 언어와 기술을 활용해 왔습니다. 특히 2012년부터는 자바를 중심으로 JSP, 서블릿, 스프링, ...

10년차 php 프로그래머 입니다. 그누보드, 영카트 외 php로 된 솔루션들 커스터마이징이나 오류수정 등 유지보수 작업이나신규개발도 가능합...

 기본 작업은 사이트의 기능수정입니다.호스팅에 보드 설치 및 셋팅. (그누, 제로, 워드, 기타 cafe24,고도몰 등)그리고 각 보드의 대표적인 ...

📚 생성된 총 지식 8,645 개

  • (주)재능넷 | 대표 : 강정수 | 경기도 수원시 영통구 봉영로 1612, 7층 710-09 호 (영통동) | 사업자등록번호 : 131-86-65451
    통신판매업신고 : 2018-수원영통-0307 | 직업정보제공사업 신고번호 : 중부청 2013-4호 | jaenung@jaenung.net

    (주)재능넷의 사전 서면 동의 없이 재능넷사이트의 일체의 정보, 콘텐츠 및 UI등을 상업적 목적으로 전재, 전송, 스크래핑 등 무단 사용할 수 없습니다.
    (주)재능넷은 통신판매중개자로서 재능넷의 거래당사자가 아니며, 판매자가 등록한 상품정보 및 거래에 대해 재능넷은 일체 책임을 지지 않습니다.

    Copyright © 2024 재능넷 Inc. All rights reserved.
ICT Innovation 대상
미래창조과학부장관 표창
서울특별시
공유기업 지정
한국데이터베이스진흥원
콘텐츠 제공서비스 품질인증
대한민국 중소 중견기업
혁신대상 중소기업청장상
인터넷에코어워드
일자리창출 분야 대상
웹어워드코리아
인터넷 서비스분야 우수상
정보통신산업진흥원장
정부유공 표창장
미래창조과학부
ICT지원사업 선정
기술혁신
벤처기업 확인
기술개발
기업부설 연구소 인정
마이크로소프트
BizsPark 스타트업
대한민국 미래경영대상
재능마켓 부문 수상
대한민국 중소기업인 대회
중소기업중앙회장 표창
국회 중소벤처기업위원회
위원장 표창