學習筆記——Let’s Build A Simple Interpreter 1

為什麼要你學解釋器和編譯器?這裡你三條理由。

  1. 要寫一個解釋器或編譯器,你必須同時用到很多技術。編寫一個解釋器或編譯器會幫助 你提高這些技能並且成為一個更好的軟件開發者。而且,你將學到的這些技能在開發任 何軟件時都有可能用到,而不僅僅是解釋器或編譯器。
  2. 你確實想要知道計算機如何工作。一般解釋器和編譯器看上去都像魔法一樣。但你不應 該對這些魔法感到舒服。你想要揭開解釋器和編譯器的神秘面紗,理解它們如何工作並 控制所有一切。
  3. 你想要創造自己的編程語言或者領域特定語言。如果是這樣,你就需要為這個語言創建 一個解釋器或編譯器。最近,創建新語言再度興起。你幾乎每天都可以看到一門新語言 的誕生:Elixir, Go, Rust 等。

原文鏈接:https://ruslanspivak.com/lsbasi-part1/

好了,但什麼是解釋器編譯器呢?

解釋器與編譯器

解釋器與編譯器都是“高級語言與機器之間的翻譯官”。都是將代碼翻譯成機器可以執行的二進制機器碼,只不過在運行原理和翻譯過程不同。

那它們的區別在於:

  • 編譯器:先整體編譯完,然後一次性執行。比如:C語言代碼被編譯成二進制代碼(exe程序),在windows平臺上執行。
  • 解釋器:解釋一句後就提交計算機執行一句,即便捷式邊執行。比如php,postscritp,javascript就是典型的解釋性語言。

用一個通俗的例子來講:我們去飯館吃飯,點了八菜一湯。編譯器的方式就是廚師把所有的菜給你全做好了,一起給你端上來,至於你在哪吃,怎麼吃,隨便。解釋器的方式就是廚師做好一個菜給你上一個菜,你就吃這個菜,而且必須在飯店裡吃。

學習筆記——Let’s Build A Simple Interpreter 1

編譯器與解釋器的工作流程的差別:

學習筆記——Let’s Build A Simple Interpreter 1

編譯器與解釋器的各自的特點:

學習筆記——Let’s Build A Simple Interpreter 1

構造解釋器V1.0

該系列文章的作者使用 Python 編寫Pascal語言的解釋器。

第一版V1.0,構造的計算器有諸多限制。如:

  • 只輸入一位的數字
  • 現階段僅支持加法操作
  • 輸入中不允許有空白符

這些約束使得構建一個計算器很簡單,代碼如下:

<code> # Token types:
 # EOF (end-of-file) token is used to indicate that
 # there is no more input left for lexical analysis
 INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF'
 
 
 class Token(object):
     def __init__(self, type, value):
  # token type: INTEGER, PLUS, or EOF
  self.type = type
  # token value: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '+', or None
  self.value = value
 
     def __str__(self):
  """String representation of the class instance.
 ​
  Examples:
    Token(INTEGER, 3)
    Token(PLUS, '+')
  """
  return 'Token({type}, {value})'.format(
     type=self.type,
     value=repr(self.value)
  )
 ​
     def __repr__(self):
  return self.__str__()
 ​
 class Interpreter(object):
     def __init__(self, text):
  # client string input, e.g. "3+5"
  self.text = text
  # self.pos is an index into self.text
  self.pos = 0
  # current token instance
  self.current_token = None
 ​
     def error(self):
  raise Exception('Error parsing input')
 ​
     def get_next_token(self):

  """Lexical analyzer (also known as scanner or tokenizer)
 ​
  This method is responsible for breaking a sentence
  apart into tokens. One token at a time.
  """
  text = self.text
 ​
  # is self.pos index past the end of the self.text ?
  # if so, then return EOF token because there no more
  # input left to convert into tokens
  if self.pos > len(text) - 1:
     return Token(EOF, None)
 ​
  # get a character at the position self.pos and decide
  # what token to create based on the single character
  current_char = text[self.pos]
 ​
  # if the character is a digit then convert it to
  # integer, create an INTEGER token, increment self.pos
  # index to point to the next character after the digit,
  # and return the INTEGER token
  if current_char.isdigit():
     token     = Token(INTEGER, int(current_char))
     self.pos += 1
     return token
 ​
  if current_char == '+':
     token     = Token(PLUS, current_char)
     self.pos += 1
     return token
 ​
  self.error()
 ​
     def eat(self, token_type):
  # compare the current token type with the passed token
  # type and if they match then "eat" the current token
  # and assign the next token to the self.current_token,
  # otherwise raise an exception.
  if self.current_token.type == token_type:
     self.current_token = self.get_next_token()
  else:
     self.error()
 ​
     def expr(self):
  """expr -> INTEGER PLUS INTEGER"""
  # set current token to the first token taken from the input
  self.current_token = self.get_next_token()
 ​
  # we expect the current token to be a single-digit integer
  left = self.current_token

  self.eat(INTEGER)
 ​
  # we expect the current token to be a '+' token
  op = self.current_token
  self.eat(PLUS)
 ​
  # we expect the current token to be a single-digit integer
  right = self.current_token
  self.eat(INTEGER)
  # after the above call the self.current_token is set to
  # EOF token
 ​
  # at this point INTEGER PLUS INTEGER sequence of tokens
  # has been successfully found and the method can just
  # return the result of adding two integers, thus
  # effectively interpreting client input
  result = left.value + right.value
  return result
 ​
 def main():
     while True:
  try:
     # To run under Python3 replace 'raw_input' call with 'input'
     text = input('calc> ')
  except EOFError:
     break
  if not text:
     continue
  interpreter = Interpreter(text)
  result = interpreter.expr()
  print(result)
 ​
 if __name__ == '__main__':
     main()/<code>

把以上代碼保存到名為 calc1.py 中,或者直接從 GitHub 上下載。在你開始仔細研究代 碼之前,在命令行上運行這個計算器並看它實現運行。把玩一下!下面是在我筆記本上的一 次嘗試(如果你想在 Python3 下運行,就需要把 raw_input 替換為 input):

<code> $ python calc1.py
 calc> 3+4
 7
 calc> 3+5

 8
 calc> 3+9
 12
 calc>/<code>

代碼分析

假設我們在命令行輸入一個表達式“3+5”。你的解釋器得到一個字符串 “3+5”。為了使解釋器真正理解如何處理這個字符串,需要先把輸入的 “3+5” 拆分成被叫做 token 的部件。

詞法分析:(lexical analysis,簡稱lexer,亦稱scannertokenizer

詞法分析也稱為 分詞 ,此階段編譯器從左向右掃描源文件,將其字符流分割成一個個的 token記號 ,後文中將稱為 token )。


Token

所謂 token ,就是源文件中不可再進一步分割的一串字符,類似於英語中單詞,或漢語中的詞。


這裡的 token 就是一個有類型的值的對象(即,token還存著值的類型)。例如對於字符串“3”來說,token 類型為 INTEGER , 相應的值是整數 3 。

解釋器Interpreter要做的第一步就是讀取輸入的字符串並把他轉化成 token 。解釋器中做這個工作的部分被稱為 詞法分析器(lexical analyzer),簡稱 lexer 。也可以稱它為: scannertokenizer 。他們的含義是一樣的:表示解釋器或編譯器中將輸入的字符串轉化為 token 流的部分。

那是如何轉化為token流呢?

  • 解釋器 Interpreter中的 get_next_token 方法就是你的詞法分析器。你每次調用它,就會從輸入到解釋器的字符流中得到下一個 token。讓我們仔細看一下這個方法,看看它是怎麼把字符轉化 為 token 的。輸入被存放在變量 text 中,它保存了輸入的字符串, pos 是指向該字符串的一個索引(把字符串看作是一個字符數組)。 pos 的初值被設為 0, 指向字符‘3’。 該方法首先檢查該字符是不是數字,若是數字,就遞增 pos 並返回一個類型為 INTEGER 值 為整數 3 的 token:
學習筆記——Let’s Build A Simple Interpreter 1

現在 pos 指向了 text 中的字符‘+’,下次你調用這個方法時,它會先測試 pos 位 置的字符是否是數字,然後再測試它是否是加號,此時它是加號。這樣該方法就遞增 pos 並返回一個類型為 PLUS 值為‘+’的 token:

學習筆記——Let’s Build A Simple Interpreter 1


現在 pos 指向了字符‘5’。當你再次調用 get_next_token 時,它會檢查 pos 位置 是否是一個數字,此時是的,因此它遞增 pos 並返回一個類型為 INTEGER 值為‘5’的 token:

學習筆記——Let’s Build A Simple Interpreter 1

現在索引 pos 越過了字符串“3+5”的末尾,接下來每次調用 get_next_token 方法都會 返回 EOF token:

學習筆記——Let’s Build A Simple Interpreter 1

自己動手試試看看你的計算器的 lexer 組件怎麼工作的:

<code> >>> from calc1 import Interpreter
 >>>
 >>> interpreter = Interpreter('3+5')
 >>> interpreter.get_next_token()
 Token(INTEGER, 3)
 >>>
 >>> interpreter.get_next_token()
 Token(PLUS, '+')
 >>>
 >>> interpreter.get_next_token()
 Token(INTEGER, 5)
 >>>
 >>> interpreter.get_next_token()
 Token(EOF, None)
 >>>/<code>

此時你的解釋器已經可以從輸入的字符流中獲得 token 流了,解釋器需要對它做點什麼: 它需要從使用 lexer get_next_token 得到的字符流中找到結構。你的解釋器期望從 流中找到如下的結構: INTEGER -> PLUS -> INTEGER. 即,它試著找到這樣一個 token 序 列:整數後跟一個加號再跟一個整數。

負責查找和解釋這個結構的方法是 expr. 這個方法驗證一個 token 序列是否遵從期望的 token 序列,即 INTEGER -> PLUS -> INTEGER. 當確定遵從這個結構後,它就把 PLUS 左 邊和右邊 token 的值相加來生成結果,從而成功地解釋了你傳給解釋器的算術表達式。

expr 方法使用了輔助方法 eat 來驗證傳給 eat 的 token 類型與當前的 token 類 型相匹配。在匹配到傳入的 token 類型後, eat 方法會取得下一個 token 並把它賦值 給變量 current_token, 這樣實際上是“吃掉”了當前匹配的 token 並把想象中的 token 流中的指針向前移動了。如果 token 流中的結構不遵從期望的 INTEGER PLUS INTEGER 序 列, eat 方法就會拋出一個異常。

小結

回顧一下你的解釋器為了對一個算術表達式求值都做了什麼:

  1. 解釋器接Interpreter收一個輸入字符串,假設為“3+5”
  2. 解釋器調用了 expr 方法來從詞法解析器 get_next_token 返回的 token 流中尋找一個結構。這個結構就是一個 INTEGER PLUS INTEGER 的形式。當確認了這個結構以後,它就使用把兩個 INTEGER token 相加的方式來解釋這個輸入,因為此時解釋器已經清楚 地知道它要做的就是把 3 和 5 兩個整數相加。

祝賀你。你剛剛學會了怎麼構造你的第一個解釋器!


現在是時候做此練習了。

學習筆記——Let’s Build A Simple Interpreter 1

你不會覺得你剛剛讀了這篇文章就足夠了,是吧?好了,自己動手做下面的練習:

  1. 修改代碼使得允許輸入多位整數,例如“12+3”
  2. 增加一個跳過空白符的方法,使你的計算器可以處理包含空白符的輸入如 “ 12 + 3”
  3. 修改代碼使得它可以處理‘-’而非‘+’的情況

檢查你的理解。

  1. 什麼是解釋器?
  2. 什麼是編譯器?
  3. 解釋器和編譯器的區別是什麼?
  4. 什麼是 token?
  5. 將輸入拆分成 token 的過程叫什麼?
  6. 解釋器中做詞法分析的部分叫什麼?
  7. 解釋器或編譯器的這個部分還有什麼其他常見的名字?


相關文章鏈接:

編譯器與解釋器:https://www.liujiangblog.com/course/python/9

Let’s Build A Simple Interpreter. Part 1:https://feng-qi.github.io/2018/01/23/lets-build-a-simple-interpreter-part-01/


分享到:


相關文章: