Skip to content

Commit b981115

Browse files
author
Nicolas Laurent
committed
[GR-29687] Fix control flow bug when assigning constants using ||= (#1489)
PullRequest: truffleruby/2448
2 parents b97777e + e76a460 commit b981115

File tree

14 files changed

+321
-109
lines changed

14 files changed

+321
-109
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Bug fixes:
2020
* Fix `Truffle::Interop.source_location` to return unavailable source sections for modules instead of null (#2257).
2121
* Fix usage of `Thread.handle_interrupt` in `MonitorMixin#mon_synchronize`.
2222
* Fixed `TruffleRuby.synchronized` to handle guest safepoints (#2277).
23+
* Fix control flow bug when assigning constants using ||= (#1489).
2324

2425
Compatibility:
2526

@@ -87,7 +88,7 @@ Changes:
8788

8889
Release notes:
8990

90-
* The new IRB is quite slow when copy/pasting code into it. This is due to an inefficient `io/console` implementation which will be addressed in the next release. A workaround is to use `irb --readline`, which disables some IRB features but is much faster for copy/pasting code.
91+
* The new IRB is quite slow when copy/pasting code into it. This is due to an inefficient `io/console` implementation which will be addressed in the next release. A workaround is to use `irb --readline`, which disables some IRB features but is much faster for copy/pasting code.
9192

9293
New features:
9394

@@ -184,7 +185,7 @@ Compatibility:
184185
* Pass the final `super` specs (#2104, @chrisseaton).
185186
* Fix arity for arguments with optional kwargs (#1669, @ssnickolay)
186187
* Fix arity for `Proc` (#2098, @ssnickolay)
187-
* Check bounds for `FFI::Pointer` accesses when the size of the memory behind is known.
188+
* Check bounds for `FFI::Pointer` accesses when the size of the memory behind is known.
188189
* Implement negative line numbers for eval (#1482).
189190
* Support refinements for `#to_s` called by string interpolation (#2110, @ssnickolay)
190191
* Module#using raises error in method scope (#2112, @ssnickolay)

spec/mspec/lib/mspec/helpers.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
require 'mspec/helpers/ruby_exe'
1111
require 'mspec/helpers/scratch'
1212
require 'mspec/helpers/tmp'
13-
require 'mspec/helpers/warning'
13+
require 'mspec/helpers/warning'

spec/mspec/lib/mspec/helpers/warning.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require 'mspec/guards/version'
22

3+
# You might be looking for #silence_warnings, use #suppress_warning instead.
4+
# MSpec calls it #suppress_warning for consistency with EnvUtil.suppress_warning in CRuby test/.
35
def suppress_warning
46
verbose = $VERBOSE
57
$VERBOSE = nil

spec/ruby/language/constants_spec.rb

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -381,52 +381,6 @@ class << self
381381
ConstantSpecs::ClassA.constx.should == :CS_CONSTX
382382
ConstantSpecs::ClassA.new.constx.should == :CS_CONSTX
383383
end
384-
385-
describe "with ||=" do
386-
it "assigns a scoped constant if previously undefined" do
387-
ConstantSpecs.should_not have_constant(:OpAssignUndefined)
388-
module ConstantSpecs
389-
OpAssignUndefined ||= 42
390-
end
391-
ConstantSpecs::OpAssignUndefined.should == 42
392-
ConstantSpecs::OpAssignUndefinedOutside ||= 42
393-
ConstantSpecs::OpAssignUndefinedOutside.should == 42
394-
ConstantSpecs.send(:remove_const, :OpAssignUndefined)
395-
ConstantSpecs.send(:remove_const, :OpAssignUndefinedOutside)
396-
end
397-
398-
it "assigns a global constant if previously undefined" do
399-
OpAssignGlobalUndefined ||= 42
400-
::OpAssignGlobalUndefinedExplicitScope ||= 42
401-
OpAssignGlobalUndefined.should == 42
402-
::OpAssignGlobalUndefinedExplicitScope.should == 42
403-
Object.send :remove_const, :OpAssignGlobalUndefined
404-
Object.send :remove_const, :OpAssignGlobalUndefinedExplicitScope
405-
end
406-
407-
end
408-
409-
describe "with &&=" do
410-
it "re-assigns a scoped constant if already true" do
411-
module ConstantSpecs
412-
OpAssignTrue = true
413-
end
414-
suppress_warning do
415-
ConstantSpecs::OpAssignTrue &&= 1
416-
end
417-
ConstantSpecs::OpAssignTrue.should == 1
418-
ConstantSpecs.send :remove_const, :OpAssignTrue
419-
end
420-
421-
it "leaves scoped constant if not true" do
422-
module ConstantSpecs
423-
OpAssignFalse = false
424-
end
425-
ConstantSpecs::OpAssignFalse &&= 1
426-
ConstantSpecs::OpAssignFalse.should == false
427-
ConstantSpecs.send :remove_const, :OpAssignFalse
428-
end
429-
end
430384
end
431385

432386
describe "Constant resolution within a singleton class (class << obj)" do
@@ -775,4 +729,4 @@ class PrivateClass
775729
eval("mod::ἍBB").should == 1
776730
end
777731
end
778-
end
732+
end

spec/ruby/language/optional_assignments_spec.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require_relative '../spec_helper'
2+
require_relative '../fixtures/constants'
23

34
describe 'Optional variable assignments' do
45
describe 'using ||=' do
@@ -351,3 +352,107 @@ def []=(k, v)
351352
end
352353
end
353354
end
355+
356+
describe 'Optional constant assignment' do
357+
describe 'with ||=' do
358+
it "assigns a scoped constant if previously undefined" do
359+
ConstantSpecs.should_not have_constant(:OpAssignUndefined)
360+
module ConstantSpecs
361+
OpAssignUndefined ||= 42
362+
end
363+
ConstantSpecs::OpAssignUndefined.should == 42
364+
ConstantSpecs::OpAssignUndefinedOutside ||= 42
365+
ConstantSpecs::OpAssignUndefinedOutside.should == 42
366+
ConstantSpecs.send(:remove_const, :OpAssignUndefined)
367+
ConstantSpecs.send(:remove_const, :OpAssignUndefinedOutside)
368+
end
369+
370+
it "assigns a global constant if previously undefined" do
371+
OpAssignGlobalUndefined ||= 42
372+
::OpAssignGlobalUndefinedExplicitScope ||= 42
373+
OpAssignGlobalUndefined.should == 42
374+
::OpAssignGlobalUndefinedExplicitScope.should == 42
375+
Object.send :remove_const, :OpAssignGlobalUndefined
376+
Object.send :remove_const, :OpAssignGlobalUndefinedExplicitScope
377+
end
378+
379+
it 'correctly defines non-existing constants' do
380+
ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT1 ||= :assigned
381+
ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT1.should == :assigned
382+
end
383+
384+
it 'correctly overwrites nil constants' do
385+
suppress_warning do # already initialized constant
386+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1 = nil
387+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1 ||= :assigned
388+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1.should == :assigned
389+
end
390+
end
391+
392+
it 'causes side-effects of the module part to be applied only once (for undefined constant)' do
393+
x = 0
394+
(x += 1; ConstantSpecs::ClassA)::OR_ASSIGNED_CONSTANT2 ||= :assigned
395+
x.should == 1
396+
ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT2.should == :assigned
397+
end
398+
399+
it 'causes side-effects of the module part to be applied (for nil constant)' do
400+
suppress_warning do # already initialized constant
401+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT2 = nil
402+
x = 0
403+
(x += 1; ConstantSpecs::ClassA)::NIL_OR_ASSIGNED_CONSTANT2 ||= :assigned
404+
x.should == 1
405+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT2.should == :assigned
406+
end
407+
end
408+
409+
it 'does not evaluate the right-hand side if the module part raises an exception (for undefined constant)' do
410+
x = 0
411+
y = 0
412+
413+
-> {
414+
(x += 1; raise Exception; ConstantSpecs::ClassA)::OR_ASSIGNED_CONSTANT3 ||= (y += 1; :assigned)
415+
}.should raise_error(Exception)
416+
417+
x.should == 1
418+
y.should == 0
419+
defined?(ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT3).should == nil
420+
end
421+
422+
it 'does not evaluate the right-hand side if the module part raises an exception (for nil constant)' do
423+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT3 = nil
424+
x = 0
425+
y = 0
426+
427+
-> {
428+
(x += 1; raise Exception; ConstantSpecs::ClassA)::NIL_OR_ASSIGNED_CONSTANT3 ||= (y += 1; :assigned)
429+
}.should raise_error(Exception)
430+
431+
x.should == 1
432+
y.should == 0
433+
ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT3.should == nil
434+
end
435+
end
436+
437+
describe "with &&=" do
438+
it "re-assigns a scoped constant if already true" do
439+
module ConstantSpecs
440+
OpAssignTrue = true
441+
end
442+
suppress_warning do
443+
ConstantSpecs::OpAssignTrue &&= 1
444+
end
445+
ConstantSpecs::OpAssignTrue.should == 1
446+
ConstantSpecs.send :remove_const, :OpAssignTrue
447+
end
448+
449+
it "leaves scoped constant if not true" do
450+
module ConstantSpecs
451+
OpAssignFalse = false
452+
end
453+
ConstantSpecs::OpAssignFalse &&= 1
454+
ConstantSpecs::OpAssignFalse.should == false
455+
ConstantSpecs.send :remove_const, :OpAssignFalse
456+
end
457+
end
458+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.language.constants;
11+
12+
import com.oracle.truffle.api.exception.AbstractTruffleException;
13+
import org.truffleruby.language.control.RaiseException;
14+
15+
/** Wraps a {@link RaiseException} occuring during a module autoload, whenever the autoloaded constant is itself the
16+
* module for another constant read. */
17+
public class AutoloadException extends AbstractTruffleException {
18+
19+
private static final long serialVersionUID = 1L;
20+
21+
public final RaiseException raiseException;
22+
23+
public AutoloadException(RaiseException raiseException) {
24+
this.raiseException = raiseException;
25+
}
26+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.language.constants;
11+
12+
import com.oracle.truffle.api.CompilerDirectives;
13+
import com.oracle.truffle.api.frame.VirtualFrame;
14+
import com.oracle.truffle.api.profiles.ConditionProfile;
15+
import org.truffleruby.core.cast.BooleanCastNode;
16+
import org.truffleruby.core.cast.BooleanCastNodeGen;
17+
import org.truffleruby.core.module.RubyModule;
18+
import org.truffleruby.language.RubyConstant;
19+
import org.truffleruby.language.RubyContextSourceNode;
20+
import org.truffleruby.utils.RunTwiceBranchProfile;
21+
22+
/** e.g. for {@code module::FOO ||= 1}
23+
*
24+
* <p>
25+
* We need a separate class for this because we need to check if the constant is defined. Doing so will evaluate the
26+
* module part, which will be evaluated again when assigning the constant. If evaluating the module part has any
27+
* side-effect, this is incorrect and differs from MRI semantics. */
28+
public class OrAssignConstantNode extends RubyContextSourceNode {
29+
30+
@Child protected ReadConstantNode readConstant;
31+
@Child protected WriteConstantNode writeConstant;
32+
@Child private BooleanCastNode cast;
33+
34+
private final ConditionProfile triviallyUndefined = ConditionProfile.create();
35+
private final ConditionProfile defined = ConditionProfile.create();
36+
private final RunTwiceBranchProfile writeTwiceProfile = new RunTwiceBranchProfile();
37+
38+
public OrAssignConstantNode(ReadConstantNode readConstant, WriteConstantNode writeConstant) {
39+
this.readConstant = readConstant;
40+
this.writeConstant = writeConstant;
41+
}
42+
43+
@Override
44+
public Object execute(VirtualFrame frame) {
45+
46+
if (triviallyUndefined.profile(readConstant.isModuleTriviallyUndefined(frame, getLanguage(), getContext()))) {
47+
// It might not be defined because of autoloaded constants (maybe other reasons?),
48+
// simply attempt writing (which will trigger autoloads if required).
49+
// Since we didn't evaluate the module part yet, no side-effects can occur.
50+
writeTwiceProfile.enter();
51+
return writeConstant.execute(frame);
52+
}
53+
54+
// Conceptually, we want to rewrite `<x>::Foo ||= <y>` to `(defined?(<x>) && <x>) || (<x>::Foo = <y>)`
55+
// BUT, we want the side-effects of <x> (the module part) to be only triggered once.
56+
// We do let any exception raised bubble through. Normally they would be swallowed by `defined?`, but
57+
// MRI raises them anyway, *before* evaluation the right-hand side (which is a different behaviour from
58+
// regular constant assignment, which evaluates the right-hand side first).
59+
final RubyModule module = readConstant.evaluateModule(frame);
60+
61+
// Next we check if the constant itself is defined, and if it is, we get its value.
62+
final RubyConstant constant = readConstant.getConstantIfDefined(module);
63+
64+
final boolean isDefined = defined.profile(constant != null);
65+
66+
final Object value = isDefined
67+
? readConstant.getConstant(module, constant)
68+
: null;
69+
70+
// Write if the constant is undefined or if its value is falsy.
71+
if (!isDefined || !castToBoolean(value)) {
72+
writeTwiceProfile.enter();
73+
return writeConstant.execute(frame, module);
74+
} else {
75+
return value;
76+
}
77+
}
78+
79+
private boolean castToBoolean(final Object value) {
80+
if (cast == null) {
81+
CompilerDirectives.transferToInterpreterAndInvalidate();
82+
cast = insert(BooleanCastNodeGen.create(null));
83+
}
84+
return cast.executeToBoolean(value);
85+
}
86+
}

0 commit comments

Comments
 (0)