簡體   English   中英

在 python PLY(lex/yacc) 中使用空生產規則的語法錯誤

[英]Syntax error using empty production rule in python PLY(lex/yacc)

此處給出了完整的示例:

import ply.lex as lex
import Property
# List of token names.   This is always required
tokens = [
    'CheckupInformation',
    'Introduction',
    'Information',
    'perfect',
    'sick',
    'LPAREN',
    'RPAREN',
    'CHAR',
    'NUMBER'
    ] 
def t_CheckupInformation(t)     : 'CheckupInformation'     ; return t
def t_Introduction(t)  : 'Introduction'  ; return t
def t_Information(t) : 'Information' ; return t
def t_perfect(t): 'perfect'; return t
def t_sick(t) : 'sick'; return t



t_LPAREN  = r'\('
t_RPAREN  = r'\)'
t_CHAR = r'[a-zA-Z_][a-zA-Z0-9_\-]*'
t_ignore = " \t"
# Define a rule so we can track line numbers

def t_NUMBER(t):
    r'[+\-0-9_][0-9_]*'
    t.lexer.lineno += len(t.value)
    try:
        t.value = int(t.value)
    except ValueError:
        print("Integer value too large %s" % t.value)
        t.value = 0
    return t
def t_SEMICOLON(t):
    r'\;.*'
    t.lexer.lineno += len(t.value)
def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)
# Error handling rule
def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)

 # Build the lexer
lexer = lex.lex()
# define upper level classes first     
class stat:
    def __init__(self):
        self.statement = ""
        self.intro = list()
        self.body = list()


P=stat()
def p_stat(p):
    'Stat : LPAREN CheckupInformation statIntro statBody RPAREN'
    p[0]=(p[1],p[2],p[3],p[4],p[5])

def p_Intro(p) : 
    '''statIntro : LPAREN Introduction Name RPAREN
                 | statIntro LPAREN Introduction Name RPAREN
                 | empty'''

    if len(p)==5:
       p[0] = (p[3])
    elif len(p)==6:
       p[0] = (p[4])
    else:
       p[0]= None
    P.intro.append(p[0])

def p_Name(p):
    'Name : CHAR'
    p[0]=p[1]



def p_Body(p):
    '''statBody : LPAREN Information bodyinfo RPAREN
                | statBody LPAREN Information bodyinfo RPAREN'''
    if len(p)==5:
       p[0] = (p[3])
    elif len(p)==6:
       p[0] = (p[4])
    P.body.append(p[0])
def p_bodyinfo(p):
    '''bodyinfo : LPAREN CHAR perfect RPAREN
                | LPAREN CHAR sick RPAREN'''
    p[0]=p[2],p[3]


def p_empty(p):
    'empty :  '
    print("This function is called")
    pass   
def p_error(p):
    print("Syntax error in input '%s'!" % p.value)

import ply.yacc as yacc
parser = yacc.yacc()
import sys
if len(sys.argv) < 2 :
    sys.exit("Usage: %s <filename>" % sys.argv[0])
fp = open(sys.argv[1])
contents=fp.read()
result=parser.parse(contents)

print("(CheckupInformation")
if (P.intro) != None:
    for x in range(len(P.intro)):
        print("    (Introduction %s)" %(P.intro[x]))
for x in range(len(P.body)):
        print("    (Information( %s %s))" %(P.body[x]))
print(")")

文本 File1 是:

(CheckupInformation
  (Introduction John)
  (Introduction Patt)
  (Information(Anonymous1 perfect))
  (Information(Anonymous2 sick))
)

文本 File2 是:

(CheckupInformation
  (Information(Anonymous1 perfect))
  (Information(Anonymous2 sick))
)

根據我的 bnf 語法,“介紹”是可選的。 帶有文本 file1 的上層代碼運行良好。 但是當我從文本文件中刪除“介紹”部分作為文本文件 2 時,它會在“正文”部分給出語法錯誤,這意味着它無法處理空生產。 請幫助我如何解決錯誤。 如何處理我的代碼的空生產規則?

錯誤消息:錯誤消息片段

您的程序無法運行,因為您import Property ,它不是標准庫模塊。 但是刪除該行至少足以到達 Ply 嘗試構建解析器的地步,此時它會生成多個警告,包括 shift/reduce 沖突警告。 最后一個警告很重要; 你應該嘗試修復它,你當然不應該忽視它。 (這意味着您應該將其作為問題的一部分報告。)正是這種沖突阻止了您的解析器工作。

