Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closure tracking #2357

Merged
merged 7 commits into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/opal/compiler.rb
Expand Up @@ -7,6 +7,7 @@
require 'opal/eof_content'
require 'opal/errors'
require 'opal/magic_comments'
require 'opal/nodes/closure'

module Opal
# Compile a string of ruby code into javascript.
Expand Down Expand Up @@ -46,6 +47,8 @@ def self.compile(source, options = {})
# compiler.source_map # => #<SourceMap:>
#
class Compiler
include Nodes::Closure::CompilerSupport

# Generated code gets indented with two spaces on each scope
INDENT = ' '

Expand Down
1 change: 1 addition & 0 deletions lib/opal/nodes.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'opal/nodes/closure'
require 'opal/nodes/base'
require 'opal/nodes/literal'
require 'opal/nodes/variables'
Expand Down
6 changes: 6 additions & 0 deletions lib/opal/nodes/base.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true

require 'opal/nodes/helpers'
require 'opal/nodes/closure'

module Opal
module Nodes
class Base
include Helpers
include Closure::NodeSupport

def self.handlers
@handlers ||= {}
Expand Down Expand Up @@ -126,6 +128,10 @@ def expr_or_nil(sexp)
sexp ? expr(sexp) : 'nil'
end

def expr_or_empty(sexp)
sexp && sexp.type != :nil ? expr(sexp) : ''
end

def add_local(name)
scope.add_scope_local name.to_sym
end
Expand Down
20 changes: 6 additions & 14 deletions lib/opal/nodes/call.rb
Expand Up @@ -3,7 +3,6 @@
require 'set'
require 'pathname'
require 'opal/nodes/base'
require 'opal/rewriters/break_finder'

module Opal
module Nodes
Expand Down Expand Up @@ -64,9 +63,7 @@ def compile
def iter_has_break?
return false unless iter

finder = Opal::Rewriters::BreakFinder.new
finder.process(iter)
finder.found_break?
iter.meta[:has_break]
end

# Opal has a runtime helper 'Opal.send_method_name' that assigns
Expand Down Expand Up @@ -103,15 +100,15 @@ def default_compile
scope.await_encountered = true
end

push_closure(Closure::SEND) if iter_has_break?
if invoke_using_refinement?
compile_using_refined_send
elsif invoke_using_send?
compile_using_send
else
compile_simple_call_chain
end

compile_break_catcher
pop_closure if iter_has_break?

if auto_await?
push ')'
Expand Down Expand Up @@ -188,14 +185,6 @@ def compile_refinements
push expr(s(:array, *refinements)), ', '
end

def compile_break_catcher
if iter_has_break?
unshift 'return '
unshift '(function(){var $brk = Opal.new_brk(); try {'
line '} catch (err) { if (err === $brk) { return err.$v } else { throw err } }})()'
end
end

def compile_simple_call_chain
push recv(receiver_sexp), method_jsid, '(', expr(arglist), ')'
end
Expand Down Expand Up @@ -402,6 +391,9 @@ def using_refinement(arg)
# This can be refactored in terms of binding, but it would need 'corelib/binding'
# to be required in existing code.
add_special :eval do |compile_default|
# Catch the return throw coming from eval
thrower(:eval_return)

next compile_default.call if arglist.children.length != 1 || ![s(:self), nil].include?(recvr)

scope.nesting
Expand Down
4 changes: 3 additions & 1 deletion lib/opal/nodes/class.rb
Expand Up @@ -24,7 +24,9 @@ def compile
line " var self = $klass($base, $super, '#{name}');"
in_scope do
scope.name = name
compile_body
in_closure(Closure::MODULE | Closure::JS_FUNCTION) do
compile_body
end
end

if await_encountered
Expand Down
250 changes: 250 additions & 0 deletions lib/opal/nodes/closure.rb
@@ -0,0 +1,250 @@
# frozen_string_literal: true

module Opal
module Nodes
# This module takes care of providing information about the
# closure stack that we have for the nodes during compile time.
# This is not a typical node.
#
# Also, while loops are not closures per se, this module also
# takes a note about them.
#
# Then we can use this information for control flow like
# generating breaks, nexts, returns.
class Closure
NONE = 0

@types = {}

def self.add_type(name, value)
const_set(name, value)
@types[name] = value
end

def self.type_inspect(type)
@types.reject do |_name, value|
(type & value) == 0
end.map(&:first).join("|")
end

add_type(:JS_FUNCTION, 1 << 0) # everything that generates an IIFE
add_type(:JS_LOOP, 1 << 1) # exerything that generates a JS loop
add_type(:JS_LOOP_INSIDE, 1 << 2) # everything that generates an inside of a loop

add_type(:DEF, 1 << 3) # def
add_type(:LAMBDA, 1 << 4) # lambda
add_type(:ITER, 1 << 5) # iter, lambda
add_type(:MODULE, 1 << 6)
add_type(:LOOP, 1 << 7) # for building a catcher outside a loop
add_type(:LOOP_INSIDE, 1 << 8) # for building a catcher inside a loop
add_type(:SEND, 1 << 9) # to generate a break catcher after send with a block
add_type(:TOP, 1 << 10)
add_type(:RESCUE_RETRIER, 1 << 11) # a virtual loop to catch a retrier

ANY = 0xffffffff

def initialize(node, type, parent)
@node, @type, @parent = node, type, parent
@catchers = []
@throwers = {}
end

def register_catcher(type = :return)
@catchers << type unless @catchers.include? type

"$t_#{type}"
end

def register_thrower(type, id)
@throwers[type] = id
end

def is?(type)
(@type & type) != 0
end

def inspect
"#<Closure #{Closure.type_inspect(type)} #{@node.class}>"
end

attr_accessor :node, :type, :parent, :catchers, :throwers

module NodeSupport
def push_closure(type = JS_FUNCTION)
closure = Closure.new(self, type, select_closure)
@compiler.closure_stack << closure
@closure = closure
end

attr_accessor :closure

def pop_closure
compile_catcher
@compiler.closure_stack.pop
last = @compiler.closure_stack.last
@closure = last if last&.node == self
end

def in_closure(type = JS_FUNCTION)
closure = push_closure(type)
out = yield closure
pop_closure
out
end

def select_closure(type = ANY, break_after: NONE)
@compiler.closure_stack.reverse.find do |i|
break if (i.type & break_after) != 0
(i.type & type) != 0
end
end

def generate_thrower(type, closure, value)
id = closure.register_catcher(type)
closure.register_thrower(type, id)
push id, '.$throw(', expr_or_empty(value), ')'
id
end

def generate_thrower_without_catcher(type, closure, value)
helper :thrower

if closure.throwers.key? type
id = closure.throwers[type]
else
id = compiler.unique_temp('t_')
scope = closure.node.scope&.parent || top_scope
scope.add_scope_temp("#{id} = $thrower('#{type}')")
closure.register_thrower(type, id)
end
push id, '.$throw(', expr_or_empty(value), ')'
id
end

def thrower(type, value = nil)
case type
when :return
thrower_closure = select_closure(DEF, break_after: MODULE | TOP)
last_closure = select_closure(JS_FUNCTION)

if !thrower_closure
iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
if iter_closure
generate_thrower_without_catcher(:return, iter_closure, value)
elsif compiler.eval?
push 'Opal.t_eval_return.$throw(', expr_or_empty(value), ')'
else
error 'Invalid return'
end
elsif thrower_closure == last_closure
push 'return ', expr_or_nil(value)
else
id = generate_thrower(:return, thrower_closure, value)
# Additionally, register our thrower on the surrounding iter, if present
iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
iter_closure.register_thrower(:return, id) if iter_closure
end
when :eval_return
thrower_closure = select_closure(DEF | LAMBDA, break_after: MODULE | TOP)

if thrower_closure
thrower_closure.register_catcher(:eval_return)
end
when :next, :redo
thrower_closure = select_closure(ITER | LOOP_INSIDE, break_after: DEF | MODULE | TOP)
last_closure = select_closure(JS_FUNCTION | JS_LOOP_INSIDE)

if !thrower_closure
error 'Invalid next'
elsif thrower_closure == last_closure
if thrower_closure.is? LOOP_INSIDE
push 'continue'
elsif thrower_closure.is? ITER | LAMBDA
push 'return ', expr_or_nil(value)
end
else
generate_thrower(:next, thrower_closure, value)
end
when :break
thrower_closure = select_closure(SEND | LAMBDA | LOOP, break_after: DEF | MODULE | TOP)
last_closure = select_closure(JS_FUNCTION | JS_LOOP)

if !thrower_closure
iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
if iter_closure
generate_thrower_without_catcher(:break, iter_closure, value)
else
error 'Invalid break'
end
elsif thrower_closure == last_closure
if thrower_closure.is? JS_FUNCTION | LAMBDA
push 'return ', expr_or_nil(value)
elsif thrower_closure.is? LOOP
push 'break'
end
else
generate_thrower(:break, thrower_closure, value)
end
when :retry
thrower_closure = select_closure(RESCUE_RETRIER, break_after: DEF | MODULE | TOP)
last_closure = select_closure(JS_LOOP_INSIDE)

if !thrower_closure
error 'Invalid retry'
elsif thrower_closure == last_closure
push 'continue'
else
generate_thrower(:retry, thrower_closure, value)
end
end
end

def closure_is?(type)
@closure.is?(type)
end

# Generate a catcher if thrower has been used
def compile_catcher
catchers = @closure.catchers

return if catchers.empty?

helper :thrower

push "} catch($e) {"
indent do
@closure.catchers.each do |type|
case type
when :eval_return
line "if ($e === Opal.t_eval_return) return $e.$v;"
else
line "if ($e === $t_#{type}) return $e.$v;"
end
end
line "throw $e;"
end
line "}"

unshift "return " if closure_is? SEND

unshift "var ", catchers.map { |type| "$t_#{type} = $thrower('#{type}')" }.join(", "), "; "
unshift "try { "

unless closure_is? JS_FUNCTION
if scope.await_encountered
wrap "(await (async function(){", "})())"
else
wrap "(function(){", "})()"
end
end
end
end

module CompilerSupport
def closure_stack
@closure_stack ||= []
end
end
end
end
end