Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: opal/opal
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 423af6ce6a26
Choose a base ref
...
head repository: opal/opal
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 49aaf1cf8b9f
Choose a head ref

Commits on Oct 23, 2013

  1. Copy the full SHA
    bceb785 View commit details
  2. Copy the full SHA
    9afc625 View commit details
  3. Some more node generation

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    b0fa09a View commit details
  4. Copy the full SHA
    320243e View commit details
  5. Copy the full SHA
    e5ea41e View commit details
  6. Copy the full SHA
    cda41cf View commit details
  7. Copy the full SHA
    c3489b2 View commit details
  8. Copy the full SHA
    999c542 View commit details
  9. Add some definition nodes

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    7931e88 View commit details
  10. Copy the full SHA
    65cd7ab View commit details
  11. Copy the full SHA
    447ea4b View commit details
  12. Copy the full SHA
    b028dc7 View commit details
  13. Copy the full SHA
    bddd78c View commit details
  14. Copy the full SHA
    5ac0604 View commit details
  15. masgn compiler node

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    b7a2ed4 View commit details
  16. Some more nodes

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    f668dbd View commit details
  17. Copy the full SHA
    c2b7f14 View commit details
  18. Copy the full SHA
    f3ab61e View commit details
  19. Copy the full SHA
    ab6b019 View commit details
  20. Cleanup parser a little

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    6ef6fc4 View commit details
  21. Copy the full SHA
    ecc6628 View commit details
  22. Copy the full SHA
    0a183c4 View commit details
  23. Cleanup node hierarchy

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    b78ba9e View commit details
  24. Copy the full SHA
    41e83ca View commit details
  25. Use correct opal-sprockets

    adambeynon committed Oct 23, 2013
    Copy the full SHA
    49aaf1c View commit details
Showing with 3,090 additions and 2,425 deletions.
  1. +2 −0 Gemfile
  2. +2 −2 README.md
  3. +7 −8 Rakefile
  4. +4 −10 lib/opal.rb
  5. +4 −4 lib/opal/builder.rb
  6. +9 −20 lib/opal/cli.rb
  7. +310 −0 lib/opal/compiler.rb
  8. +16 −13 lib/opal/{require_parser.rb → dependency_compiler.rb}
  9. +3 −3 lib/opal/erb.rb
  10. +25 −27 lib/opal/fragment.rb
  11. +24 −0 lib/opal/nodes.rb
  12. +54 −0 lib/opal/nodes/array.rb
  13. +150 −0 lib/opal/nodes/base.rb
  14. +11 −0 lib/opal/nodes/base_scope.rb
  15. +146 −0 lib/opal/nodes/call.rb
  16. +132 −0 lib/opal/nodes/call_special.rb
  17. +96 −0 lib/opal/nodes/case.rb
  18. +43 −0 lib/opal/nodes/class.rb
  19. +91 −0 lib/opal/nodes/constants.rb
  20. +158 −0 lib/opal/nodes/def.rb
  21. +111 −0 lib/opal/nodes/defined.rb
  22. +182 −0 lib/opal/nodes/definitions.rb
  23. +67 −0 lib/opal/nodes/hash.rb
  24. +105 −0 lib/opal/nodes/helpers.rb
  25. +60 −0 lib/opal/nodes/if.rb
  26. +118 −0 lib/opal/nodes/iter.rb
  27. +193 −0 lib/opal/nodes/literal.rb
  28. +211 −0 lib/opal/nodes/logic.rb
  29. +62 −0 lib/opal/nodes/masgn.rb
  30. +46 −0 lib/opal/nodes/module.rb
  31. +134 −0 lib/opal/nodes/rescue.rb
  32. +26 −0 lib/opal/nodes/singleton_class.rb
  33. +97 −0 lib/opal/nodes/super.rb
  34. +63 −0 lib/opal/nodes/top.rb
  35. +155 −0 lib/opal/nodes/variables.rb
  36. +65 −0 lib/opal/nodes/while.rb
  37. +84 −0 lib/opal/nodes/yield.rb
  38. +0 −2,314 lib/opal/parser.rb
  39. +6 −6 lib/opal/target_scope.rb
  40. +1 −1 opal.gemspec
  41. +9 −9 spec/opal/parser/irb_spec.rb
  42. +1 −1 spec/opal/source_map_spec.rb
  43. +2 −2 spec/parser/parse_spec.rb
  44. +3 −3 stdlib/opal-gem.js.erb
  45. +1 −1 stdlib/opal-parser.rb
  46. +1 −1 stdlib/opal-source-maps.js.erb
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
source 'https://rubygems.org'
gemspec

gem 'opal-sprockets', :github => 'opal/opal-sprockets'

# Stick with older racc until
# https://github.com/tenderlove/racc/issues/32
# is solved.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -15,11 +15,11 @@ See the website, [http://opalrb.org](http://opalrb.org).

### Compiling ruby code

`Opal.parse` is a simple interface to just compile a string of ruby into a
`Opal.compile` is a simple interface to just compile a string of ruby into a
string of javascript code.

```ruby
Opal.parse("puts 'wow'") # => "(function() { ... })()"
Opal.compile("puts 'wow'") # => "(function() { ... })()"
```

Running this by itself is not enough, you need the opal runtime/corelib.
15 changes: 7 additions & 8 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -29,22 +29,22 @@ require 'opal-sprockets'
# We can't do this at runtime, so we hijack the method (and make sure we only
# do this at the top level). We figure out which file we are including, and
# add it to our require list
class ::Opal::Parser
alias_method :mspec_process_call, :process_call
class Opal::Compiler
alias_method :mspec_handle_call, :handle_call

def process_call(sexp, level)
if sexp[1] == :language_version and @scope.top?
lang_type = sexp[2][2][1]
def handle_call(sexp)
if sexp[2] == :language_version and @scope.top?
lang_type = sexp[3][2][1]
target = "rubyspec/language/versions/#{lang_type}_1.9"

if File.exist?(target)
@requires << target
end

return f("nil")
return fragment("nil")
end

mspec_process_call sexp, level
mspec_handle_call sexp
end
end

@@ -138,7 +138,6 @@ task :build_specs do

env = SpecEnvironment.new
env.build
env.build_min
end

desc <<-DESC
14 changes: 4 additions & 10 deletions lib/opal.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
require 'opal/parser'
require 'opal/require_parser'
require 'opal/compiler'
require 'opal/dependency_compiler'
require 'opal/builder'
require 'opal/erb'
require 'opal/version'

# Opal is a ruby to javascript compiler, with a runtime for running
# in any javascript environment.
#
# Opal::Parser is the core class used for parsing ruby and generating
# javascript from its syntax tree. Opal::Processor is the main system used
# for compiling larger programs. Opal::Processor uses sprockets to maintain
# an environment of load paths, which can be used to require other ruby or
# javascript sources.
module Opal

def self.parse(source, options = {})
Parser.new.parse(source, options)
def self.compile(source, options = {})
Compiler.new.compile(source, options)
end

def self.gem_dir
8 changes: 4 additions & 4 deletions lib/opal/builder.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'opal/require_parser'
require 'opal/dependency_compiler'
require 'erb'

module Opal
@@ -70,10 +70,10 @@ def build_asset(path)
end

def compile_ruby(str, options={})
parser = RequireParser.new
result = parser.parse str, options
compiler = DependencyCompiler.new
result = compiler.compile str, options

parser.requires.each do |r|
compiler.requires.each do |r|
require_asset r
end

29 changes: 9 additions & 20 deletions lib/opal/cli.rb
Original file line number Diff line number Diff line change
@@ -108,9 +108,9 @@ def show_compiled_source
if sprockets[filename]
puts sprockets[filename].to_a.last
elsif File.exist?(filename)
puts Opal.parse File.read(filename), options
puts Opal.compile File.read(filename), options
else
puts Opal.parse(filename, options)
puts Opal.compile(filename, options)
end
end

@@ -132,9 +132,9 @@ def set_processor_options
end

def map
parser = Opal::Parser.new
parser.parse(filename, options)
parser.source_map
compiler = Opal::Compiler.new
compiler.compile(filename, options)
compiler.source_map
end

def source
@@ -153,9 +153,7 @@ def processor_options
]
end




##
# SPROCKETS

def sprockets
@@ -184,18 +182,14 @@ def require_opal_sprockets
end
end




##
# OUTPUT

def puts *args
output.puts *args
end




##
# EVALS

def evals_source
@@ -215,16 +209,11 @@ def prepare_eval_code
end
end




##
# SOURCE

def sexp
Opal::Grammar.new.parse(source)
end



end
end
310 changes: 310 additions & 0 deletions lib/opal/compiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
require 'opal/lexer'
require 'opal/grammar'
require 'opal/target_scope'
require 'opal/version'
require 'opal/fragment'
require 'opal/nodes'
require 'set'

module Opal
class Compiler
# Generated code gets indented with two spaces on each scope
INDENT = ' '

# All compare method nodes - used to optimize performance of
# math comparisons
COMPARE = %w[< > <= >=]

attr_reader :result, :fragments

# Current scope
attr_reader :scope

# Any helpers required by this file
attr_reader :helpers

# Method calls made in this file
attr_reader :method_calls

# Current case_stmt
attr_reader :case_stmt

def initialize
@line = 1
@indent = ''
@unique = 0

@method_calls = Set.new
@helpers = Set.new([:breaker, :slice])
end

# Compile some ruby code to a string.
def compile(source, options = {})
@source = source
setup_options options

@sexp = Grammar.new.parse(@source, @file)

top_node = Nodes::TopNode.new(@sexp, :expr, self)
@fragments = top_node.compile_to_fragments.flatten

@result = @fragments.map(&:code).join('')
end

def setup_options(options = {})
@file = (options[:file] || '(file)')
@source_file = (options[:source_file] || @file)
@method_missing = (options[:method_missing] != false)
@arity_check = (options[:arity_check])
@const_missing = (options[:const_missing] == true)
@irb_vars = (options[:irb] == true)
end

# Is method_missing enabled for this file
def method_missing?
@method_missing
end

# const_missing enabled or not
def const_missing?
@const_missing
end

def arity_check?
@arity_check
end

# Are top level irb style vars enabled
def irb_vars?
@irb_vars
end

def source_map
Opal::SourceMap.new(@fragments, '(file)')
end

# This is called when a parsing/processing error occurs. This
# method simply appends the filename and curent line number onto
# the message and raises it.
def error(msg)
raise SyntaxError, "#{msg} :#{@file}:#{@line}"
end

# This is called when a parsing/processing warning occurs. This
# method simply appends the filename and curent line number onto
# the message and issues a warning.
def warning(msg)
warn "#{msg} :#{@file}:#{@line}"
end

# Instances of `Scope` can use this to determine the current
# scope indent. The indent is used to keep generated code easily
# readable.
def parser_indent
@indent
end

# Create a new sexp using the given parts. Even though this just
# returns an array, it must be used incase the internal structure
# of sexps does change.
def s(*parts)
sexp = Sexp.new(parts)
sexp.line = @line
sexp
end

def fragment(str, sexp = nil)
Fragment.new(str, sexp)
end

# Used to generate a unique id name per file. These are used
# mainly to name method bodies for methods that use blocks.
def unique_temp
"TMP_#{@unique += 1}"
end

# Use the given helper
def helper(name)
@helpers << name
end

# Every time the parser enters a new scope, this is called with
# the scope type as an argument. Valid types are `:top` for the
# top level/file scope; `:class`, `:module` and `:sclass` for the
# obvious ruby classes/modules; `:def` and `:iter` for methods
# and blocks respectively.
#
# This method just pushes a new instance of `Opal::Scope` onto the
# stack, sets the new scope as the `@scope` variable, and yields
# the given block. Once the block returns, the old scope is put
# back on top of the stack.
def in_scope(type)
return unless block_given?

parent = @scope
@scope = TargetScope.new(type, self).tap { |s| s.parent = parent }
yield @scope

@scope = parent
end

# To keep code blocks nicely indented, this will yield a block after
# adding an extra layer of indent, and then returning the resulting
# code after reverting the indent.
def indent(&block)
indent = @indent
@indent += INDENT
@space = "\n#@indent"
res = yield
@indent = indent
@space = "\n#@indent"
res
end

# Temporary varibales will be needed from time to time in the
# generated code, and this method will assign (or reuse) on
# while the block is yielding, and queue it back up once it is
# finished. Variables are queued once finished with to save the
# numbers of variables needed at runtime.
def with_temp(&block)
tmp = @scope.new_temp
res = yield tmp
@scope.queue_temp tmp
res
end

# Used when we enter a while statement. This pushes onto the current
# scope's while stack so we know how to handle break, next etc.
def in_while
return unless block_given?
@while_loop = @scope.push_while
result = yield
@scope.pop_while

result
end

def in_case
return unless block_given?
old = @case_stmt
@case_stmt = {}
yield
@case_stmt = old
end

# Returns true if the parser is curently handling a while sexp,
# false otherwise.
def in_while?
@scope.in_while?
end

# Process the given sexp by creating a node instance, based on its type,
# and compiling it to fragments.
def process(sexp, level = :expr)
if handler = handlers[sexp.type]
@line = sexp.line
return handler.new(sexp, level, self).compile_to_fragments
else
raise "Unsupported sexp: #{sexp.type}"
end
end

def handlers
@handlers ||= Opal::Nodes::Base.handlers
end

# Handle "special" method calls, e.g. require(). Subclasses can override
# this method. If this method returns nil, then the method will continue
# to be generated by CallNode.
def handle_call(sexp)
nil
end

# The last sexps in method bodies, for example, need to be returned
# in the compiled javascript. Due to syntax differences between
# javascript any ruby, some sexps need to be handled specially. For
# example, `if` statemented cannot be returned in javascript, so
# instead the "truthy" and "falsy" parts of the if statement both
# need to be returned instead.
#
# Sexps that need to be returned are passed to this method, and the
# alterned/new sexps are returned and should be used instead. Most
# sexps can just be added into a s(:return) sexp, so that is the
# default action if no special case is required.
def returns(sexp)
return returns s(:nil) unless sexp

case sexp.type
when :break, :next, :redo
sexp
when :yield
sexp[0] = :returnable_yield
sexp
when :scope
sexp[1] = returns sexp[1]
sexp
when :block
if sexp.length > 1
sexp[-1] = returns sexp[-1]
else
sexp << returns(s(:nil))
end
sexp
when :when
sexp[2] = returns(sexp[2])
sexp
when :rescue
sexp[1] = returns sexp[1]

if sexp[2] and sexp[2][0] == :resbody
if sexp[2][2]
sexp[2][2] = returns sexp[2][2]
else
sexp[2][2] = returns s(:nil)
end
end
sexp
when :ensure
sexp[1] = returns sexp[1]
sexp
when :begin
sexp[1] = returns sexp[1]
sexp
when :rescue_mod
sexp[1] = returns sexp[1]
sexp[2] = returns sexp[2]
sexp
when :while
# sexp[2] = returns(sexp[2])
sexp
when :return, :js_return
sexp
when :xstr
sexp[1] = "return #{sexp[1]};" unless /return|;/ =~ sexp[1]
sexp
when :dxstr
sexp[1] = "return #{sexp[1]}" unless /return|;|\n/ =~ sexp[1]
sexp
when :if
sexp[2] = returns(sexp[2] || s(:nil))
sexp[3] = returns(sexp[3] || s(:nil))
sexp
else
s(:js_return, sexp).tap { |s|
s.line = sexp.line
}
end
end

def js_block_given(sexp, level)
@scope.uses_block!
if @scope.block_name
fragment("(#{@scope.block_name} !== nil)", sexp)
elsif scope = @scope.find_parent_def and scope.block_name
fragment("(#{scope.block_name} !== nil)", sexp)
else
fragment("false", sexp)
end
end
end
end
29 changes: 16 additions & 13 deletions lib/opal/require_parser.rb → lib/opal/dependency_compiler.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
require 'opal/parser'
require 'opal/compiler'

module Opal
# A parser which collects all require() statments for dependency building
class RequireParser < Parser
def self.parse source, options = {}
self.new.parse source, options
class DependencyCompiler < Compiler
def self.compile source, options = {}
self.new.compile source, options
end

# Holds an array of paths which this file 'requires'.
# @return Array<String>
attr_reader :requires

def parse source, options = {}
def compile(source, options = {})
@requires = []
@dynamic_require_severity = (options[:dynamic_require_severity] || :error)
super source, options
end

def process_call sexp, level
if sexp[1] == :require
return handle_require sexp[2][1]
elsif sexp[1] == :autoload and @scope.class_scope?
return handle_require sexp[2][2]
def handle_call(sexp)
case sexp[2]
when :require
return handle_require(sexp[3][1])
when :autoload
if @scope.class_scope?
return handle_require(sexp[3][2])
end
else
super sexp
end

super sexp, level
end

def handle_require(sexp)
str = handle_require_sexp sexp
@requires << str unless str.nil? if @requires
f("", sexp)
fragment("", sexp)
end

def handle_require_sexp(sexp)
6 changes: 3 additions & 3 deletions lib/opal/erb.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'opal/parser'
require 'opal/compiler'

module Opal
module ERB
def self.parse(source, file_name = '(erb)')
def self.compile(source, file_name = '(erb)')
Compiler.new.compile source, file_name
end

@@ -15,7 +15,7 @@ def compile(source, file_name = '(erb)')
self.find_code
self.wrap_compiled

Opal.parse @result
Opal.compile @result
end

def fix_quotes
52 changes: 25 additions & 27 deletions lib/opal/fragment.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
module Opal
class Parser
# A fragment holds a string of generated javascript that will be written
# to the destination. It also keeps hold of the original sexp from which
# it was generated. Using this sexp, when writing fragments in order, a
# mapping can be created of the original location => target location,
# aka, source-maps!
class Fragment
# String of javascript this fragment holds
attr_reader :code
# A fragment holds a string of generated javascript that will be written
# to the destination. It also keeps hold of the original sexp from which
# it was generated. Using this sexp, when writing fragments in order, a
# mapping can be created of the original location => target location,
# aka, source-maps!
class Fragment
# String of javascript this fragment holds
attr_reader :code

def initialize(code, sexp = nil)
@code = code.to_s
@sexp = sexp
end
def initialize(code, sexp = nil)
@code = code.to_s
@sexp = sexp
end

# In debug mode we may wish to include the original line as a comment
def to_code
if @sexp
"/*:#{@sexp.line}*/#{@code}"
else
@code
end
# In debug mode we may wish to include the original line as a comment
def to_code
if @sexp
"/*:#{@sexp.line}*/#{@code}"
else
@code
end
end

# inspect the contents of this fragment, f("fooo")
def inspect
"f(#{@code.inspect})"
end
# inspect the contents of this fragment, f("fooo")
def inspect
"f(#{@code.inspect})"
end

def line
@sexp.line if @sexp
end
def line
@sexp.line if @sexp
end
end
end
24 changes: 24 additions & 0 deletions lib/opal/nodes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'opal/nodes/base'
require 'opal/nodes/literal'
require 'opal/nodes/variables'
require 'opal/nodes/constants'
require 'opal/nodes/call'
require 'opal/nodes/call_special'
require 'opal/nodes/module'
require 'opal/nodes/class'
require 'opal/nodes/singleton_class'
require 'opal/nodes/iter'
require 'opal/nodes/def'
require 'opal/nodes/if'
require 'opal/nodes/logic'
require 'opal/nodes/definitions'
require 'opal/nodes/yield'
require 'opal/nodes/rescue'
require 'opal/nodes/case'
require 'opal/nodes/super'
require 'opal/nodes/top'
require 'opal/nodes/while'
require 'opal/nodes/hash'
require 'opal/nodes/array'
require 'opal/nodes/defined'
require 'opal/nodes/masgn'
54 changes: 54 additions & 0 deletions lib/opal/nodes/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'opal/nodes/array'

module Opal
module Nodes
class ArrayNode < Base
handle :array

def compile
return push('[]') if children.empty?

code, work = [], []

children.each do |child|
splat = child.type == :splat
part = expr(child)

if splat
if work.empty?
if code.empty?
code << fragment("[].concat(") << part << fragment(")")
else
code << fragment(".concat(") << part << fragment(")")
end
else
if code.empty?
code << fragment("[") << work << fragment("]")
else
code << fragment(".concat([") << work << fragment("])")
end

code << fragment(".concat(") << part << fragment(")")
end
work = []
else
work << fragment(", ") unless work.empty?
work << part
end
end

unless work.empty?
join = [fragment("["), work, fragment("]")]

if code.empty?
code = join
else
code.push([fragment(".concat("), join, fragment(")")])
end
end

push code
end
end
end
end
150 changes: 150 additions & 0 deletions lib/opal/nodes/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
require 'opal/nodes/helpers'

module Opal
module Nodes
class Base
include Helpers

def self.handlers
@handlers ||= {}
end

def self.handle(*types)
types.each do |type|
Base.handlers[type] = self
end
end

def self.children(*names)
names.each_with_index do |name, idx|
define_method(name) do
@sexp[idx + 1]
end
end
end

attr_reader :compiler

def initialize(sexp, level, compiler)
@sexp = sexp
@level = level
@compiler = compiler
end

def type
@sexp.type
end

def children
@sexp[1..-1]
end

def compile_to_fragments
return @fragments if @fragments

@fragments = []
self.compile
@fragments
end

def compile
raise "Not Implemented"
end

def push(*strs)
strs.each do |str|
str = fragment(str) if str.is_a?(String)
@fragments << str
end
end

def unshift(*strs)
strs.reverse.each do |str|
str = fragment(str) if str.is_a?(String)
@fragments.unshift str
end
end

def wrap(pre, post)
unshift pre
push post
end

def fragment(str)
Opal::Fragment.new str, @sexp
end

def error(msg)
@compiler.error msg
end

def scope
@compiler.scope
end

def s(*args)
@compiler.s(*args)
end

def expr?
@level == :expr
end

def recv?
@level == :recv
end

def stmt?
@level == :stmt
end

def process(sexp, level = :expr)
@compiler.process sexp, level
end

def expr(sexp)
@compiler.process sexp, :expr
end

def recv(sexp)
@compiler.process sexp, :recv
end

def stmt(sexp)
@compiler.process sexp, :stmt
end

def expr_or_nil(sexp)
sexp ? expr(sexp) : "nil"
end

def add_local(name)
scope.add_local name.to_sym
end

def add_ivar(name)
scope.add_ivar name
end

def add_temp(temp)
scope.add_temp temp
end

def helper(name)
@compiler.helper name
end

def with_temp(&block)
@compiler.with_temp(&block)
end

def in_while?
@compiler.in_while?
end

def while_loop
@compiler.instance_variable_get(:@while_loop)
end
end
end
end
11 changes: 11 additions & 0 deletions lib/opal/nodes/base_scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'opal/nodes/base'

module Opal
module Nodes
class BaseScopeNode < Base
def in_scope(type, &block)
indent { compiler.in_scope(type, &block) }
end
end
end
end
146 changes: 146 additions & 0 deletions lib/opal/nodes/call.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
require 'opal/nodes/base'

module Opal
module Nodes
class CallNode < Base
handle :call

children :recvr, :meth, :arglist, :iter

def compile
if handled = compiler.handle_call(@sexp)
push handled
return
end

mid = mid_to_jsid meth.to_s

compiler.method_calls << meth.to_sym

# trying to access an lvar in irb mode
if using_irb?
with_temp do |tmp|
lvar = variable(meth)
call = s(:call, s(:self), meth.intern, s(:arglist))
push "((#{tmp} = $opal.irb_vars.#{lvar}) == null ? ", expr(call), " : #{tmp})"
end

return
end

case meth
when :block_given?
return push @compiler.js_block_given(@sexp, @level)
when :__method__, :__callee__
if scope.def?
return push(scope.mid.to_s.inspect)
else
return push("nil")
end
end

splat = arglist[1..-1].any? { |a| a.first == :splat }

if Sexp === arglist.last and arglist.last.type == :block_pass
block = expr(arglist.pop)
elsif iter
block = expr(iter)
end

tmpfunc = scope.new_temp if block
tmprecv = scope.new_temp if splat || tmpfunc

recv_code = recv(recv_sexp)
call_recv = s(:js_tmp, tmprecv || recv_code)

if tmpfunc and !splat
arglist.insert 1, call_recv
end

args = expr(arglist)

if tmprecv
push "(#{tmprecv} = ", recv_code, ")#{mid}"
else
push recv_code, mid
end

if tmpfunc
unshift "(#{tmpfunc} = "
push ", #{tmpfunc}._p = ", block, ", #{tmpfunc})"
end

if splat
push ".apply("
push(tmprecv || recv_code)
push ", ", args, ")"
elsif tmpfunc
push ".call(", args, ")"
else
push "(", args, ")"
end

scope.queue_temp tmpfunc if tmpfunc
end

def recv_sexp
recvr || s(:self)
end

def using_irb?
@compiler.irb_vars? and scope.top? and arglist == s(:arglist) and recvr.nil? and iter.nil?
end
end

# FIXME: needs rewrite
class ArglistNode < Base
handle :arglist

def compile
code, work = [], []

children.each do |current|
splat = current.first == :splat
arg = expr(current)

if splat
if work.empty?
if code.empty?
code << fragment("[].concat(")
code << arg
code << fragment(")")
else
code += ".concat(#{arg})"
end
else
if code.empty?
code << [fragment("["), work, fragment("]")]
else
code << [fragment(".concat(["), work, fragment("])")]
end

code << [fragment(".concat("), arg, fragment(")")]
end

work = []
else
work << fragment(", ") unless work.empty?
work << arg
end
end

unless work.empty?
join = work

if code.empty?
code = join
else
code << fragment(".concat(") << join << fragment(")")
end
end

push(*code)
end
end
end
end
132 changes: 132 additions & 0 deletions lib/opal/nodes/call_special.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
require 'opal/nodes/base'

module Opal
module Nodes
# recv.mid = rhs
# s(:recv, :mid=, s(:arglist, rhs))
class AttrAssignNode < Base
handle :attrasgn

children :recvr, :mid, :arglist

def compile
sexp = s(:call, recvr, mid, arglist)
push process(sexp, @level)
end
end

# lhs =~ rhs
# s(:match3, lhs, rhs)
class Match3Node < Base
handle :match3

children :lhs, :rhs

def compile
sexp = s(:call, lhs, :=~, s(:arglist, rhs))
push process(sexp, @level)
end
end

# a ||= rhs
# s(:op_asgn_or, s(:lvar, :a), s(:lasgn, :a, rhs))
class OpAsgnOrNode < Base
handle :op_asgn_or

children :recvr, :rhs

def compile
sexp = s(:or, recvr, rhs)
push expr(sexp)
end
end

# a &&= rhs
# s(:op_asgn_and, s(:lvar, :a), s(:lasgn, a:, rhs))
class OpAsgnAndNode < Base
handle :op_asgn_and

children :recvr, :rhs

def compile
sexp = s(:and, recvr, rhs)
push expr(sexp)
end
end

# lhs[args] ||= rhs
# s(:op_asgn1, lhs, args, :||, rhs)
class OpAsgn1Node < Base
handle :op_asgn1

children :lhs, :args, :op, :rhs

def first_arg
args[1]
end

def compile
with_temp do |a| # args
with_temp do |r| # recv
aref = s(:call, s(:js_tmp, r), :[], s(:arglist, s(:js_tmp, a)))
aset = s(:call, s(:js_tmp, r), :[]=, s(:arglist, s(:js_tmp, a), rhs))
orop = s(:or, aref, aset)

push "(#{a} = ", expr(first_arg), ", #{r} = ", expr(lhs)
push ", ", expr(orop), ")"
end
end
end
end

# lhs.b += rhs
# s(:op_asgn2, lhs, :b=, :+, rhs)
class OpAsgn2Node < Base
handle :op_asgn2

children :lhs, :mid, :op, :rhs

def meth
mid.to_s[0..-2]
end

def compile
case op.to_s
when '||' then compile_or
when '&&' then compile_and
else compile_operator
end
end

def compile_or
with_temp do |tmp|
getr = s(:call, s(:js_tmp, tmp), meth, s(:arglist))
asgn = s(:call, s(:js_tmp, tmp), mid, s(:arglist, rhs))
orop = s(:or, getr, asgn)

push "(#{tmp} = ", expr(lhs), ", ", expr(orop), ")"
end
end

def compile_and
with_temp do |tmp|
getr = s(:call, s(:js_tmp, tmp), meth, s(:arglist))
asgn = s(:call, s(:js_tmp, tmp), mid, s(:arglist, rhs))
andop = s(:and, getr, asgn)

push "(#{tmp} = ", expr(lhs), ", ", expr(andop), ")"
end
end

def compile_operator
with_temp do |tmp|
getr = s(:call, s(:js_tmp, tmp), meth, s(:arglist))
oper = s(:call, getr, op, s(:arglist, rhs))
asgn = s(:call, s(:js_tmp, tmp), mid, s(:arglist, oper))

push "(#{tmp} = ", expr(lhs), ", ", expr(asgn), ")"
end
end
end
end
end
96 changes: 96 additions & 0 deletions lib/opal/nodes/case.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'opal/nodes/base'

module Opal
module Nodes
class CaseNode < Base
handle :case

children :condition

def compile
handled_else = false

compiler.in_case do
if condition
case_stmt[:cond] = true
add_local '$case'

push "$case = ", expr(condition), ";"
end

case_parts.each_with_index do |wen, idx|
if wen and wen.type == :when
compiler.returns(wen) if needs_closure?
push "else " unless idx == 0
push stmt(wen)
elsif wen # s(:else)
handled_else = true
wen = compiler.returns(wen) if needs_closure?
push "else {", stmt(wen), "}"
end
end

# if we are having a closure, we must return a usable value
if needs_closure? and !handled_else
push "else { return nil }"
end

wrap '(function() {', '})()' if needs_closure?
end
end

def needs_closure?
!stmt?
end

def case_parts
children[1..-1]
end

def case_stmt
compiler.case_stmt
end
end

class WhenNode < Base
handle :when

children :whens, :body

def compile
push "if ("

when_checks.each_with_index do |check, idx|
push ' || ' unless idx == 0

if check.type == :splat
push "(function($splt) { for (var i = 0; i < $splt.length; i++) {"
push "if ($splt[i]['$===']($case)) { return true; }"
push "} return false; })(", expr(check[1]), ")"
else
if case_stmt[:cond]
call = s(:call, check, :===, s(:arglist, s(:js_tmp, '$case')))
push expr(call)
else
push js_truthy(check)
end
end
end

push ") {", process(body_code, @level), "}"
end

def when_checks
whens.children
end

def case_stmt
compiler.case_stmt
end

def body_code
body || s(:nil)
end
end
end
end
43 changes: 43 additions & 0 deletions lib/opal/nodes/class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'opal/nodes/module'

module Opal
module Nodes
class ClassNode < ModuleNode
handle :class

children :cid, :sup, :body

def compile
name, base = name_and_base
helper :klass

push "(function($base, $super) {"
line " function #{name}(){};"
line " var self = #{name} = $klass($base, $super, '#{name}', #{name});"

in_scope(:class) do
scope.name = name
add_temp "#{scope.proto} = #{name}._proto"
add_temp "$scope = #{name}._scope"

body_code = self.body_code
empty_line

line scope.to_vars
line body_code
end

line "})(", base, ", ", self.super_code, ")"
end

def super_code
sup ? expr(sup) : 'null'
end

def body_code
body[1] = s(:nil) unless body[1]
stmt(compiler.returns(body))
end
end
end
end
91 changes: 91 additions & 0 deletions lib/opal/nodes/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require 'opal/nodes/base'

module Opal
module Nodes
class ConstNode < Base
handle :const

children :name

def compile
if compiler.const_missing?
with_temp do |tmp|
push "((#{tmp} = $scope.#{name}) == null ? $opal.cm('#{name}') : #{tmp})"
end
else
push "$scope.#{name}"
end
end
end

class ConstDeclarationNode < Base
handle :cdecl

children :name, :base

def compile
push expr(base)
wrap "$opal.cdecl($scope, '#{name}', ", ")"
end
end

class ConstAssignNode < Base
handle :casgn

children :base, :name, :value

def compile
push "$opal.casgn("
push expr(base)
push ", '#{name}', "
push expr(value)
push ")"
end
end

class ConstGetNode < Base
handle :colon2

children :base, :name

def compile
if compiler.const_missing?
with_temp do |tmp|
push "((#{tmp} = ("
push expr(base)
push ")._scope).#{name} == null ? #{tmp}.cm('#{name}') : "
push "#{tmp}.#{name})"
end
else
push expr(base)
wrap '(', ")._scope.#{name}"
end
end
end

class TopConstNode < Base
handle :colon3

children :name

def compile
with_temp do |tmp|
push "((#{tmp} = $opal.Object._scope.#{name}) == null ? "
push "$opal.cm('#{name}') : #{tmp})"
end
end
end

class TopConstAssignNode < Base
handle :casgn3

children :name, :value

def compile
push "$opal.casgn($opal.Object, '#{name}', "
push expr(value)
push ")"
end
end
end
end
158 changes: 158 additions & 0 deletions lib/opal/nodes/def.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
require 'opal/nodes/base_scope'

module Opal
module Nodes
# FIXME: needs rewrite
class DefNode < BaseScopeNode
handle :def

children :recvr, :mid, :args, :stmts

def compile
jsid = mid_to_jsid mid.to_s
params = nil
scope_name = nil

# opt args if last arg is sexp
opt = args.pop if Sexp === args.last

argc = args.length - 1

# block name (&block)
if args.last.to_s.start_with? '&'
block_name = args.pop.to_s[1..-1].to_sym
argc -= 1
end

# splat args *splat
if args.last.to_s.start_with? '*'
uses_splat = true
if args.last == :*
argc -= 1
else
splat = args[-1].to_s[1..-1].to_sym
args[-1] = splat
argc -= 1
end
end

if compiler.arity_check?
arity_code = arity_check(args, opt, uses_splat, block_name, mid)
end

in_scope(:def) do
scope.mid = mid
scope.defs = true if recvr

if block_name
scope.uses_block!
scope.add_arg block_name
end

yielder = block_name || '$yield'
scope.block_name = yielder

params = process(args)
stmt_code = stmt(stmts)

add_temp 'self = this'

line "#{splat} = $slice.call(arguments, #{argc});" if splat

scope_name = scope.identity

if scope.uses_block?
add_temp "$iter = #{scope_name}._p"
add_temp "#{yielder} = $iter || nil"
end

opt[1..-1].each do |o|
next if o[2][2] == :undefined
line "if (#{variable(o[1])} == null) {"
line ' ', expr(o)
line "}"
end if opt

if scope.uses_block?
line "#{scope_name}._p = null;"
end

unshift "\n#{current_indent}", scope.to_vars
line stmt_code

unshift arity_code if arity_code

unshift "var $zuper = $slice.call(arguments, 0);" if scope.uses_zuper

if scope.catch_return
unshift "try {\n"
line "} catch ($returner) { if ($returner === $opal.returner) { return $returner.$v }"
push " throw $returner; }"
end
end

unshift ") {"
unshift(params)
unshift "function("
unshift "#{scope_name} = " if scope_name
line "}"

if recvr
unshift '$opal.defs(', recv(recvr), ", '$#{mid}', "
push ')'
elsif scope.class? and %w(Object BasicObject).include?(scope.name)
wrap "$opal.defn(self, '$#{mid}', ", ')'
elsif scope.class_scope?
scope.methods << "$#{mid}"
unshift "#{scope.proto}#{jsid} = "
elsif scope.iter?
wrap "$opal.defn(self, '$#{mid}', ", ')'
elsif scope.type == :sclass
unshift "self._proto#{jsid} = "
elsif scope.top?
unshift "$opal.Object._proto#{jsid} = "
else
unshift "def#{jsid} = "
end

wrap '(', ', nil)' if expr?
end

# Returns code used in debug mode to check arity of method call
def arity_check(args, opt, splat, block_name, mid)
meth = mid.to_s.inspect

arity = args.size - 1
arity -= (opt.size - 1) if opt
arity -= 1 if splat
arity = -arity - 1 if opt or splat

# $arity will point to our received arguments count
aritycode = "var $arity = arguments.length;"

if arity < 0 # splat or opt args
aritycode + "if ($arity < #{-(arity + 1)}) { $opal.ac($arity, #{arity}, this, #{meth}); }"
else
aritycode + "if ($arity !== #{arity}) { $opal.ac($arity, #{arity}, this, #{meth}); }"
end
end
end

# def args list
class ArgsNode < Base
handle :args

def compile
children.each_with_index do |child, idx|
next if child.to_s == '*'

child = child.to_sym
push ', ' unless idx == 0
child = variable(child)
scope.add_arg child.to_sym
push child.to_s
end
end
end
end
end
111 changes: 111 additions & 0 deletions lib/opal/nodes/defined.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
require 'opal/nodes/base'

module Opal
module Nodes
class DefinedNode < Base
handle :defined

children :value

def compile
type = value.type

case type
when :self, :nil, :false, :true
push type.to_s.inspect
when :lasgn, :iasgn, :gasgn, :cvdecl, :masgn, :op_asgn_or, :op_asgn_and
push "'assignment'"
when :paren, :not
push expr(s(:defined, value[1]))
when :lvar
push "'local-variable'"
else
if respond_to? "compile_#{type}"
__send__ "compile_#{type}"
else
push "'expression'"
end
end
end

def compile_call
mid = mid_to_jsid value[2].to_s
recv = value[1] ? expr(value[1]) : 'self'

push '(', recv, "#{mid} || ", recv
push "['$respond_to_missing?']('#{value[2].to_s}') ? 'method' : nil)"
end

def compile_ivar
# FIXME: this check should be positive for ivars initialized as nil too.
# Since currently all known ivars are inialized to nil in the constructor
# we can't tell if it was the user that put nil and made the ivar #defined?
# or not.
with_temp do |tmp|
name = value[1].to_s[1..-1]

push "((#{tmp} = self['#{name}'], #{tmp} != null && #{tmp} !== nil) ? "
push "'instance-variable' : nil)"
end
end

def compile_super
push expr(s(:defined_super, value))
end

def compile_yield
push compiler.js_block_given(@sexp, @level)
wrap '((', ') != null ? "yield" : nil)'
end

def compile_xstr
push expr(value)
wrap '(typeof(', ') !== "undefined")'
end
alias compile_dxstr compile_xstr

def compile_const
push "($scope.#{value[1]} != null)"
end

def compile_colon2
# TODO: avoid try/catch, probably a #process_colon2 alternative that
# does not raise errors is needed
push "(function(){ try { return (("
push expr(value)
push ") != null ? 'constant' : nil); } catch (err) { if (err._klass"
push " === Opal.NameError) { return nil; } else { throw(err); }}; })()"
end

def compile_colon3
push "($opal.Object._scope.#{value[1]} == null ? nil : 'constant')"
end

def compile_cvar
push "($opal.cvars['#{value[1]}'] != null ? 'class variable' : nil)"
end

def compile_gvar
name = value[1].to_s[1..-1]

if %w[~ !].include? name
push "'global-variable'"
elsif %w[` ' + &].include? name
with_temp do |tmp|
push "((#{tmp} = $gvars['~'], #{tmp} != null && #{tmp} !== nil) ? "
push "'global-variable' : nil)"
end
else
push "($gvars[#{name.inspect}] != null ? 'global-variable' : nil)"
end
end

def compile_nth_ref
with_temp do |tmp|
push "((#{tmp} = $gvars['~'], #{tmp} != null && #{tmp} != nil) ? "
push "'global-variable' : nil)"
end
end
end
end
end
182 changes: 182 additions & 0 deletions lib/opal/nodes/definitions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
require 'opal/nodes/base'

module Opal
module Nodes

class SvalueNode < Base
handle :svalue

children :value

def compile
push process(value, @level)
end
end

# :scope nodes are actually inside scopes (e.g. :module, :class).
# These are not actually the scopes themselves.
class ScopeNode < Base
handle :scope

children :body

def compile
body = self.body || s(:nil)
body = compiler.returns(body) unless scope.class_scope?
push stmt(body)
end
end

class UndefNode < Base
handle :undef

children :mid

# FIXME: we should be setting method to a stub method here
def compile
push "delete #{scope.proto}#{mid_to_jsid mid[1].to_s}"
end
end

class AliasNode < Base
handle :alias

children :new_name, :old_name

def new_mid
mid_to_jsid new_name[1].to_s
end

def old_mid
mid_to_jsid old_name[1].to_s
end

def compile
if scope.class? or scope.module?
scope.methods << "$#{new_name[1]}"
push "$opal.defn(self, '$#{new_name[1]}', #{scope.proto}#{old_mid})"
else
push "self._proto#{new_mid} = self._proto#{old_mid}"
end
end
end

class BeginNode < Base
handle :begin

children :body

def compile
if !stmt? and body.type == :block
push stmt(compiler.returns(body))
wrap '(function() {', '})()'
else
push process(body, @level)
end
end
end

class ParenNode < Base
handle :paren

children :body

def compile
if body.type == :block
body.children.each_with_index do |child, idx|
push ', ' unless idx == 0
push expr(child)
end

wrap '(', ')'
else
push process(body, @level)
wrap '(', ')' unless stmt?
end
end
end

class BlockNode < Base
handle :block

def compile
return push "nil" if children.empty?

children.each_with_index do |child, idx|
push stmt_join unless idx == 0

if yasgn = find_inline_yield(child)
push compiler.process(yasgn, @level)
push ";"
end

push compiler.process(child, @level)
push ";" if child_is_expr?(child)
end
end

def stmt_join
scope.class_scope? ? "\n\n#{current_indent}" : "\n#{current_indent}"
end

def child_is_expr?(child)
raw_expression?(child) and [:stmt, :stmt_closure].include?(@level)
end

def raw_expression?(child)
![:xstr, :dxstr].include?(child.type)
end

# When a block sexp gets generated, any inline yields (i.e. yield
# statements that are not direct members of the block) need to be
# generated as a top level member. This is because if a yield
# is returned by a break statement, then the method must return.
#
# As inline expressions in javascript cannot return, the block
# must be rewritten.
#
# For example, a yield inside an array:
#
# [1, 2, 3, yield(4)]
#
# Must be rewitten into:
#
# tmp = yield 4
# [1, 2, 3, tmp]
#
# This rewriting happens on sexps directly.
#
# @param [Sexp] stmt sexps to (maybe) rewrite
# @return [Sexp]
def find_inline_yield(stmt)
found = nil
case stmt.first
when :js_return
if found = find_inline_yield(stmt[1])
found = found[2]
end
when :array
stmt[1..-1].each_with_index do |el, idx|
if el.first == :yield
found = el
stmt[idx+1] = s(:js_tmp, '$yielded')
end
end
when :call
arglist = stmt[3]
arglist[1..-1].each_with_index do |el, idx|
if el.first == :yield
found = el
arglist[idx+1] = s(:js_tmp, '$yielded')
end
end
end

if found
scope.add_temp '$yielded' unless scope.has_temp? '$yielded'
s(:yasgn, '$yielded', found)
end
end
end
end
end
67 changes: 67 additions & 0 deletions lib/opal/nodes/hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'opal/nodes/base'

module Opal
module Nodes
class HashNode < Base
handle :hash

def keys_and_values
keys, values = [], []

children.each_with_index do |obj, idx|
if idx.even?
keys << obj
else
values << obj
end
end

[keys, values]
end

def simple_keys?(keys)
keys.all? { |key| [:sym, :str].include? key.type }
end

def compile
keys, values = keys_and_values

if simple_keys? keys
compile_hash2 keys, values
else
compile_hash
end
end

def compile_hash
helper :hash

children.each_with_index do |child, idx|
push ', ' unless idx == 0
push expr(child)
end

wrap '$hash(', ')'
end

def compile_hash2(keys, values)
hash_obj, hash_keys = {}, []
helper :hash2

keys.size.times do |idx|
key = keys[idx][1].to_s.inspect
hash_keys << key unless hash_obj.include? key
hash_obj[key] = expr(values[idx])
end

hash_keys.each_with_index do |key, idx|
push ', ' unless idx == 0
push "#{key}: "
push hash_obj[key]
end

wrap "$hash2([#{hash_keys.join ', '}], {", "})"
end
end
end
end
105 changes: 105 additions & 0 deletions lib/opal/nodes/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
module Opal
module Nodes
module Helpers

# Reserved javascript keywords - we cannot create variables with the
# same name
RESERVED = %w[
break case catch continue debugger default delete do else finally for
function if in instanceof new return switch this throw try typeof var let
void while with class enum export extends import super true false native
const static
]

def property(name)
reserved?(name) ? "['#{name}']" : ".#{name}"
end

def reserved?(name)
RESERVED.include? name
end

def variable(name)
reserved?(name.to_s) ? "#{name}$" : name
end

# Converts a ruby lvar/arg name to a js identifier. Not all ruby names
# are valid in javascript. A $ suffix is added to non-valid names.
# varibales
def lvar_to_js(var)
var = "#{var}$" if RESERVED.include? var.to_s
var.to_sym
end

# Converts a ruby method name into its javascript equivalent for
# a method/function call. All ruby method names get prefixed with
# a '$', and if the name is a valid javascript identifier, it will
# have a '.' prefix (for dot-calling), otherwise it will be
# wrapped in brackets to use reference notation calling.
def mid_to_jsid(mid)
if /\=|\+|\-|\*|\/|\!|\?|\<|\>|\&|\||\^|\%|\~|\[/ =~ mid.to_s
"['$#{mid}']"
else
'.$' + mid
end
end

def indent(&block)
compiler.indent(&block)
end

def current_indent
compiler.parser_indent
end

def line(*strs)
push "\n#{current_indent}"
push(*strs)
end

def empty_line
push "\n"
end

def js_truthy(sexp)
if optimize = js_truthy_optimize(sexp)
return optimize
end

with_temp do |tmp|
[fragment("(#{tmp} = "), expr(sexp), fragment(") !== false && #{tmp} !== nil")]
end
end

def js_falsy(sexp)
if sexp.type == :call
mid = sexp[2]
if mid == :block_given?
scope.uses_block!
return "#{scope.block_name} === nil"
end
end

with_temp do |tmp|
[fragment("(#{tmp} = "), expr(sexp), fragment(") === false || #{tmp} === nil")]
end
end

def js_truthy_optimize(sexp)
if sexp.type == :call
mid = sexp[2]

if mid == :block_given?
expr(sexp)
elsif Compiler::COMPARE.include? mid.to_s
expr(sexp)
elsif mid == :"=="
expr(sexp)
end
elsif [:lvar, :self].include? sexp.type
[expr(sexp.dup), fragment(" !== false && "), expr(sexp.dup), fragment(" !== nil")]
end
end
end
end
end
60 changes: 60 additions & 0 deletions lib/opal/nodes/if.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'opal/nodes/base'

module Opal
module Nodes
class IfNode < Base
handle :if

children :test, :true_body, :false_body

def compile
truthy, falsy = self.truthy, self.falsy

push "if ("

# optimize unless (we don't want a else() unless we need to)
if falsy and !truthy
truthy = falsy
falsy = nil
push js_falsy(test)
else
push js_truthy(test)
end

push ") {"

# skip if-body if no truthy sexp
indent { line stmt(truthy) } if truthy

if falsy
if falsy.type == :if
line "} else ", stmt(falsy)
else
indent do
line "} else {"
line stmt(falsy)
end

line "}"
end
else
push "}"
end

wrap "(function() {", "; return nil; })()" if needs_wrapper?
end

def truthy
needs_wrapper? ? compiler.returns(true_body || s(:nil)) : true_body
end

def falsy
needs_wrapper? ? compiler.returns(false_body || s(:nil)) : false_body
end

def needs_wrapper?
expr? or recv?
end
end
end
end
118 changes: 118 additions & 0 deletions lib/opal/nodes/iter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
require 'opal/nodes/base_scope'

module Opal
module Nodes
# FIXME: this needs a rewrite very urgently
class IterNode < BaseScopeNode
handle :iter

children :args_sexp, :body_sexp

def compile
# opt args are last (if present) and are a s(:block)
if args.last.is_a?(Sexp) and args.last.type == :block
opt_args = args.pop
opt_args.shift
end

# does this iter define a block_pass
if args.last.is_a?(Sexp) and args.last.type == :block_pass
block_arg = args.pop
block_arg = block_arg[1][1].to_sym
end

# find any splat args
if args.last.is_a?(Sexp) and args.last.type == :splat
splat = args.last[1][1]
args.pop
len = args.length
end

params = args_to_params(args[1..-1])
params << splat if splat

to_vars = identity = nil

in_scope(:iter) do
identity = scope.identify!
add_temp "self = #{identity}._s || this"


args[1..-1].each_with_index do |arg, idx|
if arg.type == :lasgn
arg = variable(arg[1])

if opt_args and current_opt = opt_args.find { |s| s[1] == arg.to_sym }
push "if (#{arg} == null) #{arg} = ", expr(current_opt[2]), ";"
else
push "if (#{arg} == null) #{arg} = nil;"
end
elsif arg.type == :array
arg[1..-1].each_with_index do |_arg, _idx|
_arg = variable(_arg[1])
push "#{_arg} = #{params[idx]}#{_idx};"
end
else
raise "Bad block arg type"
end
end

if splat
scope.add_arg splat
push "#{splat} = $slice.call(arguments, #{len - 1});"
end

if block_arg
scope.block_name = block_arg
scope.add_temp block_arg
scope_name = scope.identify!

line "#{block_arg} = #{scope_name}._p || nil, #{scope_name}._p = null;"
end

line stmt(body)
to_vars = scope.to_vars
end

unshift to_vars
unshift "function(#{params.join ', '}) {"
wrap "(#{identity} = ", "}, #{identity}._s = self, #{identity})"
end

def args
if Fixnum === args_sexp or args_sexp.nil?
s(:array)
elsif args_sexp.type == :lasgn
s(:array, args_sexp)
else
args_sexp[1]
end
end

def body
compiler.returns(body_sexp || s(:nil))
end

# Maps block args into array of jsid. Adds $ suffix to invalid js
# identifiers.
#
# s(:args, parts...) => ["a", "b", "break$"]
def args_to_params(sexp)
result = []
sexp.each do |arg|
if arg[0] == :lasgn
ref = variable(arg[1])
scope.add_arg ref
result << ref
elsif arg[0] == :array
result << scope.next_temp
else
raise "Bad js_block_arg: #{arg[0]}"
end
end

result
end
end
end
end
193 changes: 193 additions & 0 deletions lib/opal/nodes/literal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
require 'opal/nodes/base'

module Opal
module Nodes
class ValueNode < Base
handle :true, :false, :self, :nil

def compile
# :self, :true, :false, :nil
push type.to_s
end
end

class LiteralNode < Base
children :value
end

class NumericNode < LiteralNode
handle :int, :float

def compile
push value.to_s
wrap '(', ')' if recv?
end
end

class StringNode < LiteralNode
handle :str

def compile
push value.inspect
end
end

class SymbolNode < LiteralNode
handle :sym

def compile
push value.to_s.inspect
end
end

class RegexpNode < LiteralNode
handle :regexp

def compile
push((value == // ? /^/ : value).inspect)
end
end

class XStringNode < LiteralNode
handle :xstr

def needs_semicolon?
stmt? and !value.to_s.include?(';')
end

def compile
push value.to_s
push ';' if needs_semicolon?

wrap '(', ')' if recv?
end
end

class DynamicStringNode < Base
handle :dstr

def compile
children.each_with_index do |part, idx|
push " + " unless idx == 0

if String === part
push part.inspect
elsif part.type == :evstr
push "("
push expr(part[1])
push ")"
elsif part.type == :str
push part[1].inspect
else
raise "Bad dstr part"
end

wrap '(', ')' if recv?
end
end
end

class DynamicSymbolNode < Base
handle :dsym

def compile
children.each_with_index do |part, idx|
push " + " unless idx == 0

if String === part
push part.inspect
elsif part.type == :evstr
push expr(s(:call, part.last, :to_s, s(:arglist)))
elsif part.type == :str
push part.last.inspect
else
raise "Bad dsym part"
end
end

wrap '(', ')'
end
end

class DynamicXStringNode < Base
handle :dxstr

def requires_semicolon(code)
stmt? and !code.include?(';')
end

def compile
needs_semicolon = false

children.each do |part|
if String === part
push part.to_s
needs_semicolon = true if requires_semicolon(part.to_s)
elsif part.type == :evstr
push expr(part[1])
elsif part.type == :str
push part.last.to_s
needs_semicolon = true if requires_semicolon(part.last.to_s)
else
raise "Bad dxstr part"
end
end

push ';' if needs_semicolon
wrap '(', ')' if recv?
end
end

class DynamicRegexpNode < Base
handle :dregx

def compile
children.each_with_index do |part, idx|
push " + " unless idx == 0

if String === part
push part.inspect
elsif part.type == :str
push part[1].inspect
else
push expr(part[1])
end
end

wrap '(new RegExp(', '))'
end
end

class ExclusiveRangeNode < Base
handle :dot2

children :start, :finish

def compile
helper :range

push "$range("
push expr(start)
push ", "
push expr(finish)
push ", false)"
end
end

class InclusiveRangeNode < Base
handle :dot3

children :start, :finish

def compile
helper :range

push "$range("
push expr(start)
push ", "
push expr(finish)
push ", true)"
end
end
end
end
211 changes: 211 additions & 0 deletions lib/opal/nodes/logic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
require 'opal/nodes/base'

module Opal
module Nodes
class NextNode < Base
handle :next

children :value

def compile
return push "continue;" if in_while?

push expr_or_nil(value)
wrap "return ", ";"
end
end

class BreakNode < Base
handle :break

children :value

def compile
if in_while?
compile_while
elsif scope.iter?
compile_iter
else
error "void value expression: cannot use break outside of iter/while"
end
end

def compile_while
if while_loop[:closure]
push "return ", expr_or_nil(value)
else
push "break;"
end
end

def compile_iter
error "break must be used as a statement" unless stmt?
push expr_or_nil(value)
wrap "return ($breaker.$v = ", ", $breaker)"
end
end

class RedoNode < Base
handle :redo

def compile
if in_while?
compile_while
elsif scope.iter?
compile_iter
else
push "REDO()"
end
end

def compile_while
while_loop[:use_redo] = true
push "#{while_loop[:redo_var]} = true"
end

def compile_iter
push "return #{scope.identity}.apply(null, $slice.call(arguments))"
end
end

class NotNode < Base
handle :not

children :value

def compile
with_temp do |tmp|
push expr(value)
wrap "(#{tmp} = ", ", (#{tmp} === nil || #{tmp} === false))"
end
end
end

class SplatNode < Base
handle :splat

children :value

def empty_splat?
value == [:nil] or value == [:paren, [:nil]]
end

def compile
if empty_splat?
push '[]'
elsif value.type == :sym
push '[', expr(value), ']'
else
push recv(value)
end
end
end

class OrNode < Base
handle :or

children :lhs, :rhs

def compile
with_temp do |tmp|
push "(((#{tmp} = "
push expr(lhs)
push ") !== false && #{tmp} !== nil) ? #{tmp} : "
push expr(rhs)
push ")"
end
end
end

class AndNode < Base
handle :and

children :lhs, :rhs

def compile
truthy_opt = nil

with_temp do |tmp|
if truthy_opt = js_truthy_optimize(lhs)
push "((#{tmp} = ", truthy_opt
push ") ? "
push expr(rhs)
push " : #{tmp})"
else
push "(#{tmp} = "
push expr(lhs)
push ", #{tmp} !== false && #{tmp} !== nil ?"
push expr(rhs)
push " : #{tmp})"
end
end
end
end

class ReturnNode < Base
handle :return

children :value

def return_val
expr_or_nil value
end

def return_in_iter?
if scope.iter? and parent_def = scope.find_parent_def
parent_def
end
end

def return_expr_in_def?
return scope if expr? and scope.def?
end

def scope_to_catch_return
return_in_iter? or return_expr_in_def?
end

def compile
if def_scope = scope_to_catch_return
def_scope.catch_return = true
push '$opal.$return(', return_val, ')'
elsif stmt?
push 'return ', return_val
else
raise SyntaxError, "void value expression: cannot return as an expression"
end
end
end

class JSReturnNode < Base
handle :js_return

children :value

def compile
push "return "
push expr(value)
end
end

class JSTempNode < Base
handle :js_tmp

children :value

def compile
push value.to_s
end
end

class BlockPassNode < Base
handle :block_pass

children :value

def compile
push expr(s(:call, value, :to_proc, s(:arglist)))
end
end
end
end
62 changes: 62 additions & 0 deletions lib/opal/nodes/masgn.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'opal/nodes/base'

module Opal
module Nodes
class MassAssignNode < Base
handle :masgn

children :lhs, :rhs

def compile
tmp = scope.new_temp
len = 0 # how many rhs items are we sure we have

if rhs.type == :array
len = rhs.size - 1
push "#{tmp} = ", expr(rhs)
elsif rhs.type == :to_ary
push "#{tmp} = $opal.to_ary(", expr(rhs[1]), ")"
elsif rhs.type == :splat
push "(#{tmp} = ", expr(rhs[1]), ")['$to_a'] ? (#{tmp} = #{tmp}['$to_a']())"
push " : (#{tmp})._isArray ? #{tmp} : (#{tmp} = [#{tmp}])"
else
raise "unsupported mlhs type"
end

lhs.children.each_with_index do |child, idx|
push ', '

if child.type == :splat
if part = child[1]
part = part.dup
part << s(:js_tmp, "$slice.call(#{tmp}, #{idx})")
push expr(part)
end
else
if idx >= len
assign = s(:js_tmp, "(#{tmp}[#{idx}] == null ? nil : #{tmp}[#{idx}])")
else
assign = s(:js_tmp, "#{tmp}[#{idx}]")
end

part = child.dup
if child.type == :lasgn or child.type == :iasgn or child.type == :lvar
part << assign
elsif child.type == :call
part[2] = "#{part[2]}=".to_sym
part.last << assign
elsif child.type == :attrasgn
part.last << assign
else
raise "Bad lhs for masgn"
end

push expr(part)
end
end

scope.queue_temp tmp
end
end
end
end
46 changes: 46 additions & 0 deletions lib/opal/nodes/module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'opal/nodes/base_scope'

module Opal
module Nodes
class ModuleNode < BaseScopeNode
handle :module

children :cid, :body

def compile
name, base = name_and_base
helper :module

push "(function($base) {"
line " var self = $module($base, '#{name}');"

in_scope(:module) do
scope.name = name
add_temp "#{scope.proto} = self._proto"
add_temp '$scope = self._scope'

body_code = stmt(body)
empty_line

line scope.to_vars
line body_code
line scope.to_donate_methods
end

line "})(", base, ")"
end

def name_and_base
if Symbol === cid or String === cid
[cid.to_s, 'self']
elsif cid.type == :colon2
[cid[2].to_s, expr(cid[1])]
elsif cid.type == :colon3
[cid[1].to_s, '$opal.Object']
else
raise "Bad receiver in module"
end
end
end
end
end
134 changes: 134 additions & 0 deletions lib/opal/nodes/rescue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
require 'opal/nodes/base'

module Opal
module Nodes
class RescueModNode < Base
handle :rescue_mod

children :lhs, :rhs

def body
stmt? ? lhs : compiler.returns(lhs)
end

def rescue_val
stmt? ? rhs : compiler.returns(rhs)
end

def compile
push "try {", expr(body), " } catch ($err) { ", expr(rescue_val), " }"

wrap '(function() {', '})()' unless stmt?
end
end

class EnsureNode < Base
handle :ensure

children :begn, :ensr

def compile
push "try {"
line compiler.process(body_sexp, @level)
line "} finally {"
line compiler.process(ensr_sexp, @level)
line "}"

wrap '(function() {', '; })()' if wrap_in_closure?
end

def body_sexp
wrap_in_closure? ? compiler.returns(begn) : begn
end

def ensr_sexp
ensr || s(:nil)
end

def wrap_in_closure?
recv? or expr?
end
end

class RescueNode < Base
handle :rescue

children :body

def compile
handled_else = false

push "try {"
line(indent { process(body_code, @level) })
line "} catch ($err) {"

children[1..-1].each_with_index do |child, idx|
handled_else = true unless child.type == :resbody

push "else " unless idx == 0
push(indent { process(child, @level) })
end

# if no resbodys capture our error, then rethrow
unless handled_else
push "else { throw $err; }"
end

line "}"

wrap '(function() { ', '})()' if expr?
end

def body_code
body.type == :resbody ? s(:nil) : body
end
end

class ResBodyNode < Base
handle :resbody

children :args, :body

def compile
push "if ("

rescue_classes.each_with_index do |cls, idx|
push ', ' unless idx == 0
call = s(:call, cls, :===, s(:arglist, s(:js_tmp, '$err')))
push expr(call)
end

# if no classes are given, then catch all errors
push "true" if rescue_classes.empty?

push ") {"

if variable = rescue_variable
variable[2] = s(:js_tmp, '$err')
push expr(variable), ';'
end

line process(rescue_body, @level)
line "}"
end

def rescue_variable
variable = args.last

if Sexp === variable and [:lasgn, :iasgn].include?(variable.type)
variable.dup
end
end

def rescue_classes
classes = args.children
classes.pop if classes.last and classes.last.type != :const
classes
end

def rescue_body
body || s(:nil)
end
end
end
end
26 changes: 26 additions & 0 deletions lib/opal/nodes/singleton_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'opal/nodes/base_scope'

module Opal
module Nodes
class SingletonClassNode < BaseScopeNode
handle :sclass

children :object, :body

def compile
push "(function(self) {"

in_scope(:sclass) do
add_temp '$scope = self._scope'
add_temp 'def = self._proto'

line scope.to_vars
line stmt(body)
end

line "})(", recv(object), ".$singleton_class())"
end
end

end
end
97 changes: 97 additions & 0 deletions lib/opal/nodes/super.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require 'opal/nodes/base'

module Opal
module Nodes
# This base class is used just to child the find_super_dispatcher method
# body. This is then used by actual super calls, or a defined?(super) style
# call.
class BaseSuperNode < Base
children :arglist, :iter

def compile_dispatcher
if arglist or iter
iter = expr(iter_sexp)
else
scope.uses_block!
iter = '$iter'
end
if scope.def?
scope.uses_block!
scope_name = scope.identify!
class_name = scope.parent.name || 'self._klass._proto'

if scope.defs
push "$opal.find_super_dispatcher(this, '#{scope.mid.to_s}', #{scope_name}, "
push iter
push ", #{class_name})"
else
push "$opal.find_super_dispatcher(self, '#{scope.mid.to_s}', #{scope_name}, "
push iter
push ")"
end
elsif scope.iter?
chain, cur_defn, mid = scope.get_super_chain
trys = chain.map { |c| "#{c}._def" }.join(' || ')

push "$opal.find_iter_super_dispatcher(self, #{mid}, (#{trys} || #{cur_defn}), null)"
else
raise "Cannot call super() from outside a method block"
end
end

def args
arglist || s(:arglist)
end

def iter_sexp
iter || s(:js_tmp, 'null')
end
end

class DefinedSuperNode < BaseSuperNode
handle :defined_super

def compile
# insert method body to find super method
self.compile_dispatcher

wrap '((', ') != null ? "super" : nil)'
end
end

class SuperNode < BaseSuperNode
handle :super

children :arglist, :iter

def compile
if arglist or iter
splat = has_splat?
args = expr(self.args)

unless splat
args = [fragment('['), args, fragment(']')]
end
else
if scope.def?
scope.uses_zuper = true
args = fragment('$zuper')
else
args = fragment('$slice.call(arguments)')
end
end

# compile our call to runtime to get super method
self.compile_dispatcher

push ".apply(self, "
push(*args)
push ")"
end

def has_splat?
args.children.any? { |child| child.type == :splat }
end
end
end
end
63 changes: 63 additions & 0 deletions lib/opal/nodes/top.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require 'opal/nodes/base_scope'

module Opal
module Nodes
# Generates code for an entire file, i.e. the base sexp
class TopNode < BaseScopeNode
def compile
push version_comment

line "(function($opal) {"

in_scope(:top) do
body_code = stmt(stmts)
body_code = [body_code] unless body_code.is_a?(Array)

add_temp 'self = $opal.top'
add_temp '$scope = $opal'
add_temp 'nil = $opal.nil'

add_used_helpers
line scope.to_vars

compile_method_stubs
compile_irb_vars

line body_code
end

line "})(Opal);\n"
end

def stmts
sexp = @sexp || s(:nil)
scope = s(:scope, sexp)
scope.line = sexp.line
scope
end

def compile_irb_vars
if compiler.irb_vars?
line "if (!$opal.irb_vars) { $opal.irb_vars = {}; }"
end
end

def add_used_helpers
helpers = compiler.helpers.to_a
helpers.to_a.each { |h| add_temp "$#{h} = $opal.#{h}" }
end

def compile_method_stubs
if compiler.method_missing?
calls = compiler.method_calls
stubs = calls.to_a.map { |k| "'$#{k}'" }.join(', ')
line "$opal.add_stubs([#{stubs}]);"
end
end

def version_comment
"/* Generated by Opal #{Opal::VERSION} */"
end
end
end
end
Loading