Parse EBNF: Difference between revisions

From Rosetta Code
Content added Content deleted
m (Fix incompetent marking of solution that resulted in wrong language community being notified)
(→‎{{header|Ruby}}: It can now print errors about tokens.)
Line 64: Line 64:
=={{header|Ruby}}==
=={{header|Ruby}}==
{{in progress|lang=Ruby|day=12|month=May|year=2011}}
{{in progress|lang=Ruby|day=12|month=May|year=2011}}
{{incomplete|Ruby|This code divides the input into lexical tokens, but does not parse the tokens by grammar.}}
{{incomplete|Ruby|The tokenizer is here, but the parser is very incomplete.}}
<lang ruby>require 'strscan'
<lang ruby>#--
# The tokenizer splits the input into Tokens like "identifier",
# ":", ")*" and so on. This design uses a StringScanner on each line of
# input, therefore a Token can never span more than one line.
#
# Each Token knows its original line and position, so an error message
# can locate a bad token.
#++


require 'strscan'

# A line of input.
# where:: A location like "file.txt:3"
# str:: String of this line
Line = Struct.new :where, :str
Line = Struct.new :where, :str

# A token.
# cat:: A category like :colon, :ident or so on
# str:: String of this token
# line:: Line containing this token
# pos:: Position of this token within this line
Token = Struct.new :cat, :str, :line, :pos
Token = Struct.new :cat, :str, :line, :pos


# Reads and returns the next Token. At end of file, returns nil.
# Reads and returns the next Token. At end of file, returns nil.
#--
# Needs @filename and @in.
#++
def next_token
def next_token
# Loop until we reach a Token.
# Loop until we reach a Token.
loop do
loop do
# If before first line, or if at end of line,
# If at end of line, then get next line, or else declare end of
# then get next line, or else declare end of file.
# file.
if (not @scanner) or @scanner.eos?
if @scanner.eos?
if s = @in.gets
if s = @in.gets
# Each line needs a new Line object. Tokens can hold references
# Each line needs a new Line object. Tokens can hold references
# to old Line objects.
# to old Line objects.
@line = Line.new("#{@filename}:#{@in.lineno}", s)
@line = Line.new("#{@filename}:#{@in.lineno}", s)
@scanner = StringScanner.new(s)
@scanner.string = s
else
else
return nil # End of file
return nil # End of file
Line 91: Line 112:
end
end


# Find token by regexp.
# Read token by regular expression.
# The :unknown regexp must not swallow any good tokens.
if s = @scanner.scan(/:/)
if s = @scanner.scan(/:/)
c = :colon
c = :colon
Line 98: Line 118:
c = :semicolon
c = :semicolon
elsif s = @scanner.scan(/\(/)
elsif s = @scanner.scan(/\(/)
c = :group
c = :paren
elsif s = @scanner.scan(/\)\?/)
elsif s = @scanner.scan(/\)\?/)
c = :option
c = :option
Line 104: Line 124:
c = :repeat
c = :repeat
elsif s = @scanner.scan(/\)/)
elsif s = @scanner.scan(/\)/)
c = :one
c = :group
elsif s = @scanner.scan(/\|/)
elsif s = @scanner.scan(/\|/)
c = :bar
c = :bar
elsif s = @scanner.scan(/'[^']*'|"[^"]*"/)
c = :string
elsif s = @scanner.scan(/[[:alpha:]][[:alnum:]]*/)
elsif s = @scanner.scan(/[[:alpha:]][[:alnum:]]*/)
c = :ident
c = :ident
elsif s = @scanner.scan(/[^:;()'"[:alpha:][:space:]]*/)
elsif s = @scanner.scan(/'[^']*'|"[^"]*"/)
# Fix syntax highlighting for Rosetta Code. => '
c = :string
elsif s = @scanner.scan(/'[^']*|"[^"]*/)
c = :bad_string
elsif s = @scanner.scan(/.*/)
c = :unknown
c = :unknown
end
end
Line 118: Line 141:
end
end


# Prints a _message_ to standard error, along with location of _token_.
def read_it
def error(token, message)
line = token.line

# We print a caret ^ pointing at the bad token. We make a very crude
# attempt to align the caret ^ in the correct column. If the input
# line has a non-[:print:] character, like a tab, then we print it as
# a space.
STDERR.puts <<EOF
#{line.where}: #{message}
#{line.str.gsub(/[^[:print:]]/, " ")}
#{" " * token.pos}^
EOF
end


#--
# The parser converts Tokens to a Grammar object. The parser also
# detects syntax errors.
#++

# A parsed EBNF grammar. It is an Array of Productions.
class Grammar < Array; end

# A production.
# ident:: The identifier
# alts:: An Array of Alternatives
Production = Struct.new :ident, :alts

# An array of Alternatives, as from "(a | b)".
class Group < Array; end

# An optional group, as from "(a | b)?".
class OptionalGroup < Group; end

# A repeated group, as from "(a | b)*".
class RepeatedGroup < Group; end

# An array of identifiers and string literals.
class Alternative < Array; end

#--
# Needs @filename and @in.
#++
def parse
# TODO: this only dumps the tokens.
# TODO: this only dumps the tokens.
while t = next_token
while t = next_token
p t
error(t, "#{t.cat}")
end
end
end
end


# Set @filename and @in. Parse input.
# Read files from ARGV, or else read STDIN.
case ARGV.length
case ARGV.length
when 0 then @filename = "-"
when 0 then @filename = "-"
Line 131: Line 198:
else fail "Too many arguments"
else fail "Too many arguments"
end
end
open(@filename) { |f| @in = f; read_it }</lang>
open(@filename) do |f|
@in = f
@scanner = StringScanner.new("")
parse
end
</lang>


=={{header|Tcl}}==
=={{header|Tcl}}==

Revision as of 22:44, 12 May 2011

Parse EBNF is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Create a simple parser for EBNF grammars. Here is an ebnf grammar in itself and a parser for it in php.

Here are simple parser rules for a calculator taken from the antlr tutorial

expr	: term ( ( PLUS | MINUS )  term )* ;

term	: factor ( ( MULT | DIV ) factor )* ;

factor	: NUMBER ;

PicoLisp

