วิธีการตั้งค่าไวยากรณ์ที่สามารถจัดการกับความกำกวม


9

ฉันกำลังพยายามสร้างไวยากรณ์เพื่อแยกสูตรคล้าย Excel ที่ฉันคิดขึ้นมาโดยที่อักขระพิเศษในตอนต้นของสตริงแสดงถึงแหล่งที่มาที่แตกต่างกัน ยกตัวอย่างเช่น$สามารถมีความหมายสตริงดังนั้น " $This is text" จะได้รับการปฏิบัติเป็นสัญญาณเข้าสตริงในโปรแกรมและ&สามารถมีความหมายฟังก์ชั่นเพื่อให้สามารถจะถือว่าเป็นการเรียกร้องให้ฟังก์ชั่นภายใน&foo()foo

ปัญหาที่ฉันเผชิญคือการสร้างไวยากรณ์อย่างถูกต้อง ตัวอย่างเช่นนี่เป็นเวอร์ชั่นย่อที่มีชื่อว่า MWE:

grammar = r'''start: instruction

?instruction: simple
            | func

STARTSYMBOL: "!"|"#"|"$"|"&"|"~"
SINGLESTR: (LETTER+|DIGIT+|"_"|" ")*
simple: STARTSYMBOL [SINGLESTR] (WORDSEP SINGLESTR)*
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: STARTSYMBOL SINGLESTR "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''
parser = lark.Lark(grammar, parser='earley')

ดังนั้นด้วยไวยากรณ์นี้สิ่งที่ชอบ: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)และ&foo(!w1,w2,w3,,!w4,w5,w6)มีการแยกวิเคราะห์ทั้งหมดเป็นไปตามคาด แต่ถ้าฉันต้องการเพิ่มความยืดหยุ่นให้กับsimpleเทอร์มินัลของฉันฉันต้องเริ่มเล่นซอกับSINGLESTRนิยามโทเค็นซึ่งไม่สะดวก

ฉันลองทำอะไร

ส่วนที่ฉันไม่สามารถผ่านได้คือถ้าฉันต้องการมีสตริงรวมถึงวงเล็บ (ซึ่งเป็นตัวอักษรfunc) จากนั้นฉันไม่สามารถจัดการกับพวกเขาในสถานการณ์ปัจจุบันของฉัน

  • ถ้าฉันเพิ่มวงเล็บเข้าไปSINGLESTRฉันก็จะได้รับExpected STARTSYMBOLเพราะมันปะปนกับfuncคำจำกัดความและคิดว่าอาร์กิวเมนต์ของฟังก์ชันควรถูกส่งผ่านซึ่งทำให้เข้าใจได้
  • ถ้าฉัน redefine ไวยากรณ์เพื่อรองรับสัญลักษณ์เครื่องหมายสำหรับฟังก์ชั่นเท่านั้นและเพิ่มวงเล็บในSINGLESTRแล้วผมสามารถแยกสตริงกับวงเล็บ Expected LPARแต่ฟังก์ชั่นทุกฉันพยายามที่จะแยกให้

ความตั้งใจของฉันคือสิ่งที่เริ่มต้นด้วย$จะถูกแยกเป็นโทเค็นแล้วฉันจะแยกสิ่งที่ต้องการSINGLESTR&foo($first arg (has) parentheses,,$second arg)

ตอนนี้ทางออกของฉันคือฉันใช้คำว่า 'escape' เช่น LEFTPAR และ RIGHTPAR ในสตริงของฉันและฉันได้เขียนฟังก์ชันผู้ช่วยเพื่อเปลี่ยนสิ่งเหล่านั้นเป็นวงเล็บเมื่อฉันประมวลผลต้นไม้ ดังนั้นผลิตต้นไม้ที่ถูกต้องและเมื่อผมดำเนินการได้แล้วนี้ได้รับการแปลเป็น$This is a LEFTPARtestRIGHTPARThis is a (test)

เพื่อกำหนดคำถามทั่วไป: ฉันสามารถกำหนดไวยากรณ์ของฉันในลักษณะที่อักขระบางตัวที่พิเศษสำหรับไวยากรณ์นั้นได้รับการปฏิบัติเหมือนตัวอักษรปกติในบางสถานการณ์และพิเศษในกรณีอื่น ๆ ได้หรือไม่


แก้ไข 1

จากความคิดเห็นจากjbndlrฉันได้แก้ไขไวยากรณ์ของฉันเพื่อสร้างแต่ละโหมดตามสัญลักษณ์เริ่มต้น:

grammar = r'''start: instruction

?instruction: simple
            | func

SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|")")*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

เรื่องนี้ตก (ค่อนข้าง) ภายใต้กรณีทดสอบที่สองของฉัน ฉันสามารถแยกsimpleประเภทของสตริงทั้งหมด (โทเค็น TEXT, MD หรือ DB ที่สามารถมีวงเล็บ) และฟังก์ชั่นที่ว่างเปล่า; ตัวอย่างเช่น&foo()หรือ&foo(&bar())แยกอย่างถูกต้อง ขณะที่ฉันใส่อาร์กิวเมนต์ภายในฟังก์ชั่น (ไม่ว่าประเภท) UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEPผมได้รับ เพื่อเป็นการพิสูจน์แนวคิดถ้าฉันลบวงเล็บออกจากคำจำกัดความของ SINGLESTR ในไวยากรณ์ใหม่ข้างต้นทุกอย่างก็ทำงานได้ตามที่ควรจะเป็น แต่ฉันกลับไปที่จตุรัสคนหนึ่ง


คุณมีตัวละครที่ระบุสิ่งที่เกิดขึ้นหลังจากพวกเขา (ของคุณSTARTSYMBOL) และคุณเพิ่มตัวคั่นและวงเล็บที่จำเป็นต้องมีการล้าง; ฉันไม่เห็นความกำกวมใด ๆ ที่นี่ คุณยังคงต้องแยกSTARTSYMBOLรายการของคุณออกเป็นแต่ละรายการเพื่อให้สามารถแยกแยะได้
jbndlr

ฉันจะโพสต์คำตอบจริงเร็ว ๆ นี้ทำงานกับมันมาหลายวันแล้ว
คุ้นเคย

ฉันให้คำตอบ แม้ว่าจะใช้เวลาเพียง 2 ชั่วโมงจนกว่าเงินรางวัลจะหมดอายุ แต่คุณยังสามารถให้รางวัลด้วยตนเองได้ในช่วงเวลาผ่อนผันต่อไปนี้ 24 ชั่วโมง หากคำตอบของฉันไม่ดีโปรดบอกฉันเร็ว ๆ นี้และฉันจะแก้ไข
คุ้นเคย

คำตอบ:


