Belirsizlikle başa çıkabilen bir gramer nasıl kurulur


9

Bir dize başlangıcında özel bir karakter farklı bir kaynak anlamına gelir, tasarladığım bazı Excel benzeri formülleri ayrıştırmak için bir dilbilgisi oluşturmaya çalışıyorum. Örneğin, $bir dizeyi ifade edebilir, böylece " $This is text" programda dize girişi olarak kabul edilir ve& bir işlevi ifade edebilir, bu nedenle &foo()dahili işleve çağrı olarak değerlendirilebilir foo.

Karşılaştığım sorun dilbilgisinin nasıl düzgün bir şekilde oluşturulacağı. Örneğin, Bu bir MWE olarak basitleştirilmiş bir sürümdür:

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

Yani, bu dilbilgisi ile, gibi şeyler: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)ve &foo(!w1,w2,w3,,!w4,w5,w6)beklendiği gibi tüm ayrıştırıldı. Ama simpleterminalime daha fazla esneklik eklemek istersem SINGLESTR, uygun olmayan jeton tanımıyla uğraşmaya başlamam gerekiyor .

Ne denedim

Geçmişi alamıyorum kısmı (parantezler dahil) parantez içeren bir dize sahip olmak istiyorsanız func, o zaman geçerli durumumda bunları işleyemezsiniz.

  • Parantezleri eklersem SINGLESTR, o zaman alıyorum Expected STARTSYMBOL, çünkü functanımla karışıyor ve bir işlev argümanının geçirilmesi gerektiğini düşünüyor, bu da mantıklı.
  • Ve işareti sadece işlevler için ayırmak ve parantez eklemek için dilbilgisi yeniden tanımlamak SINGLESTR, o zaman parantez ile bir dize ayrıştırabilir, ancak ayrıştırmaya çalıştığım her işlev verir Expected LPAR.

Amacım bir ile başlayan her şeyin $bir SINGLESTRjeton olarak ayrıştırılması ve sonra gibi şeyler ayrıştırmak olabilir &foo($first arg (has) parentheses,,$second arg).

Benim çözümüm, şimdilik, dizelerimde LEFTPAR ve RIGHTPAR gibi 'escape' kelimeleri kullanıyorum ve ağacı işlediğimde bunları parantez haline getirmek için yardımcı işlevler yazdım. Yani,$This is a LEFTPARtestRIGHTPAR doğru ağacı üretir ve işlediğimde, bu tercüme edilir This is a (test).

Genel bir soru formüle etmek için: Dilbilgimi, dilbilgisine özel bazı karakterlerin bazı durumlarda normal karakterler ve diğer durumlarda özel olarak ele alınacak şekilde tanımlayabilir miyim?


DÜZENLEME 1

jbndlrBaşlangıç yorumuna dayanarak tek tek modlar oluşturmak için dilbilgimi gözden geçirdim:

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

Bu (biraz) ikinci test durumumun altına düşüyor. Tüm simpledizeleri (metin, parantez içeren MD veya DB belirteçleri) ve boş işlevleri ayrıştırabilir ; örneğin &foo()veya &foo(&bar())doğru ayrıştırın. Bir fonksiyonun içine bir argüman koyduğum an (hangi tür olursa olsun), bir UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. Bir kavram kanıtı olarak, yukarıdaki yeni dilbilgisinde parantezleri SINGLESTR tanımından kaldırırsam, her şey olması gerektiği gibi çalışır, ancak kareye geri dönüyorum.


Onlardan sonra gelenleri (sizin STARTSYMBOL) tanımlayan karakterleriniz var ve açık olması gerektiğinde ayırıcılar ve parantezler ekliyorsunuz; Burada herhangi bir belirsizlik görmüyorum. Yine STARTSYMBOLde ayırt edilebilir olmak için listenizi tek tek öğelere ayırmanız gerekir.
jbndlr

Yakında gerçek bir cevap göndereceğim, birkaç gündür üzerinde çalışıyorum.
iliar

Bir cevap verdim. Ödülün geçerliliğinin sona ermesi sadece 2 saat olsa da, ödülü 24 saatlik bir sonraki yetkisiz kullanım süresinde manuel olarak verebilirsiniz. Cevabım iyi değilse, lütfen yakında söyleyin, ben de düzeltirim.
iliar

Yanıtlar:


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

Çıktı:

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

Umarım aradığın şey budur.

