PL真有意思(二):程序設計語言語法

前言

雖然標題是程序語言的語法,可是講的是對詞法和語法的解析,其實關於這個前面那個寫編譯器系列的描述會更清楚,有關語言語法的部分應該是穿插在整個設計當中的,也看語言設計者的心情了git

和英語漢語這些天然語言不同,計算機語言必須是精確的,它們的語法和語義都必須保證沒有歧義,這固然也讓語法分析更加簡單github

因此對於編譯器一項很重要的任務就是時別程序設計語言的結構規則,要完成這個目標就須要兩個要求:正則表達式

  • 完成對語法規則的描述
  • 肯定給定程序是否按照這些規則構造起來,也就是符合語法規則

第一個要求主要由正則表達式和上下文無關文法來描述完成,而第二個要求就是由編譯器來完成,也就是語法分析了markdown

描述語法:正則表達式和上下文無關語法

對於詞法,均可以用三種規則描述出來:閉包

  1. 拼接
  2. 選擇
  3. Kleene(也就是重複任意屢次)

好比一個整數常量就能夠是多個數字重複任意屢次,也叫作正則語言。若是對於一個字符串,咱們再加入遞歸定義便可以描述整個語法,就能夠稱做上下文無關語法函數

單詞正則表達式

對於程序語言,單詞的類型不外乎關鍵字、標識符、符合和各類類型的常量oop

對於整數常量就能夠用這樣的正則表達式來表示spa

integer -> digit digit* digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9設計

上下文無關文法

通常正則表達式只適用於描述單詞,由於正則表達式沒法描述嵌套結構,通常正則表達式的實現都是用有限狀態自動機,以前用Python實現了一個簡單的正則表達式引擎也是這樣,可是對於匹配任意深度的嵌套結構就須要有一個任意大的狀態機,顯然不符合。而定義嵌套結構對於描述語法很是有用,因此就有了上下文無關文法code

expr := id | number | - expr | ( expr ) | expr or expr

op := + | - | * | /

對於上下文無關文法,每條規則叫作一個產生式,產生式左部的符合稱爲非終結符,而右部則是多個終結符或者非終結符,最後全部規則都會推到至終結符上,而終結符就是正則表達式定義的單詞

推導和語法樹

一個正確的上下文無關文法,就能夠指導咱們如何生成一個合乎語法的終結符串

最簡單的就是從開始符號開始,用這個產生式的右部取代開始符合,再從獲得的串選擇一個非終結符繼續進行推導,直到沒有剩下的非終結符,這個過程就像遞歸構造一個樹的過程

expr := expr op expr
     := expr op id
     := expr + id
     := expr op expr + id
     := expr op id + id
     := expr * id + id
     := id * id + id
複製代碼

可是對於給定的上下文語法有可能會推導出不止一顆語法分析樹,咱們就說這個上下文語法是存在歧義性的。因此對於上面的上下文無關語法還有更好的文法

掃描

掃描也就是詞法分析,詞法分析徹底能夠不須要什麼正則表達式、自動機什麼的,徒手擼出來,如今業界爲了更好的生成錯誤信息,應該不少也是手工的詞法分析器

手工的詞法分析器,無非就是一直讀入字符,到能判斷出它的token在送入語法分析器

有限狀態自動機

使用有限狀態機的詞法分析通常都是這樣的幾個步驟

  • 給出詞法的正則表達式

  • 將正則表達式轉換爲非肯定有限自動機(NFA)

其實對於任意的正則表達式均可以用拼接、選擇和Kleene閉包來表示