3
import lark
grammar = r'''start: instruction

?instruction: simple
            | func

MIDTEXTRPAR: /\)+(?!(\)|,,|$))/
SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|MIDTEXTRPAR)*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

parser = lark.Lark(grammar, parser='earley')
parser.parse("&foo($first arg (has) parentheses,,$second arg)")

เอาท์พุท:

Tree(start, [Tree(func, [Token(FUNCNAME, 'foo'), Tree(simple, [Token(TEXT, '$first arg (has) parentheses')]), Token(ARGSEP, ',,'), Tree(simple, [Token(TEXT, '$second arg')])])])

ฉันหวังว่ามันคือสิ่งที่คุณกำลังมองหา

พวกนั้นบ้าไปแล้วสองสามวัน ฉันเล่นสนุกสนานและล้มเหลว ฉันยังพยายามและpersimonious pyparsingตัวแยกวิเคราะห์ที่แตกต่างกันเหล่านี้ทั้งหมดมีปัญหาเดียวกันกับโทเค็น 'อาร์กิวเมนต์' ที่ใช้วงเล็บขวาที่เป็นส่วนหนึ่งของฟังก์ชันในที่สุดก็ล้มเหลวเนื่องจากวงเล็บของฟังก์ชันไม่ได้ถูกปิด

เคล็ดลับคือการคิดออกว่าคุณจะกำหนดวงเล็บที่ถูกต้องที่ "ไม่พิเศษ" ได้อย่างไร ดูการแสดงออกปกติสำหรับMIDTEXTRPARในรหัสข้างต้น ฉันกำหนดว่าเป็นวงเล็บขวาที่ไม่ได้ตามด้วยการแยกอาร์กิวเมนต์หรือตามท้ายสตริง ฉันทำอย่างนั้นโดยใช้ส่วนขยายนิพจน์ทั่วไป(?!...)ซึ่งจับคู่เฉพาะถ้ามันไม่ได้ตามด้วย...แต่ไม่ใช้อักขระ โชคดีที่มันช่วยให้จับคู่ส่วนท้ายของสตริงภายในส่วนขยายนิพจน์ทั่วไปพิเศษ

แก้ไข:

วิธีการดังกล่าวข้างต้นใช้งานได้เฉพาะในกรณีที่คุณไม่มีอาร์กิวเมนต์ที่ลงท้ายด้วย a) เพราะจากนั้นนิพจน์ทั่วไป MIDTEXTRPAR จะไม่ตรวจจับนั้น) และจะคิดว่านั่นเป็นจุดสิ้นสุดของฟังก์ชันแม้ว่าจะมีอาร์กิวเมนต์มากกว่าในการประมวลผล นอกจากนี้อาจมีความกำกวมเช่น ... asdf) ,, ... มันอาจเป็นการสิ้นสุดของการประกาศฟังก์ชั่นภายในการโต้แย้งหรือ 'ข้อความเหมือน') ในการโต้แย้งและการประกาศฟังก์ชั่นไป

ปัญหานี้เกี่ยวข้องกับความจริงที่ว่าสิ่งที่คุณอธิบายในคำถามของคุณไม่ใช่ไวยากรณ์แบบไม่มีบริบท ( https://en.wikipedia.org/wiki/Context-free_grammar ) ซึ่งมีตัวแยกวิเคราะห์เช่นสนุกสนาน แต่เป็นไวยากรณ์ไวตามบริบท ( https://en.wikipedia.org/wiki/Context-sensitive_grammar )

เหตุผลในการเป็นไวยากรณ์ไวต่อบริบทคือเนื่องจากคุณต้องการให้โปรแกรมแยกวิเคราะห์ 'จำ' ว่ามันซ้อนอยู่ภายในฟังก์ชันและมีการซ้อนกันหลายระดับและมีหน่วยความจำนี้อยู่ในไวยากรณ์ของไวยากรณ์ในบางวิธี

EDIT2:

ดูที่ parser ต่อไปนี้ซึ่งคำนึงถึงบริบทและดูเหมือนว่าจะแก้ปัญหาได้ แต่มีความซับซ้อนของเวลาแบบเอกซ์โปเนนเชียลในจำนวนหน้าที่ซ้อนกันในขณะที่มันพยายามแยกวิเคราะห์อุปสรรคของฟังก์ชันที่เป็นไปได้ทั้งหมดจนกระทั่งพบ ฉันเชื่อว่ามันต้องมีความซับซ้อนแบบเอกซ์โปเนนเชียลเนื่องจากมันไม่ได้ไร้บริบท


_funcPrefix = '&'
_debug = False

class ParseException(Exception):
    pass

def GetRecursive(c):
    if isinstance(c,ParserBase):
        return c.GetRecursive()
    else:
        return c

class ParserBase:
    def __str__(self):
        return type(self).__name__ + ": [" + ','.join(str(x) for x in self.contents) +"]"
    def GetRecursive(self):
        return (type(self).__name__,[GetRecursive(c) for c in self.contents])

class Simple(ParserBase):
    def __init__(self,s):
        self.contents = [s]

class MD(Simple):
    pass

class DB(ParserBase):
    def __init__(self,s):
        self.contents = s.split(',')

class Func(ParserBase):
    def __init__(self,s):
        if s[-1] != ')':
            raise ParseException("Can't find right parenthesis: '%s'" % s)
        lparInd = s.find('(')
        if lparInd < 0:
            raise ParseException("Can't find left parenthesis: '%s'" % s)
        self.contents = [s[:lparInd]]
        argsStr = s[(lparInd+1):-1]
        args = list(argsStr.split(',,'))
        i = 0
        while i<len(args):
            a = args[i]
            if a[0] != _funcPrefix:
                self.contents.append(Parse(a))
                i += 1
            else:
                j = i+1
                while j<=len(args):
                    nestedFunc = ',,'.join(args[i:j])
                    if _debug:
                        print(nestedFunc)
                    try:
                        self.contents.append(Parse(nestedFunc))
                        break
                    except ParseException as PE:
                        if _debug:
                            print(PE)
                        j += 1
                if j>len(args):
                    raise ParseException("Can't parse nested function: '%s'" % (',,'.join(args[i:])))
                i = j

def Parse(arg):
    if arg[0] not in _starterSymbols:
        raise ParseException("Bad prefix: " + arg[0])
    return _starterSymbols[arg[0]](arg[1:])

_starterSymbols = {_funcPrefix:Func,'$':Simple,'!':DB,'#':MD}

P = Parse("&foo($first arg (has)) parentheses,,&f($asdf,,&nested2($23423))),,&second(!arg,wer))")
print(P)

import pprint
pprint.pprint(P.GetRecursive())

1
ขอบคุณมันใช้งานได้ตามที่ตั้งใจไว้! มอบรางวัลให้แก่คุณเพราะคุณไม่จำเป็นต้องหลบเลี่ยงวงเล็บในทางใดทางหนึ่ง คุณผ่านไปแล้วและมันก็แสดงให้เห็น! ยังมีกรณีขอบของอาร์กิวเมนต์ 'ข้อความ' ที่ลงท้ายด้วยวงเล็บ แต่ฉันจะต้องอยู่กับมัน คุณอธิบายความคลุมเครืออย่างชัดเจนและฉันจะต้องทดสอบอีกเล็กน้อย แต่ฉันคิดว่าสำหรับวัตถุประสงค์ของฉันสิ่งนี้จะใช้ได้ดีมาก ขอขอบคุณที่ให้ข้อมูลเพิ่มเติมเกี่ยวกับไวยากรณ์ตามบริบท ฉันซาบซึ้งจริงๆ!
Dima1982

@ Dima1982 ขอบคุณมาก!
คุ้นเคย

@ Dima1982 ลองดูที่การแก้ไขฉันทำ parser ที่อาจจะสามารถแก้ปัญหาของคุณในราคาที่ซับซ้อนเวลาชี้แจง นอกจากนี้ฉันคิดเกี่ยวกับมันและถ้าปัญหาของคุณมีค่าจริงการหนีวงเล็บอาจเป็นทางออกที่ง่ายที่สุด หรือทำให้วงเล็บฟังก์ชั่นเป็นอย่างอื่นเช่นคั่นจุดสิ้นสุดของรายการอาร์กิวเมนต์ฟังก์ชันด้วย&ตัวอย่างเช่น
iliar

1

ปัญหาคือข้อโต้แย้งของฟังก์ชั่นจะอยู่ในวงเล็บโดยที่หนึ่งในอาร์กิวเมนต์อาจมีวงเล็บ
หนึ่งในวิธีแก้ปัญหาที่เป็นไปได้คือใช้ backspace \ before (หรือ) เมื่อเป็นส่วนหนึ่งของ String

  SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"\("|"\)")*

โซลูชันที่คล้ายกันซึ่งใช้โดย C เพื่อรวมเครื่องหมายคำพูดคู่ (") เป็นส่วนหนึ่งของค่าคงที่สตริงโดยที่ค่าคงที่สตริงถูกใส่ในเครื่องหมายคำพูดคู่

  example_string1='&f(!g\()'
  example_string2='&f(#g)'
  print(parser.parse(example_string1).pretty())
  print(parser.parse(example_string2).pretty())

ผลผลิตคือ

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

ฉันคิดว่ามันเหมือนกับวิธีของ OP ในการแทนที่ "(" และ ")" ด้วย LEFTPAR และ RIGHTPAR
คุ้นเคย
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.