Bunlar çılgınca günler oldu. Toygar denedim ve başarısız oldu. Ben de denedim persimoniousvepyparsing . Bu farklı ayrıştırıcıların tümü, işlevin bir parçası olan doğru parantez tüketen 'argüman' belirteciyle aynı soruna sahipti, çünkü işlevin parantezleri kapanmadığı için başarısız oldu.

İşin püf noktası, "özel olmayan" bir doğru parantezin nasıl tanımlandığını bulmaktı. MIDTEXTRPARYukarıdaki kodda için normal ifadeye bakın . Bunu bağımsız değişken ayırma veya dizenin sonunda gelmeyen bir sağ parantez olarak tanımladım. Bunu (?!...)sadece takip ...etmeyen ancak karakter tüketmeyen normal ifade uzantısını kullanarak yaptım . Neyse ki, bu özel düzenli ifade uzantısı içinde dizenin eşleşen sonuna bile izin verir.

DÜZENLE:

Yukarıda belirtilen yöntem yalnızca a) ile biten bir argümanınız yoksa çalışır, çünkü MIDTEXTRPAR normal ifadesi bunu yakalamaz) ve işlenecek daha fazla argüman olmasına rağmen işlevin sonu olduğunu düşünecektir. Ayrıca, ... asdf) ,, ... gibi belirsizlikler olabilir, bu bir bağımsız değişken içindeki bir işlev bildiriminin sonu olabilir veya bir bağımsız değişken içindeki bir 'metin benzeri') olabilir ve işlev bildirimi devam eder.

Bu sorun, sorunuzda tarif ettiğiniz şeyin , toygar gibi ayrıştırıcıların bulunduğu bağlamsız bir dilbilgisi ( https://en.wikipedia.org/wiki/Context-free_grammar ) olmamasıyla ilgilidir . Bunun yerine bağlama duyarlı bir dilbilgisidir ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Bağlama duyarlı bir dilbilgisi olmasının nedeni, ayrıştırıcının bir işlevin içinde iç içe yerleştirildiğini ve kaç iç içe yerleştirme düzeyinin olduğunu ve bu belleğin dilbilgisinin sözdizimi içinde bir şekilde kullanılabilir olmasını 'hatırlaması' gerektiğidir.

EDIT2:

Ayrıca, içeriğe duyarlı ve sorunu çözüyor gibi görünen, ancak çalışan bir işlev bulana kadar tüm olası işlev engellerini ayrıştırmaya çalıştığından, iç içe işlevlerin sayısında üstel bir zaman karmaşıklığına sahip olan aşağıdaki ayrıştırıcıya bir göz atın. Bağlamdan bağımsız olmadığı için üstel bir karmaşıklığa sahip olması gerektiğine inanıyorum.


_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
Teşekkür ederim, bu amaçlandığı gibi çalışıyor! Parantezlerden herhangi bir şekilde kaçmanıza gerek olmadığı için ödül kazanın. Fazladan gittiniz ve gösteriyor! Hala parantez ile biten bir 'metin' argümanının uç vakası var, ama sadece bununla yaşamak zorundayım. Ayrıca belirsizlikleri açık bir şekilde açıkladınız ve bunu biraz daha test etmem gerekecek, ancak bence amacım için bu çok işe yarayacak. Bağlama duyarlı dilbilgisi hakkında daha fazla bilgi verdiğiniz için teşekkür ederiz. Gerçekten onu takdir ederim!
Dima1982

@ Dima1982 Çok teşekkür ederim!
iliar

@ Dima1982 Düzenlemeye bir göz atın, muhtemelen bir üstel zaman karmaşıklığı pahasına sorununuzu çözebilecek bir ayrıştırıcı yaptım. Ayrıca, bunu düşündüm ve sorununuz pratik bir değere sahipse, parantezlerden kaçmak en basit çözüm olabilir. Veya işlev parantezini başka bir şey yapmak, örneğin bir işlev bağımsız değişken listesinin sonunu sınırlamak gibi &.
iliar

1

Sorun, işlev argümanlarının parantez içine alınmasıdır, burada argümanlardan biri parantez içerebilir.
Olası çözümlerden biri, String'in bir parçası olduğunda (veya) önce backspace \ 'i kullanmaktır.

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

C tarafından, dize sabitinin çift tırnak içine alındığı dize sabitinin bir parçası olarak çift tırnak (") eklemek için kullanılan benzer çözüm.

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

Çıktı

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

Bence OP'nin "(" ve ")" yerine LEFTPAR ve RIGHTPAR'ın yerini alan kendi çözümü ile hemen hemen aynı.
iliar
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.