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]]
151 # Skip whitespace, comments, and empty lines
152 skip_to_next_top_level
154 validate_top_level do
155 parse_top_level_nonvalidating
160 # Parses a body for a function or a conditional
167 kind_text = 'function definition'
169 kind_text = kind.to_s
175 statement = parse_top_level_nonvalidating
178 parse_error "End of input while inside #{kind_text}", nil
180 elsif statement[0] == :end
183 elsif kind == :conditional && statement[0] == :else
184 # Done parsing body, but there is another one coming up
188 # Should be a normal statement. Validate it, then add it to body
189 if statement[0] == :function
190 parse_error "Function definitions are only allowed at top-level"
193 Validator.validate_statement statement
195 rescue Validator::ValidationError => e
196 parse_error e.message
201 # Got some kind of error. Still try to parse the rest of the body.
206 # Raise error if we had just one.
207 # If we had more than one, raise a MultipleErrors instance.
208 if errors.length == 1
210 elsif errors.length > 1
211 raise MultipleErrors.new errors
218 # Parses an escape sequence.
219 # This method should be called while the lookahead is the escape
220 # character (backslash). It decodes the escape sequence and returns
221 # the result as a string.
228 parse_error "Unexpected end of input in escape sequence", nil
237 # \r is carriage return
241 # \xXX is byte with hex value XX
243 @column = @column + 2
246 result = [code].pack('H2')
248 # \<newline> is line continuation character
250 # Skip indentation of next line
251 while lookahead =~ /\s/
256 # Default to just passing on next character
265 # This method should be called while the lookahead is the first
266 # character of the number.
271 while lookahead =~ /\d/
280 # This method should be called while the lookahead is the opening
292 result << parse_escape
303 # This method should be called while the lookahead is the first
304 # character of the symbol name.
308 while lookahead != :eof
316 # Colon parsed as last character of the symbol name
333 # Consumes the current lookahead character.
334 # The character is appended to @text.
346 # Tests if a symbol is a label
348 symbol.to_s[-1] == ?:
351 # Tests if a symbol is a conditional starter
352 def is_conditional? symbol
353 [:ifeq, :ifge, :ifgt, :ifle, :iflt, :ifne].member? symbol
356 # Returns the current lookahead character,
357 # or +:eof+ when the end of the input has been reached.
360 @lookahead = @input.getc
364 @lookahead = @lookahead.chr
365 @column = @column.succ
371 # Parses a conditional statement
372 def parse_conditional1 condition, operands
373 # Parse first clause and condition for next clause
374 consequent, next_condition = split_if_clause parse_body(:conditional)
375 if next_condition == nil
378 elsif next_condition == :else
379 # Next clause is else without if
380 alternative = parse_body :conditional
382 # Next clause is else with if
383 alternative = [parse_conditional1(next_condition[0],
386 [condition, operands, consequent, alternative]
389 # Raises a ParseError at the current input position
390 def parse_error message, text = @text
391 # Create the error object
392 error = ParseError.new(message, @input_name, @start_line,
395 # Set a backtrace to the calling method
396 error.set_backtrace caller
398 # If we are not at a new line, skip until the next line
399 while @column > 1 && lookahead != :eof
407 # Parses a top-level directive without validating it
408 def parse_top_level_nonvalidating
409 # Skip whitespace, comments, and empty lines
410 skip_to_next_top_level
416 word = try_parse_token
418 # Word is nil; that means we did not get a token
426 # Exit the loop, but only if the line wasn't empty
427 break unless words.empty?
430 while lookahead != :eof && lookahead != "\n"
434 parse_error "Unexpected character (#{lookahead}) in input"
437 # Word is not nil - we got a token
438 if words.empty? && word.kind_of?(::Symbol) && word.to_s[-1] == ?:
439 # First word is a label
440 words = [:label, word.to_s[0..-2].to_sym]
443 # Add word to statement
449 # We have a line of input. Conditionals and function declarations
450 # must be handled specially, because they consist of more than one
453 # Nothing to parse; return nil
455 elsif words[0] == :function
456 # Function declaration. Parse function body
457 body = parse_body :function
458 [:function, words[1..-1]] + body
459 elsif is_conditional?(words[0])
460 parse_conditional1 words[0], words[1..-1]
461 elsif words[0] == :block
462 body = parse_body :block
465 # Statement or data declaration; simply return it
470 # Skips whitespace, newlines, and comments before a top-level directive
471 def skip_to_next_top_level
482 while lookahead != :eof && lookahead != "\n"
491 # Consumes characters until a character other than space or tab is
494 while lookahead == " " || lookahead == "\t"
499 # Splits a parsed if-clause into two parts:
500 # 1. The list of statements making up the clause proper
501 # 2. The condition for the next clause:
502 # - If there is no next clause, nil
503 # - If the next clause is introduced by else without a condition, :else
504 # - If the next clause is introduced by else iflt x y, [:iflt [:x, :y]]
505 # - And so on for other if.. instances
506 def split_if_clause clause
508 if last.respond_to?(:[]) && last.length > 0 && last[0] == :else
509 clause = clause[0..-2]
512 [clause, [last[1], last[2..-1]]]
523 # Tries to parse a symbol, number, string, or at-expression. If
524 # such a token starts at the current position, it is parsed and returned.
525 # Else, nil is returned.
531 # Digit; parse number
534 # Letter, underscore, or backslash; parse symbol
535 # Note: \w matches digits, too, so keep this case after \d
538 # Double quote; parse string
541 # Parse at-expression.
542 # '@' must be followed by a number or symbol.
550 parse_error "Invalid character (#{lookahead}) " +
551 "in at-expression; expecting number or symbol"
555 # No valid starter for a token, return nil
560 # Evaluate block and check that the result is a valid top-level
562 def validate_top_level &block
567 Validator.validate_top_level result
570 rescue Validator::ValidationError => e
571 parse_error e.message
576 # Evaluate block, keeping track of @start_line, @start_column
577 # at the beginning of the block, and @text during the evaluation
579 def with_position &block
581 old_line = @start_line
582 old_column = @start_column
585 # Evaluate block with new values
588 @start_column = @column
595 @start_line = old_line
596 @start_column = old_column
597 @text = old_text + @text
601 # Ensures that any exceptions that escape from block are instances of
603 def wrap_exceptions &block
607 # Already an instance of Parser::Error; pass it through.
610 # Some other error; wrap in ParserInternalError.
611 wrapped = ParserInternalError.new(e, @input_name, @line,
613 wrapped.set_backtrace e.backtrace