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

Correct Binding#dup to properly propagate values it captures #7688

Merged
merged 8 commits into from
May 2, 2024
7 changes: 6 additions & 1 deletion core/src/main/java/org/jruby/RubyBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ public IRubyObject initialize(ThreadContext context) {

return this;
}


@JRubyMethod
public IRubyObject dup(ThreadContext context) {
return newBinding(context.runtime, binding.dup(context));
}

@JRubyMethod(name = "initialize_copy", visibility = Visibility.PRIVATE)
@Override
public IRubyObject initialize_copy(IRubyObject other) {
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/parser/StaticScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
* will point to the previous scope of the enclosing module/class (cref).
*
*/
public class StaticScope implements Serializable {
public class StaticScope implements Serializable, Cloneable {
public static final int MAX_SPECIALIZED_SIZE = 50;
private static final long serialVersionUID = 3423852552352498148L;

Expand Down
12 changes: 12 additions & 0 deletions core/src/main/java/org/jruby/runtime/Binding.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,16 @@ public BacktraceElement getBacktrace() {
// Can't use Frame.DUMMY because of circular static init seeing it before it's assigned
new Frame(),
Visibility.PUBLIC);

/**
* Duplicate this binding and setup the proper cloned instance of the eval scope so that any previously
* captured variables still exist but are not shared with the original binding.
* @param context the current thread context
* @return the duplicated binding
*/
public Binding dup(ThreadContext context) {
Binding newBinding = new Binding(this);
newBinding.evalScope = getEvalScope(context.runtime).dupEvalScope();
return newBinding;
}
}
11 changes: 11 additions & 0 deletions core/src/main/java/org/jruby/runtime/DynamicScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.jruby.ir.JIT;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.scope.ManyVarsDynamicScope;

public abstract class DynamicScope implements Cloneable {
// Static scoping information for this scope
Expand Down Expand Up @@ -598,4 +599,14 @@ public DynamicScope cloneScope() {
throw new RuntimeException("BUG: failed to clone scope type " + getClass().getName());
}
}

/**
* Binding needs to clone its scope with all the current values.
* @return a duplicate of this scope with all the current values
*/
public DynamicScope dupEvalScope() {
ManyVarsDynamicScope newScope = new ManyVarsDynamicScope(staticScope.duplicate(), parent);
newScope.setVariableValues(getValues());
return newScope;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ private void allocate() {
if(variableValues == null) variableValues = IRubyObject.array(staticScope.getNumberOfVariables());
}

// WARNING: This is used when dup'ing an eval scope. We know the size and that it will 100% always be
// a ManyVarsDynamicScope. This should not be used by anything else. If there ever ends up being another
// use then it should be documented in this warning.
public void setVariableValues(IRubyObject[] variableValues) {
this.variableValues = variableValues;
}

public IRubyObject[] getValues() {
return variableValues;
}
Expand Down
17 changes: 17 additions & 0 deletions spec/ruby/core/binding/dup_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,21 @@
bind.frozen?.should == true
bind.dup.frozen?.should == false
end

it "retains original binding variables but the list is distinct" do
bind1 = binding
eval "a = 1", bind1

bind2 = bind1.dup
eval("a = 2", bind2)
eval("a", bind1).should == 2
eval("a", bind2).should == 2

eval("b = 2", bind2)
-> { eval("b", bind1) }.should raise_error(NameError)
eval("b", bind2).should == 2

bind1.local_variables.sort.should == [:a, :bind1, :bind2]
bind2.local_variables.sort.should == [:a, :b, :bind1, :bind2]
end
end
2 changes: 1 addition & 1 deletion spec/tags/ruby/core/kernel/eval_tags.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
fails:Kernel#eval raises a LocalJumpError if there is no lambda-style closure in the chain
fails:Kernel#eval with a magic encoding comment ignores the magic encoding comment if it is after a frozen_string_literal magic comment
fails:Kernel#eval makes flip-flop operator work correctly

1 change: 0 additions & 1 deletion spec/tags/ruby/library/erb/result_tags.txt

This file was deleted.

1 change: 0 additions & 1 deletion spec/tags/ruby/library/erb/run_tags.txt

This file was deleted.