Il modo giusto per leggere dall'elenco dei token per implementare l'albero di analisi

4

Sto provando ad affrontare il While problema di lingua su HackerRank usando Scala. Mi è stato assegnato un set di regole grammaticali e devo implementare l'interprete vero e proprio sottostante.

Ho deciso di avvicinarmi a questo costruendo un lexer e un parser in due fasi separate. Il mio lexer funziona perfettamente: codifica correttamente i token e li racchiude in un List di Tokens dove Tokens è definito come segue:

case class Token(typeof: String, value: Any);

In questo momento, il mio problema è puramente concettuale: come dovrei scorrere l'elenco dei token per implementare un albero di analisi?

Nota che non ho ancora implementazioni per i nodi reali del mio albero di analisi, poiché voglio risolvere questo problema prima (anche se sono felice di sentire suggerimenti per lo stesso se è indispensabile per risolvere questo problema) .

Ecco cosa ho fatto finora:

  • Ho dato un'occhiata a l'algoritmo dello shunting-yard , ma non lo sono certo come generalizzarlo per qualcosa al di là delle operazioni aritmetiche. Ho pensato di avere stack multipli progettati per diversi tipi di nodi, ma non sono convinto che sia il modo giusto per farlo - questo sarebbe facilmente impossibile da mantenere.

  • Ho considerato un ingenuo attraversamento lineare in cui creo una lista vuota, inserisco i miei token dal mio elenco di token uno alla volta e analizzo ogni volta lo stack cercando di far corrispondere una regola di grammatica ad esso (se corrisponde, rimuovo i nodi dalla lista altrimenti lo lascio essere), ma sono riluttante a farlo perché sembra a) inefficiente e b) sconfigge il punto di una fase di lexing separata. Sto cercando qualcosa di più pulito.

Oltre a questo, sono bloccato. Sono nuovo nel gioco del parsing e vorrei capire qual è l'approccio giusto.

HackerRank supporta solo Scala 2.11, che ha rimosso ufficialmente la libreria parser-combinator dalla libreria standard, quindi non posso usarla. Sono felice di utilizzare qualsiasi funzione funzionale che Scala porti, ma non mi interessa fare affidamento su qualcosa che implementa solo linguaggi funzionali solo .

Per la cronaca, ecco il mio codice Lexer:

import scala.collection.mutable.HashMap

class Lexer {
    // defining keywords and tokens
    val parenthesis_mapping = HashMap("""(""" -> """)""","""{""" -> """}""");
    val keywords = List("if", "then", "else", "while", "do", "true", "false");
    val boolean_operators = List("and","or")
    val arithmetic_operators = List("""+""","""-""","""/""","""*""",""">""","""<""")
    val variables = """([a-z]+)""".r
    val numbers = """([0-9]*\.?[0-9]+)""".r

    // converts string token to corresponding Token type
    def convert(input : String): Token = {
        if (keywords.contains(input)) { return Token("KEYWORD",input) }
        else if (boolean_operators.contains(input)) { return Token("BOOLEAN",input) }
        else if (arithmetic_operators.contains(input)) { return Token("ARITHMETIC", input) }
        else if (parenthesis_mapping.keys.exists(_ == input)) { return Token("OPENING", input) }
        else if (parenthesis_mapping.values.exists(_ == input)) { return Token("CLOSING", input) }
        else { return input match {
            case variables(output) => Token("VARIABLE", output)
            case """:=""" => Token("ASSIGNMENT", null)
            case numbers(output) => Token("NUMBER", output.toInt)
            case """;""" => Token("BREAK", null)
            case _ => Token("NULL", input)
        } }
    }

   // splits input string and applies 'convert' to each token
   def lex(strings: String): List[Token] = {...} // only providing signature here
  }

ed ecco la grammatica che devo implementare

Below is the description of grammar that we will use:

  • x, y ∈ Var (variables)

  • n ∈ Num (numerals/integers)

  • op_{a} ∈ Opa (arithmetic operators)
    ob_{a} ::= + | - | * | /

  • op_{b} ∈ Opb (boolean operators)
    op_{b} ::= and | or

  • op_{r} ∈ Opr (relational operators)
    op_{r} ::= > | <

  • a ∈ AExp (arithmetic expressions)
    a ::= x | n | a1 opa a2 | ( a )

  • b ∈ BExp (boolean expressions)
    b ::= true | false | b1 opb b2 | a1 opr a2 | ( b )

  • S ∈ Stmt (statements)
    S ::= x := a | S1 ; S2 | if b then { S1 } else { S2 } | while b do { S }

Here all operators are left associative. Their precedence order is as follows.

  1. Arithmetic Operators: (*, /) > (+, -) > (>, <)
  2. Boolean Operators: and > or.

You can safely assume that all variables have integer type and are initialized properly. All variables name will consist of only lowercase letter ('a'-'z') and it's length will not exceed 10.

Note that ";" is more like of a sequencing operator. It is used to concatenate two statements. That's why there will be no ";" at the end of block of statements.

Un esempio della sintassi risultante:

fact := 1 ;
val := 10000 ;
cur := val ;
mod := 1000000007 ;

while ( cur > 1 )
  do
   {
      fact := fact * cur ;
      fact := fact - fact / mod * mod ;
      cur := cur - 1
   } ;

cur := 0
    
posta Akshat Mahajan 09.05.2016 - 20:30
fonte

1 risposta

2

Ti suggerisco di utilizzare il tuo costrutto Lexer come più di un generatore , piuttosto che identificare tutti i token subito e inviando un'intera lista al parser. Il tuo lexer potrebbe semplicemente dichiarare un'interfaccia con un metodo .NextToken() o .Next() (e probabilmente .Backup() ) e ogni volta che Parser richiede un nuovo token, potrebbe semplicemente chiamare uno di questi metodi.

Se la tua lingua richiede token lookahead , puoi semplicemente tenere un elenco di token lookahead sul parser e sempre ritardo N numero di token dietro al token corrente del Lexer.

Per quanto riguarda le tecniche di analisi effettiva, dovresti probabilmente considerare l'analisi della discesa ricorsiva . A mio parere, questi sono in genere i tipi più semplici di parser da codificare a mano e concettualmente sono i parser più semplici da capire.

L'analisi della discesa ricorsiva funziona generalmente abbinando una produzione grammaticale terminale a una funzione o un metodo. Quindi, se hai una produzione di grammatica per una dichiarazione di variabile come var x := 1 , il tuo metodo potrebbe essere simile a questo:

// Grammar Production:
//   'var' Identifier ':=' Expression
private Declaration ParseDeclaration() {
    Declaration decl = new Declaration();

    if (!Expect(TokenType.Var)) {
        throw new ParseError("Expected 'var' keyword");
    }

    decl.Identifier = ParseIdentifier();

    if (!Expect(TokenType.AssignmentEquals)) {
        throw new ParseError("Expected token ':='");
    }

    decl.Expression = ParseExpression();
    return decl;
}

Per ogni produzione nella tua grammatica, puoi definire un metodo come quello e ricostruire ricorsivamente il tuo albero di analisi. Finisce per essere un sistema molto elegante da codificare, piuttosto che cercare di abbinare le produzioni usando i modelli di token. È possibile utilizzare la potenza dello stack di chiamate del linguaggio di programmazione per controllare il flusso e gestire la ripetizione e gli errori di analisi. Lo scalo di smistamento è difficile da generalizzare anche alle regole grammaticali di non-espressione, quindi non consiglio di seguire questa strada.

P.S. Mi dispiace, non conosco la sintassi di Scala a mano a mano, quindi ho deciso di utilizzare solo un tipo di sintassi Java / C #.

    
risposta data 09.05.2016 - 21:16
fonte

Leggi altre domande sui tag