class Apricot::Reader
Constants
- APPLY
- CHAR_ESCAPES
- CONCAT
- DIGITS
- FN
- FnState
- HEX
- IDENTIFIER
- LIST
- OCTAL
- QUOTE
- REGEXP_OPTIONS
- UNQUOTE
- UNQUOTE_SPLICING
Public Class Methods
new(io, filename = '(none)', line = 1)
click to toggle source
@param [IO] io an input stream object to read forms from
# File lib/apricot/reader.rb, line 32 def initialize(io, filename = '(none)', line = 1) @filename = filename @io = io @location = 0 @line = line @fn_state = [] # Read the first character next_char end
read_file(filename)
click to toggle source
# File lib/apricot/reader.rb, line 44 def self.read_file(filename) File.open(filename) {|f| new(f, filename).read } end
read_string(source, filename = '(none)', line = 1)
click to toggle source
# File lib/apricot/reader.rb, line 48 def self.read_string(source, filename = '(none)', line = 1) new(StringIO.new(source), filename, line).read end
Public Instance Methods
read()
click to toggle source
Return a list of the forms that were read.
# File lib/apricot/reader.rb, line 53 def read forms = [] skip_whitespace while @char forms << read_form skip_whitespace end forms end
read_one()
click to toggle source
# File lib/apricot/reader.rb, line 65 def read_one skip_whitespace form = read_form # Unget the last character because the reader always reads one character # ahead. @io.ungetc(@char) if @char form end
Private Instance Methods
char_escape_helper(base, regex, n)
click to toggle source
Read digits in a certain base for string character escapes
# File lib/apricot/reader.rb, line 327 def char_escape_helper(base, regex, n) number = "" n.times do number << @char next_char break if @char !~ regex end number.to_i(base).chr end
cons(head, tail)
click to toggle source
# File lib/apricot/reader.rb, line 540 def cons(head, tail) tail.cons(head) end
consume_char()
click to toggle source
# File lib/apricot/reader.rb, line 506 def consume_char char = @char next_char char end
hashify(array)
click to toggle source
# File lib/apricot/reader.rb, line 552 def hashify(array) array.each_slice(2).with_object({}) do |(key, value), hash| hash[key] = value end end
incomplete_error(message)
click to toggle source
# File lib/apricot/reader.rb, line 531 def incomplete_error(message) raise SyntaxError.new(@filename, @line, message, true) end
is_unquote?(form)
click to toggle source
# File lib/apricot/reader.rb, line 544 def is_unquote?(form) form.is_a?(Seq) && form.first == UNQUOTE end
is_unquote_splicing?(form)
click to toggle source
# File lib/apricot/reader.rb, line 548 def is_unquote_splicing?(form) form.is_a?(Seq) && form.first == UNQUOTE_SPLICING end
next_char()
click to toggle source
# File lib/apricot/reader.rb, line 512 def next_char @line += 1 if @char == "\n" @char = @io.getc return nil unless @char @location += 1 @char end
opposite_delimiter(c)
click to toggle source
# File lib/apricot/reader.rb, line 558 def opposite_delimiter(c) case c when '(' then ')' when '[' then ']' when '{' then '}' when '<' then '>' else c end end
peek_char()
click to toggle source
# File lib/apricot/reader.rb, line 520 def peek_char char = @io.getc return nil unless char @io.ungetc char char end
read_array()
click to toggle source
# File lib/apricot/reader.rb, line 264 def read_array line = @line next_char # skip the [ with_location read_forms_until(']'), line end
read_dispatch()
click to toggle source
# File lib/apricot/reader.rb, line 120 def read_dispatch next_char # skip # case @char when '|' then read_pipe_identifier when '{' then read_set when '(' then read_fn when 'r' then read_regex when 'q' then read_quotation(false) when 'Q' then read_quotation(true) else syntax_error "Unknown reader macro: ##{@char}" end end
read_fn()
click to toggle source
# File lib/apricot/reader.rb, line 242 def read_fn line = @line @fn_state << FnState.new([], nil) body = read_list state = @fn_state.pop state.args << Identifier.intern(:'&') << state.rest if state.rest args = state.args.map.with_index do |x, i| x || Apricot.gensym("p#{i + 1}") end with_location List[FN, args, body], line end
read_form()
click to toggle source
Read a single Lisp form
# File lib/apricot/reader.rb, line 98 def read_form case @char when '#' then read_dispatch when "'" then read_quote when "`" then read_syntax_quote when "~" then read_unquote when '(' then read_list when '[' then read_array when '{' then read_hash when '"' then read_string when ':' then read_symbol when /\d/ then read_number when IDENTIFIER if @char =~ /[+-]/ && peek_char =~ /\d/ read_number else read_identifier end else syntax_error "Unexpected character: #{@char}" end end
read_forms_until(terminator)
click to toggle source
Read forms until the given character is encountered
# File lib/apricot/reader.rb, line 79 def read_forms_until(terminator) skip_whitespace forms = [] while @char if @char == terminator next_char # consume the terminator return forms end forms << read_form skip_whitespace end # Can only reach here if we run out of chars without getting a terminator incomplete_error "Unexpected end of program, expected #{terminator}" end
read_hash()
click to toggle source
# File lib/apricot/reader.rb, line 270 def read_hash line = @line next_char # skip the { forms = read_forms_until('}') syntax_error "Odd number of forms in key-value hash" if forms.count.odd? with_location hashify(forms), line end
read_identifier()
click to toggle source
# File lib/apricot/reader.rb, line 454 def read_identifier line = @line identifier = "" while @char =~ IDENTIFIER identifier << @char next_char end case identifier when 'true' then return true when 'false' then return false when 'nil' then return nil end state = @fn_state.last # Handle % identifiers in #() syntax if state && identifier[0] == '%' case identifier[1..-1] when '' # % is equivalent to %1 state.args[0] ||= with_location Apricot.gensym('p1'), line when '&' state.rest ||= with_location Apricot.gensym('rest'), line when /^[1-9]\d*$/ n = identifier[1..-1].to_i state.args[n - 1] ||= with_location Apricot.gensym("p#{n}"), line else syntax_error "arg literal must be %, %& or %integer" end else with_location Identifier.intern(identifier), line end end
read_list()
click to toggle source
# File lib/apricot/reader.rb, line 258 def read_list line = @line next_char # skip the ( with_location read_forms_until(')').to_list, line end
read_number()
click to toggle source
# File lib/apricot/reader.rb, line 429 def read_number number = "" while @char =~ IDENTIFIER number << @char next_char end case number when /^[+-]?\d+$/ number.to_i when /^([+-]?)(\d+)r([a-zA-Z0-9]+)$/ sign, radix, digits = $1, $2.to_i, $3 syntax_error "Radix out of range: #{radix}" unless 2 <= radix && radix <= 36 syntax_error "Invalid digits for radix in number: #{number}" unless digits.downcase.chars.all? {|d| DIGITS[0..radix-1].include?(d) } (sign + digits).to_i(radix) when /^[+-]?\d+\.?\d*(?:e[+-]?\d+)?$/ number.to_f when /^([+-]?\d+)\/(\d+)$/ Rational($1.to_i, $2.to_i) else syntax_error "Invalid number: #{number}" end end
read_pipe_identifier()
click to toggle source
# File lib/apricot/reader.rb, line 489 def read_pipe_identifier line = @line next_char # skip the | identifier = "" while @char if @char == '|' next_char # consume the | return with_location Identifier.intern(identifier), line end identifier << read_string_char end incomplete_error "Unexpected end of program while parsing pipe identifier" end
read_quotation(double_quote)
click to toggle source
# File lib/apricot/reader.rb, line 378 def read_quotation(double_quote) line = @line next_char # skip the prefix delimiter = opposite_delimiter(@char) next_char # skip delimiter string = "" while @char if @char == delimiter next_char # consume delimiter return with_location string, line end if double_quote string << read_string_char elsif @char == "\\" && (peek_char == delimiter || peek_char == "\\") next_char string << consume_char else string << consume_char end end incomplete_error "Unexpected end of program while parsing quotation" end
read_quote()
click to toggle source
# File lib/apricot/reader.rb, line 154 def read_quote line = @line next_char # skip the ' skip_whitespace incomplete_error "Unexpected end of program after quote ('), expected a form" unless @char with_location List[QUOTE, read_form], line end
read_regex()
click to toggle source
# File lib/apricot/reader.rb, line 339 def read_regex line = @line next_char # skip the r delimiter = opposite_delimiter(@char) next_char # skip delimiter regex = "" while @char if @char == delimiter next_char # consume delimiter options = regex_options_helper return with_location Regexp.new(regex, options), line elsif @char == "\\" && peek_char == delimiter next_char elsif @char == "\\" && peek_char == "\\" regex << consume_char end regex << consume_char end incomplete_error "Unexpected end of program while parsing regex" end
read_set()
click to toggle source
# File lib/apricot/reader.rb, line 278 def read_set line = @line next_char # skip the { with_location read_forms_until('}').to_set, line end
read_string()
click to toggle source
# File lib/apricot/reader.rb, line 284 def read_string line = @line next_char # skip the opening " string = "" while @char if @char == '"' next_char # consume the " return with_location string, line end string << read_string_char end # Can only reach here if we run out of chars without getting a " incomplete_error "Unexpected end of program while parsing string" end
read_string_char()
click to toggle source
# File lib/apricot/reader.rb, line 302 def read_string_char char = if @char == "\\" next_char if CHAR_ESCAPES.has_key?(@char) CHAR_ESCAPES[consume_char] elsif @char =~ OCTAL char_escape_helper(8, OCTAL, 3) elsif @char == 'x' next_char syntax_error "Invalid hex character escape" unless @char =~ HEX char_escape_helper(16, HEX, 2) else consume_char end else consume_char end incomplete_error "Unexpected end of file while parsing character escape" unless char char end
read_symbol()
click to toggle source
# File lib/apricot/reader.rb, line 404 def read_symbol line = @line next_char # skip the : symbol = "" if @char == '"' next_char # skip opening " while @char break if @char == '"' symbol << read_string_char end incomplete_error "Unexpected end of program while parsing symbol" unless @char == '"' next_char # skip closing " else while @char =~ IDENTIFIER symbol << @char next_char end syntax_error "Empty symbol name" if symbol.empty? end symbol.to_sym end
read_syntax_quote()
click to toggle source
# File lib/apricot/reader.rb, line 163 def read_syntax_quote line = @line next_char # skip the ` skip_whitespace incomplete_error "Unexpected end of program after syntax quote (`), expected a form" unless @char with_location syntax_quote(read_form, {}), line end
read_unquote()
click to toggle source
# File lib/apricot/reader.rb, line 222 def read_unquote line = @line unquote_type = UNQUOTE next_char # skip the ~ if @char == '@' next_char # skip the ~@ unquote_type = UNQUOTE_SPLICING end skip_whitespace unless @char syntax = (unquote_type == UNQUOTE ? '~' : '~@') incomplete_error "Unexpected end of program after #{syntax}, expected a form" end with_location List[unquote_type, read_form], line end
regex_options_helper()
click to toggle source
# File lib/apricot/reader.rb, line 362 def regex_options_helper options = 0 while @char =~ /[a-zA-Z]/ if option = REGEXP_OPTIONS[@char] options |= option else syntax_error "Unknown regexp option: '#{@char}'" end next_char end options end
skip_whitespace()
click to toggle source
Skips whitespace, commas, and comments
# File lib/apricot/reader.rb, line 134 def skip_whitespace while @char =~ /[\s,;#]/ # Comments begin with a semicolon and extend to the end of the line # Treat #! as a comment for shebang lines if @char == ';' || (@char == '#' && peek_char == '!') while @char && @char != "\n" next_char end elsif @char == '#' break unless peek_char == '_' next_char; next_char # skip #_ skip_whitespace incomplete_error "Unexpected end of program after #_, expected a form" unless @char read_form # discard next form else next_char end end end
syntax_error(message)
click to toggle source
# File lib/apricot/reader.rb, line 527 def syntax_error(message) raise SyntaxError.new(@filename, @line, message) end
syntax_quote(form, gensyms)
click to toggle source
# File lib/apricot/reader.rb, line 172 def syntax_quote(form, gensyms) case form when Seq if is_unquote? form form.rest.first elsif is_unquote_splicing? form syntax_error "splicing unquote (~@) not in list" else syntax_quote_list(form, gensyms) end when Array syntax_quote_coll(:array, form, gensyms) when Set syntax_quote_coll(:'hash-set', form, gensyms) when Hash syntax_quote_coll(:hash, form.to_a.flatten(1), gensyms) when Identifier name = form.name if name.to_s.end_with?('#') gensyms[name] ||= Apricot.gensym(name) List[QUOTE, gensyms[name]] else List[QUOTE, form] end else form end end
syntax_quote_coll(creator_name, elements, gensyms)
click to toggle source
# File lib/apricot/reader.rb, line 202 def syntax_quote_coll(creator_name, elements, gensyms) creator = Identifier.intern(creator_name) list = syntax_quote_list(elements, gensyms) List[APPLY, creator, list] end
syntax_quote_list(elements, gensyms)
click to toggle source
# File lib/apricot/reader.rb, line 208 def syntax_quote_list(elements, gensyms) parts = elements.map do |form| if is_unquote? form List[LIST, form.rest.first] elsif is_unquote_splicing? form form.rest.first else List[LIST, syntax_quote(form, gensyms)] end end Cons.new(CONCAT, parts) end
with_location(obj, line)
click to toggle source
# File lib/apricot/reader.rb, line 535 def with_location(obj, line) obj.apricot_meta = {line: line} obj end