|
|
- # swank.rb --- swank server for Ruby.
- #
- # This is my first Ruby program and looks probably rather strange. Some
- # people write Scheme interpreters when learning new languages, I
- # write swank backends.
- #
- # Only a few things work.
- # 1. Start the server with something like: ruby -r swank -e swank
- # 2. Use M-x slime-connect to establish a connection
-
- require "socket"
-
- def swank(port=4005)
- accept_connections port, false
- end
-
- def start_swank(port_file)
- accept_connections false, port_file
- end
-
- def accept_connections(port, port_file)
- server = TCPServer.new("localhost", port || 0)
- puts "Listening on #{server.addr.inspect}\n"
- if port_file
- write_port_file server.addr[1], port_file
- end
- socket = begin server.accept ensure server.close end
- begin
- serve socket.to_io
- ensure
- socket.close
- end
- end
-
- def write_port_file(port, filename)
- File.open(filename, File::CREAT|File::EXCL|File::WRONLY) do |f|
- f.puts port
- end
- end
-
- def serve(io)
- main_loop(io)
- end
-
- def main_loop(io)
- c = Connection.new(io)
- while true
- catch :swank_top_level do
- c.dispatch(read_packet(io))
- end
- end
- end
-
- class Connection
-
- def initialize(io)
- @io = io
- end
-
- def dispatch(event)
- puts "dispatch: %s\n" % event.inspect
- case event[0]
- when :":emacs-rex"
- emacs_rex *event[1..4]
- else raise "Unhandled event: #{event.inspect}"
- end
- end
-
- def send_to_emacs(obj)
- payload = write_sexp_to_string(obj)
- @io.write("%06x" % payload.length)
- @io.write payload
- @io.flush
- end
-
- def emacs_rex(form, pkg, thread, id)
- proc = $rpc_entries[form[0]]
- args = form[1..-1];
- begin
- raise "Undefined function: #{form[0]}" unless proc
- value = proc[*args]
- rescue Exception => exc
- begin
- pseudo_debug exc
- ensure
- send_to_emacs [:":return", [:":abort"], id]
- end
- else
- send_to_emacs [:":return", [:":ok", value], id]
- end
- end
-
- def pseudo_debug(exc)
- level = 1
- send_to_emacs [:":debug", 0, level] + sldb_info(exc, 0, 20)
- begin
- sldb_loop exc
- ensure
- send_to_emacs [:":debug-return", 0, level, :nil]
- end
- end
-
- def sldb_loop(exc)
- $sldb_context = [self,exc]
- while true
- dispatch(read_packet(@io))
- end
- end
-
- def sldb_info(exc, start, _end)
- [[exc.to_s,
- " [%s]" % exc.class.name,
- :nil],
- sldb_restarts(exc),
- sldb_backtrace(exc, start, _end),
- []]
- end
-
- def sldb_restarts(exc)
- [["Quit", "SLIME top-level."]]
- end
-
- def sldb_backtrace(exc, start, _end)
- bt = []
- exc.backtrace[start.._end].each_with_index do |frame, i|
- bt << [i, frame]
- end
- bt
- end
-
- def frame_src_loc(exc, frame)
- string = exc.backtrace[frame]
- match = /([^:]+):([0-9]+)/.match(string)
- if match
- file,line = match[1..2]
- [:":location", [:":file", file], [:":line", line.to_i], :nil]
- else
- [:":error", "no src-loc for frame: #{string}"]
- end
- end
-
- end
-
- $rpc_entries = Hash.new
-
- $rpc_entries[:"swank:connection-info"] = lambda do ||
- [:":pid", $$,
- :":package", [:":name", "ruby", :":prompt", "ruby> "],
- :":lisp-implementation", [:":type", "Ruby",
- :":name", "ruby",
- :":version", RUBY_VERSION]]
- end
-
- def swank_interactive_eval(string)
- eval(string,TOPLEVEL_BINDING).inspect
- end
-
- $rpc_entries[:"swank:interactive-eval"] = \
- $rpc_entries[:"swank:interactive-eval-region"] = \
- $rpc_entries[:"swank:pprint-eval"] = lambda { |string|
- swank_interactive_eval string
- }
-
- $rpc_entries[:"swank:throw-to-toplevel"] = lambda {
- throw :swank_top_level
- }
-
- $rpc_entries[:"swank:backtrace"] = lambda do |from, to|
- conn, exc = $sldb_context
- conn.sldb_backtrace(exc, from, to)
- end
-
- $rpc_entries[:"swank:frame-source-location"] = lambda do |frame|
- conn, exc = $sldb_context
- conn.frame_src_loc(exc, frame)
- end
-
- #ignored
- $rpc_entries[:"swank:buffer-first-change"] = \
- $rpc_entries[:"swank:operator-arglist"] = lambda do
- :nil
- end
-
- $rpc_entries[:"swank:simple-completions"] = lambda do |prefix, pkg|
- swank_simple_completions prefix, pkg
- end
-
- # def swank_simple_completions(prefix, pkg)
-
- def read_packet(io)
- header = read_chunk(io, 6)
- len = header.hex
- payload = read_chunk(io, len)
- #$deferr.puts payload.inspect
- read_sexp_from_string(payload)
- end
-
- def read_chunk(io, len)
- buffer = io.read(len)
- raise "short read" if buffer.length != len
- buffer
- end
-
- def write_sexp_to_string(obj)
- string = ""
- write_sexp_to_string_loop obj, string
- string
- end
-
- def write_sexp_to_string_loop(obj, string)
- if obj.is_a? String
- string << "\""
- string << obj.gsub(/(["\\])/,'\\\\\1')
- string << "\""
- elsif obj.is_a? Array
- string << "("
- max = obj.length-1
- obj.each_with_index do |e,i|
- write_sexp_to_string_loop e, string
- string << " " unless i == max
- end
- string << ")"
- elsif obj.is_a? Symbol or obj.is_a? Numeric
- string << obj.to_s
- elsif obj == false
- string << "nil"
- elsif obj == true
- string << "t"
- else raise "Can't write: #{obj.inspect}"
- end
- end
-
- def read_sexp_from_string(string)
- stream = StringInputStream.new(string)
- reader = LispReader.new(stream)
- reader.read
- end
-
- class LispReader
- def initialize(io)
- @io = io
- end
-
- def read(allow_consing_dot=false)
- skip_whitespace
- c = @io.getc
- case c
- when ?( then read_list(true)
- when ?" then read_string
- when ?' then read_quote
- when nil then raise EOFError.new("EOF during read")
- else
- @io.ungetc(c)
- obj = read_number_or_symbol
- if obj == :"." and not allow_consing_dot
- raise "Consing-dot in invalid context"
- end
- obj
- end
- end
-
- def read_list(head)
- list = []
- loop do
- skip_whitespace
- c = @io.readchar
- if c == ?)
- break
- else
- @io.ungetc(c)
- obj = read(!head)
- if obj == :"."
- error "Consing-dot not implemented" # would need real conses
- end
- head = false
- list << obj
- end
- end
- list
- end
-
- def read_string
- string = ""
- loop do
- c = @io.getc
- case c
- when ?"
- break
- when ?\\
- c = @io.getc
- case c
- when ?\\, ?" then string << c
- else raise "Invalid escape char: \\%c" % c
- end
- else
- string << c
- end
- end
- string
- end
-
- def read_quote
- [:quote, read]
- end
-
- def read_number_or_symbol
- token = read_token
- if token.empty?
- raise EOFError.new
- elsif /^[0-9]+$/.match(token)
- token.to_i
- elsif /^[0-9]+\.[0-9]+$/.match(token)
- token.to_f
- else
- token.intern
- end
- end
-
- def read_token
- token = ""
- loop do
- c = @io.getc
- if c.nil?
- break
- elsif terminating?(c)
- @io.ungetc(c)
- break
- else
- token << c
- end
- end
- token
- end
-
- def skip_whitespace
- loop do
- c = @io.getc
- case c
- when ?\s, ?\n, ?\t then next
- when nil then break
- else @io.ungetc(c); break
- end
- end
- end
-
- def terminating?(char)
- " \n\t()\"'".include?(char)
- end
-
- end
-
-
- class StringInputStream
- def initialize(string)
- @string = string
- @pos = 0
- @max = string.length
- end
-
- def pos() @pos end
-
- def getc
- if @pos == @max
- nil
- else
- c = @string[@pos]
- @pos += 1
- c
- end
- end
-
- def readchar
- getc or raise EOFError.new
- end
-
- def ungetc(c)
- if @pos > 0 && @string[@pos-1] == c
- @pos -= 1
- else
- raise "Invalid argument: %c [at %d]" % [c, @pos]
- end
- end
-
- end
-
|