Come impostare una grammatica in grado di gestire l'ambiguità


9

Sto cercando di creare una grammatica per analizzare alcune formule simili a Excel che ho ideato, in cui un carattere speciale all'inizio di una stringa indica una fonte diversa. Ad esempio, $può indicare una stringa, pertanto " $This is text" verrebbe trattato come un input di stringa nel programma e &può indicare una funzione, quindi &foo()può essere trattato come una chiamata alla funzione interna foo.

Il problema che sto affrontando è come costruire correttamente la grammatica. Ad esempio, questa è una versione semplificata come 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')

Così, con questa grammatica, cose come: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)e &foo(!w1,w2,w3,,!w4,w5,w6)sono tutti analizzati come previsto. Ma se vorrei aggiungere maggiore flessibilità al mio simpleterminale, allora devo iniziare a giocherellare con la SINGLESTRdefinizione di token che non è conveniente.

Cosa ho provato

La parte che non posso superare è che se voglio avere una stringa tra parentesi (che sono letterali func), non posso gestirle nella mia situazione attuale.

  • Se aggiungo le parentesi SINGLESTR, ottengo Expected STARTSYMBOL, perché si confonde con la funcdefinizione e pensa che dovrebbe essere passato un argomento di funzione, il che ha senso.
  • Se ridefinisco la grammatica per riservare il simbolo e commerciale solo per le funzioni e aggiungo le parentesi SINGLESTR, allora posso analizzare una stringa con parentesi, ma ogni funzione che sto cercando di analizzare dà Expected LPAR.

Il mio intento è che qualsiasi cosa che inizia con a $verrebbe analizzata come SINGLESTRtoken e quindi potrei analizzare cose del genere &foo($first arg (has) parentheses,,$second arg).

La mia soluzione, per ora, è che sto usando parole "escape" come LEFTPAR e RIGHTPAR nelle mie stringhe e ho scritto funzioni di aiuto per cambiarle tra parentesi quando elaboro l'albero. Quindi, $This is a LEFTPARtestRIGHTPARproduce l'albero corretto e quando lo elaboro, questo viene tradotto in This is a (test).

Per formulare una domanda generale: posso definire la mia grammatica in modo tale che alcuni caratteri speciali della grammatica vengano trattati come caratteri normali in alcune situazioni e speciali in ogni altro caso?


MODIFICA 1

Sulla base di un commento di jbndlrho rivisto la mia grammatica per creare singole modalità basate sul simbolo iniziale:

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

Questo rientra (in qualche modo) nel mio secondo caso di test. Posso analizzare tutti i simpletipi di stringhe (token TEXT, MD o DB che possono contenere parentesi) e funzioni vuote; per esempio, &foo()o &foo(&bar())analizzare correttamente. Nel momento in cui inserisco un argomento in una funzione (non importa quale tipo), ottengo un UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. Come prova del concetto, se rimuovo le parentesi dalla definizione di SINGLESTR nella nuova grammatica sopra, allora tutto funziona come dovrebbe, ma sono tornato al punto di partenza.


Hai personaggi che identificano ciò che li segue (i tuoi STARTSYMBOL) e aggiungi separatori e parentesi dove è necessario essere chiari; Non vedo alcuna ambiguità qui. Dovresti comunque dividere la tua STARTSYMBOLlista in singoli elementi per essere distinguibili.
jbndlr,

Pubblicherò presto una risposta, ci sto lavorando da diversi giorni.
iliar

Ho fornito una risposta. Anche se mancano solo 2 ore alla scadenza della taglia, puoi comunque assegnarla manualmente nel seguente periodo di grazia di 24 ore. Se la mia risposta non è buona, per favore dimmelo presto e la riparerò.
iliar

Risposte:


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

Produzione:

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

Spero sia quello che stavi cercando.

Sono stati pazzi da pochi giorni. Ho provato allodola e ho fallito. Ho anche provato persimoniouse pyparsing. Tutti questi diversi parser avevano tutti lo stesso problema con il token 'argomento' che consumava la giusta parentesi che faceva parte della funzione, alla fine fallendo perché le parentesi della funzione non erano chiuse.

Il trucco era capire come definire una parentesi giusta "non speciale". Vedi l'espressione regolare per MIDTEXTRPARnel codice sopra. L'ho definita come una parentesi corretta che non è seguita dalla separazione degli argomenti o dalla fine della stringa. L'ho fatto usando l'estensione delle espressioni regolari (?!...)che corrisponde solo se non è seguita da, ...ma non consuma caratteri. Fortunatamente, consente persino la corrispondenza della fine della stringa all'interno di questa speciale estensione di espressioni regolari.

MODIFICARE:

Il metodo sopra menzionato funziona solo se non si dispone di un argomento che termina con a), perché l'espressione regolare MIDTEXTRPAR non lo capirà) e penserà che sia la fine della funzione anche se ci sono più argomenti da elaborare. Inoltre, potrebbero esserci delle ambiguità come ... asdf) ,, ..., potrebbe essere la fine di una dichiarazione di funzione all'interno di un argomento o un "testo simile" all'interno di un argomento e la dichiarazione di funzione continua.

Questo problema è legato al fatto che ciò che descrivi nella tua domanda non è una grammatica senza contesto ( https://en.wikipedia.org/wiki/Context-free_grammar ) per la quale esistono parser come l'allodola. Invece è una grammatica sensibile al contesto ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Il motivo per cui è una grammatica sensibile al contesto è perché è necessario che il parser 'ricordi' che è nidificato all'interno di una funzione e quanti livelli di annidamento ci sono, e che questa memoria sia disponibile all'interno della sintassi della grammatica in qualche modo.

EDIT2:

Dai anche un'occhiata al seguente parser che è sensibile al contesto e sembra risolvere il problema, ma ha una complessità temporale esponenziale nel numero di funzioni nidificate, poiché tenta di analizzare tutte le possibili barriere delle funzioni fino a quando non trova quella che funziona. Credo che debba avere una complessità esponenziale poiché non è privo di contesto.


_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
Grazie, questo funziona come previsto! Premiato con la generosità in quanto non è necessario sfuggire alle parentesi in alcun modo. Hai fatto il possibile e lo dimostra! C'è ancora il caso limite di un argomento "testuale" che termina con una parentesi, ma dovrò convivere con quello. Hai anche spiegato le ambiguità in modo chiaro e dovrò solo provarlo un po 'di più, ma penso che per i miei scopi questo funzionerà molto bene. Grazie per aver fornito anche maggiori informazioni sulla grammatica sensibile al contesto. Lo apprezzo molto!
Dima1982,

@ Dima1982 Grazie mille!
iliar

@ Dima1982 Dai un'occhiata alla modifica, ho creato un parser che può forse risolvere il tuo problema a costo di una complessità temporale esponenziale. Inoltre, ci ho pensato e se il tuo problema ha un valore pratico, fuggire tra parentesi potrebbe essere la soluzione più semplice. O Rendere la funzione tra parentesi qualcos'altro, come ad esempio la delimitazione della fine di un elenco di argomenti di funzioni &.
iliar

1

Il problema è che gli argomenti della funzione sono racchiusi tra parentesi dove uno degli argomenti può contenere parentesi.
Una delle possibili soluzioni è utilizzare backspace \ before (o) quando fa parte di String

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

Soluzione simile utilizzata da C per includere virgolette doppie (") come parte della costante di stringa in cui la costante di stringa è racchiusa tra virgolette doppie.

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

L'output è

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

Penso che sia praticamente la stessa soluzione OP di sostituire "(" e ")" con LEFTPAR e RIGHTPAR.
iliar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.