1 require 'voodoo/validator'
5 # The parser reads Voodoo[http://inglorion.net/documents/designs/voodoo/]
6 # source code and turns it into Ruby[http://www.ruby-lang.org/] objects.
8 # The public interface to Parser consists of the methods #new and
13 # require 'voodoo/parser'
15 # File.open('example.voo') do |infile|
16 # parser = Voodoo::Parser.new infile
18 # while (element = parser.parse_top_level)
19 # puts element.inspect
23 # Creates a parser using the specified object as input.
24 # The input object must support a method +getc+, which must
25 # return the next character of the input, or +nil+ to indicate
26 # the end of the input has been reached.
29 @input_name = input.respond_to?(:path) ? input.path : nil
30 @start_line = @line = 1
31 @start_column = @column = 0
36 # Base class for errors reported from the parser.
37 # This provides methods to get the name of the input being processed,
38 # as well as the start_line, start_column, and text of the code
39 # that triggered the error.
40 class Error < StandardError
41 def initialize message, input_name, start_line, start_column, text
43 @input_name = input_name
44 @start_line = start_line
45 @start_column = start_column
49 attr_reader :input_name, :start_line, :start_column, :text
52 # Class for parse errors.
53 # A ParseError indicates an error in the code being parsed.
54 # For other errors that the parser may raise, see ParserInternalError.
55 class ParseError < Parser::Error
56 def initialize message, input_name, start_line, start_column, text
57 super message, input_name, start_line, start_column, text
61 # Class for parser internal errors.
62 # A ParserInternalError indicates an error in the parser that is not
63 # flagged as an error in the code being parsed. Possible causes
64 # include I/O errors while reading code, as well as bugs in the
67 # The +cause+ attribute indicates the initial cause for the error.
68 # The other attributes of ParserInternalError are inherited from
69 # Parser::Error and indicate the input that was being
70 # processed when the error occurred.
71 class ParserInternalError < Parser::Error
72 def initialize cause, input_name, start_line, start_column, text
73 super cause.message, input_name, start_line, start_column, text
80 # Class wrapping multiple Parser::Errors.
81 class MultipleErrors < Parser::Error
84 super(nil, errors[0].input_name, errors[0].start_line,
85 errors[0].start_column, nil)
92 msg = "Multiple errors:\n\n"
93 @errors.each do |error|
94 msg << error.input_name << ":" if error.input_name
95 msg << "#{error.start_line}: " << error.message
97 msg << "\n\n #{error.text.gsub("\n", "\n ")}"
108 texts = @errors.map {|error| error.text}
109 @text = texts.join "\n"
115 # Parses a top-level element.
116 # Returns an array containing the parts of the element.
118 # Some examples (Voodoo code, Ruby return values in comments):
121 # # [:section, :functions]
124 # # [:call, :foo, :x, 12]
127 # # [:set, :x, :add, :x, 42]
130 # # [:"set-byte", [:"@", :x], 1, 10]
137 # # [:ifeq, [:x, :y], [[:set, :z, :equal]], [[:set, :z, :"not-equal"]]]
146 # # [:function, [:x, :y], [:let, :z, :add, :x, :y], [:return, :z]]
150 # Skip whitespace, comments, and empty lines
151 skip_to_next_top_level
153 validate_top_level do
154 parse_top_level_nonvalidating
159 # Parses a body for a function or a conditional
165 kind_text = 'function definition'
167 kind_text = kind.to_s
173 statement = parse_top_level_nonvalidating
176 parse_error "End of input while inside #{kind_text}", nil
178 elsif statement[0] == :end
181 elsif kind == :conditional && statement[0] == :else
182 # Done parsing body, but there is another one coming up
186 # Should be a normal statement. Validate it, then add it to body
187 if statement[0] == :function
188 parse_error "Function definitions are only allowed at top-level"
191 Validator.validate_statement statement
193 rescue Validator::ValidationError => e
194 parse_error e.message
199 # Got some kind of error. Still try to parse the rest of the body.
204 # Raise error if we had just one.
205 # If we had more than one, raise a MultipleErrors instance.
206 if errors.length == 1
208 elsif errors.length > 1
209 raise MultipleErrors.new errors
215 # Parses an escape sequence.
216 # This method should be called while the lookahead is the escape
217 # character (backslash). It decodes the escape sequence and returns
218 # the result as a string.
224 parse_error "Unexpected end of input in escape sequence", nil
233 # \r is carriage return
237 # \xXX is byte with hex value XX
239 @column = @column + 2
242 result = [code].pack('H2')
244 # \<newline> is line continuation character
246 # Skip indentation of next line
247 while lookahead =~ /\s/
252 # Default to just passing on next character
260 # This method should be called while the lookahead is the first
261 # character of the number.
265 while lookahead =~ /\d/
273 # This method should be called while the lookahead is the opening
284 result << parse_escape
294 # This method should be called while the lookahead is the first
295 # character of the symbol name.
306 # Colon parsed as last character of the symbol name
322 # Consumes the current lookahead character.
323 # The character is appended to @text.
336 # Tests if a symbol is a label
338 symbol.to_s[-1] == ?:
341 # Tests if a symbol is a conditional starter
342 def is_conditional? symbol
343 [:ifeq, :ifge, :ifgt, :ifle, :iflt, :ifne].member? symbol
346 # Returns the current lookahead character,
347 # or +:eof+ when the end of the input has been reached.
350 @lookahead = @input.getc
354 @lookahead = @lookahead.chr
355 @column = @column.succ
361 # Parses a conditional statement
362 def parse_conditional1 condition, operands
363 # Parse first clause and condition for next clause
364 consequent, next_condition = split_if_clause parse_body(:conditional)
365 if next_condition == nil
368 elsif next_condition == :else
369 # Next clause is else without if
370 alternative = parse_body :conditional
372 # Next clause is else with if
373 alternative = [parse_conditional1(next_condition[0],
376 [condition, operands, consequent, alternative]
379 # Raises a ParseError at the current input position
380 def parse_error message, text = @text
381 # Create the error object
382 error = ParseError.new(message, @input_name, @start_line,
385 # Set a backtrace to the calling method
386 error.set_backtrace caller
388 # If we are not at a new line, skip until the next line
389 while @column != 1 && lookahead != :eof
397 # Parses a top-level directive without validating it
398 def parse_top_level_nonvalidating
399 # Skip whitespace, comments, and empty lines
400 skip_to_next_top_level
406 word = try_parse_token
408 # Word is nil; that means we did not get a token
416 # Exit the loop, but only if the line wasn't empty
417 break unless words.empty?
420 while lookahead != :eof && lookahead != "\n"
424 parse_error "Unexpected character (#{lookahead}) in input"
427 # Word is not nil - we got a token
428 if words.empty? && word.kind_of?(::Symbol) && word.to_s[-1] == ?:
429 # First word is a label
430 words = [:label, word.to_s[0..-2].to_sym]
433 # Add word to statement
438 # We have a line of input. Conditionals and function declarations
439 # must be handled specially, because they consist of more than one
442 # Nothing to parse; return nil
444 elsif words[0] == :function
445 # Function declaration. Parse function body
446 body = parse_body :function
447 [:function, words[1..-1]] + body
448 elsif is_conditional?(words[0])
449 parse_conditional1 words[0], words[1..-1]
450 elsif words[0] == :block
451 body = parse_body :block
454 # Statement or data declaration; simply return it
459 # Skips whitespace, newlines, and comments before a top-level directive
460 def skip_to_next_top_level
471 while lookahead != :eof && lookahead != "\n"
480 # Consumes characters until a character other than space or tab is
483 while lookahead == " " || lookahead == "\t"
488 # Splits a parsed if-clause into two parts:
489 # 1. The list of statements making up the clause proper
490 # 2. The condition for the next clause:
491 # - If there is no next clause, nil
492 # - If the next clause is introduced by else without a condition, :else
493 # - If the next clause is introduced by else iflt x y, [:iflt [:x, :y]]
494 # - And so on for other if.. instances
495 def split_if_clause clause
497 if last.respond_to?(:[]) && last.length > 0 && last[0] == :else
498 clause = clause[0..-2]
501 [clause, [last[1], last[2..-1]]]
512 # Tries to parse a symbol, number, string, or at-expression. If
513 # such a token starts at the current position, it is parsed and returned.
514 # Else, nil is returned.
518 # Digit; parse number
521 # Letter, underscore, or backslash; parse symbol
522 # Note: \w matches digits, too, so keep this case after \d
525 # Double quote; parse string
528 # Parse at-expression.
529 # '@' must be followed by a number or symbol.
537 parse_error "Invalid character (#{lookahead}) " +
538 "in at-expression; expecting number or symbol"
542 # No valid starter for a token, return nil
547 # Evaluate block and check that the result is a valid top-level
549 def validate_top_level &block
554 Validator.validate_top_level result
557 rescue Validator::ValidationError => e
558 parse_error e.message
563 # Evaluate block, keeping track of @start_line, @start_column
564 # at the beginning of the block, and @text during the evaluation
566 def with_position &block
568 old_line = @start_line
569 old_column = @start_column
572 # Evaluate block with new values
575 @start_column = @column
580 @start_line = old_line
581 @start_column = old_column
582 @text = old_text + @text
586 # Ensures that any exceptions that escape from block are instances of
588 def wrap_exceptions &block
592 # Already an instance of Parser::Error; pass it through.
595 # Some other error; wrap in ParserInternalError.
596 raise ParserInternalError.new(e, @input_name, @line,