Cara mengatur tata bahasa yang dapat menangani ambiguitas

9

Saya mencoba membuat tata bahasa untuk mem-parsing beberapa rumus mirip-Excel yang telah saya buat, di mana karakter khusus di awal string menandakan sumber yang berbeda. Sebagai contoh, $dapat menandakan sebuah string, sehingga " $This is text" akan diperlakukan sebagai input string dalam program dan &dapat menandakan suatu fungsi, sehingga &foo()dapat diperlakukan sebagai panggilan ke fungsi internal foo.

Masalah yang saya hadapi adalah bagaimana membangun tata bahasa dengan benar. Misalnya, ini adalah versi yang disederhanakan sebagai 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')

Jadi, dengan tata bahasa ini, hal-hal seperti: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)dan &foo(!w1,w2,w3,,!w4,w5,w6)semua diurai seperti yang diharapkan. Tetapi jika saya ingin menambahkan lebih banyak fleksibilitas ke simpleterminal saya , maka saya perlu mulai mengutak-atik SINGLESTRdefinisi token yang tidak nyaman.

Apa yang sudah saya coba

Bagian yang tidak bisa saya lewati adalah jika saya ingin memiliki string termasuk tanda kurung (yang merupakan literal dari func), maka saya tidak dapat menangani mereka dalam situasi saya saat ini.

  • Jika saya menambahkan tanda kurung di SINGLESTR, maka saya mendapatkan Expected STARTSYMBOL, karena semakin dicampur dengan funcdefinisi dan berpikir bahwa argumen fungsi harus dilewati, yang masuk akal.
  • Jika saya mendefinisikan kembali tata bahasa untuk cadangan simbol ampersand hanya untuk fungsi dan menambahkan tanda kurung SINGLESTR, maka saya dapat mengurai string dengan tanda kurung, tetapi setiap fungsi yang saya coba parsing memberi Expected LPAR.

Maksud saya adalah bahwa apa pun yang dimulai dengan a $akan diurai sebagai SINGLESTRtoken dan kemudian saya bisa menguraikan hal-hal seperti &foo($first arg (has) parentheses,,$second arg).

Solusi saya, untuk saat ini, adalah saya menggunakan kata 'escape' seperti LEFTPAR dan RIGHTPAR di string saya dan saya telah menulis fungsi pembantu untuk mengubahnya menjadi tanda kurung ketika saya memproses pohon. Jadi, $This is a LEFTPARtestRIGHTPARhasilkan pohon yang benar dan ketika saya memprosesnya, maka ini diterjemahkan This is a (test).

Untuk merumuskan pertanyaan umum: Dapatkah saya mendefinisikan tata bahasa saya sedemikian rupa sehingga beberapa karakter yang khusus untuk tata bahasa diperlakukan sebagai karakter normal dalam beberapa situasi dan sebagai istimewa dalam kasus lain?


EDIT 1

Berdasarkan komentar dari jbndlrsaya merevisi tata bahasa saya untuk membuat mode individual berdasarkan simbol awal:

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
'''

Ini jatuh (agak) di bawah test case kedua saya. Saya bisa menguraikan semua simplejenis string (TEXT, MD atau token DB yang dapat berisi tanda kurung) dan fungsi yang kosong; misalnya, &foo()atau &foo(&bar())parse dengan benar. Saat saya menempatkan argumen dalam suatu fungsi (tidak peduli jenis apa), saya mendapatkan UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. Sebagai bukti konsep, jika saya menghapus tanda kurung dari definisi SINGLESTR di tata bahasa baru di atas, maka semuanya berfungsi sebagaimana mestinya, tapi saya kembali ke titik awal.

Dima1982
sumber
Anda memang memiliki karakter yang mengidentifikasi apa yang datang setelah mereka (Anda STARTSYMBOL) dan Anda menambahkan pemisah dan tanda kurung di mana harus jelas; Saya tidak melihat ambiguitas di sini. Anda masih harus membagi STARTSYMBOLdaftar menjadi beberapa item agar dapat dibedakan.
jbndlr
Saya akan memposting jawaban segera, telah mengerjakannya selama beberapa hari sekarang.
iliar
Saya memberikan jawaban. Meskipun hanya 2 jam sampai hadiahnya berakhir, Anda masih dapat secara manual memberikan hadiah dalam masa tenggang 24 jam berikutnya. Jika jawaban saya tidak bagus, tolong beri tahu saya segera dan saya akan memperbaikinya.
iliar

Jawaban:

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)")

Keluaran:

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

Saya harap itu yang Anda cari.

Itu gila beberapa hari. Saya mencoba burung dan gagal. Saya juga mencoba persimoniousdan pyparsing. Semua parser yang berbeda ini semua memiliki masalah yang sama dengan token 'argumen' yang menggunakan tanda kurung yang tepat yang merupakan bagian dari fungsi, akhirnya gagal karena tanda kurung fungsi tidak ditutup.

Caranya adalah dengan mencari tahu bagaimana Anda mendefinisikan tanda kurung yang benar yang "tidak istimewa". Lihat persamaan reguler MIDTEXTRPARdalam kode di atas. Saya mendefinisikannya sebagai tanda kurung benar yang tidak diikuti oleh pemisahan argumen atau pada akhir string. Saya melakukannya dengan menggunakan ekstensi ekspresi reguler (?!...)yang hanya cocok jika tidak diikuti ...tetapi tidak menggunakan karakter. Untungnya itu bahkan memungkinkan pencocokan ujung string di dalam ekstensi ekspresi reguler khusus ini.

EDIT:

Metode yang disebutkan di atas hanya berfungsi jika Anda tidak memiliki argumen yang diakhiri dengan a), karena kemudian ekspresi reguler MIDTEXTRPAR tidak akan menangkap itu) dan akan berpikir bahwa itu adalah akhir dari fungsi walaupun ada lebih banyak argumen untuk diproses. Juga, mungkin ada ambiguitas seperti ... asdf) ,, ..., itu mungkin merupakan akhir dari deklarasi fungsi di dalam argumen, atau 'teks-seperti') di dalam argumen dan deklarasi fungsi berjalan.

Masalah ini terkait dengan fakta bahwa apa yang Anda uraikan dalam pertanyaan Anda bukan tata bahasa bebas konteks ( https://en.wikipedia.org/wiki/Context-free_grammar ) yang parser seperti lark ada. Sebaliknya itu adalah tata bahasa yang sensitif terhadap konteks ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Alasan untuk itu menjadi tata bahasa yang sensitif konteks adalah karena Anda perlu parser untuk 'mengingat' bahwa itu bersarang di dalam suatu fungsi, dan berapa banyak tingkat sarang yang ada, dan memiliki memori ini tersedia dalam sintaks tata bahasa dengan cara tertentu.

EDIT2:

Lihat juga parser berikut yang peka konteks, dan tampaknya menyelesaikan masalah, tetapi memiliki kompleksitas waktu yang eksponensial dalam jumlah fungsi bersarang, karena mencoba mengurai semua hambatan fungsi yang mungkin sampai menemukan yang berfungsi. Saya percaya itu harus memiliki kompleksitas eksponensial karena tidak bebas konteks.


_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())
iliar
sumber
1
Terima kasih, ini berfungsi sebagaimana mestinya! Diberikan hadiah karena Anda tidak perlu melepaskan kurung dengan cara apa pun. Anda bekerja ekstra dan itu menunjukkan! Masih ada kasus tepi dari argumen 'teks' yang diakhiri dengan tanda kurung, tetapi saya hanya harus hidup dengan argumen itu. Anda juga menjelaskan ambiguitas dengan cara yang jelas dan saya hanya perlu mengujinya sedikit lagi, tetapi saya pikir untuk tujuan saya ini akan bekerja dengan sangat baik. Terima kasih telah memberikan info lebih lanjut tentang tata bahasa yang sensitif terhadap konteks. Saya sangat menghargai itu!
Dima1982
@ Dima1982 Terima kasih banyak!
iliar
@ Dima1982 Lihatlah edit, saya membuat parser yang mungkin dapat menyelesaikan masalah Anda dengan biaya kompleksitas waktu yang eksponensial. Juga, saya memikirkannya dan jika masalah Anda memiliki nilai praktis, menghindari tanda kurung mungkin merupakan solusi paling sederhana. Atau Membuat fungsi kurung sesuatu yang lain, seperti membatasi akhir daftar argumen fungsi dengan &misalnya.
iliar
1

Masalahnya adalah argumen fungsi dilampirkan dalam tanda kurung di mana salah satu argumen dapat berisi tanda kurung.
Salah satu solusi yang mungkin adalah menggunakan backspace \ before (atau) ketika itu adalah bagian dari String

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

Solusi serupa digunakan oleh C, untuk memasukkan tanda kutip ganda (") sebagai bagian dari konstanta string di mana konstanta string terlampir dalam tanda kutip ganda.

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

Output adalah

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g
Venkatesh Nandigama
sumber
Saya pikir itu hampir sama dengan solusi OP sendiri untuk mengganti "(" dan ")" dengan LEFTPAR dan RIGHTPAR.
iliar