它是這樣說的:

WARNING: Token 'NUMBER' defined, but not used
WARNING: There is 1 unused token
Generating LALR tables
WARNING: 1 shift/reduce conflict

Ply 生成文件parser.out ,其中包含有關您的語法的更多信息,包括移位/減少沖突的詳細描述。 檢查該文件,我們發現以下內容:

state 3

    (1) Stat -> LPAREN CheckupInformation . statIntro statBody RPAREN
    (2) statIntro -> . LPAREN Introduction Name RPAREN
    (3) statIntro -> . statIntro LPAREN Introduction Name RPAREN
    (4) statIntro -> . empty
    (10) empty -> .

  ! shift/reduce conflict for LPAREN resolved as shift
    LPAREN          shift and go to state 4

  ! LPAREN          [ reduce using rule 10 (empty -> .) ]

    statIntro                      shift and go to state 5
    empty                          shift and go to state 6

解析器在處理Stat時進入 State 3 :

Stat -> LPAREN CheckupInformation . statIntro statBody RPAREN

CheckupInformationstatIntro之間的點表示進度。 state 中可能有不止一個產生式,中間有點,這意味着解析器還沒有弄清楚要選擇哪些替代方案。 也有以點開頭的作品; 這些將對應於緊跟在點之后的非終結符,表明現在需要考慮這些產生式。

也可能有結尾帶有點的產生式,這表明在解析的這一點上,遇到的符號序列可以“減少”到相應的非終結符。 換句話說,解析器已經識別出非終結符。

當它們被識別或根本不被識別時,必須執行減少。 如果后面的記號——“前瞻記號”——不能跟隨要約簡的非終結符,則可能不會執行約簡。 一般來說,解析器需要考慮以下問題,這些問題可以通過查閱 state 轉換表立即得到解答(這些問題緊跟在產生式之后):

  1. 解析是否可以通過繼續下一個令牌而不執行縮減來進行? (這稱為“移位”動作,因為解析器在活動產生式中將一個標記向右移動。)
  2. 對於此 state 中的每個可能的縮減,通過執行縮減可以進行解析嗎?

如果對這些問題中的一個以上的答案是“是”,則會發生沖突。 這並不一定意味着語法是模棱兩可的,但它確實意味着解析器無法決定如何在兩個備選方案之間進行選擇。

像大多數解析器生成器一樣,Ply 使用一些內置規則解決了這個問題。 其中一個規則是,在沒有其他信息(優先級聲明)的情況下,如果第一個問題的答案是“是”,則解析器應該繼續進行而不執行任何歸約。

在這個 state 的特定示例中,可以進行的減少是empty: Although it's not obvious from this state (we'd have to look at the state the parser enters after doing that reduction, which is state 6), after reducing empty , the parser's next move will necessarily be to reduce statIntro -> empty , after它將 go 到 State 5,其中包括生產

Stat -> LPAREN CheckupInformation statIntro . statBody RPAREN

為了使該序列有效,解析器需要知道它將能夠進行,這意味着前瞻令牌(在這種情況下( )必須是 State 5 中的可能輸入。當然,這是因為statBody可以以左括號開頭。因此可以進行歸約。

但是statIntro也可以以(開頭,因此解析器不必進行歸約即可繼續進行。鑒於這兩種選擇,Ply 選擇采取 shift 動作,這意味着它放棄了statIntro可能為空的可能性,並且假設(屬於statIntro 。如果有statIntro ,這是正確的選擇。但是如果缺少statIntro ,則(屬於statBody ,並且應該進行縮減。

所以這就是你的語法問題。 這表明所寫的語法需要不止一個前瞻標記。 不幸的是,包括 Ply 在內的許多解析器生成器沒有一種機制來處理需要多個前瞻標記的語法。 (如果需要的前瞻量有一些限制——例如,在這種情況下,可以通過查看接下來的兩個標記來解決沖突——那么理論上可以找到相同語言的等效語法只需要一個前瞻令牌。但這必須是你的責任,因為 Ply 不會為你做這件事。)

在這種情況下,解決方案非常簡單。 只需從statIntro中刪除空產生式,而是通過為Stat提供兩個產生式使其成為可選,一個具有statIntro一個沒有:

def p_stat_1(p):
    'Stat : LPAREN CheckupInformation statIntro statBody RPAREN'
    p[0]=(p[1],p[2],p[3],p[4],p[5])

def p_stat_2(p):
    'Stat : LPAREN CheckupInformation           statBody RPAREN'
    p[0]=(p[1],p[2],None,p[3],p[4])

def p_Intro(p) :
    '''statIntro : LPAREN Introduction Name RPAREN
                 | statIntro LPAREN Introduction Name RPAREN
    '''

(我還從語法中刪除了p_empty 。)

此修改后的語法不會產生任何沖突,並將按預期解析您的測試輸入:

$ python3 learner.py learner.1
(CheckupInformation
    (Introduction John)
    (Introduction Patt)
    (Information( Anonymous1 perfect))
    (Information( Anonymous2 sick))
)
$ python3 learner.py learner.2
(CheckupInformation
    (Information( Anonymous1 perfect))
    (Information( Anonymous2 sick))
)

后記:

上面建議的轉換很簡單,並且可以在大量情況下工作,而不僅僅是可以通過前瞻兩個令牌來解決沖突的情況。 但是,正如評論中所指出的,它確實增加了語法的大小,特別是當產生式具有多個可選組件時。 例如,生產:

A : optional_B optional_C optional_D

必須將其擴展為七個不同的產品,一個用於B C D中的每個非空選擇,此外,每個使用A地方都需要復制以允許A為空的情況。

這似乎很多,但可能並非所有這些作品都是必要的。 只有當可以啟動可選組件的終端集合和可以跟隨它的符號集合之間存在重疊時,才需要進行轉換。 因此,例如,如果BCD都可以以括號開頭,但A后面不能跟括號,則optional_D不會引起沖突,只需擴展BC

A : B C optional_D
  |   C optional_D
  | B   optional_D
  |     optional_D

這需要一些語法分析來弄清楚A后面可以做什么,但在常見的語法中,手工操作並不難。

如果這仍然看起來太多作品,還有其他一些不太普遍的可能性,但無論如何可能會有所幫助。

首先,您可能會認為上述產品中出現的順序BCD並不重要。 在這種情況下,您可以更換

A : optional_B optional_C optional_D

使用不太復雜,更容易接受的替代方案:

A : 
  | A X
X : B | C | D

(或者您可以通過在A的產生中單獨寫出替代方案來避免X 。)

這允許多次使用BCD ,但這似乎與您的語法相對應,其中可選組件實際上可能是空重復。

這就留下了如何產生合理的 AST 的問題,但這很容易解決,至少在 Ply 的上下文中是這樣。 這是一種可能的實際解決方案,再次假設重復是可以接受的:

# This solution assumes that A cannot be followed by
# any token which might appear at the start of a component
def p_A(p):
    """ A : A1 """
    # Create the list of lists B, C, D, as in the original
    p[0] = [ p[1]["B"], p[1]["C"], p[1]["D"] ]

def p_A1_empty(p):
    """ A1 : """
    # Start with a dictionary of empty lists
    p[0] = { "A":[], "B":[], "C":[] }

def p_A1_B(p):
    """ A1 : A1 B """
    p[1]["B"].append(p[2])
    p[0] = p[1]

def p_A1_C(p):
    """ A1 : A1 C """
    p[1]["C"].append(p[2])
    p[0] = p[1]

def p_A1_D(p):
    """ A1 : A1 D """
    p[1]["D"].append(p[2])
    p[0] = p[1]

如果您安排BCD的語義值以包含它們是什么的指示,則可以簡化最后三個操作函數。 因此,例如,如果B返回["B", value]而不是value ,那么您可以將最后三個A1操作組合成一個 function:

def p_A1_BCD(p):
    """ A1 : A1 B
           | A1 C
           | A1 D
    """
    p[1][p[2][0]].append(p[2][1])
    p[0] = p[1]

如果這些都不令人滿意,並且所有沖突都可以通過一個額外的前瞻令牌來解決,那么您可以嘗試在詞法分析器中解決問題。

例如,您的語言似乎完全由 S 表達式組成,這些表達式以左括號開頭,后跟某種關鍵字。 因此詞法分析器可以將左括號與以下關鍵字組合成一個標記。 一旦你這樣做了,你的可選組件就不再以相同的標記開始,沖突就消失了。 (這種技術通常用於解析 XML 輸入,它們具有相同的問題:如果所有有趣的東西都以<開頭,那么沖突就會很多。但是如果您將<tag為單個標記,那么沖突就會消失。在 XML 的情況下, <和標記名之間不允許有空格;如果您的語法允許(和以下關鍵字之間有空格,您的詞法分析器模式將變得稍微復雜一些。但它仍然是可管理的。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM