2013-05-12 13 views
9

Próbuję parsować prostą składnię wrażliwą przy użyciu biblioteki Parslet w Ruby.Parsera wrażliwego z użyciem indentacji przy użyciu Parslet w Ruby?

Poniżej przedstawiono przykład składni Ja próbuje zanalizować:

level0child0 
level0child1 
    level1child0 
    level1child1 
    level2child0 
    level1child2 

Powstały drzewo będzie wyglądać tak:

[ 
    { 
    :identifier => "level0child0", 
    :children => [] 
    }, 
    { 
    :identifier => "level0child1", 
    :children => [ 
     { 
     :identifier => "level1child0", 
     :children => [] 
     }, 
     { 
     :identifier => "level1child1", 
     :children => [ 
      { 
      :identifier => "level2child0", 
      :children => [] 
      } 
     ] 
     }, 
     { 
     :identifier => "level1child2", 
     :children => [] 
     }, 
    ] 
    } 
] 

parser, że mam teraz można analizować poziom zagnieżdżenia 0 i 1 węzłów, ale nie można przetworzyć przeszłości:

require 'parslet' 

class IndentationSensitiveParser < Parslet::Parser 

    rule(:indent) { str(' ') } 
    rule(:newline) { str("\n") } 
    rule(:identifier) { match['A-Za-z0-9'].repeat.as(:identifier) } 

    rule(:node) { identifier >> newline >> (indent >> identifier >> newline.maybe).repeat.as(:children) } 

    rule(:document) { node.repeat } 

    root :document 

end 

require 'ap' 
require 'pp' 

begin 
    input = DATA.read 

    puts '', '----- input ----------------------------------------------------------------------', '' 
    ap input 

    tree = IndentationSensitiveParser.new.parse(input) 

    puts '', '----- tree -----------------------------------------------------------------------', '' 
    ap tree 

rescue IndentationSensitiveParser::ParseFailed => failure 
    puts '', '----- error ----------------------------------------------------------------------', '' 
    puts failure.cause.ascii_tree 
end 

__END__ 
user 
    name 
    age 
recipe 
    name 
foo 
bar 

Jasne, że potrzebuję dynamiki c licznik, który oczekuje 3 węzłów wcięcia pasujących do identyfikatora na poziomie zagnieżdżenia 3.

Jak mogę zaimplementować parser składni wrażliwy przy użyciu Parsleta w ten sposób? Czy to możliwe?

+0

Nie wiem, czy to jest lepiej zrobione jako parse/budować oddzielne etapy. Prawie każda kombinacja poziomów wcięć byłaby ważna i parsowana, więc dla mnie jest to bardzo prosty parser oparty na liniach, który przechwytuje tylko poziom wcięcia, a następnie coś, co pobiera dane wyjściowe analizatora składni i buduje strukturę zagnieżdżoną. –

Odpowiedz

13

Istnieje kilka podejść.

  1. Przetwarza dokument, uznając każdą linię jako zbiór nacięć i identyfikator, a następnie zastosować transformację potem odtworzyć hierarchię opartą na liczbie tiret.

  2. Zastosowanie oddaje do przechowywania wcięcie bieżącego i oczekiwać następnego węzła to, że wcięcie plus więcej, aby dopasować jak dziecko (nie kopać w tym podejściu samo jak następny przyszło mi do głowy)

  3. Reguły to tylko metody. Możesz więc zdefiniować "węzeł" jako metodę, co oznacza, że ​​możesz przekazywać parametry! (Następująco)

ta pozwala zdefiniować node(depth) pod względem node(depth+1). Problem z tym podejściem polega jednak na tym, że metoda node nie pasuje do napisu, generuje analizator składni. Tak więc rekursywne połączenie nigdy się nie zakończy.

Istnieje powód, dla którego dynamic istnieje. Zwraca analizator składni, który nie został rozstrzygnięty, aż do momentu, w którym spróbuje go dopasować, umożliwiając teraz powtarzanie bez problemów.

Zobacz następujący kod:

require 'parslet' 

class IndentationSensitiveParser < Parslet::Parser 

    def indent(depth) 
    str(' '*depth) 
    end 

    rule(:newline) { str("\n") } 

    rule(:identifier) { match['A-Za-z0-9'].repeat(1).as(:identifier) } 

    def node(depth) 
    indent(depth) >> 
    identifier >> 
    newline.maybe >> 
    (dynamic{|s,c| node(depth+1).repeat(0)}).as(:children) 
    end 

    rule(:document) { node(0).repeat } 

    root :document 
end 

To moja ulubionym rozwiązaniem.

+1

Przepraszam, sir. Od pewnego czasu próbuję podejścia numer 2. Mój problem polegał na tym, że nie miałem jasnego zrozumienia "dynamicznego", ponieważ doktorzy nie zanurzają się w temacie tak bardzo, jak bym chciał. Dziękuję bardzo za odpowiedź! Ten parser prawdopodobnie zapewni podstawy dla wielu innych w mojej przyszłości: p – RyanScottLewis

+0

Bardzo dziękuję za to, użyłem tego rozwiązania w https://github.com/alphagov/smartdown – heathd

