Skip to content

Commit d2baccc

Browse files
committed
[GR-44200] Support foreign big integers
PullRequest: truffleruby/3666
2 parents ead9146 + 2f6d1dc commit d2baccc

File tree

14 files changed

+304
-64
lines changed

14 files changed

+304
-64
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ New features:
44

55
* Updated to Ruby 3.1.3 (#2733, @andrykonchin, @eregon).
66
* `foreign_object.is_a?(foreign_meta_object)` is now supported (@eregon).
7+
* Foreign big integers are now supported and work with all `Numeric` operators (@eregon).
78

89
Bug fixes:
910

@@ -91,6 +92,7 @@ Performance:
9192
Changes:
9293

9394
* Remove `Truffle::Interop.deproxy` as it is unsafe and not useful (@eregon).
95+
* Removed `Truffle::Interop.unbox_without_conversion` (should not be needed by user code) (@eregon).
9496

9597
# 22.3.0
9698

doc/contributor/interop_implicit_api.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ Format: `Ruby code` sends `InteropLibrary message`
3737
- `foreign_object.to_str` raises `NameError` otherwise
3838
- `foreign_object.to_a` converts to a Ruby `Array` with `Truffle::Interop.to_array(foreign_object)`
3939
- `foreign_object.to_ary` converts to a Ruby `Array` with `Truffle::Interop.to_array(foreign_object)`
40-
- `foreign_object.to_f` tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` or raises `NameError`
41-
- `foreign_object.to_i` tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` or raises `NameError`
40+
- `foreign_object.to_f` tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` and `asBigInteger().doubleValue()` or raises `NameError`
41+
- `foreign_object.to_i` tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` and `asBigInteger()` or raises `NameError`
4242
- `foreign_object.equal?(other)` sends `isIdentical(foreign_object, other)`
4343
- `foreign_object.eql?(other)` sends `isIdentical(foreign_object, other)`
4444
- `foreign_object.object_id` sends `identityHashCode(foreign_object)` when `hasIdentity()` is true (which might not be unique)
@@ -55,7 +55,7 @@ Use `.respond_to?` for calling `InteropLibrary` predicates:
5555
- `foreign_object.respond_to?(:to_a)` sends `hasArrayElements(foreign_object)`
5656
- `foreign_object.respond_to?(:to_ary)` sends `hasArrayElements(foreign_object)`
5757
- `foreign_object.respond_to?(:to_f)` sends `fitsInDouble()`
58-
- `foreign_object.respond_to?(:to_i)` sends `fitsInLong()`
58+
- `foreign_object.respond_to?(:to_i)` sends `fitsInBigInteger()`
5959
- `foreign_object.respond_to?(:size)` sends `hasArrayElements(foreign_object)`
6060
- `foreign_object.respond_to?(:call)` sends `isExecutable(foreign_object)`
6161
- `foreign_object.respond_to?(:new)` sends `isInstantiable(foreign_object)`

spec/truffle/interop/foreign_inspect_to_s_spec.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@
8888
describe "Java BigInteger" do
8989
it "gives a similar representation to Ruby" do
9090
foreign = Truffle::Interop.java_type("java.math.BigInteger").new('14')
91-
# TODO: simplify to just ForeignNumber
92-
foreign.inspect.should =~ /\A#<Polyglot::Foreign(Number|Object)\[Java\] java\.math\.BigInteger:0x\h+>\z/
93-
foreign.to_s.should =~ /\A#<Polyglot::Foreign(Number|Object)\[Java\] 14>\z/
91+
foreign.inspect.should =~ /\A#<Polyglot::ForeignNumber\[Java\] java\.math\.BigInteger 14>\z/
92+
foreign.to_s.should =~ /\A#<Polyglot::ForeignNumber\[Java\] 14>\z/
9493
end
9594
end
9695
end
@@ -112,6 +111,22 @@
112111
end
113112
end
114113

114+
describe "number" do
115+
it "gives a similar representation to Ruby" do
116+
foreign = Truffle::Debug.foreign_boxed_value(42)
117+
foreign.inspect.should == "#<Polyglot::ForeignNumber 42>"
118+
foreign.to_s.should == "#<Polyglot::ForeignNumber 42>"
119+
120+
foreign = Truffle::Debug.foreign_boxed_value(1 << 84)
121+
foreign.inspect.should == "#<Polyglot::ForeignNumber[Ruby] Integer 19342813113834066795298816>"
122+
foreign.to_s.should == "#<Polyglot::ForeignNumber[Ruby] 19342813113834066795298816>"
123+
124+
foreign = Truffle::Debug.foreign_boxed_value(3.14)
125+
foreign.inspect.should == "#<Polyglot::ForeignNumber 3.14>"
126+
foreign.to_s.should == "#<Polyglot::ForeignNumber 3.14>"
127+
end
128+
end
129+
115130
describe "executable" do
116131
it "gives a similar representation to Ruby" do
117132
foreign = Truffle::Debug.foreign_executable(14)

spec/truffle/interop/polyglot/class_spec.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Truffle::Debug.foreign_iterator.class.should == Polyglot::ForeignIterator
2222
Truffle::Debug.java_null.class.should == Polyglot::ForeignNull
2323
Truffle::Debug.foreign_boxed_value(42).class.should == Polyglot::ForeignNumber
24+
Truffle::Debug.foreign_boxed_value(1 << 84).class.should == Polyglot::ForeignNumber
2425
Truffle::Debug.foreign_boxed_value(3.14).class.should == Polyglot::ForeignNumber
2526
Truffle::Debug.foreign_pointer(0x1234).class.should == Polyglot::ForeignPointer
2627
Truffle::Debug.foreign_string("foo").class.should == Polyglot::ForeignString
@@ -39,6 +40,7 @@
3940
Truffle::Interop.proxy_foreign_object((1..3).each).class.should == Polyglot::ForeignIterableIterator
4041
Truffle::Interop.proxy_foreign_object(nil).class.should == Polyglot::ForeignNull
4142
Truffle::Interop.proxy_foreign_object(42).class.should == Polyglot::ForeignNumber
43+
Truffle::Interop.proxy_foreign_object(1 << 84).class.should == Polyglot::ForeignNumber
4244
Truffle::Interop.proxy_foreign_object(3.14).class.should == Polyglot::ForeignNumber
4345
Truffle::Interop.proxy_foreign_object(Truffle::FFI::Pointer::NULL).class.should == Polyglot::ForeignPointer
4446
Truffle::Interop.proxy_foreign_object("foo").class.should == Polyglot::ForeignString
@@ -59,7 +61,7 @@
5961
Java.type('java.util.ArrayDeque').new.class.should == Polyglot::ForeignIterable
6062
Truffle::Interop.to_java_list([1, 2, 3]).iterator.class.should == Polyglot::ForeignIterator
6163
Truffle::Debug.java_null.class.should == Polyglot::ForeignNull
62-
# ForeignNumber
64+
Java.type('java.math.BigInteger').valueOf(42).class.should == Polyglot::ForeignNumber
6365
# ForeignPointer
6466
Java.type('java.lang.String').new("foo").class.should == Polyglot::ForeignString
6567
Truffle::Interop.to_java_string("foo").class.should == Polyglot::ForeignString
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright (c) 2022, 2023 Oracle and/or its affiliates. All rights reserved. This
2+
# code is released under a tri EPL/GPL/LGPL license. You can use it,
3+
# redistribute it and/or modify it under the terms of the:
4+
#
5+
# Eclipse Public License version 2.0, or
6+
# GNU General Public License version 2, or
7+
# GNU Lesser General Public License version 2.1.
8+
9+
require_relative '../../../ruby/spec_helper'
10+
11+
describe "Polyglot::ForeignNumber" do
12+
before :each do
13+
@numbers = [
14+
[Truffle::Debug.foreign_boxed_value(42), 42],
15+
[Truffle::Debug.foreign_boxed_value(1 << 84), 1 << 84],
16+
[Truffle::Debug.foreign_boxed_value(3.14), 3.14],
17+
]
18+
end
19+
20+
it "supports #==" do
21+
@numbers.each do |foreign, ruby|
22+
foreign.should == ruby
23+
foreign.should == foreign
24+
ruby.should == foreign
25+
end
26+
end
27+
28+
it "supports #+@" do
29+
@numbers.each do |foreign, ruby|
30+
(+foreign).should == (+ruby)
31+
end
32+
end
33+
34+
it "supports #-@" do
35+
@numbers.each do |foreign, ruby|
36+
(-foreign).should == (-ruby)
37+
end
38+
end
39+
40+
it "supports #+" do
41+
@numbers.each do |foreign, ruby|
42+
(foreign + 3).should == (ruby + 3)
43+
(3 + foreign).should == (3 + ruby)
44+
end
45+
end
46+
47+
it "supports #-" do
48+
@numbers.each do |foreign, ruby|
49+
(foreign - 3).should == (ruby - 3)
50+
(3 - foreign).should == (3 - ruby)
51+
end
52+
end
53+
54+
it "supports #*" do
55+
@numbers.each do |foreign, ruby|
56+
(foreign * 3).should == (ruby * 3)
57+
(3 * foreign).should == (3 * ruby)
58+
end
59+
end
60+
61+
it "supports #/" do
62+
@numbers.each do |foreign, ruby|
63+
(foreign / 3).should == (ruby / 3)
64+
(3 / foreign).should == (3 / ruby)
65+
end
66+
end
67+
68+
it "supports #**" do
69+
@numbers.each do |foreign, ruby|
70+
(foreign ** 2).should == (ruby ** 2)
71+
end
72+
end
73+
74+
it "does not support odd? yet" do
75+
@numbers.each do |foreign, _ruby|
76+
-> { foreign.odd? }.should raise_error(Polyglot::UnsupportedMessageError)
77+
end
78+
end
79+
end

spec/truffle/interop/special_forms_spec.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@
270270
-> { pfo.to_a }.should raise_error(NameError)
271271
end
272272

273-
it doc['.to_f', 'tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` or raises `NameError`'] do
273+
it doc['.to_f', 'tries to converts to a Ruby `Float` using `asDouble()` and `(double) asLong()` and `asBigInteger().doubleValue()` or raises `NameError`'] do
274274
pfo, _, l = proxy[42]
275275
pfo.to_f.should.eql?(42.0)
276276
l.log.should include(["fitsInDouble"])
@@ -282,12 +282,18 @@
282282
l.log.should include(["fitsInDouble"])
283283
l.log.should include(["asLong"])
284284

285+
does_not_fit_perfectly_in_double = (1 << 84) + 1
286+
pfo, _, l = proxy[does_not_fit_perfectly_in_double]
287+
pfo.to_f.should.eql?(does_not_fit_perfectly_in_double.to_f)
288+
l.log.should include(["fitsInDouble"])
289+
l.log.should include(["asBigInteger"])
290+
285291
pfo, _, l = proxy[Object.new]
286292
-> { pfo.to_f }.should raise_error(NameError, /to_f/)
287293
l.log.should include(["isNumber"])
288294
end
289295

290-
it doc['.to_i', 'tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` or raises `NameError`'] do
296+
it doc['.to_i', 'tries to converts to a Ruby `Integer` using `asInt()` and `asLong()` and `asBigInteger()` or raises `NameError`'] do
291297
pfo, _, l = proxy[42]
292298
pfo.to_i.should.eql?(42)
293299
l.log.should include(["fitsInInt"])
@@ -298,6 +304,11 @@
298304
l.log.should include(["fitsInLong"])
299305
l.log.should include(["asLong"])
300306

307+
pfo, _, l = proxy[1 << 84]
308+
pfo.to_i.should.eql?(1 << 84)
309+
l.log.should include(["fitsInBigInteger"])
310+
l.log.should include(["asBigInteger"])
311+
301312
pfo, _, l = proxy[Object.new]
302313
-> { pfo.to_i }.should raise_error(NameError, /to_i/)
303314
l.log.should include(["isNumber"])
@@ -403,10 +414,10 @@
403414
l.log.should include(["fitsInDouble"])
404415
end
405416

406-
it doc['.respond_to?(:to_i)', 'sends `fitsInLong()`'] do
417+
it doc['.respond_to?(:to_i)', 'sends `fitsInBigInteger()`'] do
407418
pfo, _, l = proxy[42]
408419
pfo.should.respond_to?(:to_i)
409-
l.log.should include(["fitsInLong"])
420+
l.log.should include(["fitsInBigInteger"])
410421
end
411422

412423
it description['.respond_to?(:size)', :hasArrayElements] do

src/main/java/org/truffleruby/core/numeric/RubyBignum.java

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111

1212
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
1313
import com.oracle.truffle.api.interop.InteropLibrary;
14+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
1415
import com.oracle.truffle.api.library.CachedLibrary;
1516
import com.oracle.truffle.api.library.ExportLibrary;
1617
import com.oracle.truffle.api.library.ExportMessage;
1718
import org.truffleruby.RubyContext;
1819
import org.truffleruby.core.klass.RubyClass;
1920
import org.truffleruby.language.ImmutableRubyObjectNotCopyable;
2021

22+
import java.math.BigDecimal;
2123
import java.math.BigInteger;
2224

2325
@ExportLibrary(InteropLibrary.class)
@@ -34,6 +36,11 @@ public RubyBignum(BigInteger value) {
3436
this.value = value;
3537
}
3638

39+
@TruffleBoundary
40+
private int bitLength() {
41+
return value.bitLength();
42+
}
43+
3744
@TruffleBoundary
3845
@Override
3946
public String toString() {
@@ -58,4 +65,129 @@ public RubyClass getMetaObject(
5865
return RubyContext.get(node).getCoreLibrary().integerClass;
5966
}
6067
// endregion
68+
69+
// region Number messages
70+
@ExportMessage
71+
boolean isNumber() {
72+
return true;
73+
}
74+
75+
@ExportMessage
76+
boolean fitsInByte() {
77+
return bitLength() < Byte.SIZE;
78+
}
79+
80+
@ExportMessage
81+
boolean fitsInShort() {
82+
return bitLength() < Short.SIZE;
83+
}
84+
85+
@ExportMessage
86+
boolean fitsInInt() {
87+
return bitLength() < Integer.SIZE;
88+
}
89+
90+
@ExportMessage
91+
boolean fitsInLong() {
92+
return bitLength() < Long.SIZE;
93+
}
94+
95+
@ExportMessage
96+
boolean fitsInBigInteger() {
97+
return true;
98+
}
99+
100+
@TruffleBoundary
101+
@ExportMessage
102+
boolean fitsInFloat() {
103+
if (bitLength() <= 24) { // 24 = size of float mantissa + 1
104+
return true;
105+
} else {
106+
float floatValue = value.floatValue();
107+
if (!Float.isFinite(floatValue)) {
108+
return false;
109+
}
110+
return new BigDecimal(floatValue).toBigIntegerExact().equals(value);
111+
}
112+
}
113+
114+
@TruffleBoundary
115+
@ExportMessage
116+
boolean fitsInDouble() {
117+
if (bitLength() <= 53) { // 53 = size of double mantissa + 1
118+
return true;
119+
} else {
120+
double doubleValue = value.doubleValue();
121+
if (!Double.isFinite(doubleValue)) {
122+
return false;
123+
}
124+
return new BigDecimal(doubleValue).toBigIntegerExact().equals(value);
125+
}
126+
}
127+
128+
@TruffleBoundary
129+
@ExportMessage
130+
byte asByte() throws UnsupportedMessageException {
131+
try {
132+
return value.byteValueExact();
133+
} catch (ArithmeticException e) {
134+
throw UnsupportedMessageException.create();
135+
}
136+
}
137+
138+
@TruffleBoundary
139+
@ExportMessage
140+
short asShort() throws UnsupportedMessageException {
141+
try {
142+
return value.shortValueExact();
143+
} catch (ArithmeticException e) {
144+
throw UnsupportedMessageException.create();
145+
}
146+
}
147+
148+
@TruffleBoundary
149+
@ExportMessage
150+
int asInt() throws UnsupportedMessageException {
151+
try {
152+
return value.intValueExact();
153+
} catch (ArithmeticException e) {
154+
throw UnsupportedMessageException.create();
155+
}
156+
}
157+
158+
@TruffleBoundary
159+
@ExportMessage
160+
long asLong() throws UnsupportedMessageException {
161+
try {
162+
return value.longValueExact();
163+
} catch (ArithmeticException e) {
164+
throw UnsupportedMessageException.create();
165+
}
166+
}
167+
168+
@TruffleBoundary
169+
@ExportMessage
170+
float asFloat() throws UnsupportedMessageException {
171+
if (fitsInFloat()) {
172+
return value.floatValue();
173+
} else {
174+
throw UnsupportedMessageException.create();
175+
}
176+
}
177+
178+
@TruffleBoundary
179+
@ExportMessage
180+
double asDouble() throws UnsupportedMessageException {
181+
if (fitsInDouble()) {
182+
return value.doubleValue();
183+
} else {
184+
throw UnsupportedMessageException.create();
185+
}
186+
}
187+
188+
@ExportMessage
189+
BigInteger asBigInteger() {
190+
return value;
191+
}
192+
// endregion
61193
}

0 commit comments

Comments
 (0)