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

More comprehensive recursion guard. #460

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions lib-topaz/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ def initialize(size_or_arr = nil, obj = nil, &block)

def inspect
result = "["
recursion = Thread.current.recursion_guard(self) do
self.each_with_index do |obj, i|
if i > 0
result << ", "
Thread.current.recursion_guard(:array_inspect, self) do |recursion|
if recursion
result << "..."
else
self.each_with_index do |obj, i|
if i > 0
result << ", "
end
result << obj.inspect
end
result << obj.inspect
end
end
if recursion
result << "..."
end
result << "]"
end

Expand Down Expand Up @@ -138,7 +139,8 @@ def first

def flatten(level = -1)
list = []
recursion = Thread.current.recursion_guard(self) do
Thread.current.recursion_guard(:array_flatten, self) do |recursion|
raise ArgumentError.new("tried to flatten recursive array") if recursion
self.each do |item|
if level == 0
list << item
Expand All @@ -150,9 +152,6 @@ def flatten(level = -1)
end
return list
end
if recursion
raise ArgumentError.new("tried to flatten recursive array")
end
end

def flatten!(level = -1)
Expand All @@ -178,10 +177,12 @@ def ==(other)
if self.size != other.size
return false
end
Thread.current.recursion_guard(self) do
self.each_with_index do |x, i|
if x != other[i]
return false
Thread.current.recursion_guard(:array_equals, self) do |recursion|
if !recursion
self.each_with_index do |x, i|
if x != other[i]
return false
end
end
end
end
Expand All @@ -198,10 +199,12 @@ def eql?(other)
if self.length != other.length
return false
end
Thread.current.recursion_guard(self) do
self.each_with_index do |x, i|
if !x.eql?(other[i])
return false
Thread.current.recursion_guard(:array_eqlp, self) do |recursion|
if !recursion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change in API from returning the the in_recursion value to yielding it to the block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into a case where not letting the block decide what to do when recursion is detected made things difficult, but I'm not sure if that's an issue anymore.

self.each_with_index do |x, i|
if !x.eql?(other[i])
return false
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib-topaz/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
load(File.join(lib_topaz, file))
end

load_bootstrap.call("thread.rb")
load_bootstrap.call("array.rb")
load_bootstrap.call("class.rb")
load_bootstrap.call("comparable.rb")
Expand Down
27 changes: 27 additions & 0 deletions lib-topaz/thread.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Thread
def recursion_guard_outer(identifier, obj, &block)
# We want to throw something less likely to be caught accidentally outside
# our own code than the recursion identifier. Ideally this should be an
# object that is unique to this particular recursion guard. Since doing
# that properly requires pushing extra state all the way up into
# ExecutionContext, we do this instead.
throw_symbol = "__recursion_guard_#{identifier}".to_sym

if self.in_recursion_guard?(identifier)
self.recursion_guard(identifier, obj) do |recursion|
if !recursion
yield(false)
else
throw(throw_symbol)
end
end
else
self.recursion_guard(identifier, obj) do |recursion|
catch(throw_symbol) do
return yield(false)
end
return yield(true)
end
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used yet, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but I need it for Array#hash in #456.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is becoming my official theme song, but let's split this up :D, so this just has the identifier for recursion guards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it have Thread.in_recursion_guard? as well? That's only used by the outer guard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, just what's needed. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have been more precise, just what's needed for the recursion_guard() changes.

60 changes: 60 additions & 0 deletions tests/objects/test_threadobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,63 @@ def test_thread_local_storage(self, space):
return Thread.current[:a]
""")
assert space.int_w(w_res) == 1

def test_recursion_guard(self, space):
w_res = space.execute("""
def foo(objs, depth = 0)
obj = objs.shift
Thread.current.recursion_guard(:foo, obj) do |recursion|
if recursion
[depth, obj]
else
foo(objs, depth + 1)
end
end
end
return foo([:a, :b, :c, :a, :d])
""")
w_depth, w_symbol = space.listview(w_res)
assert space.int_w(w_depth) == 3
assert space.symbol_w(w_symbol) == "a"

def test_recursion_guard_nested(self, space):
w_res = space.execute("""
def foo(objs, depth = 0)
obj = objs.shift
Thread.current.recursion_guard(:foo, obj) do |recursion|
return bar(objs, depth + 1) unless recursion
end
return [depth, obj]
end

def bar(objs, depth)
obj = objs.shift
Thread.current.recursion_guard(:bar, obj) do |recursion|
return foo(objs, depth + 1) unless recursion
end
return [depth, obj]
end

return foo([:a, :a, :b, :b, :c, :a, :d, :d])
""")
w_depth, w_symbol = space.listview(w_res)
assert space.int_w(w_depth) == 5
assert space.symbol_w(w_symbol) == "a"

def test_recursion_guard_outer(self, space):
w_res = space.execute("""
def foo(objs, depth = 0)
obj = objs.shift
Thread.current.recursion_guard_outer(:foo, obj) do |recursion|
if recursion
[depth, obj]
else
foo(objs, depth + 1)
end
end
end
return foo([:a, :b, :c, :a, :d])
""")
w_depth, w_symbol = space.listview(w_res)
assert space.int_w(w_depth) == 0
assert space.symbol_w(w_symbol) == "a"
9 changes: 5 additions & 4 deletions tests/test_executioncontext.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
class TestExecutionContext(object):
def test_recursion_guard(self, space):
f = "my_func"
x = object()
y = object()
with space.getexecutioncontext().recursion_guard(x) as in_recursion:
with space.getexecutioncontext().recursion_guard(f, x) as in_recursion:
assert not in_recursion
with space.getexecutioncontext().recursion_guard(y) as ir2:
with space.getexecutioncontext().recursion_guard(f, y) as ir2:
assert not ir2
with space.getexecutioncontext().recursion_guard(x) as ir3:
with space.getexecutioncontext().recursion_guard(f, x) as ir3:
assert ir3
with space.getexecutioncontext().recursion_guard(x) as ir3:
with space.getexecutioncontext().recursion_guard(f, x) as ir3:
assert ir3
25 changes: 18 additions & 7 deletions topaz/executioncontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self):
self.regexp_match_cell = None
self.w_trace_proc = None
self.in_trace_proc = False
self.recursive_objects = {}
self.recursive_calls = {}
self.catch_names = {}

def settraceproc(self, w_proc):
Expand Down Expand Up @@ -75,8 +75,13 @@ def gettoprubyframe(self):
frame = frame.backref()
return frame

def recursion_guard(self, w_obj):
return _RecursionGuardContextManager(self, w_obj)
def recursion_guard(self, func_id, w_obj):
# We need independent recursion detection for different blocks of
# potentially recursive code so that they don't interfere with each
# other and cause false positives. This is only likely to be a problem
# if one recursion-guarded function calls another, but we can't
# guarantee that won't happen.
return _RecursionGuardContextManager(self, func_id, w_obj)

def catch_block(self, name):
return _CatchContextManager(self, name)
Expand All @@ -103,21 +108,27 @@ def __exit__(self, exc_type, exc_value, tb):


class _RecursionGuardContextManager(object):
def __init__(self, ec, w_obj):
def __init__(self, ec, func_id, w_obj):
self.ec = ec
if func_id not in self.ec.recursive_calls:
self.ec.recursive_calls[func_id] = {}
self.recursive_objects = self.ec.recursive_calls[func_id]
self.func_id = func_id
self.w_obj = w_obj
self.added = False

def __enter__(self):
if self.w_obj in self.ec.recursive_objects:
if self.w_obj in self.recursive_objects:
return True
self.ec.recursive_objects[self.w_obj] = None
self.recursive_objects[self.w_obj] = None
self.added = True
return False

def __exit__(self, exc_type, exc_value, tb):
if self.added:
del self.ec.recursive_objects[self.w_obj]
del self.recursive_objects[self.w_obj]
if not self.recursive_objects:
del self.ec.recursive_calls[self.func_id]


class _CatchContextManager(object):
Expand Down
5 changes: 3 additions & 2 deletions topaz/objects/fileobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,12 @@ def singleton_method_join(self, space, args_w):
result = []
for w_arg in args_w:
if isinstance(w_arg, W_ArrayObject):
with space.getexecutioncontext().recursion_guard(w_arg) as in_recursion:
ec = space.getexecutioncontext()
with ec.recursion_guard("file_singleton_method_join", w_arg) as in_recursion:
if in_recursion:
raise space.error(space.w_ArgumentError, "recursive array")
string = space.str_w(
W_FileObject.singleton_method_join(self, space, space.listview(w_arg))
space.send(space.getclassfor(W_FileObject), space.newsymbol("join"), space.listview(w_arg))
)
else:
w_string = space.convert_type(w_arg, space.w_string, "to_path", raise_error=False)
Expand Down
21 changes: 14 additions & 7 deletions topaz/objects/threadobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,20 @@ def method_subscript_assign(self, space, key, w_value):
return w_value

@classdef.method("recursion_guard")
def method_recursion_guard(self, space, w_obj, block):
def method_recursion_guard(self, space, w_identifier, w_obj, block):
"""
Detects recursion. If there is none, yield and return false. Else
return true
Calls the block with true if recursion is detected, false otherwise.
It is up to the block to decide what to do in either case.
"""
with space.getexecutioncontext().recursion_guard(w_obj) as in_recursion:
if in_recursion:
return space.w_true
space.invoke_block(block, [])
ec = space.getexecutioncontext()
identifier = space.str_w(w_identifier)
with ec.recursion_guard(identifier, w_obj) as in_recursion:
return space.invoke_block(block, [space.newbool(in_recursion)])

@classdef.method("in_recursion_guard?")
def method_in_recursion_guardp(self, space, w_identifier):
ec = space.getexecutioncontext()
identifier = space.str_w(w_identifier)
if identifier in ec.recursive_calls:
return space.w_true
return space.w_false