Updated Makefiles to fit new directory structure.
[voodoo-lang.git] / lib / voodoo / parser.rb
blob2657ec8cdff38b1508f91b6ebb086b85bbe2d5cb
1 module Voodoo
2   # Voodoo parser.
3   # The parser reads Voodoo[http://inglorion.net/documents/designs/voodoo/]
4   # source code and turns it into Ruby[http://www.ruby-lang.org/] objects.
5   #
6   # The public interface to Parser consists of the methods #new and
7   # #parse_top_level
8   #
9   # Example usage:
10   #
11   #   require 'voodoo/parser'
12   #
13   #   File.open('example.voo') do |infile|
14   #     parser = Voodoo::Parser.new infile
15   #
16   #     while (element = parser.parse_top_level)
17   #       puts element.inspect
18   #     end
19   #   end
20   class Parser
21     # Creates a parser using the specified object as input.
22     # The input object must support a method +getc+, which must
23     # return the next character of the input, or +nil+ to indicate
24     # the end of the input has been reached.
25     def initialize input
26       @input = input
27       @line = 1
28       @char = 0
29       @lookahead = nil
30     end
32     # Consumes the current lookahead character
33     def consume
34       old = @lookahead
35       if old == 10
36         @line = @line.succ
37         @char = 0
38       end
39       @lookahead = @input.getc
40       @char = @char.succ unless @lookahead == nil
41       old
42     end
44     # Returns the current lookahead character,
45     # or +nil+ when the end of the input has been reached.
46     def lookahead
47       if @lookahead == nil
48         @lookahead = @input.getc
49         @char = @char.succ
50       end
51       @lookahead == nil ? nil : @lookahead.chr
52     end
54     # Parses a top-level element.
55     # Returns an array containing the parts of the element.
56     # Eeach element of the array is a Symbol, a String, or an
57     # Integer.
58     #
59     # For a label, returns:
60     #   [:label, label_name]
61     #
62     # For a function definition, returns:
63     #   [:function, [formala, formalb, ...], [statementa, statementb, ...]]
64     #
65     # For a conditional, returns:
66     #   [condition, expression, [truea, trueb, ...], [falsea, falseb, ...]]
67     #
68     def parse_top_level
69       words = []
70       word = ""
72       while true
73         case lookahead
74         when nil
75           # End of input
76           break
77         when "\n"
78           # Newline
79           consume
80           # Exit the loop, but only if the line wasn't empty
81           break unless words.empty?
82         when "#"
83           # Skip comment
84           while lookahead != nil && lookahead != "\n"
85             word << lookahead
86             consume
87           end
88         when /\d|-/
89           # Digit; parse number
90           words << parse_number
91         when /\w|\\/
92           # Letter, underscore, or backslash; parse symbol
93           # Note: \w matches digites, too, so keep this case after \d
94           words << parse_symbol
95           if words.length == 1 && is_label?(words[-1])
96             # We have a label; return it
97             return [:label, words[-1].to_s[0..-2].to_sym]
98           end
99         when "\""
100           # Double quote; parse string
101           words << parse_string
102         when /\s/
103           # Skip whitespace
104           consume
105         else
106           raise "Invalid character (#{@lookahead}) at #{@line}:#{@char}"
107         end
108       end
110       # We have a line of input. Conditionals and function declarations
111       # must be handled specially, because they consist of more than one
112       # line.
113       if words.empty?
114         # Nothing to parse; return nil
115         nil
116       elsif words[0] == :function
117         # Function declaration. Parse function body
118         body = parse_body :function
119         [:function, words[1..-1], body]
120       elsif is_conditional?(words[0])
121         # Conditional. Parse true part
122         true_body = parse_body :conditional
123         # Parse false part, if present
124         if true_body[-1] == :else
125           true_body = true_body[0..-2]
126           false_body = parse_body :conditional
127         else
128           false_body = []
129         end
130         [words[0], words[1..-1], true_body, false_body]
131       else
132         # Statement or data declaration; simply return it
133         words
134       end
135     end
137     # Parses a body for a function or a conditional
138     def parse_body kind
139       body = []
140       case kind
141       when :function
142         kind_text = 'function definition'
143       else
144         kind_text = kind.to_s
145       end
146       while true
147         statement = parse_top_level
148         if statement == nil
149           raise "End of input while inside #{kind_text}"
150         elsif statement[0] == :end
151           # Done parsing body
152           break
153         elsif kind == :conditional && statement[0] == :else
154           # Done parsing body, but there is another one coming up
155           body << :else
156           break
157         else
158           # Parsed a statement. Add it to body.
159           body << statement
160         end
161       end
162       body
163     end
165     # Parses an escape sequence.
166     # This method should be called while the lookahead is the escape
167     # character (backslash). It decodes the escape sequence and returns
168     # the result as a string.
169     def parse_escape
170       result = nil
171       consume
172       case lookahead
173       when nil
174         raise "Unexpected end of input in escape sequence"
175       when "\\", "\"", " "
176         result = lookahead
177         consume
178       when "n"
179         # \n is newline
180         consume
181         result = "\n"
182       when "r"
183         # \r is carriage return
184         consume
185         result = "\r"
186       when "x"
187         # \xXX is byte with hex value XX
188         code = @input.read 2
189         @char = @char + 2
190         consume
191         result = [code].pack('H2')
192       when "\n"
193         # \<newline> is line continuation character
194         consume
195         # Skip indentation of next line
196         while lookahead =~ /\s/
197           consume
198         end
199         result = ''
200       else
201         # Default to just passing on next character
202         result = lookahead
203         consume
204       end
205       result
206     end
208     # Parses a number.
209     # This method should be called while the lookahead is the first
210     # character of the number.
211     def parse_number
212       text = lookahead
213       consume
214       while lookahead =~ /\d/
215         text << lookahead
216         consume
217       end
218       text.to_i
219     end
221     # Parses a string.
222     # This method should be called while the lookahead is the opening
223     # double quote.
224     def parse_string
225       consume
226       result = ''
227       while true
228         case lookahead
229         when "\""
230           consume
231           break
232         when "\\"
233           result << parse_escape
234         else
235           result << lookahead
236           consume
237         end
238       end
239       result
240     end
242     # Parses a symbol.
243     # This method should be called while the lookahead is the first
244     # character of the symbol name.
245     def parse_symbol
246       name = ''
247       while true
248         case lookahead
249         when "\\"
250           name << parse_escape
251         when /\w|-/
252           name << lookahead
253           consume
254         when ':'
255           # Colon parsed as last character of the symbol name
256           name << lookahead
257           consume
258           break
259         else
260           break
261         end
262       end
263       name.to_sym
264     end
266     # Tests if a symbol is a label
267     def is_label? symbol
268       symbol.to_s[-1] == ?:
269     end
271     # Tests if a symbol is a conditional starter
272     def is_conditional? symbol
273       [:ifeq, :ifge, :ifgt, :ifle, :iflt, :ifne].member? symbol
274     end
275   end