而一樣的,有限自動機也能夠經過這三種方式來表示,圖就不畫了,這個在以前寫Python正則表達式引擎的文章裏都畫過了(溜了

  • 將NFA轉換爲肯定性有限狀態自動機(DFA)

將NFA轉換到DFA能夠採用的是子集構造法,主要思想就是,在讀入給定輸入以後所到達的DFA狀態,表示的是原來NFA讀入一樣輸入以後可能澳大的全部狀態

  • 最小化DFA

對於最小化DFA的主要思想是,咱們把DFA全部狀態分爲兩個等價類,終止態狀態和非終止狀態。而後咱們就反覆搜索等價類X和輸入符合c,使得當給定C做爲輸入時,X的狀態能轉換到位於k>1個不一樣等價類中的狀態。以後咱們就把X劃分爲k個類,使得類中全部轉檯對於C都會轉移到同一個老類的成員。直到沒法再按這種方式找到劃分的類時,咱們就完成了

這四個步驟在以前的寫的正則表達式引擎中都完成了,在那三篇文章裏會更詳細一點

語法分析

通常語法分析器的輸入是token流,而輸出是一顆語法分析樹。其中分析方法通常能夠分爲自上而下和自下而上兩類,這些類中最重要的兩個分別稱爲LL和LR

LL表示從左向右,最左推導,LR表示從左向右,最右推導。這兩類文法都是從左到右的順序讀取輸入,而後語法分析器試圖找出輸入的推導結果

自上而下的方式

通常自上而下的語法分析器比較符合以前的推導方法,從根節點開始像葉節點反覆的遞歸推導,直到當前的葉節點都是終結符

  • 遞歸降低

遞歸降低很符合上面說的從根節點出發進行推導,通常用於一些相對簡單一些的語言

read A
read B
sum := A + B
write sum
write sum / 2
複製代碼

好比對於這個程序的遞歸降低,語法分析器一開始調用program函數,在讀入第一個單詞read後,program將調用stmt_list,再接着調用stmt才真正開始匹配read A。以這種方式繼續下去,語法分析器執行路徑將追溯出語法分析樹的從左向右、自上而下的遍歷

  • 表格驅動的LL自上而下

表格驅動的LL是基於一個語法分析表格和一個棧

分析流程是

  1. 初始化一個棧
  2. 將開始符號壓入棧
  3. 彈出棧頂,而後根據棧頂的符號和當前的輸入符號查表
  4. 若是彈出的是非終結符,將會繼續查表來肯定下一個壓入棧中的產生式
  5. 若是是終結符將進行匹配

預測集合

從上面能夠看出來最重要的就是那個語法分析表格了,語法分析表格其實就是根據當前輸入字符對下一個產生式的預測,這裏就要用到一個概念:預測集合,也就是First和Follow集合。這個在以前寫編譯器系列講的比較詳細,在這裏就不寫了

固然LL語法也會有不少處理不了的文法,因此也纔會有其它的語法分析方法

自下而上的方式

在實踐中,自下而上的語法分析都是表格驅動的,這種分析器在一個棧中保存全部部分完成的子樹的根。當它從掃描器中獲得一個新的單詞時,就會將這個單詞移入棧。當它發現位於棧頂的若干符號組成一個右部時,它就會將這些符號歸約到對應的左部。

一個自底向上的語法分析過程對應爲一個輸入串構造語法分析書的過程,它從葉子節點開始,經過shift和reduce操做逐漸向上到達根節點

自底向上的語法分析須要一個堆棧來存放解析的符號,例如對於以下語法:

0.	statement -> expr
1.	expr -> expr + factor
2.	         | factor
3.	factor ->  ( expr )
4.	         | NUM
複製代碼

來解析1+2

stack input
null 1 + 2
NUM + 2 開始讀入一個字符,並把對應的token放入解析堆棧,稱爲shift操做
factor + 2 根據語法推導式,factor -> NUM,將NUM出棧,factor入棧,這個操做稱爲reduce
expr + 2 這裏繼續作reduce操做,可是因爲語法推導式有兩個產生式,因此須要向前看一個符合才能判斷是進行shift仍是reduce,也就是語法解析的LA
expr + 2 shift操做
expr + NUM null shift操做
expr + factor null 根據fator的產生式進行reduce
expr null reduce操做
statement null reduce操做

此時規約到開始符號,而且輸入串也爲空,表明語法解析成功

有限狀態自動機的構建

0.	s -> e
1.	e -> e + t
2.	e -> t
3.	t -> t * f
4.	t -> f
5.	f -> ( e )
6.	f -> NUM
複製代碼
  • 對起始推導式作閉包操做

先在起始產生式->右邊加上一個.

s -> .e
複製代碼

對.右邊的符號作閉包操做,也就是說若是 . 右邊的符號是一個非終結符,那麼確定有某個表達式,->左邊是該非終結符,把這些表達式添加進來

s -> . e
e -> . e + t
e -> . t
複製代碼

對新添加進來的推導式反覆重複這個操做,直到全部推導式->右邊是非終結符的那個所在推導式都引入

  • 對引入的產生式進行分區

把 . 右邊擁有相同非終結符的表達式劃入一個分區,好比

e -> t .
t -> t . * f
複製代碼

就做爲同一個分區。最後把每一個分區中的表達式中的 . 右移動一位,造成新的狀態節點

  • 對全部分區節點構建跳轉關係

根據每一個節點 . 左邊的符號來判斷輸入什麼字符來跳入該節點

好比, . 左邊的符號是 t, 因此當狀態機處於狀態0時,輸入時 t 時, 跳轉到狀態1。

  • 對全部新生成的節點重複構建

最後對每一個新生成的節點進行重複的構建,直到完成全部全部的狀態節點的構建和跳轉

小結

這一篇主要是提了對詞法和語法的分析過程,由於想要結合語言設計和實踐,更詳細的應該去看前面的寫一個編譯器系列

相關文章
相關標籤/搜索