0

Nie podoba mi się pomysł tkania wiedzy o procesie wcięcia przez całą gramatykę. Wolałbym raczej produkować tokeny INDENT i DEDENT, które inne reguły mogłyby używać podobnie do dopasowywania znaków "{" i "}". Oto moje rozwiązanie. Jest to klasa IndentParser, którą każdy analizator składni może rozciągnąć, aby uzyskać wygenerowane tokeny.

require 'parslet' 

# Atoms returned from a dynamic that aren't meant to match anything. 
class AlwaysMatch < Parslet::Atoms::Base 
    def try(source, context, consume_all) 
    succ("") 
    end 
end 
class NeverMatch < Parslet::Atoms::Base 
    attr_accessor :msg 
    def initialize(msg = "ignore") 
    self.msg = msg 
    end 
    def try(source, context, consume_all) 
    context.err(self, source, msg) 
    end 
end 
class ErrorMatch < Parslet::Atoms::Base 
    attr_accessor :msg 
    def initialize(msg) 
    self.msg = msg 
    end 
    def try(source, context, consume_all) 
    context.err(self, source, msg) 
    end 
end 

class IndentParser < Parslet::Parser 

    ## 
    # Indentation handling: when matching a newline we check the following indentation. If 
    # that indicates an indent token or detent tokens (1+) then we stick these in a class 
    # variable and the high-priority indent/dedent rules will match as long as these 
    # remain. The nl rule consumes the indentation itself. 

    rule(:indent) { dynamic {|s,c| 
    if @indent.nil? 
     NeverMatch.new("Not an indent") 
    else 
     @indent = nil 
     AlwaysMatch.new 
    end 
    }} 
    rule(:dedent) { dynamic {|s,c| 
    if @dedents.nil? or @dedents.length == 0 
     NeverMatch.new("Not a dedent") 
    else 
     @dedents.pop 
     AlwaysMatch.new 
    end 
    }} 

    def checkIndentation(source, ctx) 
    # See if next line starts with indentation. If so, consume it and then process 
    # whether it is an indent or some number of dedents. 
    indent = "" 
    while source.matches?(Regexp.new("[ \t]")) 
     indent += source.consume(1).to_s #returns a Slice 
    end 

    if @indentStack.nil? 
     @indentStack = [""] 
    end 

    currentInd = @indentStack[-1] 
    return AlwaysMatch.new if currentInd == indent #no change, just match nl 

    if indent.start_with?(currentInd) 
     # Getting deeper 
     @indentStack << indent 
     @indent = indent #tells the indent rule to match one 
     return AlwaysMatch.new 
    else 
     # Either some number of de-dents or an error 

     # Find first match starting from back 
     count = 0 
     @indentStack.reverse.each do |level| 
     break if indent == level #found it, 

     if level.start_with?(indent) 
      # New indent is prefix, so we de-dented this level. 
      count += 1 
      next 
     end 

     # Not a match, not a valid prefix. So an error! 
     return ErrorMatch.new("Mismatched indentation level") 
     end 

     @dedents = [] if @dedents.nil? 
     count.times { @dedents << @indentStack.pop } 
     return AlwaysMatch.new 
    end 
    end 
    rule(:nl)   { anynl >> dynamic {|source, ctx| checkIndentation(source,ctx) }} 

    rule(:unixnl)  { str("\n") } 
    rule(:macnl)  { str("\r") } 
    rule(:winnl)  { str("\r\n") } 
    rule(:anynl)  { unixnl | macnl | winnl } 

end 

Jestem pewien, że wiele można poprawić, ale do tej pory doszedłem do tej pory.

Przykład użycia:

class MyParser < IndentParser 
    rule(:colon)  { str(':') >> space? } 

    rule(:space)  { match(' \t').repeat(1) } 
    rule(:space?)  { space.maybe } 

    rule(:number)  { match['0-9'].repeat(1).as(:num) >> space? } 
    rule(:identifier) { match['a-zA-Z'] >> match["a-zA-Z0-9"].repeat(0) } 

    rule(:block)  { colon >> nl >> indent >> stmt.repeat.as(:stmts) >> dedent } 
    rule(:stmt)  { identifier.as(:id) >> nl | number.as(:num) >> nl | testblock } 
    rule(:testblock) { identifier.as(:name) >> block } 

    rule(:prgm)  { testblock >> nl.repeat } 
    root :prgm 
end 
+0

Znalazłem problem z moją odpowiedzią. Ponieważ PEG chciwie konsumować następny mecz bez uwzględnienia niedeterministycznych alternatyw, nic nie sprawdza reguł dedentowania/wcięcia podczas dodawania instrukcji wewnątrz bloku. Tak więc, jeśli po dziecinnym bloku następuje dedyko wane i więcej stwierdzeń, te sygnały są chciwie dodawane do bloku potomnego, ignorując dedent. Aby to naprawić, przeplatając same reguły dentystyczne za pomocą gramatyki (fuj) lub wrzucając tokeny do strumienia wejściowego, może to być lexer. – webjprgm

Powiązane problemy