Skip to content

Commit

Permalink
Add compiler nodes for rescue/ensure and case/when
Browse files Browse the repository at this point in the history
  • Loading branch information
adambeynon committed Oct 22, 2013
1 parent 4e9d955 commit 6791fa8
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 163 deletions.
2 changes: 2 additions & 0 deletions lib/opal/nodes.rb
Expand Up @@ -8,3 +8,5 @@
require 'opal/nodes/logic'
require 'opal/nodes/definitions'
require 'opal/nodes/yield'
require 'opal/nodes/rescue'
require 'opal/nodes/case'
92 changes: 92 additions & 0 deletions lib/opal/nodes/case.rb
@@ -0,0 +1,92 @@
require 'opal/nodes/base'

module Opal
class Parser
class CaseNode < Node
children :condition

def compile
handled_else = false

@parser.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
@parser.returns(wen) if needs_closure?
push "else " unless idx == 0
push stmt(wen)
elsif wen # s(:else)
handled_else = true
wen = @parser.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
@parser.instance_variable_get(:@case_stmt)
end
end

class WhenNode < Node
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 @parser.js_truthy(check)
end
end
end

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

def when_checks
whens.children
end

def case_stmt
@parser.instance_variable_get(:@case_stmt)
end

def body_code
body || s(:nil)
end
end
end
end
108 changes: 108 additions & 0 deletions lib/opal/nodes/rescue.rb
@@ -0,0 +1,108 @@
require 'opal/nodes/base'

module Opal
class Parser
class EnsureNode < Node
children :begn, :ensr

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

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

def body_sexp
wrap_in_closure? ? @parser.returns(begn) : begn
end

def ensr_sexp
ensr || s(:nil)
end

def wrap_in_closure?
recv? or expr?
end
end

class RescueNode < Node
children :body

def compile
handled_else = false

push "try {"
line(indent { @parser.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 { @parser.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 < Node
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 @parser.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
172 changes: 9 additions & 163 deletions lib/opal/parser.rb
Expand Up @@ -115,6 +115,15 @@ def add_handler(klass, *types)
add_handler WhileNode, :while
add_handler UntilNode, :until

# rescue/ensure
add_handler EnsureNode, :ensure
add_handler RescueNode, :rescue
add_handler ResBodyNode, :resbody

# case
add_handler CaseNode, :case
add_handler WhenNode, :when

# Final generated javascript for this parser
attr_reader :result

Expand Down Expand Up @@ -1110,83 +1119,6 @@ def js_falsy(sexp)
end
end

# s(:case, expr, when1, when2, ..)
def process_case(exp, level)
pre, code = [], []

# are we inside a statement_closure
returnable = level != :stmt
done_else = false

in_case do
if cond = exp[0]
@case_stmt[:cond] = true
@scope.add_local "$case"
expr = process cond
pre << f("$case = ", exp) << expr << f(";", exp)
end

exp[1..-1].each do |wen|
if wen and wen.first == :when
returns(wen) if returnable
wen = process(wen, :stmt)
code << f("else ", exp) unless code.empty?
code << wen
elsif wen # s(:else)
done_else = true
wen = returns(wen) if returnable
code << f("else {", exp) << process(wen, :stmt) << f("}", exp)
end
end
end

code << f("else { return nil }", exp) if returnable and !done_else

code.unshift pre

if returnable
code.unshift f("(function() { ", exp)
code << f(" }).call(self)", exp)
end

code
end

# when foo
# bar
#
# s(:when, s(:array, foo), bar)
def process_when(exp, level)
arg = exp[0][1..-1]
body = exp[1] || s(:nil)
body = process body, level

test = []
arg.each do |a|
test << f(" || ") unless test.empty?

if a.first == :splat # when inside another when means a splat of values
call = f("$splt[i]['$===']($case)", a)

splt = [f("(function($splt) { for(var i = 0; i < $splt.length; i++) {", exp)]
splt << f("if (") << call << f(") { return true; }", exp)
splt << f("} return false; }).call(self, ", exp)
splt << process(a[1]) << f(")")

test << splt
else
if @case_stmt[:cond]
call = s(:call, a, :===, s(:arglist, s(:js_tmp, "$case")))
test << process(call)
else
test << js_truthy(a)
end
end
end

[f("if ("), test, f(") {#@space"), body, f("#@space}")]
end

# super a, b, c
#
# s(:super, arg1, arg2, ...)
Expand Down Expand Up @@ -1247,91 +1179,5 @@ def process_super(sexp, level, skip_call=false)
skip_call ? [f("null")] : raise("Cannot call super() from outside a method block")
end
end

# s(:ensure, body, ensure)
def process_ensure(exp, level)
begn = exp[0]
if level == :recv || level == :expr
retn = true
begn = returns begn
end

result = []
body = process begn, level
ensr = exp[1] || s(:nil)
ensr = process ensr, level

body = [f("try {\n", exp), body, f("}", exp)]

result << body << f("#{@space}finally {#@space", exp) << ensr << f("}", exp)

if retn
[f("(function() { ", exp), result, f("; }).call(self)", exp)]
else
result
end
end

def process_rescue(exp, level)
body = exp.first.first == :resbody ? s(:nil) : exp[0]
body = indent { process body, level }
handled_else = false

parts = []
exp[1..-1].each do |a|
handled_else = true unless a.first == :resbody
part = indent { process a, level }

unless parts.empty?
parts << f("else ", exp)
end

parts << part
end
# if no rescue statement captures our error, we should rethrow
parts << indent { f("else { throw $err; }", exp) } unless handled_else

code = []
code << f("try {#@space#{INDENT}", exp)
code << body
code << f("#@space} catch ($err) {#@space", exp)
code << parts
code << f("#@space}", exp)

if level == :expr
code.unshift f("(function() { ", exp)
code << f(" }).call(self)", exp)
end

code
end

def process_resbody(exp, level)
args = exp[0]
body = exp[1]

body = process(body || s(:nil), level)
types = args[1..-1]
types.pop if types.last and types.last.first != :const

err = []
types.each do |t|
err << f(", ", exp) unless err.empty?
call = s(:call, t, :===, s(:arglist, s(:js_tmp, "$err")))
a = process call
err << a
end
err << f("true", exp) if err.empty?

if Sexp === args.last and [:lasgn, :iasgn].include? args.last.first
val = args.last
val[2] = s(:js_tmp, "$err")
val = [process(val) , f(";", exp)]
end

val = [] unless val

[f("if (", exp), err, f("){#@space", exp), val, body, f("}", exp)]
end
end
end

0 comments on commit 6791fa8

Please sign in to comment.