<lang PicoLisp>(def 'expr 'ebnf '(term ((PLUS | MINUS) term) *)) (def 'term 'ebnf '(factor ((MULT | DIV) factor) *)) (def 'factor 'ebnf '(NUMBER))</lang> <lang PicoLisp>(de matchEbnf (Pat)

  (cond
     ((asoq Pat '((PLUS . +) (MINUS . -) (MULT . *) (DIV . /)))
        (let Op (cdr @)
           (when (= Op (car *Lst))
              (pop '*Lst)
              Op ) ) )
     ((== 'NUMBER Pat)
        (cond
           ((num? (car *Lst))
              (pop '*Lst)
              @ )
           ((and (= "-" (car *Lst)) (num? (cadr *Lst)))
              (setq *Lst (cddr *Lst))
              (- @) ) ) )
     ((get Pat 'ebnf) (parseLst @))
     ((atom Pat))
     (T
        (loop
           (T (matchEbnf (pop 'Pat)) @)
           (NIL Pat)
           (NIL (== '| (pop 'Pat)))
           (NIL Pat) ) ) ) )

(de parseLst (Pat)

  (let (P (pop 'Pat)  X (matchEbnf P))
     (loop
        (NIL Pat)
        (if (n== '* (cadr Pat))
           (if (matchEbnf (pop 'Pat))
              (setq X (list @ X))
              (throw) )
           (loop
              (NIL *Lst)
              (NIL (matchEbnf (car Pat)))
              (setq X (list @ X (or (matchEbnf P) (throw)))) )
           (setq Pat (cddr Pat)) ) )
     X ) )

(de parseEbnf (Str)

  (let *Lst (str Str "")
     (catch NIL
        (parseLst (get 'expr 'ebnf)) ) ) )</lang>

Output:

: (parseEbnf "1 + 2 * -3 / 7 - 3 * 4")
-> (- (+ 1 (/ (* 2 -3) 7)) (* 3 4))

Ruby

This example is under development. It was marked thus on 12/May/2011. Please help complete the example.
This example is incomplete. The tokenizer is here, but the parser is very incomplete. Please ensure that it meets all task requirements and remove this message.

<lang ruby>#--

  1. The tokenizer splits the input into Tokens like "identifier",
  2. ":", ")*" and so on. This design uses a StringScanner on each line of
  3. input, therefore a Token can never span more than one line.
  4. Each Token knows its original line and position, so an error message
  5. can locate a bad token.
  6. ++

require 'strscan'

  1. A line of input.
  2. where:: A location like "file.txt:3"
  3. str:: String of this line

Line = Struct.new :where, :str

  1. A token.
  2. cat:: A category like :colon, :ident or so on
  3. str:: String of this token
  4. line:: Line containing this token
  5. pos:: Position of this token within this line

Token = Struct.new :cat, :str, :line, :pos

  1. Reads and returns the next Token. At end of file, returns nil.
  2. --
  3. Needs @filename and @in.
  4. ++

def next_token

 # Loop until we reach a Token.
 loop do
   # If at end of line, then get next line, or else declare end of
   # file.
   if @scanner.eos?
     if s = @in.gets
       # Each line needs a new Line object. Tokens can hold references
       # to old Line objects.
       @line = Line.new("#{@filename}:#{@in.lineno}", s)
       @scanner.string = s
     else
       return nil  # End of file
     end
   end
   # Skip whitespace.
   break unless @scanner.skip(/space:+/)
 end
 # Read token by regular expression.
 if s = @scanner.scan(/:/)
   c = :colon
 elsif s = @scanner.scan(/;/)
   c = :semicolon
 elsif s = @scanner.scan(/\(/)
   c = :paren
 elsif s = @scanner.scan(/\)\?/)
   c = :option
 elsif s = @scanner.scan(/\)\*/)
   c = :repeat
 elsif s = @scanner.scan(/\)/)
   c = :group
 elsif s = @scanner.scan(/\|/)
   c = :bar
 elsif s = @scanner.scan(/alpha:alnum:*/)
   c = :ident
 elsif s = @scanner.scan(/'[^']*'|"[^"]*"/)
   # Fix syntax highlighting for Rosetta Code. => '
   c = :string
 elsif s = @scanner.scan(/'[^']*|"[^"]*/)
   c = :bad_string
 elsif s = @scanner.scan(/.*/)
   c = :unknown
 end
 Token.new(c, s, @line, (@scanner.pos - s.length))

end

  1. Prints a _message_ to standard error, along with location of _token_.

def error(token, message)

 line = token.line
 # We print a caret ^ pointing at the bad token. We make a very crude
 # attempt to align the caret ^ in the correct column. If the input
 # line has a non-[:print:] character, like a tab, then we print it as
 # a space.
 STDERR.puts <<EOF
  1. {line.where}: #{message}
  2. {line.str.gsub(/[^[:print:]]/, " ")}
  3. {" " * token.pos}^

EOF end


  1. --
  2. The parser converts Tokens to a Grammar object. The parser also
  3. detects syntax errors.
  4. ++
  1. A parsed EBNF grammar. It is an Array of Productions.

class Grammar < Array; end

  1. A production.
  2. ident:: The identifier
  3. alts:: An Array of Alternatives

Production = Struct.new :ident, :alts

  1. An array of Alternatives, as from "(a | b)".

class Group < Array; end

  1. An optional group, as from "(a | b)?".

class OptionalGroup < Group; end

  1. A repeated group, as from "(a | b)*".

class RepeatedGroup < Group; end

  1. An array of identifiers and string literals.

class Alternative < Array; end

  1. --
  2. Needs @filename and @in.
  3. ++

def parse

 # TODO: this only dumps the tokens.
 while t = next_token
   error(t, "#{t.cat}")
 end

end

  1. Set @filename and @in. Parse input.

case ARGV.length when 0 then @filename = "-" when 1 then @filename = ARGV[0] else fail "Too many arguments" end open(@filename) do |f|

 @in = f
 @scanner = StringScanner.new("")
 parse

end </lang>

Tcl

This example is in need of improvement:

This is not an EBNF parser. It never uses EBNF. It is a calculator parser, but there is already a calculator parser at Arithmetic evaluation#Tcl. One should adjust this solution to parse the EBNF language, not the calculator language.

Demonstration lexer and parser. Note that this parser supports parenthesized expressions, making the grammar recursive. <lang tcl>package require Tcl 8.6

  1. Utilities to make the coroutine easier to use

proc provide args {while {![yield $args]} {yield}} proc next lexer {$lexer 1} proc pushback lexer {$lexer 0}

  1. Lexical analyzer coroutine core

proc lexer {str} {

   yield [info coroutine]
   set symbols {+ PLUS - MINUS * MULT / DIV ( LPAR ) RPAR}
   set idx 0
   while 1 {

switch -regexp -matchvar m -- $str { {^\s+} { # No special action for whitespace } {^([-+*/()])} { provide [dict get $symbols [lindex $m 1]] [lindex $m 1] $idx } {^(\d+)} { provide NUMBER [lindex $m 1] $idx } {^$} { provide EOT "EOT" $idx return } . { provide PARSE_ERROR [lindex $m 0] $idx } } # Trim the matched string set str [string range $str [string length [lindex $m 0]] end] incr idx [string length [lindex $m 0]]

   }

}

  1. Utility functions to help with making an LL(1) parser; ParseLoop handles
  2. EBNF looping constructs, ParseSeq handles sequence constructs.

proc ParseLoop {lexer def} {

   upvar 1 token token payload payload index index
   foreach {a b} $def {

if {$b ne "-"} {set b [list set c $b]} lappend m $a $b

   }
   lappend m default {pushback $lexer; break}
   while 1 {

lassign [next $lexer] token payload index switch -- $token {*}$m if {[set c [catch {uplevel 1 $c} res opt]]} { dict set opt -level [expr {[dict get $opt -level]+1}] return -options $opt $res }

   }

} proc ParseSeq {lexer def} {

   upvar 1 token token payload payload index index
   foreach {t s} $def {

lassign [next $lexer] token payload index switch -- $token $t { if {[set c [catch {uplevel 1 $s} res opt]]} { dict set opt -level [expr {[dict get $opt -level]+1}] return -options $opt $res } } EOT { throw SYNTAX "end of text at position $index" } default { throw SYNTAX "\"$payload\" at position $index" }

   }

}

  1. Main parser driver; contains "master" grammar that ensures that the whole
  2. text is matched and not just a prefix substring. Note also that the parser
  3. runs the lexer as a coroutine (with a fixed name in this basic demonstration
  4. code).

proc parse {str} {

   set lexer [coroutine l lexer $str]
   try {

set parsed [parse.expr $lexer] ParseLoop $lexer { EOT { return $parsed } } throw SYNTAX "\"$payload\" at position $index"

   } trap SYNTAX msg {

return -code error "syntax error: $msg"

   } finally {

catch {rename $lexer ""}

   }

}

  1. Now the descriptions of how to match each production in the grammar...

proc parse.expr {lexer} {

   set expr [parse.term $lexer]
   ParseLoop $lexer {

PLUS - MINUS { set expr [list $token $expr [parse.term $lexer]] }

   }
   return $expr

} proc parse.term {lexer} {

   set term [parse.factor $lexer]
   ParseLoop $lexer {

MULT - DIV { set term [list $token $term [parse.factor $lexer]] }

   }
   return $term

} proc parse.factor {lexer} {

   ParseLoop $lexer {

NUMBER { return $payload } MINUS { ParseSeq $lexer { NUMBER {return -$payload} } } LPAR { set result [parse.expr $lexer] ParseSeq $lexer { RPAR {return $result} } break } EOT { throw SYNTAX "end of text at position $index" }

   }
   throw SYNTAX "\"$payload\" at position $index"

}</lang>

<lang tcl># Demonstration code puts [parse "1 - 2 - -3 * 4 + 5"] puts [parse "1 - 2 - -3 * (4 + 5)"]</lang> Output:

PLUS {MINUS {MINUS 1 2} {MULT -3 4}} 5
MINUS {MINUS 1 2} {MULT -3 {PLUS 4 5}}