diff --git a/VERSION b/VERSION index 71a0fc5c5a4..4d1ad8c631a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.4.5.0-SNAPSHOT +9.4.6.0-SNAPSHOT diff --git a/core/pom.xml b/core/pom.xml index faabf1e0e12..3160e0694fa 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-parent - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT jruby-base JRuby Base @@ -711,7 +711,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-base - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT diff --git a/core/src/main/java/org/jruby/FiberScheduler.java b/core/src/main/java/org/jruby/FiberScheduler.java new file mode 100644 index 00000000000..0a4af4b33ba --- /dev/null +++ b/core/src/main/java/org/jruby/FiberScheduler.java @@ -0,0 +1,210 @@ +package org.jruby; + +import jnr.constants.platform.Errno; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.io.OpenFile; + +import java.nio.ByteBuffer; + +public class FiberScheduler { + // MRI: rb_fiber_scheduler_kernel_sleep + public static IRubyObject kernelSleep(ThreadContext context, IRubyObject scheduler, IRubyObject timeout) { + return scheduler.callMethod(context, "kernel_sleep", timeout); + } + + // MRI: rb_fiber_scheduler_kernel_sleepv + public static IRubyObject kernelSleep(ThreadContext context, IRubyObject scheduler, IRubyObject[] args) { + return scheduler.callMethod(context, "kernel_sleep", args); + } + + // MRI: rb_fiber_scheduler_process_wait + public static IRubyObject processWait(ThreadContext context, IRubyObject scheduler, long pid, int flags) { + return Helpers.invokeChecked(context, scheduler, "process_wait", context.runtime.newFixnum(pid), context.runtime.newFixnum(flags)); + } + + // MRI: rb_fiber_scheduler_block + public static IRubyObject block(ThreadContext context, IRubyObject scheduler, IRubyObject blocker, IRubyObject timeout) { + return Helpers.invoke(context, scheduler, "block", blocker, timeout); + } + + // MRI: rb_fiber_scheduler_unblock + public static IRubyObject unblock(ThreadContext context, IRubyObject scheduler, IRubyObject blocker, IRubyObject fiber) { + return Helpers.invoke(context, scheduler, "unblock", blocker, fiber); + } + + // MRI: rb_fiber_scheduler_io_wait + public static IRubyObject ioWait(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject events, IRubyObject timeout) { + return Helpers.invoke(context, scheduler, "io_wait", io, events, timeout); + } + + // MRI: rb_fiber_scheduler_io_wait_readable + public static IRubyObject ioWaitReadable(ThreadContext context, IRubyObject scheduler, IRubyObject io) { + return ioWait(context, scheduler, io, context.runtime.newFixnum(OpenFile.READABLE), context.nil); + } + + // MRI: rb_fiber_scheduler_io_wait_writable + public static IRubyObject ioWaitWritable(ThreadContext context, IRubyObject scheduler, IRubyObject io) { + return ioWait(context, scheduler, io, context.runtime.newFixnum(OpenFile.WRITABLE), context.nil); + } + + // MRI: rb_fiber_scheduler_io_select + public static IRubyObject ioSelect(ThreadContext context, IRubyObject scheduler, IRubyObject readables, IRubyObject writables, IRubyObject exceptables, IRubyObject timeout) { + return ioSelectv(context, scheduler, readables, writables, exceptables, timeout); + } + + // MRI: rb_fiber_scheduler_io_selectv + public static IRubyObject ioSelectv(ThreadContext context, IRubyObject scheduler, IRubyObject... args) { + return Helpers.invokeChecked(context, scheduler, "io_select", args); + } + + // MRI: rb_fiber_scheduler_io_read + public static IRubyObject ioRead(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, int length, int offset) { + Ruby runtime = context.runtime; + return Helpers.invokeChecked(context, scheduler, "io_read", io, buffer, runtime.newFixnum(length), runtime.newFixnum(offset)); + } + + public static IRubyObject ioRead(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, RubyInteger length, RubyInteger offset) { + return Helpers.invokeChecked(context, scheduler, "io_read", io, buffer, length, offset); + } + + // MRI: rb_fiber_scheduler_io_pread + public static IRubyObject ioPRead(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, int from, int length, int offset) { + return Helpers.invokeChecked(context, scheduler, "io_pread", io, buffer, context.runtime.newFixnum(from), context.runtime.newFixnum(length), context.runtime.newFixnum(offset)); + } + + public static IRubyObject ioPRead(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, RubyInteger from, RubyInteger length, RubyInteger offset) { + return Helpers.invokeChecked(context, scheduler, "io_pread", io, buffer, from, length, offset); + } + + // MRI: rb_fiber_scheduler_io_write + public static IRubyObject ioWrite(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, int length, int offset) { + Ruby runtime = context.runtime; + return Helpers.invokeChecked(context, scheduler, "io_write", io, buffer, runtime.newFixnum(length), runtime.newFixnum(offset)); + } + + public static IRubyObject ioWrite(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, RubyInteger length, RubyInteger offset) { + return Helpers.invokeChecked(context, scheduler, "io_write", io, buffer, length, offset); + } + + // MRI: rb_fiber_scheduler_io_pwrite + public static IRubyObject ioPWrite(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, int from, int length, int offset) { + return Helpers.invokeChecked(context, scheduler, "io_pwrite", io, buffer, context.runtime.newFixnum(from), context.runtime.newFixnum(length), context.runtime.newFixnum(offset)); + } + + public static IRubyObject ioPWrite(ThreadContext context, IRubyObject scheduler, IRubyObject io, IRubyObject buffer, RubyInteger from, RubyInteger length, RubyInteger offset) { + return Helpers.invokeChecked(context, scheduler, "io_pwrite", io, buffer, from, length, offset); + } + + // MRI: rb_fiber_scheduler_io_read_memory + public static IRubyObject ioReadMemory(ThreadContext context, IRubyObject scheduler, IRubyObject io, ByteBuffer base, int size, int length) { + RubyIOBuffer buffer = RubyIOBuffer.newBuffer(context.runtime, base, size, RubyIOBuffer.LOCKED); + + IRubyObject result = ioRead(context, scheduler, io, buffer, length, 0); + + buffer.unlock(context); + buffer.free(context); + + return result; + } + + // MRI: rb_fiber_scheduler_io_pread_memory + public static IRubyObject ioPReadMemory(ThreadContext context, IRubyObject scheduler, IRubyObject io, ByteBuffer base, int from, int size, int length) { + RubyIOBuffer buffer = RubyIOBuffer.newBuffer(context.runtime, base, size, RubyIOBuffer.LOCKED); + + IRubyObject result = ioPRead(context, scheduler, io, buffer, from, length, 0); + + buffer.unlock(context); + buffer.free(context); + + return result; + } + + // MRI: rb_fiber_scheduler_io_write_memory + public static IRubyObject ioWriteMemory(ThreadContext context, IRubyObject scheduler, IRubyObject io, ByteBuffer base, int size, int length) { + RubyIOBuffer buffer = RubyIOBuffer.newBuffer(context.runtime, base, size, RubyIOBuffer.LOCKED | RubyIOBuffer.READONLY); + + IRubyObject result = ioWrite(context, scheduler, io, buffer, length, 0); + + buffer.unlock(context); + buffer.free(context); + + return result; + } + + // MRI: p + public static IRubyObject ioPWriteMemory(ThreadContext context, IRubyObject scheduler, IRubyObject io, ByteBuffer base, int from, int size, int length) { + RubyIOBuffer buffer = RubyIOBuffer.newBuffer(context.runtime, base, size, RubyIOBuffer.LOCKED | RubyIOBuffer.READONLY); + + IRubyObject result = ioPWrite(context, scheduler, io, buffer, from, length, 0); + + buffer.unlock(context); + buffer.free(context); + + return result; + } + + // MRI: rb_fiber_scheduler_io_close + public static IRubyObject ioClose(ThreadContext context, IRubyObject scheduler, IRubyObject io) { + return Helpers.invokeChecked(context, scheduler, "io_close", io); + } + + // MRI: rb_fiber_scheduler_address_resolve + public static IRubyObject addressResolve(ThreadContext context, IRubyObject scheduler, IRubyObject hostname) { + return Helpers.invokeChecked(context, scheduler, "address_resolve", hostname); + } + + // MRI: verify_scheduler + static void verifyInterface(IRubyObject scheduler) { + if (!scheduler.respondsTo("block")) { + throw scheduler.getRuntime().newArgumentError("Scheduler must implement #block"); + } + + if (!scheduler.respondsTo("unblock")) { + throw scheduler.getRuntime().newArgumentError("Scheduler must implement #unblock"); + } + + if (!scheduler.respondsTo("kernel_sleep")) { + throw scheduler.getRuntime().newArgumentError("Scheduler must implement #kernel_sleep"); + } + + if (!scheduler.respondsTo("io_wait")) { + throw scheduler.getRuntime().newArgumentError("Scheduler must implement #io_wait"); + } + } + + // MRI: rb_fiber_scheduler_close + public static IRubyObject close(ThreadContext context, IRubyObject scheduler) { +// VM_ASSERT(ruby_thread_has_gvl_p()); + + IRubyObject result; + + result = Helpers.invokeChecked(context, scheduler, "scheduler_close"); + if (result != null) return result; + + result = Helpers.invokeChecked(context, scheduler, "close"); + if (result != null) return result; + + return context.nil; + } + + // MRI: rb_fiber_scheduler_io_result_apply + public static int resultApply(ThreadContext context, IRubyObject result) { + int resultInt; + if (result instanceof RubyFixnum && (resultInt = RubyNumeric.num2int(result)) < 0) { + context.runtime.getPosix().errno(-resultInt); + return -1; + } else { + return RubyNumeric.num2int(result); + } + } + + public static IRubyObject result(Ruby runtime, int result, Errno error) { + if (result == -1) { + return RubyFixnum.newFixnum(runtime, error.value()); + } else { + return RubyFixnum.newFixnum(runtime, result); + } + } +} diff --git a/core/src/main/java/org/jruby/Ruby.java b/core/src/main/java/org/jruby/Ruby.java index 0a65b89ddde..4736f50fb18 100644 --- a/core/src/main/java/org/jruby/Ruby.java +++ b/core/src/main/java/org/jruby/Ruby.java @@ -443,6 +443,11 @@ private Ruby(RubyInstanceConfig config) { randomClass = null; } ioClass = RubyIO.createIOClass(this); + if (Options.FIBER_SCHEDULER.load()) { + ioBufferClass = RubyIOBuffer.createIOBufferClass(this); + } else { + ioBufferClass = null; + } structClass = profile.allowClass("Struct") ? RubyStruct.createStructClass(this) : null; bindingClass = profile.allowClass("Binding") ? RubyBinding.createBindingClass(this) : null; @@ -590,6 +595,7 @@ private void initBootLibraries() { loadService.provide("rational.rb"); loadService.provide("complex.rb"); loadService.provide("thread.rb"); + loadService.provide("fiber.rb"); loadService.provide("ruby2_keywords.rb"); // Load preludes @@ -1714,6 +1720,17 @@ private void initExceptions() { ifAllowed("KeyError", (ruby) -> keyError = RubyKeyError.define(ruby, indexError)); ifAllowed("DomainError", (ruby) -> mathDomainError = RubyDomainError.define(ruby, argumentError, mathModule)); + RubyClass runtimeError = this.runtimeError; + ObjectAllocator runtimeErrorAllocator = runtimeError.getAllocator(); + + if (Options.FIBER_SCHEDULER.load()) { + bufferLockedError = ioBufferClass.defineClassUnder("LockedError", runtimeError, runtimeErrorAllocator); + bufferAllocationError = ioBufferClass.defineClassUnder("AllocationError", runtimeError, runtimeErrorAllocator); + bufferAccessError = ioBufferClass.defineClassUnder("AccessError", runtimeError, runtimeErrorAllocator); + bufferInvalidatedError = ioBufferClass.defineClassUnder("InvalidatedError", runtimeError, runtimeErrorAllocator); + bufferMaskError = ioBufferClass.defineClassUnder("MaskError", runtimeError, runtimeErrorAllocator); + } + initErrno(); initNativeException(); @@ -2174,6 +2191,10 @@ public RubyClass getIO() { return ioClass; } + public RubyClass getIOBuffer() { + return ioBufferClass; + } + public RubyClass getThread() { return threadClass; } @@ -2485,6 +2506,26 @@ public RubyClass getInvalidByteSequenceError() { return invalidByteSequenceError; } + public RubyClass getBufferLockedError() { + return bufferLockedError; + } + + public RubyClass getBufferAllocationError() { + return bufferAllocationError; + } + + public RubyClass getBufferAccessError() { + return bufferAccessError; + } + + public RubyClass getBufferInvalidatedError() { + return bufferInvalidatedError; + } + + public RubyClass getBufferMaskError() { + return bufferMaskError; + } + @Deprecated RubyRandom.RandomType defaultRand; @@ -4246,6 +4287,26 @@ public RaiseException newInvalidByteSequenceError(String message) { return newRaiseException(getInvalidByteSequenceError(), message); } + public RaiseException newBufferLockedError(String message) { + return newRaiseException(getBufferLockedError(), message); + } + + public RaiseException newBufferAllocationError(String message) { + return newRaiseException(getBufferAllocationError(), message); + } + + public RaiseException newBufferAccessError(String message) { + return newRaiseException(getBufferAccessError(), message); + } + + public RaiseException newBufferInvalidatedError(String message) { + return newRaiseException(getBufferInvalidatedError(), message); + } + + public RaiseException newBufferMaskError(String message) { + return newRaiseException(getBufferMaskError(), message); + } + /** * Construct a new RaiseException wrapping a new Ruby exception object appropriate to the given exception class. * @@ -5361,6 +5422,7 @@ public RubyClass getData() { private final RubyClass fileClass; private final RubyClass fileStatClass; private final RubyClass ioClass; + private final RubyClass ioBufferClass; private final RubyClass threadClass; private final RubyClass threadGroupClass; private final RubyClass continuationClass; @@ -5422,6 +5484,11 @@ public RubyClass getData() { private RubyClass keyError; private RubyClass locationClass; private RubyClass interruptedRegexpError; + private RubyClass bufferLockedError; + private RubyClass bufferAllocationError; + private RubyClass bufferAccessError; + private RubyClass bufferInvalidatedError; + private RubyClass bufferMaskError; /** * All the core modules we keep direct references to, for quick access and diff --git a/core/src/main/java/org/jruby/RubyArray.java b/core/src/main/java/org/jruby/RubyArray.java index 70525ae0936..6bd4ce03b8d 100644 --- a/core/src/main/java/org/jruby/RubyArray.java +++ b/core/src/main/java/org/jruby/RubyArray.java @@ -2924,7 +2924,7 @@ public IRubyObject select_bang(ThreadContext context, Block block) { } finally { if (modified) checkFrozen(); - selectBangEnsure(runtime, len, beg, len0, len1); + selectBangEnsure(runtime, len0, len1); } } @@ -3082,17 +3082,20 @@ public IRubyObject rejectBang(ThreadContext context, Block block) { } finally { if (modified) checkFrozen(); - selectBangEnsure(runtime, realLength, beg, len0, len1); + selectBangEnsure(runtime, len0, len1); } } // MRI: select_bang_ensure - private void selectBangEnsure(final Ruby runtime, final int len, final int beg, - int i1, int i2) { - if (i2 < i1) { - realLength = len - i1 + i2; + private void selectBangEnsure(final Ruby runtime, int i1, int i2) { + int len = realLength; + + if (i2 < len && i2 < i1) { + int tail = 0; + int beg = begin; if (i1 < len) { - safeArrayCopy(runtime, values, beg + i1, values, beg + i2, len - i1); + tail = len - i1; + safeArrayCopy(runtime, values, beg + i1, values, beg + i2, tail); } else if (realLength > 0) { // nil out left-overs to avoid leaks (MRI doesn't) @@ -3102,6 +3105,7 @@ else if (realLength > 0) { throw concurrentModification(runtime, ex); } } + realLength = i2 + tail; } } diff --git a/core/src/main/java/org/jruby/RubyIO.java b/core/src/main/java/org/jruby/RubyIO.java index 33361dcc8a4..e224f8c9b91 100644 --- a/core/src/main/java/org/jruby/RubyIO.java +++ b/core/src/main/java/org/jruby/RubyIO.java @@ -86,6 +86,7 @@ import org.jruby.runtime.encoding.EncodingService; import org.jruby.util.ShellLauncher.POpenProcess; import org.jruby.util.*; +import org.jruby.util.cli.Options; import org.jruby.util.io.ChannelFD; import org.jruby.util.io.EncodingUtils; import org.jruby.util.io.FilenoUtil; @@ -3137,7 +3138,8 @@ IRubyObject getPartial(ThreadContext context, IRubyObject[] args, boolean nonblo fptr = getOpenFileChecked(); - final boolean locked = fptr.lock(); int n; + final boolean locked = fptr.lock(); + int n; try { fptr.checkByteReadable(context); @@ -3794,6 +3796,14 @@ public static RubyIO convertToIO(ThreadContext context, IRubyObject obj) { @JRubyMethod(name = "select", required = 1, optional = 3, checkArity = false, meta = true) public static IRubyObject select(ThreadContext context, IRubyObject recv, IRubyObject[] argv) { + if (Options.FIBER_SCHEDULER.load()) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioSelectv(context, scheduler, argv); + if (result != UNDEF) return result; + } + } + int argc = Arity.checkArgumentCount(context, argv, 1, 4); IRubyObject read, write, except, _timeout; @@ -4790,14 +4800,14 @@ public IRubyObject pread(ThreadContext context, IRubyObject len, IRubyObject off } @JRubyMethod(name = "pread") - public IRubyObject pread(ThreadContext context, IRubyObject len, IRubyObject offset, IRubyObject str) { + public IRubyObject pread(ThreadContext context, IRubyObject _length, IRubyObject _from, IRubyObject str) { Ruby runtime = context.runtime; - int count = len.convertToInteger().getIntValue(); - long off = offset.convertToInteger().getIntValue(); + int length = _length.convertToInteger().getIntValue(); + int from = _from.convertToInteger().getIntValue(); - RubyString string = EncodingUtils.setStrBuf(runtime, str, count); - if (count == 0) return string; + RubyString string = EncodingUtils.setStrBuf(runtime, str, length); + if (length == 0) return string; OpenFile fptr = getOpenFile(); fptr.checkByteReadable(context); @@ -4806,54 +4816,14 @@ public IRubyObject pread(ThreadContext context, IRubyObject len, IRubyObject off fptr.checkClosed(); ByteList strByteList = string.getByteList(); + int read; - try { - return context.getThread().executeTaskBlocking(context, fd, new RubyThread.Task() { - @Override - public IRubyObject run(ThreadContext context, ChannelFD channelFD) throws InterruptedException { - Ruby runtime = context.runtime; - - ByteBuffer wrap = ByteBuffer.wrap(strByteList.unsafeBytes(), strByteList.begin(), count); - int read = 0; - - try { - if (fd.chFile != null) { - read = fd.chFile.read(wrap, off); - - if (read == -1) { - throw runtime.newEOFError(); - } - } else if (fd.chNative != null) { - read = (int) runtime.getPosix().pread(fd.chNative.getFD(), wrap, count, off); - - if (read == 0) { - throw runtime.newEOFError(); - } else if (read == -1) { - throw runtime.newErrnoFromInt(runtime.getPosix().errno()); - } - } else if (fd.chRead != null) { - read = fd.chRead.read(wrap); - } else { - throw runtime.newIOError("not opened for reading"); - } - } catch (IOException ioe) { - throw Helpers.newIOErrorFromException(runtime, ioe); - } - - string.setReadLength(read); + ByteBuffer wrap = ByteBuffer.wrap(strByteList.unsafeBytes(), strByteList.begin(), length); + read = OpenFile.preadInternal(context, fptr, fd, wrap, from, length); - return string; - } + string.setReadLength(read); - @Override - public void wakeup(RubyThread thread, ChannelFD channelFD) { - // FIXME: NO! This will kill many native channels. Must be nonblocking to interrupt. - thread.getNativeThread().interrupt(); - } - }); - } catch (InterruptedException ie) { - throw context.runtime.newConcurrencyError("IO operation interrupted"); - } + return string; } @JRubyMethod(name = "pwrite") @@ -4868,7 +4838,7 @@ public IRubyObject pwrite(ThreadContext context, IRubyObject str, IRubyObject of string = str.convertToString(); } - long off = offset.convertToInteger().getLongValue(); + int off = offset.convertToInteger().getIntValue(); RubyIO io = GetWriteIO(); fptr = io.getOpenFile(); @@ -4879,41 +4849,14 @@ public IRubyObject pwrite(ThreadContext context, IRubyObject str, IRubyObject of ByteList strByteList = buf.getByteList(); - try { - return context.getThread().executeTaskBlocking(context, fd, new RubyThread.Task() { - @Override - public IRubyObject run(ThreadContext context, ChannelFD channelFD) throws InterruptedException { - Ruby runtime = context.runtime; - - int length = strByteList.realSize(); - ByteBuffer wrap = ByteBuffer.wrap(strByteList.unsafeBytes(), strByteList.begin(), length); - int written = 0; - - try { - if (fd.chFile != null) { - written = fd.chFile.write(wrap, off); - } else if (fd.chNative != null) { - written = (int) runtime.getPosix().pwrite(fd.chNative.getFD(), wrap, length, off); - } else if (fd.chWrite != null) { - written = fd.chWrite.write(wrap); - } else { - throw runtime.newIOError("not opened for writing"); - } - } catch (IOException ioe) { - throw Helpers.newIOErrorFromException(runtime, ioe); - } - return runtime.newFixnum(written); - } + int length = strByteList.realSize(); + ByteBuffer wrap = ByteBuffer.wrap(strByteList.unsafeBytes(), strByteList.begin(), length); - @Override - public void wakeup(RubyThread thread, ChannelFD channelFD) { - // FIXME: NO! This will kill many native channels. Must be nonblocking to interrupt. - thread.getNativeThread().interrupt(); - } - }); - } catch (InterruptedException ie) { - throw context.runtime.newConcurrencyError("IO operation interrupted"); - } + int written; + + written = OpenFile.pwriteInternal(context, fptr, fd, wrap, off, length); + + return context.runtime.newFixnum(written); } /** @@ -5262,7 +5205,7 @@ public final OpenFile MakeOpenFile() { rb_io_fptr_finalize(runtime, openFile); openFile = null; } - openFile = new OpenFile(runtime.getNil()); + openFile = new OpenFile(this, runtime.getNil()); runtime.addInternalFinalizer(openFile); return openFile; } diff --git a/core/src/main/java/org/jruby/RubyIOBuffer.java b/core/src/main/java/org/jruby/RubyIOBuffer.java new file mode 100644 index 00000000000..dfe2916cf58 --- /dev/null +++ b/core/src/main/java/org/jruby/RubyIOBuffer.java @@ -0,0 +1,2260 @@ +package org.jruby; + +import jnr.ffi.NativeType; +import jnr.ffi.Platform; +import jnr.ffi.Runtime; +import jnr.ffi.Type; +import org.jcodings.Encoding; +import org.jcodings.specific.ASCIIEncoding; +import org.jruby.anno.JRubyConstant; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Arity; +import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; +import org.jruby.util.cli.Options; +import org.jruby.util.io.ChannelFD; +import org.jruby.util.io.OpenFile; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.Arrays; + +import static org.jruby.RubyBoolean.newBoolean; + +public class RubyIOBuffer extends RubyObject { + + public static final Runtime FFI_RUNTIME = Runtime.getSystemRuntime(); + + public static RubyClass createIOBufferClass(Ruby runtime) { + RubyClass IOBuffer = runtime.getIO().defineClassUnder("Buffer", runtime.getObject(), RubyIOBuffer::new); + + IOBuffer.includeModule(runtime.getComparable()); + + IOBuffer.defineAnnotatedMethods(RubyIOBuffer.class); + IOBuffer.defineAnnotatedConstants(RubyIOBuffer.class); + + RubyClass IO = runtime.getIO(); + + IO.setConstant("READABLE", runtime.newFixnum(OpenFile.READABLE)); + IO.setConstant("WRITABLE", runtime.newFixnum(OpenFile.WRITABLE)); + + return IOBuffer; + } + + @JRubyConstant + public static final int PAGE_SIZE = 8196; + @JRubyConstant + public static final int DEFAULT_SIZE = 8196; + @JRubyConstant + public static final int EXTERNAL = 1; + @JRubyConstant + public static final int INTERNAL = 2; + @JRubyConstant + public static final int MAPPED = 4; + @JRubyConstant + public static final int SHARED = 8; + @JRubyConstant + public static final int LOCKED = 32; + @JRubyConstant + public static final int PRIVATE = 64; + @JRubyConstant + public static final int READONLY = 128; + @JRubyConstant + public static final int LITTLE_ENDIAN = 4; + @JRubyConstant + public static final int BIG_ENDIAN = 8; + @JRubyConstant + public static final int HOST_ENDIAN = Platform.getNativePlatform().isBigEndian() ? BIG_ENDIAN : LITTLE_ENDIAN; + @JRubyConstant + public static final int NETWORK_ENDIAN = BIG_ENDIAN; + + public static RubyIOBuffer newBuffer(Ruby runtime, ByteBuffer base, int size, int flags) { + if (base == null) return newBuffer(runtime, size, flags); + + return new RubyIOBuffer(runtime, runtime.getIOBuffer(), base, size, flags); + } + + public static RubyIOBuffer newBuffer(Ruby runtime, int size, int flags) { + return new RubyIOBuffer(runtime, runtime.getIOBuffer(), newBufferBase(runtime, size, flags), size, flags); + } + + public static RubyIOBuffer newBuffer(ThreadContext context, RubyString string, int flags) { + ByteList bytes = string.getByteList(); + int size = bytes.realSize(); + + return newBuffer(context.runtime, ByteBuffer.wrap(bytes.unsafeBytes(), bytes.begin(), size), size, flags); + } + + public RubyIOBuffer(Ruby runtime, RubyClass metaClass) { + super(runtime, metaClass); + } + + public RubyIOBuffer(Ruby runtime, RubyClass metaClass, ByteBuffer base, int size, int flags) { + super(runtime, metaClass); + + this.base = base; + this.size = size; + this.flags = flags; + } + + @JRubyMethod(name = "for", meta = true) + public static IRubyObject rbFor(ThreadContext context, IRubyObject self, IRubyObject _string, Block block) { + RubyString string = _string.convertToString(); + int flags = string.isFrozen() ? READONLY : 0; + + // If the string is frozen, both code paths are okay. + // If the string is not frozen, if a block is not given, it must be frozen. + if (!block.isGiven()) { + // This internally returns the source string if it's already frozen. + string = string.newFrozen(); + flags = READONLY; + } else { + if ((flags & READONLY) != READONLY) { + string.modify(); + } + } + + RubyIOBuffer buffer = newBuffer(context, string, flags); + + if (block.isGiven()) { + return block.yieldSpecific(context, buffer); + } + + return buffer; + } + + @JRubyMethod(meta = true) + public static IRubyObject string(ThreadContext context, IRubyObject self, IRubyObject _length, Block block) { + Ruby runtime = context.runtime; + + int size = _length.convertToInteger().getIntValue(); + if (size < 0) throw runtime.newArgumentError("negative string size (or size too big)"); + RubyString string = RubyString.newString(runtime, new byte[size]); + ByteList bytes = string.getByteList(); + ByteBuffer wrap = ByteBuffer.wrap(bytes.unsafeBytes(), bytes.begin(), size); + + RubyIOBuffer buffer = newBuffer(context.runtime, wrap, size, 0); + + block.yieldSpecific(context, buffer); + + return string; + } + + @JRubyMethod(name = "map", meta = true) + public static IRubyObject map(ThreadContext context, IRubyObject self, IRubyObject _file) { + RubyFile file = checkFile(context, _file); + + int size = getSizeFromFile(context, file); + + return map(context, file, size, 0, 0); + } + + private static RubyFile checkFile(ThreadContext context, IRubyObject _file) { + RubyIO io = RubyIO.convertToIO(context, _file); + + if (!(io instanceof RubyFile)) { + throw context.runtime.newTypeError(_file, context.runtime.getFile()); + } + + RubyFile file = (RubyFile) io; + return file; + } + + @JRubyMethod(name = "map", meta = true) + public static IRubyObject map(ThreadContext context, IRubyObject self, IRubyObject _file, IRubyObject _size) { + RubyFile file = checkFile(context, _file); + + int size = getSizeForMap(context, file, _size); + + return map(context, file, size, 0, 0); + } + + @JRubyMethod(name = "map", meta = true) + public static IRubyObject map(ThreadContext context, IRubyObject self, IRubyObject _file, IRubyObject _size, IRubyObject _offset) { + RubyFile file = checkFile(context, _file); + + int size = getSizeForMap(context, file, _size); + + // This is the file offset, not the buffer offset: + int offset = RubyNumeric.num2int(_offset); + + return map(context, file, size, offset, 0); + } + + private static int getSizeForMap(ThreadContext context, RubyFile file, IRubyObject _size) { + int size; + if (!_size.isNil()) { + size = extractSize(context, _size); + } else { + size = getSizeFromFile(context, file); + } + return size; + } + + private static int getSizeFromFile(ThreadContext context, RubyFile _file) { + int size; + long file_size = _file.getSize(context); + + // Compiler can confirm that we handled file_size < 0 case: + if (file_size < 0) { + throw context.runtime.newArgumentError("Invalid negative file size!"); + } + // Here, we assume that file_size is positive: + else if (file_size > Integer.MAX_VALUE) { + throw context.runtime.newArgumentError("File larger than address space!"); + } + else { + // This conversion should be safe: + size = (int) file_size; + } + return size; + } + + @JRubyMethod(name = "map", required = 1, optional = 3, meta = true) + public static IRubyObject map(ThreadContext context, IRubyObject self, IRubyObject[] args) { + switch (args.length) { + case 1: + return map(context, self, args[0]); + case 2: + return map(context, self, args[0], args[1]); + case 3: + return map(context, self, args[0], args[1], args[2]); + case 4: + return map(context, self, args[0], args[1], args[2], args[3]); + } + return context.nil; + } + + public static IRubyObject map(ThreadContext context, IRubyObject self, IRubyObject _file, IRubyObject _size, IRubyObject _offset, IRubyObject _flags) { + RubyFile file = checkFile(context, _file); + + int size = getSizeForMap(context, file, _size); + + // This is the file offset, not the buffer offset: + int offset = RubyNumeric.num2int(_offset); + + int flags = RubyNumeric.num2int(_flags); + + return map(context, file, size, offset, flags); + } + + private static RubyIOBuffer map(ThreadContext context, RubyFile file, int size, int offset, int flags) { + RubyIOBuffer buffer = new RubyIOBuffer(context.runtime, context.runtime.getIOBuffer()); + + ChannelFD descriptor = file.getOpenFileChecked().fd(); + + mapFile(context, buffer, descriptor, size, offset, flags); + + return buffer; + } + + private static void mapFile(ThreadContext context, RubyIOBuffer buffer, ChannelFD descriptor, int size, int offset, int flags) { + FileChannel.MapMode protect = FileChannel.MapMode.READ_ONLY; + int access = 0; + + if ((flags & READONLY) == READONLY) { + buffer.flags |= READONLY; + } else { + protect = FileChannel.MapMode.READ_WRITE; + } + + if ((flags & PRIVATE) == PRIVATE) { + buffer.flags |= PRIVATE; + protect = FileChannel.MapMode.PRIVATE; + } else { + // This buffer refers to external buffer. + buffer.flags |= EXTERNAL; + buffer.flags |= SHARED; + } + + ByteBuffer base; + + if (descriptor.chFile == null) { + throw context.runtime.newTypeError("Cannot map non-file resource: " + descriptor.ch); + } + + try { + base = descriptor.chFile.map(protect, offset, size); + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(context.runtime, ioe); + } + + buffer.base = base; + buffer.size = size; + + buffer.flags |= MAPPED; + } + + public static IRubyObject map(ThreadContext context, IRubyObject self, RubyFile _file, int _size, int _offset, int _flags) { + return context.nil; + } + + @JRubyMethod(name = "initialize") + public IRubyObject initialize(ThreadContext context) { + return initialize(context, DEFAULT_SIZE); + } + + @JRubyMethod(name = "initialize") + public IRubyObject initialize(ThreadContext context, IRubyObject size) { + return initialize(context, size.convertToInteger().getIntValue()); + } + + @JRubyMethod(name = "initialize") + public IRubyObject initialize(ThreadContext context, IRubyObject _size, IRubyObject flags) { + IRubyObject nil = context.nil; + + int size = _size.convertToInteger().getIntValue(); + + initialize(context, new byte[size], size, flags.convertToInteger().getIntValue(), nil); + + return nil; + } + + public IRubyObject initialize(ThreadContext context, int size) { + IRubyObject nil = context.nil; + + initialize(context, new byte[size], size, flagsForSize(size), nil); + + return nil; + } + + // MRI: io_buffer_initialize + public void initialize(ThreadContext context, byte[] baseBytes, int size, int flags, IRubyObject source) { + ByteBuffer base = null; + + if (baseBytes != null) { + // If we are provided a pointer, we use it. + base = ByteBuffer.wrap(baseBytes); + } else if (size != 0) { + base = newBufferBase(context.runtime, size, flags); + } else { + // Otherwise we don't do anything. + return; + } + + this.base = base; + this.size = size; + this.flags = flags; + this.source = source.isNil() ? null : source; + } + + private static ByteBuffer newBufferBase(Ruby runtime, int size, int flags) { + ByteBuffer base; + + // If we are provided a non-zero size, we allocate it: + if ((flags & INTERNAL) == INTERNAL) { + base = ByteBuffer.allocate(size); + } else if ((flags & MAPPED) == MAPPED) { + // no support for SHARED, PRIVATE yet + base = ByteBuffer.allocateDirect(size); + } else { + throw runtime.newBufferAllocationError("Could not allocate buffer!"); + } + + return base; + } + + // MRI: io_flags_for_size + private static int flagsForSize(int size) { + if (size >= PAGE_SIZE) { + return MAPPED; + } + + return INTERNAL; + } + + @JRubyMethod(name = "initialize_copy") + public IRubyObject initialize_copy(ThreadContext context, IRubyObject other) { + RubyIOBuffer otherBuffer = (RubyIOBuffer) other; + ByteBuffer sourceBase = otherBuffer.getBufferForReading(context); + int sourceSize = otherBuffer.size; + + initialize(context, null, sourceSize, flagsForSize(size), context.nil); + + return copy(context, otherBuffer, 0, sourceSize, 0); + } + + @JRubyMethod(name = "inspect") + public IRubyObject inspect(ThreadContext context) { + RubyString result = to_s(context); + + if (validate()) { + // Limit the maximum size genearted by inspect. + if (size <= 256) { + hexdump(context, result, 16, base, size, false); + } + } + + return result; + } + + private boolean validate() { + if (source != null) { + return validateSlice(source, base, size); + } + + return true; + } + + private boolean validateSlice(IRubyObject source, ByteBuffer base, int size) { + ByteBuffer sourceBase = null; + int sourceSize = 0; + + if (source instanceof RubyString) { + ByteList sourceBytes = ((RubyString) source).getByteList(); + sourceSize = sourceBytes.getRealSize(); + sourceBase = ByteBuffer.wrap(sourceBytes.getUnsafeBytes(), sourceBytes.begin(), sourceSize); + } else { + RubyIOBuffer sourceBuffer = (RubyIOBuffer) source; + sourceBase = sourceBuffer.base; + sourceSize = sourceBuffer.size; + } + + // Source is invalid: + if (sourceBase == null) return false; + + // Base is out of range: + if (base.hasArray() && sourceBase.hasArray() && base.array() != sourceBase.array()) return false; + + int sourceEnd = sourceSize; + int end = size; + + // End is out of range: + if (end > sourceEnd) return false; + + // It seems okay: + return true; + } + + @JRubyMethod(name = "hexdump") + public IRubyObject hexdump(ThreadContext context) { + ByteBuffer base = this.base; + int size = this.size; + if (validate() && base != null) { + RubyString result = RubyString.newStringLight(context.runtime, size * 3 + (size / 16) * 12 + 1); + + hexdump(context, result, 16, base, size, true); + + return result; + } + + return context.nil; + } + + private static RubyString hexdump(ThreadContext context, RubyString string, int width, ByteBuffer base, int size, boolean first) { + byte[] text = new byte[width+1]; + text[width] = '\0'; + + for (int offset = 0; offset < size; offset += width) { + Arrays.fill(text, (byte) 0); + if (first) { + string.cat("0x".getBytes()); + String hex = String.format("0x%08x ", offset); + string.cat(hex.getBytes()); + first = false; + } else { + string.cat(String.format("\n0x%08x ", offset).getBytes()); + } + + for (int i = 0; i < width; i += 1) { + if (offset+i < size) { + int value = Byte.toUnsignedInt(base.get(offset+i)); + + if (value < 127 && value >= 32) { + text[i] = (byte)value; + } + else { + text[i] = '.'; + } + + string.cat(String.format("%02x", value).getBytes()); + } + else { + string.cat(" ".getBytes()); + } + } + + string.cat(' '); + string.cat(text); + } + + return string; + } + + @JRubyMethod(name = "to_s") + public RubyString to_s(ThreadContext context) { + RubyString result = RubyString.newString(context.runtime, "#<"); + + result.append(this.getMetaClass().name(context)); + result.cat(String.format(" %d+%d", System.identityHashCode(base), size).getBytes()); + + if (base == null) { + result.cat(" NULL".getBytes()); + } + + if (isExternal()) { + result.cat(" EXTERNAL".getBytes()); + } + + if (isInternal()) { + result.cat(" INTERNAL".getBytes()); + } + + if (isMapped()) { + result.cat(" MAPPED".getBytes()); + } + + if (isShared()) { + result.cat(" SHARED".getBytes()); + } + + if (isLocked()) { + result.cat(" LOCKED".getBytes()); + } + + if (isReadonly()) { + result.cat(" READONLY".getBytes()); + } + + if (source != null) { + result.cat(" SLICE".getBytes()); + } + + if (!validate()) { + result.cat(" INVALID".getBytes()); + } + + return result.cat(">".getBytes()); + } + + @JRubyMethod(name = "size") + public IRubyObject size(ThreadContext context) { + return context.runtime.newFixnum(size); + } + + @JRubyMethod(name = "valid?") + public IRubyObject valid_p(ThreadContext context) { + return RubyBoolean.newBoolean(context, validate()); + } + + @JRubyMethod(name = "transfer") + public IRubyObject transfer(ThreadContext context) { + if (isLocked()) { + throw context.runtime.newBufferLockedError("Cannot transfer ownership of locked buffer!"); + } + + RubyIOBuffer instance = new RubyIOBuffer(context.runtime, getMetaClass()); + + instance.base = base; + instance.size = size; + instance.flags = flags; + instance.source = source; + + zero(context); + + return instance; + } + + private void zero(ThreadContext context) { + base = null; + size = 0; + source = null; + } + + @JRubyMethod(name = "null?") + public IRubyObject null_p(ThreadContext context) { + return newBoolean(context, base == null); + } + + @JRubyMethod(name = "empty?") + public IRubyObject empty_p(ThreadContext context) { + return newBoolean(context, size == 0); + } + + @JRubyMethod(name = "external?") + public IRubyObject external_p(ThreadContext context) { + return newBoolean(context, isExternal()); + } + + private boolean isExternal() { + return (flags & EXTERNAL) == EXTERNAL; + } + + @JRubyMethod(name = "internal?") + public IRubyObject internal_p(ThreadContext context) { + return newBoolean(context, isInternal()); + } + + private boolean isInternal() { + return (flags & INTERNAL) == INTERNAL; + } + + @JRubyMethod(name = "mapped?") + public IRubyObject mapped_p(ThreadContext context) { + return newBoolean(context, isMapped()); + } + + private boolean isMapped() { + return (flags & MAPPED) == MAPPED; + } + + @JRubyMethod(name = "shared?") + public IRubyObject shared_p(ThreadContext context) { + // no support for shared yet + return newBoolean(context, false); + } + + private boolean isShared() { + return (flags & SHARED) == SHARED; + } + + @JRubyMethod(name = "locked?") + public IRubyObject locked_p(ThreadContext context) { + return newBoolean(context, isLocked()); + } + + private boolean isLocked() { + return (flags & LOCKED) == LOCKED; + } + + @JRubyMethod(name = "readonly?") + public IRubyObject readonly_p(ThreadContext context) { + return newBoolean(context, isReadonly()); + } + + private boolean isReadonly() { + return (flags & READONLY) == READONLY; + } + + @JRubyMethod(name = "locked") + public IRubyObject locked(ThreadContext context, Block block) { + checkLocked(context); + + flags |= LOCKED; + + IRubyObject result = block.yield(context, this); + + flags &= ~LOCKED; + + return result; + } + + private void checkLocked(ThreadContext context) { + if (isLocked()) { + throw context.runtime.newBufferLockedError("Buffer already locked!"); + } + } + + public IRubyObject lock(ThreadContext context) { + checkLocked(context); + + flags |= LOCKED; + + return this; + } + + public IRubyObject unlock(ThreadContext context) { + if ((flags & LOCKED) == 0) { + throw context.runtime.newBufferLockedError("Buffer not locked!"); + } + + flags &= ~LOCKED; + + return this; + } + + private boolean tryUnlock() { + if (isLocked()) { + flags &= ~LOCKED; + return true; + } + + return false; + } + + @JRubyMethod(name = "slice") + public IRubyObject slice(ThreadContext context) { + return slice(context, 0, size); + } + + @JRubyMethod(name = "slice") + public IRubyObject slice(ThreadContext context, IRubyObject _offset) { + int offset = RubyNumeric.num2int(_offset); + + if (offset < 0) { + throw context.runtime.newArgumentError("Offset can't be negative!"); + } + + return slice(context, offset, size - offset); + } + + @JRubyMethod(name = "slice") + public IRubyObject slice(ThreadContext context, IRubyObject _offset, IRubyObject _length) { + int offset = RubyNumeric.num2int(_offset); + + if (offset < 0) { + throw context.runtime.newArgumentError("Offset can't be negative!"); + } + + int length = RubyNumeric.num2int(_length); + + if (length < 0) { + throw context.runtime.newArgumentError("Length can't be negative!"); + } + + return slice(context, offset, length); + } + + // MRI: rb_io_buffer_slice + public IRubyObject slice(ThreadContext context, int offset, int length) { + validateRange(context, offset, length); + + // gross, but slice(int, int) is 13+ + base.position(offset); + base.limit(offset + length); + ByteBuffer slice = base.slice(); + base.clear(); + + return newBuffer(context.runtime, slice, length, flags); + } + + // MRI: io_buffer_validate_range + private void validateRange(ThreadContext context, int offset, int length) { + if (offset + length > size) { + throw context.runtime.newArgumentError("Specified offset+length is bigger than the buffer size!"); + } + } + + @JRubyMethod(name = "<=>") + public IRubyObject op_cmp(ThreadContext context, IRubyObject other) { + return context.runtime.newFixnum(base.compareTo(((RubyIOBuffer) other).base)); + } + + @JRubyMethod(name = "resize") + public IRubyObject resize(ThreadContext context, IRubyObject size) { + resize(context, size.convertToInteger().getIntValue()); + + return this; + } + + // MRI: rb_io_buffer_resize + public void resize(ThreadContext context, int size) { + if (isLocked()) { + throw context.runtime.newBufferLockedError("Cannot resize locked buffer!"); + } + + if (this.base == null) { + initialize(context, null, size, flagsForSize(size), context.nil); + return; + } + + if (isExternal()) { + throw context.runtime.newBufferAccessError("Cannot resize external buffer!"); + } + + // no special behavior for isInternal=true since we do not control the internals of ByteBuffers. + + ByteBuffer newBase = this.base.isDirect() ? ByteBuffer.allocateDirect(size) : ByteBuffer.allocate(size); + this.base.limit(Math.min(size, this.base.capacity())); + newBase.put(this.base); + newBase.clear(); + + this.base = newBase; + this.size = size; + } + + // MRI: io_buffer_clear + @JRubyMethod(name = "clear") + public IRubyObject clear(ThreadContext context) { + return clear(context, 0, 0, size); + } + + @JRubyMethod(name = "clear") + public IRubyObject clear(ThreadContext context, IRubyObject value) { + return clear(context, RubyNumeric.num2int(value), 0, size); + } + + @JRubyMethod(name = "clear") + public IRubyObject clear(ThreadContext context, IRubyObject _value, IRubyObject _offset) { + int value = RubyNumeric.num2int(_value); + int offset = RubyNumeric.num2int(_offset); + return clear(context, value, offset, size - offset); + } + + @JRubyMethod(name = "clear") + public IRubyObject clear(ThreadContext context, IRubyObject _value, IRubyObject _offset, IRubyObject _length) { + int value = RubyNumeric.num2int(_value); + int offset = RubyNumeric.num2int(_offset); + int length = RubyNumeric.num2int(_length); + return clear(context, value, offset, length); + } + + // MRI: rb_io_buffer_clear + private IRubyObject clear(ThreadContext context, int value, int offset, int length) { + ByteBuffer buffer = getBufferForWriting(context); + + if (offset + length > size) { + throw context.runtime.newArgumentError("The given offset + length out of bounds!"); + } + + if (buffer.hasArray()) { + Arrays.fill(buffer.array(), offset, offset + length, (byte) value); + } + + return this; + } + + private ByteBuffer getBufferForWriting(ThreadContext context) { + if (isReadonly()) { + throw context.runtime.newBufferAccessError("Buffer is not writable!"); + } + + // TODO: validate our buffer + + if (base != null) { + return base; + } + + throw context.runtime.newBufferAllocationError("The buffer is not allocated!"); + } + + private ByteBuffer getBufferForReading(ThreadContext context) { + // TODO: validate our buffer + + if (base != null) { + return base; + } + + throw context.runtime.newBufferAllocationError("The buffer is not allocated!"); + } + + @JRubyMethod(name = "free") + public IRubyObject free(ThreadContext context) { + if (isLocked()) { + throw context.runtime.newBufferLockedError("Buffer is locked!"); + } + + freeInternal(context); + + return this; + } + + private boolean freeInternal(ThreadContext context) { + if (this.base != null) { + // No special handling for internal yet + + // No special handling for mapped yet + + // We can only dereference and allow GC to clean it up + this.base = null; + this.size = 0; + this.flags = 0; + this.source = null; + + return true; + } + + return false; + } + + @JRubyMethod(name = "size_of", meta = true) + public static IRubyObject size_of(ThreadContext context, IRubyObject self, IRubyObject dataType) { + if (dataType instanceof RubyArray) { + long total = 0; + RubyArray array = (RubyArray) dataType; + int size = array.size(); + for (int i = 0; i < size; i++) { + IRubyObject elt = array.eltOk(i); + total += getDataType(elt).type.size(); + } + } + + return RubyFixnum.newFixnum(context.runtime, getDataType(dataType).type.size()); + } + + private boolean isBigEndian() { + return (flags & BIG_ENDIAN) == BIG_ENDIAN; + } + + private boolean isLittleEndian() { + return (flags & LITTLE_ENDIAN) == LITTLE_ENDIAN; + } + + private boolean isHostEndian() { + return (flags & (BIG_ENDIAN | LITTLE_ENDIAN)) == HOST_ENDIAN; + } + + private static DataType getDataType(IRubyObject dataType) { + return DataType.valueOf(RubySymbol.objectToSymbolString(dataType)); + } + + private static byte readByte(ThreadContext context, ByteBuffer buffer, int offset) { + return buffer.get(offset); + } + + private static int readUnsignedByte(ThreadContext context, ByteBuffer buffer, int offset) { + return Byte.toUnsignedInt(buffer.get(offset)); + } + + private static void writeByte(ThreadContext context, ByteBuffer buffer, int offset, byte value) { + buffer.put(offset, (byte) value); + } + + private static void writeUnsignedByte(ThreadContext context, ByteBuffer buffer, int offset, int value) { + buffer.put(offset, (byte) value); + } + + private static short readShort(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + short s = buffer.getShort(offset); + + if (order == ByteOrder.BIG_ENDIAN) return s; + + return Short.reverseBytes(s); + } + + private static int readUnsignedShort(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + return Short.toUnsignedInt(readShort(context, buffer, offset, order)); + } + + private static void writeShort(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, short value) { + if (order == ByteOrder.BIG_ENDIAN) { + buffer.putShort(offset, value); + return; + } + + buffer.putShort(offset, Short.reverseBytes(value)); + } + + private static void writeUnsignedShort(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, int value) { + writeShort(context, buffer, offset, order, (short) value); + } + + private static int readInt(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + int i = buffer.getInt(offset); + + if (order == ByteOrder.BIG_ENDIAN) return i; + + return Integer.reverseBytes(i); + } + + private static long readUnsignedInt(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + return Integer.toUnsignedLong(readInt(context, buffer, offset, order)); + } + + private static void writeInt(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, int value) { + if (order == ByteOrder.BIG_ENDIAN) { + buffer.putInt(offset, value); + return; + } + + buffer.putInt(offset, Integer.reverseBytes(value)); + } + + private static void writeUnsignedInt(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, long value) { + writeInt(context, buffer, offset, order, (int) value); + } + + private static long readLong(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + long l = buffer.getLong(offset); + + if (order == ByteOrder.BIG_ENDIAN) return l; + + return Long.reverseBytes(l); + } + + private static BigInteger readUnsignedLong(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + long l = readLong(context, buffer, offset, order); + if (l > 0L) return BigInteger.valueOf(l); + + byte[] bytes = new byte[8]; + for (int i = 7; i >= 0; i--) { + bytes[i] = (byte)(l & 0xFF); + l >>= 8; + } + + return new BigInteger(1, bytes); + } + + private static void writeLong(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, long value) { + if (order == ByteOrder.BIG_ENDIAN) { + buffer.putLong(offset, value); + return; + } + + buffer.putLong(offset, Long.reverseBytes(value)); + } + + private static void writeUnsignedLong(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, long value) { + writeLong(context, buffer, offset, order, value); + } + + private static float readFloat(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + float f = buffer.getFloat(offset); + + if (order == ByteOrder.BIG_ENDIAN) return f; + + return Float.intBitsToFloat(Integer.reverseBytes(Float.floatToIntBits(f))); + } + + private static void writeFloat(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, float value) { + if (order == ByteOrder.BIG_ENDIAN) { + buffer.putFloat(offset, value); + return; + } + + buffer.putFloat(offset, Float.intBitsToFloat(Integer.reverseBytes(Float.floatToIntBits(value)))); + } + + private static double readDouble(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order) { + double f = buffer.getDouble(offset); + + if (order == ByteOrder.BIG_ENDIAN) return f; + + return Double.longBitsToDouble(Long.reverseBytes(Double.doubleToLongBits(f))); + } + + private static void writeDouble(ThreadContext context, ByteBuffer buffer, int offset, ByteOrder order, double value) { + if (order == ByteOrder.BIG_ENDIAN) { + buffer.putDouble(offset, value); + return; + } + + buffer.putDouble(offset, Double.longBitsToDouble(Long.reverseBytes(Double.doubleToLongBits(value)))); + } + + private static IRubyObject wrap(Ruby runtime, long value) { + return RubyFixnum.newFixnum(runtime, value); + } + + private static IRubyObject wrap(Ruby runtime, BigInteger value) { + return RubyBignum.newBignum(runtime, value); + } + + private static IRubyObject wrap(Ruby runtime, double value) { + return RubyFloat.newFloat(runtime, value); + } + + private static long unwrapLong(IRubyObject value) { + return value.convertToInteger().getLongValue(); + } + + private static double unwrapDouble(IRubyObject value) { + return value.convertToFloat().getDoubleValue(); + } + + private static long unwrapUnsignedLong(IRubyObject value) { + return RubyNumeric.num2ulong(value); + } + + @JRubyMethod(name = "get_value") + public IRubyObject get_value(ThreadContext context, IRubyObject type, IRubyObject _offset) { + ByteBuffer buffer = getBufferForReading(context); + + DataType dataType = getDataType(type); + int offset = RubyNumeric.num2int(_offset); + int size = this.size; + + return getValue(context, buffer, size, dataType, offset); + } + + private static IRubyObject getValue(ThreadContext context, ByteBuffer buffer, int size, DataType dataType, int offset) { + Ruby runtime = context.runtime; + + // TODO: validate size + + switch (dataType) { + case S8: + return wrap(runtime, readByte(context, buffer, offset)); + case U8: + return wrap(runtime, readUnsignedByte(context, buffer, offset)); + case u16: + return wrap(runtime, readUnsignedShort(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case U16: + return wrap(runtime, readUnsignedShort(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case s16: + return wrap(runtime, readShort(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case S16: + return wrap(runtime, readShort(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case u32: + return wrap(runtime, readUnsignedInt(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case U32: + return wrap(runtime, readUnsignedInt(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case s32: + return wrap(runtime, readInt(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case S32: + return wrap(runtime, readInt(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case u64: + return wrap(runtime, readUnsignedLong(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case U64: + return wrap(runtime, readUnsignedLong(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case s64: + return wrap(runtime, readLong(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case S64: + return wrap(runtime, readLong(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case f32: + return wrap(runtime, readFloat(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case F32: + return wrap(runtime, readFloat(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + case f64: + return wrap(runtime, readDouble(context, buffer, offset, ByteOrder.LITTLE_ENDIAN)); + case F64: + return wrap(runtime, readDouble(context, buffer, offset, ByteOrder.BIG_ENDIAN)); + } + + throw runtime.newArgumentError("Unknown data_type: " + dataType); // should never happen + } + + @JRubyMethod(name = "get_values") + public IRubyObject get_values(ThreadContext context, IRubyObject dataTypes, IRubyObject _offset) { + Ruby runtime = context.runtime; + + int offset = RubyNumeric.num2int(_offset); + + int size = this.size; + + ByteBuffer buffer = getBufferForReading(context); + + if (!(dataTypes instanceof RubyArray)) { + throw runtime.newArgumentError("Argument data_types should be an array!"); + } + + RubyArray dataTypesArray = (RubyArray) dataTypes; + int dataTypesSize = dataTypesArray.size(); + RubyArray values = RubyArray.newArray(runtime, dataTypesSize); + + for (long i = 0; i < dataTypesSize; i++) { + IRubyObject type = dataTypesArray.eltOk(i); + DataType dataType = getDataType(type); + + IRubyObject value = getValue(context, buffer, size, dataType, offset); + + offset += dataType.type.size(); + + values.push(value); + } + + return values; + } + + @JRubyMethod(name = "each") + public IRubyObject each(ThreadContext context, IRubyObject _dataType, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each", _dataType); + + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + + return each(context, buffer, dataType, 0, size, block); + } + + @JRubyMethod(name = "each") + public IRubyObject each(ThreadContext context, IRubyObject _dataType, IRubyObject _offset, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each", Helpers.arrayOf(_dataType, _offset)); + + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + int offset = _offset.convertToInteger().getIntValue(); + + return each(context, buffer, dataType, offset, size - offset, block); + } + + @JRubyMethod(name = "each") + public IRubyObject each(ThreadContext context, IRubyObject _dataType, IRubyObject _offset, IRubyObject _count, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each", Helpers.arrayOf(_dataType, _offset, _count)); + + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + int offset = _offset.convertToInteger().getIntValue(); + int count = _count.convertToInteger().getIntValue(); + + return each(context, buffer, dataType, offset, count, block); + } + + private IRubyObject each(ThreadContext context, ByteBuffer buffer, DataType dataType, int offset, int count, Block block) { + Ruby runtime = context.runtime; + + for (int i = 0 ; i < count; i++) { + int currentOffset = offset; + IRubyObject value = getValue(context, buffer, size, dataType, offset); + offset += dataType.type.size(); + block.yieldSpecific(context, RubyFixnum.newFixnum(runtime, currentOffset), value); + } + + return this; + } + + @JRubyMethod(name = "values") + public IRubyObject values(ThreadContext context, IRubyObject _dataType) { + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + + return values(context, buffer, dataType, 0, size); + } + + @JRubyMethod(name = "values") + public IRubyObject values(ThreadContext context, IRubyObject _dataType, IRubyObject _offset) { + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + int offset = _offset.convertToInteger().getIntValue(); + + return values(context, buffer, dataType, offset, size - offset); + } + + @JRubyMethod(name = "values") + public IRubyObject values(ThreadContext context, IRubyObject _dataType, IRubyObject _offset, IRubyObject _count) { + ByteBuffer buffer = getBufferForReading(context); + DataType dataType = getDataType(_dataType); + int offset = _offset.convertToInteger().getIntValue(); + int count = _count.convertToInteger().getIntValue(); + + return values(context, buffer, dataType, offset, count); + } + + private RubyArray values(ThreadContext context, ByteBuffer buffer, DataType dataType, int offset, int count) { + Ruby runtime = context.runtime; + + RubyArray values = RubyArray.newArray(runtime, count); + + for (int i = 0 ; i < count; i++) { + int currentOffset = offset; + IRubyObject value = getValue(context, buffer, size, dataType, offset); + offset += dataType.type.size(); + values.push(value); + } + + return values; + } + + @JRubyMethod(name = "each_byte") + public IRubyObject each_byte(ThreadContext context, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each_byte"); + + ByteBuffer buffer = getBufferForReading(context); + + return eachByte(context, buffer, 0, size, block); + } + + @JRubyMethod(name = "each_byte") + public IRubyObject each_byte(ThreadContext context, IRubyObject _offset, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each_byte", Helpers.arrayOf(_offset)); + + ByteBuffer buffer = getBufferForReading(context); + int offset = _offset.convertToInteger().getIntValue(); + + return eachByte(context, buffer, offset, size - offset, block); + } + + @JRubyMethod(name = "each_byte") + public IRubyObject each_byte(ThreadContext context, IRubyObject _offset, IRubyObject _count, Block block) { + if (!block.isGiven()) return RubyEnumerator.enumeratorize(context.runtime, this, "each_byte", Helpers.arrayOf(_offset, _count)); + + ByteBuffer buffer = getBufferForReading(context); + int offset = _offset.convertToInteger().getIntValue(); + int count = _count.convertToInteger().getIntValue(); + + return eachByte(context, buffer, offset, count, block); + } + + private IRubyObject eachByte(ThreadContext context, ByteBuffer buffer, int offset, int count, Block block) { + Ruby runtime = context.runtime; + + for (int i = 0 ; i < count; i++) { + IRubyObject value = wrap(runtime, readByte(context, buffer, offset + i)); + block.yieldSpecific(context, value); + } + + return this; + } + + private static void setValue(ThreadContext context, ByteBuffer buffer, int size, DataType dataType, int offset, IRubyObject value) { + // TODO: validate size + + switch (dataType) { + case S8: + writeByte(context, buffer, offset, (byte) unwrapLong(value)); + return; + case U8: + writeUnsignedByte(context, buffer, offset, (int) unwrapLong(value)); + return; + case u16: + writeUnsignedShort(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, (int) unwrapLong(value)); + return; + case U16: + writeUnsignedShort(context, buffer, offset, ByteOrder.BIG_ENDIAN, (int) unwrapLong(value)); + return; + case s16: + writeShort(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, (short) unwrapLong(value)); + return; + case S16: + writeShort(context, buffer, offset, ByteOrder.BIG_ENDIAN, (short) unwrapLong(value)); + return; + case u32: + writeUnsignedInt(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, unwrapLong(value)); + return; + case U32: + writeUnsignedInt(context, buffer, offset, ByteOrder.BIG_ENDIAN, unwrapLong(value)); + return; + case s32: + writeInt(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, (int) unwrapLong(value)); + return; + case S32: + writeInt(context, buffer, offset, ByteOrder.BIG_ENDIAN, (int) unwrapLong(value)); + return; + case u64: + writeUnsignedLong(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, unwrapUnsignedLong(value)); + return; + case U64: + writeUnsignedLong(context, buffer, offset, ByteOrder.BIG_ENDIAN, unwrapUnsignedLong(value)); + return; + case s64: + writeLong(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, unwrapLong(value)); + return; + case S64: + writeLong(context, buffer, offset, ByteOrder.BIG_ENDIAN, unwrapLong(value)); + return; + case f32: + writeFloat(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, (float) unwrapDouble(value)); + return; + case F32: + writeFloat(context, buffer, offset, ByteOrder.BIG_ENDIAN, (float) unwrapDouble(value)); + return; + case f64: + writeDouble(context, buffer, offset, ByteOrder.LITTLE_ENDIAN, unwrapDouble(value)); + return; + case F64: + writeDouble(context, buffer, offset, ByteOrder.BIG_ENDIAN, unwrapDouble(value)); + return; + } + + throw context.runtime.newArgumentError("Unknown data_type: " + dataType); // should never happen + } + + @JRubyMethod(name = "set_value") + public IRubyObject set_value(ThreadContext context, IRubyObject _dataType, IRubyObject _offset, IRubyObject _value) { + ByteBuffer buffer = getBufferForWriting(context); + + DataType dataType = getDataType(_dataType); + int offset = RubyNumeric.num2int(_offset); + int size = this.size; + + setValue(context, buffer, size, dataType, offset, _value); + + return RubyFixnum.newFixnum(context.runtime, offset + dataType.type.size()); + } + + @JRubyMethod(name = "set_values") + public IRubyObject set_values(ThreadContext context, IRubyObject _dataTypes, IRubyObject _offset, IRubyObject _values) { + Ruby runtime = context.runtime; + + int offset = RubyNumeric.num2int(_offset); + + int size = this.size; + + ByteBuffer buffer = getBufferForWriting(context); + + if (!(_dataTypes instanceof RubyArray)) { + throw runtime.newArgumentError("Argument data_types should be an array!"); + } + RubyArray dataTypes = (RubyArray) _dataTypes; + + if (!(_values instanceof RubyArray)) { + throw runtime.newArgumentError("Argument values should be an array!"); + } + RubyArray values = (RubyArray) _values; + + if (dataTypes.size() != values.size()) { + throw runtime.newArgumentError("Argument data_types and values should have the same length!"); + } + + int dataTypesSize = dataTypes.size(); + + for (long i = 0; i < dataTypesSize; i++) { + IRubyObject type = dataTypes.eltOk(i); + DataType dataType = getDataType(type); + + IRubyObject value = values.eltOk(i); + + setValue(context, buffer, size, dataType, offset, value); + + offset += dataType.type.size(); + } + + return RubyFixnum.newFixnum(runtime, offset); + } + + @JRubyMethod(name = "copy") + public IRubyObject copy(ThreadContext context, IRubyObject source) { + RubyIOBuffer sourceBuffer = (RubyIOBuffer) source; + + return copy(context, sourceBuffer, 0, sourceBuffer.size, 0); + } + + @JRubyMethod(name = "copy") + public IRubyObject copy(ThreadContext context, IRubyObject source, IRubyObject _offset) { + RubyIOBuffer sourceBuffer = (RubyIOBuffer) source; + + int offset = RubyNumeric.num2int(_offset); + + return copy(context, sourceBuffer, offset, sourceBuffer.size, 0); + } + + @JRubyMethod(name = "copy") + public IRubyObject copy(ThreadContext context, IRubyObject source, IRubyObject _offset, IRubyObject _length) { + RubyIOBuffer sourceBuffer = (RubyIOBuffer) source; + + int offset = RubyNumeric.num2int(_offset); + int length = RubyNumeric.num2int(_length); + + return copy(context, sourceBuffer, offset, length, 0); + } + + @JRubyMethod(name = "copy", required = 1, optional = 3, checkArity = false) + public IRubyObject copy(ThreadContext context, IRubyObject[] args) { + Arity.checkArgumentCount(context, args, 1, 3); + + switch (args.length) { + case 1: + return copy(context, args[0]); + case 2: + return copy(context, args[0], args[1]); + case 3: + return copy(context, args[0], args[1], args[2]); + case 4: + return copy(context, args[0], args[1], args[2], args[3]); + } + + return context.nil; + } + + public IRubyObject copy(ThreadContext context, IRubyObject source, IRubyObject _offset, IRubyObject _length, IRubyObject _sourceOffset) { + RubyIOBuffer sourceBuffer = (RubyIOBuffer) source; + + int offset = RubyNumeric.num2int(_offset); + int length = RubyNumeric.num2int(_length); + int sourceOffset = RubyNumeric.num2int(_sourceOffset); + + return copy(context, sourceBuffer, offset, length, sourceOffset); + } + + public IRubyObject copy(ThreadContext context, RubyIOBuffer source, int offset, int length, int sourceOffset) { + if (sourceOffset > length) { + throw context.runtime.newArgumentError("The given source offset is bigger than the source itself!"); + } + + ByteBuffer sourceBuffer = source.getBufferForReading(context); + + bufferCopy(context, offset, sourceBuffer, sourceOffset, source.size, length); + + return RubyFixnum.newFixnum(context.runtime, length); + } + + // MRI: io_buffer_copy_from + public IRubyObject copy(ThreadContext context, RubyString source, int offset, int length, int sourceOffset) { + if (sourceOffset > length) { + throw context.runtime.newArgumentError("The given source offset is bigger than the source itself!"); + } + + bufferCopy(context, offset, source.getByteList(), sourceOffset, source.size(), length); + + return RubyFixnum.newFixnum(context.runtime, length); + } + + private void bufferCopy(ThreadContext context, int offset, ByteBuffer sourceBuffer, int sourceOffset, int sourceSize, int length) { + ByteBuffer destBuffer = getBufferForWriting(context); + + sourceBuffer.position(sourceOffset); + sourceBuffer.limit(sourceOffset + length); + if (offset == 0) { + destBuffer.put(sourceBuffer); + } else { + destBuffer.position(offset); + destBuffer.put(sourceBuffer); + } + destBuffer.clear(); + sourceBuffer.clear(); + } + + private void bufferCopy(ThreadContext context, int offset, ByteList sourceBuffer, int sourceOffset, int sourceSize, int length) { + ByteBuffer destBuffer = getBufferForWriting(context); + + if (offset == 0) { + destBuffer.put(sourceBuffer.getUnsafeBytes(), sourceBuffer.begin() + sourceOffset, length); + } else { + destBuffer.position(offset); + destBuffer.put(sourceBuffer.getUnsafeBytes(), sourceBuffer.begin() + sourceOffset, length); + } + destBuffer.clear(); + } + + @JRubyMethod(name = "get_string") + public IRubyObject get_string(ThreadContext context) { + return getString(context, 0, size, ASCIIEncoding.INSTANCE); + } + + @JRubyMethod(name = "get_string") + public IRubyObject get_string(ThreadContext context, IRubyObject _offset) { + int offset = extractOffset(context, _offset); + + return getString(context, offset, size, ASCIIEncoding.INSTANCE); + } + + @JRubyMethod(name = "get_string") + public IRubyObject get_string(ThreadContext context, IRubyObject _offset, IRubyObject _length) { + int offset = extractOffset(context, _offset); + int length = extractLength(context, _length, offset); + + return getString(context, offset, length, ASCIIEncoding.INSTANCE); + } + + @JRubyMethod(name = "get_string") + public IRubyObject get_string(ThreadContext context, IRubyObject _offset, IRubyObject _length, IRubyObject _encoding) { + int offset = extractOffset(context, _offset); + int length = extractLength(context, _length, offset); + Encoding encoding = context.runtime.getEncodingService().getEncodingFromObject(_encoding); + + return getString(context, offset, length, encoding); + } + + private IRubyObject getString(ThreadContext context, int offset, int length, Encoding encoding) { + ByteBuffer buffer = getBufferForReading(context); + + validateRange(context, offset, length); + + byte[] bytes = new byte[length]; + if (offset == 0) { + buffer.get(bytes, 0, length); + } else { + buffer.position(offset); + buffer.get(bytes, 0, length); + buffer.clear(); + } + + return RubyString.newString(context.runtime, bytes, 0, length, encoding); + } + + @JRubyMethod(name = "set_string") + public IRubyObject set_string(ThreadContext context, IRubyObject _string) { + RubyString string = _string.convertToString(); + + return copy(context, string, 0, string.size(), 0); + } + + @JRubyMethod(name = "set_string") + public IRubyObject set_string(ThreadContext context, IRubyObject _string, IRubyObject _offset) { + RubyString string = _string.convertToString(); + int offset = extractOffset(context, _offset); + + return copy(context, string, offset, string.size(), 0); + } + + @JRubyMethod(name = "set_string") + public IRubyObject set_string(ThreadContext context, IRubyObject _string, IRubyObject _offset, IRubyObject _length) { + RubyString string = _string.convertToString(); + int offset = extractOffset(context, _offset); + int length = extractLength(context, _length, offset); + + return copy(context, string, offset, length, 0); + } + + @JRubyMethod(name = "set_string", required = 1, optional = 3, checkArity = false) + public IRubyObject set_string(ThreadContext context, IRubyObject[] args) { + Arity.checkArgumentCount(context, args, 1, 3); + + switch (args.length) { + case 1: + return set_string(context, args[0]); + case 2: + return set_string(context, args[0], args[1]); + case 3: + return set_string(context, args[0], args[1], args[2]); + case 4: + return set_string(context, args[0], args[1], args[2], args[3]); + } + + return context.nil; + } + + public IRubyObject set_string(ThreadContext context, IRubyObject _string, IRubyObject _offset, IRubyObject _length, IRubyObject _stringOffset) { + RubyString string = _string.convertToString(); + int offset = RubyNumeric.num2int(_offset); + int length = RubyNumeric.num2int(_length); + int stringOffset = RubyNumeric.num2int(_stringOffset); + + return copy(context, string, offset, length, stringOffset); + } + + @JRubyMethod(name = "&") + public IRubyObject op_and(ThreadContext context, IRubyObject _mask) { + RubyIOBuffer maskBuffer = (RubyIOBuffer) _mask; + + checkMask(context, maskBuffer); + + RubyIOBuffer outputBuffer = newBuffer(context.runtime, size, flagsForSize(size)); + + bufferAnd(outputBuffer.base, base, size, maskBuffer.base, maskBuffer.size); + + return outputBuffer; + } + + @JRubyMethod(name = "|") + public IRubyObject op_or(ThreadContext context, IRubyObject _mask) { + RubyIOBuffer maskBuffer = (RubyIOBuffer) _mask; + + checkMask(context, maskBuffer); + + RubyIOBuffer outputBuffer = newBuffer(context.runtime, size, flagsForSize(size)); + + bufferOr(outputBuffer.base, base, size, maskBuffer.base, maskBuffer.size); + + return outputBuffer; + } + + @JRubyMethod(name = "^") + public IRubyObject op_xor(ThreadContext context, IRubyObject _mask) { + RubyIOBuffer maskBuffer = (RubyIOBuffer) _mask; + + checkMask(context, maskBuffer); + + RubyIOBuffer outputBuffer = newBuffer(context.runtime, size, flagsForSize(size)); + + bufferXor(outputBuffer.base, base, size, maskBuffer.base, maskBuffer.size); + + return outputBuffer; + } + + @JRubyMethod(name = "~") + public IRubyObject op_not(ThreadContext context) { + RubyIOBuffer outputBuffer = newBuffer(context.runtime, size, flagsForSize(size)); + + bufferNot(outputBuffer.base, base, size); + + return outputBuffer; + } + + @JRubyMethod(name = "and!") + public IRubyObject and_bang(ThreadContext context, IRubyObject _mask) { + if (!(_mask instanceof RubyIOBuffer)) { + throw context.runtime.newTypeError(_mask, context.runtime.getIOBuffer()); + } + + RubyIOBuffer maskData = (RubyIOBuffer) _mask; + + checkMask(context, maskData); + checkOverlaps(context, maskData); + + ByteBuffer base = getBufferForWriting(context); + ByteBuffer maskBase = maskData.getBufferForReading(context); + + bufferAndInPlace(base, size, maskBase, maskData.size); + + return this; + } + + @JRubyMethod(name = "or!") + public IRubyObject or_bang(ThreadContext context, IRubyObject _mask) { + if (!(_mask instanceof RubyIOBuffer)) { + throw context.runtime.newTypeError(_mask, context.runtime.getIOBuffer()); + } + + RubyIOBuffer maskData = (RubyIOBuffer) _mask; + + checkMask(context, maskData); + checkOverlaps(context, maskData); + + ByteBuffer base = getBufferForWriting(context); + ByteBuffer maskBase = maskData.getBufferForReading(context); + + bufferOrInPlace(base, size, maskBase, maskData.size); + + return this; + } + + @JRubyMethod(name = "xor!") + public IRubyObject xor_bang(ThreadContext context, IRubyObject _mask) { + if (!(_mask instanceof RubyIOBuffer)) { + throw context.runtime.newTypeError(_mask, context.runtime.getIOBuffer()); + } + + RubyIOBuffer maskData = (RubyIOBuffer) _mask; + + checkMask(context, maskData); + checkOverlaps(context, maskData); + + ByteBuffer base = getBufferForWriting(context); + ByteBuffer maskBase = maskData.getBufferForReading(context); + + bufferXorInPlace(base, size, maskBase, maskData.size); + + return this; + } + + @JRubyMethod(name = "not!") + public IRubyObject not_bang(ThreadContext context) { + ByteBuffer base = getBufferForWriting(context); + + bufferNotInPlace(base, size); + + return this; + } + + private static void checkMask(ThreadContext context, RubyIOBuffer buffer) { + if (buffer.size == 0) { + throw context.runtime.newBufferMaskError("Zero-length mask given!"); + } + } + + private void checkOverlaps(ThreadContext context, RubyIOBuffer other) { + if (bufferOverlaps(other)) { + throw context.runtime.newBufferMaskError("Mask overlaps source data!"); + } + } + + private boolean bufferOverlaps(RubyIOBuffer other) { + if (base != null && base.hasArray() && other.base != null && other.base.hasArray()) { + if (base.array() == other.base.array()) { + return true; + } + } + + // unsure how to detect overlap for native buffers + return false; + } + + private static void bufferAnd(ByteBuffer output, ByteBuffer base, int size, ByteBuffer mask, int maskSize) { + for (int offset = 0; offset < size; offset += 1) { + output.put(offset, (byte) (base.get(offset) & mask.get(offset % maskSize))); + } + } + + private static void bufferAndInPlace(ByteBuffer a, int aSize, ByteBuffer b, int bSize) { + bufferAnd(a, a, aSize, b, bSize); + } + + private static void bufferOr(ByteBuffer output, ByteBuffer base, int size, ByteBuffer mask, int maskSize) { + for (int offset = 0; offset < size; offset += 1) { + output.put(offset, (byte) (base.get(offset) | mask.get(offset % maskSize))); + } + } + + private static void bufferOrInPlace(ByteBuffer a, int aSize, ByteBuffer b, int bSize) { + bufferOr(a, a, aSize, b, bSize); + } + + private static void bufferXor(ByteBuffer output, ByteBuffer base, int size, ByteBuffer mask, int maskSize) { + for (int offset = 0; offset < size; offset += 1) { + output.put(offset, (byte) (base.get(offset) ^ mask.get(offset % maskSize))); + } + } + + private static void bufferXorInPlace(ByteBuffer a, int aSize, ByteBuffer b, int bSize) { + bufferXor(a, a, aSize, b, bSize); + } + + private static void bufferNot(ByteBuffer output, ByteBuffer base, int size) { + for (int offset = 0; offset < size; offset += 1) { + output.put(offset, (byte) ~base.get(offset)); + } + } + + private static void bufferNotInPlace(ByteBuffer a, int aSize) { + bufferNot(a, a, aSize); + } + + @JRubyMethod(name = "read") + public IRubyObject read(ThreadContext context, IRubyObject io) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + + if (!scheduler.isNil()) { + Ruby runtime = context.runtime; + IRubyObject result = FiberScheduler.ioRead(context, scheduler, io, this, RubyFixnum.newFixnum(runtime, size), RubyFixnum.zero(runtime)); + + if (result != UNDEF) { + return result; + } + } + + return read(context, io, size, 0); + } + + @JRubyMethod(name = "read") + public IRubyObject read(ThreadContext context, IRubyObject io, IRubyObject _length) { + if (_length.isNil()) { + return read(context, io); + } + + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger lengthInteger = _length.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioRead(context, scheduler, io, this, lengthInteger, RubyFixnum.zero(context.runtime)); + + if (result != null) { + return result; + } + } + + return read(context, io, lengthInteger.getIntValue(), 0); + } + + @JRubyMethod(name = "read") + public IRubyObject read(ThreadContext context, IRubyObject io, IRubyObject _length, IRubyObject _offset) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger offsetInteger = _offset.convertToInteger(); + int offset = offsetInteger.getIntValue(); + + int length; + RubyInteger lengthInteger; + if (_length.isNil()) { + length = size - offset; + lengthInteger = null; + } else { + lengthInteger = _length.convertToInteger(); + length = lengthInteger.getIntValue(); + } + + if (!scheduler.isNil()) { + if (lengthInteger == null) lengthInteger = RubyFixnum.newFixnum(context.runtime, length); + IRubyObject result = FiberScheduler.ioRead(context, scheduler, io, this, lengthInteger, offsetInteger); + + if (result != UNDEF) { + return result; + } + } + + return read(context, io, length, offset); + } + + public IRubyObject read(ThreadContext context, IRubyObject io, int length, int offset) { + validateRange(context, offset, length); + + ByteBuffer buffer = getBufferForWriting(context); + + return readInternal(context, RubyIO.convertToIO(context, io), buffer, offset, length); + } + + /** + * Read from the given io into the given buffer base at the given offset and size limit. The buffer will be left + * with its position after the last byte read. + * + * MRI: io_buffer_read_internal + * + * @param context + * @param io + * @param base + * @param offset + * @param size + * @return + */ + private static IRubyObject readInternal(ThreadContext context, RubyIO io, ByteBuffer base, int offset, int size) { + OpenFile fptr = io.getOpenFileChecked(); + final boolean locked = fptr.lock(); + try { + base.position(offset); + base.limit(offset + size); + int result = OpenFile.readInternal(context, fptr, fptr.fd(), base, offset, size); + return FiberScheduler.result(context.runtime, result, fptr.errno()); + } finally { + base.clear(); + if (locked) fptr.unlock(); + } + } + + @JRubyMethod(name = "pread") + public IRubyObject pread(ThreadContext context, IRubyObject io, IRubyObject _from) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + int offset = 0; + int length = defaultLength(context, offset); + + if (!scheduler.isNil()) { + Ruby runtime = context.runtime; + IRubyObject result = FiberScheduler.ioPRead(context, scheduler, io, this, fromInteger, RubyFixnum.newFixnum(runtime, length), RubyFixnum.zero(runtime)); + + if (result != UNDEF) { + return result; + } + } + + int from = RubyNumeric.num2int(_from); + + + return pread(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + @JRubyMethod(name = "pread") + public IRubyObject pread(ThreadContext context, IRubyObject io, IRubyObject _from, IRubyObject _length) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + RubyInteger lengthInteger = _length.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPRead(context, scheduler, io, this, fromInteger, lengthInteger, RubyFixnum.zero(context.runtime)); + + if (result != UNDEF) { + return result; + } + } + + int from = RubyNumeric.num2int(fromInteger); + + int offset = 0; + int length = extractLength(context, lengthInteger, offset); + + return pread(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + @JRubyMethod(name = "pread", required = 2, optional = 2, checkArity = false) + public IRubyObject pread(ThreadContext context, IRubyObject[] args) { + Arity.checkArgumentCount(context, args, 2, 4); + + switch (args.length) { + case 2: + return pread(context, args[0], args[1]); + case 3: + return pread(context, args[0], args[1], args[2]); + case 4: + return pread(context, args[0], args[1], args[2], args[3]); + } + + return context.nil; + } + + public IRubyObject pread(ThreadContext context, IRubyObject io, IRubyObject _from, IRubyObject _length, IRubyObject _offset) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + RubyInteger lengthInteger = _length.convertToInteger(); + RubyInteger offsetInteger = _offset.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPRead(context, scheduler, io, this, fromInteger, lengthInteger, offsetInteger); + + if (result != UNDEF) { + return result; + } + } + + int from = RubyNumeric.num2int(fromInteger); + + int offset = extractOffset(context, offsetInteger); + int length = extractLength(context, lengthInteger, offset); + + return pread(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + public IRubyObject pread(ThreadContext context, RubyIO io, int from, int length, int offset) { + validateRange(context, offset, length); + + ByteBuffer buffer = getBufferForWriting(context); + + return preadInternal(context, io, buffer, from, offset, length); + } + + /** + * Read from the given io into the given buffer base at the given offset, from location, and size limit. The buffer will be left + * with its position unchanged. + * + * MRI: io_buffer_pread_internal + * + * @param context + * @param io + * @param base + * @param from + * @param offset + * @param size + * @return + */ + private static IRubyObject preadInternal(ThreadContext context, RubyIO io, ByteBuffer base, int from, int offset, int size) { + OpenFile fptr = io.getOpenFileChecked(); + final boolean locked = fptr.lock(); + try { + base.position(offset); + base.limit(offset + size); + int result = OpenFile.preadInternal(context, fptr, fptr.fd(), base, from, size); + return FiberScheduler.result(context.runtime, result, fptr.errno()); + } finally { + base.clear(); + if (locked) fptr.unlock(); + } + } + + // MRI: length parts of io_buffer_extract_length_offset and io_buffer_extract_length + private int extractLength(ThreadContext context, IRubyObject _length, int offset) { + if (!_length.isNil()) { + if (RubyNumeric.negativeInt(context, _length)) { + throw context.runtime.newArgumentError("Length can't be negative!"); + } + + return RubyNumeric.num2int(_length); + } + + return defaultLength(context, offset); + } + + private int defaultLength(ThreadContext context, int offset) { + if (offset > size) { + throw context.runtime.newArgumentError("The given offset is bigger than the buffer size!"); + } + + // Note that the "length" is computed by the size the offset. + return size - offset; + } + + // MRI: offset parts of io_buffer_extract_length_offset and io_buffer_extract_offset + private static int extractOffset(ThreadContext context, IRubyObject _offset) { + if (RubyNumeric.negativeInt(context, _offset)) { + throw context.runtime.newArgumentError("Offset can't be negative!"); + } + + return RubyNumeric.num2int(_offset); + } + + private static int extractSize(ThreadContext context, IRubyObject _size) { + if (RubyNumeric.negativeInt(context, _size)) { + throw context.runtime.newArgumentError("Size can't be negative!"); + } + + return RubyNumeric.num2int(_size); + } + + @JRubyMethod(name = "write") + public IRubyObject write(ThreadContext context, IRubyObject io) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + + if (!scheduler.isNil()) { + Ruby runtime = context.runtime; + IRubyObject result = FiberScheduler.ioWrite(context, scheduler, io, this, RubyFixnum.newFixnum(runtime, size), RubyFixnum.zero(runtime)); + + if (result != null) { + return result; + } + } + + return write(context, io, size, 0); + } + + @JRubyMethod(name = "write") + public IRubyObject write(ThreadContext context, IRubyObject io, IRubyObject length) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger lengthInteger = length.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioWrite(context, scheduler, io, this, lengthInteger, RubyFixnum.zero(context.runtime)); + + if (result != null) { + return result; + } + } + + return write(context, io, lengthInteger.getIntValue(), 0); + } + + @JRubyMethod(name = "write") + public IRubyObject write(ThreadContext context, IRubyObject io, IRubyObject length, IRubyObject offset) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger lengthInteger = length.convertToInteger(); + RubyInteger offsetInteger = offset.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioWrite(context, scheduler, io, this, lengthInteger, offsetInteger); + + if (result != null) { + return result; + } + } + + return write(context, io, lengthInteger.getIntValue(), offsetInteger.getIntValue()); + } + + public IRubyObject write(ThreadContext context, IRubyObject io, int length, int offset) { + validateRange(context, offset, length); + + ByteBuffer buffer = getBufferForReading(context); + + return writeInternal(context, RubyIO.convertToIO(context, io), buffer, offset, length); + } + + private static IRubyObject writeInternal(ThreadContext context, RubyIO io, ByteBuffer base, int offset, int size) { + OpenFile fptr = io.getOpenFileChecked(); + final boolean locked = fptr.lock(); + try { + base.position(offset); + base.limit(offset + size); + int result = OpenFile.writeInternal(context, fptr, base, offset, size); + return FiberScheduler.result(context.runtime, result, fptr.errno()); + } finally { + base.clear(); + if (locked) fptr.unlock(); + } + } + + @JRubyMethod(name = "pwrite") + public IRubyObject pwrite(ThreadContext context, IRubyObject io, IRubyObject _from) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + int offset = 0; + int length = defaultLength(context, offset); + + if (!scheduler.isNil()) { + Ruby runtime = context.runtime; + IRubyObject result = FiberScheduler.ioPWrite(context, scheduler, io, this, fromInteger, RubyFixnum.newFixnum(runtime, length), RubyFixnum.zero(runtime)); + + if (result != null) { + return result; + } + } + + int from = RubyNumeric.num2int(fromInteger); + + return pwrite(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + @JRubyMethod(name = "pwrite") + public IRubyObject pwrite(ThreadContext context, IRubyObject io, IRubyObject _from, IRubyObject _length) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + RubyInteger lengthInteger = _length.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPWrite(context, scheduler, io, this, fromInteger, lengthInteger, RubyFixnum.zero(context.runtime)); + + if (result != null) { + return result; + } + } + + int from = RubyNumeric.num2int(fromInteger); + + int offset = 0; + int length = extractLength(context, lengthInteger, offset); + + return pwrite(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + @JRubyMethod(name = "pwrite", required = 2, optional = 2, checkArity = false) + public IRubyObject pwrite(ThreadContext context, IRubyObject[] args) { + Arity.checkArgumentCount(context, args, 2, 4); + + switch (args.length) { + case 2: + return pwrite(context, args[0], args[1]); + case 3: + return pwrite(context, args[0], args[1], args[2]); + case 4: + return pwrite(context, args[0], args[1], args[2], args[3]); + } + + return context.nil; + } + + public IRubyObject pwrite(ThreadContext context, IRubyObject io, IRubyObject _from, IRubyObject _length, IRubyObject _offset) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + RubyInteger fromInteger = _from.convertToInteger(); + RubyInteger lengthInteger = _length.convertToInteger(); + RubyInteger offsetInteger = _offset.convertToInteger(); + + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPWrite(context, scheduler, io, this, fromInteger, lengthInteger, offsetInteger); + + if (result != null) { + return result; + } + } + + int from = RubyNumeric.num2int(fromInteger); + + int offset = extractOffset(context, offsetInteger); + int length = extractLength(context, lengthInteger, offset); + + return pwrite(context, RubyIO.convertToIO(context, io), from, length, offset); + } + + public IRubyObject pwrite(ThreadContext context, RubyIO io, int from, int length, int offset) { + validateRange(context, offset, length); + + ByteBuffer buffer = getBufferForReading(context); + int size = this.size - offset; + + return pwriteInternal(context, RubyIO.convertToIO(context, io), buffer, from, offset, length); + } + + private static IRubyObject pwriteInternal(ThreadContext context, RubyIO io, ByteBuffer base, int from, int offset, int size) { + OpenFile fptr = io.getOpenFileChecked(); + final boolean locked = fptr.lock(); + try { + base.position(offset); + base.limit(offset + size); + int result = OpenFile.pwriteInternal(context, fptr, fptr.fd(), base, from, size); + return FiberScheduler.result(context.runtime, result, fptr.errno()); + } finally { + base.clear(); + if (locked) fptr.unlock(); + } + } + + enum DataType { + U8(NativeType.UCHAR, BIG_ENDIAN), + S8(NativeType.SCHAR, BIG_ENDIAN), + u16(NativeType.USHORT, LITTLE_ENDIAN), + U16(NativeType.USHORT, BIG_ENDIAN), + s16(NativeType.SSHORT, LITTLE_ENDIAN), + S16(NativeType.SSHORT, BIG_ENDIAN), + u32(NativeType.UINT, LITTLE_ENDIAN), + U32(NativeType.UINT, BIG_ENDIAN), + s32(NativeType.SINT, LITTLE_ENDIAN), + S32(NativeType.SINT, BIG_ENDIAN), + u64(NativeType.ULONG, LITTLE_ENDIAN), + U64(NativeType.ULONG, BIG_ENDIAN), + s64(NativeType.SLONG, LITTLE_ENDIAN), + S64(NativeType.SLONG, BIG_ENDIAN), + f32(NativeType.FLOAT,LITTLE_ENDIAN), + F32(NativeType.FLOAT, BIG_ENDIAN), + f64(NativeType.DOUBLE, LITTLE_ENDIAN), + F64(NativeType.DOUBLE, BIG_ENDIAN); + + DataType(NativeType type, int endian) { + this.type = FFI_RUNTIME.findType(type); + this.endian = endian; + } + + private final Type type; + private final int endian; + } + + private static long swapAsShort(long l) { + short s = (short) l; + + return (s >>> 8) | (int) (s << 8); + } + + private static short swap(short s) { + return (short) ((s >>> 8) | (s << 8)); + } + + private static long swapAsInt(long l) { + int s = (int) l; + + return (s >>> 16) | (int) (s << 16); + } + + private static long swapAsLong(long l) { + return (l >>> 32) | (l << 32); + } + + private static long swapAsFloat(long l) { + return swapAsInt(l); + } + + private static long swapAsDouble(long l) { + return swapAsLong(l); + } + + private ByteBuffer base; + private int size; + private int flags; + private IRubyObject source; +} diff --git a/core/src/main/java/org/jruby/RubyModule.java b/core/src/main/java/org/jruby/RubyModule.java index 0e4d06c9ff9..75306f95c77 100644 --- a/core/src/main/java/org/jruby/RubyModule.java +++ b/core/src/main/java/org/jruby/RubyModule.java @@ -5108,6 +5108,10 @@ private RaiseException cannotRemoveError(String id) { return getRuntime().newNameError(str(runtime, "cannot remove ", ids(runtime, id), " for ", types(runtime, this)), id); } + public static boolean testModuleMatch(ThreadContext context, IRubyObject arg0, int id) { + return arg0 instanceof RubyModule && ((RubyModule) arg0).id == id; + } + // ////////////////// INTERNAL MODULE VARIABLE API METHODS //////////////// @@ -6159,5 +6163,5 @@ public synchronized void defineAliases(List aliases, String oldId) { */ private static final MethodHandle testModuleMatch = Binder .from(boolean.class, ThreadContext.class, IRubyObject.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "testModuleMatch"); + .invokeStaticQuiet(LOOKUP, RubyModule.class, "testModuleMatch"); } diff --git a/core/src/main/java/org/jruby/RubyString.java b/core/src/main/java/org/jruby/RubyString.java index 02a954b31b8..09ea2a90c4c 100644 --- a/core/src/main/java/org/jruby/RubyString.java +++ b/core/src/main/java/org/jruby/RubyString.java @@ -513,6 +513,10 @@ public static RubyString newString(Ruby runtime, byte[] bytes, int start, int le return new RubyString(runtime, runtime.getString(), new ByteList(copy, encoding, false)); } + public static RubyString newStringNoCopy(Ruby runtime, byte[] bytes, int start, int length, Encoding encoding) { + return new RubyString(runtime, runtime.getString(), new ByteList(bytes, start, length, encoding, false)); + } + public static RubyString newString(Ruby runtime, ByteList bytes) { return new RubyString(runtime, runtime.getString(), bytes); } @@ -744,7 +748,8 @@ public static EmptyByteListHolder getEmptyByteList(Encoding enc) { if (enc == null) enc = ASCIIEncoding.INSTANCE; int index = enc.getIndex(); EmptyByteListHolder bytes; - if (index < EMPTY_BYTELISTS.length && (bytes = EMPTY_BYTELISTS[index]) != null) { + EmptyByteListHolder[] emptyBytelists = EMPTY_BYTELISTS; + if (index < emptyBytelists.length && (bytes = emptyBytelists[index]) != null) { return bytes; } return prepareEmptyByteList(enc); @@ -753,12 +758,11 @@ public static EmptyByteListHolder getEmptyByteList(Encoding enc) { private static EmptyByteListHolder prepareEmptyByteList(Encoding enc) { if (enc == null) enc = ASCIIEncoding.INSTANCE; int index = enc.getIndex(); - if (index >= EMPTY_BYTELISTS.length) { - EmptyByteListHolder tmp[] = new EmptyByteListHolder[index + 4]; - System.arraycopy(EMPTY_BYTELISTS,0, tmp, 0, EMPTY_BYTELISTS.length); - EMPTY_BYTELISTS = tmp; + EmptyByteListHolder[] emptyBytelists = EMPTY_BYTELISTS; + if (index >= emptyBytelists.length) { + EMPTY_BYTELISTS = emptyBytelists = Arrays.copyOfRange(emptyBytelists, 0, index + 4); } - return EMPTY_BYTELISTS[index] = new EmptyByteListHolder(enc); + return emptyBytelists[index] = new EmptyByteListHolder(enc); } public static RubyString newEmptyString(Ruby runtime, RubyClass metaClass, Encoding enc) { diff --git a/core/src/main/java/org/jruby/RubyThread.java b/core/src/main/java/org/jruby/RubyThread.java index 69eb9201004..17dc8760637 100644 --- a/core/src/main/java/org/jruby/RubyThread.java +++ b/core/src/main/java/org/jruby/RubyThread.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; @@ -64,7 +65,6 @@ import org.jruby.exceptions.RaiseException; import org.jruby.exceptions.ThreadKill; import org.jruby.exceptions.Unrescuable; -import org.jruby.ext.thread.Mutex; import org.jruby.internal.runtime.RubyNativeThread; import org.jruby.internal.runtime.RubyRunnable; import org.jruby.internal.runtime.ThreadLike; @@ -84,7 +84,6 @@ import org.jruby.runtime.backtrace.FrameType; import org.jruby.runtime.backtrace.RubyStackTraceElement; import org.jruby.runtime.builtin.IRubyObject; -import org.jruby.runtime.load.LoadService; import org.jruby.util.ByteList; import org.jruby.util.StringSupport; import org.jruby.util.io.BlockingIO; @@ -212,10 +211,15 @@ public enum Status { /** Short circuit to avoid-re-scanning for interrupts */ private volatile boolean pendingInterruptQueueChecked = false; + private volatile BlockingTask currentBlockingTask; + private volatile Selector currentSelector; private volatile RubyThread fiberCurrentThread; + private IRubyObject scheduler; + private volatile int blockingCount = 1; + private static final AtomicIntegerFieldUpdater INTERRUPT_FLAG_UPDATER = AtomicIntegerFieldUpdater.newUpdater(RubyThread.class, "interruptFlag"); @@ -234,6 +238,7 @@ protected RubyThread(Ruby runtime, RubyClass type, boolean adopted) { finalResult = errorInfo = runtime.getNil(); reportOnException = runtime.isReportOnException(); + scheduler = runtime.getNil(); this.adopted = adopted; } @@ -431,6 +436,11 @@ public void dispose() { // unlock all locked locks unlockAll(); + // close scheduler, if any + if (scheduler != null && !scheduler.isNil()) { + FiberScheduler.close(getContext(), scheduler); + } + // mark thread as DEAD beDead(); } @@ -1785,25 +1795,53 @@ public int executeReadWrite( ReadWrite task) throws InterruptedException { Status oldStatus = STATUS.get(this); try { - this.unblockArg = data; - this.unblockFunc = task; + preReadWrite(context, data, task); - // check for interrupt before going into blocking call - blockingThreadPoll(context); + return task.run(context, data, bytes, start, length); + } finally { + postReadWrite(context, oldStatus); + } + } - STATUS.set(this, Status.SLEEP); + public int executeReadWrite( + ThreadContext context, + Data data, ByteBuffer bytes, int start, int length, + ReadWrite task) throws InterruptedException { + Status oldStatus = STATUS.get(this); + try { + preReadWrite(context, data, task); return task.run(context, data, bytes, start, length); } finally { - STATUS.set(this, oldStatus); - this.unblockFunc = null; - this.unblockArg = null; - pollThreadEvents(context); + postReadWrite(context, oldStatus); } } + private void postReadWrite(ThreadContext context, Status oldStatus) { + STATUS.set(this, oldStatus); + this.unblockFunc = null; + this.unblockArg = null; + pollThreadEvents(context); + } + + private void preReadWrite(ThreadContext context, Data data, ReadWrite task) { + this.unblockArg = data; + this.unblockFunc = task; + + // check for interrupt before going into blocking call + blockingThreadPoll(context); + + STATUS.set(this, Status.SLEEP); + } + public interface ReadWrite extends Unblocker { - public int run(ThreadContext context, Data data, byte[] bytes, int start, int length) throws InterruptedException; + /** + * @deprecated Prefer version that receives ByteBuffer rather than recreating every time. + */ + public default int run(ThreadContext context, Data data, byte[] bytes, int start, int length) throws InterruptedException { + return run(context, data, ByteBuffer.wrap(bytes), start, length); + } + public int run(ThreadContext context, Data data, ByteBuffer bytes, int start, int length) throws InterruptedException; public void wakeup(RubyThread thread, Data data); } @@ -2558,100 +2596,51 @@ public static IRubyObject uninterruptible(ThreadContext context, Sta f); } - private static final String MUTEX_FOR_THREAD_EXCLUSIVE = "MUTEX_FOR_THREAD_EXCLUSIVE"; - - @Deprecated // Thread.exclusive(&block) - @JRubyMethod(meta = true) - public static IRubyObject exclusive(ThreadContext context, IRubyObject recv, Block block) { - recv.callMethod(context, "warn", context.runtime.newString("Thread.exclusive is deprecated, use Thread::Mutex")); - return getMutexForThreadExclusive(context, (RubyClass) recv).synchronize(context, block); - } - - private static Mutex getMutexForThreadExclusive(ThreadContext context, RubyClass recv) { - Mutex mutex = (Mutex) recv.getConstantNoConstMissing(MUTEX_FOR_THREAD_EXCLUSIVE, false, false); - if (mutex != null) return mutex; - synchronized (recv) { - mutex = (Mutex) recv.getConstantNoConstMissing(MUTEX_FOR_THREAD_EXCLUSIVE, false, false); - if (mutex == null) { - mutex = Mutex.newInstance(context, context.runtime.getMutex(), NULL_ARRAY, Block.NULL_BLOCK); - recv.setConstant(MUTEX_FOR_THREAD_EXCLUSIVE, mutex, true); - } - return mutex; - } - } - /** - * This is intended to be used to raise exceptions in Ruby threads from non- - * Ruby threads like Timeout's thread. + * Set the scheduler for the current thread. * - * @param args Same args as for Thread#raise + * MRI: rb_fiber_scheduler_set */ - @Deprecated - public void internalRaise(IRubyObject[] args) { - ThreadContext context = getRuntime().getCurrentContext(); - genericRaise(context, context.getThread(), args); - } + public IRubyObject setFiberScheduler(IRubyObject scheduler) { +// VM_ASSERT(ruby_thread_has_gvl_p()); - @Deprecated - public void receiveMail(ThreadService.Event event) { - } + scheduler.getClass(); // !null - @Deprecated - public void checkMail(ThreadContext context) { - } + if (scheduler != null && !scheduler.isNil()) { + FiberScheduler.verifyInterface(scheduler); + } - @Deprecated - private volatile BlockingTask currentBlockingTask; + if (!this.scheduler.isNil()) { + FiberScheduler.close(getContext(), this.scheduler); + } - @Deprecated - public boolean selectForAccept(RubyIO io) { - return select(io, SelectionKey.OP_ACCEPT); - } + this.scheduler = scheduler; - @Deprecated - public IRubyObject backtrace20(ThreadContext context, IRubyObject[] args) { - return backtrace(context); + return scheduler; } - @Deprecated - public IRubyObject backtrace(ThreadContext context, IRubyObject[] args) { - switch (args.length) { - case 0: - return backtrace(context); - case 1: - return backtrace(context, args[0]); - case 2: - return backtrace(context, args[0], args[1]); - default: - Arity.checkArgumentCount(context.runtime, args, 0, 2); - return null; // not reached - } + public IRubyObject getScheduler() { + return scheduler; } - @Deprecated - public IRubyObject backtrace_locations(ThreadContext context, IRubyObject[] args) { - switch (args.length) { - case 0: - return backtrace_locations(context); - case 1: - return backtrace_locations(context, args[0]); - case 2: - return backtrace_locations(context, args[0], args[1]); - default: - Arity.checkArgumentCount(context.runtime, args, 0, 2); - return null; // not reached + // MRI: rb_fiber_scheduler_current_for_threadptr, rb_fiber_scheduler_current + public IRubyObject getSchedulerCurrent() { + if (!isBlocking()) { + return scheduler; } + + return getRuntime().getNil(); } - @Deprecated - public static IRubyObject pass(IRubyObject recv) { - Ruby runtime = recv.getRuntime(); + public void incrementBlocking() { + blockingCount++; + } - return pass(runtime.getCurrentContext(), recv); + public void decrementBlocking() { + blockingCount--; } - @Deprecated - public IRubyObject safe_level() { - throw getRuntime().newNotImplementedError("Thread-specific SAFE levels are not supported"); + public boolean isBlocking() { + return blockingCount > 0; } } diff --git a/core/src/main/java/org/jruby/RubyTime.java b/core/src/main/java/org/jruby/RubyTime.java index 5a81b3a78b7..a3c960c29b8 100644 --- a/core/src/main/java/org/jruby/RubyTime.java +++ b/core/src/main/java/org/jruby/RubyTime.java @@ -1715,16 +1715,21 @@ private IRubyObject initializeNow(ThreadContext context, IRubyObject zone) { public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { boolean keywords = hasKeywords(context.resetCallInfo()); IRubyObject zone = null; - context.resetCallInfo(); IRubyObject nil = context.nil; int argc = args.length; if (keywords) { - IRubyObject in = ArgsUtil.extractKeywordArg(context, (RubyHash) args[argc - 1], "in"); - if (in != null && argc > 7) { - throw context.runtime.newArgumentError("timezone argument given as positional and keyword arguments"); + IRubyObject in = ArgsUtil.extractKeywordArg(context, args[argc - 1], "in"); + if (in != null) { + if (argc > 7) { + throw context.runtime.newArgumentError("timezone argument given as positional and keyword arguments"); + } + zone = in; + } else { + if (argc > 6) { + zone = args[6]; + } } - zone = in; } else if (argc > 6) { zone = args[6]; } diff --git a/core/src/main/java/org/jruby/ext/fiber/FiberQueue.java b/core/src/main/java/org/jruby/ext/fiber/FiberQueue.java index 50b6484d0bc..0a9aa0b2814 100644 --- a/core/src/main/java/org/jruby/ext/fiber/FiberQueue.java +++ b/core/src/main/java/org/jruby/ext/fiber/FiberQueue.java @@ -50,7 +50,7 @@ public FiberQueue(Ruby runtime) { this.queue = new ArrayBlockingQueue<>(1, false); } - final RubyThread.Task takeTask = new RubyThread.Task() { + private static final RubyThread.Task TAKE_TASK = new RubyThread.Task() { @Override public FiberRequest run(ThreadContext context, FiberQueue queue) throws InterruptedException { return queue.getQueueSafe().take(); @@ -89,7 +89,7 @@ public synchronized void checkShutdown() { public FiberRequest pop(ThreadContext context) { try { - return context.getThread().executeTaskBlocking(context, this, takeTask); + return context.getThread().executeTaskBlocking(context, this, TAKE_TASK); } catch (InterruptedException ie) { throw context.runtime.newThreadError("interrupted in FiberQueue.pop"); } diff --git a/core/src/main/java/org/jruby/ext/fiber/ThreadFiber.java b/core/src/main/java/org/jruby/ext/fiber/ThreadFiber.java index 36b803899dc..7844b49fa5d 100644 --- a/core/src/main/java/org/jruby/ext/fiber/ThreadFiber.java +++ b/core/src/main/java/org/jruby/ext/fiber/ThreadFiber.java @@ -70,6 +70,10 @@ private static void nativeThreadLauncher(Ruby runtime, Runnable runnable) { runtime.getFiberExecutor().submit(runnable); } + public boolean isBlocking() { + return data.blocking; + } + private static class VirtualThreadLauncher implements BiConsumer { @Override public void accept(Ruby ruby, Runnable runnable) { @@ -96,7 +100,7 @@ public static void initRootFiber(ThreadContext context, RubyThread currentThread ThreadFiber rootFiber = new ThreadFiber(runtime, runtime.getFiber(), true); - rootFiber.data = new FiberData(new FiberQueue(runtime), currentThread, rootFiber, false); + rootFiber.data = new FiberData(new FiberQueue(runtime), currentThread, rootFiber, true); rootFiber.thread = currentThread; context.setRootFiber(rootFiber); } @@ -127,10 +131,15 @@ public IRubyObject initialize(ThreadContext context, IRubyObject _opts, Block bl boolean blocking = false; if (!opts.isNil()) { - IRubyObject blockingOpt = ArgsUtil.extractKeywordArg(context, opts, "blocking"); + IRubyObject[] blockingPoolOpt = ArgsUtil.extractKeywordArgs(context, opts, "blocking", "pool"); - if (!blockingOpt.isNil()) { - blocking = blockingOpt.isTrue(); + if (blockingPoolOpt != null) { + IRubyObject blockingOpt = blockingPoolOpt[0]; + if (blockingOpt != null && !blockingOpt.isNil()) { + blocking = blockingOpt.isTrue(); + } + + // TODO: pooling } } @@ -181,6 +190,10 @@ public IRubyObject resume(ThreadContext context, IRubyObject[] values) { data.prev = null; } + if (data.blocking) { + context.getFiberCurrentThread().decrementBlocking(); + } + if (result.type == RequestType.RAISE) { throw ((RubyException) result.data).toThrowable(); } @@ -221,12 +234,28 @@ private static FiberRequest exchangeWithFiber(ThreadContext context, FiberData c // interrupted again and must abandon the fiber. try { + adjustThreadBlocking(context, currentFiberData, targetFiberData); + return currentFiberData.queue.pop(context); } catch (RaiseException re) { handleExceptionDuringExchange(context, currentFiberData, targetFiberData, re); // if we get here, we forwarded exception so try once more return currentFiberData.queue.pop(context); + } finally { + adjustThreadBlocking(context, targetFiberData, currentFiberData); + } + } + + private static void adjustThreadBlocking(ThreadContext context, FiberData currentFiberData, FiberData targetFiberData) { + // if fiber we are leaving is blocking, decrement thread blocking count + if (currentFiberData.blocking) { + context.getFiberCurrentThread().decrementBlocking(); + } + + // if fiber we are entering is blocking, increment thread blocking count + if (targetFiberData.blocking) { + context.getFiberCurrentThread().incrementBlocking(); } } @@ -276,7 +305,7 @@ private static void handleExceptionDuringExchange(ThreadContext context, FiberDa } @JRubyMethod(rest = true) - public IRubyObject __transfer__(ThreadContext context, IRubyObject[] values) { + public IRubyObject transfer(ThreadContext context, IRubyObject[] values) { Ruby runtime = context.runtime; final FiberData data = this.data; @@ -426,13 +455,13 @@ private static FiberData verifyCurrentFiber(ThreadContext context, Ruby runtime) return currentFiberData; } - @JRubyMethod - public IRubyObject __alive__(ThreadContext context) { + @JRubyMethod(name = "alive?") + public IRubyObject alive_p(ThreadContext context) { return RubyBoolean.newBoolean(context, alive()); } @JRubyMethod(meta = true) - public static IRubyObject __current__(ThreadContext context, IRubyObject recv) { + public static IRubyObject current(ThreadContext context, IRubyObject recv) { return context.getFiber(); } @@ -462,8 +491,9 @@ static RubyThread createThread(final Ruby runtime, final FiberData data, final F ThreadContext context = runtime.getCurrentContext(); context.setFiber(data.fiber.get()); context.useRecursionGuardsFrom(data.parent.getContext()); - fiberThread.set(context.getThread()); - context.getThread().setFiberCurrentThread(data.parent); + RubyThread rubyThread = context.getThread(); + fiberThread.set(rubyThread); + rubyThread.setFiberCurrentThread(data.parent); Thread thread = Thread.currentThread(); String oldName = thread.getName(); @@ -586,17 +616,43 @@ protected void finalize() throws Throwable { @JRubyMethod(name = "blocking?") public IRubyObject blocking_p(ThreadContext context) { - return RubyBoolean.newBoolean(context, data.blocking); + return RubyBoolean.newBoolean(context, isBlocking()); } @JRubyMethod(name = "blocking?", meta = true) public static IRubyObject blocking_p_s(ThreadContext context, IRubyObject self) { - boolean blocking = context.getFiber().data.blocking; + boolean blocking = context.getFiber().isBlocking(); if (!blocking) return context.fals; return RubyFixnum.one(context.runtime); } + @JRubyMethod(name = "blocking", meta = true) + public static IRubyObject blocking(ThreadContext context, IRubyObject self, Block block) { + ThreadFiber currentFiber = context.getFiber(); + boolean blocking = currentFiber.isBlocking(); + + // If we are already blocking, this is essentially a no-op: + if (currentFiber.isBlocking()) { + return block.yieldSpecific(context, currentFiber); + } + + try { + assert !currentFiber.isBlocking() : "fiber was blocking when it should not have been"; + + currentFiber.data.blocking = true; + + // Once the fiber is blocking, and current, we increment the thread blocking state: + context.getFiberCurrentThread().incrementBlocking(); + + return block.yieldSpecific(context, currentFiber); + } finally { + // We are no longer blocking: + currentFiber.data.blocking = false; + context.getFiberCurrentThread().decrementBlocking(); + } + } + @JRubyMethod(name = "backtrace") public IRubyObject backtrace(ThreadContext context) { return backtrace(context, null, null); @@ -635,6 +691,42 @@ public IRubyObject backtrace_locations(ThreadContext context, IRubyObject level, return threadFiber.thread.backtrace_locations(context, level, length); } + public static class FiberSchedulerSupport { + // MRI: rb_fiber_s_schedule_kw and rb_fiber_s_schedule, kw passes on context + @JRubyMethod(name = "schedule", meta = true, rest = true, keywords = true) + public static IRubyObject schedule(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) { + RubyThread thread = context.getThread(); + IRubyObject scheduler = thread.getScheduler(); + IRubyObject fiber = context.nil; + + if (!scheduler.isNil()) { + fiber = scheduler.callMethod(context, "fiber", args, block); + } else { + throw context.runtime.newRuntimeError("No scheduler is available!"); + } + + return fiber; + } + + // MRI: rb_fiber_s_scheduler + @JRubyMethod(name = "scheduler", meta = true) + public static IRubyObject get_scheduler(ThreadContext context, IRubyObject self) { + return context.getFiberCurrentThread().getScheduler(); + } + + // MRI: rb_fiber_current_scheduler + @JRubyMethod(name = "current_scheduler", meta = true) + public static IRubyObject current_scheduler(ThreadContext context, IRubyObject self) { + return context.getFiberCurrentThread().getSchedulerCurrent(); + } + + // MRI: rb_fiber_set_scheduler + @JRubyMethod(name = "set_scheduler", meta = true) + public static IRubyObject set_scheduler(ThreadContext context, IRubyObject self, IRubyObject scheduler) { + return context.getFiberCurrentThread().setFiberScheduler(scheduler); + } + } + public FiberData getData() { return data; } @@ -647,8 +739,8 @@ public static class FiberData { FiberData(FiberQueue queue, RubyThread parent, ThreadFiber fiber, boolean blocking) { this.queue = queue; this.parent = parent; - this.blocking = blocking; this.fiber = new WeakReference(fiber); + this.blocking = blocking; } public ThreadFiber getPrev() { @@ -660,7 +752,7 @@ public ThreadFiber getPrev() { final RubyThread parent; final WeakReference fiber; volatile boolean transferred; - final boolean blocking; + volatile boolean blocking; } volatile FiberData data; diff --git a/core/src/main/java/org/jruby/ext/fiber/ThreadFiberLibrary.java b/core/src/main/java/org/jruby/ext/fiber/ThreadFiberLibrary.java index e6582249d6a..92d2c722269 100644 --- a/core/src/main/java/org/jruby/ext/fiber/ThreadFiberLibrary.java +++ b/core/src/main/java/org/jruby/ext/fiber/ThreadFiberLibrary.java @@ -33,6 +33,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.load.Library; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.cli.Options; /** * A thread-based implementation of Ruby 1.9 Fiber library. @@ -43,6 +44,11 @@ public RubyClass createFiberClass(final Ruby runtime) { cFiber.defineAnnotatedMethods(ThreadFiber.class); + if (Options.FIBER_SCHEDULER.load()) { + // define additional methods for Fiber::Scheduler support + cFiber.defineAnnotatedMethods(ThreadFiber.FiberSchedulerSupport.class); + } + return cFiber; } } diff --git a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java index e15b1089658..3bbbfd07118 100644 --- a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java +++ b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java @@ -48,8 +48,9 @@ import org.jruby.ir.representations.BasicBlock; import org.jruby.ir.runtime.IRRuntimeHelpers; import org.jruby.ir.targets.IRBytecodeAdapter.BlockPassType; -import org.jruby.ir.targets.indy.Bootstrap; import org.jruby.ir.targets.indy.CallTraceSite; +import org.jruby.ir.targets.indy.CoverageSite; +import org.jruby.ir.targets.indy.MetaClassBootstrap; import org.jruby.parser.StaticScope; import org.jruby.runtime.ArgumentDescriptor; import org.jruby.runtime.Block; @@ -57,7 +58,6 @@ import org.jruby.runtime.DynamicScope; import org.jruby.runtime.Frame; import org.jruby.runtime.Helpers; -import org.jruby.runtime.RubyEvent; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.IRubyObject; @@ -1515,7 +1515,7 @@ public void DefineMetaClassInstr(DefineMetaClassInstr definemetaclassinstr) { jvmAdapter().invokedynamic( "openMetaClass", sig(DynamicMethod.class, ThreadContext.class, IRubyObject.class, String.class, StaticScope.class), - Bootstrap.OPEN_META_CLASS, + MetaClassBootstrap.OPEN_META_CLASS, bodyHandle, scopeHandle, setScopeHandle, @@ -1738,7 +1738,7 @@ public void LineNumberInstr(LineNumberInstr linenumberinstr) { jvmAdapter().invokedynamic( "coverLine", sig(void.class, ThreadContext.class), - Bootstrap.coverLineHandle(), + CoverageSite.COVER_LINE_BOOTSTRAP, jvm.methodData().scope.getFile(), linenumberinstr.getLineNumber(), linenumberinstr.oneshot ? 1 : 0); diff --git a/core/src/main/java/org/jruby/ir/targets/indy/ArrayBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/ArrayBootstrap.java new file mode 100644 index 00000000000..a3ffbe429e9 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/ArrayBootstrap.java @@ -0,0 +1,48 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.RubyArray; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.specialized.RubyArraySpecialized; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class ArrayBootstrap { + public static final Handle ARRAY_H = new Handle( + Opcodes.H_INVOKESTATIC, + p(ArrayBootstrap.class), + "array", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private static final MethodHandle ARRAY_HANDLE = + Binder + .from(RubyArray.class, ThreadContext.class, IRubyObject[].class) + .invokeStaticQuiet(LOOKUP, ArrayBootstrap.class, "array"); + + public static CallSite array(MethodHandles.Lookup lookup, String name, MethodType type) { + MethodHandle handle = Binder + .from(type) + .collect(1, IRubyObject[].class) + .invoke(ARRAY_HANDLE); + + return new ConstantCallSite(handle); + } + + public static RubyArray array(ThreadContext context, IRubyObject[] ary) { + assert ary.length > RubyArraySpecialized.MAX_PACKED_SIZE; + // array() only dispatches here if ^^ holds + return RubyArray.newArrayNoCopy(context.runtime, ary); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java index cd3d6f09581..2c20d13e35d 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java @@ -26,95 +26,21 @@ package org.jruby.ir.targets.indy; -import com.headius.invokebinder.Binder; -import com.headius.invokebinder.Signature; -import com.headius.invokebinder.SmartBinder; -import com.headius.invokebinder.SmartHandle; -import org.jcodings.Encoding; -import org.jcodings.EncodingDB; -import org.jruby.Ruby; -import org.jruby.RubyArray; -import org.jruby.RubyBasicObject; -import org.jruby.RubyBignum; -import org.jruby.RubyBoolean; -import org.jruby.RubyClass; -import org.jruby.RubyEncoding; -import org.jruby.RubyFixnum; -import org.jruby.RubyFloat; -import org.jruby.RubyGlobal; -import org.jruby.RubyHash; -import org.jruby.RubyModule; -import org.jruby.RubyNil; -import org.jruby.RubyString; -import org.jruby.anno.JRubyMethod; -import org.jruby.internal.runtime.GlobalVariable; -import org.jruby.internal.runtime.methods.AttrReaderMethod; -import org.jruby.internal.runtime.methods.AttrWriterMethod; -import org.jruby.internal.runtime.methods.CompiledIRMethod; import org.jruby.internal.runtime.methods.DynamicMethod; -import org.jruby.internal.runtime.methods.HandleMethod; -import org.jruby.internal.runtime.methods.MixedModeIRMethod; -import org.jruby.internal.runtime.methods.NativeCallMethod; -import org.jruby.ir.JIT; -import org.jruby.ir.runtime.IRRuntimeHelpers; -import org.jruby.java.invokers.SingletonMethodInvoker; -import org.jruby.javasupport.JavaUtil; -import org.jruby.javasupport.proxy.ReifiedJavaProxy; -import org.jruby.parser.StaticScope; -import org.jruby.runtime.Binding; import org.jruby.runtime.Block; -import org.jruby.runtime.CallType; -import org.jruby.runtime.CompiledIRBlockBody; -import org.jruby.runtime.DynamicScope; -import org.jruby.runtime.Frame; -import org.jruby.runtime.Helpers; -import org.jruby.runtime.ThreadContext; -import org.jruby.runtime.builtin.IRubyObject; -import org.jruby.runtime.callsite.CacheEntry; -import org.jruby.runtime.callsite.CachingCallSite; -import org.jruby.runtime.callsite.FunctionalCachingCallSite; -import org.jruby.runtime.callsite.MonomorphicCallSite; -import org.jruby.runtime.callsite.VariableCachingCallSite; -import org.jruby.runtime.invokedynamic.GlobalSite; -import org.jruby.runtime.invokedynamic.InvocationLinker; -import org.jruby.runtime.invokedynamic.MathLinker; -import org.jruby.runtime.ivars.FieldVariableAccessor; -import org.jruby.runtime.ivars.VariableAccessor; -import org.jruby.runtime.opto.Invalidator; -import org.jruby.runtime.opto.OptoFactory; -import org.jruby.runtime.scope.DynamicScopeGenerator; -import org.jruby.specialized.RubyArraySpecialized; -import org.jruby.util.ByteList; -import org.jruby.util.CodegenUtils; -import org.jruby.util.JavaNameMangler; -import org.jruby.util.StringSupport; -import org.jruby.util.cli.Options; import org.jruby.util.log.Logger; import org.jruby.util.log.LoggerFactory; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import java.lang.invoke.CallSite; -import java.lang.invoke.ConstantCallSite; -import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.invoke.MutableCallSite; -import java.lang.invoke.SwitchPoint; -import java.math.BigInteger; import static java.lang.invoke.MethodHandles.Lookup; -import static java.lang.invoke.MethodHandles.constant; import static java.lang.invoke.MethodHandles.dropArguments; -import static java.lang.invoke.MethodHandles.explicitCastArguments; -import static java.lang.invoke.MethodHandles.filterArguments; -import static java.lang.invoke.MethodHandles.insertArguments; -import static java.lang.invoke.MethodHandles.lookup; import static java.lang.invoke.MethodType.methodType; import static org.jruby.runtime.Helpers.arrayOf; -import static org.jruby.runtime.Helpers.constructObjectArrayHandle; -import static org.jruby.runtime.invokedynamic.InvokeDynamicSupport.findStatic; -import static org.jruby.runtime.invokedynamic.InvokeDynamicSupport.findVirtual; import static org.jruby.util.CodegenUtils.p; import static org.jruby.util.CodegenUtils.sig; @@ -125,1358 +51,14 @@ public class Bootstrap { public final static String BOOTSTRAP_INT_INT_SIG = sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class, int.class); public final static String BOOTSTRAP_INT_SIG = sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class); private static final Logger LOG = LoggerFactory.getLogger(Bootstrap.class); - static final Lookup LOOKUP = MethodHandles.lookup(); - public static final Handle EMPTY_STRING_BOOTSTRAP = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "emptyString", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class), - false); - public static final Handle BUFFER_STRING_BOOTSTRAP = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "bufferString", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, int.class), - false); - private static final String[] GENERIC_CALL_PERMUTE = {"context", "self", "arg.*"}; + private static final Lookup LOOKUP = MethodHandles.lookup(); - public static CallSite string(Lookup lookup, String name, MethodType type, String value, String encodingName, int cr) { - return new ConstantCallSite(insertArguments(STRING_HANDLE, 1, bytelist(value, encodingName), cr)); - } - - private static final MethodHandle STRING_HANDLE = - Binder - .from(RubyString.class, ThreadContext.class, ByteList.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "string"); - - public static CallSite fstring(Lookup lookup, String name, MethodType type, String value, String encodingName, int cr, String file, int line) { - MutableCallSite site = new MutableCallSite(type); - - site.setTarget(insertArguments(FSTRING_HANDLE, 1, site, bytelist(value, encodingName), cr, file, line)); - - return site; - } - - private static final MethodHandle FSTRING_HANDLE = - Binder - .from(RubyString.class, ThreadContext.class, MutableCallSite.class, ByteList.class, int.class, String.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "frozenString"); - - public static CallSite emptyString(Lookup lookup, String name, MethodType type, String encodingName) { - RubyString.EmptyByteListHolder holder = RubyString.getEmptyByteList(encodingFromName(encodingName)); - return new ConstantCallSite(insertArguments(STRING_HANDLE, 1, holder.bytes, holder.cr)); - } - - public static CallSite bufferString(Lookup lookup, String name, MethodType type, String encodingName, int size) { - return new ConstantCallSite(insertArguments(BUFFERSTRING_HANDLE, 1, encodingFromName(encodingName), size, StringSupport.CR_7BIT)); - } - - private static final MethodHandle BUFFERSTRING_HANDLE = - Binder - .from(RubyString.class, ThreadContext.class, Encoding.class, int.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "bufferString"); - - public static Handle isNilBoot() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "isNil", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static CallSite isNil(Lookup lookup, String name, MethodType type) { - return new IsNilSite(); - } - - public static class IsNilSite extends MutableCallSite { - - public static final MethodType TYPE = methodType(boolean.class, IRubyObject.class); - - private static final MethodHandle INIT_HANDLE = - Binder.from(TYPE.insertParameterTypes(0, IsNilSite.class)).invokeVirtualQuiet(LOOKUP, "init"); - - private static final MethodHandle IS_NIL_HANDLE = - Binder - .from(boolean.class, IRubyObject.class, RubyNil.class) - .invokeStaticQuiet(LOOKUP, IsNilSite.class, "isNil"); - - public IsNilSite() { - super(TYPE); - - setTarget(INIT_HANDLE.bindTo(this)); - } - - public boolean init(IRubyObject obj) { - IRubyObject nil = obj.getRuntime().getNil(); - - setTarget(insertArguments(IS_NIL_HANDLE, 1, nil)); - - return nil == obj; - } - - public static boolean isNil(IRubyObject obj, RubyNil nil) { - return nil == obj; - } - } - - public static Handle isTrueBoot() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "isTrue", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static CallSite isTrue(Lookup lookup, String name, MethodType type) { - return new IsTrueSite(); - } - - public static class IsTrueSite extends MutableCallSite { - - public static final MethodType TYPE = methodType(boolean.class, IRubyObject.class); - - public static final MethodHandle INIT_HANDLE = Binder.from(TYPE.insertParameterTypes(0, IsTrueSite.class)).invokeVirtualQuiet(LOOKUP, "init"); - - private static final MethodHandle IS_TRUE_HANDLE = - Binder - .from(boolean.class, IRubyObject.class, RubyNil.class, RubyBoolean.False.class) - .invokeStaticQuiet(LOOKUP, IsTrueSite.class, "isTruthy"); - - public IsTrueSite() { - super(TYPE); - - setTarget(INIT_HANDLE.bindTo(this)); - } - - public boolean init(IRubyObject obj) { - Ruby runtime = obj.getRuntime(); - - IRubyObject nil = runtime.getNil(); - IRubyObject fals = runtime.getFalse(); - - setTarget(insertArguments(IS_TRUE_HANDLE, 1, nil, fals)); - - return nil != obj && fals != obj; - } - - public static boolean isTruthy(IRubyObject obj, RubyNil nil, RubyBoolean.False fals) { - return nil != obj && fals != obj; - } - } - - public static CallSite bytelist(Lookup lookup, String name, MethodType type, String value, String encodingName) { - return new ConstantCallSite(constant(ByteList.class, bytelist(value, encodingName))); - } - - public static ByteList bytelist(String value, String encodingName) { - Encoding encoding = encodingFromName(encodingName); - - if (value.length() == 0) { - // special case empty string and don't create a new BL - return RubyString.getEmptyByteList(encoding).bytes; - } - - return new ByteList(RubyEncoding.encodeISO(value), encoding, false); - } - - public static ByteList bytelist(int size, String encodingName) { - Encoding encoding = encodingFromName(encodingName); - - return new ByteList(size, encoding); - } - - private static Encoding encodingFromName(String encodingName) { - Encoding encoding; - EncodingDB.Entry entry = EncodingDB.getEncodings().get(encodingName.getBytes()); - if (entry == null) entry = EncodingDB.getAliases().get(encodingName.getBytes()); - if (entry == null) throw new RuntimeException("could not find encoding: " + encodingName); - encoding = entry.getEncoding(); - return encoding; - } - - public static final Handle CALLSITE = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "callSite", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, int.class), - false); - - public static CallSite callSite(Lookup lookup, String name, MethodType type, String id, int callType) { - return new ConstantCallSite(constant(CachingCallSite.class, callSite(id, callType))); - } - - private static CachingCallSite callSite(String id, int callType) { - switch (CallType.fromOrdinal(callType)) { - case NORMAL: - return new MonomorphicCallSite(id); - case FUNCTIONAL: - return new FunctionalCachingCallSite(id); - case VARIABLE: - return new VariableCachingCallSite(id); - default: - throw new RuntimeException("BUG: Unexpected call type " + callType + " in JVM6 invoke logic"); - } - } - - public static final Handle OPEN_META_CLASS = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "openMetaClass", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, int.class, int.class, int.class), - false); - - @JIT - public static CallSite openMetaClass(Lookup lookup, String name, MethodType type, MethodHandle body, MethodHandle scope, MethodHandle setScope, int line, int dynscopeEliminated, int refinements) { - try { - StaticScope staticScope = (StaticScope) scope.invokeExact(); - return new ConstantCallSite(insertArguments(OPEN_META_CLASS_HANDLE, 4, body, staticScope, setScope, line, dynscopeEliminated == 1 ? true : false, refinements == 1 ? true : false)); - } catch (Throwable t) { - Helpers.throwException(t); - return null; - } - } - - private static final MethodHandle OPEN_META_CLASS_HANDLE = - Binder - .from(DynamicMethod.class, ThreadContext.class, IRubyObject.class, String.class, StaticScope.class, MethodHandle.class, StaticScope.class, MethodHandle.class, int.class, boolean.class, boolean.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "openMetaClass"); - - @JIT - public static DynamicMethod openMetaClass(ThreadContext context, IRubyObject object, String descriptor, StaticScope parent, MethodHandle body, StaticScope scope, MethodHandle setScope, int line, boolean dynscopeEliminated, boolean refinements) throws Throwable { - if (scope == null) { - scope = Helpers.restoreScope(descriptor, parent); - setScope.invokeExact(scope); - } - return IRRuntimeHelpers.newCompiledMetaClass(context, body, scope, object, line, dynscopeEliminated, refinements); - } - - public static final Handle CHECK_ARITY_SPECIFIC_ARGS = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "checkAritySpecificArgs", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class, int.class, int.class, int.class), - false); - - public static final Handle CHECK_ARITY = new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "checkArity", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class, int.class, int.class, int.class), - false); - - @JIT - public static CallSite checkArity(Lookup lookup, String name, MethodType type, int req, int opt, int rest, int keyrest) { - return new ConstantCallSite(insertArguments(CHECK_ARITY_HANDLE, 5, req, opt, rest == 0 ? false : true, keyrest)); - } - - private static final MethodHandle CHECK_ARITY_HANDLE = - Binder - .from(void.class, ThreadContext.class, StaticScope.class, Object[].class, Object.class, Block.class, int.class, int.class, boolean.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "checkArity"); - - @JIT - public static CallSite checkAritySpecificArgs(Lookup lookup, String name, MethodType type, int req, int opt, int rest, int keyrest) { - return new ConstantCallSite(insertArguments(CHECK_ARITY_SPECIFIC_ARGS_HANDLE, 4, req, opt, rest == 0 ? false : true, keyrest)); - } - - private static final MethodHandle CHECK_ARITY_SPECIFIC_ARGS_HANDLE = - Binder - .from(void.class, ThreadContext.class, StaticScope.class, Object[].class, Block.class, int.class, int.class, boolean.class, int.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "checkAritySpecificArgs"); - - @JIT - public static void checkArity(ThreadContext context, StaticScope scope, Object[] args, Object keywords, Block block, int req, int opt, boolean rest, int keyrest) { - IRRuntimeHelpers.checkArity(context, scope, args, keywords, req, opt, rest, keyrest, block); - } - - @JIT - public static void checkAritySpecificArgs(ThreadContext context, StaticScope scope, Object[] args, Block block, int req, int opt, boolean rest, int keyrest) { - IRRuntimeHelpers.checkAritySpecificArgs(context, scope, args, req, opt, rest, keyrest, block); - } - - public static CallSite array(Lookup lookup, String name, MethodType type) { - MethodHandle handle = Binder - .from(type) - .collect(1, IRubyObject[].class) - .invoke(ARRAY_HANDLE); - - return new ConstantCallSite(handle); - } - - private static final MethodHandle ARRAY_HANDLE = - Binder - .from(RubyArray.class, ThreadContext.class, IRubyObject[].class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "array"); - - public static CallSite hash(Lookup lookup, String name, MethodType type) { - MethodHandle handle; - - int parameterCount = type.parameterCount(); - if (parameterCount == 1) { - handle = Binder - .from(lookup, type) - .cast(type.changeReturnType(RubyHash.class)) - .filter(0, RUNTIME_FROM_CONTEXT_HANDLE) - .invokeStaticQuiet(lookup, RubyHash.class, "newHash"); - } else if (!type.parameterType(parameterCount - 1).isArray() - && (parameterCount - 1) / 2 <= Helpers.MAX_SPECIFIC_ARITY_HASH) { - handle = Binder - .from(lookup, type) - .cast(type.changeReturnType(RubyHash.class)) - .filter(0, RUNTIME_FROM_CONTEXT_HANDLE) - .invokeStaticQuiet(lookup, Helpers.class, "constructSmallHash"); - } else { - handle = Binder - .from(lookup, type) - .collect(1, IRubyObject[].class) - .invoke(HASH_HANDLE); - } - - return new ConstantCallSite(handle); - } - - private static final MethodHandle HASH_HANDLE = - Binder - .from(RubyHash.class, ThreadContext.class, IRubyObject[].class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "hash"); - - public static CallSite kwargsHash(Lookup lookup, String name, MethodType type) { - MethodHandle handle = Binder - .from(lookup, type) - .collect(2, IRubyObject[].class) - .invoke(KWARGS_HASH_HANDLE); - - return new ConstantCallSite(handle); - } - - private static final MethodHandle KWARGS_HASH_HANDLE = - Binder - .from(RubyHash.class, ThreadContext.class, RubyHash.class, IRubyObject[].class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "kwargsHash"); - - public static Handle string() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "string", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, String.class, int.class), - false); - } - - public static Handle fstring() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "fstring", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, String.class, int.class, String.class, int.class), - false); - } - - public static Handle bytelist() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "bytelist", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, String.class), - false); - } - - public static Handle array() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "array", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static Handle hash() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "hash", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static Handle kwargsHash() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "kwargsHash", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static Handle invokeSuper() { - return SuperInvokeSite.BOOTSTRAP; - } - - public static Handle global() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "globalBootstrap", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, int.class), - false); - } - - public static RubyString string(ThreadContext context, ByteList value, int cr) { - return RubyString.newStringShared(context.runtime, value, cr); - } - - public static RubyString bufferString(ThreadContext context, Encoding encoding, int size, int cr) { - return RubyString.newString(context.runtime, new ByteList(size, encoding), cr); - } - - public static RubyString frozenString(ThreadContext context, MutableCallSite site, ByteList value, int cr, String file, int line) { - RubyString frozen = IRRuntimeHelpers.newFrozenString(context, value, cr, file, line); - - // Permanently bind to the new frozen string - site.setTarget(dropArguments(constant(RubyString.class, frozen), 0, ThreadContext.class)); - - return frozen; - } - - public static RubyArray array(ThreadContext context, IRubyObject[] ary) { - assert ary.length > RubyArraySpecialized.MAX_PACKED_SIZE; - // Bootstrap.array() only dispatches here if ^^ holds - return RubyArray.newArrayNoCopy(context.runtime, ary); - } - - public static Handle contextValue() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "contextValue", - sig(CallSite.class, Lookup.class, String.class, MethodType.class), - false); - } - - public static Handle contextValueString() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "contextValueString", - sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class), - false); - } - - public static CallSite contextValue(Lookup lookup, String name, MethodType type) { - MutableCallSite site = new MutableCallSite(type); - - MethodHandle dmh; - switch (name) { - case "runtime": - dmh = RUNTIME_HANDLE; - break; - case "nil": - dmh = NIL_HANDLE; - break; - case "True": - dmh = TRUE_HANDLE; - break; - case "False": - dmh = FALSE_HANDLE; - break; - case "rubyEncoding": - dmh = RUBY_ENCODING_HANDLE; - break; - case "encoding": - dmh = ENCODING_HANDLE; - break; - default: - throw new RuntimeException("BUG: invalid context value " + name); - } - - site.setTarget(Binder.from(type).append(site).invoke(dmh)); - - return site; - } - - public static CallSite contextValueString(Lookup lookup, String name, MethodType type, String str) { - MutableCallSite site = new MutableCallSite(type); - - MethodHandle dmh; - switch (name) { - case "rubyEncoding": - dmh = RUBY_ENCODING_HANDLE; - break; - case "encoding": - dmh = ENCODING_HANDLE; - break; - default: - throw new RuntimeException("BUG: invalid context value " + name); - } - - site.setTarget(Binder.from(type).append(site, str).invoke(dmh)); - return site; - } - - private static final MethodHandle RUNTIME_HANDLE = - Binder - .from(Ruby.class, ThreadContext.class, MutableCallSite.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "runtime"); - - // We use LOOKUP here to have a full-featured MethodHandles.Lookup, avoiding jruby/jruby#7911 - private static final MethodHandle RUNTIME_FROM_CONTEXT_HANDLE = - Binder - .from(LOOKUP, Ruby.class, ThreadContext.class) - .getFieldQuiet("runtime"); - - private static final MethodHandle NIL_HANDLE = - Binder - .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "nil"); - - private static final MethodHandle TRUE_HANDLE = - Binder - .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "True"); - - private static final MethodHandle FALSE_HANDLE = - Binder - .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "False"); - - private static final MethodHandle RUBY_ENCODING_HANDLE = - Binder - .from(RubyEncoding.class, ThreadContext.class, MutableCallSite.class, String.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "rubyEncoding"); - - private static final MethodHandle ENCODING_HANDLE = - Binder - .from(Encoding.class, ThreadContext.class, MutableCallSite.class, String.class) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "encoding"); - - public static IRubyObject nil(ThreadContext context, MutableCallSite site) { - RubyNil nil = (RubyNil) context.nil; - - MethodHandle constant = (MethodHandle) nil.constant(); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(IRubyObject.class, context.nil); - - site.setTarget(constant); - - return nil; - } - - public static IRubyObject True(ThreadContext context, MutableCallSite site) { - MethodHandle constant = (MethodHandle)context.tru.constant(); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(IRubyObject.class, context.tru); - - site.setTarget(constant); - - return context.tru; - } - - public static IRubyObject False(ThreadContext context, MutableCallSite site) { - MethodHandle constant = (MethodHandle)context.fals.constant(); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(IRubyObject.class, context.fals); - - site.setTarget(constant); - - return context.fals; - } - - public static Ruby runtime(ThreadContext context, MutableCallSite site) { - MethodHandle constant = (MethodHandle)context.runtime.constant(); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(Ruby.class, context.runtime); - - site.setTarget(constant); - - return context.runtime; - } - - public static RubyEncoding rubyEncoding(ThreadContext context, MutableCallSite site, String name) { - RubyEncoding rubyEncoding = IRRuntimeHelpers.retrieveEncoding(context, name); - - MethodHandle constant = (MethodHandle)rubyEncoding.constant(); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(RubyEncoding.class, rubyEncoding); - - site.setTarget(constant); - - return rubyEncoding; - } - - public static Encoding encoding(ThreadContext context, MutableCallSite site, String name) { - Encoding encoding = IRRuntimeHelpers.retrieveJCodingsEncoding(context, name); - - MethodHandle constant = MethodHandles.constant(Encoding.class, encoding); - if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(Encoding.class, encoding); - - site.setTarget(constant); - - return encoding; - } - - public static RubyHash hash(ThreadContext context, IRubyObject[] pairs) { - Ruby runtime = context.runtime; - RubyHash hash = new RubyHash(runtime, pairs.length / 2 + 1); - for (int i = 0; i < pairs.length;) { - hash.fastASetCheckString(runtime, pairs[i++], pairs[i++]); - } - return hash; - } - - public static RubyHash kwargsHash(ThreadContext context, RubyHash hash, IRubyObject[] pairs) { - return IRRuntimeHelpers.dupKwargsHashAndPopulateFromArray(context, hash, pairs); - } - - static MethodHandle buildIndyHandle(InvokeSite site, CacheEntry entry) { - MethodHandle mh = null; - Signature siteToDyncall = site.signature.insertArgs(site.argOffset, arrayOf("class", "name"), arrayOf(RubyModule.class, String.class)); - DynamicMethod method = entry.method; - - if (method instanceof HandleMethod) { - HandleMethod handleMethod = (HandleMethod)method; - boolean blockGiven = site.signature.lastArgType() == Block.class; - - if (site.arity >= 0) { - mh = handleMethod.getHandle(site.arity); - if (mh != null) { - if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); - if (!site.functional) mh = dropArguments(mh, 1, IRubyObject.class); - } else { - mh = handleMethod.getHandle(-1); - if (!site.functional) mh = dropArguments(mh, 1, IRubyObject.class); - if (site.arity == 0) { - if (!blockGiven) { - mh = insertArguments(mh, mh.type().parameterCount() - 2, IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); - } else { - mh = insertArguments(mh, mh.type().parameterCount() - 2, (Object)IRubyObject.NULL_ARRAY); - } - } else { - // bundle up varargs - if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); - - mh = SmartBinder.from(lookup(), siteToDyncall) - .collect("args", "arg.*", Helpers.constructObjectArrayHandle(site.arity)) - .invoke(mh) - .handle(); - } - } - } else { - mh = handleMethod.getHandle(-1); - if (mh != null) { - if (!site.functional) mh = dropArguments(mh, 1, IRubyObject.class); - if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); - - mh = SmartBinder.from(lookup(), siteToDyncall) - .invoke(mh) - .handle(); - } - } - - if (mh != null) { - mh = insertArguments(mh, site.argOffset, entry.sourceModule, site.name()); - - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tbound directly to handle " + Bootstrap.logMethod(method)); - } - } - } - - return mh; - } - - static MethodHandle buildGenericHandle(InvokeSite site, CacheEntry entry) { - SmartBinder binder; - DynamicMethod method = entry.method; - - binder = SmartBinder.from(site.signature); - - binder = permuteForGenericCall(binder, method, GENERIC_CALL_PERMUTE); - - binder = binder - .insert(2, new String[]{"rubyClass", "name"}, new Class[]{RubyModule.class, String.class}, entry.sourceModule, site.name()) - .insert(0, "method", DynamicMethod.class, method); - - if (site.arity > 3) { - binder = binder.collect("args", "arg.*", constructObjectArrayHandle(site.arity)); - } - - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tbound indirectly " + method + ", " + Bootstrap.logMethod(method)); - } - - return binder.invokeVirtualQuiet(LOOKUP, "call").handle(); - } - - private static SmartBinder permuteForGenericCall(SmartBinder binder, DynamicMethod method, String... basePermutes) { - if (methodWantsBlock(method)) { - binder = binder.permute(arrayOf(basePermutes, "block", String[]::new)); - } else { - binder = binder.permute(basePermutes); - } - return binder; - } - - private static boolean methodWantsBlock(DynamicMethod method) { - // only include block if native signature receives block, whatever its arity - boolean wantsBlock = true; - if (method instanceof NativeCallMethod) { - DynamicMethod.NativeCall nativeCall = ((NativeCallMethod) method).getNativeCall(); - // if it is a non-JI native call and does not want block, drop it - // JI calls may lazily convert blocks to an interface type (jruby/jruby#7246) - if (nativeCall != null && !nativeCall.isJava()) { - Class[] nativeSignature = nativeCall.getNativeSignature(); - - // no args or last arg not a block, do no pass block - if (nativeSignature.length == 0 || nativeSignature[nativeSignature.length - 1] != Block.class) { - wantsBlock = false; - } - } - } - return wantsBlock; - } - - static MethodHandle buildMethodMissingHandle(InvokeSite site, CacheEntry entry, IRubyObject self) { - SmartBinder binder; - DynamicMethod method = entry.method; - - if (site.arity >= 0) { - binder = SmartBinder.from(site.signature); - - binder = permuteForGenericCall(binder, method, GENERIC_CALL_PERMUTE) - .insert(2, - new String[]{"rubyClass", "name", "argName"} - , new Class[]{RubyModule.class, String.class, IRubyObject.class}, - entry.sourceModule, - site.name(), - self.getRuntime().newSymbol(site.methodName)) - .insert(0, "method", DynamicMethod.class, method) - .collect("args", "arg.*", Helpers.constructObjectArrayHandle(site.arity + 1)); - } else { - SmartHandle fold = SmartBinder.from( - site.signature - .permute("context", "self", "args", "block") - .changeReturn(IRubyObject[].class)) - .permute("args") - .insert(0, "argName", IRubyObject.class, self.getRuntime().newSymbol(site.methodName)) - .invokeStaticQuiet(LOOKUP, Helpers.class, "arrayOf"); - - binder = SmartBinder.from(site.signature); - - binder = permuteForGenericCall(binder, method, "context", "self", "args") - .fold("args2", fold); - binder = permuteForGenericCall(binder, method, "context", "self", "args2") - .insert(2, - new String[]{"rubyClass", "name"} - , new Class[]{RubyModule.class, String.class}, - entry.sourceModule, - site.name()) - .insert(0, "method", DynamicMethod.class, method); - } - - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tbound to method_missing for " + method + ", " + Bootstrap.logMethod(method)); - } - - return binder.invokeVirtualQuiet(LOOKUP, "call").handle(); - } - - static MethodHandle buildAttrHandle(InvokeSite site, CacheEntry entry, IRubyObject self) { - DynamicMethod method = entry.method; - - if (method instanceof AttrReaderMethod && site.arity == 0) { - AttrReaderMethod attrReader = (AttrReaderMethod) method; - String varName = attrReader.getVariableName(); - - // we getVariableAccessorForWrite here so it is eagerly created and we don't cache the DUMMY - VariableAccessor accessor = self.getType().getVariableAccessorForWrite(varName); - - // Ruby to attr reader - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - if (accessor instanceof FieldVariableAccessor) { - LOG.info(site.name() + "\tbound as field attr reader " + logMethod(method) + ":" + ((AttrReaderMethod)method).getVariableName()); - } else { - LOG.info(site.name() + "\tbound as attr reader " + logMethod(method) + ":" + ((AttrReaderMethod)method).getVariableName()); - } - } - - return createAttrReaderHandle(site, self, self.getType(), accessor); - } else if (method instanceof AttrWriterMethod && site.arity == 1) { - AttrWriterMethod attrReader = (AttrWriterMethod)method; - String varName = attrReader.getVariableName(); - - // we getVariableAccessorForWrite here so it is eagerly created and we don't cache the DUMMY - VariableAccessor accessor = self.getType().getVariableAccessorForWrite(varName); - - // Ruby to attr reader - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - if (accessor instanceof FieldVariableAccessor) { - LOG.info(site.name() + "\tbound as field attr writer " + logMethod(method) + ":" + ((AttrWriterMethod) method).getVariableName()); - } else { - LOG.info(site.name() + "\tbound as attr writer " + logMethod(method) + ":" + ((AttrWriterMethod) method).getVariableName()); - } - } - - return createAttrWriterHandle(site, self, self.getType(), accessor); - } - - return null; - } - - private static MethodHandle createAttrReaderHandle(InvokeSite site, IRubyObject self, RubyClass cls, VariableAccessor accessor) { - MethodHandle nativeTarget; - - MethodHandle filter = cls.getClassRuntime().getNullToNilHandle(); - - MethodHandle getValue; - - if (accessor instanceof FieldVariableAccessor) { - MethodHandle getter = ((FieldVariableAccessor)accessor).getGetter(); - getValue = Binder.from(site.type()) - .drop(0, site.argOffset - 1) - .filterReturn(filter) - .cast(methodType(Object.class, self.getClass())) - .invoke(getter); - } else { - getValue = Binder.from(site.type()) - .drop(0, site.argOffset - 1) - .filterReturn(filter) - .cast(methodType(Object.class, Object.class)) - .prepend(accessor) - .invokeVirtualQuiet(LOOKUP, "get"); - } - - // NOTE: Must not cache the fully-bound handle in the method, since it's specific to this class - - return getValue; - } - - public static IRubyObject valueOrNil(IRubyObject value, IRubyObject nil) { - return value == null ? nil : value; - } - - private static MethodHandle createAttrWriterHandle(InvokeSite site, IRubyObject self, RubyClass cls, VariableAccessor accessor) { - MethodHandle nativeTarget; - - MethodHandle filter = Binder - .from(IRubyObject.class, Object.class) - .drop(0) - .constant(cls.getRuntime().getNil()); - - MethodHandle setValue; - - if (accessor instanceof FieldVariableAccessor) { - MethodHandle setter = ((FieldVariableAccessor)accessor).getSetter(); - setValue = Binder.from(site.type()) - .drop(0, site.argOffset - 1) - .filterReturn(filter) - .cast(methodType(void.class, self.getClass(), Object.class)) - .invoke(setter); - } else { - setValue = Binder.from(site.type()) - .drop(0, site.argOffset - 1) - .filterReturn(filter) - .cast(methodType(void.class, Object.class, Object.class)) - .prepend(accessor) - .invokeVirtualQuiet(LOOKUP, "set"); - } - - return setValue; - } - - static MethodHandle buildJittedHandle(InvokeSite site, CacheEntry entry, boolean blockGiven) { - MethodHandle mh = null; - SmartBinder binder; - CompiledIRMethod compiledIRMethod = null; - DynamicMethod method = entry.method; - RubyModule sourceModule = entry.sourceModule; - - if (method instanceof CompiledIRMethod) { - compiledIRMethod = (CompiledIRMethod)method; - } else if (method instanceof MixedModeIRMethod) { - DynamicMethod actualMethod = ((MixedModeIRMethod)method).getActualMethod(); - if (actualMethod instanceof CompiledIRMethod) { - compiledIRMethod = (CompiledIRMethod) actualMethod; - } else { - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tfailed direct binding due to unjitted method " + Bootstrap.logMethod(method)); - } - } - } - - if (compiledIRMethod != null) { - - // attempt IR direct binding - // TODO: this will have to expand when we start specializing arities - - binder = SmartBinder.from(site.signature) - .permute("context", "self", "arg.*", "block"); - - if (site.arity == -1) { - // already [], nothing to do - mh = (MethodHandle)compiledIRMethod.getHandle(); - } else if (site.arity == 0) { - MethodHandle specific; - if ((specific = compiledIRMethod.getHandleFor(site.arity)) != null) { - mh = specific; - } else { - mh = (MethodHandle)compiledIRMethod.getHandle(); - binder = binder.insert(2, "args", IRubyObject.NULL_ARRAY); - } - } else { - MethodHandle specific; - if ((specific = compiledIRMethod.getHandleFor(site.arity)) != null) { - mh = specific; - } else { - mh = (MethodHandle) compiledIRMethod.getHandle(); - binder = binder.collect("args", "arg.*", Helpers.constructObjectArrayHandle(site.arity)); - } - } - - if (!blockGiven) { - binder = binder.append("block", Block.class, Block.NULL_BLOCK); - } - - binder = binder - .insert(1, "scope", StaticScope.class, compiledIRMethod.getStaticScope()) - .append("class", RubyModule.class, sourceModule) - .append("frameName", String.class, site.name()); - - mh = binder.invoke(mh).handle(); - - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tbound directly to jitted method " + Bootstrap.logMethod(method)); - } - } - - return mh; - } - - static MethodHandle buildNativeHandle(InvokeSite site, CacheEntry entry, boolean blockGiven) { - MethodHandle mh = null; - SmartBinder binder = null; - DynamicMethod method = entry.method; - - if (method instanceof NativeCallMethod && ((NativeCallMethod) method).getNativeCall() != null) { - NativeCallMethod nativeMethod = (NativeCallMethod)method; - DynamicMethod.NativeCall nativeCall = nativeMethod.getNativeCall(); - - DynamicMethod.NativeCall nc = nativeCall; - - if (nc.isJava()) { - return createJavaHandle(site, method); - } else { - int nativeArgCount = getNativeArgCount(method, nativeCall); - - if (nativeArgCount >= 0) { // native methods only support arity 3 - if (nativeArgCount == site.arity) { - // nothing to do - binder = SmartBinder.from(lookup(), site.signature); - } else { - // arity mismatch...leave null and use DynamicMethod.call below - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tdid not match the primary arity for a native method " + Bootstrap.logMethod(method)); - } - } - } else { - // varargs - if (site.arity == -1) { - // ok, already passing [] - binder = SmartBinder.from(lookup(), site.signature); - } else if (site.arity == 0) { - // no args, insert dummy - binder = SmartBinder.from(lookup(), site.signature) - .insert(site.argOffset, "args", IRubyObject.NULL_ARRAY); - } else { - // 1 or more args, collect into [] - binder = SmartBinder.from(lookup(), site.signature) - .collect("args", "arg.*", Helpers.constructObjectArrayHandle(site.arity)); - } - } - - if (binder != null) { - - // clean up non-arguments, ordering, types - if (!nc.hasContext()) { - binder = binder.drop("context"); - } - - if (nc.hasBlock() && !blockGiven) { - binder = binder.append("block", Block.NULL_BLOCK); - } else if (!nc.hasBlock() && blockGiven) { - binder = binder.drop("block"); - } - - if (nc.isStatic()) { - mh = binder - .permute("context", "self", "arg.*", "block") // filter caller - .cast(nc.getNativeReturn(), nc.getNativeSignature()) - .invokeStaticQuiet(LOOKUP, nc.getNativeTarget(), nc.getNativeName()) - .handle(); - } else { - mh = binder - .permute("self", "context", "arg.*", "block") // filter caller, move self - .castArg("self", nc.getNativeTarget()) - .castVirtual(nc.getNativeReturn(), nc.getNativeTarget(), nc.getNativeSignature()) - .invokeVirtualQuiet(LOOKUP, nc.getNativeName()) - .handle(); - } - - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tbound directly to JVM method " + Bootstrap.logMethod(method)); - } - - JRubyMethod anno = nativeCall.getMethod().getAnnotation(JRubyMethod.class); - if (anno != null && anno.frame()) { - mh = InvocationLinker.wrapWithFrameOnly(site.signature, entry.sourceModule, site.name(), mh); - } - } - } - } - - return mh; - } - - public static int getNativeArgCount(DynamicMethod method, DynamicMethod.NativeCall nativeCall) { - // if non-Java, must: - // * exactly match arities or both are [] boxed - // * 3 or fewer arguments - return getArgCount(nativeCall.getNativeSignature(), nativeCall.isStatic()); - } - - private static int getArgCount(Class[] args, boolean isStatic) { - int length = args.length; - boolean hasContext = false; - if (isStatic) { - if (args.length > 1 && args[0] == ThreadContext.class) { - length--; - hasContext = true; - } - - // remove self object - assert args.length >= 1; - length--; - - if (args.length > 1 && args[args.length - 1] == Block.class) { - length--; - } - - if (length == 1) { - if (hasContext && args[2] == IRubyObject[].class) { - length = -1; - } else if (args[1] == IRubyObject[].class) { - length = -1; - } - } - } else { - if (args.length > 0 && args[0] == ThreadContext.class) { - length--; - hasContext = true; - } - - if (args.length > 0 && args[args.length - 1] == Block.class) { - length--; - } - - if (length == 1) { - if (hasContext && args[1] == IRubyObject[].class) { - length = -1; - } else if (args[0] == IRubyObject[].class) { - length = -1; - } - } - } - return length; - } - - public static boolean testType(RubyClass original, IRubyObject self) { - // naive test - return original == RubyBasicObject.getMetaClass(self); - } - - - //////////////////////////////////////////////////////////////////////////// - // Dispatch from Ruby to Java via Java integration - //////////////////////////////////////////////////////////////////////////// - - private static MethodHandle createJavaHandle(InvokeSite site, DynamicMethod method) { - if (!(method instanceof NativeCallMethod)) return null; - - DynamicMethod.NativeCall nativeCall = ((NativeCallMethod) method).getNativeCall(); - - if (nativeCall == null) return null; - - boolean isStatic = nativeCall.isStatic(); - - // This logic does not handle closure conversion yet - if (site.signature.lastArgType() == Block.class) { - if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { - LOG.info(site.name() + "\tpassed a closure to Java method " + nativeCall + ": " + Bootstrap.logMethod(method)); - } - return null; - } - - // mismatched arity not supported - if (site.arity != nativeCall.getNativeSignature().length) { - return null; - } - - // varargs broken, so ignore methods that take a trailing array - Class[] signature = nativeCall.getNativeSignature(); - if (signature.length > 0 && signature[signature.length - 1].isArray()) { - return null; - } - - // Scala singletons have slightly different JI logic, so skip for now - if (method instanceof SingletonMethodInvoker) return null; - - // get prepared handle if previously cached - MethodHandle nativeTarget = (MethodHandle)method.getHandle(); - - if (nativeTarget == null) { - // no cache, proceed to construct the adapted handle - - // the "apparent" type from the NativeCall, excluding receiver - MethodType apparentType = methodType(nativeCall.getNativeReturn(), nativeCall.getNativeSignature()); - - if (isStatic) { - nativeTarget = findStatic(nativeCall.getNativeTarget(), nativeCall.getNativeName(), apparentType); - } else { - nativeTarget = findVirtual(nativeCall.getNativeTarget(), nativeCall.getNativeName(), apparentType); - } - - // the actual native type with receiver - MethodType nativeType = nativeTarget.type(); - Class[] nativeParams = nativeType.parameterArray(); - Class nativeReturn = nativeType.returnType(); - - // convert arguments - MethodHandle[] argConverters = new MethodHandle[nativeType.parameterCount()]; - for (int i = 0; i < argConverters.length; i++) { - MethodHandle converter; - if (!isStatic && i == 0) { - // handle non-static receiver specially - converter = Binder - .from(nativeParams[0], IRubyObject.class) - .cast(Object.class, IRubyObject.class) - .invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy"); - } else { - // all other arguments use toJava - converter = Binder - .from(nativeParams[i], IRubyObject.class) - .insert(1, nativeParams[i]) - .cast(Object.class, IRubyObject.class, Class.class) - .invokeVirtualQuiet(lookup(), "toJava"); - } - argConverters[i] = converter; - } - nativeTarget = filterArguments(nativeTarget, 0, argConverters); - - Ruby runtime = method.getImplementationClass().getRuntime(); - Class[] convertedParams = CodegenUtils.params(IRubyObject.class, nativeTarget.type().parameterCount()); - - // handle return value - MethodHandle returnFilter; - if (nativeReturn == byte.class - || nativeReturn == short.class - || nativeReturn == char.class - || nativeReturn == int.class - || nativeReturn == long.class) { - // native integral type, produce a Fixnum - nativeTarget = explicitCastArguments(nativeTarget, methodType(long.class, convertedParams)); - returnFilter = insertArguments( - findStatic(RubyFixnum.class, "newFixnum", methodType(RubyFixnum.class, Ruby.class, long.class)), - 0, - runtime); - } else if (nativeReturn == Byte.class - || nativeReturn == Short.class - || nativeReturn == Character.class - || nativeReturn == Integer.class - || nativeReturn == Long.class) { - // boxed integral type, produce a Fixnum or nil - returnFilter = insertArguments( - findStatic(Bootstrap.class, "fixnumOrNil", methodType(IRubyObject.class, Ruby.class, nativeReturn)), - 0, - runtime); - } else if (nativeReturn == float.class - || nativeReturn == double.class) { - // native decimal type, produce a Float - nativeTarget = explicitCastArguments(nativeTarget, methodType(double.class, convertedParams)); - returnFilter = insertArguments( - findStatic(RubyFloat.class, "newFloat", methodType(RubyFloat.class, Ruby.class, double.class)), - 0, - runtime); - } else if (nativeReturn == Float.class - || nativeReturn == Double.class) { - // boxed decimal type, produce a Float or nil - returnFilter = insertArguments( - findStatic(Bootstrap.class, "floatOrNil", methodType(IRubyObject.class, Ruby.class, nativeReturn)), - 0, - runtime); - } else if (nativeReturn == boolean.class) { - // native boolean type, produce a Boolean - nativeTarget = explicitCastArguments(nativeTarget, methodType(boolean.class, convertedParams)); - returnFilter = insertArguments( - findStatic(RubyBoolean.class, "newBoolean", methodType(RubyBoolean.class, Ruby.class, boolean.class)), - 0, - runtime); - } else if (nativeReturn == Boolean.class) { - // boxed boolean type, produce a Boolean or nil - returnFilter = insertArguments( - findStatic(Bootstrap.class, "booleanOrNil", methodType(IRubyObject.class, Ruby.class, Boolean.class)), - 0, - runtime); - } else if (CharSequence.class.isAssignableFrom(nativeReturn)) { - // character sequence, produce a String or nil - nativeTarget = explicitCastArguments(nativeTarget, methodType(CharSequence.class, convertedParams)); - returnFilter = insertArguments( - findStatic(Bootstrap.class, "stringOrNil", methodType(IRubyObject.class, Ruby.class, CharSequence.class)), - 0, - runtime); - } else if (nativeReturn == void.class) { - // void return, produce nil - returnFilter = constant(IRubyObject.class, runtime.getNil()); - } else if (nativeReturn == ByteList.class) { - // bytelist, produce a String or nil - nativeTarget = explicitCastArguments(nativeTarget, methodType(ByteList.class, convertedParams)); - returnFilter = insertArguments( - findStatic(Bootstrap.class, "stringOrNil", methodType(IRubyObject.class, Ruby.class, ByteList.class)), - 0, - runtime); - } else if (nativeReturn == BigInteger.class) { - // bytelist, produce a String or nil - nativeTarget = explicitCastArguments(nativeTarget, methodType(BigInteger.class, convertedParams)); - returnFilter = insertArguments( - findStatic(Bootstrap.class, "bignumOrNil", methodType(IRubyObject.class, Ruby.class, BigInteger.class)), - 0, - runtime); - } else { - // all other object types - nativeTarget = explicitCastArguments(nativeTarget, methodType(Object.class, convertedParams)); - returnFilter = insertArguments( - findStatic(JavaUtil.class, "convertJavaToUsableRubyObject", methodType(IRubyObject.class, Ruby.class, Object.class)), - 0, - runtime); - } - - nativeTarget = MethodHandles.filterReturnValue(nativeTarget, returnFilter); - - // cache adapted handle - method.setHandle(nativeTarget); - } - - // perform final adaptation by dropping JRuby-specific arguments - int dropCount; - if (isStatic) { - if (site.functional) { - dropCount = 2; // context, self - } else { - dropCount = 3; // context, caller, self - } - } else { - if (site.functional) { - dropCount = 1; // context - } else { - dropCount = 2; // context, caller - } - } - - return Binder - .from(site.type()) - .drop(0, dropCount) - .invoke(nativeTarget); - } - - public static boolean subclassProxyTest(Object target) { - return target instanceof ReifiedJavaProxy; - } - - private static final MethodHandle IS_JAVA_SUBCLASS = findStatic(Bootstrap.class, "subclassProxyTest", methodType(boolean.class, Object.class)); - - private static Object nullValue(Class type) { - if (type == boolean.class || type == Boolean.class) return false; - if (type == byte.class || type == Byte.class) return (byte)0; - if (type == short.class || type == Short.class) return (short)0; - if (type == int.class || type == Integer.class) return 0; - if (type == long.class || type == Long.class) return 0L; - if (type == float.class || type == Float.class) return 0.0F; - if (type == double.class || type == Double.class)return 0.0; - return null; - } - - public static IRubyObject fixnumOrNil(Ruby runtime, Byte b) { - return b == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, b); - } - - public static IRubyObject fixnumOrNil(Ruby runtime, Short s) { - return s == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, s); - } - - public static IRubyObject fixnumOrNil(Ruby runtime, Character c) { - return c == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, c); - } - - public static IRubyObject fixnumOrNil(Ruby runtime, Integer i) { - return i == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, i); - } - - public static IRubyObject fixnumOrNil(Ruby runtime, Long l) { - return l == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, l); - } - - public static IRubyObject floatOrNil(Ruby runtime, Float f) { - return f == null ? runtime.getNil() : RubyFloat.newFloat(runtime, f); - } - - public static IRubyObject floatOrNil(Ruby runtime, Double d) { - return d == null ? runtime.getNil() : RubyFloat.newFloat(runtime, d); - } - - public static IRubyObject booleanOrNil(Ruby runtime, Boolean b) { - return b == null ? runtime.getNil() : RubyBoolean.newBoolean(runtime, b); - } - - public static IRubyObject stringOrNil(Ruby runtime, CharSequence cs) { - return cs == null ? runtime.getNil() : RubyString.newUnicodeString(runtime, cs); - } - - public static IRubyObject stringOrNil(Ruby runtime, ByteList bl) { - return bl == null ? runtime.getNil() : RubyString.newString(runtime, bl); - } - - public static IRubyObject bignumOrNil(Ruby runtime, BigInteger bi) { - return bi == null ? runtime.getNil() : RubyBignum.newBignum(runtime, bi); - } - - /////////////////////////////////////////////////////////////////////////// - // Fixnum binding - - public static boolean testModuleMatch(ThreadContext context, IRubyObject arg0, int id) { - return arg0 instanceof RubyModule && ((RubyModule)arg0).id == id; - } - - public static Handle getFixnumOperatorHandle() { - return getBootstrapHandle("fixnumOperatorBootstrap", MathLinker.class, BOOTSTRAP_LONG_STRING_INT_SIG); - } - - public static Handle getFloatOperatorHandle() { - return getBootstrapHandle("floatOperatorBootstrap", MathLinker.class, BOOTSTRAP_DOUBLE_STRING_INT_SIG); - } - - public static Handle checkpointHandle() { - return getBootstrapHandle("checkpointBootstrap", BOOTSTRAP_BARE_SIG); - } - - public static Handle callInfoHandle() { - return getBootstrapHandle("callInfoBootstrap", BOOTSTRAP_INT_SIG); - } - - public static Handle coverLineHandle() { - return getBootstrapHandle("coverLineBootstrap", sig(CallSite.class, Lookup.class, String.class, MethodType.class, String.class, int.class, int.class)); - } - - public static Handle getHeapLocalHandle() { - return getBootstrapHandle("getHeapLocalBootstrap", BOOTSTRAP_INT_INT_SIG); + static String logMethod(DynamicMethod method) { + return "[#" + method.getSerialNumber() + " " + method.getImplementationClass().getMethodLocation() + "]"; } - public static Handle getHeapLocalOrNilHandle() { - return getBootstrapHandle("getHeapLocalOrNilBootstrap", BOOTSTRAP_INT_INT_SIG); + static String logBlock(Block block) { + return "[" + block.getBody().getFile() + ":" + block.getBody().getLine() + "]"; } public static Handle getBootstrapHandle(String name, String sig) { @@ -1491,286 +73,4 @@ public static Handle getBootstrapHandle(String name, Class type, String sig) { sig, false); } - - public static CallSite checkpointBootstrap(Lookup lookup, String name, MethodType type) throws Throwable { - MutableCallSite site = new MutableCallSite(type); - MethodHandle handle = lookup.findStatic(Bootstrap.class, "checkpointFallback", methodType(void.class, MutableCallSite.class, ThreadContext.class)); - - handle = handle.bindTo(site); - site.setTarget(handle); - - return site; - } - - public static void checkpointFallback(MutableCallSite site, ThreadContext context) throws Throwable { - Ruby runtime = context.runtime; - Invalidator invalidator = runtime.getCheckpointInvalidator(); - - MethodHandle target = Binder - .from(void.class, ThreadContext.class) - .nop(); - MethodHandle fallback = lookup().findStatic(Bootstrap.class, "checkpointFallback", methodType(void.class, MutableCallSite.class, ThreadContext.class)); - fallback = fallback.bindTo(site); - - target = ((SwitchPoint)invalidator.getData()).guardWithTest(target, fallback); - - site.setTarget(target); - - // poll for events once since we've ended up back in fallback - context.pollThreadEvents(); - } - - public static CallSite callInfoBootstrap(Lookup lookup, String name, MethodType type, int callInfo) throws Throwable { - MethodHandle handle; - if (callInfo == 0) { - handle = lookup.findVirtual(ThreadContext.class, "clearCallInfo", methodType(void.class)); - } else { - handle = lookup.findStatic(IRRuntimeHelpers.class, "setCallInfo", methodType(void.class, ThreadContext.class, int.class)); - handle = insertArguments(handle, 1, callInfo); - } - - return new ConstantCallSite(handle); - } - - public static CallSite coverLineBootstrap(Lookup lookup, String name, MethodType type, String filename, int line, int oneshot) throws Throwable { - MutableCallSite site = new MutableCallSite(type); - MethodHandle handle = lookup.findStatic(Bootstrap.class, "coverLineFallback", methodType(void.class, MutableCallSite.class, ThreadContext.class, String.class, int.class, boolean.class)); - - handle = handle.bindTo(site); - handle = insertArguments(handle, 1, filename, line, oneshot != 0); - site.setTarget(handle); - - return site; - } - - public static void coverLineFallback(MutableCallSite site, ThreadContext context, String filename, int line, boolean oneshot) throws Throwable { - IRRuntimeHelpers.updateCoverage(context, filename, line); - - if (oneshot) site.setTarget(Binder.from(void.class, ThreadContext.class).dropAll().nop()); - } - - public static CallSite getHeapLocalBootstrap(Lookup lookup, String name, MethodType type, int depth, int location) throws Throwable { - // no null checking needed for method bodies - MethodHandle getter; - Binder binder = Binder - .from(type); - - if (depth == 0) { - if (location < DynamicScopeGenerator.SPECIALIZED_GETS.size()) { - getter = binder.invokeVirtualQuiet(LOOKUP, DynamicScopeGenerator.SPECIALIZED_GETS.get(location)); - } else { - getter = binder - .insert(1, location) - .invokeVirtualQuiet(LOOKUP, "getValueDepthZero"); - } - } else { - getter = binder - .insert(1, arrayOf(int.class, int.class), location, depth) - .invokeVirtualQuiet(LOOKUP, "getValue"); - } - - ConstantCallSite site = new ConstantCallSite(getter); - - return site; - } - - public static CallSite getHeapLocalOrNilBootstrap(Lookup lookup, String name, MethodType type, int depth, int location) throws Throwable { - MethodHandle getter; - Binder binder = Binder - .from(type) - .filter(1, contextValue(lookup, "nil", methodType(IRubyObject.class, ThreadContext.class)).dynamicInvoker()); - - if (depth == 0) { - if (location < DynamicScopeGenerator.SPECIALIZED_GETS_OR_NIL.size()) { - getter = binder.invokeVirtualQuiet(LOOKUP, DynamicScopeGenerator.SPECIALIZED_GETS_OR_NIL.get(location)); - } else { - getter = binder - .insert(1, location) - .invokeVirtualQuiet(LOOKUP, "getValueDepthZeroOrNil"); - } - } else { - getter = binder - .insert(1, arrayOf(int.class, int.class), location, depth) - .invokeVirtualQuiet(LOOKUP, "getValueOrNil"); - } - - ConstantCallSite site = new ConstantCallSite(getter); - - return site; - } - - public static CallSite globalBootstrap(Lookup lookup, String name, MethodType type, String file, int line) throws Throwable { - String[] names = name.split(":"); - String operation = names[0]; - String varName = JavaNameMangler.demangleMethodName(names[1]); - GlobalSite site = new GlobalSite(type, varName, file, line); - MethodHandle handle; - - if (operation.equals("get")) { - handle = lookup.findStatic(Bootstrap.class, "getGlobalFallback", methodType(IRubyObject.class, GlobalSite.class, ThreadContext.class)); - } else { - handle = lookup.findStatic(Bootstrap.class, "setGlobalFallback", methodType(void.class, GlobalSite.class, IRubyObject.class, ThreadContext.class)); - } - - handle = handle.bindTo(site); - site.setTarget(handle); - - return site; - } - - public static IRubyObject getGlobalFallback(GlobalSite site, ThreadContext context) throws Throwable { - Ruby runtime = context.runtime; - GlobalVariable variable = runtime.getGlobalVariables().getVariable(site.name()); - - if (site.failures() > Options.INVOKEDYNAMIC_GLOBAL_MAXFAIL.load() || - variable.getScope() != GlobalVariable.Scope.GLOBAL || - RubyGlobal.UNCACHED_GLOBALS.contains(site.name())) { - - // use uncached logic forever - if (Options.INVOKEDYNAMIC_LOG_GLOBALS.load()) LOG.info("global " + site.name() + " (" + site.file() + ":" + site.line() + ") uncacheable or rebound > " + Options.INVOKEDYNAMIC_GLOBAL_MAXFAIL.load() + " times, reverting to simple lookup"); - - MethodHandle uncached = lookup().findStatic(Bootstrap.class, "getGlobalUncached", methodType(IRubyObject.class, GlobalVariable.class)); - uncached = uncached.bindTo(variable); - uncached = dropArguments(uncached, 0, ThreadContext.class); - site.setTarget(uncached); - return (IRubyObject)uncached.invokeWithArguments(context); - } - - Invalidator invalidator = variable.getInvalidator(); - IRubyObject value = variable.getAccessor().getValue(); - - MethodHandle target = constant(IRubyObject.class, value); - target = dropArguments(target, 0, ThreadContext.class); - MethodHandle fallback = lookup().findStatic(Bootstrap.class, "getGlobalFallback", methodType(IRubyObject.class, GlobalSite.class, ThreadContext.class)); - fallback = fallback.bindTo(site); - - target = ((SwitchPoint)invalidator.getData()).guardWithTest(target, fallback); - - site.setTarget(target); - -// if (Options.INVOKEDYNAMIC_LOG_GLOBALS.load()) LOG.info("global " + site.name() + " (" + site.file() + ":" + site.line() + ") cached"); - - return value; - } - - public static IRubyObject getGlobalUncached(GlobalVariable variable) throws Throwable { - return variable.getAccessor().getValue(); - } - - public static void setGlobalFallback(GlobalSite site, IRubyObject value, ThreadContext context) throws Throwable { - Ruby runtime = context.runtime; - GlobalVariable variable = runtime.getGlobalVariables().getVariable(site.name()); - MethodHandle uncached = lookup().findStatic(Bootstrap.class, "setGlobalUncached", methodType(void.class, GlobalVariable.class, IRubyObject.class)); - uncached = uncached.bindTo(variable); - uncached = dropArguments(uncached, 1, ThreadContext.class); - site.setTarget(uncached); - uncached.invokeWithArguments(value, context); - } - - public static void setGlobalUncached(GlobalVariable variable, IRubyObject value) throws Throwable { - // FIXME: duplicated logic from GlobalVariables.set - variable.getAccessor().setValue(value); - variable.trace(value); - variable.invalidate(); - } - - public static Handle prepareBlock() { - return new Handle( - Opcodes.H_INVOKESTATIC, - p(Bootstrap.class), - "prepareBlock", - sig(CallSite.class, Lookup.class, String.class, - MethodType.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, String.class, long.class, - String.class, int.class, String.class), - false); - } - - public static CallSite prepareBlock(Lookup lookup, String name, MethodType type, - MethodHandle bodyHandle, MethodHandle scopeHandle, MethodHandle setScopeHandle, MethodHandle parentHandle, String scopeDescriptor, long encodedSignature, - String file, int line, String encodedArgumentDescriptors - ) throws Throwable { - StaticScope staticScope = (StaticScope) scopeHandle.invokeExact(); - - if (staticScope == null) { - staticScope = Helpers.restoreScope(scopeDescriptor, (StaticScope) parentHandle.invokeExact()); - setScopeHandle.invokeExact(staticScope); - } - - CompiledIRBlockBody body = new CompiledIRBlockBody(bodyHandle, staticScope, file, line, encodedArgumentDescriptors, encodedSignature); - - Binder binder = Binder.from(type); - - binder = binder.fold(FRAME_SCOPE_BINDING); - - // This optimization can't happen until we can see into the method we're calling to know if it reifies the block - if (false) { - /* - if (needsBinding) { - if (needsFrame) { - FullInterpreterContext fic = scope.getExecutionContext(); - if (fic.needsBinding()) { - if (fic.needsFrame()) { - binder = binder.fold(FRAME_SCOPE_BINDING); - } else { - binder = binder.fold(SCOPE_BINDING); - } - } else { - if (needsFrame) { - binder = binder.fold(FRAME_BINDING); - } else { - binder = binder.fold(SELF_BINDING); - } - }*/ - } - - MethodHandle blockMaker = binder.drop(1, 3) - .append(body) - .invoke(CONSTRUCT_BLOCK); - - return new ConstantCallSite(blockMaker); - } - - public CallSite checkArrayArity(Lookup lookup, String name, MethodType methodType, int required, int opt, int rest) { - return new ConstantCallSite(MethodHandles.insertArguments(CHECK_ARRAY_ARITY, 2, required, opt, (rest == 0 ? false : true))); - } - - public static final Handle CHECK_ARRAY_ARITY_BOOTSTRAP = new Handle(Opcodes.H_INVOKESTATIC, p(Helpers.class), "checkArrayArity", sig(CallSite.class, Lookup.class, String.class, MethodType.class, int.class, int.class, int.class), false); - - private static final MethodHandle CHECK_ARRAY_ARITY = - Binder - .from(void.class, ThreadContext.class, RubyArray.class, int.class, int.class, boolean.class) - .invokeStaticQuiet(LOOKUP, Helpers.class, "irCheckArgsArrayArity"); - - static String logMethod(DynamicMethod method) { - return "[#" + method.getSerialNumber() + " " + method.getImplementationClass().getMethodLocation() + "]"; - } - - static String logBlock(Block block) { - return "[" + block.getBody().getFile() + ":" + block.getBody().getLine() + "]"; - } - - private static final Binder BINDING_MAKER_BINDER = Binder.from(Binding.class, ThreadContext.class, IRubyObject.class, DynamicScope.class); - - private static final MethodHandle FRAME_SCOPE_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, IRRuntimeHelpers.class, "newFrameScopeBinding"); - - private static final MethodHandle FRAME_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, Bootstrap.class, "frameBinding"); - public static Binding frameBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { - Frame frame = context.getCurrentFrame().capture(); - return new Binding(self, frame, frame.getVisibility()); - } - - private static final MethodHandle SCOPE_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, Bootstrap.class, "scopeBinding"); - public static Binding scopeBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { - return new Binding(self, scope); - } - - private static final MethodHandle SELF_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, Bootstrap.class, "selfBinding"); - public static Binding selfBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { - return new Binding(self); - } - - private static final MethodHandle CONSTRUCT_BLOCK = Binder.from(Block.class, Binding.class, CompiledIRBlockBody.class).invokeStaticQuiet(LOOKUP, Bootstrap.class, "constructBlock"); - public static Block constructBlock(Binding binding, CompiledIRBlockBody body) throws Throwable { - return new Block(body, binding); - } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CallInfoBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/CallInfoBootstrap.java new file mode 100644 index 00000000000..d27bda3fda3 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/CallInfoBootstrap.java @@ -0,0 +1,37 @@ +package org.jruby.ir.targets.indy; + +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.runtime.ThreadContext; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; + +public class CallInfoBootstrap { + public static final Handle CALL_INFO_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(CallInfoBootstrap.class), + "callInfoBootstrap", + Bootstrap.BOOTSTRAP_INT_SIG, + false); + + public static CallSite callInfoBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int callInfo) throws Throwable { + MethodHandle handle; + if (callInfo == 0) { + handle = lookup.findVirtual(ThreadContext.class, "clearCallInfo", methodType(void.class)); + } else { + handle = lookup.findStatic(IRRuntimeHelpers.class, "setCallInfo", methodType(void.class, ThreadContext.class, int.class)); + handle = insertArguments(handle, 1, callInfo); + } + + return new ConstantCallSite(handle); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CallSiteCacheBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/CallSiteCacheBootstrap.java new file mode 100644 index 00000000000..dd6a5764a22 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/CallSiteCacheBootstrap.java @@ -0,0 +1,44 @@ +package org.jruby.ir.targets.indy; + +import org.jruby.runtime.CallType; +import org.jruby.runtime.callsite.CachingCallSite; +import org.jruby.runtime.callsite.FunctionalCachingCallSite; +import org.jruby.runtime.callsite.MonomorphicCallSite; +import org.jruby.runtime.callsite.VariableCachingCallSite; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodHandles.constant; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class CallSiteCacheBootstrap { + public static final Handle CALLSITE = new Handle( + Opcodes.H_INVOKESTATIC, + p(CallSiteCacheBootstrap.class), + "callSite", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, int.class), + false); + + public static CallSite callSite(MethodHandles.Lookup lookup, String name, MethodType type, String id, int callType) { + return new ConstantCallSite(constant(CachingCallSite.class, callSite(id, callType))); + } + + private static CachingCallSite callSite(String id, int callType) { + switch (CallType.fromOrdinal(callType)) { + case NORMAL: + return new MonomorphicCallSite(id); + case FUNCTIONAL: + return new FunctionalCachingCallSite(id); + case VARIABLE: + return new VariableCachingCallSite(id); + default: + throw new RuntimeException("BUG: Unexpected call type " + callType + " in JVM6 invoke logic"); + } + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CheckArityBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/CheckArityBootstrap.java new file mode 100644 index 00000000000..6f80c7a60ad --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/CheckArityBootstrap.java @@ -0,0 +1,76 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.RubyArray; +import org.jruby.ir.JIT; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.parser.StaticScope; +import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class CheckArityBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Handle CHECK_ARITY_SPECIFIC_ARGS = new Handle( + Opcodes.H_INVOKESTATIC, + p(CheckArityBootstrap.class), + "checkAritySpecificArgs", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, int.class, int.class), + false); + public static final Handle CHECK_ARITY = new Handle( + Opcodes.H_INVOKESTATIC, + p(CheckArityBootstrap.class), + "checkArity", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, int.class, int.class), + false); + public static final Handle CHECK_ARRAY_ARITY_BOOTSTRAP = new Handle(Opcodes.H_INVOKESTATIC, p(Helpers.class), "checkArrayArity", sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, int.class), false); + private static final MethodHandle CHECK_ARITY_HANDLE = + Binder + .from(void.class, ThreadContext.class, StaticScope.class, Object[].class, Object.class, Block.class, int.class, int.class, boolean.class, int.class) + .invokeStaticQuiet(LOOKUP, CheckArityBootstrap.class, "checkArity"); + private static final MethodHandle CHECK_ARITY_SPECIFIC_ARGS_HANDLE = + Binder + .from(void.class, ThreadContext.class, StaticScope.class, Object[].class, Block.class, int.class, int.class, boolean.class, int.class) + .invokeStaticQuiet(LOOKUP, CheckArityBootstrap.class, "checkAritySpecificArgs"); + private static final MethodHandle CHECK_ARRAY_ARITY = + Binder + .from(void.class, ThreadContext.class, RubyArray.class, int.class, int.class, boolean.class) + .invokeStaticQuiet(LOOKUP, Helpers.class, "irCheckArgsArrayArity"); + + @JIT + public static CallSite checkArity(MethodHandles.Lookup lookup, String name, MethodType type, int req, int opt, int rest, int keyrest) { + return new ConstantCallSite(insertArguments(CHECK_ARITY_HANDLE, 5, req, opt, rest == 0 ? false : true, keyrest)); + } + + @JIT + public static CallSite checkAritySpecificArgs(MethodHandles.Lookup lookup, String name, MethodType type, int req, int opt, int rest, int keyrest) { + return new ConstantCallSite(insertArguments(CHECK_ARITY_SPECIFIC_ARGS_HANDLE, 4, req, opt, rest == 0 ? false : true, keyrest)); + } + + @JIT + public static void checkArity(ThreadContext context, StaticScope scope, Object[] args, Object keywords, Block block, int req, int opt, boolean rest, int keyrest) { + IRRuntimeHelpers.checkArity(context, scope, args, keywords, req, opt, rest, keyrest, block); + } + + @JIT + public static void checkAritySpecificArgs(ThreadContext context, StaticScope scope, Object[] args, Block block, int req, int opt, boolean rest, int keyrest) { + IRRuntimeHelpers.checkAritySpecificArgs(context, scope, args, req, opt, rest, keyrest, block); + } + + public static CallSite checkArrayArity(MethodHandles.Lookup lookup, String name, MethodType methodType, int required, int opt, int rest) { + return new ConstantCallSite(MethodHandles.insertArguments(CHECK_ARRAY_ARITY, 2, required, opt, (rest == 0 ? false : true))); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CheckpointSite.java b/core/src/main/java/org/jruby/ir/targets/indy/CheckpointSite.java new file mode 100644 index 00000000000..caba15b302b --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/CheckpointSite.java @@ -0,0 +1,60 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.Ruby; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.opto.Invalidator; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; +import java.lang.invoke.SwitchPoint; + +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; + +public class CheckpointSite extends MutableCallSite { + public static final Handle CHECKPOINT_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(CheckpointSite.class), + "checkpointBootstrap", + Bootstrap.BOOTSTRAP_BARE_SIG, + false); + + public CheckpointSite(MethodType type) { + super(type); + } + + public static CallSite checkpointBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) throws Throwable { + CheckpointSite site = new CheckpointSite(type); + MethodHandle handle = lookup.findVirtual(CheckpointSite.class, "checkpointFallback", methodType(void.class, ThreadContext.class)); + + handle = handle.bindTo(site); + site.setTarget(handle); + + return site; + } + + public void checkpointFallback(ThreadContext context) throws Throwable { + Ruby runtime = context.runtime; + Invalidator invalidator = runtime.getCheckpointInvalidator(); + + MethodHandle target = Binder + .from(void.class, ThreadContext.class) + .nop(); + MethodHandle fallback = lookup().findVirtual(CheckpointSite.class, "checkpointFallback", methodType(void.class, ThreadContext.class)); + fallback = fallback.bindTo(this); + + target = ((SwitchPoint)invalidator.getData()).guardWithTest(target, fallback); + + this.setTarget(target); + + // poll for events once since we've ended up back in fallback + context.pollThreadEvents(); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/ConstantLookupSite.java b/core/src/main/java/org/jruby/ir/targets/indy/ConstantLookupSite.java index 32fdae1b5e7..5fb91edc95f 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/ConstantLookupSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/ConstantLookupSite.java @@ -40,6 +40,8 @@ public class ConstantLookupSite extends MutableCallSite { private final SiteTracker tracker = new SiteTracker(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + public static final Handle BOOTSTRAP = new Handle( Opcodes.H_INVOKESTATIC, p(ConstantLookupSite.class), @@ -105,7 +107,7 @@ public IRubyObject searchConst(ThreadContext context, StaticScope staticScope) { .constant(constant); MethodHandle fallback = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "searchConst"); + .invokeVirtualQuiet(LOOKUP, "searchConst"); setTarget(switchPoint.guardWithTest(target, fallback)); @@ -296,7 +298,7 @@ public IRubyObject lexicalSearchConst(ThreadContext context, StaticScope scope) MethodHandle fallback = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "lexicalSearchConst"); + .invokeVirtualQuiet(LOOKUP, "lexicalSearchConst"); setTarget(switchPoint.guardWithTest(target, fallback)); @@ -312,7 +314,7 @@ private MethodHandle SMFC() { if (_SMFC != null) return _SMFC; return _SMFC = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "searchModuleForConst"); + .invokeVirtualQuiet(LOOKUP, "searchModuleForConst"); } private MethodHandle _noCacheSMFC; @@ -320,7 +322,7 @@ private MethodHandle noCacheSMFC() { if (_noCacheSMFC != null) return _noCacheSMFC; return _noCacheSMFC = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "noCacheSearchModuleForConst"); + .invokeVirtualQuiet(LOOKUP, "noCacheSearchModuleForConst"); } private MethodHandle _ISC; @@ -328,7 +330,7 @@ private MethodHandle ISC() { if (_ISC != null) return _ISC; return _ISC = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "inheritanceSearchConst"); + .invokeVirtualQuiet(LOOKUP, "inheritanceSearchConst"); } private MethodHandle _noCacheISC; @@ -336,6 +338,6 @@ private MethodHandle noCacheISC() { if (_noCacheISC != null) return _noCacheISC; return _noCacheISC = Binder.from(type()) .insert(0, this) - .invokeVirtualQuiet(Bootstrap.LOOKUP, "noCacheInheritanceSearchConst"); + .invokeVirtualQuiet(LOOKUP, "noCacheInheritanceSearchConst"); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/ConstructBlockBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/ConstructBlockBootstrap.java new file mode 100644 index 00000000000..aa54a1046b3 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/ConstructBlockBootstrap.java @@ -0,0 +1,104 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.parser.StaticScope; +import org.jruby.runtime.Binding; +import org.jruby.runtime.Block; +import org.jruby.runtime.CompiledIRBlockBody; +import org.jruby.runtime.DynamicScope; +import org.jruby.runtime.Frame; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class ConstructBlockBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private static final Binder BINDING_MAKER_BINDER = Binder.from(Binding.class, ThreadContext.class, IRubyObject.class, DynamicScope.class); + private static final MethodHandle SELF_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, ConstructBlockBootstrap.class, "selfBinding"); + private static final MethodHandle SCOPE_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, ConstructBlockBootstrap.class, "scopeBinding"); + private static final MethodHandle FRAME_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, ConstructBlockBootstrap.class, "frameBinding"); + private static final MethodHandle FRAME_SCOPE_BINDING = BINDING_MAKER_BINDER.invokeStaticQuiet(LOOKUP, IRRuntimeHelpers.class, "newFrameScopeBinding"); + private static final MethodHandle CONSTRUCT_BLOCK = Binder.from(Block.class, Binding.class, CompiledIRBlockBody.class).invokeStaticQuiet(LOOKUP, ConstructBlockBootstrap.class, "constructBlock"); + public static final Handle PREPARE_BLOCK_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(ConstructBlockBootstrap.class), + "prepareBlock", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, + MethodType.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, String.class, long.class, + String.class, int.class, String.class), + false); + + public static CallSite prepareBlock(MethodHandles.Lookup lookup, String name, MethodType type, + MethodHandle bodyHandle, MethodHandle scopeHandle, MethodHandle setScopeHandle, MethodHandle parentHandle, String scopeDescriptor, long encodedSignature, + String file, int line, String encodedArgumentDescriptors + ) throws Throwable { + StaticScope staticScope = (StaticScope) scopeHandle.invokeExact(); + + if (staticScope == null) { + staticScope = Helpers.restoreScope(scopeDescriptor, (StaticScope) parentHandle.invokeExact()); + setScopeHandle.invokeExact(staticScope); + } + + CompiledIRBlockBody body = new CompiledIRBlockBody(bodyHandle, staticScope, file, line, encodedArgumentDescriptors, encodedSignature); + + Binder binder = Binder.from(type); + + binder = binder.fold(FRAME_SCOPE_BINDING); + + // This optimization can't happen until we can see into the method we're calling to know if it reifies the block + if (false) { + /* + if (needsBinding) { + if (needsFrame) { + FullInterpreterContext fic = scope.getExecutionContext(); + if (fic.needsBinding()) { + if (fic.needsFrame()) { + binder = binder.fold(FRAME_SCOPE_BINDING); + } else { + binder = binder.fold(SCOPE_BINDING); + } + } else { + if (needsFrame) { + binder = binder.fold(FRAME_BINDING); + } else { + binder = binder.fold(SELF_BINDING); + } + }*/ + } + + MethodHandle blockMaker = binder.drop(1, 3) + .append(body) + .invoke(CONSTRUCT_BLOCK); + + return new ConstantCallSite(blockMaker); + } + + public static Binding frameBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { + Frame frame = context.getCurrentFrame().capture(); + return new Binding(self, frame, frame.getVisibility()); + } + + public static Binding scopeBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { + return new Binding(self, scope); + } + + public static Binding selfBinding(ThreadContext context, IRubyObject self, DynamicScope scope) { + return new Binding(self); + } + + public static Block constructBlock(Binding binding, CompiledIRBlockBody body) throws Throwable { + return new Block(body, binding); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CoverageSite.java b/core/src/main/java/org/jruby/ir/targets/indy/CoverageSite.java new file mode 100644 index 00000000000..3f1b525fc2b --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/CoverageSite.java @@ -0,0 +1,44 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.runtime.ThreadContext; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class CoverageSite { + public static final Handle COVER_LINE_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(CoverageSite.class), + "coverLineBootstrap", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, int.class, int.class), + false); + + public static CallSite coverLineBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String filename, int line, int oneshot) throws Throwable { + MutableCallSite site = new MutableCallSite(type); + MethodHandle handle = lookup.findStatic(CoverageSite.class, "coverLineFallback", methodType(void.class, MutableCallSite.class, ThreadContext.class, String.class, int.class, boolean.class)); + + handle = handle.bindTo(site); + handle = insertArguments(handle, 1, filename, line, oneshot != 0); + site.setTarget(handle); + + return site; + } + + public static void coverLineFallback(MutableCallSite site, ThreadContext context, String filename, int line, boolean oneshot) throws Throwable { + IRRuntimeHelpers.updateCoverage(context, filename, line); + + if (oneshot) site.setTarget(Binder.from(void.class, ThreadContext.class).dropAll().nop()); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/HashBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/HashBootstrap.java new file mode 100644 index 00000000000..5c755a22c99 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/HashBootstrap.java @@ -0,0 +1,99 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.Ruby; +import org.jruby.RubyHash; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class HashBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private static final MethodHandle HASH_HANDLE = + Binder + .from(RubyHash.class, ThreadContext.class, IRubyObject[].class) + .invokeStaticQuiet(LOOKUP, HashBootstrap.class, "hash"); + private static final MethodHandle KWARGS_HASH_HANDLE = + Binder + .from(RubyHash.class, ThreadContext.class, RubyHash.class, IRubyObject[].class) + .invokeStaticQuiet(LOOKUP, HashBootstrap.class, "kwargsHash"); + public static final Handle HASH_H = new Handle( + Opcodes.H_INVOKESTATIC, + p(HashBootstrap.class), + "hash", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + public static final Handle KWARGS_HASH_H = new Handle( + Opcodes.H_INVOKESTATIC, + p(HashBootstrap.class), + "kwargsHash", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + // We use LOOKUP here to have a full-featured MethodHandles.Lookup, avoiding jruby/jruby#7911 + private static final MethodHandle RUNTIME_FROM_CONTEXT_HANDLE = + Binder + .from(LOOKUP, Ruby.class, ThreadContext.class) + .getFieldQuiet("runtime"); + + public static CallSite hash(MethodHandles.Lookup lookup, String name, MethodType type) { + MethodHandle handle; + + int parameterCount = type.parameterCount(); + if (parameterCount == 1) { + handle = Binder + .from(lookup, type) + .cast(type.changeReturnType(RubyHash.class)) + .filter(0, RUNTIME_FROM_CONTEXT_HANDLE) + .invokeStaticQuiet(lookup, RubyHash.class, "newHash"); + } else if (!type.parameterType(parameterCount - 1).isArray() + && (parameterCount - 1) / 2 <= Helpers.MAX_SPECIFIC_ARITY_HASH) { + handle = Binder + .from(lookup, type) + .cast(type.changeReturnType(RubyHash.class)) + .filter(0, RUNTIME_FROM_CONTEXT_HANDLE) + .invokeStaticQuiet(lookup, Helpers.class, "constructSmallHash"); + } else { + handle = Binder + .from(lookup, type) + .collect(1, IRubyObject[].class) + .invoke(HASH_HANDLE); + } + + return new ConstantCallSite(handle); + } + + public static CallSite kwargsHash(MethodHandles.Lookup lookup, String name, MethodType type) { + MethodHandle handle = Binder + .from(lookup, type) + .collect(2, IRubyObject[].class) + .invoke(KWARGS_HASH_HANDLE); + + return new ConstantCallSite(handle); + } + + public static RubyHash hash(ThreadContext context, IRubyObject[] pairs) { + Ruby runtime = context.runtime; + RubyHash hash = new RubyHash(runtime, pairs.length / 2 + 1); + for (int i = 0; i < pairs.length;) { + hash.fastASetCheckString(runtime, pairs[i++], pairs[i++]); + } + return hash; + } + + public static RubyHash kwargsHash(ThreadContext context, RubyHash hash, IRubyObject[] pairs) { + return IRRuntimeHelpers.dupKwargsHashAndPopulateFromArray(context, hash, pairs); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/HeapVariableBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/HeapVariableBootstrap.java new file mode 100644 index 00000000000..38b218e4042 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/HeapVariableBootstrap.java @@ -0,0 +1,84 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.scope.DynamicScopeGenerator; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.runtime.Helpers.arrayOf; +import static org.jruby.util.CodegenUtils.p; + +public class HeapVariableBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + public static final Handle GET_HEAP_LOCAL_OR_NIL_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(HeapVariableBootstrap.class), + "getHeapLocalOrNilBootstrap", + Bootstrap.BOOTSTRAP_INT_INT_SIG, + false); + public static final Handle GET_HEAP_LOCAL_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(HeapVariableBootstrap.class), + "getHeapLocalBootstrap", + Bootstrap.BOOTSTRAP_INT_INT_SIG, + false); + + public static CallSite getHeapLocalBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int depth, int location) throws Throwable { + // no null checking needed for method bodies + MethodHandle getter; + Binder binder = Binder + .from(type); + + if (depth == 0) { + if (location < DynamicScopeGenerator.SPECIALIZED_GETS.size()) { + getter = binder.invokeVirtualQuiet(LOOKUP, DynamicScopeGenerator.SPECIALIZED_GETS.get(location)); + } else { + getter = binder + .insert(1, location) + .invokeVirtualQuiet(LOOKUP, "getValueDepthZero"); + } + } else { + getter = binder + .insert(1, arrayOf(int.class, int.class), location, depth) + .invokeVirtualQuiet(LOOKUP, "getValue"); + } + + ConstantCallSite site = new ConstantCallSite(getter); + + return site; + } + + public static CallSite getHeapLocalOrNilBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int depth, int location) throws Throwable { + MethodHandle getter; + Binder binder = Binder + .from(type) + .filter(1, LiteralValueBootstrap.contextValue(lookup, "nil", methodType(IRubyObject.class, ThreadContext.class)).dynamicInvoker()); + + if (depth == 0) { + if (location < DynamicScopeGenerator.SPECIALIZED_GETS_OR_NIL.size()) { + getter = binder.invokeVirtualQuiet(LOOKUP, DynamicScopeGenerator.SPECIALIZED_GETS_OR_NIL.get(location)); + } else { + getter = binder + .insert(1, location) + .invokeVirtualQuiet(LOOKUP, "getValueDepthZeroOrNil"); + } + } else { + getter = binder + .insert(1, arrayOf(int.class, int.class), location, depth) + .invokeVirtualQuiet(LOOKUP, "getValueOrNil"); + } + + ConstantCallSite site = new ConstantCallSite(getter); + + return site; + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyArgumentsCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyArgumentsCompiler.java index eeab3596aa9..cc3aba45a24 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyArgumentsCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyArgumentsCompiler.java @@ -23,6 +23,6 @@ public void kwargsHash(int length) { if (length > IRBytecodeAdapter.MAX_ARGUMENTS / 2) throw new NotCompilableException("kwargs hash has more than " + (IRBytecodeAdapter.MAX_ARGUMENTS / 2) + " pairs"); - compiler.adapter.invokedynamic("kwargsHash", CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, RubyHash.class, JVM.OBJECT, length * 2)), Bootstrap.kwargsHash()); + compiler.adapter.invokedynamic("kwargsHash", CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, RubyHash.class, JVM.OBJECT, length * 2)), HashBootstrap.KWARGS_HASH_H); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyBlockCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyBlockCompiler.java index a5d8662f8a0..f79a88e084f 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyBlockCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyBlockCompiler.java @@ -49,6 +49,6 @@ public void prepareBlock(IRClosure closure, String parentScopeField, Handle hand long encodedSignature = signature.encode(); compiler.adapter.invokedynamic(handle.getName(), sig(Block.class, ThreadContext.class, IRubyObject.class, DynamicScope.class), - Bootstrap.prepareBlock(), handle, scopeHandle, setScopeHandle, parentScopeHandle, scopeDescriptor, encodedSignature, file, line, encodedArgumentDescriptors); + ConstructBlockBootstrap.PREPARE_BLOCK_BOOTSTRAP, handle, scopeHandle, setScopeHandle, parentScopeHandle, scopeDescriptor, encodedSignature, file, line, encodedArgumentDescriptors); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyBranchCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyBranchCompiler.java index e0b3c472da9..8f50d88e3d8 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyBranchCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyBranchCompiler.java @@ -23,13 +23,13 @@ public IndyBranchCompiler(IRBytecodeAdapter compiler) { @Override public void branchIfNil(Label label) { - compiler.adapter.invokedynamic("isNil", sig(boolean.class, IRubyObject.class), Bootstrap.isNilBoot()); + compiler.adapter.invokedynamic("isNil", sig(boolean.class, IRubyObject.class), IsNilSite.IS_NIL_BOOTSTRAP_HANDLE); compiler.adapter.iftrue(label); } @Override public void branchIfTruthy(Label label) { - compiler.adapter.invokedynamic("isTrue", sig(boolean.class, IRubyObject.class), Bootstrap.isTrueBoot()); + compiler.adapter.invokedynamic("isTrue", sig(boolean.class, IRubyObject.class), IsTrueSite.IS_TRUE_BOOTSTRAP_HANDLE); compiler.adapter.iftrue(label); } @@ -46,14 +46,14 @@ public void btrue(Label label) { public void checkArgsArity(Runnable args, int required, int opt, boolean rest) { compiler.loadContext(); args.run(); - compiler.adapter.invokedynamic("checkArrayArity", sig(void.class, ThreadContext.class, RubyArray.class), Bootstrap.CHECK_ARRAY_ARITY_BOOTSTRAP, required, opt, rest ? 1 : 0); + compiler.adapter.invokedynamic("checkArrayArity", sig(void.class, ThreadContext.class, RubyArray.class), CheckArityBootstrap.CHECK_ARRAY_ARITY_BOOTSTRAP, required, opt, rest ? 1 : 0); } public void checkArity(int required, int opt, boolean rest, int restKey) { compiler.adapter.invokedynamic( "checkArity", sig(void.class, ThreadContext.class, StaticScope.class, Object[].class, Object.class, Block.class), - Bootstrap.CHECK_ARITY, + CheckArityBootstrap.CHECK_ARITY, required, opt, rest ? 1 : 0, restKey); } @@ -61,7 +61,7 @@ public void checkAritySpecificArgs(int required, int opt, boolean rest, int rest compiler.adapter.invokedynamic( "checkArity", sig(void.class, ThreadContext.class, StaticScope.class, Object[].class, Block.class), - Bootstrap.CHECK_ARITY_SPECIFIC_ARGS, + CheckArityBootstrap.CHECK_ARITY_SPECIFIC_ARGS, required, opt, rest ? 1 : 0, restKey); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyCheckpointCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyCheckpointCompiler.java index 86c42a211b9..2b375de36ce 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyCheckpointCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyCheckpointCompiler.java @@ -18,6 +18,6 @@ public void checkpoint() { compiler.adapter.invokedynamic( "checkpoint", sig(void.class, ThreadContext.class), - Bootstrap.checkpointHandle()); + CheckpointSite.CHECKPOINT_BOOTSTRAP); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyDynamicValueCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyDynamicValueCompiler.java index 03df2b978bd..28cc0f2e48f 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyDynamicValueCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyDynamicValueCompiler.java @@ -37,13 +37,13 @@ public void array(int length) { return; } - compiler.adapter.invokedynamic("array", CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, length)), Bootstrap.array()); + compiler.adapter.invokedynamic("array", CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, length)), ArrayBootstrap.ARRAY_H); } public void hash(int length) { if (length > IRBytecodeAdapter.MAX_ARGUMENTS / 2) throw new NotCompilableException("literal hash has more than " + (IRBytecodeAdapter.MAX_ARGUMENTS / 2) + " pairs"); - compiler.adapter.invokedynamic("hash", sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, length * 2)), Bootstrap.hash()); + compiler.adapter.invokedynamic("hash", sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, length * 2)), HashBootstrap.HASH_H); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyGlobalVariableCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyGlobalVariableCompiler.java index 7ae0d457a68..6362ee36f5d 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyGlobalVariableCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyGlobalVariableCompiler.java @@ -4,6 +4,7 @@ import org.jruby.ir.targets.IRBytecodeAdapter; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.invokedynamic.GlobalSite; import org.jruby.util.JavaNameMangler; import static org.jruby.util.CodegenUtils.sig; @@ -21,7 +22,7 @@ public void getGlobalVariable(String name, String file) { compiler.adapter.invokedynamic( "get:" + JavaNameMangler.mangleMethodName(name), sig(IRubyObject.class, ThreadContext.class), - Bootstrap.global(), + org.jruby.runtime.invokedynamic.GlobalSite.GLOBAL_BOOTSTRAP_H, file, compiler.getLastLine()); } @@ -31,7 +32,7 @@ public void setGlobalVariable(String name, String file) { compiler.adapter.invokedynamic( "set:" + JavaNameMangler.mangleMethodName(name), sig(void.class, IRubyObject.class, ThreadContext.class), - Bootstrap.global(), + GlobalSite.GLOBAL_BOOTSTRAP_H, file, compiler.getLastLine()); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java index d28d80469d5..3c980ff2719 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java @@ -16,6 +16,7 @@ import org.jruby.runtime.MethodIndex; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.invokedynamic.MathLinker; import org.jruby.util.CodegenUtils; import org.jruby.util.JavaNameMangler; @@ -78,7 +79,7 @@ public void invokeOtherOneFixnum(String file, CallBase call, long fixnum) { compiler.adapter.invokedynamic( "fixnumOperator:" + JavaNameMangler.mangleMethodName(id), signature, - Bootstrap.getFixnumOperatorHandle(), + MathLinker.FIXNUM_OPERATOR_BOOTSTRAP, fixnum, call.getCallType().ordinal(), file, @@ -99,7 +100,7 @@ public void invokeOtherOneFloat(String file, CallBase call, double flote) { compiler.adapter.invokedynamic( "floatOperator:" + JavaNameMangler.mangleMethodName(id), signature, - Bootstrap.getFloatOperatorHandle(), + MathLinker.FLOAT_OPERATOR_BOOTSTRAP, flote, call.getCallType().ordinal(), file, @@ -150,9 +151,9 @@ public void invokeInstanceSuper(String file, String name, int arity, boolean has String splatmapString = IRRuntimeHelpers.encodeSplatmap(splatmap); if (hasClosure) { String operation = literalClosure ? "invokeInstanceSuperIter" : "invokeInstanceSuper"; - compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } else { - compiler.adapter.invokedynamic("invokeInstanceSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic("invokeInstanceSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } } @@ -163,9 +164,9 @@ public void invokeClassSuper(String file, String name, int arity, boolean hasClo String splatmapString = IRRuntimeHelpers.encodeSplatmap(splatmap); if (hasClosure) { String operation = literalClosure ? "invokeClassSuperIter" : "invokeClassSuper"; - compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } else { - compiler.adapter.invokedynamic("invokeClassSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic("invokeClassSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } } @@ -176,9 +177,9 @@ public void invokeUnresolvedSuper(String file, String name, int arity, boolean h String splatmapString = IRRuntimeHelpers.encodeSplatmap(splatmap); if (hasClosure) { String operation = literalClosure ? "invokeUnresolvedSuperIter" : "invokeUnresolvedSuper"; - compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic(operation + ":" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } else { - compiler.adapter.invokedynamic("invokeUnresolvedSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic("invokeUnresolvedSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } } @@ -188,9 +189,9 @@ public void invokeZSuper(String file, String name, int arity, boolean hasClosure String splatmapString = IRRuntimeHelpers.encodeSplatmap(splatmap); if (hasClosure) { - compiler.adapter.invokedynamic("invokeZSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic("invokeZSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity, Block.class)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } else { - compiler.adapter.invokedynamic("invokeZSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), Bootstrap.invokeSuper(), splatmapString, flags, file, compiler.getLastLine()); + compiler.adapter.invokedynamic("invokeZSuper:" + JavaNameMangler.mangleMethodName(name), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, RubyClass.class, JVM.OBJECT, arity)), SuperInvokeSite.BOOTSTRAP, splatmapString, flags, file, compiler.getLastLine()); } } @@ -212,6 +213,6 @@ public void asString(AsStringInstr call, String scopeFieldName, String file) { @Override public void setCallInfo(int flags) { compiler.loadContext(); - compiler.adapter.invokedynamic("callInfo", sig(void.class, ThreadContext.class), Bootstrap.callInfoHandle(), flags); + compiler.adapter.invokedynamic("callInfo", sig(void.class, ThreadContext.class), CallInfoBootstrap.CALL_INFO_BOOTSTRAP, flags); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyLocalVariableCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyLocalVariableCompiler.java index eb5fa363204..47aa232d5b5 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyLocalVariableCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyLocalVariableCompiler.java @@ -26,12 +26,12 @@ public void getHeapLocal(int depth, int location) { normalLocalVariableCompiler.getHeapLocal(depth, location); return; } - compiler.adapter.invokedynamic("getHeapLocal", sig(IRubyObject.class, DynamicScope.class), Bootstrap.getHeapLocalHandle(), depth, location); + compiler.adapter.invokedynamic("getHeapLocal", sig(IRubyObject.class, DynamicScope.class), HeapVariableBootstrap.GET_HEAP_LOCAL_BOOTSTRAP, depth, location); } @Override public void getHeapLocalOrNil(int depth, int location) { compiler.loadContext(); - compiler.adapter.invokedynamic("getHeapLocalOrNil", sig(IRubyObject.class, DynamicScope.class, ThreadContext.class), Bootstrap.getHeapLocalOrNilHandle(), depth, location); + compiler.adapter.invokedynamic("getHeapLocalOrNil", sig(IRubyObject.class, DynamicScope.class, ThreadContext.class), HeapVariableBootstrap.GET_HEAP_LOCAL_OR_NIL_BOOTSTRAP, depth, location); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyValueCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyValueCompiler.java index d630c9cd8c4..08e414b9986 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyValueCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyValueCompiler.java @@ -24,7 +24,6 @@ import org.jruby.util.CodegenUtils; import java.math.BigInteger; -import java.util.function.Consumer; import static org.jruby.util.CodegenUtils.ci; import static org.jruby.util.CodegenUtils.p; @@ -41,7 +40,7 @@ public IndyValueCompiler(IRBytecodeAdapter compiler) { public void pushRuntime() { compiler.loadContext(); - compiler.adapter.invokedynamic("runtime", sig(Ruby.class, ThreadContext.class), Bootstrap.contextValue()); + compiler.adapter.invokedynamic("runtime", sig(Ruby.class, ThreadContext.class), LiteralValueBootstrap.CONTEXT_VALUE_HANDLE); } public void pushArrayClass() { compiler.loadContext(); @@ -74,26 +73,26 @@ public void pushFloat(double d) { public void pushString(ByteList bl, int cr) { compiler.loadContext(); - compiler.adapter.invokedynamic("string", sig(RubyString.class, ThreadContext.class), Bootstrap.string(), RubyEncoding.decodeRaw(bl), bl.getEncoding().toString(), cr); + compiler.adapter.invokedynamic("string", sig(RubyString.class, ThreadContext.class), StringBootstrap.STRING_BOOTSTRAP, RubyEncoding.decodeRaw(bl), bl.getEncoding().toString(), cr); } public void pushFrozenString(ByteList bl, int cr, String file, int line) { compiler.loadContext(); - compiler.adapter.invokedynamic("frozen", sig(RubyString.class, ThreadContext.class), Bootstrap.fstring(), RubyEncoding.decodeRaw(bl), bl.getEncoding().toString(), cr, file, line); + compiler.adapter.invokedynamic("frozen", sig(RubyString.class, ThreadContext.class), StringBootstrap.FSTRING_BOOTSTRAP, RubyEncoding.decodeRaw(bl), bl.getEncoding().toString(), cr, file, line); } public void pushEmptyString(Encoding encoding) { compiler.loadContext(); - compiler.adapter.invokedynamic("emptyString", sig(RubyString.class, ThreadContext.class), Bootstrap.EMPTY_STRING_BOOTSTRAP, encoding.toString()); + compiler.adapter.invokedynamic("emptyString", sig(RubyString.class, ThreadContext.class), StringBootstrap.EMPTY_STRING_BOOTSTRAP, encoding.toString()); } public void pushBufferString(Encoding encoding, int size) { compiler.loadContext(); - compiler.adapter.invokedynamic("bufferString", sig(RubyString.class, ThreadContext.class), Bootstrap.BUFFER_STRING_BOOTSTRAP, encoding.toString(), size); + compiler.adapter.invokedynamic("bufferString", sig(RubyString.class, ThreadContext.class), StringBootstrap.BUFFER_STRING_BOOTSTRAP, encoding.toString(), size); } public void pushByteList(ByteList bl) { - compiler.adapter.invokedynamic("bytelist", sig(ByteList.class), Bootstrap.bytelist(), RubyEncoding.decodeRaw(bl), bl.getEncoding().toString()); + compiler.adapter.invokedynamic("bytelist", sig(ByteList.class), StringBootstrap.BYTELIST_BOOTSTRAP, RubyEncoding.decodeRaw(bl), bl.getEncoding().toString()); } public void pushRange(Runnable begin, Runnable end, boolean exclusive) { @@ -120,22 +119,22 @@ public void pushSymbolProc(final ByteList bytes) { public void pushRubyEncoding(Encoding encoding) { compiler.loadContext(); - compiler.adapter.invokedynamic("rubyEncoding", sig(RubyEncoding.class, ThreadContext.class), Bootstrap.contextValueString(), new String(encoding.getName())); + compiler.adapter.invokedynamic("rubyEncoding", sig(RubyEncoding.class, ThreadContext.class), LiteralValueBootstrap.CONTEXT_VALUE_STRING_HANDLE, new String(encoding.getName())); } public void pushEncoding(Encoding encoding) { compiler.loadContext(); - compiler.adapter.invokedynamic("encoding", sig(RubyEncoding.class, ThreadContext.class), Bootstrap.contextValueString(), new String(encoding.getName())); + compiler.adapter.invokedynamic("encoding", sig(RubyEncoding.class, ThreadContext.class), LiteralValueBootstrap.CONTEXT_VALUE_STRING_HANDLE, new String(encoding.getName())); } public void pushNil() { compiler.loadContext(); - compiler.adapter.invokedynamic("nil", sig(IRubyObject.class, ThreadContext.class), Bootstrap.contextValue()); + compiler.adapter.invokedynamic("nil", sig(IRubyObject.class, ThreadContext.class), LiteralValueBootstrap.CONTEXT_VALUE_HANDLE); } public void pushBoolean(boolean b) { compiler.loadContext(); - compiler.adapter.invokedynamic(b ? "True" : "False", sig(IRubyObject.class, ThreadContext.class), Bootstrap.contextValue()); + compiler.adapter.invokedynamic(b ? "True" : "False", sig(IRubyObject.class, ThreadContext.class), LiteralValueBootstrap.CONTEXT_VALUE_HANDLE); } public void pushBignum(BigInteger bigint) { @@ -156,7 +155,7 @@ public void pushCallSite(String className, String siteName, String scopeFieldNam if (!specialSite) { // use indy to cache the site object - method.invokedynamic("callSite", sig(CachingCallSite.class), Bootstrap.CALLSITE, call.getId(), callType.ordinal()); + method.invokedynamic("callSite", sig(CachingCallSite.class), CallSiteCacheBootstrap.CALLSITE, call.getId(), callType.ordinal()); return; } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java index d4956c9cc8c..a081e72ce25 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java @@ -15,8 +15,15 @@ import org.jruby.RubyNil; import org.jruby.RubyStruct; import org.jruby.RubySymbol; +import org.jruby.anno.JRubyMethod; import org.jruby.internal.runtime.methods.AliasMethod; +import org.jruby.internal.runtime.methods.AttrReaderMethod; +import org.jruby.internal.runtime.methods.AttrWriterMethod; +import org.jruby.internal.runtime.methods.CompiledIRMethod; import org.jruby.internal.runtime.methods.DynamicMethod; +import org.jruby.internal.runtime.methods.HandleMethod; +import org.jruby.internal.runtime.methods.MixedModeIRMethod; +import org.jruby.internal.runtime.methods.NativeCallMethod; import org.jruby.internal.runtime.methods.PartialDelegatingMethod; import org.jruby.ir.JIT; import org.jruby.ir.runtime.IRRuntimeHelpers; @@ -24,6 +31,7 @@ import org.jruby.java.invokers.InstanceFieldGetter; import org.jruby.java.invokers.InstanceFieldSetter; import org.jruby.javasupport.JavaUtil; +import org.jruby.parser.StaticScope; import org.jruby.runtime.Block; import org.jruby.runtime.CallType; import org.jruby.runtime.Helpers; @@ -32,7 +40,10 @@ import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.callsite.CacheEntry; +import org.jruby.runtime.invokedynamic.InvocationLinker; import org.jruby.runtime.invokedynamic.JRubyCallSite; +import org.jruby.runtime.ivars.FieldVariableAccessor; +import org.jruby.runtime.ivars.VariableAccessor; import org.jruby.util.cli.Options; import org.jruby.util.log.Logger; import org.jruby.util.log.LoggerFactory; @@ -49,9 +60,13 @@ import java.util.Map; import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; import static java.lang.invoke.MethodHandles.foldArguments; import static java.lang.invoke.MethodHandles.insertArguments; import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.runtime.Helpers.arrayOf; +import static org.jruby.runtime.Helpers.constructObjectArrayHandle; import static org.jruby.runtime.invokedynamic.JRubyCallSite.SITE_ID; /** @@ -60,6 +75,8 @@ public abstract class InvokeSite extends MutableCallSite { private static final Logger LOG = LoggerFactory.getLogger(InvokeSite.class); + private static final String[] GENERIC_CALL_PERMUTE = {"context", "self", "arg.*"}; + static { // enable DEBUG output if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.setDebugEnable(true); } @@ -82,6 +99,469 @@ public abstract class InvokeSite extends MutableCallSite { private boolean literalClosure; protected CacheEntry cache = CacheEntry.NULL_CACHE; + public static boolean testType(RubyClass original, IRubyObject self) { + // naive test + return original == RubyBasicObject.getMetaClass(self); + } + + MethodHandle buildIndyHandle(CacheEntry entry) { + MethodHandle mh = null; + Signature siteToDyncall = signature.insertArgs(argOffset, arrayOf("class", "name"), arrayOf(RubyModule.class, String.class)); + DynamicMethod method = entry.method; + + if (method instanceof HandleMethod) { + HandleMethod handleMethod = (HandleMethod)method; + boolean blockGiven = signature.lastArgType() == Block.class; + + if (arity >= 0) { + mh = handleMethod.getHandle(arity); + if (mh != null) { + if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); + if (!functional) mh = dropArguments(mh, 1, IRubyObject.class); + } else { + mh = handleMethod.getHandle(-1); + if (!functional) mh = dropArguments(mh, 1, IRubyObject.class); + if (arity == 0) { + if (!blockGiven) { + mh = insertArguments(mh, mh.type().parameterCount() - 2, IRubyObject.NULL_ARRAY, Block.NULL_BLOCK); + } else { + mh = insertArguments(mh, mh.type().parameterCount() - 2, (Object)IRubyObject.NULL_ARRAY); + } + } else { + // bundle up varargs + if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); + + mh = SmartBinder.from(lookup(), siteToDyncall) + .collect("args", "arg.*", Helpers.constructObjectArrayHandle(arity)) + .invoke(mh) + .handle(); + } + } + } else { + mh = handleMethod.getHandle(-1); + if (mh != null) { + if (!functional) mh = dropArguments(mh, 1, IRubyObject.class); + if (!blockGiven) mh = insertArguments(mh, mh.type().parameterCount() - 1, Block.NULL_BLOCK); + + mh = SmartBinder.from(lookup(), siteToDyncall) + .invoke(mh) + .handle(); + } + } + + if (mh != null) { + mh = insertArguments(mh, argOffset, entry.sourceModule, name()); + + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tbound directly to handle " + Bootstrap.logMethod(method)); + } + } + } + + return mh; + } + + MethodHandle buildGenericHandle(CacheEntry entry) { + SmartBinder binder; + DynamicMethod method = entry.method; + + binder = SmartBinder.from(signature); + + binder = permuteForGenericCall(binder, method, GENERIC_CALL_PERMUTE); + + + binder = binder + .insert(2, new String[]{"rubyClass", "name"}, new Class[]{RubyModule.class, String.class}, entry.sourceModule, name()) + .insert(0, "method", DynamicMethod.class, method); + + if (arity > 3) { + binder = binder.collect("args", "arg.*", constructObjectArrayHandle(arity)); + } + + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tbound indirectly " + method + ", " + Bootstrap.logMethod(method)); + } + + return binder.invokeVirtualQuiet(LOOKUP, "call").handle(); + } + + private static SmartBinder permuteForGenericCall(SmartBinder binder, DynamicMethod method, String... basePermutes) { + if (methodWantsBlock(method)) { + binder = binder.permute(arrayOf(basePermutes, "block", String[]::new)); + } else { + binder = binder.permute(basePermutes); + } + return binder; + } + + private static boolean methodWantsBlock(DynamicMethod method) { + // only include block if native signature receives block, whatever its arity + boolean wantsBlock = true; + if (method instanceof NativeCallMethod) { + DynamicMethod.NativeCall nativeCall = ((NativeCallMethod) method).getNativeCall(); + // if it is a non-JI native call and does not want block, drop it + // JI calls may lazily convert blocks to an interface type (jruby/jruby#7246) + if (nativeCall != null && !nativeCall.isJava()) { + Class[] nativeSignature = nativeCall.getNativeSignature(); + + // no args or last arg not a block, do no pass block + if (nativeSignature.length == 0 || nativeSignature[nativeSignature.length - 1] != Block.class) { + wantsBlock = false; + } + } + } + return wantsBlock; + } + + static MethodHandle buildMethodMissingHandle(InvokeSite site, CacheEntry entry, IRubyObject self) { + SmartBinder binder; + DynamicMethod method = entry.method; + + if (site.arity >= 0) { + binder = SmartBinder.from(site.signature); + + binder = permuteForGenericCall(binder, method, GENERIC_CALL_PERMUTE) + .insert(2, + new String[]{"rubyClass", "name", "argName"} + , new Class[]{RubyModule.class, String.class, IRubyObject.class}, + entry.sourceModule, + site.name(), + self.getRuntime().newSymbol(site.methodName)) + .insert(0, "method", DynamicMethod.class, method) + .collect("args", "arg.*", Helpers.constructObjectArrayHandle(site.arity + 1)); + } else { + SmartHandle fold = SmartBinder.from( + site.signature + .permute("context", "self", "args", "block") + .changeReturn(IRubyObject[].class)) + .permute("args") + .insert(0, "argName", IRubyObject.class, self.getRuntime().newSymbol(site.methodName)) + .invokeStaticQuiet(LOOKUP, Helpers.class, "arrayOf"); + + binder = SmartBinder.from(site.signature); + + binder = permuteForGenericCall(binder, method, "context", "self", "args") + .fold("args2", fold); + binder = permuteForGenericCall(binder, method, "context", "self", "args2") + .insert(2, + new String[]{"rubyClass", "name"} + , new Class[]{RubyModule.class, String.class}, + entry.sourceModule, + site.name()) + .insert(0, "method", DynamicMethod.class, method); + } + + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(site.name() + "\tbound to method_missing for " + method + ", " + Bootstrap.logMethod(method)); + } + + return binder.invokeVirtualQuiet(LOOKUP, "call").handle(); + } + + MethodHandle buildAttrHandle(CacheEntry entry, IRubyObject self) { + DynamicMethod method = entry.method; + + if (method instanceof AttrReaderMethod && arity == 0) { + AttrReaderMethod attrReader = (AttrReaderMethod) method; + String varName = attrReader.getVariableName(); + + // we getVariableAccessorForWrite here so it is eagerly created and we don't cache the DUMMY + VariableAccessor accessor = self.getType().getVariableAccessorForWrite(varName); + + // Ruby to attr reader + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + if (accessor instanceof FieldVariableAccessor) { + LOG.info(name() + "\tbound as field attr reader " + Bootstrap.logMethod(method) + ":" + ((AttrReaderMethod)method).getVariableName()); + } else { + LOG.info(name() + "\tbound as attr reader " + Bootstrap.logMethod(method) + ":" + ((AttrReaderMethod)method).getVariableName()); + } + } + + return createAttrReaderHandle(self, self.getType(), accessor); + } else if (method instanceof AttrWriterMethod && arity == 1) { + AttrWriterMethod attrReader = (AttrWriterMethod)method; + String varName = attrReader.getVariableName(); + + // we getVariableAccessorForWrite here so it is eagerly created and we don't cache the DUMMY + VariableAccessor accessor = self.getType().getVariableAccessorForWrite(varName); + + // Ruby to attr reader + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + if (accessor instanceof FieldVariableAccessor) { + LOG.info(name() + "\tbound as field attr writer " + Bootstrap.logMethod(method) + ":" + ((AttrWriterMethod) method).getVariableName()); + } else { + LOG.info(name() + "\tbound as attr writer " + Bootstrap.logMethod(method) + ":" + ((AttrWriterMethod) method).getVariableName()); + } + } + + return createAttrWriterHandle(self, self.getType(), accessor); + } + + return null; + } + + private MethodHandle createAttrReaderHandle(IRubyObject self, RubyClass cls, VariableAccessor accessor) { + MethodHandle nativeTarget; + + MethodHandle filter = cls.getClassRuntime().getNullToNilHandle(); + + MethodHandle getValue; + + if (accessor instanceof FieldVariableAccessor) { + MethodHandle getter = ((FieldVariableAccessor)accessor).getGetter(); + getValue = Binder.from(type()) + .drop(0, argOffset - 1) + .filterReturn(filter) + .cast(methodType(Object.class, self.getClass())) + .invoke(getter); + } else { + getValue = Binder.from(type()) + .drop(0, argOffset - 1) + .filterReturn(filter) + .cast(methodType(Object.class, Object.class)) + .prepend(accessor) + .invokeVirtualQuiet(LOOKUP, "get"); + } + + // NOTE: Must not cache the fully-bound handle in the method, since it's specific to this class + + return getValue; + } + + private MethodHandle createAttrWriterHandle(IRubyObject self, RubyClass cls, VariableAccessor accessor) { + MethodHandle nativeTarget; + + MethodHandle filter = Binder + .from(IRubyObject.class, Object.class) + .drop(0) + .constant(cls.getRuntime().getNil()); + + MethodHandle setValue; + + if (accessor instanceof FieldVariableAccessor) { + MethodHandle setter = ((FieldVariableAccessor)accessor).getSetter(); + setValue = Binder.from(type()) + .drop(0, argOffset - 1) + .filterReturn(filter) + .cast(methodType(void.class, self.getClass(), Object.class)) + .invoke(setter); + } else { + setValue = Binder.from(type()) + .drop(0, argOffset - 1) + .filterReturn(filter) + .cast(methodType(void.class, Object.class, Object.class)) + .prepend(accessor) + .invokeVirtualQuiet(LOOKUP, "set"); + } + + return setValue; + } + + MethodHandle buildJittedHandle(CacheEntry entry, boolean blockGiven) { + MethodHandle mh = null; + SmartBinder binder; + CompiledIRMethod compiledIRMethod = null; + DynamicMethod method = entry.method; + RubyModule sourceModule = entry.sourceModule; + + if (method instanceof CompiledIRMethod) { + compiledIRMethod = (CompiledIRMethod)method; + } else if (method instanceof MixedModeIRMethod) { + DynamicMethod actualMethod = ((MixedModeIRMethod)method).getActualMethod(); + if (actualMethod instanceof CompiledIRMethod) { + compiledIRMethod = (CompiledIRMethod) actualMethod; + } else { + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tfailed direct binding due to unjitted method " + Bootstrap.logMethod(method)); + } + } + } + + if (compiledIRMethod != null) { + + // attempt IR direct binding + // TODO: this will have to expand when we start specializing arities + + binder = SmartBinder.from(signature) + .permute("context", "self", "arg.*", "block"); + + if (arity == -1) { + // already [], nothing to do + mh = (MethodHandle)compiledIRMethod.getHandle(); + } else if (arity == 0) { + MethodHandle specific; + if ((specific = compiledIRMethod.getHandleFor(arity)) != null) { + mh = specific; + } else { + mh = (MethodHandle)compiledIRMethod.getHandle(); + binder = binder.insert(2, "args", IRubyObject.NULL_ARRAY); + } + } else { + MethodHandle specific; + if ((specific = compiledIRMethod.getHandleFor(arity)) != null) { + mh = specific; + } else { + mh = (MethodHandle) compiledIRMethod.getHandle(); + binder = binder.collect("args", "arg.*", Helpers.constructObjectArrayHandle(arity)); + } + } + + if (!blockGiven) { + binder = binder.append("block", Block.class, Block.NULL_BLOCK); + } + + binder = binder + .insert(1, "scope", StaticScope.class, compiledIRMethod.getStaticScope()) + .append("class", RubyModule.class, sourceModule) + .append("frameName", String.class, name()); + + mh = binder.invoke(mh).handle(); + + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tbound directly to jitted method " + Bootstrap.logMethod(method)); + } + } + + return mh; + } + + MethodHandle buildNativeHandle(CacheEntry entry, boolean blockGiven) { + MethodHandle mh = null; + SmartBinder binder = null; + DynamicMethod method = entry.method; + + if (method instanceof NativeCallMethod && ((NativeCallMethod) method).getNativeCall() != null) { + NativeCallMethod nativeMethod = (NativeCallMethod)method; + DynamicMethod.NativeCall nativeCall = nativeMethod.getNativeCall(); + + DynamicMethod.NativeCall nc = nativeCall; + + if (nc.isJava()) { + return JavaBootstrap.createJavaHandle(this, method); + } else { + int nativeArgCount = getNativeArgCount(method, nativeCall); + + if (nativeArgCount >= 0) { // native methods only support arity 3 + if (nativeArgCount == arity) { + // nothing to do + binder = SmartBinder.from(lookup(), signature); + } else { + // arity mismatch...leave null and use DynamicMethod.call below + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tdid not match the primary arity for a native method " + Bootstrap.logMethod(method)); + } + } + } else { + // varargs + if (arity == -1) { + // ok, already passing [] + binder = SmartBinder.from(lookup(), signature); + } else if (arity == 0) { + // no args, insert dummy + binder = SmartBinder.from(lookup(), signature) + .insert(argOffset, "args", IRubyObject.NULL_ARRAY); + } else { + // 1 or more args, collect into [] + binder = SmartBinder.from(lookup(), signature) + .collect("args", "arg.*", Helpers.constructObjectArrayHandle(arity)); + } + } + + if (binder != null) { + + // clean up non-arguments, ordering, types + if (!nc.hasContext()) { + binder = binder.drop("context"); + } + + if (nc.hasBlock() && !blockGiven) { + binder = binder.append("block", Block.NULL_BLOCK); + } else if (!nc.hasBlock() && blockGiven) { + binder = binder.drop("block"); + } + + if (nc.isStatic()) { + mh = binder + .permute("context", "self", "arg.*", "block") // filter caller + .cast(nc.getNativeReturn(), nc.getNativeSignature()) + .invokeStaticQuiet(LOOKUP, nc.getNativeTarget(), nc.getNativeName()) + .handle(); + } else { + mh = binder + .permute("self", "context", "arg.*", "block") // filter caller, move self + .castArg("self", nc.getNativeTarget()) + .castVirtual(nc.getNativeReturn(), nc.getNativeTarget(), nc.getNativeSignature()) + .invokeVirtualQuiet(LOOKUP, nc.getNativeName()) + .handle(); + } + + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(name() + "\tbound directly to JVM method " + Bootstrap.logMethod(method)); + } + + JRubyMethod anno = nativeCall.getMethod().getAnnotation(JRubyMethod.class); + if (anno != null && anno.frame()) { + mh = InvocationLinker.wrapWithFrameOnly(signature, entry.sourceModule, name(), mh); + } + } + } + } + + return mh; + } + + public static int getNativeArgCount(DynamicMethod method, DynamicMethod.NativeCall nativeCall) { + // if non-Java, must: + // * exactly match arities or both are [] boxed + // * 3 or fewer arguments + return getArgCount(nativeCall.getNativeSignature(), nativeCall.isStatic()); + } + + private static int getArgCount(Class[] args, boolean isStatic) { + int length = args.length; + boolean hasContext = false; + if (isStatic) { + if (args.length > 1 && args[0] == ThreadContext.class) { + length--; + hasContext = true; + } + + // remove self object + assert args.length >= 1; + length--; + + if (args.length > 1 && args[args.length - 1] == Block.class) { + length--; + } + + if (length == 1) { + if (hasContext && args[2] == IRubyObject[].class) { + length = -1; + } else if (args[1] == IRubyObject[].class) { + length = -1; + } + } + } else { + if (args.length > 0 && args[0] == ThreadContext.class) { + length--; + hasContext = true; + } + + if (args.length > 0 && args[args.length - 1] == Block.class) { + length--; + } + + if (length == 1) { + if (hasContext && args[1] == IRubyObject[].class) { + length = -1; + } else if (args[0] == IRubyObject[].class) { + length = -1; + } + } + } + return length; + } + public String name() { return methodName; } @@ -145,7 +625,7 @@ public InvokeSite(MethodType type, String name, CallType callType, boolean liter this.arity = arity; - this.fallback = prepareBinder(true).invokeVirtualQuiet(Bootstrap.LOOKUP, "invoke"); + this.fallback = prepareBinder(true).invokeVirtualQuiet(LOOKUP, "invoke"); } public static CallSite bootstrap(InvokeSite site, MethodHandles.Lookup lookup) { @@ -167,7 +647,7 @@ public IRubyObject invoke(ThreadContext context, IRubyObject caller, IRubyObject // only pass symbol below if we be calling a user-defined method_missing (default ones do it for us) passSymbol = !(entry.method instanceof RubyKernel.MethodMissingMethod || entry.method instanceof Helpers.MethodMissingWrapper); - mh = Bootstrap.buildGenericHandle(this, entry); + mh = buildGenericHandle(entry); } else { mh = getHandle(self, entry); } @@ -190,7 +670,7 @@ public IRubyObject invoke(ThreadContext context, IRubyObject self, IRubyObject[] // only pass symbol below if we be calling a user-defined method_missing (default ones do it for us) passSymbol = !(entry.method instanceof RubyKernel.MethodMissingMethod || entry.method instanceof Helpers.MethodMissingWrapper); - mh = Bootstrap.buildGenericHandle(this, entry); + mh = buildGenericHandle(entry); } else { mh = getHandle(self, entry); } @@ -523,26 +1003,26 @@ protected MethodHandle getHandle(IRubyObject self, CacheEntry entry) throws Thro MethodHandle mh = buildNewInstanceHandle(entry, self); if (mh == null) mh = buildNotEqualHandle(entry, self); - if (mh == null) mh = Bootstrap.buildNativeHandle(this, entry, blockGiven); - if (mh == null) mh = buildJavaFieldHandle(this, entry, self); - if (mh == null) mh = Bootstrap.buildIndyHandle(this, entry); - if (mh == null) mh = Bootstrap.buildJittedHandle(this, entry, blockGiven); - if (mh == null) mh = Bootstrap.buildAttrHandle(this, entry, self); + if (mh == null) mh = buildNativeHandle(entry, blockGiven); + if (mh == null) mh = buildJavaFieldHandle(entry, self); + if (mh == null) mh = buildIndyHandle(entry); + if (mh == null) mh = buildJittedHandle(entry, blockGiven); + if (mh == null) mh = buildAttrHandle(entry, self); if (mh == null) mh = buildAliasHandle(entry, self); if (mh == null) mh = buildStructHandle(entry); - if (mh == null) mh = Bootstrap.buildGenericHandle(this, entry); + if (mh == null) mh = buildGenericHandle(entry); assert mh != null : "we should have a method handle of some sort by now"; return mh; } - MethodHandle buildJavaFieldHandle(InvokeSite site, CacheEntry entry, IRubyObject self) throws Throwable { + MethodHandle buildJavaFieldHandle(CacheEntry entry, IRubyObject self) throws Throwable { DynamicMethod method = entry.method; if (method instanceof InstanceFieldGetter) { // only matching arity - if (site.arity != 0 || site.signature.lastArgType() == Block.class) return null; + if (arity != 0 || signature.lastArgType() == Block.class) return null; Field field = ((InstanceFieldGetter) method).getField(); @@ -565,7 +1045,7 @@ MethodHandle buildJavaFieldHandle(InvokeSite site, CacheEntry entry, IRubyObject .invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy"); fieldHandle = Binder - .from(site.type()) + .from(type()) .permute(2) .filter(0, receiverConverter) .filterReturn(filter) @@ -577,7 +1057,7 @@ MethodHandle buildJavaFieldHandle(InvokeSite site, CacheEntry entry, IRubyObject return fieldHandle; } else if (method instanceof InstanceFieldSetter) { // only matching arity - if (site.arity != 1 || site.signature.lastArgType() == Block.class) return null; + if (arity != 1 || signature.lastArgType() == Block.class) return null; Field field = ((InstanceFieldSetter) method).getField(); @@ -598,7 +1078,7 @@ MethodHandle buildJavaFieldHandle(InvokeSite site, CacheEntry entry, IRubyObject .invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy"); fieldHandle = Binder - .from(site.type()) + .from(type()) .permute(2, 3) .filter(0, receiverConverter) .filterReturn(constant(IRubyObject.class, self.getRuntime().getNil())) @@ -842,7 +1322,7 @@ protected SmartHandle testTarget(IRubyObject self, RubyModule testClass) { .from(signature.changeReturn(boolean.class)) .permute("self") .insert(0, "selfClass", RubyClass.class, testClass) - .invokeStaticQuiet(LOOKUP, Bootstrap.class, "testType"); + .invokeStaticQuiet(LOOKUP, InvokeSite.class, "testType"); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IsNilSite.java b/core/src/main/java/org/jruby/ir/targets/indy/IsNilSite.java new file mode 100644 index 00000000000..77c7bc96c89 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/IsNilSite.java @@ -0,0 +1,60 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.RubyNil; +import org.jruby.runtime.builtin.IRubyObject; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class IsNilSite extends MutableCallSite { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final MethodType TYPE = methodType(boolean.class, IRubyObject.class); + public static final Handle IS_NIL_BOOTSTRAP_HANDLE = new Handle( + Opcodes.H_INVOKESTATIC, + p(IsNilSite.class), + "isNil", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + + private static final MethodHandle INIT_HANDLE = + Binder.from(TYPE.insertParameterTypes(0, IsNilSite.class)).invokeVirtualQuiet(LOOKUP, "init"); + + private static final MethodHandle IS_NIL_HANDLE = + Binder + .from(boolean.class, IRubyObject.class, RubyNil.class) + .invokeStaticQuiet(LOOKUP, IsNilSite.class, "isNil"); + + public IsNilSite() { + super(TYPE); + + setTarget(INIT_HANDLE.bindTo(this)); + } + + public static CallSite isNil(MethodHandles.Lookup lookup, String name, MethodType type) { + return new IsNilSite(); + } + + public boolean init(IRubyObject obj) { + IRubyObject nil = obj.getRuntime().getNil(); + + setTarget(insertArguments(IS_NIL_HANDLE, 1, nil)); + + return nil == obj; + } + + public static boolean isNil(IRubyObject obj, RubyNil nil) { + return nil == obj; + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IsTrueSite.java b/core/src/main/java/org/jruby/ir/targets/indy/IsTrueSite.java new file mode 100644 index 00000000000..451313f261a --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/IsTrueSite.java @@ -0,0 +1,64 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.Ruby; +import org.jruby.RubyBoolean; +import org.jruby.RubyNil; +import org.jruby.runtime.builtin.IRubyObject; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class IsTrueSite extends MutableCallSite { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final MethodType TYPE = methodType(boolean.class, IRubyObject.class); + + public static final MethodHandle INIT_HANDLE = Binder.from(TYPE.insertParameterTypes(0, IsTrueSite.class)).invokeVirtualQuiet(LOOKUP, "init"); + public static final Handle IS_TRUE_BOOTSTRAP_HANDLE = new Handle( + Opcodes.H_INVOKESTATIC, + p(IsTrueSite.class), + "isTrue", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + + private static final MethodHandle IS_TRUE_HANDLE = + Binder + .from(boolean.class, IRubyObject.class, RubyNil.class, RubyBoolean.False.class) + .invokeStaticQuiet(LOOKUP, IsTrueSite.class, "isTruthy"); + + public IsTrueSite() { + super(TYPE); + + setTarget(INIT_HANDLE.bindTo(this)); + } + + public static CallSite isTrue(MethodHandles.Lookup lookup, String name, MethodType type) { + return new IsTrueSite(); + } + + public boolean init(IRubyObject obj) { + Ruby runtime = obj.getRuntime(); + + IRubyObject nil = runtime.getNil(); + IRubyObject fals = runtime.getFalse(); + + setTarget(insertArguments(IS_TRUE_HANDLE, 1, nil, fals)); + + return nil != obj && fals != obj; + } + + public static boolean isTruthy(IRubyObject obj, RubyNil nil, RubyBoolean.False fals) { + return nil != obj && fals != obj; + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/JavaBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/JavaBootstrap.java new file mode 100644 index 00000000000..0b4c937ae51 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/JavaBootstrap.java @@ -0,0 +1,292 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.Ruby; +import org.jruby.RubyBignum; +import org.jruby.RubyBoolean; +import org.jruby.RubyFixnum; +import org.jruby.RubyFloat; +import org.jruby.RubyString; +import org.jruby.internal.runtime.methods.DynamicMethod; +import org.jruby.internal.runtime.methods.NativeCallMethod; +import org.jruby.java.invokers.SingletonMethodInvoker; +import org.jruby.javasupport.JavaUtil; +import org.jruby.javasupport.proxy.ReifiedJavaProxy; +import org.jruby.runtime.Block; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ByteList; +import org.jruby.util.CodegenUtils; +import org.jruby.util.cli.Options; +import org.jruby.util.log.Logger; +import org.jruby.util.log.LoggerFactory; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.math.BigInteger; + +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.explicitCastArguments; +import static java.lang.invoke.MethodHandles.filterArguments; +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.runtime.invokedynamic.InvokeDynamicSupport.findStatic; +import static org.jruby.runtime.invokedynamic.InvokeDynamicSupport.findVirtual; + +public class JavaBootstrap { + //////////////////////////////////////////////////////////////////////////// + // Dispatch from Ruby to Java via Java integration + //////////////////////////////////////////////////////////////////////////// + + private static final Logger LOG = LoggerFactory.getLogger(JavaBootstrap.class); + + public static boolean subclassProxyTest(Object target) { + return target instanceof ReifiedJavaProxy; + } + + private static final MethodHandle IS_JAVA_SUBCLASS = findStatic(JavaBootstrap.class, "subclassProxyTest", methodType(boolean.class, Object.class)); + + private static Object nullValue(Class type) { + if (type == boolean.class || type == Boolean.class) return false; + if (type == byte.class || type == Byte.class) return (byte)0; + if (type == short.class || type == Short.class) return (short)0; + if (type == int.class || type == Integer.class) return 0; + if (type == long.class || type == Long.class) return 0L; + if (type == float.class || type == Float.class) return 0.0F; + if (type == double.class || type == Double.class)return 0.0; + return null; + } + + public static MethodHandle createJavaHandle(InvokeSite site, DynamicMethod method) { + if (!(method instanceof NativeCallMethod)) return null; + + DynamicMethod.NativeCall nativeCall = ((NativeCallMethod) method).getNativeCall(); + + if (nativeCall == null) return null; + + boolean isStatic = nativeCall.isStatic(); + + // This logic does not handle closure conversion yet + if (site.signature.lastArgType() == Block.class) { + if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) { + LOG.info(site.name() + "\tpassed a closure to Java method " + nativeCall + ": " + Bootstrap.logMethod(method)); + } + return null; + } + + // mismatched arity not supported + if (site.arity != nativeCall.getNativeSignature().length) { + return null; + } + + // varargs broken, so ignore methods that take a trailing array + Class[] signature = nativeCall.getNativeSignature(); + if (signature.length > 0 && signature[signature.length - 1].isArray()) { + return null; + } + + // Scala singletons have slightly different JI logic, so skip for now + if (method instanceof SingletonMethodInvoker) return null; + + // get prepared handle if previously cached + MethodHandle nativeTarget = (MethodHandle)method.getHandle(); + + if (nativeTarget == null) { + // no cache, proceed to construct the adapted handle + + // the "apparent" type from the NativeCall, excluding receiver + MethodType apparentType = methodType(nativeCall.getNativeReturn(), nativeCall.getNativeSignature()); + + if (isStatic) { + nativeTarget = findStatic(nativeCall.getNativeTarget(), nativeCall.getNativeName(), apparentType); + } else { + nativeTarget = findVirtual(nativeCall.getNativeTarget(), nativeCall.getNativeName(), apparentType); + } + + // the actual native type with receiver + MethodType nativeType = nativeTarget.type(); + Class[] nativeParams = nativeType.parameterArray(); + Class nativeReturn = nativeType.returnType(); + + // convert arguments + MethodHandle[] argConverters = new MethodHandle[nativeType.parameterCount()]; + for (int i = 0; i < argConverters.length; i++) { + MethodHandle converter; + if (!isStatic && i == 0) { + // handle non-static receiver specially + converter = Binder + .from(nativeParams[0], IRubyObject.class) + .cast(Object.class, IRubyObject.class) + .invokeStaticQuiet(lookup(), JavaUtil.class, "objectFromJavaProxy"); + } else { + // all other arguments use toJava + converter = Binder + .from(nativeParams[i], IRubyObject.class) + .insert(1, nativeParams[i]) + .cast(Object.class, IRubyObject.class, Class.class) + .invokeVirtualQuiet(lookup(), "toJava"); + } + argConverters[i] = converter; + } + nativeTarget = filterArguments(nativeTarget, 0, argConverters); + + Ruby runtime = method.getImplementationClass().getRuntime(); + Class[] convertedParams = CodegenUtils.params(IRubyObject.class, nativeTarget.type().parameterCount()); + + // handle return value + MethodHandle returnFilter; + if (nativeReturn == byte.class + || nativeReturn == short.class + || nativeReturn == char.class + || nativeReturn == int.class + || nativeReturn == long.class) { + // native integral type, produce a Fixnum + nativeTarget = explicitCastArguments(nativeTarget, methodType(long.class, convertedParams)); + returnFilter = insertArguments( + findStatic(RubyFixnum.class, "newFixnum", methodType(RubyFixnum.class, Ruby.class, long.class)), + 0, + runtime); + } else if (nativeReturn == Byte.class + || nativeReturn == Short.class + || nativeReturn == Character.class + || nativeReturn == Integer.class + || nativeReturn == Long.class) { + // boxed integral type, produce a Fixnum or nil + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "fixnumOrNil", methodType(IRubyObject.class, Ruby.class, nativeReturn)), + 0, + runtime); + } else if (nativeReturn == float.class + || nativeReturn == double.class) { + // native decimal type, produce a Float + nativeTarget = explicitCastArguments(nativeTarget, methodType(double.class, convertedParams)); + returnFilter = insertArguments( + findStatic(RubyFloat.class, "newFloat", methodType(RubyFloat.class, Ruby.class, double.class)), + 0, + runtime); + } else if (nativeReturn == Float.class + || nativeReturn == Double.class) { + // boxed decimal type, produce a Float or nil + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "floatOrNil", methodType(IRubyObject.class, Ruby.class, nativeReturn)), + 0, + runtime); + } else if (nativeReturn == boolean.class) { + // native boolean type, produce a Boolean + nativeTarget = explicitCastArguments(nativeTarget, methodType(boolean.class, convertedParams)); + returnFilter = insertArguments( + findStatic(RubyBoolean.class, "newBoolean", methodType(RubyBoolean.class, Ruby.class, boolean.class)), + 0, + runtime); + } else if (nativeReturn == Boolean.class) { + // boxed boolean type, produce a Boolean or nil + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "booleanOrNil", methodType(IRubyObject.class, Ruby.class, Boolean.class)), + 0, + runtime); + } else if (CharSequence.class.isAssignableFrom(nativeReturn)) { + // character sequence, produce a String or nil + nativeTarget = explicitCastArguments(nativeTarget, methodType(CharSequence.class, convertedParams)); + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "stringOrNil", methodType(IRubyObject.class, Ruby.class, CharSequence.class)), + 0, + runtime); + } else if (nativeReturn == void.class) { + // void return, produce nil + returnFilter = constant(IRubyObject.class, runtime.getNil()); + } else if (nativeReturn == ByteList.class) { + // bytelist, produce a String or nil + nativeTarget = explicitCastArguments(nativeTarget, methodType(ByteList.class, convertedParams)); + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "stringOrNil", methodType(IRubyObject.class, Ruby.class, ByteList.class)), + 0, + runtime); + } else if (nativeReturn == BigInteger.class) { + // bytelist, produce a String or nil + nativeTarget = explicitCastArguments(nativeTarget, methodType(BigInteger.class, convertedParams)); + returnFilter = insertArguments( + findStatic(JavaBootstrap.class, "bignumOrNil", methodType(IRubyObject.class, Ruby.class, BigInteger.class)), + 0, + runtime); + } else { + // all other object types + nativeTarget = explicitCastArguments(nativeTarget, methodType(Object.class, convertedParams)); + returnFilter = insertArguments( + findStatic(JavaUtil.class, "convertJavaToUsableRubyObject", methodType(IRubyObject.class, Ruby.class, Object.class)), + 0, + runtime); + } + + nativeTarget = MethodHandles.filterReturnValue(nativeTarget, returnFilter); + + // cache adapted handle + method.setHandle(nativeTarget); + } + + // perform final adaptation by dropping JRuby-specific arguments + int dropCount; + if (isStatic) { + if (site.functional) { + dropCount = 2; // context, self + } else { + dropCount = 3; // context, caller, self + } + } else { + if (site.functional) { + dropCount = 1; // context + } else { + dropCount = 2; // context, caller + } + } + + return Binder + .from(site.type()) + .drop(0, dropCount) + .invoke(nativeTarget); + } + + public static IRubyObject fixnumOrNil(Ruby runtime, Byte b) { + return b == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, b); + } + + public static IRubyObject fixnumOrNil(Ruby runtime, Short s) { + return s == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, s); + } + + public static IRubyObject fixnumOrNil(Ruby runtime, Character c) { + return c == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, c); + } + + public static IRubyObject fixnumOrNil(Ruby runtime, Integer i) { + return i == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, i); + } + + public static IRubyObject fixnumOrNil(Ruby runtime, Long l) { + return l == null ? runtime.getNil() : RubyFixnum.newFixnum(runtime, l); + } + + public static IRubyObject floatOrNil(Ruby runtime, Float f) { + return f == null ? runtime.getNil() : RubyFloat.newFloat(runtime, f); + } + + public static IRubyObject floatOrNil(Ruby runtime, Double d) { + return d == null ? runtime.getNil() : RubyFloat.newFloat(runtime, d); + } + + public static IRubyObject booleanOrNil(Ruby runtime, Boolean b) { + return b == null ? runtime.getNil() : RubyBoolean.newBoolean(runtime, b); + } + + public static IRubyObject stringOrNil(Ruby runtime, CharSequence cs) { + return cs == null ? runtime.getNil() : RubyString.newUnicodeString(runtime, cs); + } + + public static IRubyObject stringOrNil(Ruby runtime, ByteList bl) { + return bl == null ? runtime.getNil() : RubyString.newString(runtime, bl); + } + + public static IRubyObject bignumOrNil(Ruby runtime, BigInteger bi) { + return bi == null ? runtime.getNil() : RubyBignum.newBignum(runtime, bi); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/LiteralValueBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/LiteralValueBootstrap.java new file mode 100644 index 00000000000..781fc15611c --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/LiteralValueBootstrap.java @@ -0,0 +1,175 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jcodings.Encoding; +import org.jruby.Ruby; +import org.jruby.RubyEncoding; +import org.jruby.RubyNil; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.opto.OptoFactory; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class LiteralValueBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Handle CONTEXT_VALUE_HANDLE = new Handle( + Opcodes.H_INVOKESTATIC, + p(LiteralValueBootstrap.class), + "contextValue", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class), + false); + public static final Handle CONTEXT_VALUE_STRING_HANDLE = new Handle( + Opcodes.H_INVOKESTATIC, + p(LiteralValueBootstrap.class), + "contextValueString", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class), + false); + + private static final MethodHandle RUNTIME_HANDLE = + Binder + .from(Ruby.class, ThreadContext.class, MutableCallSite.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "runtime"); + private static final MethodHandle NIL_HANDLE = + Binder + .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "nil"); + private static final MethodHandle TRUE_HANDLE = + Binder + .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "True"); + private static final MethodHandle FALSE_HANDLE = + Binder + .from(IRubyObject.class, ThreadContext.class, MutableCallSite.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "False"); + private static final MethodHandle RUBY_ENCODING_HANDLE = + Binder + .from(RubyEncoding.class, ThreadContext.class, MutableCallSite.class, String.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "rubyEncoding"); + private static final MethodHandle ENCODING_HANDLE = + Binder + .from(Encoding.class, ThreadContext.class, MutableCallSite.class, String.class) + .invokeStaticQuiet(LOOKUP, LiteralValueBootstrap.class, "encoding"); + + public static CallSite contextValue(MethodHandles.Lookup lookup, String name, MethodType type) { + MutableCallSite site = new MutableCallSite(type); + + MethodHandle dmh; + switch (name) { + case "runtime": + dmh = RUNTIME_HANDLE; + break; + case "nil": + dmh = NIL_HANDLE; + break; + case "True": + dmh = TRUE_HANDLE; + break; + case "False": + dmh = FALSE_HANDLE; + break; + case "rubyEncoding": + dmh = RUBY_ENCODING_HANDLE; + break; + case "encoding": + dmh = ENCODING_HANDLE; + break; + default: + throw new RuntimeException("BUG: invalid context value " + name); + } + + site.setTarget(Binder.from(type).append(site).invoke(dmh)); + + return site; + } + + public static CallSite contextValueString(MethodHandles.Lookup lookup, String name, MethodType type, String str) { + MutableCallSite site = new MutableCallSite(type); + + MethodHandle dmh; + switch (name) { + case "rubyEncoding": + dmh = RUBY_ENCODING_HANDLE; + break; + case "encoding": + dmh = ENCODING_HANDLE; + break; + default: + throw new RuntimeException("BUG: invalid context value " + name); + } + + site.setTarget(Binder.from(type).append(site, str).invoke(dmh)); + return site; + } + + public static IRubyObject nil(ThreadContext context, MutableCallSite site) { + RubyNil nil = (RubyNil) context.nil; + + MethodHandle constant = (MethodHandle) nil.constant(); + if (constant == null) constant = (MethodHandle) OptoFactory.newConstantWrapper(IRubyObject.class, context.nil); + + site.setTarget(constant); + + return nil; + } + + public static IRubyObject True(ThreadContext context, MutableCallSite site) { + MethodHandle constant = (MethodHandle)context.tru.constant(); + if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(IRubyObject.class, context.tru); + + site.setTarget(constant); + + return context.tru; + } + + public static IRubyObject False(ThreadContext context, MutableCallSite site) { + MethodHandle constant = (MethodHandle)context.fals.constant(); + if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(IRubyObject.class, context.fals); + + site.setTarget(constant); + + return context.fals; + } + + public static Ruby runtime(ThreadContext context, MutableCallSite site) { + MethodHandle constant = (MethodHandle)context.runtime.constant(); + if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(Ruby.class, context.runtime); + + site.setTarget(constant); + + return context.runtime; + } + + public static RubyEncoding rubyEncoding(ThreadContext context, MutableCallSite site, String name) { + RubyEncoding rubyEncoding = IRRuntimeHelpers.retrieveEncoding(context, name); + + MethodHandle constant = (MethodHandle)rubyEncoding.constant(); + if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(RubyEncoding.class, rubyEncoding); + + site.setTarget(constant); + + return rubyEncoding; + } + + public static Encoding encoding(ThreadContext context, MutableCallSite site, String name) { + Encoding encoding = IRRuntimeHelpers.retrieveJCodingsEncoding(context, name); + + MethodHandle constant = MethodHandles.constant(Encoding.class, encoding); + if (constant == null) constant = (MethodHandle)OptoFactory.newConstantWrapper(Encoding.class, encoding); + + site.setTarget(constant); + + return encoding; + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/MetaClassBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/MetaClassBootstrap.java new file mode 100644 index 00000000000..45e4f6dd417 --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/MetaClassBootstrap.java @@ -0,0 +1,57 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jruby.internal.runtime.methods.DynamicMethod; +import org.jruby.ir.JIT; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.parser.StaticScope; +import org.jruby.runtime.Helpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodHandles.insertArguments; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class MetaClassBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Handle OPEN_META_CLASS = new Handle( + Opcodes.H_INVOKESTATIC, + p(MetaClassBootstrap.class), + "openMetaClass", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, MethodHandle.class, MethodHandle.class, MethodHandle.class, int.class, int.class, int.class), + false); + private static final MethodHandle OPEN_META_CLASS_HANDLE = + Binder + .from(DynamicMethod.class, ThreadContext.class, IRubyObject.class, String.class, StaticScope.class, MethodHandle.class, StaticScope.class, MethodHandle.class, int.class, boolean.class, boolean.class) + .invokeStaticQuiet(LOOKUP, MetaClassBootstrap.class, "openMetaClass"); + + @JIT + public static CallSite openMetaClass(MethodHandles.Lookup lookup, String name, MethodType type, MethodHandle body, MethodHandle scope, MethodHandle setScope, int line, int dynscopeEliminated, int refinements) { + try { + StaticScope staticScope = (StaticScope) scope.invokeExact(); + return new ConstantCallSite(insertArguments(OPEN_META_CLASS_HANDLE, 4, body, staticScope, setScope, line, dynscopeEliminated == 1 ? true : false, refinements == 1 ? true : false)); + } catch (Throwable t) { + Helpers.throwException(t); + return null; + } + } + + @JIT + public static DynamicMethod openMetaClass(ThreadContext context, IRubyObject object, String descriptor, StaticScope parent, MethodHandle body, StaticScope scope, MethodHandle setScope, int line, boolean dynscopeEliminated, boolean refinements) throws Throwable { + if (scope == null) { + scope = Helpers.restoreScope(descriptor, parent); + setScope.invokeExact(scope); + } + return IRRuntimeHelpers.newCompiledMetaClass(context, body, scope, object, line, dynscopeEliminated, refinements); + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/indy/RegexpObjectSite.java b/core/src/main/java/org/jruby/ir/targets/indy/RegexpObjectSite.java index 84aa08f1a67..076962558c1 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/RegexpObjectSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/RegexpObjectSite.java @@ -38,7 +38,7 @@ public RegexpObjectSite(MethodType type, ByteList pattern, int embeddedOptions) false); public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String value, String encodingName, int options) { - return new RegexpObjectSite(type, Bootstrap.bytelist(value, encodingName), options).bootstrap(lookup); + return new RegexpObjectSite(type, StringBootstrap.bytelist(value, encodingName), options).bootstrap(lookup); } // normal regexp diff --git a/core/src/main/java/org/jruby/ir/targets/indy/StringBootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/StringBootstrap.java new file mode 100644 index 00000000000..b4f0c93ca4b --- /dev/null +++ b/core/src/main/java/org/jruby/ir/targets/indy/StringBootstrap.java @@ -0,0 +1,142 @@ +package org.jruby.ir.targets.indy; + +import com.headius.invokebinder.Binder; +import org.jcodings.Encoding; +import org.jcodings.EncodingDB; +import org.jruby.RubyEncoding; +import org.jruby.RubyString; +import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.runtime.ThreadContext; +import org.jruby.util.ByteList; +import org.jruby.util.StringSupport; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.insertArguments; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; + +public class StringBootstrap { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Handle BYTELIST_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(StringBootstrap.class), + "bytelist", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, String.class), + false); + public static final Handle STRING_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(StringBootstrap.class), + "string", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, String.class, int.class), + false); + public static final Handle EMPTY_STRING_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(StringBootstrap.class), + "emptyString", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class), + false); + public static final Handle BUFFER_STRING_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(StringBootstrap.class), + "bufferString", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, int.class), + false); + public static final Handle FSTRING_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(StringBootstrap.class), + "fstring", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, String.class, int.class, String.class, int.class), + false); + + private static final MethodHandle STRING_HANDLE = + Binder + .from(RubyString.class, ThreadContext.class, ByteList.class, int.class) + .invokeStaticQuiet(LOOKUP, StringBootstrap.class, "string"); + private static final MethodHandle FSTRING_HANDLE = + Binder + .from(RubyString.class, ThreadContext.class, MutableCallSite.class, ByteList.class, int.class, String.class, int.class) + .invokeStaticQuiet(LOOKUP, StringBootstrap.class, "frozenString"); + private static final MethodHandle BUFFERSTRING_HANDLE = + Binder + .from(RubyString.class, ThreadContext.class, Encoding.class, int.class, int.class) + .invokeStaticQuiet(LOOKUP, StringBootstrap.class, "bufferString"); + + public static CallSite bytelist(MethodHandles.Lookup lookup, String name, MethodType type, String value, String encodingName) { + return new ConstantCallSite(constant(ByteList.class, bytelist(value, encodingName))); + } + + public static CallSite string(MethodHandles.Lookup lookup, String name, MethodType type, String value, String encodingName, int cr) { + return new ConstantCallSite(insertArguments(STRING_HANDLE, 1, bytelist(value, encodingName), cr)); + } + + public static CallSite emptyString(MethodHandles.Lookup lookup, String name, MethodType type, String encodingName) { + RubyString.EmptyByteListHolder holder = RubyString.getEmptyByteList(encodingFromName(encodingName)); + return new ConstantCallSite(insertArguments(STRING_HANDLE, 1, holder.bytes, holder.cr)); + } + + public static CallSite bufferString(MethodHandles.Lookup lookup, String name, MethodType type, String encodingName, int size) { + return new ConstantCallSite(insertArguments(BUFFERSTRING_HANDLE, 1, encodingFromName(encodingName), size, StringSupport.CR_7BIT)); + } + + public static CallSite fstring(MethodHandles.Lookup lookup, String name, MethodType type, String value, String encodingName, int cr, String file, int line) { + MutableCallSite site = new MutableCallSite(type); + + site.setTarget(insertArguments(FSTRING_HANDLE, 1, site, bytelist(value, encodingName), cr, file, line)); + + return site; + } + + public static RubyString string(ThreadContext context, ByteList value, int cr) { + return RubyString.newStringShared(context.runtime, value, cr); + } + + public static RubyString bufferString(ThreadContext context, Encoding encoding, int size, int cr) { + return RubyString.newString(context.runtime, new ByteList(size, encoding), cr); + } + + public static RubyString frozenString(ThreadContext context, MutableCallSite site, ByteList value, int cr, String file, int line) { + RubyString frozen = IRRuntimeHelpers.newFrozenString(context, value, cr, file, line); + + // Permanently bind to the new frozen string + site.setTarget(dropArguments(constant(RubyString.class, frozen), 0, ThreadContext.class)); + + return frozen; + } + + public static ByteList bytelist(String value, String encodingName) { + Encoding encoding = encodingFromName(encodingName); + + if (value.length() == 0) { + // special case empty string and don't create a new BL + return RubyString.getEmptyByteList(encoding).bytes; + } + + return new ByteList(RubyEncoding.encodeISO(value), encoding, false); + } + + public static ByteList bytelist(int size, String encodingName) { + Encoding encoding = encodingFromName(encodingName); + + return new ByteList(size, encoding); + } + + private static Encoding encodingFromName(String encodingName) { + Encoding encoding; + EncodingDB.Entry entry = EncodingDB.getEncodings().get(encodingName.getBytes()); + if (entry == null) entry = EncodingDB.getAliases().get(encodingName.getBytes()); + if (entry == null) throw new RuntimeException("could not find encoding: " + encodingName); + encoding = entry.getEncoding(); + return encoding; + } +} diff --git a/core/src/main/java/org/jruby/ir/targets/simple/NormalBranchCompiler.java b/core/src/main/java/org/jruby/ir/targets/simple/NormalBranchCompiler.java index 499d8c71521..35e7d9edb2f 100644 --- a/core/src/main/java/org/jruby/ir/targets/simple/NormalBranchCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/simple/NormalBranchCompiler.java @@ -4,6 +4,7 @@ import org.jruby.ir.targets.BranchCompiler; import org.jruby.ir.targets.IRBytecodeAdapter; import org.jruby.ir.targets.indy.Bootstrap; +import org.jruby.ir.targets.indy.CheckArityBootstrap; import org.jruby.parser.StaticScope; import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; @@ -55,10 +56,11 @@ public void checkArgsArity(Runnable args, int required, int opt, boolean rest) { public void checkArity(int required, int opt, boolean rest, int restKey) { compiler.adapter.ldc(required); + compiler.adapter.ldc(opt); compiler.adapter.ldc(rest); compiler.adapter.ldc(restKey); - compiler.adapter.invokestatic(p(Bootstrap.class), "checkArity", sig(void.class, params(ThreadContext.class, StaticScope.class, Object[].class, Object.class, Block.class, int.class, int.class, boolean.class, int.class))); + compiler.adapter.invokestatic(p(CheckArityBootstrap.class), "checkArity", sig(void.class, params(ThreadContext.class, StaticScope.class, Object[].class, Object.class, Block.class, int.class, int.class, boolean.class, int.class))); } public void checkAritySpecificArgs(int required, int opt, boolean rest, int restKey) { @@ -66,6 +68,6 @@ public void checkAritySpecificArgs(int required, int opt, boolean rest, int rest compiler.adapter.ldc(opt); compiler.adapter.ldc(rest); compiler.adapter.ldc(restKey); - compiler.adapter.invokestatic(p(Bootstrap.class), "checkAritySpecificArgs", sig(void.class, params(ThreadContext.class, StaticScope.class, Object[].class, Block.class, int.class, int.class, boolean.class, int.class))); + compiler.adapter.invokestatic(p(CheckArityBootstrap.class), "checkAritySpecificArgs", sig(void.class, params(ThreadContext.class, StaticScope.class, Object[].class, Block.class, int.class, int.class, boolean.class, int.class))); } } diff --git a/core/src/main/java/org/jruby/java/proxies/ConcreteJavaProxy.java b/core/src/main/java/org/jruby/java/proxies/ConcreteJavaProxy.java index d3dcc16716d..759b5e1d06b 100644 --- a/core/src/main/java/org/jruby/java/proxies/ConcreteJavaProxy.java +++ b/core/src/main/java/org/jruby/java/proxies/ConcreteJavaProxy.java @@ -397,6 +397,7 @@ private static boolean isClassOrIncludedPrependedModule(final RubyModule methodS RubyClass candidate = klass.getSuperClass(); while (candidate != null && (candidate.isIncluded() || candidate.isPrepended())) { // up till 'real' superclass if (candidate == klass) return true; + candidate = candidate.getSuperClass(); } return false; diff --git a/core/src/main/java/org/jruby/runtime/Helpers.java b/core/src/main/java/org/jruby/runtime/Helpers.java index c836b06e930..1e99920cceb 100644 --- a/core/src/main/java/org/jruby/runtime/Helpers.java +++ b/core/src/main/java/org/jruby/runtime/Helpers.java @@ -2926,6 +2926,18 @@ public static IRubyObject[] arrayOf(IRubyObject first, IRubyObject... values) { return newValues; } + public static IRubyObject[] arrayOf(IRubyObject first) { + return new IRubyObject[] {first}; + } + + public static IRubyObject[] arrayOf(IRubyObject first, IRubyObject second) { + return new IRubyObject[] {first, second}; + } + + public static IRubyObject[] arrayOf(IRubyObject first, IRubyObject second, IRubyObject third) { + return new IRubyObject[] {first, second, third}; + } + public static T[] arrayOf(T[] values, T last, IntFunction allocator) { T[] newValues = allocator.apply(values.length + 1); newValues[values.length] = last; diff --git a/core/src/main/java/org/jruby/runtime/invokedynamic/GlobalSite.java b/core/src/main/java/org/jruby/runtime/invokedynamic/GlobalSite.java index 3a8f79e6a34..b44bbf16819 100644 --- a/core/src/main/java/org/jruby/runtime/invokedynamic/GlobalSite.java +++ b/core/src/main/java/org/jruby/runtime/invokedynamic/GlobalSite.java @@ -1,16 +1,40 @@ package org.jruby.runtime.invokedynamic; +import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandle; import org.jruby.Ruby; -import org.jruby.RubyClass; -import org.jruby.common.IRubyWarnings; +import org.jruby.RubyGlobal; +import org.jruby.internal.runtime.GlobalVariable; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.opto.Invalidator; +import org.jruby.util.JavaNameMangler; +import org.jruby.util.cli.Options; +import org.jruby.util.log.Logger; +import org.jruby.util.log.LoggerFactory; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.invoke.MutableCallSite; +import java.lang.invoke.SwitchPoint; + +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.methodType; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; public class GlobalSite extends MutableCallSite { + public static final Handle GLOBAL_BOOTSTRAP_H = new Handle( + Opcodes.H_INVOKESTATIC, + p(GlobalSite.class), + "globalBootstrap", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, int.class), + false); + private static final Logger LOG = LoggerFactory.getLogger(GlobalSite.class); private final String name; private volatile int failures; private final String file; @@ -22,16 +46,16 @@ public GlobalSite(MethodType type, String name, String file, int line) { this.file = file; this.line = line; } - + public void setTarget(MethodHandle target) { super.setTarget(target); incrementFailures(); } - + public int failures() { return failures; } - + public void incrementFailures() { failures += 1; } @@ -43,4 +67,79 @@ public String name() { public String file() { return file; } public int line() { return line; } + + public static CallSite globalBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String file, int line) throws Throwable { + String[] names = name.split(":"); + String operation = names[0]; + String varName = JavaNameMangler.demangleMethodName(names[1]); + GlobalSite site = new GlobalSite(type, varName, file, line); + MethodHandle handle; + + if (operation.equals("get")) { + handle = lookup.findVirtual(GlobalSite.class, "getGlobalFallback", methodType(IRubyObject.class, ThreadContext.class)); + } else { + handle = lookup.findVirtual(GlobalSite.class, "setGlobalFallback", methodType(void.class, IRubyObject.class, ThreadContext.class)); + } + + handle = handle.bindTo(site); + site.setTarget(handle); + + return site; + } + + public IRubyObject getGlobalFallback(ThreadContext context) throws Throwable { + Ruby runtime = context.runtime; + GlobalVariable variable = runtime.getGlobalVariables().getVariable(name()); + + if (failures() > Options.INVOKEDYNAMIC_GLOBAL_MAXFAIL.load() || + variable.getScope() != GlobalVariable.Scope.GLOBAL || + RubyGlobal.UNCACHED_GLOBALS.contains(name())) { + + // use uncached logic forever + if (Options.INVOKEDYNAMIC_LOG_GLOBALS.load()) LOG.info("global " + name() + " (" + file() + ":" + line() + ") uncacheable or rebound > " + Options.INVOKEDYNAMIC_GLOBAL_MAXFAIL.load() + " times, reverting to simple lookup"); + + MethodHandle uncached = lookup().findStatic(GlobalSite.class, "getGlobalUncached", methodType(IRubyObject.class, GlobalVariable.class)); + uncached = uncached.bindTo(variable); + uncached = dropArguments(uncached, 0, ThreadContext.class); + setTarget(uncached); + return (IRubyObject)uncached.invokeWithArguments(context); + } + + Invalidator invalidator = variable.getInvalidator(); + IRubyObject value = variable.getAccessor().getValue(); + + MethodHandle target = constant(IRubyObject.class, value); + target = dropArguments(target, 0, ThreadContext.class); + MethodHandle fallback = lookup().findVirtual(GlobalSite.class, "getGlobalFallback", methodType(IRubyObject.class, ThreadContext.class)); + fallback = fallback.bindTo(this); + + target = ((SwitchPoint)invalidator.getData()).guardWithTest(target, fallback); + + setTarget(target); + +// if (Options.INVOKEDYNAMIC_LOG_GLOBALS.load()) LOG.info("global " + name() + " (" + file() + ":" + line() + ") cached"); + + return value; + } + + public static IRubyObject getGlobalUncached(GlobalVariable variable) throws Throwable { + return variable.getAccessor().getValue(); + } + + public void setGlobalFallback(IRubyObject value, ThreadContext context) throws Throwable { + Ruby runtime = context.runtime; + GlobalVariable variable = runtime.getGlobalVariables().getVariable(name()); + MethodHandle uncached = lookup().findStatic(GlobalSite.class, "setGlobalUncached", methodType(void.class, GlobalVariable.class, IRubyObject.class)); + uncached = uncached.bindTo(variable); + uncached = dropArguments(uncached, 1, ThreadContext.class); + setTarget(uncached); + uncached.invokeWithArguments(value, context); + } + + public static void setGlobalUncached(GlobalVariable variable, IRubyObject value) throws Throwable { + // FIXME: duplicated logic from GlobalVariables.set + variable.getAccessor().setValue(value); + variable.trace(value); + variable.invalidate(); + } } diff --git a/core/src/main/java/org/jruby/runtime/invokedynamic/MathLinker.java b/core/src/main/java/org/jruby/runtime/invokedynamic/MathLinker.java index 5c02c89e8ac..02d1cd816be 100644 --- a/core/src/main/java/org/jruby/runtime/invokedynamic/MathLinker.java +++ b/core/src/main/java/org/jruby/runtime/invokedynamic/MathLinker.java @@ -29,13 +29,12 @@ import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.invoke.SwitchPoint; -import java.util.List; import static java.lang.invoke.MethodHandles.*; import static java.lang.invoke.MethodType.*; +import static org.jruby.util.CodegenUtils.p; import com.headius.invokebinder.Binder; import org.jruby.Ruby; @@ -43,6 +42,7 @@ import org.jruby.RubyFixnum; import org.jruby.RubyFloat; import org.jruby.RubyNumeric; +import org.jruby.ir.targets.indy.Bootstrap; import org.jruby.ir.targets.simple.NormalInvokeSite; import org.jruby.runtime.CallType; import org.jruby.runtime.MethodIndex; @@ -54,11 +54,25 @@ import org.jruby.util.cli.Options; import org.jruby.util.log.Logger; import org.jruby.util.log.LoggerFactory; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; public class MathLinker { private static final Logger LOG = LoggerFactory.getLogger(MathLinker.class); public static final Lookup LOOKUP = lookup(); + public static final Handle FIXNUM_OPERATOR_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(MathLinker.class), + "fixnumOperatorBootstrap", + Bootstrap.BOOTSTRAP_LONG_STRING_INT_SIG, + false); + public static final Handle FLOAT_OPERATOR_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + p(MathLinker.class), + "floatOperatorBootstrap", + Bootstrap.BOOTSTRAP_DOUBLE_STRING_INT_SIG, + false); static { // enable DEBUG output if (Options.INVOKEDYNAMIC_LOG_BINDING.load()) LOG.setDebugEnable(true); @@ -372,5 +386,4 @@ private static CacheEntry searchWithCache(final String operator, IRubyObject cal } return entry; } - } diff --git a/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java b/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java index d16fb7b50fe..58fc8a89631 100644 --- a/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java +++ b/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java @@ -33,7 +33,7 @@ import java.lang.invoke.MethodHandle; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; @@ -258,7 +258,7 @@ VariableAccessor getVariableAccessorWithBuilder(String name, Function newVariableAccessors = new HashMap(myVariableAccessors.size() + 1); + Map newVariableAccessors = new LinkedHashMap<>(myVariableAccessors.size() + 1); newVariableAccessors.putAll(myVariableAccessors); newVariableAccessors.put(name, ivarAccessor); @@ -390,7 +390,7 @@ public int getVariableTableSizeWithExtras() { * @return a map of names to accessors for all variables */ public Map getVariableTableCopy() { - return new HashMap(getVariableAccessorsForRead()); + return new LinkedHashMap(getVariableAccessorsForRead()); } /** diff --git a/core/src/main/java/org/jruby/util/cli/Category.java b/core/src/main/java/org/jruby/util/cli/Category.java index 934ef1e5432..9df89cb3dd3 100644 --- a/core/src/main/java/org/jruby/util/cli/Category.java +++ b/core/src/main/java/org/jruby/util/cli/Category.java @@ -46,7 +46,8 @@ public enum Category { JAVA_INTEGRATION("java integration"), PROFILING("profiling"), CLI("command line options"), - COMPLIANCE("compliance options"); + COMPLIANCE("compliance options"), + EXPERIMENTAL("experimental features"); Category(String desc) { this.desc = desc; diff --git a/core/src/main/java/org/jruby/util/cli/Options.java b/core/src/main/java/org/jruby/util/cli/Options.java index 77492f731d8..c2a542a3885 100644 --- a/core/src/main/java/org/jruby/util/cli/Options.java +++ b/core/src/main/java/org/jruby/util/cli/Options.java @@ -193,6 +193,8 @@ public class Options { public static final Option PROFILE_MAX_METHODS = integer(PROFILING, "profile.max.methods", 100000, "Maximum number of methods to consider for profiling."); + public static final Option FIBER_SCHEDULER = bool(EXPERIMENTAL, "experimental.fiber.scheduler", false, "Enable experimental Fiber::Scheduler support."); + public static final Option CLI_AUTOSPLIT = bool(CLI, "cli.autosplit", false, "Split $_ into $F for -p or -n. Same as -a."); public static final Option CLI_DEBUG = bool(CLI, "cli.debug", false, "Enable debug mode logging. Same as -d."); public static final Option CLI_PROCESS_LINE_ENDS = bool(CLI, "cli.process.line.ends", false, "Enable line ending processing. Same as -l."); diff --git a/core/src/main/java/org/jruby/util/io/FilenoUtil.java b/core/src/main/java/org/jruby/util/io/FilenoUtil.java index 22ddad63b83..795002c79a2 100644 --- a/core/src/main/java/org/jruby/util/io/FilenoUtil.java +++ b/core/src/main/java/org/jruby/util/io/FilenoUtil.java @@ -16,6 +16,7 @@ import org.jruby.javasupport.JavaUtil; import org.jruby.platform.Platform; import org.jruby.runtime.Helpers; +import org.jruby.util.cli.Options; import org.jruby.util.collections.NonBlockingHashMapLong; import org.jruby.util.log.Logger; import org.jruby.util.log.LoggerFactory; @@ -269,14 +270,16 @@ private static class ReflectiveAccess { FILE_DESCRIPTOR_SET_FILENO_HANDLE = fdSetFileno; if (selChImplGetFD == null || fileChannelGetFD == null || fdGetFileno == null) { - // Warn users since we don't currently handle half-native process control. - Module module = Modules.getModule(ReflectiveAccess.class); - String moduleName = module.getName(); - if (moduleName == null) { - moduleName = "ALL-UNNAMED"; + if (Options.NATIVE_VERBOSE.load()) { + // Warn users since we don't currently handle half-native process control. + Module module = Modules.getModule(ReflectiveAccess.class); + String moduleName = module.getName(); + if (moduleName == null) { + moduleName = "ALL-UNNAMED"; + } + LOG.warn("Native IO integration requires open access to the JDK IO subsystem\n" + + "Pass '--add-opens java.base/sun.nio.ch=" + moduleName + " --add-opens java.base/java.io=" + moduleName + "' to enable."); } - LOG.warn("Native subprocess control requires open access to the JDK IO subsystem\n" + - "Pass '--add-opens java.base/sun.nio.ch=" + moduleName + " --add-opens java.base/java.io=" + moduleName + "' to enable."); } } diff --git a/core/src/main/java/org/jruby/util/io/OpenFile.java b/core/src/main/java/org/jruby/util/io/OpenFile.java index aad2c7d16c4..87ea1e962c8 100644 --- a/core/src/main/java/org/jruby/util/io/OpenFile.java +++ b/core/src/main/java/org/jruby/util/io/OpenFile.java @@ -2,6 +2,7 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; @@ -9,7 +10,6 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.util.HashSet; @@ -34,29 +34,34 @@ import org.jruby.RubyFixnum; import org.jruby.RubyIO; import org.jruby.RubyNumeric; +import org.jruby.FiberScheduler; import org.jruby.RubyString; import org.jruby.RubyThread; import org.jruby.exceptions.RaiseException; import org.jruby.ext.fcntl.FcntlLibrary; import org.jruby.platform.Platform; import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import org.jruby.util.ShellLauncher; import org.jruby.util.StringSupport; +import org.jruby.util.cli.Options; import static org.jruby.util.StringSupport.*; public class OpenFile implements Finalizable { // RB_IO_FPTR_NEW, minus fields that Java already initializes the same way - public OpenFile(IRubyObject nil) { + public OpenFile(RubyIO io, IRubyObject nil) { + this.io = io; runtime = nil.getRuntime(); writeconvAsciicompat = null; writeconvPreEcopts = nil; encs.ecopts = nil; posix = new PosixShim(runtime); + fiberScheduler = Options.FIBER_SCHEDULER.load(); } // IO Mode flags @@ -144,6 +149,7 @@ public static class Buffer { public final Buffer wbuf = new Buffer(), rbuf = new Buffer(), cbuf = new Buffer(); + public final RubyIO io; public RubyIO tiedIOForWriting; private boolean nonblock = false; @@ -157,6 +163,8 @@ public static class Buffer { private final Ptr spPtr = new Ptr(); private final Ptr dpPtr = new Ptr(); + private final boolean fiberScheduler; + public void clearStdio() { stdio_file = null; } @@ -457,6 +465,8 @@ public int io_fflush(ThreadContext context) { // rb_io_wait_writable public boolean waitWritable(ThreadContext context, long timeout) { + IRubyObject scheduler = fiberScheduler ? context.getFiberCurrentThread().getSchedulerCurrent() : null; + boolean locked = lock(); try { if (posix.getErrno() == null) return false; @@ -470,6 +480,10 @@ public boolean waitWritable(ThreadContext context, long timeout) { return true; case EAGAIN: case EWOULDBLOCK: + if (fiberScheduler && !scheduler.isNil()) { + return FiberScheduler.ioWaitWritable(context, scheduler, RubyIO.newIO(context.runtime, channel())).isTrue(); + } + ready(runtime, context.getThread(), SelectExecutor.WRITE_CONNECT_OPS, timeout); return true; default: @@ -487,6 +501,8 @@ public boolean waitWritable(ThreadContext context) { // rb_io_wait_readable public boolean waitReadable(ThreadContext context, long timeout) { + IRubyObject scheduler = fiberScheduler ? context.getFiberCurrentThread().getSchedulerCurrent() : null; + boolean locked = lock(); try { if (posix.getErrno() == null) return false; @@ -500,6 +516,10 @@ public boolean waitReadable(ThreadContext context, long timeout) { return true; case EAGAIN: case EWOULDBLOCK: + if (fiberScheduler && !scheduler.isNil()) { + return FiberScheduler.ioWaitReadable(context, scheduler, RubyIO.newIO(context.runtime, channel())).isTrue(); + } + ready(runtime, context.getThread(), SelectionKey.OP_READ, timeout); return true; default: @@ -1320,7 +1340,16 @@ && waitReadable(context, fd)) { final static RubyThread.ReadWrite READ_TASK = new RubyThread.ReadWrite() { @Override - public int run(ThreadContext context, OpenFile fptr, byte[] buffer, int start, int length) throws InterruptedException { + public int run(ThreadContext context, OpenFile fptr, ByteBuffer buffer, int start, int length) throws InterruptedException { + ChannelFD fd = preRead(context, fptr); + try { + return fptr.posix.read(fd, buffer, start, length, fptr.nonblock); + } finally { + fptr.lock(); + } + } + + private ChannelFD preRead(ThreadContext context, OpenFile fptr) { ChannelFD fd = fptr.fd; if (fd == null) { @@ -1331,11 +1360,7 @@ public int run(ThreadContext context, OpenFile fptr, byte[] buffer, int start, i assert fptr.lockedByMe(); fptr.unlock(); - try { - return fptr.posix.read(fd, buffer, start, length, fptr.nonblock); - } finally { - fptr.lock(); - } + return fd; } @Override @@ -1346,7 +1371,18 @@ public void wakeup(RubyThread thread, OpenFile data) { final static RubyThread.ReadWrite WRITE_TASK = new RubyThread.ReadWrite() { @Override - public int run(ThreadContext context, OpenFile fptr, byte[] bytes, int start, int length) throws InterruptedException { + public int run(ThreadContext context, OpenFile fptr, ByteBuffer bytes, int start, int length) throws InterruptedException { + ChannelFD fd = preWrite(context, fptr); + try { + bytes.position(start); + bytes.limit(start + length); + return fptr.posix.write(fd, bytes, start, length, fptr.nonblock); + } finally { + fptr.lock(); + } + } + + private ChannelFD preWrite(ThreadContext context, OpenFile fptr) { ChannelFD fd = fptr.fd; if (fd == null) { @@ -1357,22 +1393,107 @@ public int run(ThreadContext context, OpenFile fptr, byte[] bytes, int start, in assert fptr.lockedByMe(); fptr.unlock(); + return fd; + } + + @Override + public void wakeup(RubyThread thread, OpenFile data) { + // FIXME: NO! This will kill many native channels. Must be nonblocking to interrupt. + thread.getNativeThread().interrupt(); + } + }; + + public static final RubyThread.ReadWrite PREAD_TASK = new RubyThread.ReadWrite() { + @Override + public int run(ThreadContext context, ChannelFD fd, ByteBuffer bytes, int from, int length) throws InterruptedException { + Ruby runtime = context.runtime; + + int read = 0; + try { - return fptr.posix.write(fd, bytes, start, length, fptr.nonblock); - } finally { - fptr.lock(); + if (fd.chFile != null) { + read = fd.chFile.read(bytes, from); + + if (read == -1) { + throw runtime.newEOFError(); + } + } else if (fd.chNative != null) { + read = (int) runtime.getPosix().pread(fd.chNative.getFD(), bytes, length, from); + + if (read == 0) { + throw runtime.newEOFError(); + } else if (read == -1) { + throw runtime.newErrnoFromInt(runtime.getPosix().errno()); + } + } else if (fd.chRead != null) { + read = fd.chRead.read(bytes); + } else { + throw runtime.newIOError("not opened for reading"); + } + + return read; + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(runtime, ioe); } } @Override - public void wakeup(RubyThread thread, OpenFile data) { + public void wakeup(RubyThread thread, ChannelFD channelFD) { + // FIXME: NO! This will kill many native channels. Must be nonblocking to interrupt. + thread.getNativeThread().interrupt(); + } + }; + + public static final RubyThread.ReadWrite PWRITE_TASK = new RubyThread.ReadWrite() { + @Override + public int run(ThreadContext context, ChannelFD fd, ByteBuffer bytes, int from, int length) throws InterruptedException { + Ruby runtime = context.runtime; + int written = 0; + + try { + if (fd.chFile != null) { + written = fd.chFile.write(bytes, from); + } else if (fd.chNative != null) { + written = (int) runtime.getPosix().pwrite(fd.chNative.getFD(), bytes, length, from); + } else if (fd.chWrite != null) { + written = fd.chWrite.write(bytes); + } else { + throw runtime.newIOError("not opened for writing"); + } + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(runtime, ioe); + } + return written; + } + + @Override + public void wakeup(RubyThread thread, ChannelFD channelFD) { // FIXME: NO! This will kill many native channels. Must be nonblocking to interrupt. thread.getNativeThread().interrupt(); } }; - // rb_read_internal + // rb_read_internal, rb_io_read_memory, rb_io_buffer_read_internal public static int readInternal(ThreadContext context, OpenFile fptr, ChannelFD fd, byte[] bufBytes, int buf, int count) { + return readInternal(context, fptr, fd, ByteBuffer.wrap(bufBytes, buf, count), buf, count); + } + + // rb_io_buffer_read_internal + public static int readInternal(ThreadContext context, OpenFile fptr, ChannelFD fd, ByteBuffer buffer, int buf, int count) { + // try scheduler first + if (fptr.fiberScheduler) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioReadMemory(context, scheduler, fptr.io, buffer, buf, count); + + if (result != null) { + return FiberScheduler.resultApply(context, result); + } + } + } + + // proceed to builtin read logic + // if we can do selection and this is not a non-blocking call, do selection /* @@ -1386,6 +1507,60 @@ simple read(2) because EINTR does not damage the descriptor. working with any native descriptor. */ + preRead(context, fptr, fd); + + try { + return context.getThread().executeReadWrite(context, fptr, buffer, buf, count, READ_TASK); + } catch (InterruptedException ie) { + throw context.runtime.newConcurrencyError("IO operation interrupted"); + } + } + + public static int preadInternal(ThreadContext context, OpenFile fptr, ChannelFD fd, ByteBuffer buffer, int from, int length) { + // try scheduler first + if (fptr.fiberScheduler) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPReadMemory(context, scheduler, fptr.io, buffer, from, length, 0); + + if (result != null) { + return FiberScheduler.resultApply(context, result); + } + } + } + + // proceed to builtin pread logic + try { + return context.getThread().executeReadWrite(context, fd, buffer, from, length, PREAD_TASK); + } catch (InterruptedException ie) { + throw context.runtime.newConcurrencyError("IO operation interrupted"); + } + } + + public static int pwriteInternal(ThreadContext context, OpenFile fptr, ChannelFD fd, ByteBuffer buffer, int from, int length) { + // try scheduler first + if (fptr.fiberScheduler) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioPWriteMemory(context, scheduler, fptr.io, buffer, from, length, 0); + + if (result != null) { + return FiberScheduler.resultApply(context, result); + } + } + } + + // proceed to builtin pread logic + int written; + try { + written = context.getThread().executeReadWrite(context, fd, buffer, from, length, PWRITE_TASK); + } catch (InterruptedException ie) { + throw context.runtime.newConcurrencyError("IO operation interrupted"); + } + return written; + } + + private static void preRead(ThreadContext context, OpenFile fptr, ChannelFD fd) { if (fd == null) { // stream was closed on its way in, raise appropriate error throw context.runtime.newErrnoEBADFError(); @@ -1401,12 +1576,6 @@ simple read(2) because EINTR does not damage the descriptor. } finally { fptr.lock(); } - - try { - return context.getThread().executeReadWrite(context, fptr, bufBytes, buf, count, READ_TASK); - } catch (InterruptedException ie) { - throw context.runtime.newConcurrencyError("IO operation interrupted"); - } } /** @@ -2312,8 +2481,24 @@ static int binwriteString(OpenFile fptr, byte[] bytes, int start, int length) { return fptr.writeInternal2(fptr.fd, bytes, start, l); } - // rb_write_internal + // rb_write_internal, rb_io_write_memory public static int writeInternal(ThreadContext context, OpenFile fptr, byte[] bufBytes, int buf, int count) { + return writeInternal(context, fptr, ByteBuffer.wrap(bufBytes, buf, count), buf, count); + } + + // rb_io_buffer_write_internal + public static int writeInternal(ThreadContext context, OpenFile fptr, ByteBuffer bufBytes, int buf, int count) { + if (fptr.fiberScheduler) { + IRubyObject scheduler = context.getFiberCurrentThread().getSchedulerCurrent(); + if (!scheduler.isNil()) { + IRubyObject result = FiberScheduler.ioWriteMemory(context, scheduler, fptr.io, bufBytes, buf, count); + + if (result != null) { + return FiberScheduler.resultApply(context, result); + } + } + } + try { return context.getThread().executeReadWrite(context, fptr, bufBytes, buf, count, WRITE_TASK); } catch (InterruptedException ie) { diff --git a/core/src/main/java/org/jruby/util/io/PosixShim.java b/core/src/main/java/org/jruby/util/io/PosixShim.java index ae0c72e6ee4..45c7ccbc154 100644 --- a/core/src/main/java/org/jruby/util/io/PosixShim.java +++ b/core/src/main/java/org/jruby/util/io/PosixShim.java @@ -89,10 +89,15 @@ public long lseek(ChannelFD fd, long offset, int type) { } public int write(ChannelFD fd, byte[] bytes, int offset, int length, boolean nonblock) { - clear(); - // FIXME: don't allocate every time ByteBuffer tmp = ByteBuffer.wrap(bytes, offset, length); + + return write(fd, tmp, offset, length, nonblock); + } + + public int write(ChannelFD fd, ByteBuffer buffer, int offset, int length, boolean nonblock) { + clear(); + try { if (nonblock) { // TODO: figure out what nonblocking writes against atypical streams (files?) actually do @@ -104,7 +109,7 @@ public int write(ChannelFD fd, byte[] bytes, int offset, int length, boolean non setErrno(Errno.EACCES); return -1; } - int written = fd.chWrite.write(tmp); + int written = fd.chWrite.write(buffer); if (written == 0 && length > 0) { // if it's a nonblocking write against a file and we've hit EOF, do EAGAIN @@ -129,55 +134,79 @@ public int read(ChannelFD fd, byte[] target, int offset, int length, boolean non clear(); try { - if (nonblock) { - // need to ensure channels that don't support nonblocking IO at least - // appear to be nonblocking - if (fd.chSelect != null) { - // ok...we should have set it nonblocking already in setNonblock - } else { - if (fd.chFile != null) { - long position = fd.chFile.position(); - long size = fd.chFile.size(); - if (position != -1 && size != -1 && position < size) { - // there should be bytes available...proceed - } else { - setErrno(Errno.EAGAIN); - return -1; - } - } else if (fd.chNative != null && fd.isNativeFile) { - // it's a native file, so we don't do selection or nonblock - } else { - setErrno(Errno.EAGAIN); - return -1; - } - } - } + if (checkForBlocking(fd, nonblock)) return -1; // FIXME: inefficient to recreate ByteBuffer every time ByteBuffer buffer = ByteBuffer.wrap(target, offset, length); - int read = fd.chRead.read(buffer); + return performRead(fd, buffer, nonblock); + } catch (IOException ioe) { + setErrno(Helpers.errnoFromException(ioe)); + return -1; + } + } - if (nonblock) { - if (read == JAVA_EOF) { - read = NATIVE_EOF; // still treat EOF as EOF - } else if (read == 0) { - setErrno(Errno.EAGAIN); - return -1; - } else { - return read; - } - } else { - // NIO channels will always raise for errors, so -1 only means EOF. - if (read == JAVA_EOF) read = NATIVE_EOF; - } + public int read(ChannelFD fd, ByteBuffer buffer, int offset, int length, boolean nonblock) { + clear(); + + try { + if (checkForBlocking(fd, nonblock)) return -1; - return read; + buffer.position(offset); + buffer.limit(offset + length); + return performRead(fd, buffer, nonblock); } catch (IOException ioe) { setErrno(Helpers.errnoFromException(ioe)); return -1; } } + private int performRead(ChannelFD fd, ByteBuffer buffer, boolean nonblock) throws IOException { + int read = fd.chRead.read(buffer); + + if (nonblock) { + if (read == JAVA_EOF) { + read = NATIVE_EOF; // still treat EOF as EOF + } else if (read == 0) { + setErrno(Errno.EAGAIN); + return -1; + } else { + return read; + } + } else { + // NIO channels will always raise for errors, so -1 only means EOF. + if (read == JAVA_EOF) read = NATIVE_EOF; + } + + return read; + } + + private boolean checkForBlocking(ChannelFD fd, boolean nonblock) throws IOException { + if (nonblock) { + // need to ensure channels that don't support nonblocking IO at least + // appear to be nonblocking + if (fd.chSelect != null) { + // ok...we should have set it nonblocking already in setNonblock + } else { + if (fd.chFile != null) { + long position = fd.chFile.position(); + long size = fd.chFile.size(); + if (position != -1 && size != -1 && position < size) { + // there should be bytes available...proceed + } else { + setErrno(Errno.EAGAIN); + return true; + } + } else if (fd.chNative != null && fd.isNativeFile) { + // it's a native file, so we don't do selection or nonblock + } else { + setErrno(Errno.EAGAIN); + return true; + } + } + } + return false; + } + // rb_thread_flock public int flock(ChannelFD fd, int lockMode) { // TODO: null channel always succeeds for all locking operations diff --git a/core/src/main/ruby/jruby/kernel/enumerator.rb b/core/src/main/ruby/jruby/kernel/enumerator.rb index cc428d9650f..e9835feee00 100644 --- a/core/src/main/ruby/jruby/kernel/enumerator.rb +++ b/core/src/main/ruby/jruby/kernel/enumerator.rb @@ -98,7 +98,7 @@ def next? end def next - reset unless @fiber&.__alive__ + reset unless @fiber&.alive? val = @fiber.resume @@ -109,7 +109,7 @@ def next def rewind fiber, @fiber = @fiber, nil - fiber.send(:__finalize__) if fiber&.__alive__ + fiber.send(:__finalize__) if fiber&.alive? @state.done = false end diff --git a/core/src/test/java/org/jruby/javasupport/TestJava.java b/core/src/test/java/org/jruby/javasupport/TestJava.java index ee49e27acd4..dbf2864c1aa 100644 --- a/core/src/test/java/org/jruby/javasupport/TestJava.java +++ b/core/src/test/java/org/jruby/javasupport/TestJava.java @@ -1,6 +1,7 @@ package org.jruby.javasupport; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; import org.jruby.*; import org.jruby.exceptions.RaiseException; @@ -117,4 +118,23 @@ public void testJavaConstructorExceptionHandling() throws Exception { } } + @Test + public void testOverrideNewOnConcreteJavaProxySubClassRegression() { + String script = + "class FormatImpl < java.text.SimpleDateFormat\n" + + " include Enumerable\n" + + " public_class_method :new\n" + + " class << self\n" + + " def new(thread_provider: true)\n" + + " super()\n" + + " end\n" + + " end\n" + + "end\n"; + + final Ruby runtime = Ruby.newInstance(); + runtime.evalScriptlet(script); + + assertNotNull(runtime.evalScriptlet("FormatImpl.new")); // used to cause an infinite loop + assertTrue(runtime.evalScriptlet("FormatImpl.new").toJava(Object.class) instanceof SimpleDateFormat); + } } diff --git a/lib/pom.rb b/lib/pom.rb index c95e17af36a..38f91d860cd 100644 --- a/lib/pom.rb +++ b/lib/pom.rb @@ -339,7 +339,7 @@ def installer.ensure_required_ruby_version_met; end spec.executables.each do |f| bin = Dir.glob(File.join( gems, "#{gem_name}*", spec.bindir ))[0] source = File.join( bin, f ) - target = File.join( bin_stubs, source.sub( /#{gems}/, '' ) ) + target = File.join( bin_stubs, source.sub( gems, '' ) ) log "copy #{f} to #{target}" FileUtils.mkdir_p( File.dirname( target ) ) FileUtils.cp_r( source, target ) diff --git a/lib/pom.xml b/lib/pom.xml index 1e324ccb65a..4ec8d384366 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -12,7 +12,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-parent - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT jruby-stdlib JRuby Lib Setup @@ -28,7 +28,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-core - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT test diff --git a/lib/ruby/stdlib/fiber.rb b/lib/ruby/stdlib/fiber.rb deleted file mode 100644 index d568fc8e963..00000000000 --- a/lib/ruby/stdlib/fiber.rb +++ /dev/null @@ -1,12 +0,0 @@ -## -# Extensions to Fiber. In JRuby, these are all still defined in the normal -# Fiber class, so we alias them to the right names here. -# -class Fiber - class << self - alias current __current__ - end - - alias transfer __transfer__ - alias alive? __alive__ -end \ No newline at end of file diff --git a/pom.xml b/pom.xml index 613d7046828..fcf2be8a141 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-parent - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT pom JRuby JRuby is the effort to recreate the Ruby (https://www.ruby-lang.org) interpreter in Java. diff --git a/rakelib/test.rake b/rakelib/test.rake index 1f531dee797..51b20dc86d6 100644 --- a/rakelib/test.rake +++ b/rakelib/test.rake @@ -100,9 +100,9 @@ namespace :test do mri_suites = [:core, :extra, :stdlib] mri_suites = { - core: "-Xbacktrace.style=mri -Xdebug.fullTrace", - extra: "--disable-gems -Xbacktrace.style=mri -Xdebug.fullTrace", - stdlib: "-Xbacktrace.style=mri -Xdebug.fullTrace", + core: "-Xbacktrace.style=mri -Xdebug.fullTrace -Xexperimental.fiber.scheduler", + extra: "--disable-gems -Xbacktrace.style=mri -Xdebug.fullTrace -Xexperimental.fiber.scheduler", + stdlib: "-Xbacktrace.style=mri -Xdebug.fullTrace -Xexperimental.fiber.scheduler", } mri_suites.each do |suite, extra_jruby_opts| diff --git a/shaded/pom.xml b/shaded/pom.xml index dc8e2c85502..4ed7d106a02 100644 --- a/shaded/pom.xml +++ b/shaded/pom.xml @@ -12,7 +12,7 @@ DO NOT MODIFY - GENERATED CODE org.jruby jruby-parent - 9.4.5.0-SNAPSHOT + 9.4.6.0-SNAPSHOT jruby-core JRuby Core diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb index a6e60945cdf..d8d9efd2816 100755 --- a/spec/mspec/tool/tag_from_output.rb +++ b/spec/mspec/tool/tag_from_output.rb @@ -30,9 +30,9 @@ if spec_file spec_file = spec_file[SPEC_FILE, 1] or raise else - if error_line =~ /^(\w+)[#\.](\w+) / - module_method = error_line.split(' ', 2).first - file = "#{$1.downcase}/#{$2}_spec.rb" + if error_line =~ /^([\w:]+)[#\.](\w+) / + mod, method = $1, $2 + file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb" spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir| path = "#{dir}/#{file}" break path if File.exist?(path) diff --git a/spec/ruby/.rubocop.yml b/spec/ruby/.rubocop.yml index 99209b607bd..9ad57dd51cb 100644 --- a/spec/ruby/.rubocop.yml +++ b/spec/ruby/.rubocop.yml @@ -33,6 +33,10 @@ Lint/AssignmentInCondition: Lint/BooleanSymbol: Enabled: false +Lint/DeprecatedOpenSSLConstant: + Exclude: + - library/openssl/digest/**/*.rb + Lint/InterpolationCheck: Enabled: false diff --git a/spec/ruby/CONTRIBUTING.md b/spec/ruby/CONTRIBUTING.md index 4a1e1d46bdb..c82eb5ea4fa 100644 --- a/spec/ruby/CONTRIBUTING.md +++ b/spec/ruby/CONTRIBUTING.md @@ -179,6 +179,7 @@ In case there is a bug in MRI and the fix will be backported to previous version If it is not backported or not likely, use `ruby_version_is` instead. First, file a bug at https://bugs.ruby-lang.org/. The problem is `ruby_bug` would make non-MRI implementations fail this spec while MRI itself does not pass it, so it should only be used if the bug is/will be fixed and backported. +Otherwise, non-MRI implementations would have to choose between being incompatible with the latest release of MRI to pass the spec or fail the spec, both which make no sense. ```ruby ruby_bug '#13669', ''...'3.2' do diff --git a/spec/ruby/README.md b/spec/ruby/README.md index 536388e7c89..115392835f9 100644 --- a/spec/ruby/README.md +++ b/spec/ruby/README.md @@ -128,6 +128,12 @@ MSpec can automatically add new top-level constants in this file with: $ CHECK_LEAKS=save mspec ../mspec/bin/mspec file +### Running Specs on S390x CPU Architecture + +Run the specs with `DFLTCC=0` if you see failing specs related to the zlib library on s390x CPU architecture. The failures can happen with the zlib library applying the patch madler/zlib#410 to enable the deflate algorithm producing a different compressed byte stream. + + $ DFLTCC=0 ../mspec/bin/mspec + ### Contributing and Writing Specs See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) for documentation about contributing and writing specs (guards, matchers, etc). diff --git a/spec/ruby/command_line/dash_a_spec.rb b/spec/ruby/command_line/dash_a_spec.rb index 9ea135dc76f..43d923ce164 100644 --- a/spec/ruby/command_line/dash_a_spec.rb +++ b/spec/ruby/command_line/dash_a_spec.rb @@ -6,13 +6,13 @@ end it "runs the code in loop conditional on Kernel.gets()" do - ruby_exe("puts $F.last", options: "-n -a", escape: true, + ruby_exe("puts $F.last", options: "-n -a", args: " < #{@names}").should == "jones\nfield\ngrey\n" end it "sets $-a" do - ruby_exe("puts $-a", options: "-n -a", escape: true, + ruby_exe("puts $-a", options: "-n -a", args: " < #{@names}").should == "true\ntrue\ntrue\n" end diff --git a/spec/ruby/command_line/dash_l_spec.rb b/spec/ruby/command_line/dash_l_spec.rb index 5c1d3cf4cd9..44a98445f37 100644 --- a/spec/ruby/command_line/dash_l_spec.rb +++ b/spec/ruby/command_line/dash_l_spec.rb @@ -6,25 +6,25 @@ end it "chomps lines with default separator" do - ruby_exe('puts $_.end_with?("\n")', options: "-n -l", escape: true, + ruby_exe('puts $_.end_with?("\n")', options: "-n -l", args: " < #{@names}").should == "false\nfalse\nfalse\n" end it "chomps last line based on $/" do - ruby_exe('BEGIN { $/ = "ones\n" }; puts $_', options: "-W0 -n -l", escape: true, + ruby_exe('BEGIN { $/ = "ones\n" }; puts $_', options: "-W0 -n -l", args: " < #{@names}").should == "alice j\nbob field\njames grey\n" end it "sets $\\ to the value of $/" do - ruby_exe("puts $\\ == $/", options: "-W0 -n -l", escape: true, + ruby_exe("puts $\\ == $/", options: "-W0 -n -l", args: " < #{@names}").should == "true\ntrue\ntrue\n" end it "sets $-l" do - ruby_exe("puts $-l", options: "-n -l", escape: true, + ruby_exe("puts $-l", options: "-n -l", args: " < #{@names}").should == "true\ntrue\ntrue\n" end diff --git a/spec/ruby/command_line/dash_n_spec.rb b/spec/ruby/command_line/dash_n_spec.rb index 9d331d6065d..1dd9379259b 100644 --- a/spec/ruby/command_line/dash_n_spec.rb +++ b/spec/ruby/command_line/dash_n_spec.rb @@ -6,19 +6,19 @@ end it "runs the code in loop conditional on Kernel.gets()" do - ruby_exe("puts $_", options: "-n", escape: true, + ruby_exe("puts $_", options: "-n", args: " < #{@names}").should == "alice\nbob\njames\n" end it "only evaluates BEGIN blocks once" do - ruby_exe("BEGIN { puts \"hi\" }; puts $_", options: "-n", escape: true, + ruby_exe("BEGIN { puts \"hi\" }; puts $_", options: "-n", args: " < #{@names}").should == "hi\nalice\nbob\njames\n" end it "only evaluates END blocks once" do - ruby_exe("puts $_; END {puts \"bye\"}", options: "-n", escape: true, + ruby_exe("puts $_; END {puts \"bye\"}", options: "-n", args: " < #{@names}").should == "alice\nbob\njames\nbye\n" end @@ -29,7 +29,7 @@ $total += 1 END { puts $total } script - ruby_exe(script, options: "-n", escape: true, + ruby_exe(script, options: "-n", args: " < #{@names}").should == "3\n" end diff --git a/spec/ruby/command_line/dash_p_spec.rb b/spec/ruby/command_line/dash_p_spec.rb index 39827c3868c..967e3796de2 100644 --- a/spec/ruby/command_line/dash_p_spec.rb +++ b/spec/ruby/command_line/dash_p_spec.rb @@ -6,13 +6,13 @@ end it "runs the code in loop conditional on Kernel.gets() and prints $_" do - ruby_exe("$_ = $_.upcase", options: "-p", escape: true, + ruby_exe("$_ = $_.upcase", options: "-p", args: " < #{@names}").should == "ALICE\nBOB\nJAMES\n" end it "sets $-p" do - ruby_exe("$_ = $-p", options: "-p", escape: true, + ruby_exe("$_ = $-p", options: "-p", args: " < #{@names}").should == "truetruetrue" end diff --git a/spec/ruby/command_line/dash_upper_f_spec.rb b/spec/ruby/command_line/dash_upper_f_spec.rb index 967acc2ece1..5c10a7140d1 100644 --- a/spec/ruby/command_line/dash_upper_f_spec.rb +++ b/spec/ruby/command_line/dash_upper_f_spec.rb @@ -6,7 +6,7 @@ end it "specifies the field separator pattern for -a" do - ruby_exe("puts $F[0]", options: "-naF:", escape: true, + ruby_exe("puts $F[0]", options: "-naF:", args: " < #{@passwd}").should == "nobody\nroot\ndaemon\n" end diff --git a/spec/ruby/command_line/rubyopt_spec.rb b/spec/ruby/command_line/rubyopt_spec.rb index bbea4d557d1..734db8d519e 100644 --- a/spec/ruby/command_line/rubyopt_spec.rb +++ b/spec/ruby/command_line/rubyopt_spec.rb @@ -11,14 +11,14 @@ it "adds the -I path to $LOAD_PATH" do ENV["RUBYOPT"] = "-Ioptrubyspecincl" - result = ruby_exe("puts $LOAD_PATH.grep(/byspecin/)", escape: true) + result = ruby_exe("puts $LOAD_PATH.grep(/byspecin/)") result.chomp[-15..-1].should == "optrubyspecincl" end it "sets $DEBUG to true for '-d'" do ENV["RUBYOPT"] = '-d' command = %[puts "value of $DEBUG is \#{$DEBUG}"] - result = ruby_exe(command, escape: true, args: "2>&1") + result = ruby_exe(command, args: "2>&1") result.should =~ /value of \$DEBUG is true/ end @@ -36,27 +36,27 @@ it "sets $VERBOSE to true for '-w'" do ENV["RUBYOPT"] = '-w' - ruby_exe("p $VERBOSE", escape: true).chomp.should == "true" + ruby_exe("p $VERBOSE").chomp.should == "true" end it "sets $VERBOSE to true for '-W'" do ENV["RUBYOPT"] = '-W' - ruby_exe("p $VERBOSE", escape: true).chomp.should == "true" + ruby_exe("p $VERBOSE").chomp.should == "true" end it "sets $VERBOSE to nil for '-W0'" do ENV["RUBYOPT"] = '-W0' - ruby_exe("p $VERBOSE", escape: true).chomp.should == "nil" + ruby_exe("p $VERBOSE").chomp.should == "nil" end it "sets $VERBOSE to false for '-W1'" do ENV["RUBYOPT"] = '-W1' - ruby_exe("p $VERBOSE", escape: true).chomp.should == "false" + ruby_exe("p $VERBOSE").chomp.should == "false" end it "sets $VERBOSE to true for '-W2'" do ENV["RUBYOPT"] = '-W2' - ruby_exe("p $VERBOSE", escape: true).chomp.should == "true" + ruby_exe("p $VERBOSE").chomp.should == "true" end it "suppresses deprecation warnings for '-W:no-deprecated'" do diff --git a/spec/ruby/core/argf/readpartial_spec.rb b/spec/ruby/core/argf/readpartial_spec.rb index 5e284b34233..bbc8831131d 100644 --- a/spec/ruby/core/argf/readpartial_spec.rb +++ b/spec/ruby/core/argf/readpartial_spec.rb @@ -69,7 +69,7 @@ print ARGF.readpartial(#{@stdin.size}) ARGF.readpartial(1) rescue print $!.class STR - stdin = ruby_exe(ruby_str, args: "< #{@stdin_name}", escape: true) + stdin = ruby_exe(ruby_str, args: "< #{@stdin_name}") stdin.should == @stdin + "EOFError" end end diff --git a/spec/ruby/core/array/bsearch_index_spec.rb b/spec/ruby/core/array/bsearch_index_spec.rb index df2c7c098e2..94d85b37f33 100644 --- a/spec/ruby/core/array/bsearch_index_spec.rb +++ b/spec/ruby/core/array/bsearch_index_spec.rb @@ -63,10 +63,6 @@ @array.bsearch_index { |x| -1 }.should be_nil end - it "returns the middle element when block always returns zero" do - @array.bsearch_index { |x| 0 }.should == 2 - end - context "magnitude does not effect the result" do it "returns the index of any matched elements where element is between 4n <= xn < 8n" do [1, 2].should include(@array.bsearch_index { |x| (1 - x / 4) * (2**100) }) diff --git a/spec/ruby/core/dir/glob_spec.rb b/spec/ruby/core/dir/glob_spec.rb index 61115b5eec2..32f515c81d8 100644 --- a/spec/ruby/core/dir/glob_spec.rb +++ b/spec/ruby/core/dir/glob_spec.rb @@ -106,11 +106,11 @@ ruby_version_is '3.1' do it "recursively matches files and directories in nested dot subdirectory except . with 'nested/**/*' from the current directory and option File::FNM_DOTMATCH" do expected = %w[ - nested/. - nested/.dotsubir - nested/.dotsubir/.dotfile - nested/.dotsubir/nondotfile - ] + nested/. + nested/.dotsubir + nested/.dotsubir/.dotfile + nested/.dotsubir/nondotfile + ] Dir.glob('nested/**/*', File::FNM_DOTMATCH).sort.should == expected.sort end diff --git a/spec/ruby/core/dir/home_spec.rb b/spec/ruby/core/dir/home_spec.rb index 90a008faf12..3cf745ab468 100644 --- a/spec/ruby/core/dir/home_spec.rb +++ b/spec/ruby/core/dir/home_spec.rb @@ -40,22 +40,21 @@ home.should == "C:/rubyspäc/home" home.encoding.should == Encoding::UTF_8 end - end - it "retrieves the directory from HOME, USERPROFILE, HOMEDRIVE/HOMEPATH and the WinAPI in that order" do - old_dirs = [ENV.delete('HOME'), ENV.delete('USERPROFILE'), ENV.delete('HOMEDRIVE'), ENV.delete('HOMEPATH')] + it "retrieves the directory from HOME, USERPROFILE, HOMEDRIVE/HOMEPATH and the WinAPI in that order" do + old_dirs = [ENV.delete('HOME'), ENV.delete('USERPROFILE'), ENV.delete('HOMEDRIVE'), ENV.delete('HOMEPATH')] - Dir.home.should == old_dirs[1].gsub("\\", "/") - ENV['HOMEDRIVE'] = "C:" - ENV['HOMEPATH'] = "\\rubyspec\\home1" - Dir.home.should == "C:/rubyspec/home1" - ENV['USERPROFILE'] = "C:\\rubyspec\\home2" - # https://bugs.ruby-lang.org/issues/19244 - # Dir.home.should == "C:/rubyspec/home2" - ENV['HOME'] = "C:\\rubyspec\\home3" - Dir.home.should == "C:/rubyspec/home3" - ensure - ENV['HOME'], ENV['USERPROFILE'], ENV['HOMEDRIVE'], ENV['HOMEPATH'] = *old_dirs + Dir.home.should == old_dirs[1].gsub("\\", "/") + ENV['HOMEDRIVE'] = "C:" + ENV['HOMEPATH'] = "\\rubyspec\\home1" + Dir.home.should == "C:/rubyspec/home1" + ENV['USERPROFILE'] = "C:\\rubyspec\\home2" + Dir.home.should == "C:/rubyspec/home2" + ENV['HOME'] = "C:\\rubyspec\\home3" + Dir.home.should == "C:/rubyspec/home3" + ensure + ENV['HOME'], ENV['USERPROFILE'], ENV['HOMEDRIVE'], ENV['HOMEPATH'] = *old_dirs + end end end end diff --git a/spec/ruby/core/exception/detailed_message_spec.rb b/spec/ruby/core/exception/detailed_message_spec.rb index bd85927dbe6..fbe4443daab 100644 --- a/spec/ruby/core/exception/detailed_message_spec.rb +++ b/spec/ruby/core/exception/detailed_message_spec.rb @@ -7,6 +7,14 @@ RuntimeError.new("new error").detailed_message.should == "new error (RuntimeError)" end + it "is called by #full_message to allow message customization" do + exception = Exception.new("new error") + def exception.detailed_message(**) + "#{message}" + end + exception.full_message(highlight: false).should.include? "new error" + end + it "accepts highlight keyword argument and adds escape control sequences" do RuntimeError.new("new error").detailed_message(highlight: true).should == "\e[1mnew error (\e[1;4mRuntimeError\e[m\e[1m)\e[m" end @@ -23,13 +31,13 @@ RuntimeError.new("").detailed_message.should == "unhandled exception" end - it "returns just class name for an instance of RuntimeError sublass with empty message" do + it "returns just class name for an instance of RuntimeError subclass with empty message" do DetailedMessageSpec::C.new("").detailed_message.should == "DetailedMessageSpec::C" end it "returns a generated class name for an instance of RuntimeError anonymous subclass with empty message" do klass = Class.new(RuntimeError) - klass.new("").detailed_message.should =~ /\A#\z/ + klass.new("").detailed_message.should =~ /\A#\z/ end end end diff --git a/spec/ruby/core/exception/full_message_spec.rb b/spec/ruby/core/exception/full_message_spec.rb index ee66582022e..e15649ca756 100644 --- a/spec/ruby/core/exception/full_message_spec.rb +++ b/spec/ruby/core/exception/full_message_spec.rb @@ -107,21 +107,49 @@ ruby_version_is "3.2" do it "relies on #detailed_message" do e = RuntimeError.new("new error") - e.define_singleton_method(:detailed_message) { |**opt| "DETAILED MESSAGE" } + e.define_singleton_method(:detailed_message) { |**| "DETAILED MESSAGE" } e.full_message.lines.first.should =~ /DETAILED MESSAGE/ end - it "passes all its own keyword arguments to #detailed_message" do + it "passes all its own keyword arguments (with :highlight default value and without :order default value) to #detailed_message" do e = RuntimeError.new("new error") - opt_ = nil - e.define_singleton_method(:detailed_message) do |**opt| - opt_ = opt + options_passed = nil + e.define_singleton_method(:detailed_message) do |**options| + options_passed = options "DETAILED MESSAGE" end e.full_message(foo: "bar") - opt_.should == { foo: "bar", highlight: Exception.to_tty? } + options_passed.should == { foo: "bar", highlight: Exception.to_tty? } + end + + it "converts #detailed_message returned value to String if it isn't a String" do + message = Object.new + def message.to_str; "DETAILED MESSAGE"; end + + e = RuntimeError.new("new error") + e.define_singleton_method(:detailed_message) { |**| message } + + e.full_message.lines.first.should =~ /DETAILED MESSAGE/ + end + + it "uses class name if #detailed_message returns nil" do + e = RuntimeError.new("new error") + e.define_singleton_method(:detailed_message) { |**| nil } + + e.full_message(highlight: false).lines.first.should =~ /RuntimeError/ + e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/ + end + + it "uses class name if exception object doesn't respond to #detailed_message" do + e = RuntimeError.new("new error") + class << e + undef :detailed_message + end + + e.full_message(highlight: false).lines.first.should =~ /RuntimeError/ + e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/ end end end diff --git a/spec/ruby/core/file/flock_spec.rb b/spec/ruby/core/file/flock_spec.rb index 751e99d994e..070d830bc47 100644 --- a/spec/ruby/core/file/flock_spec.rb +++ b/spec/ruby/core/file/flock_spec.rb @@ -30,7 +30,7 @@ it "returns false if trying to lock an exclusively locked file" do @file.flock File::LOCK_EX - ruby_exe(<<-END_OF_CODE, escape: true).should == "false" + ruby_exe(<<-END_OF_CODE).should == "false" File.open('#{@name}', "w") do |f2| print f2.flock(File::LOCK_EX | File::LOCK_NB).to_s end @@ -40,7 +40,7 @@ it "blocks if trying to lock an exclusively locked file" do @file.flock File::LOCK_EX - out = ruby_exe(<<-END_OF_CODE, escape: true) + out = ruby_exe(<<-END_OF_CODE) running = false t = Thread.new do diff --git a/spec/ruby/core/file/new_spec.rb b/spec/ruby/core/file/new_spec.rb index 715ac1aaf33..3e2641aed3e 100644 --- a/spec/ruby/core/file/new_spec.rb +++ b/spec/ruby/core/file/new_spec.rb @@ -188,6 +188,12 @@ }.should raise_error(Errno::EEXIST, /File exists/) end + it "does not use the given block and warns to use File::open" do + -> { + @fh = File.new(@file) { raise } + }.should complain(/warning: File::new\(\) does not take block; use File::open\(\) instead/) + end + it "raises a TypeError if the first parameter can't be coerced to a string" do -> { File.new(true) }.should raise_error(TypeError) -> { File.new(false) }.should raise_error(TypeError) diff --git a/spec/ruby/core/file/shared/fnmatch.rb b/spec/ruby/core/file/shared/fnmatch.rb index f943c01eb8c..db4b5c5d8c6 100644 --- a/spec/ruby/core/file/shared/fnmatch.rb +++ b/spec/ruby/core/file/shared/fnmatch.rb @@ -102,6 +102,7 @@ it "matches ranges of characters using exclusive bracket expression (e.g. [^t] or [!t])" do File.send(@method, 'ca[^t]', 'cat').should == false + File.send(@method, 'ca[^t]', 'cas').should == true File.send(@method, 'ca[!t]', 'cat').should == false end @@ -172,9 +173,19 @@ File.should_not.send(@method, '*/*', 'dave/.profile', File::FNM_PATHNAME) end - it "matches patterns with leading periods to dotfiles by default" do + it "matches patterns with leading periods to dotfiles" do File.send(@method, '.*', '.profile').should == true + File.send(@method, '.*', '.profile', File::FNM_PATHNAME).should == true File.send(@method, ".*file", "nondotfile").should == false + File.send(@method, ".*file", "nondotfile", File::FNM_PATHNAME).should == false + end + + it "does not match directories with leading periods by default with FNM_PATHNAME" do + File.send(@method, '.*', '.directory/nondotfile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', '.directory/.profile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', 'foo/.directory/nondotfile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', 'foo/.directory/.profile', File::FNM_PATHNAME).should == false + File.send(@method, '**/.dotfile', '.dotsubdir/.dotfile', File::FNM_PATHNAME).should == false end it "matches leading periods in filenames when flags includes FNM_DOTMATCH" do @@ -228,6 +239,33 @@ File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true end + it "has special handling for ./ when using * and FNM_PATHNAME" do + File.send(@method, './*', '.', File::FNM_PATHNAME).should be_false + File.send(@method, './*', './', File::FNM_PATHNAME).should be_true + File.send(@method, './*/', './', File::FNM_PATHNAME).should be_false + File.send(@method, './**', './', File::FNM_PATHNAME).should be_true + File.send(@method, './**/', './', File::FNM_PATHNAME).should be_true + File.send(@method, './*', '.', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + File.send(@method, './*', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, './*/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + File.send(@method, './**', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, './**/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + end + + it "matches **/* with FNM_PATHNAME to recurse directories" do + File.send(@method, 'nested/**/*', 'nested/subdir', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**/*', 'nested/subdir/file', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**/*', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, 'nested/**/*', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + end + + it "matches ** with FNM_PATHNAME only in current directory" do + File.send(@method, 'nested/**', 'nested/subdir', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**', 'nested/subdir/file', File::FNM_PATHNAME).should be_false + File.send(@method, 'nested/**', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, 'nested/**', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + end + it "accepts an object that has a #to_path method" do File.send(@method, '\*', mock_to_path('a')).should == false end diff --git a/spec/ruby/core/io/copy_stream_spec.rb b/spec/ruby/core/io/copy_stream_spec.rb index df9c5c7390e..ffa2ea992ce 100644 --- a/spec/ruby/core/io/copy_stream_spec.rb +++ b/spec/ruby/core/io/copy_stream_spec.rb @@ -69,9 +69,12 @@ end it "raises an IOError if the destination IO is not open for writing" do - @to_io.close - @to_io = new_io @to_name, "r" - -> { IO.copy_stream @object.from, @to_io }.should raise_error(IOError) + to_io = new_io __FILE__, "r" + begin + -> { IO.copy_stream @object.from, to_io }.should raise_error(IOError) + ensure + to_io.close + end end it "does not close the destination IO" do @@ -109,7 +112,8 @@ end after :each do - rm_r @to_name, @from_bigfile + rm_r @to_name if @to_name + rm_r @from_bigfile end describe "from an IO" do @@ -164,6 +168,25 @@ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream end + + describe "to a Tempfile" do + before :all do + require 'tempfile' + end + + before :each do + @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY) + @to_name = @to_io.path + end + + after :each do + @to_io.close! + @to_name = nil # do not rm_r it, already done by Tempfile#close! + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end end describe "from a file name" do @@ -277,10 +300,8 @@ @io.should_not_receive(:pos) IO.copy_stream(@io, @to_name) end - end - describe "with a destination that does partial reads" do before do @from_out, @from_in = IO.pipe diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb index 73d76b3abdf..ca64bf860e4 100644 --- a/spec/ruby/core/io/gets_spec.rb +++ b/spec/ruby/core/io/gets_spec.rb @@ -24,6 +24,12 @@ end end + it "sets $_ to nil after the last line has been read" do + while @io.gets + end + $_.should be_nil + end + it "returns nil if called at the end of the stream" do IOSpecs.lines.length.times { @io.gets } @io.gets.should == nil diff --git a/spec/ruby/core/io/new_spec.rb b/spec/ruby/core/io/new_spec.rb index 9d14ec18ad4..979ac0efcbd 100644 --- a/spec/ruby/core/io/new_spec.rb +++ b/spec/ruby/core/io/new_spec.rb @@ -5,6 +5,12 @@ describe "IO.new" do it_behaves_like :io_new, :new + + it "does not use the given block and warns to use IO::open" do + -> { + @io = IO.send(@method, @fd) { raise } + }.should complain(/warning: IO::new\(\) does not take block; use IO::open\(\) instead/) + end end describe "IO.new" do diff --git a/spec/ruby/core/io/open_spec.rb b/spec/ruby/core/io/open_spec.rb index d3a3961df75..d151da9ce5a 100644 --- a/spec/ruby/core/io/open_spec.rb +++ b/spec/ruby/core/io/open_spec.rb @@ -37,6 +37,19 @@ ScratchPad.recorded.should == :called end + it "propagate an exception in the block after calling #close" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + end + raise Exception + end + end.should raise_error(Exception) + ScratchPad.recorded.should == :called + end + it "propagates an exception raised by #close that is not a StandardError" do -> do IO.open(@fd, "w") do |io| diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb index fb0645dec68..aa496ee803a 100644 --- a/spec/ruby/core/io/pread_spec.rb +++ b/spec/ruby/core/io/pread_spec.rb @@ -26,11 +26,88 @@ buffer.should == "567" end + it "shrinks the buffer in case of less bytes read" do + buffer = "foo" + @file.pread(1, 0, buffer) + buffer.should == "1" + end + + it "grows the buffer in case of more bytes read" do + buffer = "foo" + @file.pread(5, 0, buffer) + buffer.should == "12345" + end + it "does not advance the file pointer" do @file.pread(4, 0).should == "1234" @file.read.should == "1234567890" end + it "ignores the current offset" do + @file.pos = 3 + @file.pread(4, 0).should == "1234" + end + + it "returns an empty string for maxlen = 0" do + @file.pread(0, 4).should == "" + end + + it "ignores the offset for maxlen = 0, even if it is out of file bounds" do + @file.pread(0, 400).should == "" + end + + it "does not reset the buffer when reading with maxlen = 0" do + buffer = "foo" + @file.pread(0, 4, buffer) + buffer.should == "foo" + + @file.pread(0, 400, buffer) + buffer.should == "foo" + end + + it "converts maxlen to Integer using #to_int" do + maxlen = mock('maxlen') + maxlen.should_receive(:to_int).and_return(4) + @file.pread(maxlen, 0).should == "1234" + end + + it "converts offset to Integer using #to_int" do + offset = mock('offset') + offset.should_receive(:to_int).and_return(0) + @file.pread(4, offset).should == "1234" + end + + it "converts a buffer to String using to_str" do + buffer = mock('buffer') + buffer.should_receive(:to_str).at_least(1).and_return("foo") + @file.pread(4, 0, buffer) + buffer.should_not.is_a?(String) + buffer.to_str.should == "1234" + end + + it "raises TypeError if maxlen is not an Integer and cannot be coerced into Integer" do + maxlen = Object.new + -> { @file.pread(maxlen, 0) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises TypeError if offset is not an Integer and cannot be coerced into Integer" do + offset = Object.new + -> { @file.pread(4, offset) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises ArgumentError for negative values of maxlen" do + -> { @file.pread(-4, 0) }.should raise_error(ArgumentError, 'negative string size (or size too big)') + end + + it "raised Errno::EINVAL for negative values of offset" do + -> { @file.pread(4, -1) }.should raise_error(Errno::EINVAL, /Invalid argument/) + end + + it "raises TypeError if the buffer is not a String and cannot be coerced into String" do + buffer = Object.new + -> { @file.pread(4, 0, buffer) }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + it "raises EOFError if end-of-file is reached" do -> { @file.pread(1, 10) }.should raise_error(EOFError) end diff --git a/spec/ruby/core/io/pwrite_spec.rb b/spec/ruby/core/io/pwrite_spec.rb index c10578a8eb1..00d40db28d7 100644 --- a/spec/ruby/core/io/pwrite_spec.rb +++ b/spec/ruby/core/io/pwrite_spec.rb @@ -28,16 +28,42 @@ @file.pread(6, 0).should == "foobar" end + it "calls #to_s on the object to be written" do + object = mock("to_s") + object.should_receive(:to_s).and_return("foo") + @file.pwrite(object, 0) + @file.pread(3, 0).should == "foo" + end + + it "calls #to_int on the offset" do + offset = mock("to_int") + offset.should_receive(:to_int).and_return(2) + @file.pwrite("foo", offset) + @file.pread(3, 2).should == "foo" + end + it "raises IOError when file is not open in write mode" do File.open(@fname, "r") do |file| - -> { file.pwrite("foo", 1) }.should raise_error(IOError) + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "not opened for writing") end end it "raises IOError when file is closed" do file = File.open(@fname, "w+") file.close - -> { file.pwrite("foo", 1) }.should raise_error(IOError) + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "closed stream") + end + + it "raises a NoMethodError if object does not respond to #to_s" do + -> { + @file.pwrite(BasicObject.new, 0) + }.should raise_error(NoMethodError, /undefined method `to_s'/) + end + + it "raises a TypeError if the offset cannot be converted to an Integer" do + -> { + @file.pwrite("foo", Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") end end end diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb index bd22fb6d6ea..996f70bf209 100644 --- a/spec/ruby/core/io/read_spec.rb +++ b/spec/ruby/core/io/read_spec.rb @@ -113,6 +113,15 @@ IO.read(@fname, 1, 10).should == nil end + it "returns an empty string when reading zero bytes" do + IO.read(@fname, 0).should == '' + end + + it "returns a String in BINARY when passed a size" do + IO.read(@fname, 1).encoding.should == Encoding::BINARY + IO.read(@fname, 0).encoding.should == Encoding::BINARY + end + it "raises an Errno::ENOENT when the requested file does not exist" do rm_r @fname -> { IO.read @fname }.should raise_error(Errno::ENOENT) @@ -274,6 +283,14 @@ @io.read(4).should == '7890' end + it "treats first nil argument as no length limit" do + @io.read(nil).should == @contents + end + + it "raises an ArgumentError when not passed a valid length" do + -> { @io.read(-1) }.should raise_error(ArgumentError) + end + it "clears the output buffer if there is nothing to read" do @io.pos = 10 @@ -565,6 +582,7 @@ it "returns a String in BINARY when passed a size" do @io.read(4).encoding.should equal(Encoding::BINARY) + @io.read(0).encoding.should equal(Encoding::BINARY) end it "does not change the buffer's encoding when passed a limit" do diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb index dbc0178dd63..aca622834f3 100644 --- a/spec/ruby/core/io/shared/each.rb +++ b/spec/ruby/core/io/shared/each.rb @@ -33,10 +33,6 @@ $_.should == "test" end - it "returns self" do - @io.send(@method) { |l| l }.should equal(@io) - end - it "raises an IOError when self is not readable" do -> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError) end diff --git a/spec/ruby/core/kernel/exec_spec.rb b/spec/ruby/core/kernel/exec_spec.rb index 1b4a7ae6f48..3d9520ad67a 100644 --- a/spec/ruby/core/kernel/exec_spec.rb +++ b/spec/ruby/core/kernel/exec_spec.rb @@ -7,12 +7,12 @@ end it "runs the specified command, replacing current process" do - ruby_exe('exec "echo hello"; puts "fail"', escape: true).should == "hello\n" + ruby_exe('exec "echo hello"; puts "fail"').should == "hello\n" end end describe "Kernel.exec" do it "runs the specified command, replacing current process" do - ruby_exe('Kernel.exec "echo hello"; puts "fail"', escape: true).should == "hello\n" + ruby_exe('Kernel.exec "echo hello"; puts "fail"').should == "hello\n" end end diff --git a/spec/ruby/core/kernel/lambda_spec.rb b/spec/ruby/core/kernel/lambda_spec.rb index 2f7abc749cb..565536ac0d6 100644 --- a/spec/ruby/core/kernel/lambda_spec.rb +++ b/spec/ruby/core/kernel/lambda_spec.rb @@ -26,42 +26,44 @@ l.lambda?.should be_true end - it "creates a lambda-style Proc if given a literal block via Kernel.public_send" do - suppress_warning do - l = Kernel.public_send(:lambda) { 42 } - l.lambda?.should be_true + ruby_version_is ""..."3.3" do + it "creates a lambda-style Proc if given a literal block via Kernel.public_send" do + suppress_warning do + l = Kernel.public_send(:lambda) { 42 } + l.lambda?.should be_true + end end - end - it "returns the passed Proc if given an existing Proc" do - some_proc = proc {} - l = suppress_warning {lambda(&some_proc)} - l.should equal(some_proc) - l.lambda?.should be_false - end + it "returns the passed Proc if given an existing Proc" do + some_proc = proc {} + l = suppress_warning {lambda(&some_proc)} + l.should equal(some_proc) + l.lambda?.should be_false + end - it "creates a lambda-style Proc when called with zsuper" do - suppress_warning do - l = KernelSpecs::LambdaSpecs::ForwardBlockWithZSuper.new.lambda { 42 } - l.lambda?.should be_true - l.call.should == 42 + it "creates a lambda-style Proc when called with zsuper" do + suppress_warning do + l = KernelSpecs::LambdaSpecs::ForwardBlockWithZSuper.new.lambda { 42 } + l.lambda?.should be_true + l.call.should == 42 - lambda { l.call(:extra) }.should raise_error(ArgumentError) + lambda { l.call(:extra) }.should raise_error(ArgumentError) + end end - end - it "returns the passed Proc if given an existing Proc through super" do - some_proc = proc { } - l = KernelSpecs::LambdaSpecs::SuperAmpersand.new.lambda(&some_proc) - l.should equal(some_proc) - l.lambda?.should be_false - end + it "returns the passed Proc if given an existing Proc through super" do + some_proc = proc { } + l = KernelSpecs::LambdaSpecs::SuperAmpersand.new.lambda(&some_proc) + l.should equal(some_proc) + l.lambda?.should be_false + end - it "does not create lambda-style Procs when captured with #method" do - kernel_lambda = method(:lambda) - l = suppress_warning {kernel_lambda.call { 42 }} - l.lambda?.should be_false - l.call(:extra).should == 42 + it "does not create lambda-style Procs when captured with #method" do + kernel_lambda = method(:lambda) + l = suppress_warning {kernel_lambda.call { 42 }} + l.lambda?.should be_false + l.call(:extra).should == 42 + end end it "checks the arity of the call when no args are specified" do @@ -137,8 +139,16 @@ def ret end context "when called without a literal block" do - it "warns when proc isn't a lambda" do - -> { lambda(&proc{}) }.should complain("#{__FILE__}:#{__LINE__}: warning: lambda without a literal block is deprecated; use the proc without lambda instead\n") + ruby_version_is ""..."3.3" do + it "warns when proc isn't a lambda" do + -> { lambda(&proc{}) }.should complain("#{__FILE__}:#{__LINE__}: warning: lambda without a literal block is deprecated; use the proc without lambda instead\n") + end + end + + ruby_version_is "3.3" do + it "raises when proc isn't a lambda" do + -> { lambda(&proc{}) }.should raise_error(ArgumentError, /the lambda method requires a literal block/) + end end it "doesn't warn when proc is lambda" do diff --git a/spec/ruby/core/kernel/printf_spec.rb b/spec/ruby/core/kernel/printf_spec.rb index d8f93ce4293..61bf955c259 100644 --- a/spec/ruby/core/kernel/printf_spec.rb +++ b/spec/ruby/core/kernel/printf_spec.rb @@ -31,6 +31,13 @@ object.should_receive(:write).with("string") Kernel.printf(object, "%s", "string") end + + it "calls #to_str to convert the format object to a String" do + object = mock('format string') + object.should_receive(:to_str).and_return("to_str: %i") + $stdout.should_receive(:write).with("to_str: 42") + Kernel.printf($stdout, object, 42) + end end describe "Kernel.printf" do diff --git a/spec/ruby/core/kernel/shared/load.rb b/spec/ruby/core/kernel/shared/load.rb index 5c41c19bf6c..0fe2d5ce167 100644 --- a/spec/ruby/core/kernel/shared/load.rb +++ b/spec/ruby/core/kernel/shared/load.rb @@ -1,5 +1,6 @@ main = self +# The big difference is Kernel#load does not attempt to add an extension to the passed path, unlike Kernel#require describe :kernel_load, shared: true do before :each do CodeLoadingSpecs.spec_setup @@ -10,22 +11,31 @@ CodeLoadingSpecs.spec_cleanup end - it "loads a non-extensioned file as a Ruby source file" do - path = File.expand_path "load_fixture", CODE_LOADING_DIR - @object.load(path).should be_true - ScratchPad.recorded.should == [:no_ext] - end + describe "(path resolution)" do + # This behavior is specific to Kernel#load, it differs for Kernel#require + it "loads a non-extensioned file as a Ruby source file" do + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.load(path).should be_true + ScratchPad.recorded.should == [:no_ext] + end - it "loads a non .rb extensioned file as a Ruby source file" do - path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR - @object.load(path).should be_true - ScratchPad.recorded.should == [:no_rb_ext] - end + it "loads a non .rb extensioned file as a Ruby source file" do + path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR + @object.load(path).should be_true + ScratchPad.recorded.should == [:no_rb_ext] + end - it "loads from the current working directory" do - Dir.chdir CODE_LOADING_DIR do - @object.load("load_fixture.rb").should be_true - ScratchPad.recorded.should == [:loaded] + it "loads from the current working directory" do + Dir.chdir CODE_LOADING_DIR do + @object.load("load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + end + + # This behavior is specific to Kernel#load, it differs for Kernel#require + it "does not look for a c-extension file when passed a path without extension (when no .rb is present)" do + path = File.join CODE_LOADING_DIR, "a", "load_fixture" + -> { @object.send(@method, path) }.should raise_error(LoadError) end end diff --git a/spec/ruby/core/kernel/shared/require.rb b/spec/ruby/core/kernel/shared/require.rb index 61081b200c3..250813191b8 100644 --- a/spec/ruby/core/kernel/shared/require.rb +++ b/spec/ruby/core/kernel/shared/require.rb @@ -212,6 +212,34 @@ describe :kernel_require, shared: true do describe "(path resolution)" do + it "loads .rb file when passed absolute path without extension" do + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.send(@method, path).should be_true + # This should _not_ be [:no_ext] + ScratchPad.recorded.should == [:loaded] + end + + platform_is :linux, :darwin do + it "loads c-extension file when passed absolute path without extension when no .rb is present" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture" + -> { @object.send(@method, path) }.should raise_error(Exception, /file too short|not a mach-o file/) + end + end + + platform_is :darwin do + it "loads .bundle file when passed absolute path with .so" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture.so" + -> { @object.send(@method, path) }.should raise_error(Exception, /load_fixture\.bundle.+(file too short|not a mach-o file)/) + end + end + + it "does not try an extra .rb if the path already ends in .rb" do + path = File.join CODE_LOADING_DIR, "d", "load_fixture.rb" + -> { @object.send(@method, path) }.should raise_error(LoadError) + end + # For reference see [ruby-core:24155] in which matz confirms this feature is # intentional for security reasons. it "does not load a bare filename unless the current working directory is in $LOAD_PATH" do diff --git a/spec/ruby/core/kernel/sleep_spec.rb b/spec/ruby/core/kernel/sleep_spec.rb index 44b417a92e8..0570629723b 100644 --- a/spec/ruby/core/kernel/sleep_spec.rb +++ b/spec/ruby/core/kernel/sleep_spec.rb @@ -1,5 +1,4 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' describe "Kernel#sleep" do it "is a private method" do diff --git a/spec/ruby/core/kernel/sprintf_spec.rb b/spec/ruby/core/kernel/sprintf_spec.rb index 7adf71be76f..9ef7f86f16d 100644 --- a/spec/ruby/core/kernel/sprintf_spec.rb +++ b/spec/ruby/core/kernel/sprintf_spec.rb @@ -3,6 +3,14 @@ require_relative 'shared/sprintf' require_relative 'shared/sprintf_encoding' +describe :kernel_sprintf_to_str, shared: true do + it "calls #to_str to convert the format object to a String" do + obj = mock('format string') + obj.should_receive(:to_str).and_return("to_str: %i") + @method.call(obj, 42).should == "to_str: 42" + end +end + describe "Kernel#sprintf" do it_behaves_like :kernel_sprintf, -> format, *args { sprintf(format, *args) @@ -11,6 +19,10 @@ it_behaves_like :kernel_sprintf_encoding, -> format, *args { sprintf(format, *args) } + + it_behaves_like :kernel_sprintf_to_str, -> format, *args { + sprintf(format, *args) + } end describe "Kernel.sprintf" do @@ -21,4 +33,8 @@ it_behaves_like :kernel_sprintf_encoding, -> format, *args { Kernel.sprintf(format, *args) } + + it_behaves_like :kernel_sprintf_to_str, -> format, *args { + Kernel.sprintf(format, *args) + } end diff --git a/spec/ruby/core/matchdata/values_at_spec.rb b/spec/ruby/core/matchdata/values_at_spec.rb index 4fd0bfc42ab..535719a2ee3 100644 --- a/spec/ruby/core/matchdata/values_at_spec.rb +++ b/spec/ruby/core/matchdata/values_at_spec.rb @@ -1,6 +1,6 @@ require_relative '../../spec_helper' -describe "Struct#values_at" do +describe "MatchData#values_at" do # Should be synchronized with core/array/values_at_spec.rb and core/struct/values_at_spec.rb # # /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").to_a # => ["HX1138", "H", "X", "113", "8"] @@ -34,7 +34,7 @@ end it "supports beginningless Range" do - /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..2).should == ["HX1138", "H", "X"] + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(..2).should == ["HX1138", "H", "X"] end it "returns an empty Array when Range is empty" do diff --git a/spec/ruby/core/method/super_method_spec.rb b/spec/ruby/core/method/super_method_spec.rb index f9a18f38785..c63a7aaa0fc 100644 --- a/spec/ruby/core/method/super_method_spec.rb +++ b/spec/ruby/core/method/super_method_spec.rb @@ -55,12 +55,10 @@ def overridden; end end end - ruby_version_is "2.7.3" do - context "after aliasing an inherited method" do - it "returns the expected super_method" do - method = MethodSpecs::InheritedMethods::C.new.method(:meow) - method.super_method.owner.should == MethodSpecs::InheritedMethods::A - end + context "after aliasing an inherited method" do + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.new.method(:meow) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A end end end diff --git a/spec/ruby/core/module/define_method_spec.rb b/spec/ruby/core/module/define_method_spec.rb index d742ece6bc6..e04bb87ceb0 100644 --- a/spec/ruby/core/module/define_method_spec.rb +++ b/spec/ruby/core/module/define_method_spec.rb @@ -724,7 +724,7 @@ def foo; end end end -describe "Method#define_method when passed a Method object" do +describe "Module#define_method when passed a Method object" do before :each do @klass = Class.new do def m(a, b, *c) @@ -749,7 +749,7 @@ def m(a, b, *c) end end -describe "Method#define_method when passed an UnboundMethod object" do +describe "Module#define_method when passed an UnboundMethod object" do before :each do @klass = Class.new do def m(a, b, *c) @@ -774,7 +774,7 @@ def m(a, b, *c) end end -describe "Method#define_method when passed a Proc object" do +describe "Module#define_method when passed a Proc object" do describe "and a method is defined inside" do it "defines the nested method in the default definee where the Proc was created" do prc = nil @@ -799,7 +799,7 @@ def nested_method_in_proc_for_define_method end end -describe "Method#define_method when passed a block" do +describe "Module#define_method when passed a block" do describe "behaves exactly like a lambda" do it "for return" do Class.new do diff --git a/spec/ruby/core/proc/lambda_spec.rb b/spec/ruby/core/proc/lambda_spec.rb index b2d3f503502..5c3c38fc2a6 100644 --- a/spec/ruby/core/proc/lambda_spec.rb +++ b/spec/ruby/core/proc/lambda_spec.rb @@ -14,9 +14,11 @@ Proc.new {}.lambda?.should be_false end - it "is preserved when passing a Proc with & to the lambda keyword" do - suppress_warning {lambda(&->{})}.lambda?.should be_true - suppress_warning {lambda(&proc{})}.lambda?.should be_false + ruby_version_is ""..."3.3" do + it "is preserved when passing a Proc with & to the lambda keyword" do + suppress_warning {lambda(&->{})}.lambda?.should be_true + suppress_warning {lambda(&proc{})}.lambda?.should be_false + end end it "is preserved when passing a Proc with & to the proc keyword" do diff --git a/spec/ruby/core/process/constants_spec.rb b/spec/ruby/core/process/constants_spec.rb index 4130bb58a55..616c54b8e1b 100644 --- a/spec/ruby/core/process/constants_spec.rb +++ b/spec/ruby/core/process/constants_spec.rb @@ -56,12 +56,18 @@ end platform_is :netbsd, :freebsd do - it "Process::RLIMIT_SBSIZE" do + it "has the correct constant values on NetBSD and FreeBSD" do Process::RLIMIT_SBSIZE.should == 9 # FIXME: what's it equal? Process::RLIMIT_AS.should == 10 end end + platform_is :freebsd do + it "has the correct constant values on FreeBSD" do + Process::RLIMIT_NPTS.should == 11 + end + end + platform_is :windows do it "does not define RLIMIT constants" do %i[ diff --git a/spec/ruby/core/process/exec_spec.rb b/spec/ruby/core/process/exec_spec.rb index 2fa8b089750..0f371b39c81 100644 --- a/spec/ruby/core/process/exec_spec.rb +++ b/spec/ruby/core/process/exec_spec.rb @@ -34,16 +34,16 @@ end it "runs the specified command, replacing current process" do - ruby_exe('Process.exec "echo hello"; puts "fail"', escape: true).should == "hello\n" + ruby_exe('Process.exec "echo hello"; puts "fail"').should == "hello\n" end it "sets the current directory when given the :chdir option" do tmpdir = tmp("")[0..-2] platform_is_not :windows do - ruby_exe("Process.exec(\"pwd\", chdir: #{tmpdir.inspect})", escape: true).should == "#{tmpdir}\n" + ruby_exe("Process.exec(\"pwd\", chdir: #{tmpdir.inspect})").should == "#{tmpdir}\n" end platform_is :windows do - ruby_exe("Process.exec(\"cd\", chdir: #{tmpdir.inspect})", escape: true).tr('\\', '/').should == "#{tmpdir}\n" + ruby_exe("Process.exec(\"cd\", chdir: #{tmpdir.inspect})").tr('\\', '/').should == "#{tmpdir}\n" end end @@ -73,13 +73,13 @@ platform_is_not :windows do it "subjects the specified command to shell expansion" do result = Dir.chdir(@dir) do - ruby_exe('Process.exec "echo *"', escape: true) + ruby_exe('Process.exec "echo *"') end result.chomp.should == @name end it "creates an argument array with shell parsing semantics for whitespace" do - ruby_exe('Process.exec "echo a b c d"', escape: true).should == "a b c d\n" + ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n" end end @@ -87,13 +87,13 @@ # There is no shell expansion on Windows it "does not subject the specified command to shell expansion on Windows" do result = Dir.chdir(@dir) do - ruby_exe('Process.exec "echo *"', escape: true) + ruby_exe('Process.exec "echo *"') end result.should == "*\n" end it "does not create an argument array with shell parsing semantics for whitespace on Windows" do - ruby_exe('Process.exec "echo a b c d"', escape: true).should == "a b c d\n" + ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n" end end @@ -105,7 +105,7 @@ platform_is :windows do cmd = '"cmd.exe", "/C", "echo", "*"' end - ruby_exe("Process.exec #{cmd}", escape: true).should == "*\n" + ruby_exe("Process.exec #{cmd}").should == "*\n" end end @@ -124,29 +124,29 @@ end it "sets environment variables in the child environment" do - ruby_exe('Process.exec({"FOO" => "BAR"}, "echo ' + var + '")', escape: true).should == "BAR\n" + ruby_exe('Process.exec({"FOO" => "BAR"}, "echo ' + var + '")').should == "BAR\n" end it "unsets environment variables whose value is nil" do platform_is_not :windows do - ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")', escape: true).should == "\n" + ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == "\n" end platform_is :windows do # On Windows, echo-ing a non-existent env var is treated as echo-ing any other string of text - ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")', escape: true).should == var + "\n" + ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == var + "\n" end end it "coerces environment argument using to_hash" do - ruby_exe('o = Object.new; def o.to_hash; {"FOO" => "BAR"}; end; Process.exec(o, "echo ' + var + '")', escape: true).should == "BAR\n" + ruby_exe('o = Object.new; def o.to_hash; {"FOO" => "BAR"}; end; Process.exec(o, "echo ' + var + '")').should == "BAR\n" end it "unsets other environment variables when given a true :unsetenv_others option" do platform_is_not :windows do - ruby_exe('Process.exec("echo ' + var + '", unsetenv_others: true)', escape: true).should == "\n" + ruby_exe('Process.exec("echo ' + var + '", unsetenv_others: true)').should == "\n" end platform_is :windows do - ruby_exe('Process.exec("' + ENV['COMSPEC'].gsub('\\', '\\\\\\') + ' /C echo ' + var + '", unsetenv_others: true)', escape: true).should == var + "\n" + ruby_exe('Process.exec("' + ENV['COMSPEC'].gsub('\\', '\\\\\\') + ' /C echo ' + var + '", unsetenv_others: true)').should == var + "\n" end end end @@ -154,19 +154,19 @@ describe "with a command array" do it "uses the first element as the command name and the second as the argv[0] value" do platform_is_not :windows do - ruby_exe('Process.exec(["/bin/sh", "argv_zero"], "-c", "echo $0")', escape: true).should == "argv_zero\n" + ruby_exe('Process.exec(["/bin/sh", "argv_zero"], "-c", "echo $0")').should == "argv_zero\n" end platform_is :windows do - ruby_exe('Process.exec(["cmd.exe", "/C"], "/C", "echo", "argv_zero")', escape: true).should == "argv_zero\n" + ruby_exe('Process.exec(["cmd.exe", "/C"], "/C", "echo", "argv_zero")').should == "argv_zero\n" end end it "coerces the argument using to_ary" do platform_is_not :windows do - ruby_exe('o = Object.new; def o.to_ary; ["/bin/sh", "argv_zero"]; end; Process.exec(o, "-c", "echo $0")', escape: true).should == "argv_zero\n" + ruby_exe('o = Object.new; def o.to_ary; ["/bin/sh", "argv_zero"]; end; Process.exec(o, "-c", "echo $0")').should == "argv_zero\n" end platform_is :windows do - ruby_exe('o = Object.new; def o.to_ary; ["cmd.exe", "/C"]; end; Process.exec(o, "/C", "echo", "argv_zero")', escape: true).should == "argv_zero\n" + ruby_exe('o = Object.new; def o.to_ary; ["cmd.exe", "/C"]; end; Process.exec(o, "/C", "echo", "argv_zero")').should == "argv_zero\n" end end @@ -200,7 +200,7 @@ end EOC - ruby_exe(cmd, escape: true) + ruby_exe(cmd) child_fd = IO.read(@child_fd_file).to_i child_fd.to_i.should > STDERR.fileno @@ -216,7 +216,7 @@ Process.exec("#{ruby_cmd(map_fd_fixture)} \#{f.fileno}", f.fileno => f.fileno) EOC - output = ruby_exe(cmd, escape: true) + output = ruby_exe(cmd) child_fd, close_on_exec = output.split child_fd.to_i.should > STDERR.fileno @@ -232,7 +232,7 @@ puts(f.close_on_exec?) EOC - output = ruby_exe(cmd, escape: true) + output = ruby_exe(cmd) output.split.should == ['true', 'false'] end end diff --git a/spec/ruby/core/process/times_spec.rb b/spec/ruby/core/process/times_spec.rb index d2610f64157..d3bff2cda9f 100644 --- a/spec/ruby/core/process/times_spec.rb +++ b/spec/ruby/core/process/times_spec.rb @@ -16,31 +16,4 @@ Process.times.utime.should > user end end - - # TODO: The precision of `getrusage` depends on platforms (OpenBSD - # seems not supporting under-milliseconds in fact); this example is - # very questionable as an example of Ruby, and it just repeats the - # guard condition. - guard -> do - 1000.times.any? do - # If getrusage has precision beyond milliseconds, there will be - # very likely at least one non-zero microsecond results when - # repeating enough. - time = Process.clock_gettime(:GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID) - not ('%.6f' % time).end_with?('000') - end - rescue Errno::EINVAL - false - end do - it "uses getrusage when available to improve precision beyond milliseconds" do - max = 10_000 - - found = (max * 100).times.find do - time = Process.times.utime - !('%.6f' % time).end_with?('000') - end - - found.should_not == nil - end - end end diff --git a/spec/ruby/core/regexp/union_spec.rb b/spec/ruby/core/regexp/union_spec.rb index 80768364710..ea5a5053f72 100644 --- a/spec/ruby/core/regexp/union_spec.rb +++ b/spec/ruby/core/regexp/union_spec.rb @@ -43,6 +43,27 @@ Regexp.union("\u00A9".encode("ISO-8859-1"), "a".encode("UTF-8")).encoding.should == Encoding::ISO_8859_1 end + it "returns ASCII-8BIT if the regexp encodings are ASCII-8BIT and at least one has non-ASCII characters" do + us_ascii_implicit, us_ascii_explicit, binary = /abc/, /[\x00-\x7f]/n, /[\x80-\xBF]/n + us_ascii_implicit.encoding.should == Encoding::US_ASCII + us_ascii_explicit.encoding.should == Encoding::US_ASCII + binary.encoding.should == Encoding::BINARY + + Regexp.union(us_ascii_implicit, us_ascii_explicit, binary).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_implicit, binary, us_ascii_explicit).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_explicit, us_ascii_implicit, binary).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_explicit, binary, us_ascii_implicit).encoding.should == Encoding::BINARY + Regexp.union(binary, us_ascii_implicit, us_ascii_explicit).encoding.should == Encoding::BINARY + Regexp.union(binary, us_ascii_explicit, us_ascii_implicit).encoding.should == Encoding::BINARY + end + + it "return US-ASCII if all patterns are ASCII-only" do + Regexp.union(/abc/e, /def/e).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/n, /def/n).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/s, /def/s).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/u, /def/u).encoding.should == Encoding::US_ASCII + end + it "returns a Regexp with UTF-8 if one part is UTF-8" do Regexp.union(/probl[éeè]me/i, /help/i).encoding.should == Encoding::UTF_8 end @@ -54,83 +75,83 @@ it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Strings" do -> { Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-16BE")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE') end it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Regexps" do -> { Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("b".encode("UTF-16BE"))) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE') end it "raises ArgumentError if the arguments include conflicting fixed encoding Regexps" do -> { Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING), Regexp.new("b".encode("US-ASCII"), Regexp::FIXEDENCODING)) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-8 and US-ASCII') end it "raises ArgumentError if the arguments include a fixed encoding Regexp and a String containing non-ASCII-compatible characters in a different encoding" do -> { Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING), "\u00A9".encode("ISO-8859-1")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-8 and ISO-8859-1') end it "raises ArgumentError if the arguments include a String containing non-ASCII-compatible characters and a fixed encoding Regexp in a different encoding" do -> { Regexp.union("\u00A9".encode("ISO-8859-1"), Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING)) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: ISO-8859-1 and UTF-8') end it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only String" do -> { Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-8")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) end it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only String" do -> { Regexp.union(Regexp.new("a".encode("UTF-16LE")), "b".encode("UTF-8")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) end it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only Regexp" do -> { Regexp.union("a".encode("UTF-16LE"), Regexp.new("b".encode("UTF-8"))) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) end it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only Regexp" do -> { Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("b".encode("UTF-8"))) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) end it "raises ArgumentError if the arguments include an ASCII-incompatible String and a String containing non-ASCII-compatible characters in a different encoding" do -> { Regexp.union("a".encode("UTF-16LE"), "\u00A9".encode("ISO-8859-1")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') end it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a String containing non-ASCII-compatible characters in a different encoding" do -> { Regexp.union(Regexp.new("a".encode("UTF-16LE")), "\u00A9".encode("ISO-8859-1")) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') end it "raises ArgumentError if the arguments include an ASCII-incompatible String and a Regexp containing non-ASCII-compatible characters in a different encoding" do -> { Regexp.union("a".encode("UTF-16LE"), Regexp.new("\u00A9".encode("ISO-8859-1"))) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') end it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a Regexp containing non-ASCII-compatible characters in a different encoding" do -> { Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("\u00A9".encode("ISO-8859-1"))) - }.should raise_error(ArgumentError) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') end it "uses to_str to convert arguments (if not Regexp)" do @@ -154,6 +175,8 @@ not_supported_on :opal do Regexp.union([/dogs/, /cats/i]).should == /(?-mix:dogs)|(?i-mx:cats)/ end - ->{Regexp.union(["skiing", "sledding"], [/dogs/, /cats/i])}.should raise_error(TypeError) + -> { + Regexp.union(["skiing", "sledding"], [/dogs/, /cats/i]) + }.should raise_error(TypeError, 'no implicit conversion of Array into String') end end diff --git a/spec/ruby/core/signal/signame_spec.rb b/spec/ruby/core/signal/signame_spec.rb index b66de9fc851..adfe895d97f 100644 --- a/spec/ruby/core/signal/signame_spec.rb +++ b/spec/ruby/core/signal/signame_spec.rb @@ -9,10 +9,22 @@ Signal.signame(-1).should == nil end + it "calls #to_int on an object to convert to an Integer" do + obj = mock('signal') + obj.should_receive(:to_int).and_return(0) + Signal.signame(obj).should == "EXIT" + end + it "raises a TypeError when the passed argument can't be coerced to Integer" do -> { Signal.signame("hello") }.should raise_error(TypeError) end + it "raises a TypeError when the passed argument responds to #to_int but does not return an Integer" do + obj = mock('signal') + obj.should_receive(:to_int).and_return('not an int') + -> { Signal.signame(obj) }.should raise_error(TypeError) + end + platform_is_not :windows do it "the original should take precedence over alias when looked up by number" do Signal.signame(Signal.list["ABRT"]).should == "ABRT" diff --git a/spec/ruby/core/signal/trap_spec.rb b/spec/ruby/core/signal/trap_spec.rb index 10e122e0721..b3186cda92b 100644 --- a/spec/ruby/core/signal/trap_spec.rb +++ b/spec/ruby/core/signal/trap_spec.rb @@ -221,6 +221,25 @@ Signal.trap(:HUP, @saved_trap).should equal(@proc) end + it "calls #to_str on an object to convert to a String" do + obj = mock("signal") + obj.should_receive(:to_str).exactly(2).times.and_return("HUP") + Signal.trap obj, @proc + Signal.trap(obj, @saved_trap).should equal(@proc) + end + + it "accepts Integer values" do + hup = Signal.list["HUP"] + Signal.trap hup, @proc + Signal.trap(hup, @saved_trap).should equal(@proc) + end + + it "does not call #to_int on an object to convert to an Integer" do + obj = mock("signal") + obj.should_not_receive(:to_int) + -> { Signal.trap obj, @proc }.should raise_error(ArgumentError, /bad signal type/) + end + it "raises ArgumentError when passed unknown signal" do -> { Signal.trap(300) { } }.should raise_error(ArgumentError, "invalid signal number (300)") -> { Signal.trap("USR10") { } }.should raise_error(ArgumentError, "unsupported signal `SIGUSR10'") diff --git a/spec/ruby/core/string/start_with_spec.rb b/spec/ruby/core/string/start_with_spec.rb index 81eed47f96e..35e33b46a66 100644 --- a/spec/ruby/core/string/start_with_spec.rb +++ b/spec/ruby/core/string/start_with_spec.rb @@ -11,7 +11,14 @@ "\xA9".should.start_with?("\xA9") # A9 is not a character head for UTF-8 end - ruby_bug "#19784", ""..."3.3" do + ruby_version_is ""..."3.3" do + it "does not check we are matching only part of a character" do + "\xe3\x81\x82".size.should == 1 + "\xe3\x81\x82".should.start_with?("\xe3") + end + end + + ruby_version_is "3.3" do # #19784 it "checks we are matching only part of a character" do "\xe3\x81\x82".size.should == 1 "\xe3\x81\x82".should_not.start_with?("\xe3") diff --git a/spec/ruby/core/thread/native_thread_id_spec.rb b/spec/ruby/core/thread/native_thread_id_spec.rb index 8460a1db8cf..17a08c8a150 100644 --- a/spec/ruby/core/thread/native_thread_id_spec.rb +++ b/spec/ruby/core/thread/native_thread_id_spec.rb @@ -19,8 +19,15 @@ main_thread_id = Thread.current.native_thread_id t_thread_id = t.native_thread_id - t_thread_id.should be_kind_of(Integer) + if ruby_version_is "3.3" + # native_thread_id can be nil on a M:N scheduler + t_thread_id.should be_kind_of(Integer) if t_thread_id != nil + else + t_thread_id.should be_kind_of(Integer) + end + main_thread_id.should_not == t_thread_id + t.run t.join t.native_thread_id.should == nil diff --git a/spec/ruby/core/unboundmethod/super_method_spec.rb b/spec/ruby/core/unboundmethod/super_method_spec.rb index 101c83b8b33..aa7c1293772 100644 --- a/spec/ruby/core/unboundmethod/super_method_spec.rb +++ b/spec/ruby/core/unboundmethod/super_method_spec.rb @@ -40,12 +40,10 @@ end end - ruby_version_is "2.7.3" do - context "after aliasing an inherited method" do - it "returns the expected super_method" do - method = MethodSpecs::InheritedMethods::C.instance_method(:meow) - method.super_method.owner.should == MethodSpecs::InheritedMethods::A - end + context "after aliasing an inherited method" do + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.instance_method(:meow) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A end end end diff --git a/spec/ruby/core/warning/element_reference_spec.rb b/spec/ruby/core/warning/element_reference_spec.rb index bd024d19b49..8cb4018c20b 100644 --- a/spec/ruby/core/warning/element_reference_spec.rb +++ b/spec/ruby/core/warning/element_reference_spec.rb @@ -1,11 +1,9 @@ require_relative '../../spec_helper' describe "Warning.[]" do - ruby_version_is '2.7.2' do - it "returns default values for categories :deprecated and :experimental" do - ruby_exe('p [Warning[:deprecated], Warning[:experimental]]').chomp.should == "[false, true]" - ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', options: "-w").chomp.should == "[true, true]" - end + it "returns default values for categories :deprecated and :experimental" do + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]').chomp.should == "[false, true]" + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', options: "-w").chomp.should == "[true, true]" end ruby_version_is '3.3' do diff --git a/spec/ruby/fixtures/code/d/load_fixture.rb.rb b/spec/ruby/fixtures/code/d/load_fixture.rb.rb new file mode 100644 index 00000000000..7e9217729a3 --- /dev/null +++ b/spec/ruby/fixtures/code/d/load_fixture.rb.rb @@ -0,0 +1 @@ +ScratchPad << :rbrb diff --git a/spec/ruby/language/alias_spec.rb b/spec/ruby/language/alias_spec.rb index ee928cbfaae..61fddb01849 100644 --- a/spec/ruby/language/alias_spec.rb +++ b/spec/ruby/language/alias_spec.rb @@ -252,7 +252,7 @@ def test_with_check(*args) it "on top level defines the alias on Object" do # because it defines on the default definee / current module - ruby_exe("def foo; end; alias bla foo; print method(:bla).owner", escape: true).should == "Object" + ruby_exe("def foo; end; alias bla foo; print method(:bla).owner").should == "Object" end it "raises a NameError when passed a missing name" do diff --git a/spec/ruby/language/case_spec.rb b/spec/ruby/language/case_spec.rb index 58e1aaed967..1a3925c9c6f 100644 --- a/spec/ruby/language/case_spec.rb +++ b/spec/ruby/language/case_spec.rb @@ -329,49 +329,6 @@ def bar; @calls << :bar; end 100 end.should == 100 end -end - -describe "The 'case'-construct with no target expression" do - it "evaluates the body of the first clause when at least one of its condition expressions is true" do - case - when true, false; 'foo' - end.should == 'foo' - end - - it "evaluates the body of the first when clause that is not false/nil" do - case - when false; 'foo' - when 2; 'bar' - when 1 == 1; 'baz' - end.should == 'bar' - - case - when false; 'foo' - when nil; 'foo' - when 1 == 1; 'bar' - end.should == 'bar' - end - - it "evaluates the body of the else clause if all when clauses are false/nil" do - case - when false; 'foo' - when nil; 'foo' - when 1 == 2; 'bar' - else 'baz' - end.should == 'baz' - end - - it "evaluates multiple conditional expressions as a boolean disjunction" do - case - when true, false; 'foo' - else 'bar' - end.should == 'foo' - - case - when false, true; 'foo' - else 'bar' - end.should == 'foo' - end it "evaluates true as only 'true' when true is the first clause" do case 1 @@ -442,6 +399,49 @@ def ===(o) :called end.should == :called end +end + +describe "The 'case'-construct with no target expression" do + it "evaluates the body of the first clause when at least one of its condition expressions is true" do + case + when true, false; 'foo' + end.should == 'foo' + end + + it "evaluates the body of the first when clause that is not false/nil" do + case + when false; 'foo' + when 2; 'bar' + when 1 == 1; 'baz' + end.should == 'bar' + + case + when false; 'foo' + when nil; 'foo' + when 1 == 1; 'bar' + end.should == 'bar' + end + + it "evaluates the body of the else clause if all when clauses are false/nil" do + case + when false; 'foo' + when nil; 'foo' + when 1 == 2; 'bar' + else 'baz' + end.should == 'baz' + end + + it "evaluates multiple conditional expressions as a boolean disjunction" do + case + when true, false; 'foo' + else 'bar' + end.should == 'foo' + + case + when false, true; 'foo' + else 'bar' + end.should == 'foo' + end # Homogeneous cases are often optimized to avoid === using a jump table, and should be tested separately. # See https://github.com/jruby/jruby/issues/6440 @@ -451,4 +451,13 @@ def ===(o) when 2; 'bar' end.should == 'foo' end + + it "expands arrays to lists of values" do + case + when *[false] + "foo" + when *[true] + "bar" + end.should == "bar" + end end diff --git a/spec/ruby/language/delegation_spec.rb b/spec/ruby/language/delegation_spec.rb index 3f24a79d5cb..020787aff60 100644 --- a/spec/ruby/language/delegation_spec.rb +++ b/spec/ruby/language/delegation_spec.rb @@ -38,28 +38,26 @@ def delegate(...) end end -ruby_version_is "2.7.3" do - describe "delegation with def(x, ...)" do - it "delegates rest and kwargs" do - a = Class.new(DelegationSpecs::Target) - a.class_eval(<<-RUBY) - def delegate(x, ...) - target(...) - end - RUBY +describe "delegation with def(x, ...)" do + it "delegates rest and kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(x, ...) + target(...) + end + RUBY - a.new.delegate(0, 1, b: 2).should == [[1], {b: 2}] - end + a.new.delegate(0, 1, b: 2).should == [[1], {b: 2}] + end - it "delegates block" do - a = Class.new(DelegationSpecs::Target) - a.class_eval(<<-RUBY) - def delegate_block(x, ...) - target_block(...) - end - RUBY + it "delegates block" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate_block(x, ...) + target_block(...) + end + RUBY - a.new.delegate_block(0, 1, b: 2) { |x| x }.should == [{b: 2}, [1]] - end + a.new.delegate_block(0, 1, b: 2) { |x| x }.should == [{b: 2}, [1]] end end diff --git a/spec/ruby/library/bigdecimal/to_s_spec.rb b/spec/ruby/library/bigdecimal/to_s_spec.rb index 63dfaf5e7c3..ba9f960eb32 100644 --- a/spec/ruby/library/bigdecimal/to_s_spec.rb +++ b/spec/ruby/library/bigdecimal/to_s_spec.rb @@ -39,19 +39,17 @@ @bigneg.to_s("+").should_not =~ /^\+.*/ end - ruby_version_is ""..."3.3" do - it "inserts a space every n chars to fraction part, if integer n is supplied" do - re =\ - /\A0\.314 159 265 358 979 323 846 264 338 327 950 288 419 716 939 937E1\z/i - @bigdec.to_s(3).should =~ re - - str1 = '-123.45678 90123 45678 9' - BigDecimal("-123.45678901234567890").to_s('5F').should == str1 - # trailing zeroes removed - BigDecimal("1.00000000000").to_s('1F').should == "1.0" - # 0 is treated as no spaces - BigDecimal("1.2345").to_s('0F').should == "1.2345" - end + it "inserts a space every n chars to fraction part, if integer n is supplied" do + re =\ + /\A0\.314 159 265 358 979 323 846 264 338 327 950 288 419 716 939 937E1\z/i + @bigdec.to_s(3).should =~ re + + str1 = '-123.45678 90123 45678 9' + BigDecimal("-123.45678901234567890").to_s('5F').should == str1 + # trailing zeroes removed + BigDecimal("1.00000000000").to_s('1F').should == "1.0" + # 0 is treated as no spaces + BigDecimal("1.2345").to_s('0F').should == "1.2345" end version_is BigDecimal::VERSION, "3.1.5" do #ruby_version_is '3.3' do diff --git a/spec/ruby/library/cgi/escapeURIComponent_spec.rb b/spec/ruby/library/cgi/escapeURIComponent_spec.rb new file mode 100644 index 00000000000..2cf283c7786 --- /dev/null +++ b/spec/ruby/library/cgi/escapeURIComponent_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require 'cgi' + +ruby_version_is "3.2" do + describe "CGI.escapeURIComponent" do + it "escapes whitespace" do + string = "&<>\" \xE3\x82\x86\xE3\x82\x93\xE3\x82\x86\xE3\x82\x93" + CGI.escapeURIComponent(string).should == '%26%3C%3E%22%20%E3%82%86%E3%82%93%E3%82%86%E3%82%93' + end + + it "does not escape with unreserved characters" do + string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + CGI.escapeURIComponent(string).should == "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + end + + it "supports String with invalid encoding" do + string = "\xC0\<\<".force_encoding("UTF-8") + CGI.escapeURIComponent(string).should == "%C0%3C%3C" + end + + it "processes String bytes one by one, not characters" do + CGI.escapeURIComponent("β").should == "%CE%B2" # "β" bytes representation is CE B2 + end + + it "raises a TypeError with nil" do + -> { + CGI.escapeURIComponent(nil) + }.should raise_error(TypeError, 'no implicit conversion of nil into String') + end + + it "encodes empty string" do + CGI.escapeURIComponent("").should == "" + end + + it "encodes single whitespace" do + CGI.escapeURIComponent(" ").should == "%20" + end + + it "encodes double whitespace" do + CGI.escapeURIComponent(" ").should == "%20%20" + end + + it "preserves encoding" do + string = "whatever".encode("ASCII-8BIT") + CGI.escapeURIComponent(string).encoding.should == Encoding::ASCII_8BIT + end + + it "uses implicit type conversion to String" do + object = Object.new + def object.to_str + "a b" + end + + CGI.escapeURIComponent(object).should == "a%20b" + end + end +end diff --git a/spec/ruby/library/cgi/initialize_spec.rb b/spec/ruby/library/cgi/initialize_spec.rb index f794f157f08..61bc971d491 100644 --- a/spec/ruby/library/cgi/initialize_spec.rb +++ b/spec/ruby/library/cgi/initialize_spec.rb @@ -29,8 +29,8 @@ it "does not extend self with any of the other HTML modules" do @cgi.send(:initialize) - @cgi.should_not be_kind_of(CGI::Html3) @cgi.should_not be_kind_of(CGI::HtmlExtension) + @cgi.should_not be_kind_of(CGI::Html3) @cgi.should_not be_kind_of(CGI::Html4) @cgi.should_not be_kind_of(CGI::Html4Tr) @cgi.should_not be_kind_of(CGI::Html4Fr) diff --git a/spec/ruby/library/datetime/rfc2822_spec.rb b/spec/ruby/library/datetime/rfc2822_spec.rb index 70bfca60b4b..83f7fa8d5b1 100644 --- a/spec/ruby/library/datetime/rfc2822_spec.rb +++ b/spec/ruby/library/datetime/rfc2822_spec.rb @@ -3,4 +3,8 @@ describe "DateTime.rfc2822" do it "needs to be reviewed for spec completeness" + + it "raises DateError if passed nil" do + -> { DateTime.rfc2822(nil) }.should raise_error(Date::Error, "invalid date") + end end diff --git a/spec/ruby/library/openssl/digest/append_spec.rb b/spec/ruby/library/openssl/digest/append_spec.rb new file mode 100644 index 00000000000..08802b72534 --- /dev/null +++ b/spec/ruby/library/openssl/digest/append_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/update' + +describe "OpenSSL::Digest#<<" do + it_behaves_like :openssl_digest_update, :<< +end diff --git a/spec/ruby/library/openssl/digest/block_length_spec.rb b/spec/ruby/library/openssl/digest/block_length_spec.rb new file mode 100644 index 00000000000..444ed9d20d8 --- /dev/null +++ b/spec/ruby/library/openssl/digest/block_length_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../../spec_helper' +require_relative '../../../library/digest/sha1/shared/constants' +require_relative '../../../library/digest/sha256/shared/constants' +require_relative '../../../library/digest/sha384/shared/constants' +require_relative '../../../library/digest/sha512/shared/constants' +require 'openssl' + +describe "OpenSSL::Digest#block_length" do + context "when the digest object is created via a name argument" do + it "returns a SHA1 block length" do + OpenSSL::Digest.new('sha1').block_length.should == SHA1Constants::BlockLength + end + + it "returns a SHA256 block length" do + OpenSSL::Digest.new('sha256').block_length.should == SHA256Constants::BlockLength + end + + it "returns a SHA384 block length" do + OpenSSL::Digest.new('sha384').block_length.should == SHA384Constants::BlockLength + end + + it "returns a SHA512 block length" do + OpenSSL::Digest.new('sha512').block_length.should == SHA512Constants::BlockLength + end + end + + context "when the digest object is created via a subclass" do + it "returns a SHA1 block length" do + OpenSSL::Digest::SHA1.new.block_length.should == SHA1Constants::BlockLength + end + + it "returns a SHA256 block length" do + OpenSSL::Digest::SHA256.new.block_length.should == SHA256Constants::BlockLength + end + + it "returns a SHA384 block length" do + OpenSSL::Digest::SHA384.new.block_length.should == SHA384Constants::BlockLength + end + + it "returns a SHA512 block length" do + OpenSSL::Digest::SHA512.new.block_length.should == SHA512Constants::BlockLength + end + end +end diff --git a/spec/ruby/library/openssl/digest/digest_length_spec.rb b/spec/ruby/library/openssl/digest/digest_length_spec.rb new file mode 100644 index 00000000000..37d1cba9a7b --- /dev/null +++ b/spec/ruby/library/openssl/digest/digest_length_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../../spec_helper' +require_relative '../../../library/digest/sha1/shared/constants' +require_relative '../../../library/digest/sha256/shared/constants' +require_relative '../../../library/digest/sha384/shared/constants' +require_relative '../../../library/digest/sha512/shared/constants' +require 'openssl' + +describe "OpenSSL::Digest#digest_length" do + context "when the digest object is created via a name argument" do + it "returns a SHA1 digest length" do + OpenSSL::Digest.new('sha1').digest_length.should == SHA1Constants::DigestLength + end + + it "returns a SHA256 digest length" do + OpenSSL::Digest.new('sha256').digest_length.should == SHA256Constants::DigestLength + end + + it "returns a SHA384 digest length" do + OpenSSL::Digest.new('sha384').digest_length.should == SHA384Constants::DigestLength + end + + it "returns a SHA512 digest length" do + OpenSSL::Digest.new('sha512').digest_length.should == SHA512Constants::DigestLength + end + end + + context "when the digest object is created via a subclass" do + it "returns a SHA1 digest length" do + OpenSSL::Digest::SHA1.new.digest_length.should == SHA1Constants::DigestLength + end + + it "returns a SHA256 digest length" do + OpenSSL::Digest::SHA256.new.digest_length.should == SHA256Constants::DigestLength + end + + it "returns a SHA384 digest length" do + OpenSSL::Digest::SHA384.new.digest_length.should == SHA384Constants::DigestLength + end + + it "returns a SHA512 digest length" do + OpenSSL::Digest::SHA512.new.digest_length.should == SHA512Constants::DigestLength + end + end +end diff --git a/spec/ruby/library/openssl/digest_spec.rb b/spec/ruby/library/openssl/digest/digest_spec.rb similarity index 84% rename from spec/ruby/library/openssl/digest_spec.rb rename to spec/ruby/library/openssl/digest/digest_spec.rb index b8e82d073f2..cf27d01b6d9 100644 --- a/spec/ruby/library/openssl/digest_spec.rb +++ b/spec/ruby/library/openssl/digest/digest_spec.rb @@ -1,12 +1,11 @@ -require_relative '../../spec_helper' -require_relative '../../library/digest/sha1/shared/constants' -require_relative '../../library/digest/sha256/shared/constants' -require_relative '../../library/digest/sha384/shared/constants' -require_relative '../../library/digest/sha512/shared/constants' +require_relative '../../../spec_helper' +require_relative '../../../library/digest/sha1/shared/constants' +require_relative '../../../library/digest/sha256/shared/constants' +require_relative '../../../library/digest/sha384/shared/constants' +require_relative '../../../library/digest/sha512/shared/constants' require 'openssl' -describe "OpenSSL::Digest" do - +describe "OpenSSL::Digest class methods" do describe ".digest" do it "returns a SHA1 digest" do OpenSSL::Digest.digest('sha1', SHA1Constants::Contents).should == SHA1Constants::Digest diff --git a/spec/ruby/library/openssl/digest/initialize_spec.rb b/spec/ruby/library/openssl/digest/initialize_spec.rb new file mode 100644 index 00000000000..1cd0409c4d8 --- /dev/null +++ b/spec/ruby/library/openssl/digest/initialize_spec.rb @@ -0,0 +1,141 @@ +require_relative '../../../spec_helper' +require_relative '../../../library/digest/sha1/shared/constants' +require_relative '../../../library/digest/sha256/shared/constants' +require_relative '../../../library/digest/sha384/shared/constants' +require_relative '../../../library/digest/sha512/shared/constants' +require 'openssl' + +describe "OpenSSL::Digest#initialize" do + describe "can be called with a digest name" do + it "returns a SHA1 object" do + OpenSSL::Digest.new("sha1").name.should == "SHA1" + end + + it "returns a SHA256 object" do + OpenSSL::Digest.new("sha256").name.should == "SHA256" + end + + it "returns a SHA384 object" do + OpenSSL::Digest.new("sha384").name.should == "SHA384" + end + + it "returns a SHA512 object" do + OpenSSL::Digest.new("sha512").name.should == "SHA512" + end + + it "throws an error when called with an unknown digest" do + -> { OpenSSL::Digest.new("wd40") }.should raise_error(RuntimeError, /Unsupported digest algorithm \(wd40\)/) + end + + it "cannot be called with a symbol" do + -> { OpenSSL::Digest.new(:SHA1) }.should raise_error(TypeError, /wrong argument type Symbol/) + end + + it "does not call #to_str on the argument" do + name = mock("digest name") + name.should_not_receive(:to_str) + -> { OpenSSL::Digest.new(name) }.should raise_error(TypeError, /wrong argument type/) + end + end + + describe "can be called with a digest object" do + it "returns a SHA1 object" do + OpenSSL::Digest.new(OpenSSL::Digest::SHA1.new).name.should == "SHA1" + end + + it "returns a SHA256 object" do + OpenSSL::Digest.new(OpenSSL::Digest::SHA256.new).name.should == "SHA256" + end + + it "returns a SHA384 object" do + OpenSSL::Digest.new(OpenSSL::Digest::SHA384.new).name.should == "SHA384" + end + + it "returns a SHA512 object" do + OpenSSL::Digest.new(OpenSSL::Digest::SHA512.new).name.should == "SHA512" + end + + it "ignores the state of the digest object" do + sha1 = OpenSSL::Digest.new('sha1', SHA1Constants::Contents) + OpenSSL::Digest.new(sha1).digest.should == SHA1Constants::BlankDigest + end + end + + it "cannot be called with a digest class" do + -> { OpenSSL::Digest.new(OpenSSL::Digest::SHA1) }.should raise_error(TypeError, /wrong argument type Class/) + end + + context "when called without an initial String argument" do + it "returns a SHA1 digest" do + OpenSSL::Digest.new("sha1").digest.should == SHA1Constants::BlankDigest + end + + it "returns a SHA256 digest" do + OpenSSL::Digest.new("sha256").digest.should == SHA256Constants::BlankDigest + end + + it "returns a SHA384 digest" do + OpenSSL::Digest.new("sha384").digest.should == SHA384Constants::BlankDigest + end + + it "returns a SHA512 digest" do + OpenSSL::Digest.new("sha512").digest.should == SHA512Constants::BlankDigest + end + end + + context "when called with an initial String argument" do + it "returns a SHA1 digest of that argument" do + OpenSSL::Digest.new("sha1", SHA1Constants::Contents).digest.should == SHA1Constants::Digest + end + + it "returns a SHA256 digest of that argument" do + OpenSSL::Digest.new("sha256", SHA256Constants::Contents).digest.should == SHA256Constants::Digest + end + + it "returns a SHA384 digest of that argument" do + OpenSSL::Digest.new("sha384", SHA384Constants::Contents).digest.should == SHA384Constants::Digest + end + + it "returns a SHA512 digest of that argument" do + OpenSSL::Digest.new("sha512", SHA512Constants::Contents).digest.should == SHA512Constants::Digest + end + end + + context "can be called on subclasses" do + describe "can be called without an initial String argument on subclasses" do + it "returns a SHA1 digest" do + OpenSSL::Digest::SHA1.new.digest.should == SHA1Constants::BlankDigest + end + + it "returns a SHA256 digest" do + OpenSSL::Digest::SHA256.new.digest.should == SHA256Constants::BlankDigest + end + + it "returns a SHA384 digest" do + OpenSSL::Digest::SHA384.new.digest.should == SHA384Constants::BlankDigest + end + + it "returns a SHA512 digest" do + OpenSSL::Digest::SHA512.new.digest.should == SHA512Constants::BlankDigest + end + end + + describe "can be called with an initial String argument on subclasses" do + it "returns a SHA1 digest" do + OpenSSL::Digest::SHA1.new(SHA1Constants::Contents).digest.should == SHA1Constants::Digest + end + + it "returns a SHA256 digest" do + OpenSSL::Digest::SHA256.new(SHA256Constants::Contents).digest.should == SHA256Constants::Digest + end + + it "returns a SHA384 digest" do + OpenSSL::Digest::SHA384.new(SHA384Constants::Contents).digest.should == SHA384Constants::Digest + end + + it "returns a SHA512 digest" do + OpenSSL::Digest::SHA512.new(SHA512Constants::Contents).digest.should == SHA512Constants::Digest + end + end + end +end diff --git a/spec/ruby/library/openssl/digest/name_spec.rb b/spec/ruby/library/openssl/digest/name_spec.rb new file mode 100644 index 00000000000..b379f35c1c1 --- /dev/null +++ b/spec/ruby/library/openssl/digest/name_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' +require 'openssl' + +describe "OpenSSL::Digest#name" do + it "returns the name of digest" do + OpenSSL::Digest.new('SHA1').name.should == 'SHA1' + end + + it "converts the name to the internal representation of OpenSSL" do + OpenSSL::Digest.new('sha1').name.should == 'SHA1' + end + + it "works on subclasses too" do + OpenSSL::Digest::SHA1.new.name.should == 'SHA1' + end +end diff --git a/spec/ruby/library/openssl/digest/reset_spec.rb b/spec/ruby/library/openssl/digest/reset_spec.rb new file mode 100644 index 00000000000..c19bf466330 --- /dev/null +++ b/spec/ruby/library/openssl/digest/reset_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../../spec_helper' +require_relative '../../../library/digest/sha1/shared/constants' +require_relative '../../../library/digest/sha256/shared/constants' +require_relative '../../../library/digest/sha384/shared/constants' +require_relative '../../../library/digest/sha512/shared/constants' +require 'openssl' + +describe "OpenSSL::Digest#reset" do + it "works for a SHA1 digest" do + digest = OpenSSL::Digest.new('sha1', SHA1Constants::Contents) + digest.reset + digest.update(SHA1Constants::Contents) + digest.digest.should == SHA1Constants::Digest + end + + it "works for a SHA256 digest" do + digest = OpenSSL::Digest.new('sha256', SHA256Constants::Contents) + digest.reset + digest.update(SHA256Constants::Contents) + digest.digest.should == SHA256Constants::Digest + end + + it "works for a SHA384 digest" do + digest = OpenSSL::Digest.new('sha384', SHA384Constants::Contents) + digest.reset + digest.update(SHA384Constants::Contents) + digest.digest.should == SHA384Constants::Digest + end + + it "works for a SHA512 digest" do + digest = OpenSSL::Digest.new('sha512', SHA512Constants::Contents) + digest.reset + digest.update(SHA512Constants::Contents) + digest.digest.should == SHA512Constants::Digest + end +end diff --git a/spec/ruby/library/openssl/digest/shared/update.rb b/spec/ruby/library/openssl/digest/shared/update.rb new file mode 100644 index 00000000000..e5ff9dcb16f --- /dev/null +++ b/spec/ruby/library/openssl/digest/shared/update.rb @@ -0,0 +1,123 @@ +require_relative '../../../../library/digest/sha1/shared/constants' +require_relative '../../../../library/digest/sha256/shared/constants' +require_relative '../../../../library/digest/sha384/shared/constants' +require_relative '../../../../library/digest/sha512/shared/constants' +require 'openssl' + +describe :openssl_digest_update, shared: true do + context "when given input as a single string" do + it "returns a SHA1 digest" do + digest = OpenSSL::Digest.new('sha1') + digest.send(@method, SHA1Constants::Contents) + digest.digest.should == SHA1Constants::Digest + end + + it "returns a SHA256 digest" do + digest = OpenSSL::Digest.new('sha256') + digest.send(@method, SHA256Constants::Contents) + digest.digest.should == SHA256Constants::Digest + end + + it "returns a SHA384 digest" do + digest = OpenSSL::Digest.new('sha384') + digest.send(@method, SHA384Constants::Contents) + digest.digest.should == SHA384Constants::Digest + end + + it "returns a SHA512 digest" do + digest = OpenSSL::Digest.new('sha512') + digest.send(@method, SHA512Constants::Contents) + digest.digest.should == SHA512Constants::Digest + end + end + + context "when given input as multiple smaller substrings" do + it "returns a SHA1 digest" do + digest = OpenSSL::Digest.new('sha1') + SHA1Constants::Contents.each_char { |b| digest.send(@method, b) } + digest.digest.should == SHA1Constants::Digest + end + + it "returns a SHA256 digest" do + digest = OpenSSL::Digest.new('sha256') + SHA256Constants::Contents.each_char { |b| digest.send(@method, b) } + digest.digest.should == SHA256Constants::Digest + end + + it "returns a SHA384 digest" do + digest = OpenSSL::Digest.new('sha384') + SHA384Constants::Contents.each_char { |b| digest.send(@method, b) } + digest.digest.should == SHA384Constants::Digest + end + + it "returns a SHA512 digest" do + digest = OpenSSL::Digest.new('sha512') + SHA512Constants::Contents.each_char { |b| digest.send(@method, b) } + digest.digest.should == SHA512Constants::Digest + end + end + + context "when input is not a String and responds to #to_str" do + it "returns a SHA1 digest" do + str = mock('str') + str.should_receive(:to_str).and_return(SHA1Constants::Contents) + digest = OpenSSL::Digest.new('sha1') + digest.send(@method, str) + digest.digest.should == SHA1Constants::Digest + end + + it "returns a SHA256 digest" do + str = mock('str') + str.should_receive(:to_str).and_return(SHA256Constants::Contents) + digest = OpenSSL::Digest.new('sha256') + digest.send(@method, str) + digest.digest.should == SHA256Constants::Digest + end + + it "returns a SHA384 digest" do + str = mock('str') + str.should_receive(:to_str).and_return(SHA384Constants::Contents) + digest = OpenSSL::Digest.new('sha384') + digest.send(@method, str) + digest.digest.should == SHA384Constants::Digest + end + + it "returns a SHA512 digest" do + str = mock('str') + str.should_receive(:to_str).and_return(SHA512Constants::Contents) + digest = OpenSSL::Digest.new('sha512') + digest.send(@method, str) + digest.digest.should == SHA512Constants::Digest + end + end + + context "when input is not a String and does not respond to #to_str" do + it "raises a TypeError with SHA1" do + digest = OpenSSL::Digest.new('sha1') + -> { + digest.send(@method, Object.new) + }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + + it "raises a TypeError with SHA256" do + digest = OpenSSL::Digest.new('sha256') + -> { + digest.send(@method, Object.new) + }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + + it "raises a TypeError with SHA384" do + digest = OpenSSL::Digest.new('sha384') + -> { + digest.send(@method, Object.new) + }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + + it "raises a TypeError with SHA512" do + digest = OpenSSL::Digest.new('sha512') + -> { + digest.send(@method, Object.new) + }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + end +end diff --git a/spec/ruby/library/openssl/digest/update_spec.rb b/spec/ruby/library/openssl/digest/update_spec.rb new file mode 100644 index 00000000000..3a90b06c6b1 --- /dev/null +++ b/spec/ruby/library/openssl/digest/update_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/update' + +describe "OpenSSL::Digest#update" do + it_behaves_like :openssl_digest_update, :update +end diff --git a/spec/ruby/library/openssl/kdf/pbkdf2_hmac_spec.rb b/spec/ruby/library/openssl/kdf/pbkdf2_hmac_spec.rb new file mode 100644 index 00000000000..ebf71482de1 --- /dev/null +++ b/spec/ruby/library/openssl/kdf/pbkdf2_hmac_spec.rb @@ -0,0 +1,184 @@ +require_relative '../../../spec_helper' +require 'openssl' + +describe "OpenSSL::KDF.pbkdf2_hmac" do + before :each do + @defaults = { + salt: "\x00".b * 16, + iterations: 20_000, + length: 16, + hash: "sha1" + } + end + + it "creates the same value with the same input" do + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "supports nullbytes embedded in the password" do + key = OpenSSL::KDF.pbkdf2_hmac("sec\x00ret".b, **@defaults) + key.should == "\xB9\x7F\xB0\xC2\th\xC8<\x86\xF3\x94Ij7\xEF\xF1".b + end + + it "coerces the password into a String using #to_str" do + pass = mock("pass") + pass.should_receive(:to_str).and_return("secret") + key = OpenSSL::KDF.pbkdf2_hmac(pass, **@defaults) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "coerces the salt into a String using #to_str" do + salt = mock("salt") + salt.should_receive(:to_str).and_return("\x00".b * 16) + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, salt: salt) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "coerces the iterations into an Integer using #to_int" do + iterations = mock("iterations") + iterations.should_receive(:to_int).and_return(20_000) + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: iterations) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "coerces the length into an Integer using #to_int" do + length = mock("length") + length.should_receive(:to_int).and_return(16) + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, length: length) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "accepts a OpenSSL::Digest object as hash" do + hash = OpenSSL::Digest.new("sha1") + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, hash: hash) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0".b + end + + it "accepts an empty password" do + key = OpenSSL::KDF.pbkdf2_hmac("", **@defaults) + key.should == "k\x9F-\xB1\xF7\x9A\v\xA1(C\xF9\x85!P\xEF\x8C".b + end + + it "accepts an empty salt" do + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, salt: "") + key.should == "\xD5f\xE5\xEA\xF91\x1D\xD3evD\xED\xDB\xE80\x80".b + end + + it "accepts an empty length" do + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, length: 0) + key.should.empty? + end + + it "accepts an arbitrary length" do + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, length: 19) + key.should == "!\x99+\xF0^\xD0\x8BM\x158\xC4\xAC\x9C\xF1\xF0\xE0\xCF\xBB\x7F".b + end + + it "accepts any hash function known to OpenSSL" do + key = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, hash: "sha512") + key.should == "N\x12}D\xCE\x99\xDBC\x8E\xEC\xAAr\xEA1\xDF\xFF".b + end + + it "raises a TypeError when password is not a String and does not respond to #to_str" do + -> { + OpenSSL::KDF.pbkdf2_hmac(Object.new, **@defaults) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises a TypeError when salt is not a String and does not respond to #to_str" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, salt: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises a TypeError when iterations is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises a TypeError when length is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, length: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises a TypeError when hash is neither a String nor an OpenSSL::Digest" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, hash: Object.new) + }.should raise_error(TypeError, "wrong argument type Object (expected OpenSSL/Digest)") + end + + it "raises a TypeError when hash is neither a String nor an OpenSSL::Digest, it does not try to call #to_str" do + hash = mock("hash") + hash.should_not_receive(:to_str) + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, hash: hash) + }.should raise_error(TypeError, "wrong argument type MockObject (expected OpenSSL/Digest)") + end + + it "raises a RuntimeError for unknown digest algorithms" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, hash: "wd40") + }.should raise_error(RuntimeError, /Unsupported digest algorithm \(wd40\)/) + end + + it "treats salt as a required keyword" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults.except(:salt)) + }.should raise_error(ArgumentError, 'missing keyword: :salt') + end + + it "treats iterations as a required keyword" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults.except(:iterations)) + }.should raise_error(ArgumentError, 'missing keyword: :iterations') + end + + it "treats length as a required keyword" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults.except(:length)) + }.should raise_error(ArgumentError, 'missing keyword: :length') + end + + it "treats hash as a required keyword" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults.except(:hash)) + }.should raise_error(ArgumentError, 'missing keyword: :hash') + end + + it "treats all keywords as required" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret") + }.should raise_error(ArgumentError, 'missing keywords: :salt, :iterations, :length, :hash') + end + + + guard -> { OpenSSL::OPENSSL_VERSION_NUMBER < 0x30000000 } do + it "treats 0 or less iterations as a single iteration" do + salt = "\x00".b * 16 + length = 16 + hash = "sha1" + + # "Any iter less than 1 is treated as a single iteration." + key0 = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: 0) + key_negative = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: -1) + key1 = OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: 1) + key0.should == key1 + key_negative.should == key1 + end + end + + guard -> { OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 } do + it "raises an OpenSSL::KDF::KDFError for 0 or less iterations" do + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: 0) + }.should raise_error(OpenSSL::KDF::KDFError, "PKCS5_PBKDF2_HMAC: invalid iteration count") + + -> { + OpenSSL::KDF.pbkdf2_hmac("secret", **@defaults, iterations: -1) + }.should raise_error(OpenSSL::KDF::KDFError, /PKCS5_PBKDF2_HMAC/) + end + end +end diff --git a/spec/ruby/library/openssl/kdf/scrypt_spec.rb b/spec/ruby/library/openssl/kdf/scrypt_spec.rb new file mode 100644 index 00000000000..d9b83eaa553 --- /dev/null +++ b/spec/ruby/library/openssl/kdf/scrypt_spec.rb @@ -0,0 +1,207 @@ +require_relative '../../../spec_helper' +require 'openssl' + +describe "OpenSSL::KDF.scrypt" do + before :each do + @defaults = { + salt: "\x00".b * 16, + N: 2**14, + r: 8, + p: 1, + length: 32 + } + end + + it "creates the same value with the same input" do + key = OpenSSL::KDF.scrypt("secret", **@defaults) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "supports nullbytes embedded into the password" do + key = OpenSSL::KDF.scrypt("sec\x00ret".b, **@defaults) + key.should == "\xF9\xA4\xA0\xF1p\xF4\xF0\xCAT\xB4v\xEB\r7\x88N\xF7\x15]Ns\xFCwt4a\xC9\xC6\xA7\x13\x81&".b + end + + it "coerces the password into a String using #to_str" do + pass = mock("pass") + pass.should_receive(:to_str).and_return("secret") + key = OpenSSL::KDF.scrypt(pass, **@defaults) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "coerces the salt into a String using #to_str" do + salt = mock("salt") + salt.should_receive(:to_str).and_return("\x00".b * 16) + key = OpenSSL::KDF.scrypt("secret", **@defaults, salt: salt) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "coerces the N into an Integer using #to_int" do + n = mock("N") + n.should_receive(:to_int).and_return(2**14) + key = OpenSSL::KDF.scrypt("secret", **@defaults, N: n) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "coerces the r into an Integer using #to_int" do + r = mock("r") + r.should_receive(:to_int).and_return(8) + key = OpenSSL::KDF.scrypt("secret", **@defaults, r: r) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "coerces the p into an Integer using #to_int" do + p = mock("p") + p.should_receive(:to_int).and_return(1) + key = OpenSSL::KDF.scrypt("secret", **@defaults, p: p) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "coerces the length into an Integer using #to_int" do + length = mock("length") + length.should_receive(:to_int).and_return(32) + key = OpenSSL::KDF.scrypt("secret", **@defaults, length: length) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D\x17}\xF2\x84T\xD4)\xC2>\xFE\x93\xE3\xF4".b + end + + it "accepts an empty password" do + key = OpenSSL::KDF.scrypt("", **@defaults) + key.should == "\xAA\xFC\xF5^E\x94v\xFFk\xE6\xF0vR\xE7\x13\xA7\xF5\x15'\x9A\xE4C\x9Dn\x18F_E\xD2\v\e\xB3".b + end + + it "accepts an empty salt" do + key = OpenSSL::KDF.scrypt("secret", **@defaults, salt: "") + key.should == "\x96\xACDl\xCB3/aN\xB0F\x8A#\xD7\x92\xD2O\x1E\v\xBB\xCE\xC0\xAA\xB9\x0F]\xB09\xEA8\xDD\e".b + end + + it "accepts a zero length" do + key = OpenSSL::KDF.scrypt("secret", **@defaults, length: 0) + key.should.empty? + end + + it "accepts an arbitrary length" do + key = OpenSSL::KDF.scrypt("secret", **@defaults, length: 19) + key.should == "h\xB2k\xDF]\xDA\xE1.-(\xCF\xAC\x91D\x8F\xC2a\x9C\x9D".b + end + + it "raises a TypeError when password is not a String and does not respond to #to_str" do + -> { + OpenSSL::KDF.scrypt(Object.new, **@defaults) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises a TypeError when salt is not a String and does not respond to #to_str" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, salt: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises a TypeError when N is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, N: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises a TypeError when r is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, r: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises a TypeError when p is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, p: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises a TypeError when length is not an Integer and does not respond to #to_int" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, length: Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "treats salt as a required keyword" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults.except(:salt)) + }.should raise_error(ArgumentError, 'missing keyword: :salt') + end + + it "treats N as a required keyword" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults.except(:N)) + }.should raise_error(ArgumentError, 'missing keyword: :N') + end + + it "treats r as a required keyword" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults.except(:r)) + }.should raise_error(ArgumentError, 'missing keyword: :r') + end + + it "treats p as a required keyword" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults.except(:p)) + }.should raise_error(ArgumentError, 'missing keyword: :p') + end + + it "treats length as a required keyword" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults.except(:length)) + }.should raise_error(ArgumentError, 'missing keyword: :length') + end + + it "treats all keywords as required" do + -> { + OpenSSL::KDF.scrypt("secret") + }.should raise_error(ArgumentError, 'missing keywords: :salt, :N, :r, :p, :length') + end + + it "requires N to be a power of 2" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, N: 2**14 - 1) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + end + + it "requires N to be at least 2" do + key = OpenSSL::KDF.scrypt("secret", **@defaults, N: 2) + key.should == "\x06A$a\xA9!\xBE\x01\x85\xA7\x18\xBCEa\x82\xC5\xFEl\x93\xAB\xBD\xF7\x8B\x84\v\xFC\eN\xEBQ\xE6\xD2".b + + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, N: 1) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, N: 0) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, N: -1) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + end + + it "requires r to be positive" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, r: 0) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, r: -1) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + end + + it "requires p to be positive" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, p: 0) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, p: -1) + }.should raise_error(OpenSSL::KDF::KDFError, /EVP_PBE_scrypt/) + end + + it "requires length to be not negative" do + -> { + OpenSSL::KDF.scrypt("secret", **@defaults, length: -1) + }.should raise_error(ArgumentError, "negative string size (or size too big)") + end +end diff --git a/spec/ruby/library/socket/ipsocket/getaddress_spec.rb b/spec/ruby/library/socket/ipsocket/getaddress_spec.rb index 746d2ab86bf..96324982e5c 100644 --- a/spec/ruby/library/socket/ipsocket/getaddress_spec.rb +++ b/spec/ruby/library/socket/ipsocket/getaddress_spec.rb @@ -19,7 +19,7 @@ # traditionally invalidly named ones. it "raises an error on unknown hostnames" do -> { - IPSocket.getaddress("rubyspecdoesntexist.fallingsnow.net") + IPSocket.getaddress("rubyspecdoesntexist.ruby-lang.org") }.should raise_error(SocketError) end end diff --git a/spec/ruby/library/socket/tcpserver/new_spec.rb b/spec/ruby/library/socket/tcpserver/new_spec.rb index 8d9696c9d86..dd1ba676bd9 100644 --- a/spec/ruby/library/socket/tcpserver/new_spec.rb +++ b/spec/ruby/library/socket/tcpserver/new_spec.rb @@ -97,6 +97,12 @@ addr[1].should be_kind_of(Integer) end + it "does not use the given block and warns to use TCPServer::open" do + -> { + @server = TCPServer.new(0) { raise } + }.should complain(/warning: TCPServer::new\(\) does not take block; use TCPServer::open\(\) instead/) + end + it "raises Errno::EADDRNOTAVAIL when the address is unknown" do -> { TCPServer.new("1.2.3.4", 0) }.should raise_error(Errno::EADDRNOTAVAIL) end diff --git a/spec/ruby/library/socket/tcpsocket/initialize_spec.rb b/spec/ruby/library/socket/tcpsocket/initialize_spec.rb index 065c8f4190a..3bec06c6974 100644 --- a/spec/ruby/library/socket/tcpsocket/initialize_spec.rb +++ b/spec/ruby/library/socket/tcpsocket/initialize_spec.rb @@ -4,6 +4,27 @@ describe 'TCPSocket#initialize' do it_behaves_like :tcpsocket_new, :new + + describe "with a running server" do + before :each do + @server = SocketSpecs::SpecTCPServer.new + @hostname = @server.hostname + end + + after :each do + if @socket + @socket.write "QUIT" + @socket.close + end + @server.shutdown + end + + it "does not use the given block and warns to use TCPSocket::open" do + -> { + @socket = TCPSocket.new(@hostname, @server.port, nil) { raise } + }.should complain(/warning: TCPSocket::new\(\) does not take block; use TCPSocket::open\(\) instead/) + end + end end describe 'TCPSocket#initialize' do diff --git a/spec/ruby/library/socket/tcpsocket/shared/new.rb b/spec/ruby/library/socket/tcpsocket/shared/new.rb index d60260a01b0..90f8d7e6a28 100644 --- a/spec/ruby/library/socket/tcpsocket/shared/new.rb +++ b/spec/ruby/library/socket/tcpsocket/shared/new.rb @@ -19,6 +19,9 @@ -> { TCPSocket.send(@method, "192.0.2.1", 80, connect_timeout: 0) }.should raise_error(Errno::ETIMEDOUT) + rescue Errno::ENETUNREACH + # In the case all network interfaces down. + # raise_error cannot deal with multiple expected exceptions end end @@ -27,6 +30,9 @@ -> { TCPSocket.send(@method, "192.0.2.1", 80, connect_timeout: 0) }.should raise_error(IO::TimeoutError) + rescue Errno::ENETUNREACH + # In the case all network interfaces down. + # raise_error cannot deal with multiple expected exceptions end end diff --git a/spec/ruby/library/socket/udpsocket/new_spec.rb b/spec/ruby/library/socket/udpsocket/new_spec.rb index 6cc0cadbcb4..79bfcb624d2 100644 --- a/spec/ruby/library/socket/udpsocket/new_spec.rb +++ b/spec/ruby/library/socket/udpsocket/new_spec.rb @@ -26,6 +26,12 @@ @socket.should be_an_instance_of(UDPSocket) end + it "does not use the given block and warns to use UDPSocket::open" do + -> { + @socket = UDPSocket.new { raise } + }.should complain(/warning: UDPSocket::new\(\) does not take block; use UDPSocket::open\(\) instead/) + end + it 'raises Errno::EAFNOSUPPORT or Errno::EPROTONOSUPPORT if unsupported family passed' do -> { UDPSocket.new(-1) }.should raise_error(SystemCallError) { |e| [Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT].should include(e.class) diff --git a/spec/ruby/library/socket/unixserver/accept_nonblock_spec.rb b/spec/ruby/library/socket/unixserver/accept_nonblock_spec.rb index 30688b46b6d..dba3de7359d 100644 --- a/spec/ruby/library/socket/unixserver/accept_nonblock_spec.rb +++ b/spec/ruby/library/socket/unixserver/accept_nonblock_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXServer#accept_nonblock" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXServer#accept_nonblock" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -33,9 +32,7 @@ @server.accept_nonblock(exception: false).should == :wait_readable end end -end -with_feature :unix_socket do describe 'UNIXServer#accept_nonblock' do before do @path = SocketSpecs.socket_path diff --git a/spec/ruby/library/socket/unixserver/accept_spec.rb b/spec/ruby/library/socket/unixserver/accept_spec.rb index c05fbe7f226..624782d6b98 100644 --- a/spec/ruby/library/socket/unixserver/accept_spec.rb +++ b/spec/ruby/library/socket/unixserver/accept_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -platform_is_not :windows do +with_feature :unix_socket do describe "UNIXServer#accept" do before :each do @path = SocketSpecs.socket_path diff --git a/spec/ruby/library/socket/unixserver/for_fd_spec.rb b/spec/ruby/library/socket/unixserver/for_fd_spec.rb index 4f3816ad37b..e00c4d95267 100644 --- a/spec/ruby/library/socket/unixserver/for_fd_spec.rb +++ b/spec/ruby/library/socket/unixserver/for_fd_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -platform_is_not :windows do +with_feature :unix_socket do describe "UNIXServer#for_fd" do before :each do @unix_path = SocketSpecs.socket_path diff --git a/spec/ruby/library/socket/unixserver/new_spec.rb b/spec/ruby/library/socket/unixserver/new_spec.rb index f831f40bc6b..a160e3ce5c8 100644 --- a/spec/ruby/library/socket/unixserver/new_spec.rb +++ b/spec/ruby/library/socket/unixserver/new_spec.rb @@ -1,6 +1,14 @@ require_relative '../spec_helper' require_relative 'shared/new' -describe "UNIXServer.new" do - it_behaves_like :unixserver_new, :new +with_feature :unix_socket do + describe "UNIXServer.new" do + it_behaves_like :unixserver_new, :new + + it "does not use the given block and warns to use UNIXServer::open" do + -> { + @server = UNIXServer.new(@path) { raise } + }.should complain(/warning: UNIXServer::new\(\) does not take block; use UNIXServer::open\(\) instead/) + end + end end diff --git a/spec/ruby/library/socket/unixserver/open_spec.rb b/spec/ruby/library/socket/unixserver/open_spec.rb index f2506d9f6f0..16453dd3bd8 100644 --- a/spec/ruby/library/socket/unixserver/open_spec.rb +++ b/spec/ruby/library/socket/unixserver/open_spec.rb @@ -2,10 +2,10 @@ require_relative '../fixtures/classes' require_relative 'shared/new' -describe "UNIXServer.open" do - it_behaves_like :unixserver_new, :open +with_feature :unix_socket do + describe "UNIXServer.open" do + it_behaves_like :unixserver_new, :open - platform_is_not :windows do before :each do @path = SocketSpecs.socket_path end diff --git a/spec/ruby/library/socket/unixserver/shared/new.rb b/spec/ruby/library/socket/unixserver/shared/new.rb index 35395826c9c..b537f2a8713 100644 --- a/spec/ruby/library/socket/unixserver/shared/new.rb +++ b/spec/ruby/library/socket/unixserver/shared/new.rb @@ -2,21 +2,19 @@ require_relative '../../fixtures/classes' describe :unixserver_new, shared: true do - platform_is_not :windows do - before :each do - @path = SocketSpecs.socket_path - end + before :each do + @path = SocketSpecs.socket_path + end - after :each do - @server.close if @server - @server = nil - SocketSpecs.rm_socket @path - end + after :each do + @server.close if @server + @server = nil + SocketSpecs.rm_socket @path + end - it "creates a new UNIXServer" do - @server = UNIXServer.send(@method, @path) - @server.path.should == @path - @server.addr.should == ["AF_UNIX", @path] - end + it "creates a new UNIXServer" do + @server = UNIXServer.send(@method, @path) + @server.path.should == @path + @server.addr.should == ["AF_UNIX", @path] end end diff --git a/spec/ruby/library/socket/unixsocket/addr_spec.rb b/spec/ruby/library/socket/unixsocket/addr_spec.rb index e8431bea169..d93e0613123 100644 --- a/spec/ruby/library/socket/unixsocket/addr_spec.rb +++ b/spec/ruby/library/socket/unixsocket/addr_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#addr" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#addr" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) diff --git a/spec/ruby/library/socket/unixsocket/inspect_spec.rb b/spec/ruby/library/socket/unixsocket/inspect_spec.rb index d2e3cabbd36..a542ba6db54 100644 --- a/spec/ruby/library/socket/unixsocket/inspect_spec.rb +++ b/spec/ruby/library/socket/unixsocket/inspect_spec.rb @@ -1,8 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#inspect" do - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#inspect" do it "returns sockets fd for unnamed sockets" do begin s1, s2 = UNIXSocket.socketpair diff --git a/spec/ruby/library/socket/unixsocket/local_address_spec.rb b/spec/ruby/library/socket/unixsocket/local_address_spec.rb index cbf315f9f41..734253e7f5c 100644 --- a/spec/ruby/library/socket/unixsocket/local_address_spec.rb +++ b/spec/ruby/library/socket/unixsocket/local_address_spec.rb @@ -46,9 +46,7 @@ end end end -end -with_feature :unix_socket do describe 'UNIXSocket#local_address with a UNIX socket pair' do before :each do @sock, @sock2 = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) diff --git a/spec/ruby/library/socket/unixsocket/new_spec.rb b/spec/ruby/library/socket/unixsocket/new_spec.rb index 05a6b3eda2e..6d8ea6dcfe6 100644 --- a/spec/ruby/library/socket/unixsocket/new_spec.rb +++ b/spec/ruby/library/socket/unixsocket/new_spec.rb @@ -1,6 +1,14 @@ require_relative '../spec_helper' require_relative 'shared/new' -describe "UNIXSocket.new" do - it_behaves_like :unixsocket_new, :new +with_feature :unix_socket do + describe "UNIXSocket.new" do + it_behaves_like :unixsocket_new, :new + + it "does not use the given block and warns to use UNIXSocket::open" do + -> { + @client = UNIXSocket.new(@path) { raise } + }.should complain(/warning: UNIXSocket::new\(\) does not take block; use UNIXSocket::open\(\) instead/) + end + end end diff --git a/spec/ruby/library/socket/unixsocket/open_spec.rb b/spec/ruby/library/socket/unixsocket/open_spec.rb index 99ad151bb83..61def30abbb 100644 --- a/spec/ruby/library/socket/unixsocket/open_spec.rb +++ b/spec/ruby/library/socket/unixsocket/open_spec.rb @@ -2,12 +2,12 @@ require_relative '../fixtures/classes' require_relative 'shared/new' -describe "UNIXSocket.open" do - it_behaves_like :unixsocket_new, :open -end +with_feature :unix_socket do + describe "UNIXSocket.open" do + it_behaves_like :unixsocket_new, :open + end -describe "UNIXSocket.open" do - platform_is_not :windows do + describe "UNIXSocket.open" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) diff --git a/spec/ruby/library/socket/unixsocket/pair_spec.rb b/spec/ruby/library/socket/unixsocket/pair_spec.rb index 0cdc55f9989..9a66c56c108 100644 --- a/spec/ruby/library/socket/unixsocket/pair_spec.rb +++ b/spec/ruby/library/socket/unixsocket/pair_spec.rb @@ -2,9 +2,8 @@ require_relative '../fixtures/classes' require_relative '../shared/partially_closable_sockets' -describe "UNIXSocket#pair" do - platform_is_not :windows do - +with_feature :unix_socket do + describe "UNIXSocket#pair" do it_should_behave_like :partially_closable_sockets before :each do diff --git a/spec/ruby/library/socket/unixsocket/partially_closable_spec.rb b/spec/ruby/library/socket/unixsocket/partially_closable_spec.rb index 4f6ef8abd92..ef7d0f0b2aa 100644 --- a/spec/ruby/library/socket/unixsocket/partially_closable_spec.rb +++ b/spec/ruby/library/socket/unixsocket/partially_closable_spec.rb @@ -2,9 +2,8 @@ require_relative '../fixtures/classes' require_relative '../shared/partially_closable_sockets' -platform_is_not :windows do +with_feature :unix_socket do describe "UNIXSocket partial closability" do - before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -20,6 +19,5 @@ end it_should_behave_like :partially_closable_sockets - end end diff --git a/spec/ruby/library/socket/unixsocket/path_spec.rb b/spec/ruby/library/socket/unixsocket/path_spec.rb index 317ffc09751..a608378e4f9 100644 --- a/spec/ruby/library/socket/unixsocket/path_spec.rb +++ b/spec/ruby/library/socket/unixsocket/path_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#path" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#path" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -24,5 +23,4 @@ @client.path.should == "" end end - end diff --git a/spec/ruby/library/socket/unixsocket/peeraddr_spec.rb b/spec/ruby/library/socket/unixsocket/peeraddr_spec.rb index 0b6b1ccf045..72bc96b1fe9 100644 --- a/spec/ruby/library/socket/unixsocket/peeraddr_spec.rb +++ b/spec/ruby/library/socket/unixsocket/peeraddr_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#peeraddr" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#peeraddr" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -26,5 +25,4 @@ }.should raise_error(Errno::ENOTCONN) end end - end diff --git a/spec/ruby/library/socket/unixsocket/recv_io_spec.rb b/spec/ruby/library/socket/unixsocket/recv_io_spec.rb index 533f02a0fac..1dbc4538e37 100644 --- a/spec/ruby/library/socket/unixsocket/recv_io_spec.rb +++ b/spec/ruby/library/socket/unixsocket/recv_io_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#recv_io" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#recv_io" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -41,9 +40,7 @@ @io.should be_an_instance_of(File) end end -end -with_feature :unix_socket do describe 'UNIXSocket#recv_io' do before do @file = File.open('/dev/null', 'w') diff --git a/spec/ruby/library/socket/unixsocket/recvfrom_spec.rb b/spec/ruby/library/socket/unixsocket/recvfrom_spec.rb index c0e1cf670b6..fedf74bb2fa 100644 --- a/spec/ruby/library/socket/unixsocket/recvfrom_spec.rb +++ b/spec/ruby/library/socket/unixsocket/recvfrom_spec.rb @@ -1,8 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#recvfrom" do - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#recvfrom" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -42,10 +42,7 @@ sock.close end end -end - -with_feature :unix_socket do describe 'UNIXSocket#recvfrom' do describe 'using a socket pair' do before do diff --git a/spec/ruby/library/socket/unixsocket/send_io_spec.rb b/spec/ruby/library/socket/unixsocket/send_io_spec.rb index a2a7d265398..80f3550c6d3 100644 --- a/spec/ruby/library/socket/unixsocket/send_io_spec.rb +++ b/spec/ruby/library/socket/unixsocket/send_io_spec.rb @@ -1,9 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -describe "UNIXSocket#send_io" do - - platform_is_not :windows do +with_feature :unix_socket do + describe "UNIXSocket#send_io" do before :each do @path = SocketSpecs.socket_path @server = UNIXServer.open(@path) @@ -32,9 +31,7 @@ @io.read.should == File.read(@send_io_path) end end -end -with_feature :unix_socket do describe 'UNIXSocket#send_io' do before do @file = File.open('/dev/null', 'w') diff --git a/spec/ruby/library/socket/unixsocket/shared/new.rb b/spec/ruby/library/socket/unixsocket/shared/new.rb index bfb7ed3886f..f075b03c5e2 100644 --- a/spec/ruby/library/socket/unixsocket/shared/new.rb +++ b/spec/ruby/library/socket/unixsocket/shared/new.rb @@ -2,23 +2,21 @@ require_relative '../../fixtures/classes' describe :unixsocket_new, shared: true do - platform_is_not :windows do - before :each do - @path = SocketSpecs.socket_path - @server = UNIXServer.open(@path) - end + before :each do + @path = SocketSpecs.socket_path + @server = UNIXServer.open(@path) + end - after :each do - @client.close if @client - @server.close - SocketSpecs.rm_socket @path - end + after :each do + @client.close if @client + @server.close + SocketSpecs.rm_socket @path + end - it "opens a unix socket on the specified file" do - @client = UNIXSocket.send(@method, @path) + it "opens a unix socket on the specified file" do + @client = UNIXSocket.send(@method, @path) - @client.addr[0].should == "AF_UNIX" - @client.should_not.closed? - end + @client.addr[0].should == "AF_UNIX" + @client.should_not.closed? end end diff --git a/spec/ruby/library/stringio/new_spec.rb b/spec/ruby/library/stringio/new_spec.rb index 328455134eb..ec4b32aa112 100644 --- a/spec/ruby/library/stringio/new_spec.rb +++ b/spec/ruby/library/stringio/new_spec.rb @@ -2,7 +2,9 @@ require 'stringio' describe "StringIO.new" do - it "warns when called with a block" do - -> { eval("StringIO.new {}") }.should complain(/StringIO::new\(\) does not take block; use StringIO::open\(\) instead/) + it "does not use the given block and warns to use StringIO::open" do + -> { + StringIO.new { raise } + }.should complain(/warning: StringIO::new\(\) does not take block; use StringIO::open\(\) instead/) end end diff --git a/spec/ruby/optional/capi/encoding_spec.rb b/spec/ruby/optional/capi/encoding_spec.rb index aa632b963b8..36437cc7b68 100644 --- a/spec/ruby/optional/capi/encoding_spec.rb +++ b/spec/ruby/optional/capi/encoding_spec.rb @@ -657,6 +657,20 @@ end end + describe "rb_enc_raise" do + it "forces exception message encoding to the specified one" do + utf_8_incompatible_string = "\x81".b + + -> { + @s.rb_enc_raise(Encoding::UTF_8, RuntimeError, utf_8_incompatible_string) + }.should raise_error { |e| + e.message.encoding.should == Encoding::UTF_8 + e.message.valid_encoding?.should == false + e.message.bytes.should == utf_8_incompatible_string.bytes + } + end + end + describe "rb_uv_to_utf8" do it 'converts a Unicode codepoint to a UTF-8 C string' do str = ' ' * 6 @@ -674,6 +688,22 @@ end end + describe "rb_enc_left_char_head" do + it 'returns the head position of a character' do + @s.rb_enc_left_char_head("é", 1).should == 0 + @s.rb_enc_left_char_head("éééé", 7).should == 6 + + @s.rb_enc_left_char_head("a", 0).should == 0 + + # unclear if this is intended to work + @s.rb_enc_left_char_head("a", 1).should == 1 + + # Works because for single-byte encodings rb_enc_left_char_head() just returns the pointer + @s.rb_enc_left_char_head("a".force_encoding(Encoding::US_ASCII), 88).should == 88 + @s.rb_enc_left_char_head("a".b, 88).should == 88 + end + end + describe "ONIGENC_MBC_CASE_FOLD" do it "returns the correct case fold for the given string" do @s.ONIGENC_MBC_CASE_FOLD("lower").should == ["l", 1] diff --git a/spec/ruby/optional/capi/ext/encoding_spec.c b/spec/ruby/optional/capi/ext/encoding_spec.c index a454ae8395f..0ebbc9d75a8 100644 --- a/spec/ruby/optional/capi/ext/encoding_spec.c +++ b/spec/ruby/optional/capi/ext/encoding_spec.c @@ -271,6 +271,13 @@ static VALUE encoding_spec_rb_enc_str_asciionly_p(VALUE self, VALUE str) { } } +static VALUE encoding_spec_rb_enc_raise(VALUE self, VALUE encoding, VALUE exception_class, VALUE format) { + rb_encoding *e = rb_to_encoding(encoding); + const char *f = RSTRING_PTR(format); + + rb_enc_raise(e, exception_class, f); +} + static VALUE encoding_spec_rb_uv_to_utf8(VALUE self, VALUE buf, VALUE num) { int len = rb_uv_to_utf8(RSTRING_PTR(buf), NUM2INT(num)); RB_ENC_CODERANGE_CLEAR(buf); @@ -307,6 +314,12 @@ static VALUE encoding_spec_rb_enc_strlen(VALUE self, VALUE str, VALUE length, VA return LONG2FIX(rb_enc_strlen(p, e, rb_to_encoding(encoding))); } +static VALUE encoding_spec_rb_enc_left_char_head(VALUE self, VALUE str, VALUE offset) { + char *ptr = RSTRING_PTR(str); + char *result = rb_enc_left_char_head(ptr, ptr + NUM2INT(offset), RSTRING_END(str), rb_enc_get(str)); + return LONG2NUM(result - ptr); +} + void Init_encoding_spec(void) { VALUE cls; native_rb_encoding_pointer = (rb_encoding**) malloc(sizeof(rb_encoding*)); @@ -362,8 +375,10 @@ void Init_encoding_spec(void) { rb_define_method(cls, "rb_enc_nth", encoding_spec_rb_enc_nth, 2); rb_define_method(cls, "rb_enc_codepoint_len", encoding_spec_rb_enc_codepoint_len, 1); rb_define_method(cls, "rb_enc_str_asciionly_p", encoding_spec_rb_enc_str_asciionly_p, 1); + rb_define_method(cls, "rb_enc_raise", encoding_spec_rb_enc_raise, 3); rb_define_method(cls, "rb_uv_to_utf8", encoding_spec_rb_uv_to_utf8, 2); rb_define_method(cls, "ONIGENC_MBC_CASE_FOLD", encoding_spec_ONIGENC_MBC_CASE_FOLD, 1); + rb_define_method(cls, "rb_enc_left_char_head", encoding_spec_rb_enc_left_char_head, 2); } #ifdef __cplusplus diff --git a/spec/ruby/optional/capi/util_spec.rb b/spec/ruby/optional/capi/util_spec.rb index 320527521bb..2c16999cdc3 100644 --- a/spec/ruby/optional/capi/util_spec.rb +++ b/spec/ruby/optional/capi/util_spec.rb @@ -127,6 +127,19 @@ ScratchPad.recorded.should == [1, 7, 4] end + it "assigns optional arguments with no hash argument given" do + @o.rb_scan_args([1, 7], "02:", 3, @acc).should == 2 + ScratchPad.recorded.should == [1, 7, nil] + end + + it "assigns optional arguments with no hash argument given and rejects the use of optional nil argument as a hash" do + -> { + @o.rb_scan_args([1, nil], "02:", 3, @acc).should == 2 + }.should_not complain + + ScratchPad.recorded.should == [1, nil, nil] + end + it "assigns required, optional, splat, post-splat, Hash and block arguments" do h = {a: 1, b: 2} @o.rb_scan_args([1, 2, 3, 4, 5, h], "k11*1:&", 6, @acc, &@prc).should == 5 diff --git a/spec/ruby/shared/string/start_with.rb b/spec/ruby/shared/string/start_with.rb index 91fc50c4cdd..4b947a3bbf0 100644 --- a/spec/ruby/shared/string/start_with.rb +++ b/spec/ruby/shared/string/start_with.rb @@ -70,7 +70,13 @@ $1.should be_nil end - ruby_bug "#19784", ""..."3.3" do + ruby_version_is ""..."3.3" do + it "does not check that we are not matching part of a character" do + "\xC3\xA9".send(@method).should.start_with?("\xC3") + end + end + + ruby_version_is "3.3" do # #19784 it "checks that we are not matching part of a character" do "\xC3\xA9".send(@method).should_not.start_with?("\xC3") end diff --git a/spec/tags/ruby/command_line/dash_upper_i_tags.txt b/spec/tags/ruby/command_line/dash_upper_i_tags.txt index 20db6ac8d0a..88cbc3c1b0b 100644 --- a/spec/tags/ruby/command_line/dash_upper_i_tags.txt +++ b/spec/tags/ruby/command_line/dash_upper_i_tags.txt @@ -1,3 +1 @@ critical(fails on Travis):The -I command line option adds the path at the front of $LOAD_PATH -fails:The -I command line option adds the path expanded from CWD to $LOAD_PATH -fails:The -I command line option expands a path from CWD even if it does not exist diff --git a/spec/tags/ruby/core/argf/each_codepoint_tags.txt b/spec/tags/ruby/core/argf/each_codepoint_tags.txt index 628660b3465..de22fb505f6 100644 --- a/spec/tags/ruby/core/argf/each_codepoint_tags.txt +++ b/spec/tags/ruby/core/argf/each_codepoint_tags.txt @@ -1,2 +1 @@ hangs:ARGF.each_codepoint returns self when passed a block -fails:ARGF.each_codepoint yields each codepoint of all streams diff --git a/spec/tags/ruby/core/array/initialize_tags.txt b/spec/tags/ruby/core/array/initialize_tags.txt index 69278a5a61a..117732388f6 100644 --- a/spec/tags/ruby/core/array/initialize_tags.txt +++ b/spec/tags/ruby/core/array/initialize_tags.txt @@ -1,2 +1 @@ fails:Array#initialize with (size, object=nil) raises an ArgumentError if size is too large -fails:Array#initialize with no arguments does not use the given block diff --git a/spec/tags/ruby/core/exception/no_method_error_tags.txt b/spec/tags/ruby/core/exception/no_method_error_tags.txt index 653a5a20886..d44ee71de18 100644 --- a/spec/tags/ruby/core/exception/no_method_error_tags.txt +++ b/spec/tags/ruby/core/exception/no_method_error_tags.txt @@ -1,2 +1 @@ fails:NoMethodError#dup copies the name, arguments and receiver -fails:NoMethodError#message uses #name to display the receiver if it is a class or a module diff --git a/spec/tags/ruby/core/file/expand_path_tags.txt b/spec/tags/ruby/core/file/expand_path_tags.txt index dc68e78b9e2..0511481f040 100644 --- a/spec/tags/ruby/core/file/expand_path_tags.txt +++ b/spec/tags/ruby/core/file/expand_path_tags.txt @@ -2,4 +2,3 @@ fails:File.expand_path raises an Encoding::CompatibilityError if the external en windows:File.expand_path does not modify a HOME string argument fails:File.expand_path when HOME is not set uses the user database when passed '~' if HOME is nil fails:File.expand_path when HOME is not set uses the user database when passed '~/' if HOME is nil -fails:File.expand_path expands a path when the default external encoding is BINARY diff --git a/spec/tags/ruby/core/io/gets_tags.txt b/spec/tags/ruby/core/io/gets_tags.txt index 37c681ee974..42011ccde84 100644 --- a/spec/tags/ruby/core/io/gets_tags.txt +++ b/spec/tags/ruby/core/io/gets_tags.txt @@ -1,3 +1,4 @@ windows:IO#gets ignores the internal encoding if the default external encoding is ASCII-8BIT windows:IO#gets transcodes to internal encoding if the IO object's external encoding is ASCII-8BIT fails:IO#gets when passed chomp raises exception when options passed as Hash +fails:IO#gets sets $_ to nil after the last line has been read diff --git a/spec/tags/ruby/core/io/pread_tags.txt b/spec/tags/ruby/core/io/pread_tags.txt new file mode 100644 index 00000000000..3110b947dba --- /dev/null +++ b/spec/tags/ruby/core/io/pread_tags.txt @@ -0,0 +1,2 @@ +fails:IO#pread converts maxlen to Integer using #to_int +fails:IO#pread raises ArgumentError for negative values of maxlen diff --git a/spec/tags/ruby/core/io/pwrite_tags.txt b/spec/tags/ruby/core/io/pwrite_tags.txt new file mode 100644 index 00000000000..0c9a41052f5 --- /dev/null +++ b/spec/tags/ruby/core/io/pwrite_tags.txt @@ -0,0 +1,2 @@ +fails:IO#pwrite calls #to_s on the object to be written +fails:IO#pwrite raises a NoMethodError if object does not respond to #to_s diff --git a/spec/tags/ruby/core/io/read_tags.txt b/spec/tags/ruby/core/io/read_tags.txt index 35ddb8e4e5d..00e6cc06b17 100644 --- a/spec/tags/ruby/core/io/read_tags.txt +++ b/spec/tags/ruby/core/io/read_tags.txt @@ -5,5 +5,4 @@ windows:IO#read expands the buffer when too small windows:IO#read overwrites the buffer windows:IO#read truncates the buffer when too big windows:IO#read on Windows normalizes line endings in text mode -fails:IO#read raises IOError when stream is closed by another thread fails:IO.read accepts options as keyword arguments diff --git a/spec/tags/ruby/core/kernel/instance_variables_tags.txt b/spec/tags/ruby/core/kernel/instance_variables_tags.txt deleted file mode 100644 index df860386d01..00000000000 --- a/spec/tags/ruby/core/kernel/instance_variables_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Kernel#instance_variables regular objects returns the instances variables in the order declared diff --git a/spec/tags/ruby/core/kernel/lambda_tags.txt b/spec/tags/ruby/core/kernel/lambda_tags.txt index 5badd4703b5..ab3c7010a2a 100644 --- a/spec/tags/ruby/core/kernel/lambda_tags.txt +++ b/spec/tags/ruby/core/kernel/lambda_tags.txt @@ -1,2 +1 @@ fails:Kernel.lambda does not create lambda-style Procs when captured with #method -fails:Kernel.lambda when called without a literal block warns when proc isn't a lambda diff --git a/spec/tags/ruby/core/kernel/require_tags.txt b/spec/tags/ruby/core/kernel/require_tags.txt index eb3c91aa2d1..5dbb139e7f7 100644 --- a/spec/tags/ruby/core/kernel/require_tags.txt +++ b/spec/tags/ruby/core/kernel/require_tags.txt @@ -7,3 +7,5 @@ fails:Kernel.require ($LOADED_FEATURES) with symlinks in the required feature an fails:Kernel#require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES fails:Kernel.require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES fails:Kernel#require complex, enumerator, rational, thread, ruby2_keywords, fiber are already required +fails:Kernel#require (path resolution) loads c-extension file when passed absolute path without extension when no .rb is present +fails:Kernel.require (path resolution) loads c-extension file when passed absolute path without extension when no .rb is present diff --git a/spec/tags/ruby/core/kernel/sprintf_tags.txt b/spec/tags/ruby/core/kernel/sprintf_tags.txt index 4195a92a340..588543ed749 100644 --- a/spec/tags/ruby/core/kernel/sprintf_tags.txt +++ b/spec/tags/ruby/core/kernel/sprintf_tags.txt @@ -1,3 +1,2 @@ -fails(compiler):Kernel#sprintf passes some tests for negative %u fails:Kernel#sprintf %c raises error when a codepoint isn't representable in an encoding of a format string fails:Kernel.sprintf %c raises error when a codepoint isn't representable in an encoding of a format string diff --git a/spec/tags/ruby/core/marshal/dump_tags.txt b/spec/tags/ruby/core/marshal/dump_tags.txt index a054fffe222..a1ecbee8cb4 100644 --- a/spec/tags/ruby/core/marshal/dump_tags.txt +++ b/spec/tags/ruby/core/marshal/dump_tags.txt @@ -1,8 +1,5 @@ fails:Marshal.dump with a Time dumps the zone and the offset -fails(travis):Marshal.dump with an Exception dumps the message for the exception fails:Marshal.dump with a Symbol dumps a binary encoded Symbol -fails:Marshal.dump with an Exception dumps an empty Exception -fails:Marshal.dump with an Exception contains the filename in the backtrace fails:Marshal.dump with an Object dumps an Object with a non-US-ASCII instance variable fails:Marshal.dump with an Exception dumps the cause for the exception fails:Marshal.dump with a Time dumps the zone, but not the offset if zone is UTC diff --git a/spec/tags/ruby/core/module/define_method_tags.txt b/spec/tags/ruby/core/module/define_method_tags.txt index 87105537d21..6b77b22328e 100644 --- a/spec/tags/ruby/core/module/define_method_tags.txt +++ b/spec/tags/ruby/core/module/define_method_tags.txt @@ -1,6 +1,8 @@ fails:Method#define_method when passed a Proc object and a method is defined inside defines the nested method in the default definee where the Proc was created -fails:Module#define_method when the default definee is not the same as the module sets the visibility of the method to public +fails(on GHA):Module#define_method when the default definee is not the same as the module sets the visibility of the method to public fails:Method#define_method when passed a block behaves exactly like a lambda for break fails:Method#define_method when passed a block behaves exactly like a lambda for next -fails:Module#define_method raises TypeError when #to_str called on non-String name returns non-String value fails:Module#define_method raises a TypeError when the given method is no Method/Proc +fails:Module#define_method when passed a Proc object and a method is defined inside defines the nested method in the default definee where the Proc was created +fails:Module#define_method when passed a block behaves exactly like a lambda for break +fails:Module#define_method when passed a block behaves exactly like a lambda for next diff --git a/spec/tags/ruby/core/module/instance_method_tags.txt b/spec/tags/ruby/core/module/instance_method_tags.txt deleted file mode 100644 index ebfd7613fd1..00000000000 --- a/spec/tags/ruby/core/module/instance_method_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Module#instance_method raises TypeError when passed non-String name and #to_str returns non-String value diff --git a/spec/tags/ruby/core/module/private_tags.txt b/spec/tags/ruby/core/module/private_tags.txt index 788d7a8fcb6..8b04e50291b 100644 --- a/spec/tags/ruby/core/module/private_tags.txt +++ b/spec/tags/ruby/core/module/private_tags.txt @@ -1,3 +1,2 @@ fails:Module#private without arguments does not affect method definitions when itself is inside an eval and method definitions are outside fails:Module#private without arguments within a closure sets the visibility outside the closure -fails:Module#private continues to allow a prepended module method to call super diff --git a/spec/tags/ruby/core/module/refine_tags.txt b/spec/tags/ruby/core/module/refine_tags.txt index c2ddcfe693a..f56a17a41d6 100644 --- a/spec/tags/ruby/core/module/refine_tags.txt +++ b/spec/tags/ruby/core/module/refine_tags.txt @@ -1,12 +1,5 @@ -fails:Module#refine for methods accessed indirectly is honored by Kernel#send -fails:Module#refine for methods accessed indirectly is honored by BasicObject#__send__ fails:Module#refine for methods accessed indirectly is not honored by & fails(not implemented, jruby/jruby#6161):Module#refine for methods accessed indirectly is honored by & -fails(not implemented, jruby/jruby#6161):Module#refine for methods accessed indirectly is honored by Kernel#public_send -fails(not implemented, jruby/jruby#6161):Module#refine for methods accessed indirectly is honored by Kernel#respond_to? fails:Module#refine when super is called in a refinement looks in the lexical scope refinements before other active refinements -fails:Module#refine for methods accessed indirectly is honored by Kernel#method fails:Module#refine for methods accessed indirectly is honored by Kernel#public_method -fails:Module#refine for methods accessed indirectly is honored by Kernel#instance_method -fails:Module#refine applies refinements to calls in the refine block fails:Module#refine raises TypeError if not passed a class diff --git a/spec/tags/ruby/core/module/ruby2_keywords_tags.txt b/spec/tags/ruby/core/module/ruby2_keywords_tags.txt index 26967858ec1..b6df80f81b5 100644 --- a/spec/tags/ruby/core/module/ruby2_keywords_tags.txt +++ b/spec/tags/ruby/core/module/ruby2_keywords_tags.txt @@ -1,2 +1,2 @@ fails:Module#ruby2_keywords does NOT copy the Hash when calling a method taking (*args) -fails:Module#ruby2_keywords makes a copy and unmark the Hash when calling a method taking (arg) +fails(JIT mode only):Module#ruby2_keywords makes a copy and unmark the Hash when calling a method taking (arg) diff --git a/spec/tags/ruby/core/queue/initialize_tags.txt b/spec/tags/ruby/core/queue/initialize_tags.txt deleted file mode 100644 index a8d2cf8fea8..00000000000 --- a/spec/tags/ruby/core/queue/initialize_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Queue#initialize raises TypeError if #to_a does not return Array diff --git a/spec/tags/ruby/core/refinement/import_methods_tags.txt b/spec/tags/ruby/core/refinement/import_methods_tags.txt index a7e03894c00..5f71d530cde 100644 --- a/spec/tags/ruby/core/refinement/import_methods_tags.txt +++ b/spec/tags/ruby/core/refinement/import_methods_tags.txt @@ -1,4 +1,3 @@ -fails:Refinement#import_methods when methods are defined in Ruby code imports methods fails:Refinement#import_methods doesn't import any methods if one of the arguments is not a module fails:Refinement#import_methods imports methods from multiple modules so that methods see other's module's methods fails:Refinement#import_methods imports methods from module so that methods can see each other diff --git a/spec/tags/ruby/core/regexp/encoding_tags.txt b/spec/tags/ruby/core/regexp/encoding_tags.txt deleted file mode 100644 index fc74e8f486b..00000000000 --- a/spec/tags/ruby/core/regexp/encoding_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Regexp#encoding allows otherwise invalid characters if NOENCODING is specified diff --git a/spec/tags/ruby/core/signal/trap_tags.txt b/spec/tags/ruby/core/signal/trap_tags.txt index 89cb4130017..8dce29baa9d 100644 --- a/spec/tags/ruby/core/signal/trap_tags.txt +++ b/spec/tags/ruby/core/signal/trap_tags.txt @@ -7,4 +7,4 @@ fails:Signal.trap raises ArgumentError or Errno::EINVAL for SIGSTOP fails:Signal.trap accepts 'SYSTEM_DEFAULT' and uses the OS handler for SIGPIPE fails:Signal.trap allows to register a handler for all known signals, except reserved signals for which it raises ArgumentError fails:Signal.trap raises ArgumentError when passed unknown signal - +fails:Signal.trap calls #to_str on an object to convert to a String diff --git a/spec/tags/ruby/core/string/lines_tags.txt b/spec/tags/ruby/core/string/lines_tags.txt deleted file mode 100644 index 3b3806dc9bf..00000000000 --- a/spec/tags/ruby/core/string/lines_tags.txt +++ /dev/null @@ -1,2 +0,0 @@ -fails:String#lines when `chomp` keyword argument is passed removes only specified separator -fails:String#lines when `chomp` keyword argument is passed ignores new line characters when separator is specified diff --git a/spec/tags/ruby/core/string/undump_tags.txt b/spec/tags/ruby/core/string/undump_tags.txt deleted file mode 100644 index f3cb250489a..00000000000 --- a/spec/tags/ruby/core/string/undump_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:String#undump Limitations cannot undump non ASCII-compatible string diff --git a/spec/tags/ruby/core/symbol/end_with_tags.txt b/spec/tags/ruby/core/symbol/end_with_tags.txt index 7340b3604cc..ca4cca3c18d 100644 --- a/spec/tags/ruby/core/symbol/end_with_tags.txt +++ b/spec/tags/ruby/core/symbol/end_with_tags.txt @@ -1 +1 @@ -fails:Symbol#end_with? checks that we are starting to match at the head of a character +fails(on GHA):Symbol#end_with? checks that we are starting to match at the head of a character \ No newline at end of file diff --git a/spec/tags/ruby/core/thread/backtrace_tags.txt b/spec/tags/ruby/core/thread/backtrace_tags.txt index 251c5604367..faaf0defe67 100644 --- a/spec/tags/ruby/core/thread/backtrace_tags.txt +++ b/spec/tags/ruby/core/thread/backtrace_tags.txt @@ -1,4 +1,3 @@ fails:Thread#backtrace returns an array (which may be empty) immediately after the thread is created fails:Thread#backtrace returns the current backtrace of a thread fails:Thread#backtrace can be called with a range whose end is negative -fails:Thread#backtrace returns nil if omitting more locations than available diff --git a/spec/tags/ruby/core/thread/report_on_exception_tags.txt b/spec/tags/ruby/core/thread/report_on_exception_tags.txt index f215ff5d223..10a8e52a012 100644 --- a/spec/tags/ruby/core/thread/report_on_exception_tags.txt +++ b/spec/tags/ruby/core/thread/report_on_exception_tags.txt @@ -1,2 +1 @@ fails:Thread#report_on_exception= when set to true prints a backtrace on $stderr in the regular backtrace order -fails:Thread#report_on_exception= when set to true prints the backtrace even if the thread was killed just after Thread#raise diff --git a/spec/tags/ruby/core/thread/terminate_tags.txt b/spec/tags/ruby/core/thread/terminate_tags.txt index aedde8fc40e..deb5a1a3cea 100644 --- a/spec/tags/ruby/core/thread/terminate_tags.txt +++ b/spec/tags/ruby/core/thread/terminate_tags.txt @@ -1,2 +1,2 @@ unstable:Thread#terminate killing dying running does nothing -fails(we disagree that ensures should not be run):Thread#terminate kills other fibers of that thread without running their ensure clauses \ No newline at end of file +unstable(and we disagree with ruby-core about whether ensures should run):Thread#terminate kills other fibers of that thread without running their ensure clauses \ No newline at end of file diff --git a/spec/tags/ruby/core/unboundmethod/hash_tags.txt b/spec/tags/ruby/core/unboundmethod/hash_tags.txt deleted file mode 100644 index 033a044a20e..00000000000 --- a/spec/tags/ruby/core/unboundmethod/hash_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:UnboundMethod#hash returns the same value for builtin methods that are eql? diff --git a/spec/tags/ruby/core/warning/element_set_tags.txt b/spec/tags/ruby/core/warning/element_set_tags.txt deleted file mode 100644 index f5f4b2936a8..00000000000 --- a/spec/tags/ruby/core/warning/element_set_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Warning.[]= :experimental emits and suppresses warnings for :experimental diff --git a/spec/tags/ruby/core/warning/warn_tags.txt b/spec/tags/ruby/core/warning/warn_tags.txt deleted file mode 100644 index 8c45348bb34..00000000000 --- a/spec/tags/ruby/core/warning/warn_tags.txt +++ /dev/null @@ -1,2 +0,0 @@ -fails:Warning.warn is called by Kernel.warn with nil category keyword -fails:Warning.warn is called by Kernel.warn with given category keyword converted to a symbol diff --git a/spec/tags/ruby/language/method_tags.txt b/spec/tags/ruby/language/method_tags.txt index d254a6651f4..67c0e5148ad 100644 --- a/spec/tags/ruby/language/method_tags.txt +++ b/spec/tags/ruby/language/method_tags.txt @@ -1,2 +1 @@ -fails:A method assigns local variables from method parameters for definition 'def m(*a) a end' fails:A method assigns local variables from method parameters for definition 'def m(a, **nil); a end;' diff --git a/spec/tags/ruby/language/predefined_tags.txt b/spec/tags/ruby/language/predefined_tags.txt index 00debe69998..29e89f3a3b7 100644 --- a/spec/tags/ruby/language/predefined_tags.txt +++ b/spec/tags/ruby/language/predefined_tags.txt @@ -4,5 +4,3 @@ windows:The predefined global constant ARGV contains Strings encoded in locale E fails:Predefined global $! in bodies without ensure should be cleared when an exception is rescued even when a non-local return is present fails(etc not .so/.jar):$LOAD_PATH.resolve_feature_path returns what will be loaded without actual loading, .so file fails:Execution variable $: default $LOAD_PATH entries until sitelibdir included have @gem_prelude_index set -fails:Predefined global $, warns if assigned non-nil -fails:Predefined global $; warns if assigned non-nil diff --git a/spec/tags/ruby/language/range_tags.txt b/spec/tags/ruby/language/range_tags.txt index 64bc2fa65f3..d6fb003fec9 100644 --- a/spec/tags/ruby/language/range_tags.txt +++ b/spec/tags/ruby/language/range_tags.txt @@ -1 +1 @@ -fails:Literal Ranges creates a simple range as an object literal +fails(JIT mode only):Literal Ranges creates a simple range as an object literal diff --git a/spec/tags/ruby/language/regexp/repetition_tags.txt b/spec/tags/ruby/language/regexp/repetition_tags.txt deleted file mode 100644 index 348391570af..00000000000 --- a/spec/tags/ruby/language/regexp/repetition_tags.txt +++ /dev/null @@ -1,2 +0,0 @@ -fails:Regexps with repetition does not treat {m,n}+ as possessive -fails:Regexps with repetition supports nested quantifiers diff --git a/spec/tags/ruby/library/fiber/current_tags.txt b/spec/tags/ruby/library/fiber/current_tags.txt index b77dd3065e5..f83d68ad6c0 100644 --- a/spec/tags/ruby/library/fiber/current_tags.txt +++ b/spec/tags/ruby/library/fiber/current_tags.txt @@ -1,2 +1 @@ fails:Fiber.current returns the current Fiber when called from a Fiber that transferred to another -fails:Fiber.current is available without an extra require diff --git a/spec/tags/ruby/library/logger/logger/new_tags.txt b/spec/tags/ruby/library/logger/logger/new_tags.txt index 788dee7c106..1cf912d3055 100644 --- a/spec/tags/ruby/library/logger/logger/new_tags.txt +++ b/spec/tags/ruby/library/logger/logger/new_tags.txt @@ -1,2 +1 @@ windows:Logger#new creates a new logger object - diff --git a/spec/tags/ruby/library/openssl/digest/append_tags.txt b/spec/tags/ruby/library/openssl/digest/append_tags.txt new file mode 100644 index 00000000000..4ba14aae284 --- /dev/null +++ b/spec/tags/ruby/library/openssl/digest/append_tags.txt @@ -0,0 +1,8 @@ +fails:OpenSSL::Digest#<< when input is not a String and responds to #to_str returns a SHA1 digest +fails:OpenSSL::Digest#<< when input is not a String and responds to #to_str returns a SHA256 digest +fails:OpenSSL::Digest#<< when input is not a String and responds to #to_str returns a SHA384 digest +fails:OpenSSL::Digest#<< when input is not a String and responds to #to_str returns a SHA512 digest +fails:OpenSSL::Digest#<< when input is not a String and does not respond to #to_str raises a TypeError with SHA1 +fails:OpenSSL::Digest#<< when input is not a String and does not respond to #to_str raises a TypeError with SHA256 +fails:OpenSSL::Digest#<< when input is not a String and does not respond to #to_str raises a TypeError with SHA384 +fails:OpenSSL::Digest#<< when input is not a String and does not respond to #to_str raises a TypeError with SHA512 diff --git a/spec/tags/ruby/library/openssl/digest/initialize_tags.txt b/spec/tags/ruby/library/openssl/digest/initialize_tags.txt new file mode 100644 index 00000000000..6d25bb07e05 --- /dev/null +++ b/spec/tags/ruby/library/openssl/digest/initialize_tags.txt @@ -0,0 +1,13 @@ +fails:OpenSSL::Digest#initialize cannot be called with a digest class +fails:OpenSSL::Digest#initialize can be called with a digest name returns a SHA1 object +fails:OpenSSL::Digest#initialize can be called with a digest name returns a SHA256 object +fails:OpenSSL::Digest#initialize can be called with a digest name returns a SHA384 object +fails:OpenSSL::Digest#initialize can be called with a digest name returns a SHA512 object +fails:OpenSSL::Digest#initialize can be called with a digest name throws an error when called with an unknown digest +fails:OpenSSL::Digest#initialize can be called with a digest name cannot be called with a symbol +fails:OpenSSL::Digest#initialize can be called with a digest name does not call #to_str on the argument +fails:OpenSSL::Digest#initialize can be called with a digest object returns a SHA1 object +fails:OpenSSL::Digest#initialize can be called with a digest object returns a SHA256 object +fails:OpenSSL::Digest#initialize can be called with a digest object returns a SHA384 object +fails:OpenSSL::Digest#initialize can be called with a digest object returns a SHA512 object +fails:OpenSSL::Digest#initialize can be called with a digest object ignores the state of the digest object diff --git a/spec/tags/ruby/library/openssl/digest/name_tags.txt b/spec/tags/ruby/library/openssl/digest/name_tags.txt new file mode 100644 index 00000000000..c81a7dcadd6 --- /dev/null +++ b/spec/tags/ruby/library/openssl/digest/name_tags.txt @@ -0,0 +1 @@ +fails:OpenSSL::Digest#name converts the name to the internal representation of OpenSSL diff --git a/spec/tags/ruby/library/openssl/digest/update_tags.txt b/spec/tags/ruby/library/openssl/digest/update_tags.txt new file mode 100644 index 00000000000..9159dac950e --- /dev/null +++ b/spec/tags/ruby/library/openssl/digest/update_tags.txt @@ -0,0 +1,8 @@ +fails:OpenSSL::Digest#update when input is not a String and responds to #to_str returns a SHA1 digest +fails:OpenSSL::Digest#update when input is not a String and responds to #to_str returns a SHA256 digest +fails:OpenSSL::Digest#update when input is not a String and responds to #to_str returns a SHA384 digest +fails:OpenSSL::Digest#update when input is not a String and responds to #to_str returns a SHA512 digest +fails:OpenSSL::Digest#update when input is not a String and does not respond to #to_str raises a TypeError with SHA1 +fails:OpenSSL::Digest#update when input is not a String and does not respond to #to_str raises a TypeError with SHA256 +fails:OpenSSL::Digest#update when input is not a String and does not respond to #to_str raises a TypeError with SHA384 +fails:OpenSSL::Digest#update when input is not a String and does not respond to #to_str raises a TypeError with SHA512 diff --git a/spec/tags/ruby/library/openssl/kdf/pbkdf2_hmac_tags.txt b/spec/tags/ruby/library/openssl/kdf/pbkdf2_hmac_tags.txt new file mode 100644 index 00000000000..ed329d16c9d --- /dev/null +++ b/spec/tags/ruby/library/openssl/kdf/pbkdf2_hmac_tags.txt @@ -0,0 +1,9 @@ +fails:OpenSSL::KDF.pbkdf2_hmac treats all keywords as required +fails:OpenSSL::KDF.pbkdf2_hmac raises an OpenSSL::KDF::KDFError for 0 or less iterations +fails:OpenSSL::KDF.pbkdf2_hmac raises a TypeError when hash is neither a String nor an OpenSSL::Digest +fails:OpenSSL::KDF.pbkdf2_hmac raises a TypeError when hash is neither a String nor an OpenSSL::Digest, it does not try to call #to_str +fails:OpenSSL::KDF.pbkdf2_hmac raises a RuntimeError for unknown digest algorithms +fails:OpenSSL::KDF.pbkdf2_hmac treats salt as a required keyword +fails:OpenSSL::KDF.pbkdf2_hmac treats iterations as a required keyword +fails:OpenSSL::KDF.pbkdf2_hmac treats length as a required keyword +fails:OpenSSL::KDF.pbkdf2_hmac treats hash as a required keyword diff --git a/spec/tags/ruby/library/openssl/kdf/scrypt_tags.txt b/spec/tags/ruby/library/openssl/kdf/scrypt_tags.txt new file mode 100644 index 00000000000..08c626ff287 --- /dev/null +++ b/spec/tags/ruby/library/openssl/kdf/scrypt_tags.txt @@ -0,0 +1,29 @@ +fails:OpenSSL::KDF.scrypt creates the same value with the same input +fails:OpenSSL::KDF.scrypt supports nullbytes embedded into the password +fails:OpenSSL::KDF.scrypt coerces the password into a String using #to_str +fails:OpenSSL::KDF.scrypt coerces the salt into a String using #to_str +fails:OpenSSL::KDF.scrypt coerces the N into an Integer using #to_int +fails:OpenSSL::KDF.scrypt coerces the r into an Integer using #to_int +fails:OpenSSL::KDF.scrypt coerces the p into an Integer using #to_int +fails:OpenSSL::KDF.scrypt coerces the length into an Integer using #to_int +fails:OpenSSL::KDF.scrypt accepts an empty password +fails:OpenSSL::KDF.scrypt accepts an empty salt +fails:OpenSSL::KDF.scrypt accepts a zero length +fails:OpenSSL::KDF.scrypt accepts an arbitrary length +fails:OpenSSL::KDF.scrypt raises a TypeError when password is not a String and does not respond to #to_str +fails:OpenSSL::KDF.scrypt raises a TypeError when salt is not a String and does not respond to #to_str +fails:OpenSSL::KDF.scrypt raises a TypeError when N is not an Integer and does not respond to #to_int +fails:OpenSSL::KDF.scrypt raises a TypeError when r is not an Integer and does not respond to #to_int +fails:OpenSSL::KDF.scrypt raises a TypeError when p is not an Integer and does not respond to #to_int +fails:OpenSSL::KDF.scrypt raises a TypeError when length is not an Integer and does not respond to #to_int +fails:OpenSSL::KDF.scrypt treats salt as a required keyword +fails:OpenSSL::KDF.scrypt treats N as a required keyword +fails:OpenSSL::KDF.scrypt treats r as a required keyword +fails:OpenSSL::KDF.scrypt treats p as a required keyword +fails:OpenSSL::KDF.scrypt treats length as a required keyword +fails:OpenSSL::KDF.scrypt treats all keywords as required +fails:OpenSSL::KDF.scrypt requires N to be a power of 2 +fails:OpenSSL::KDF.scrypt requires N to be at least 2 +fails:OpenSSL::KDF.scrypt requires r to be positive +fails:OpenSSL::KDF.scrypt requires p to be positive +fails:OpenSSL::KDF.scrypt requires length to be not negative diff --git a/spec/tags/ruby/library/openstruct/to_h_tags.txt b/spec/tags/ruby/library/openstruct/to_h_tags.txt deleted file mode 100644 index 04e62cf398d..00000000000 --- a/spec/tags/ruby/library/openstruct/to_h_tags.txt +++ /dev/null @@ -1,5 +0,0 @@ -fails(reverted ostruct due to https://github.com/ruby/ostruct/issues/30):OpenStruct#to_h with block converts [key, value] pairs returned by the block to a hash -fails(reverted ostruct due to https://github.com/ruby/ostruct/issues/30):OpenStruct#to_h with block raises ArgumentError if block returns longer or shorter array -fails(reverted ostruct due to https://github.com/ruby/ostruct/issues/30):OpenStruct#to_h with block raises TypeError if block returns something other than Array -fails(reverted ostruct due to https://github.com/ruby/ostruct/issues/30):OpenStruct#to_h with block coerces returned pair to Array with #to_ary -fails(reverted ostruct due to https://github.com/ruby/ostruct/issues/30):OpenStruct#to_h with block does not coerce returned pair to Array with #to_a diff --git a/spec/tags/ruby/library/weakref/weakref_alive_tags.txt b/spec/tags/ruby/library/weakref/weakref_alive_tags.txt deleted file mode 100644 index ee77dd8fc64..00000000000 --- a/spec/tags/ruby/library/weakref/weakref_alive_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails(depends on GC behavior, fails intermittently):WeakRef#weakref_alive? returns a falsy value if the object is no longer reachable diff --git a/spec/tags/ruby/library/yaml/to_yaml_tags.txt b/spec/tags/ruby/library/yaml/to_yaml_tags.txt index 5c489f6230e..8a2304db8fb 100644 --- a/spec/tags/ruby/library/yaml/to_yaml_tags.txt +++ b/spec/tags/ruby/library/yaml/to_yaml_tags.txt @@ -1,2 +1 @@ fails:Object#to_yaml returns the YAML representation of a RegExp object -fails:Object#to_yaml returns the YAML representation of a Error object diff --git a/spec/tags/ruby/library/zlib/deflate/deflate_tags.txt b/spec/tags/ruby/library/zlib/deflate/deflate_tags.txt index f53c089dd72..0126f382161 100644 --- a/spec/tags/ruby/library/zlib/deflate/deflate_tags.txt +++ b/spec/tags/ruby/library/zlib/deflate/deflate_tags.txt @@ -3,4 +3,3 @@ fails:Zlib::Deflate#deflate without break deflates chunked data fails:Zlib::Deflate#deflate without break deflates chunked data with final chunk fails:Zlib::Deflate#deflate with break deflates only first chunk fails:Zlib::Deflate#deflate with break deflates chunked data with final chunk -fails:Zlib::Deflate#deflate has a binary encoding diff --git a/spec/tags/ruby/library/zlib/inflate/inflate_tags.txt b/spec/tags/ruby/library/zlib/inflate/inflate_tags.txt index 43eea6a4133..2c3eb14a842 100644 --- a/spec/tags/ruby/library/zlib/inflate/inflate_tags.txt +++ b/spec/tags/ruby/library/zlib/inflate/inflate_tags.txt @@ -1,2 +1 @@ fails:Zlib::Inflate#inflate without break inflates chunked data -fails:Zlib::Inflate#inflate has a binary encoding diff --git a/test/mri.stdlib.index b/test/mri.stdlib.index index 4d8f273cf6a..0d17870ca70 100644 --- a/test/mri.stdlib.index +++ b/test/mri.stdlib.index @@ -21,7 +21,7 @@ fiber/test_backtrace.rb #fiber/test_mutex.rb #fiber/test_process.rb fiber/test_ractor.rb -#fiber/test_scheduler.rb +fiber/test_scheduler.rb #fiber/test_sleep.rb #fiber/test_thread.rb #fiber/test_timeout.rb diff --git a/test/mri/fiber/autoload.rb b/test/mri/fiber/autoload.rb new file mode 100644 index 00000000000..dcb27164a70 --- /dev/null +++ b/test/mri/fiber/autoload.rb @@ -0,0 +1,3 @@ +sleep 0.01 +module TestFiberSchedulerAutoload +end diff --git a/test/mri/fiber/scheduler.rb b/test/mri/fiber/scheduler.rb index 4138015e4b6..5090271db15 100644 --- a/test/mri/fiber/scheduler.rb +++ b/test/mri/fiber/scheduler.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true # This is an example and simplified scheduler for test purposes. -# It is not efficient for a large number of file descriptors as it uses IO.select(). -# Production Fiber schedulers should use epoll/kqueue/etc. +# - It is not efficient for a large number of file descriptors as it uses +# IO.select(). +# - It does not correctly handle multiple calls to `wait` with the same file +# descriptor and overlapping events. +# - Production fiber schedulers should use epoll/kqueue/etc. Consider using the +# [`io-event`](https://github.com/socketry/io-event) gem instead of this +# scheduler if you want something simple to build on. require 'fiber' require 'socket' @@ -30,7 +35,7 @@ def initialize @closed = false @lock = Thread::Mutex.new - @blocking = 0 + @blocking = Hash.new.compare_by_identity @ready = [] @urgent = IO.pipe @@ -57,8 +62,8 @@ def next_timeout def run # $stderr.puts [__method__, Fiber.current].inspect - while @readable.any? or @writable.any? or @waiting.any? or @blocking.positive? - # Can only handle file descriptors up to 1024... + while @readable.any? or @writable.any? or @waiting.any? or @blocking.any? + # May only handle file descriptors up to 1024... readable, writable = IO.select(@readable.keys + [@urgent.first], @writable.keys, [], next_timeout) # puts "readable: #{readable}" if readable&.any? @@ -115,10 +120,16 @@ def run end end + # A fiber scheduler hook, invoked when the scheduler goes out of scope. def scheduler_close close(true) end + # If the `scheduler_close` hook does not exist, this method `close` will be + # invoked instead when the fiber scheduler goes out of scope. This is legacy + # behaviour, you should almost certainly use `scheduler_close`. The reason for + # this, is `scheduler_close` is called when the scheduler goes out of scope, + # while `close` may be called by the user. def close(internal = false) # $stderr.puts [__method__, Fiber.current].inspect @@ -153,6 +164,7 @@ def current_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end + # This hook is invoked by `Timeout.timeout` and related code. def timeout_after(duration, klass, message, &block) fiber = Fiber.current @@ -171,6 +183,7 @@ def timeout_after(duration, klass, message, &block) end end + # This hook is invoked by `Process.wait`, `system`, and backticks. def process_wait(pid, flags) # $stderr.puts [__method__, pid, flags, Fiber.current].inspect @@ -180,24 +193,47 @@ def process_wait(pid, flags) end.value end + # This hook is invoked by `IO#read` and `IO#write` in the case that `io_read` + # and `io_write` hooks are not available. This implementation is not + # completely general, in the sense that calling `io_wait` multiple times with + # the same `io` and `events` will not work, which is okay for tests but not + # for real code. Correct fiber schedulers should not have this limitation. def io_wait(io, events, duration) # $stderr.puts [__method__, io, events, duration, Fiber.current].inspect + fiber = Fiber.current + unless (events & IO::READABLE).zero? - @readable[io] = Fiber.current + @readable[io] = fiber + readable = true end unless (events & IO::WRITABLE).zero? - @writable[io] = Fiber.current + @writable[io] = fiber + writable = true + end + + if duration + @waiting[fiber] = current_time + duration end Fiber.yield ensure - @readable.delete(io) - @writable.delete(io) + @waiting.delete(fiber) if duration + @readable.delete(io) if readable + @writable.delete(io) if writable + end + + # This hook is invoked by `IO.select`. Using a thread ensures that the + # operation does not block the fiber scheduler. + def io_select(...) + # Emulate the operation using a non-blocking thread: + Thread.new do + IO.select(...) + end.value end - # Used for Kernel#sleep and Thread::Mutex#sleep + # This hook is invoked by `Kernel#sleep` and `Thread::Mutex#sleep`. def kernel_sleep(duration = nil) # $stderr.puts [__method__, duration, Fiber.current].inspect @@ -206,32 +242,35 @@ def kernel_sleep(duration = nil) return true end - # Used when blocking on synchronization (Thread::Mutex#lock, - # Thread::Queue#pop, Thread::SizedQueue#push, ...) + # This hook is invoked by blocking options such as `Thread::Mutex#lock`, + # `Thread::Queue#pop` and `Thread::SizedQueue#push`, which are unblocked by + # other threads/fibers. To unblock a blocked fiber, you should call `unblock` + # with the same `blocker` and `fiber` arguments. def block(blocker, timeout = nil) # $stderr.puts [__method__, blocker, timeout].inspect + fiber = Fiber.current + if timeout - @waiting[Fiber.current] = current_time + timeout + @waiting[fiber] = current_time + timeout begin Fiber.yield ensure # Remove from @waiting in the case #unblock was called before the timeout expired: - @waiting.delete(Fiber.current) + @waiting.delete(fiber) end else - @blocking += 1 + @blocking[fiber] = true begin Fiber.yield ensure - @blocking -= 1 + @blocking.delete(fiber) end end end - # Used when synchronization wakes up a previously-blocked fiber - # (Thread::Mutex#unlock, Thread::Queue#push, ...). - # This might be called from another thread. + # This method is invoked from a thread or fiber to unblock a fiber that is + # blocked by `block`. It is expected to be thread safe. def unblock(blocker, fiber) # $stderr.puts [__method__, blocker, fiber].inspect # $stderr.puts blocker.backtrace.inspect @@ -245,6 +284,9 @@ def unblock(blocker, fiber) io.write_nonblock('.') end + # This hook is invoked by `Fiber.schedule`. Strictly speaking, you should use + # it to create scheduled fibers, but it is not required in practice; + # `Fiber.new` is usually sufficient. def fiber(&block) fiber = Fiber.new(blocking: false, &block) @@ -253,6 +295,9 @@ def fiber(&block) return fiber end + # This hook is invoked by `Addrinfo.getaddrinfo`. Using a thread ensures that + # the operation does not block the fiber scheduler, since `getaddrinfo` is + # usually provided by `libc` and is blocking. def address_resolve(hostname) Thread.new do Addrinfo.getaddrinfo(hostname, nil).map(&:ip_address).uniq @@ -260,85 +305,133 @@ def address_resolve(hostname) end end +# This scheduler class implements `io_read` and `io_write` hooks which require +# `IO::Buffer`. class IOBufferScheduler < Scheduler - EAGAIN = Errno::EAGAIN::Errno + EAGAIN = -Errno::EAGAIN::Errno - def io_read(io, buffer, length) - offset = 0 + def io_read(io, buffer, length, offset) + total = 0 + io.nonblock = true while true maximum_size = buffer.size - offset - result = blocking{io.read_nonblock(maximum_size, exception: false)} - - # blocking{pp read: maximum_size, result: result, length: length} + result = blocking{buffer.read(io, maximum_size, offset)} - case result - when :wait_readable + if result > 0 + total += result + offset += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN if length > 0 self.io_wait(io, IO::READABLE, nil) else - return -EAGAIN + return result end - when :wait_writable + elsif result < 0 + return result + end + end + + return total + end + + def io_write(io, buffer, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.write(io, maximum_size, offset)} + + if result > 0 + total += result + offset += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN if length > 0 self.io_wait(io, IO::WRITABLE, nil) else - return -EAGAIN + return result end - else - break unless result - - buffer.set_string(result, offset) - - size = result.bytesize - offset += size - break if size >= length - length -= size + elsif result < 0 + return result end end - return offset + return total end - def io_write(io, buffer, length) - offset = 0 + def io_pread(io, buffer, from, length, offset) + total = 0 + io.nonblock = true while true maximum_size = buffer.size - offset + result = blocking{buffer.pread(io, from, maximum_size, offset)} - chunk = buffer.get_string(offset, maximum_size) - result = blocking{io.write_nonblock(chunk, exception: false)} - - # blocking{pp write: maximum_size, result: result, length: length} - - case result - when :wait_readable + if result > 0 + total += result + offset += result + from += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN if length > 0 self.io_wait(io, IO::READABLE, nil) else - return -EAGAIN + return result end - when :wait_writable + elsif result < 0 + return result + end + end + + return total + end + + def io_pwrite(io, buffer, from, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.pwrite(io, from, maximum_size, offset)} + + if result > 0 + total += result + offset += result + from += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN if length > 0 self.io_wait(io, IO::WRITABLE, nil) else - return -EAGAIN + return result end - else - offset += result - break if result >= length - length -= result + elsif result < 0 + return result end end - return offset + return total end def blocking(&block) - Fiber.new(blocking: true, &block).resume + Fiber.blocking(&block) end end +# This scheduler has a broken implementation of `unblock`` in the sense that it +# raises an exception. This is used to test the behavior of the scheduler when +# unblock raises an exception. class BrokenUnblockScheduler < Scheduler def unblock(blocker, fiber) super @@ -347,6 +440,9 @@ def unblock(blocker, fiber) end end +# This scheduler has a broken implementation of `unblock` in the sense that it +# sleeps. This is used to test the behavior of the scheduler when unblock +# messes with the internal thread state in an unexpected way. class SleepingUnblockScheduler < Scheduler # This method is invoked when the thread is exiting. def unblock(blocker, fiber) @@ -356,3 +452,16 @@ def unblock(blocker, fiber) sleep(0.1) end end + +# This scheduler has a broken implementation of `kernel_sleep` in the sense that +# it invokes a blocking sleep which can cause a deadlock in some cases. +class SleepingBlockingScheduler < Scheduler + def kernel_sleep(duration = nil) + # Deliberaly sleep in a blocking state which can trigger a deadlock if the implementation is not correct. + Fiber.blocking{sleep 0.0001} + + self.block(:sleep, duration) + + return true + end +end diff --git a/test/mri/fiber/test_scheduler.rb b/test/mri/fiber/test_scheduler.rb index 1870ab1c33e..34effad8163 100644 --- a/test/mri/fiber/test_scheduler.rb +++ b/test/mri/fiber/test_scheduler.rb @@ -27,6 +27,18 @@ def test_fiber_new_with_options refute f.blocking? end + def test_fiber_blocking + f = Fiber.new(blocking: false) do + fiber = Fiber.current + refute fiber.blocking? + Fiber.blocking do |_fiber| + assert_equal fiber, _fiber + assert fiber.blocking? + end + end + f.resume + end + def test_closed_at_thread_exit scheduler = Scheduler.new @@ -104,4 +116,70 @@ def test_current_scheduler thread.join end + + def test_autoload + 10.times do + Object.autoload(:TestFiberSchedulerAutoload, File.expand_path("autoload.rb", __dir__)) + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + 10.times do + Fiber.schedule do + Object.const_get(:TestFiberSchedulerAutoload) + end + end + end + + thread.join + ensure + $LOADED_FEATURES.delete(File.expand_path("autoload.rb", __dir__)) + Object.send(:remove_const, :TestFiberSchedulerAutoload) + end + end + + def test_deadlock + mutex = Thread::Mutex.new + condition = Thread::ConditionVariable.new + q = 0.0001 + + signaller = Thread.new do + loop do + mutex.synchronize do + condition.signal + end + sleep q + end + end + + i = 0 + + thread = Thread.new do + scheduler = SleepingBlockingScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + 10.times do + mutex.synchronize do + condition.wait(mutex) + sleep q + i += 1 + end + end + end + end + + # Wait for 10 seconds at most... if it doesn't finish, it's deadlocked. + thread.join(10) + + # If it's deadlocked, it will never finish, so this will be 0. + assert_equal 10, i + ensure + # Make sure the threads are dead... + thread.kill + signaller.kill + thread.join + signaller.join + end end diff --git a/test/mri/ruby/test_io_buffer.rb b/test/mri/ruby/test_io_buffer.rb index 7e3b467ed59..dea8388d7dc 100644 --- a/test/mri/ruby/test_io_buffer.rb +++ b/test/mri/ruby/test_io_buffer.rb @@ -80,7 +80,7 @@ def test_file_mapped end def test_file_mapped_invalid - assert_raise NoMethodError do + assert_raise TypeError do IO::Buffer.map("foobar") end end @@ -88,30 +88,39 @@ def test_file_mapped_invalid def test_string_mapped string = "Hello World" buffer = IO::Buffer.for(string) - refute buffer.readonly? - - # Cannot modify string as it's locked by the buffer: - assert_raise RuntimeError do - string[0] = "h" - end - - buffer.set_value(:U8, 0, "h".ord) - - # Buffer releases it's ownership of the string: - buffer.free - - assert_equal "hello World", string - string[0] = "H" - assert_equal "Hello World", string + assert buffer.readonly? end def test_string_mapped_frozen string = "Hello World".freeze buffer = IO::Buffer.for(string) - assert buffer.readonly? end + def test_string_mapped_mutable + string = "Hello World" + IO::Buffer.for(string) do |buffer| + refute buffer.readonly? + + buffer.set_value(:U8, 0, "h".ord) + + # Buffer releases it's ownership of the string: + buffer.free + + assert_equal "hello World", string + end + end + + def test_string_mapped_buffer_locked + string = "Hello World" + IO::Buffer.for(string) do |buffer| + # Cannot modify string as it's locked by the buffer: + assert_raise RuntimeError do + string[0] = "h" + end + end + end + def test_non_string not_string = Object.new @@ -120,6 +129,20 @@ def test_non_string end end + def test_string + result = IO::Buffer.string(12) do |buffer| + buffer.set_string("Hello World!") + end + + assert_equal "Hello World!", result + end + + def test_string_negative + assert_raise ArgumentError do + IO::Buffer.string(-1) + end + end + def test_resize_mapped buffer = IO::Buffer.new @@ -138,6 +161,24 @@ def test_resize_preserve assert_equal message, buffer.get_string(0, message.bytesize) end + def test_resize_zero_internal + buffer = IO::Buffer.new(1) + + buffer.resize(0) + assert_equal 0, buffer.size + + buffer.resize(1) + assert_equal 1, buffer.size + end + + def test_resize_zero_external + buffer = IO::Buffer.for('1') + + assert_raise IO::Buffer::AccessError do + buffer.resize(0) + end + end + def test_compare_same_size buffer1 = IO::Buffer.new(1) assert_equal buffer1, buffer1 @@ -165,16 +206,26 @@ def test_slice assert_equal("Hello World", buffer.get_string(8, 11)) end - def test_slice_bounds + def test_slice_arguments + buffer = IO::Buffer.for("Hello World") + + slice = buffer.slice + assert_equal "Hello World", slice.get_string + + slice = buffer.slice(2) + assert_equal("llo World", slice.get_string) + end + + def test_slice_bounds_error buffer = IO::Buffer.new(128) assert_raise ArgumentError do buffer.slice(128, 10) end - # assert_raise RuntimeError do - # pp buffer.slice(-10, 10) - # end + assert_raise ArgumentError do + buffer.slice(-10, 10) + end end def test_locked @@ -205,6 +256,18 @@ def test_get_string chunk = buffer.get_string(0, message.bytesize, Encoding::BINARY) assert_equal Encoding::BINARY, chunk.encoding + + assert_raise_with_message(ArgumentError, /bigger than the buffer size/) do + buffer.get_string(0, 129) + end + + assert_raise_with_message(ArgumentError, /bigger than the buffer size/) do + buffer.get_string(129) + end + + assert_raise_with_message(ArgumentError, /Offset can't be negative/) do + buffer.get_string(-1) + end end # We check that values are correctly round tripped. @@ -231,17 +294,59 @@ def test_get_string :F64 => [-1.0, 0.0, 0.5, 1.0, 128.0], } - def test_get_set_primitives + def test_get_set_value buffer = IO::Buffer.new(128) - RANGES.each do |type, values| + RANGES.each do |data_type, values| values.each do |value| - buffer.set_value(type, 0, value) - assert_equal value, buffer.get_value(type, 0), "Converting #{value} as #{type}." + buffer.set_value(data_type, 0, value) + assert_equal value, buffer.get_value(data_type, 0), "Converting #{value} as #{data_type}." end end end + def test_get_set_values + buffer = IO::Buffer.new(128) + + RANGES.each do |data_type, values| + format = [data_type] * values.size + + buffer.set_values(format, 0, values) + assert_equal values, buffer.get_values(format, 0), "Converting #{values} as #{format}." + end + end + + def test_values + buffer = IO::Buffer.new(128) + + RANGES.each do |data_type, values| + format = [data_type] * values.size + + buffer.set_values(format, 0, values) + assert_equal values, buffer.values(data_type, 0, values.size), "Reading #{values} as #{format}." + end + end + + def test_each + buffer = IO::Buffer.new(128) + + RANGES.each do |data_type, values| + format = [data_type] * values.size + data_type_size = IO::Buffer.size_of(data_type) + values_with_offsets = values.map.with_index{|value, index| [index * data_type_size, value]} + + buffer.set_values(format, 0, values) + assert_equal values_with_offsets, buffer.each(data_type, 0, values.size).to_a, "Reading #{values} as #{data_type}." + end + end + + def test_each_byte + string = "The quick brown fox jumped over the lazy dog." + buffer = IO::Buffer.for(string) + + assert_equal string.bytes, buffer.each_byte.to_a + end + def test_clear buffer = IO::Buffer.new(16) buffer.set_string("Hello World!") @@ -273,17 +378,38 @@ def test_invalidation input.close end - def test_read + def hello_world_tempfile io = Tempfile.new io.write("Hello World") io.seek(0) - buffer = IO::Buffer.new(128) - buffer.read(io, 5) - - assert_equal "Hello", buffer.get_string(0, 5) + yield io ensure - io.close! + io&.close! + end + + def test_read + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io) + assert_equal "Hello", buffer.get_string(0, 5) + end + end + + def test_read_with_with_length + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io, 5) + assert_equal "Hello", buffer.get_string(0, 5) + end + end + + def test_read_with_with_offset + hello_world_tempfile do |io| + buffer = IO::Buffer.new(128) + buffer.read(io, nil, 6) + assert_equal "Hello", buffer.get_string(6, 5) + end end def test_write @@ -291,7 +417,7 @@ def test_write buffer = IO::Buffer.new(128) buffer.set_string("Hello") - buffer.write(io, 5) + buffer.write(io) io.seek(0) assert_equal "Hello", io.read(5) @@ -305,7 +431,7 @@ def test_pread io.seek(0) buffer = IO::Buffer.new(128) - buffer.pread(io, 5, 6) + buffer.pread(io, 6, 5) assert_equal "World", buffer.get_string(0, 5) assert_equal 0, io.tell @@ -313,12 +439,41 @@ def test_pread io.close! end + def test_pread_offset + io = Tempfile.new + io.write("Hello World") + io.seek(0) + + buffer = IO::Buffer.new(128) + buffer.pread(io, 6, 5, 6) + + assert_equal "World", buffer.get_string(6, 5) + assert_equal 0, io.tell + ensure + io.close! + end + def test_pwrite io = Tempfile.new buffer = IO::Buffer.new(128) buffer.set_string("World") - buffer.pwrite(io, 5, 6) + buffer.pwrite(io, 6, 5) + + assert_equal 0, io.tell + + io.seek(6) + assert_equal "World", io.read(5) + ensure + io.close! + end + + def test_pwrite_offset + io = Tempfile.new + + buffer = IO::Buffer.new(128) + buffer.set_string("Hello World") + buffer.pwrite(io, 6, 5, 6) assert_equal 0, io.tell @@ -327,4 +482,39 @@ def test_pwrite ensure io.close! end + + def test_operators + source = IO::Buffer.for("1234123412") + mask = IO::Buffer.for("133\x00") + + assert_equal IO::Buffer.for("123\x00123\x0012"), (source & mask) + assert_equal IO::Buffer.for("1334133413"), (source | mask) + assert_equal IO::Buffer.for("\x00\x01\x004\x00\x01\x004\x00\x01"), (source ^ mask) + assert_equal IO::Buffer.for("\xce\xcd\xcc\xcb\xce\xcd\xcc\xcb\xce\xcd"), ~source + end + + def test_inplace_operators + source = IO::Buffer.for("1234123412") + mask = IO::Buffer.for("133\x00") + + assert_equal IO::Buffer.for("123\x00123\x0012"), source.dup.and!(mask) + assert_equal IO::Buffer.for("1334133413"), source.dup.or!(mask) + assert_equal IO::Buffer.for("\x00\x01\x004\x00\x01\x004\x00\x01"), source.dup.xor!(mask) + assert_equal IO::Buffer.for("\xce\xcd\xcc\xcb\xce\xcd\xcc\xcb\xce\xcd"), source.dup.not! + end + + def test_shared + message = "Hello World" + buffer = IO::Buffer.new(64, IO::Buffer::MAPPED | IO::Buffer::SHARED) + + pid = fork do + buffer.set_string(message) + end + + Process.wait(pid) + string = buffer.get_string(0, message.bytesize) + assert_equal message, string + rescue NotImplementedError + omit "Fork/shared memory is not supported." + end end diff --git a/tool/release.sh b/tool/release.sh index 34ae355e35e..4757da2f467 100644 --- a/tool/release.sh +++ b/tool/release.sh @@ -4,16 +4,28 @@ # 1. on failed run you must undo the version commit and probably should drop in nexus staging repo # 2. Assumes jruby you are using in your PATH works -set -x -export JRUBY_VERSION=9.4.4.0 -export WINDOWS_VERSION=9_4_4_0 +export REPO=${REPO:=jruby} + +java_version=$(java -version 2>&1 | head -1 | awk -F\" -e '{ print $2 }' | awk -F. '{print $1 "." $2 }') + +if [ "$java_version" != "1.8" ]; then + echo "You must use Java 1.8 to release JRuby" + exit 1 +fi + +[[ -z "$JRUBY_VERSION" ]] && { echo "set JRUBY_VERSION to something" ; exit 1; } + +export WINDOWS_VERSION=`echo $JRUBY_VERSION|sed -e 's/\./_/g'` echo $JRUBY_VERSION > VERSION + +set -x + mvn git add VERSION core/pom.xml lib/pom.xml pom.xml shaded/pom.xml git commit -m "Version $JRUBY_VERSION updated for release" cd .. rm -rf release -git clone jruby release +git clone $REPO release cd release pwd mvn clean deploy -Psonatype-oss-release -Prelease