Create a gist now

Instantly share code, notes, and snippets.

RubyScript - A transcompiler transforming a subset of Ruby to JavaScript

RubyScript

A ~400LOC transcompiler transforming a subset of Ruby to JavaScript.

Installing and usage

curl https://raw.github.com/gist/2017173/efebd33d6ff430347ddb953c587318c8934cfd1e/rubyscript.rb > rubyscript
chmod +x rubyscript
./rubyscript my_file.rb > my_file.js

Built for my talk "Brew your own coffee...script" at Codegram Monday Talks. Check out the slides!

Follow me on Twitter yo.

#!/usr/bin/env rbx
require 'set'
module Rubinius
module AST
class Node
# Public: Works like #visit, but it doesn't visit the children just yet;
# instead, lets the visitor decide when and how to do it.
#
# visitor - The visitor object. It must respond to methods named after the
# node names.
#
# Returns nothing.
def lazy_visit(visitor, parent=nil, indent=false)
args = [self.node_name, self, parent]
args.push true if indent
visitor.__send__ *args
end
end
end
end
module RubyScript
VERSION = '0.0.1'
class Visitor
def initialize
@locals = Set.new
@output = []
@indentation = 1
end
def emit(code)
@output.push code
end
def finalize
@output.unshift header
emit footer
@output.join
end
def header
"(function() {\n" <<
local_declarations
end
def local_declarations
if @locals.any?
current_indentation <<
"// Declare local variables\n" <<
current_indentation <<
"var #{@locals.to_a.join(', ')};\n\n"
else
""
end
end
def footer
"\n})();"
end
def current_indentation
' ' * @indentation
end
def local_variable_assignment(node, parent)
emit "%s = " % node.name
node.value.lazy_visit self, node
@locals << node.name
end
def local_variable_access(node, parent)
emit node.name
end
def instance_variable_assignment(node, parent)
emit "this.%s = " % node.name
node.value.lazy_visit self, node
end
def instance_variable_access(node, parent)
emit "this.%s" % node.name[1..-1]
end
def fixnum_literal(node, parent)
emit node.value.to_s
end
def float_literal(node, parent)
emit node.value.to_s
end
def string_literal(node, parent)
emit '"' << node.string.to_s << '"'
end
def symbol_literal(node, parent)
emit '"' << node.value.to_s << '"'
end
def true_literal(node, parent)
emit 'true'
end
def false_literal(node, parent)
emit 'false'
end
def nil_literal(node, parent)
emit 'undefined'
end
def array_literal(node, parent)
body = node.body
emit '['
body.each_with_index do |node, index|
node.lazy_visit self, node
emit ', ' unless body.length == index + 1 # last element
end
emit ']'
end
def hash_literal(node, parent)
body = node.array.each_slice(2)
emit "{\n"
@indentation += 1
body.each_with_index do |slice, index|
key, value = slice
emit current_indentation
emit key.value
emit ": "
value.lazy_visit self, node
emit ",\n" unless body.to_a.length == index + 1 # last element
end
@indentation -= 1
emit "\n"
emit current_indentation
emit '}'
end
def regex_literal(node, parent)
emit '/'
emit node.source
emit '/'
end
def send(node, parent)
unless node.receiver.is_a?(Rubinius::AST::Self)
node.receiver.lazy_visit self, node
emit '.'
end
unless node.name == :lambda
emit node.name
end
if node.block
emit ' ' unless node.name == :lambda
node.block.lazy_visit self, node if node.block
end
end
def send_with_arguments(node, parent)
return if process_binary_operator(node, parent) # 1 * 2, a / 3, true && false
unless node.receiver.is_a?(Rubinius::AST::Self)
node.receiver.lazy_visit self, node
emit '.'
end
emit node.name
emit '('
node.arguments.lazy_visit self, node
emit ')'
if node.block
emit ' '
node.block.lazy_visit self, node if node.block
end
end
def actual_arguments(node, parent)
body = node.array
body.each_with_index do |argument, index|
argument.lazy_visit self, parent
emit ', ' unless body.length == index + 1 # last element
end
end
def formal_arguments19(node, parent)
names = node.names
names.each_with_index do |argument, index|
emit argument.to_s
emit ', ' unless names.length == index + 1 # last element
end
end
def iter(node, parent)
emit 'function('
if node.arguments && node.arguments.arity != -1
node.arguments.lazy_visit self, parent
end
emit ") {\n"
@indentation += 1
if node.body.is_a?(Rubinius::AST::Block)
node.body.lazy_visit self, parent, true
else
emit current_indentation
emit 'return '
node.body.lazy_visit self, parent
emit ';'
end
emit "\n"
@indentation -=1
emit current_indentation
emit '}'
end
def iter19(node, parent)
iter(node, parent)
end
def block(node, parent, indent=false)
body = node.array
body.each_with_index do |expression, index|
last = body.length == index + 1
emit current_indentation if indent
emit 'return ' if last
expression.lazy_visit self, parent
emit ";"
emit "\n" unless last
end
end
def and(node, parent)
node.left.lazy_visit self, node
emit ' && '
node.right.lazy_visit self, node
end
def or(node, parent)
node.left.lazy_visit self, node
emit ' || '
node.right.lazy_visit self, node
end
def op_assign_and(node, parent)
node.left.lazy_visit self, node
emit ' && '
node.right.lazy_visit self, node
end
def op_assign_or(node, parent)
node.left.lazy_visit self, node
emit ' || '
node.right.lazy_visit self, node
end
def constant_access(node, parent)
emit node.name
end
def if(node, parent)
body, else_body = node.body, node.else
keyword = 'if ('
_unless = false
if node.body.is_a?(Rubinius::AST::NilLiteral) && !node.else.is_a?(Rubinius::AST::NilLiteral)
body, else_body = else_body, body
_unless = true
end
emit keyword
emit '!(' if _unless
node.condition.lazy_visit self, node
emit ')' if _unless
emit ') {'
emit "\n"
@indentation += 1
if body.is_a?(Rubinius::AST::Block)
body.lazy_visit self, parent, true
else
emit current_indentation
emit 'return '
body.lazy_visit self, parent
emit ';'
end
emit "\n"
if else_body.is_a?(Rubinius::AST::NilLiteral)
@indentation -=1
emit current_indentation
emit '}'
return
else
@indentation -=1
emit current_indentation
emit "}"
end
emit " else {\n"
@indentation +=1
if else_body.is_a?(Rubinius::AST::Block)
else_body.lazy_visit self, parent, true
else
emit current_indentation
else_body.lazy_visit self, parent
end
emit "\n"
@indentation -=1
emit current_indentation
emit '}'
end
def while(node, parent, reverse=false)
emit 'while ('
emit '!(' if reverse
node.condition.lazy_visit self, node
emit ')' if reverse
emit ') {'
emit "\n"
@indentation += 1
if node.body.is_a?(Rubinius::AST::Block)
node.body.lazy_visit self, parent, true
else
emit current_indentation
emit 'return '
node.body.lazy_visit self, parent
emit ';'
end
emit "\n"
@indentation -=1
emit current_indentation
emit '}'
end
def until(node, parent)
__send__ :while, node, parent, true
end
def return(node, parent)
emit 'return '
node.value.lazy_visit self, parent
end
def method_missing(m, *args, &block)
raise "RubyScript doesn't support #{m}."
end
private
def process_binary_operator(node, parent)
operators = %w(+ - * / & | <<).map(&:to_sym)
return false unless operators.include?(node.name)
return false if node.arguments.array.length != 1
operand = node.arguments.array[0]
unless node.receiver.is_a?(Rubinius::AST::Self)
node.receiver.lazy_visit self, node
end
emit ' ' << node.name.to_s << ' '
operand.lazy_visit self, node
end
end
end
unless ARGV.first
puts "RubyScript #{RubyScript::VERSION}\n================"
puts "\tUsage: rubyscript my_file.rb"
exit(1)
end
AST = Rubinius::Melbourne19.parse_file(ARGV[0])
visitor = RubyScript::Visitor.new
AST.lazy_visit(visitor, AST, true)
puts visitor.finalize
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment