diff --git a/Build/dlr-patches/0100-compile-pipeline.patch b/Build/dlr-patches/0100-compile-pipeline.patch new file mode 100644 index 000000000..9d1551346 --- /dev/null +++ b/Build/dlr-patches/0100-compile-pipeline.patch @@ -0,0 +1,59 @@ +diff --git a/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs b/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs +index 3b7bbb5..a979b25 100644 +--- a/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs ++++ b/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs +@@ -25,6 +25,19 @@ namespace Microsoft.Scripting.Interpreter { + + private static readonly Dictionary _cache = new(); + ++ /// ++ /// The instruction made and cached for ++ /// before, if any. A method is cached only once it is known to need none of the special handling Create ++ /// checks for (array accessors, by-ref parameters, value-type instance methods, dynamic methods, too many ++ /// arguments), so a hit can skip GetParameters() - which copies the parameter array on every call - and ++ /// those checks altogether. ++ /// ++ internal static bool TryGetCached(MethodInfo info, out CallInstruction instruction) { ++ lock (_cache) { ++ return _cache.TryGetValue(info, out instruction); ++ } ++ } ++ + /// Instruction can't be created due to insufficient privileges. + public static CallInstruction Create(MethodInfo info) { + return Create(info, info.GetParameters()); +diff --git a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs +index 457ec85..84b88f7 100644 +--- a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs ++++ b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs +@@ -1153,6 +1153,19 @@ namespace Microsoft.Scripting.Interpreter { + private void CompileMethodCallExpression(Expression expr) { + var node = (MethodCallExpression)expr; + ++ // A method compiled before has its instruction cached, which also says it takes no by-ref ++ // parameters and is not an instance method of a value type (see CallInstruction.TryGetCached). ++ if (CallInstruction.TryGetCached(node.Method, out CallInstruction cached)) { ++ if (!node.Method.IsStatic) { ++ Compile(node.Object); ++ } ++ foreach (var arg in node.Arguments) { ++ Compile(arg); ++ } ++ Instructions.Emit(cached); ++ return; ++ } ++ + var parameters = node.Method.GetParameters(); + + // TODO: +@@ -1180,6 +1193,10 @@ namespace Microsoft.Scripting.Interpreter { + } + + public void EmitCall(MethodInfo method) { ++ if (CallInstruction.TryGetCached(method, out CallInstruction cached)) { ++ Instructions.Emit(cached); ++ return; ++ } + EmitCall(method, method.GetParameters()); + } + diff --git a/Build/dlr-patches/0100-exceptions.patch b/Build/dlr-patches/0100-exceptions.patch new file mode 100644 index 000000000..51caeb89a --- /dev/null +++ b/Build/dlr-patches/0100-exceptions.patch @@ -0,0 +1,168 @@ +diff --git a/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs b/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs +index 1a32e58..0a97958 100644 +--- a/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs ++++ b/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs +@@ -146,10 +146,26 @@ namespace Microsoft.Scripting.Interpreter { + + internal void SaveTraceToException(Exception exception) { + if (exception.GetData(typeof(InterpretedFrameInfo)) is null) { +- exception.SetData(typeof(InterpretedFrameInfo), GetStackTraceDebugInfo().ToArray()); ++ exception.SetData(typeof(InterpretedFrameInfo), GetStackTraceDebugInfoArray()); + } + } + ++ // GetStackTraceDebugInfo().ToArray() without the iterator and the list it grows: every ++ // exception that passes an interpreted frame takes this snapshot of the whole chain. ++ private InterpretedFrameInfo[] GetStackTraceDebugInfoArray() { ++ int count = 0; ++ for (var frame = this; frame is not null; frame = frame.Parent) { ++ count++; ++ } ++ ++ var result = new InterpretedFrameInfo[count]; ++ int i = 0; ++ for (var frame = this; frame is not null; frame = frame.Parent) { ++ result[i++] = new InterpretedFrameInfo(frame.Name, frame.GetDebugInfo(frame.InstructionIndex)); ++ } ++ return result; ++ } ++ + public static InterpretedFrameInfo[] GetExceptionStackTrace(Exception exception) { + return exception.GetData(typeof(InterpretedFrameInfo)) as InterpretedFrameInfo[]; + } +diff --git a/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs b/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs +index 7176c59..047827a 100644 +--- a/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs ++++ b/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs +@@ -30,6 +30,131 @@ namespace Microsoft.Scripting.Utils { + public static void RemoveData(this Exception e, object key) { + e.Data.Remove(key); + } ++#elif NETCOREAPP ++ // Exception.Data's dictionary, as on the desktop: languages attach data to every exception they ++ // see go by (the interpreter its frames, IronRuby its exception state), several times per raise. ++ // The ConditionalWeakTable below costs a dependent handle per exception, which every GC has to ++ // scan, and a global lock plus a LINQ scan per lookup. The dictionary is not thread-safe, hence ++ // the lock on it - uncontended in practice. ++ // ++ // The dictionary is reached through Exception's own field where the runtime has it: the Data ++ // property is virtual, and the subclasses a language generates for its own exception classes ++ // override every virtual member to dispatch to the language first (IronRuby's do), which made a ++ // Data lookup cost microseconds. An exception type whose Data is not writable gets the table. ++ public static object GetData(this Exception e, object key) { ++ var data = GetDictionary(e, false); ++ if (data is null) { ++ return _hasTableData ? GetTableData(e, key) : null; ++ } ++ lock (data) { ++ return data[key]; ++ } ++ } ++ ++ public static void SetData(this Exception e, object key, object value) { ++ var data = GetDictionary(e, true); ++ if (data is null) { ++ SetTableData(e, key, value); ++ return; ++ } ++ lock (data) { ++ data[key] = value; ++ } ++ } ++ ++ public static void RemoveData(this Exception e, object key) { ++ var data = GetDictionary(e, false); ++ if (data is null) { ++ if (_hasTableData) { ++ RemoveTableData(e, key); ++ } ++ return; ++ } ++ lock (data) { ++ data.Remove(key); ++ } ++ } ++ ++ // null: no dictionary yet (and create is false), or one that cannot be written to ++ private static System.Collections.IDictionary GetDictionary(Exception e, bool create) { ++#if NET8_0_OR_GREATER ++ if (_dataFieldAvailable) { ++ try { ++ ref System.Collections.IDictionary field = ref DataField(e); ++ var dictionary = field; ++ if (dictionary is null) { ++ if (!create) { ++ return null; ++ } ++ Interlocked.CompareExchange(ref field, new System.Collections.Specialized.ListDictionary(), null); ++ dictionary = field; ++ } ++ return dictionary.IsReadOnly ? null : dictionary; ++ } catch (MissingFieldException) { ++ _dataFieldAvailable = false; ++ } ++ } ++#endif ++ var data = e.Data; ++ return (data is null || data.IsReadOnly) ? null : data; ++ } ++ ++#if NET8_0_OR_GREATER ++ [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_data")] ++ private static extern ref System.Collections.IDictionary DataField(Exception e); ++ ++ private static bool _dataFieldAvailable = true; ++#endif ++ ++ private static volatile bool _hasTableData; ++ ++ private static ConditionalWeakTable>> _exceptionData; ++ ++ private static void SetTableData(Exception e, object key, object value) { ++ _hasTableData = true; ++ if (_exceptionData is null) { ++ Interlocked.CompareExchange(ref _exceptionData, new ConditionalWeakTable>>(), null); ++ } ++ ++ lock (_exceptionData) { ++ var data = _exceptionData.GetOrCreateValue(e); ++ int index = data.FindIndex(entry => entry.Key == key); ++ if (index >= 0) { ++ data[index] = new KeyValuePair(key, value); ++ } else { ++ data.Add(new KeyValuePair(key, value)); ++ } ++ } ++ } ++ ++ private static object GetTableData(Exception e, object key) { ++ if (_exceptionData is null) { ++ return null; ++ } ++ ++ lock (_exceptionData) { ++ if (!_exceptionData.TryGetValue(e, out List> data)) { ++ return null; ++ } ++ return data.FirstOrDefault(entry => entry.Key == key).Value; ++ } ++ } ++ ++ private static void RemoveTableData(Exception e, object key) { ++ if (_exceptionData is null) { ++ return; ++ } ++ ++ lock (_exceptionData) { ++ if (!_exceptionData.TryGetValue(e, out List> data)) { ++ return; ++ } ++ int index = data.FindIndex(entry => entry.Key == key); ++ if (index >= 0) { ++ data.RemoveAt(index); ++ } ++ } ++ } + #else + private static ConditionalWeakTable>> _exceptionData; + diff --git a/Src/Libraries/Builtins/ArrayOps.cs b/Src/Libraries/Builtins/ArrayOps.cs index 7efde1c1e..04d078145 100644 --- a/Src/Libraries/Builtins/ArrayOps.cs +++ b/Src/Libraries/Builtins/ArrayOps.cs @@ -316,7 +316,9 @@ internal static RubyArray SortInPlace(ComparisonStorage/*!*/ comparisonStorage, var context = comparisonStorage.Context; if (block == null) { - MergeSort(self, (x, y) => Protocols.Compare(comparisonStorage, x, y)); + if (!TrySortWithoutDispatch(context, self)) { + MergeSort(self, (x, y) => Protocols.Compare(comparisonStorage, x, y)); + } } else { object nonRefBreakResult = null; try { @@ -343,6 +345,146 @@ internal static RubyArray SortInPlace(ComparisonStorage/*!*/ comparisonStorage, return self; } + /// + /// sort without a block over an array of only Strings, only Fixnums or only Floats (no NaN) - the + /// common case - compares the elements directly instead of dispatching #<=> for every pair, as + /// long as the class's #<=> is still the built-in one and no element is an instance of a + /// subclass or has a singleton class. The comparisons are the ones those #<=> make, so the + /// order is the same; anything else goes the general way. + /// + private static bool TrySortWithoutDispatch(RubyContext/*!*/ context, RubyArray/*!*/ self) { + int count = self.Count; + // (#<=> called from a C# sort ignores refinements, as rb_funcall does in MRI's sort) + if (count < 2) { + return false; + } + + object first = self[0]; + if (first != null && first.GetType() == typeof(MutableString)) { + var stringClass = context.GetClass(typeof(MutableString)); + var keys = new StringKey[count]; + for (int i = 0; i < count; i++) { + object item = self[i]; + if (item == null || item.GetType() != typeof(MutableString) || context.GetImmediateClassOf(item) != stringClass) { + return false; + } + keys[i] = new StringKey((MutableString)item); + } + if (!HasBuiltinComparison(stringClass)) { + return false; + } + SortByKeys(self, keys); + return true; + } + + if (first is int) { + var keys = new int[count]; + for (int i = 0; i < count; i++) { + object item = self[i]; + if (!(item is int)) { + return false; + } + keys[i] = (int)item; + } + if (!HasBuiltinComparison(context.GetClassOf(first))) { + return false; + } + SortByKeys(self, keys); + return true; + } + + if (first is double) { + var keys = new double[count]; + for (int i = 0; i < count; i++) { + object item = self[i]; + if (!(item is double) || Double.IsNaN((double)item)) { + return false; + } + keys[i] = (double)item; + } + if (!HasBuiltinComparison(context.GetClassOf(first))) { + return false; + } + SortByKeys(self, keys); + return true; + } + + return false; + } + + // A struct, so that the generic sort below is compiled for it and calls MutableString.CompareTo + // directly; over the class itself it would share one body and dispatch every comparison. + private struct StringKey : IComparable { + private readonly MutableString/*!*/ _value; + public StringKey(MutableString/*!*/ value) { _value = value; } + public int CompareTo(StringKey other) { return _value.CompareTo(other._value); } + } + + // The same stable merge sort as MergeSort, over keys of one type compared by their own CompareTo + // (what String#<=>, Integer#<=> and Float#<=> do for two of them), carrying the elements along. + private static void SortByKeys(RubyArray/*!*/ self, TKey[]/*!*/ keys) where TKey : IComparable { + int count = keys.Length; + object[] items = new object[count]; + self.CopyTo(items, 0); + MergeSortKeys(keys, items, new TKey[count], new object[count], 0, count); + for (int i = 0; i < count; i++) { + self[i] = items[i]; + } + } + + private static void MergeSortKeys(TKey[]/*!*/ keys, object[]/*!*/ items, TKey[]/*!*/ keyBuffer, object[]/*!*/ itemBuffer, + int start, int length) where TKey : IComparable { + + if (length <= 12) { + // insertion sort, stable: an element moves left only past strictly greater ones + for (int i = start + 1; i < start + length; i++) { + TKey key = keys[i]; + object item = items[i]; + int j = i - 1; + while (j >= start && key.CompareTo(keys[j]) < 0) { + keys[j + 1] = keys[j]; + items[j + 1] = items[j]; + j--; + } + keys[j + 1] = key; + items[j + 1] = item; + } + return; + } + + int half = length / 2; + MergeSortKeys(keys, items, keyBuffer, itemBuffer, start, half); + MergeSortKeys(keys, items, keyBuffer, itemBuffer, start + half, length - half); + + int left = start, right = start + half, target = start; + int leftEnd = start + half, rightEnd = start + length; + while (left < leftEnd && right < rightEnd) { + if (keys[right].CompareTo(keys[left]) < 0) { + keyBuffer[target] = keys[right]; + itemBuffer[target++] = items[right++]; + } else { + keyBuffer[target] = keys[left]; + itemBuffer[target++] = items[left++]; + } + } + while (left < leftEnd) { + keyBuffer[target] = keys[left]; + itemBuffer[target++] = items[left++]; + } + while (right < rightEnd) { + keyBuffer[target] = keys[right]; + itemBuffer[target++] = items[right++]; + } + + Array.Copy(keyBuffer, start, keys, start, length); + Array.Copy(itemBuffer, start, items, start, length); + } + + private static bool HasBuiltinComparison(RubyClass/*!*/ cls) { + var method = cls.ResolveMethod("<=>", VisibilityContext.AllVisible); + return method.Found && method.Info is RubyLibraryMethodInfo; + } + /// /// List<T>.Sort cannot be used here: it wraps whatever the comparer throws in an /// InvalidOperationException (so a Ruby exception or a block's break never arrives diff --git a/Src/Libraries/Builtins/BasicObjectOps.cs b/Src/Libraries/Builtins/BasicObjectOps.cs index 2b2035442..55cc316a0 100644 --- a/Src/Libraries/Builtins/BasicObjectOps.cs +++ b/Src/Libraries/Builtins/BasicObjectOps.cs @@ -14,6 +14,7 @@ * ***************************************************************************/ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Scripting.Runtime; using Microsoft.Scripting.Utils; @@ -68,70 +69,80 @@ public static object SendMessage(RubyScope/*!*/ scope, object self) { // ARGS: 0 [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { - return KernelOps.SendMessage(scope, self, methodName); + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { + return KernelOps.SendMessage(storage, scope, self, methodName); } // ARGS: 0& [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { - return KernelOps.SendMessage(scope, block, self, methodName); + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { + return KernelOps.SendMessage(storage, scope, block, self, methodName); } // ARGS: 1 [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1) { - return KernelOps.SendMessage(scope, self, methodName, arg1); + return KernelOps.SendMessage(storage, scope, self, methodName, arg1); } // ARGS: 1& [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1) { - return KernelOps.SendMessage(scope, block, self, methodName, arg1); + return KernelOps.SendMessage(storage, scope, block, self, methodName, arg1); } // ARGS: 2 [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2) { - return KernelOps.SendMessage(scope, self, methodName, arg1, arg2); + return KernelOps.SendMessage(storage, scope, self, methodName, arg1, arg2); } // ARGS: 2& [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2) { - return KernelOps.SendMessage(scope, block, self, methodName, arg1, arg2); + return KernelOps.SendMessage(storage, scope, block, self, methodName, arg1, arg2); } // ARGS: 3 [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2, object arg3) { - return KernelOps.SendMessage(scope, self, methodName, arg1, arg2, arg3); + return KernelOps.SendMessage(storage, scope, self, methodName, arg1, arg2, arg3); } // ARGS: 3& [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2, object arg3) { - return KernelOps.SendMessage(scope, block, self, methodName, arg1, arg2, arg3); + return KernelOps.SendMessage(storage, scope, block, self, methodName, arg1, arg2, arg3); } // ARGS: N [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, params object[]/*!*/ args) { - return KernelOps.SendMessage(scope, self, methodName, args); + return KernelOps.SendMessage(storage, scope, self, methodName, args); } // ARGS: N& [RubyMethod("__send__")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, params object[]/*!*/ args) { - return KernelOps.SendMessage(scope, block, self, methodName, args); + return KernelOps.SendMessage(storage, scope, block, self, methodName, args); } #endregion diff --git a/Src/Libraries/Builtins/Enumerable.cs b/Src/Libraries/Builtins/Enumerable.cs index 1809a0b0a..12abde155 100644 --- a/Src/Libraries/Builtins/Enumerable.cs +++ b/Src/Libraries/Builtins/Enumerable.cs @@ -111,6 +111,26 @@ public static object TrueForAny(CallSiteStorage/*!*/ each, BlockParam } private static object TrueForItems(CallSiteStorage/*!*/ each, BlockParam predicate, object self, bool stop, bool positiveResult) { + // An Array's #all?, #any? and #none? are its own in MRI (rb_ary_all_p etc.): they walk the + // array and yield each element once rather than going through #each and a block of ours. + var array = self as RubyArray; + if (array != null) { + for (int i = 0; i < array.Count; i++) { + object item = array[i]; + if (predicate != null) { + object blockResult; + if (predicate.Yield(item, out blockResult)) { + return blockResult; + } + item = blockResult; + } + if (Protocols.IsTrue(item) == stop) { + return ScriptingRuntimeHelpers.BooleanToObject(positiveResult); + } + } + return ScriptingRuntimeHelpers.BooleanToObject(!positiveResult); + } + object result = ScriptingRuntimeHelpers.BooleanToObject(!positiveResult); Each(each, self, SplattingBlock(each.Context, delegate(BlockParam/*!*/ selfBlock, RubyArray/*!*/ args) { object item = PackValues(args); diff --git a/Src/Libraries/Builtins/Enumerator.cs b/Src/Libraries/Builtins/Enumerator.cs index cd148878a..24bc0afb8 100644 --- a/Src/Libraries/Builtins/Enumerator.cs +++ b/Src/Libraries/Builtins/Enumerator.cs @@ -201,6 +201,100 @@ public static object GetSizeInfo(RubyContext/*!*/ context, Enumerator/*!*/ self) return result; } + #region external iteration over an Array + + /// + /// Where Enumerator#next is in an Array it walks without a fiber (see GetArrayCursor). + /// Held in an instance variable, so that a #dup shares it the way it would share the fiber. + /// + public sealed class ArrayCursor { + internal readonly RubyArray/*!*/ Array; + internal readonly System.Threading.Thread/*!*/ Thread; + internal int Index; + + internal ArrayCursor(RubyArray/*!*/ array, System.Threading.Thread/*!*/ thread) { + Array = array; + Thread = thread; + } + } + + /// + /// A cursor for an enumerator whose iteration is nothing but the built-in Array#each over an + /// Array (`ary.each`, `ary.to_enum`), or nil. Enumerator#next then steps through the Array by + /// index instead of suspending #each in a fiber - a thread of its own here. The two cannot be + /// told apart: Array#each runs no Ruby code between elements and re-reads the length before + /// each one, which is exactly what the cursor does, so an Array that grows or shrinks half + /// way through behaves the same, and #each's result (the Array) is StopIteration#result. + /// + [RubyMethod("__ir_array_cursor__", RubyMethodAttributes.PrivateInstance)] + public static ArrayCursor GetArrayCursor(RubyContext/*!*/ context, Enumerator/*!*/ self) { + var wrapper = self._impl as Wrapper; + RubyArray array; + if (wrapper == null || wrapper.TargetName != "each" || wrapper.TargetArguments.Length != 0 || + (array = wrapper.TargetObject as RubyArray) == null) { + return null; + } + + object generator; + if (context.TryGetInstanceVariable(self, "@generator", out generator) && generator != null) { + return null; + } + + // Neither the enumerator's #each nor the Array's may have been replaced (redefined, + // aliased, overridden in a subclass or a singleton, prepended to, or removed). + if (!IsBuiltinMethod(context, self, "each", typeof(Enumerator), "EachFromCaller") || + !IsBuiltinMethod(context, array, "each", typeof(IListOps), "Each")) { + return null; + } + + return new ArrayCursor(array, RubyUtils.CurrentRubyThread); + } + + private static bool IsBuiltinMethod(RubyContext/*!*/ context, object target, string/*!*/ name, Type/*!*/ declaringType, string/*!*/ clrName) { + var method = context.ResolveMethod(target, name, VisibilityContext.AllVisible); + if (!method.Found || !(method.Info is RubyLibraryMethodInfo)) { + return false; + } + var members = method.Info.GetMembers(); + if (members.Length == 0) { + return false; + } + foreach (var member in members) { + if (member.DeclaringType != declaringType || member.Name != clrName) { + return false; + } + } + return true; + } + + /// + /// The next element as a one-element Array (what the fiber would have yielded), nil at the + /// end, false when called from a thread other than the one that started the iteration - + /// where the fiber would raise FiberError. + /// + [RubyMethod("__ir_array_cursor_next__", RubyMethodAttributes.PrivateInstance)] + public static object ArrayCursorNext(Enumerator/*!*/ self, [NotNull]ArrayCursor/*!*/ cursor) { + if (cursor.Thread != RubyUtils.CurrentRubyThread) { + return ScriptingRuntimeHelpers.False; + } + RubyArray array = cursor.Array; + int index = cursor.Index; + if (index < array.Count) { + cursor.Index = index + 1; + RubyArray values = new RubyArray(1); + values.Add(array[index]); + return values; + } + return null; + } + + [RubyMethod("__ir_array_cursor_source__", RubyMethodAttributes.PrivateInstance)] + public static RubyArray/*!*/ ArrayCursorSource(Enumerator/*!*/ self, [NotNull]ArrayCursor/*!*/ cursor) { + return cursor.Array; + } + + #endregion + // TODO: 1.9: // :each_with_index, :each_with_object, :with_index, :with_object, :next, :rewind } diff --git a/Src/Libraries/Builtins/KernelOps.cs b/Src/Libraries/Builtins/KernelOps.cs index 247c38810..3daf54fed 100644 --- a/Src/Libraries/Builtins/KernelOps.cs +++ b/Src/Libraries/Builtins/KernelOps.cs @@ -641,7 +641,7 @@ public static void RaiseException(RespondToStorage/*!*/ respondToStorage, UnaryO // An exception raised before whose backtrace was since set to nil gets a new one, as in // MRI. The interpreter keeps the frames of the first throw on the exception and would // hand those back again. - if (RubyExceptionData.GetInstance(exception).Backtrace == null) { + if (!RubyExceptionData.GetInstance(exception).HasBacktrace) { exception.RemoveData(typeof(Microsoft.Scripting.Interpreter.InterpretedFrameInfo)); } @@ -721,7 +721,7 @@ public static MutableString GetSourceFileLocation(RubyContext/*!*/ context, obje // An exception with no backtrace - new, or cleared by set_backtrace(nil) - gets one from // where it is raised now. The interpreter keeps the frames of the first throw on the // exception and would otherwise report those again. - if (RubyExceptionData.GetInstance(exception).Backtrace == null) { + if (!RubyExceptionData.GetInstance(exception).HasBacktrace) { exception.SetData(typeof(Microsoft.Scripting.Interpreter.InterpretedFrameInfo), null); } return exception; @@ -743,7 +743,10 @@ public static MutableString GetSourceFileLocation(RubyContext/*!*/ context, obje return RubyExceptionData.InitializeException(new RuntimeError(message.ToClrString()), message); } - if (!Protocols.RespondTo(respondToStorage, obj, "exception")) { + // the symbol comes from the context's identifier cache: interning it anew was most of + // what this check cost + var respondTo = respondToStorage.GetCallSite(); + if (!RubyOps.IsTrue(respondTo.Target(respondTo, obj, respondToStorage.Context.StringifyIdentifier("exception")))) { throw RubyExceptions.CreateTypeError("exception class/object expected"); } @@ -1453,22 +1456,31 @@ public static MutableString GetAutoloadedConstantPath(RubyScope/*!*/ scope, obje // This method is a binder intrinsic and the behavior of the binder needs to be adjusted appropriately if changed. [RubyMethod("respond_to?")] - public static bool RespondTo(CallSiteStorage>/*!*/ respondToMissingStorage, + public static bool RespondTo(RespondToCacheStorage/*!*/ respondToMissingStorage, RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, [Optional]bool includePrivate) { var context = scope.RubyContext; RubyClass cls = context.GetImmediateClassOf(self); // A refinement active where #respond_to? was called counts, as it does for a call. Without - // one - nearly always - the answer comes from the class's lock-free method-lookup cache. + // one - nearly always - the answer comes from the class's lock-free method-lookup cache, + // and the site keeps it while the class's method version stays the same. + int version = -1; if (scope.GetActiveRefinements().IsEmpty) { - var lookup = cls.GetMethodLookup(methodName); + bool answer; + if (respondToMissingStorage.TryGetAnswer(cls, methodName, includePrivate, out answer)) { + return answer; + } + + var lookup = cls.GetMethodLookup(methodName, out version); // Without include_private only *public* methods count; MRI has answered false for // protected ones since 2.0. The visibility is the one at the name's entry point, so // `public :m' re-exporting an inherited private m counts. if (lookup.Found && (includePrivate || lookup.Visibility == RubyMethodVisibility.Public)) { - return IsImplemented(lookup.Info); + answer = IsImplemented(lookup.Info); + respondToMissingStorage.SetAnswer(cls, version, methodName, includePrivate, answer); + return answer; } } else { var visibility = includePrivate @@ -1490,8 +1502,12 @@ public static bool RespondTo(CallSiteStorage/*!*/ stringCast, RubyScope/*!*/ scope, BlockParam block, object self, - object name, params object[]/*!*/ args) { + public static object PublicSendMessage(ConversionStorage/*!*/ stringCast, + SendSiteStorage>/*!*/ sendStorage, + RubyScope/*!*/ scope, BlockParam block, object self, object name, params object[]/*!*/ args) { string methodName = Protocols.CastToSymbol(stringCast, name); - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(1, - RubyCallFlags.HasScope | RubyCallFlags.HasSplattedArgument | RubyCallFlags.HasBlock | RubyCallFlags.IsInteropCall) + var site = GetSendSite(sendStorage, scope, methodName, new RubyCallSignature(1, + RubyCallFlags.HasScope | RubyCallFlags.HasSplattedArgument | RubyCallFlags.HasBlock | RubyCallFlags.IsInteropCall) ); return site.Target(site, scope, self, block != null ? block.Proc : null, RubyOps.MakeArrayN(args)); } // ARGS: 0 [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(0, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(0, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) ); return site.Target(site, scope, self); } // ARGS: 0& [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(0, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName) { + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(0, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) ); return site.Target(site, scope, self, block != null ? block.Proc : null); } // ARGS: 1 [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) ); return site.Target(site, scope, self, arg1); @@ -1602,22 +1621,24 @@ public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProt // ARGS: 1& [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) ); return site.Target(site, scope, self, block != null ? block.Proc : null, arg1); } // ARGS: 2 [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(2, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(2, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) ); return site.Target(site, scope, self, arg1, arg2); @@ -1625,22 +1646,24 @@ public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProt // ARGS: 2& [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(2, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(2, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) ); return site.Target(site, scope, self, block != null ? block.Proc : null, arg1, arg2); } // ARGS: 3 [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2, object arg3) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(3, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(3, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf) ); return site.Target(site, scope, self, arg1, arg2, arg3); @@ -1648,22 +1671,24 @@ public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProt // ARGS: 3& [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, object arg1, object arg2, object arg3) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(3, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(3, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasBlock) ); return site.Target(site, scope, self, block != null ? block.Proc : null, arg1, arg2, arg3); } // ARGS: N [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, params object[]/*!*/ args) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasSplattedArgument) + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasSplattedArgument) ); return site.Target(site, scope, self, RubyOps.MakeArrayN(args)); @@ -1671,23 +1696,39 @@ public static object SendMessage(RubyScope/*!*/ scope, object self, [DefaultProt // ARGS: N& [RubyMethod("send")] - public static object SendMessage(RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, + public static object SendMessage(SendSiteStorage>/*!*/ storage, + RubyScope/*!*/ scope, BlockParam block, object self, [DefaultProtocol, NotNull]string/*!*/ methodName, params object[]/*!*/ args) { - var site = scope.RubyContext.GetOrCreateSendSite>( - methodName, new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasSplattedArgument | + var site = GetSendSite(storage, scope, methodName, + new RubyCallSignature(1, RubyCallFlags.HasScope | RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasSplattedArgument | RubyCallFlags.HasBlock) ); return site.Target(site, scope, self, block != null ? block.Proc : null, RubyOps.MakeArrayN(args)); } + // The site a send dispatches through: the send site's own (its storage), or - for a send made + // from library code, which has no site of its own - the context's shared one. + private static CallSite/*!*/ GetSendSite(SendSiteStorage storage, RubyScope/*!*/ scope, + string/*!*/ methodName, RubyCallSignature signature) where TSiteFunc : class { + + return (storage != null) + ? storage.GetCallSite(methodName, signature) + : scope.RubyContext.GetOrCreateSendSite(methodName, signature); + } + internal static object SendMessageOpt(RubyScope/*!*/ scope, BlockParam block, object self, string/*!*/ methodName, object[] args) { switch ((args != null ? args.Length : 0)) { - case 0: return SendMessage(scope, block, self, methodName); - case 1: return SendMessage(scope, block, self, methodName, args[0]); - case 2: return SendMessage(scope, block, self, methodName, args[0], args[1]); - case 3: return SendMessage(scope, block, self, methodName, args[0], args[1], args[2]); - default: return SendMessage(scope, block, self, methodName, args); + case 0: return SendMessage((SendSiteStorage>)null, + scope, block, self, methodName); + case 1: return SendMessage((SendSiteStorage>)null, + scope, block, self, methodName, args[0]); + case 2: return SendMessage((SendSiteStorage>)null, + scope, block, self, methodName, args[0], args[1]); + case 3: return SendMessage((SendSiteStorage>)null, + scope, block, self, methodName, args[0], args[1], args[2]); + default: return SendMessage((SendSiteStorage>)null, + scope, block, self, methodName, args); } } diff --git a/Src/Libraries/Builtins/MutableStringOps.cs b/Src/Libraries/Builtins/MutableStringOps.cs index 1dd5aaeeb..df5d2dd3d 100644 --- a/Src/Libraries/Builtins/MutableStringOps.cs +++ b/Src/Libraries/Builtins/MutableStringOps.cs @@ -3100,12 +3100,14 @@ private static Union, MutableString> ToReplacement( [RubyMethod("gsub")] public static object ReplaceAll(ConversionStorage/*!*/ tosConversion, RubyScope/*!*/ scope, MutableString/*!*/ self, [DefaultProtocol, NotNull]RubyRegex/*!*/ pattern) { + scope.MarkCaptured(); return new Enumerator((_, block) => BlockReplaceAll(tosConversion, scope, block, self, pattern)); } [RubyMethod("gsub!")] public static object ReplaceAllInPlace(ConversionStorage/*!*/ tosConversion, RubyScope/*!*/ scope, MutableString/*!*/ self, [DefaultProtocol, NotNull]RubyRegex/*!*/ pattern) { + scope.MarkCaptured(); return new Enumerator((_, block) => BlockReplaceInPlace(tosConversion, scope, block, self, pattern, true)); } diff --git a/Src/Libraries/Builtins/NilClassOps.cs b/Src/Libraries/Builtins/NilClassOps.cs index 1d955bf24..d2fc065b7 100644 --- a/Src/Libraries/Builtins/NilClassOps.cs +++ b/Src/Libraries/Builtins/NilClassOps.cs @@ -99,7 +99,8 @@ public static MutableString Inspect(object self) { /// One frozen string, answered again every time: MRI's nil.to_s is a single object, and /// a spec checks that two calls hand back the very same one. /// - private static readonly MutableString/*!*/ _EmptyString = MutableString.CreateEmpty().Freeze(); + // US-ASCII, as MRI's nil.to_s is + private static readonly MutableString/*!*/ _EmptyString = MutableString.Create(String.Empty, RubyEncoding.Ascii).Freeze(); [RubyMethodAttribute("to_s")] public static MutableString/*!*/ ToString(object self) { diff --git a/Src/Libraries/Builtins/RubyMath.cs b/Src/Libraries/Builtins/RubyMath.cs index 4b3987a82..f3cdc06bf 100644 --- a/Src/Libraries/Builtins/RubyMath.cs +++ b/Src/Libraries/Builtins/RubyMath.cs @@ -13,6 +13,9 @@ * * ***************************************************************************/ +using System; +using System.Runtime.CompilerServices; +using IronRuby.Runtime.Calls; using Microsoft.Scripting.Utils; using IronRuby.Runtime; using SM = System.Math; @@ -37,6 +40,28 @@ private static double DomainCheck(double result, string/*!*/ functionName) { return result; } + // The fast path of the functions ruby4.rb used to define in Ruby: a Float or a Fixnum argument, + // in the function's domain. Anything else - a Bignum, a Rational, nil, a String, an object with + // #to_f - and every domain error go to the Ruby definitions (__ir___ in ruby4.rb), which do + // MRI's rb_to_float conversion and raise its errors. + private static bool TryGetDouble(object x, out double d) { + if (x is double) { d = (double)x; return true; } + if (x is int) { d = (int)x; return true; } + if (x is long) { d = (long)x; return true; } + d = 0.0; + return false; + } + + private static object General(CallSiteStorage>/*!*/ general, string/*!*/ name, object self, object x) { + var site = general.GetCallSite(name, new RubyCallSignature(1, RubyCallFlags.HasImplicitSelf)); + return site.Target(site, self, x); + } + + private static object General(CallSiteStorage>/*!*/ general, string/*!*/ name, object self, object x, object y) { + var site = general.GetCallSite(name, new RubyCallSignature(2, RubyCallFlags.HasImplicitSelf)); + return site.Target(site, self, x, y); + } + private static ushort Exponent(byte[] v) { return (ushort)((((ushort)(v[7] & 0x7F)) << (ushort)4) | (((ushort)(v[6] & 0xF0)) >> 4)); } @@ -54,8 +79,13 @@ private static ulong Mantissa(byte[] v) { [RubyMethodAttribute("acos", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("acos", RubyMethodAttributes.PublicSingleton)] - public static double Acos(object self, [DefaultProtocol]double x) { - return DomainCheck(SM.Acos(x), "acos"); + public static object Acos(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + if (TryGetDouble(x, out d)) { + if (double.IsNaN(d)) return d; + if (d >= -1.0 && d <= 1.0) return SM.Acos(d); + } + return General(general, "__ir_acos__", self, x); } [RubyMethodAttribute("acosh", RubyMethodAttributes.PrivateInstance)] @@ -67,27 +97,39 @@ public static double Acosh(object self, [DefaultProtocol]double x) { [RubyMethodAttribute("asin", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("asin", RubyMethodAttributes.PublicSingleton)] - public static double Asin(object self, [DefaultProtocol]double x) { - return DomainCheck(SM.Asin(x), "asin"); + public static object Asin(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + if (TryGetDouble(x, out d)) { + if (double.IsNaN(d)) return d; + if (d >= -1.0 && d <= 1.0) return SM.Asin(d); + } + return General(general, "__ir_asin__", self, x); } [RubyMethodAttribute("asinh", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("asinh", RubyMethodAttributes.PublicSingleton)] - public static double Asinh(object self, [DefaultProtocol]double x) { - //ln(x + sqrt(x*x + 1)) - return SM.Log(x + SM.Sqrt(x * x + 1)); + public static object Asinh(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Asinh(d) : General(general, "__ir_asinh__", self, x); } [RubyMethodAttribute("atan", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("atan", RubyMethodAttributes.PublicSingleton)] - public static double Atan(object self, [DefaultProtocol]double x) { - return SM.Atan(x); + public static object Atan(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Atan(d) : General(general, "__ir_atan__", self, x); } [RubyMethodAttribute("atan2", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("atan2", RubyMethodAttributes.PublicSingleton)] - public static double Atan2(object self, [DefaultProtocol]double y, [DefaultProtocol]double x) { - return SM.Atan2(y, x); + public static object Atan2(CallSiteStorage>/*!*/ general, object self, object y, object x) { + double dy, dx; + if (TryGetDouble(y, out dy) && TryGetDouble(x, out dx)) { + if (double.IsNaN(dy)) return dy; + if (double.IsNaN(dx)) return dx; + return SM.Atan2(dy, dx); + } + return General(general, "__ir_atan2__", self, y, x); } [RubyMethodAttribute("atanh", RubyMethodAttributes.PrivateInstance)] @@ -99,20 +141,23 @@ public static double Atanh(object self, [DefaultProtocol]double x) { [RubyMethodAttribute("cos", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("cos", RubyMethodAttributes.PublicSingleton)] - public static double Cos(object self, [DefaultProtocol]double x) { - return SM.Cos(x); + public static object Cos(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Cos(d) : General(general, "__ir_cos__", self, x); } [RubyMethodAttribute("cosh", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("cosh", RubyMethodAttributes.PublicSingleton)] - public static double Cosh(object self, [DefaultProtocol]double x) { - return SM.Cosh(x); + public static object Cosh(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Cosh(d) : General(general, "__ir_cosh__", self, x); } [RubyMethodAttribute("cbrt", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("cbrt", RubyMethodAttributes.PublicSingleton)] - public static double CubeRoot(object self, [DefaultProtocol]double x) { - return System.Math.Pow(x, 1.0 / 3.0); + public static object CubeRoot(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Cbrt(d) : General(general, "__ir_cbrt__", self, x); } [RubyMethodAttribute("erf", RubyMethodAttributes.PrivateInstance)] @@ -129,8 +174,9 @@ public static double Erfc(object self, [DefaultProtocol]double x) { [RubyMethodAttribute("exp", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("exp", RubyMethodAttributes.PublicSingleton)] - public static double Exp(object self, [DefaultProtocol]double x) { - return SM.Exp(x); + public static object Exp(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Exp(d) : General(general, "__ir_exp__", self, x); } [RubyMethodAttribute("gamma", RubyMethodAttributes.PrivateInstance)] @@ -141,8 +187,21 @@ public static double Gamma(object self, [DefaultProtocol]double x) { [RubyMethodAttribute("hypot", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("hypot", RubyMethodAttributes.PublicSingleton)] - public static double Hypot(object self, [DefaultProtocol]double x, [DefaultProtocol]double y) { - return DomainCheck(SM.Sqrt(x*x+y*y), "hypot"); + public static object Hypot(CallSiteStorage>/*!*/ general, object self, object x, object y) { + double dx, dy; + if (!TryGetDouble(x, out dx) || !TryGetDouble(y, out dy)) { + return General(general, "__ir_hypot__", self, x, y); + } + dx = SM.Abs(dx); + dy = SM.Abs(dy); + // an infinity wins over a NaN, which is what C's hypot() promises + if (double.IsInfinity(dx) || double.IsInfinity(dy)) return double.PositiveInfinity; + if (double.IsNaN(dx) || double.IsNaN(dy)) return double.NaN; + if (dx < dy) { double t = dx; dx = dy; dy = t; } + if (dy == 0.0) return dx; + // scaling by the larger operand keeps x*x from overflowing for x ~ 1e300 + double r = dy / dx; + return dx * SM.Sqrt(1.0 + r * r); } [RubyMethodAttribute("frexp", RubyMethodAttributes.PrivateInstance)] @@ -194,32 +253,42 @@ public static double Log2(object self, [DefaultProtocol]double x) { [RubyMethodAttribute("sin", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("sin", RubyMethodAttributes.PublicSingleton)] - public static double Sin(object self, [DefaultProtocol]double x) { - return SM.Sin(x); + public static object Sin(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Sin(d) : General(general, "__ir_sin__", self, x); } [RubyMethodAttribute("sinh", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("sinh", RubyMethodAttributes.PublicSingleton)] - public static double Sinh(object self, [DefaultProtocol]double x) { - return SM.Sinh(x); + public static object Sinh(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Sinh(d) : General(general, "__ir_sinh__", self, x); } [RubyMethodAttribute("sqrt", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("sqrt", RubyMethodAttributes.PublicSingleton)] - public static double Sqrt(object self, [DefaultProtocol]double x) { - return DomainCheck(SM.Sqrt(x), "sqrt"); + public static object Sqrt(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + if (TryGetDouble(x, out d)) { + if (d > 0.0) return SM.Sqrt(d); + if (d == 0.0) return 0.0; // sqrt(-0.0) is -0.0 in IEEE but 0.0 in MRI + if (double.IsNaN(d)) return d; + } + return General(general, "__ir_sqrt__", self, x); } [RubyMethodAttribute("tan", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("tan", RubyMethodAttributes.PublicSingleton)] - public static double Tan(object self, [DefaultProtocol]double x) { - return SM.Tan(x); + public static object Tan(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Tan(d) : General(general, "__ir_tan__", self, x); } [RubyMethodAttribute("tanh", RubyMethodAttributes.PrivateInstance)] [RubyMethodAttribute("tanh", RubyMethodAttributes.PublicSingleton)] - public static double Tanh(object self, [DefaultProtocol]double x) { - return SM.Tanh(x); + public static object Tanh(CallSiteStorage>/*!*/ general, object self, object x) { + double d; + return TryGetDouble(x, out d) ? (object)SM.Tanh(d) : General(general, "__ir_tanh__", self, x); } #endregion diff --git a/Src/Libraries/Builtins/StructOps.cs b/Src/Libraries/Builtins/StructOps.cs index 30e2612e1..5837d4545 100644 --- a/Src/Libraries/Builtins/StructOps.cs +++ b/Src/Libraries/Builtins/StructOps.cs @@ -26,6 +26,7 @@ using Microsoft.Scripting.Runtime; using Microsoft.Scripting.Utils; using IronRuby.Runtime; +using IronRuby.Runtime.Calls; using System.Runtime.CompilerServices; namespace IronRuby.Builtins { @@ -93,8 +94,27 @@ private static object Create(BlockParam block, RubyClass/*!*/ self, string class return result; } - // Reinitialization. Called only from derived struct's initializer. + // Struct#initialize. The overwhelmingly common call - one positional value per member, the + // last of them not a Hash that could be keywords, on a class not built with keyword_init: + // true - stores the values right here, which is what keeps Point.new(1, 2) cheap. Every + // other call goes to Struct#__struct_initialize_slow__ in ruby4.rb with the arguments as + // one array, as a `def initialize(*args)' would have received them. [RubyMethod("initialize", RubyMethodAttributes.PrivateInstance)] + public static object Initialize(CallSiteStorage>/*!*/ slowPath, + RubyStruct/*!*/ self, params object[]/*!*/ items) { + + int count = self.ItemCount; + if (items.Length == count && (count == 0 || !(items[count - 1] is IronRuby.Builtins.Hash)) && !self.IsKeywordInit) { + self.SetValues(items); + return null; + } + + var site = slowPath.GetCallSite("__struct_initialize_slow__", new RubyCallSignature(1, RubyCallFlags.HasImplicitSelf)); + return site.Target(site, self, RubyOps.NormalizeRestArgument(new RubyArray(items), false)); + } + + // The plain store, for __struct_initialize_slow__. + [RubyMethod("__struct_initialize__", RubyMethodAttributes.PrivateInstance)] public static void Reinitialize(RubyStruct/*!*/ self, params object[]/*!*/ items) { self.SetValues(items); } diff --git a/Src/Libraries/Builtins/ThreadOps.Fiber.cs b/Src/Libraries/Builtins/ThreadOps.Fiber.cs new file mode 100644 index 000000000..e72d7fa24 --- /dev/null +++ b/Src/Libraries/Builtins/ThreadOps.Fiber.cs @@ -0,0 +1,367 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ +#if FEATURE_THREAD +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using IronRuby.Runtime; +using Microsoft.Scripting.Runtime; + +namespace IronRuby.Builtins { + + // The primitives the Ruby-level Fiber (Src/StdLib/ironruby/ruby4.rb) switches with. + public static partial class ThreadOps { + + /// + /// The mailbox a fiber waits on while it is not running. Every fiber is a CLR thread of its + /// own and exactly one fiber of a group runs at a time, so a switch is "post to the target's + /// mailbox, then take from my own". + /// + /// Taking spins for a while before it blocks. A resume / Fiber.yield round trip whose other + /// side answers quickly - Enumerator#next over a generator, a fiber that yields in a loop - + /// then costs two cache-line transfers instead of two kernel wake-ups, which is most of what + /// a switch used to cost. A wake-up costs tens of microseconds on a busy machine, so the spin + /// may run about as long before it gives up. Two misses in a row pause spinning for that + /// mailbox - a fiber that is resumed rarely, or threads that cannot run side by side, would + /// only burn a core on it - and every 32nd take tries again. The spin yields the CPU now and + /// then, in case the thread it waits for is queued behind it. Single-core machines never + /// spin. + /// + /// A blocked take is a Monitor wait, so Thread#kill / Thread#raise (delivered with + /// Thread.Interrupt) wake it exactly as they wake Queue#pop, which the mailbox used to be. + /// + public sealed class FiberMailbox { + // How long a take spins before it blocks, and how spinning is paused and probed. + private static readonly long _spinTicks = Stopwatch.Frequency / 10000; // 100us + private const int SpinMissesToPause = 2; + private const int PausedTakesPerProbe = 32; + private static readonly bool _canSpin = Environment.ProcessorCount > 1; + + private readonly object/*!*/ _lock = new object(); + + // Pending messages: practically always none or one, so the first one has a slot of its own. + private object _message; + private Queue _overflow; + private volatile int _count; + + // The receiver is parked in Monitor.Wait (guarded by _lock). + private bool _blocked; + + // Spin misses in a row; takes since spinning was paused (spinning is paused while + // _misses has reached SpinMissesToPause). + private int _misses; + private int _pausedTakes; + + // The fiber group's owner, and its RubyThreadInfo, cached for FiberTake. + private Thread _owner; + private RubyThreadInfo _ownerInfo; + + public void Post(object message) { + lock (_lock) { + int count = _count; + if (count == 0) { + _message = message; + } else { + if (_overflow == null) { + _overflow = new Queue(); + } + _overflow.Enqueue(message); + } + _count = count + 1; + if (_blocked) { + Monitor.Pulse(_lock); + } + } + } + + public object Take() { + if (_count == 0 && _canSpin) { + if (_misses < SpinMissesToPause || ++_pausedTakes >= PausedTakesPerProbe) { + _pausedTakes = 0; + if (Spin()) { + _misses = 0; + } else if (_misses < SpinMissesToPause) { + _misses++; + } + } + } + + while (true) { + bool interrupted = false; + lock (_lock) { + if (_count == 0) { + _blocked = true; + try { + Monitor.Wait(_lock); + } catch (ThreadInterruptedException) { + interrupted = true; + } finally { + _blocked = false; + } + } + + if (!interrupted && _count != 0) { + object message = _message; + int count = _count - 1; + _message = (count > 0) ? _overflow.Dequeue() : null; + _count = count; + return message; + } + } + + if (interrupted) { + // Thread#kill / Thread#raise nudged us: deliver the parked exception (outside the + // lock, it runs Ruby code). If nothing is parked the wait simply goes on. + RubyUtils.TranslateThreadInterrupt(); + } + } + } + + /// + /// Take without spinning, giving up after ; for the pool. + /// + internal bool TryTake(int milliseconds, out object message) { + long deadline = Environment.TickCount64 + milliseconds; + while (true) { + bool interrupted = false; + lock (_lock) { + if (_count == 0) { + int left = (int)(deadline - Environment.TickCount64); + if (left <= 0) { + message = null; + return false; + } + _blocked = true; + try { + Monitor.Wait(_lock, left); + } catch (ThreadInterruptedException) { + interrupted = true; + } finally { + _blocked = false; + } + } + + if (!interrupted && _count != 0) { + message = _message; + int count = _count - 1; + _message = (count > 0) ? _overflow.Dequeue() : null; + _count = count; + return true; + } + } + + if (interrupted) { + RubyUtils.TranslateThreadInterrupt(); + } + } + } + + private bool Spin() { + long start = Stopwatch.GetTimestamp(); + int round = 0; + while (true) { + for (int i = 0; i < 4; i++) { + Thread.SpinWait(2); + if (_count != 0) { + return true; + } + } + if ((++round & 7) == 0) { + Thread.Yield(); + if (_count != 0) { + return true; + } + } + if (Stopwatch.GetTimestamp() - start > _spinTicks) { + return false; + } + } + } + + internal RubyThreadInfo/*!*/ GetOwnerInfo(Thread/*!*/ owner) { + RubyThreadInfo info = _ownerInfo; + if (info == null || !ReferenceEquals(_owner, owner)) { + info = RubyThreadInfo.FromThread(owner); + _owner = owner; + _ownerInfo = info; + } + return info; + } + } + + // Fiber.current of this CLR thread (a fiber never leaves the thread it runs on), for the + // context it belongs to. The same thing is kept in the thread's locals as :__ir_fiber_current__ + // for the C# code that looks there; this copy is what makes Fiber.current cheap. + [ThreadStatic] + private static object _currentFiber; + [ThreadStatic] + private static RubyContext _currentFiberContext; + + #region the pool of fiber threads + + // Starting a CLR thread waits until the OS has scheduled it, which is most of what creating + // a fiber cost. The thread of a fiber that has finished therefore parks here for a while, + // and the next fiber to start runs on it. Before it parks it does for the finished fiber + // what the end of the thread would have done (drop its mutexes and any undelivered + // Thread#raise), and it forgets the fiber's locals, so the next fiber starts as on a new + // thread. A parked thread is left out of Thread.list and ThreadGroup#list. + private sealed class FiberWorker { + internal readonly Thread/*!*/ Thread; + internal readonly FiberMailbox/*!*/ Jobs = new FiberMailbox(); + + internal FiberWorker(Thread/*!*/ thread) { + Thread = thread; + } + } + + private static readonly List/*!*/ _idleFiberWorkers = new List(); + private const int MaxIdleFiberWorkers = 8; + private const int FiberWorkerIdleMilliseconds = 2000; + + [ThreadStatic] + private static FiberWorker _fiberWorker; + + /// + /// Hands to a parked fiber thread and answers that thread, or nil when + /// none is parked (the fiber then gets a new thread). + /// + [RubyMethod("__fiber_reuse__", RubyMethodAttributes.PublicSingleton)] + public static Thread ReuseFiberThread(object self, object fiber) { + FiberWorker worker; + while (true) { + lock (_idleFiberWorkers) { + int last = _idleFiberWorkers.Count - 1; + if (last < 0) { + return null; + } + worker = _idleFiberWorkers[last]; + _idleFiberWorkers.RemoveAt(last); + } + if (worker.Thread.IsAlive) { + break; + } + } + + // The thread now runs a fiber of the current thread's, as a new one would have. + RubyThreadInfo info = RubyThreadInfo.FromThread(worker.Thread); + info.Group = RubyThreadInfo.FromThread(Thread.CurrentThread).Group; + info.IsIdleFiberWorker = false; + worker.Jobs.Post(fiber); + return worker.Thread; + } + + /// + /// Called on a fiber thread whose fiber has finished: cleans up after the fiber and waits + /// for the next one. Nil means the thread should end (the pool is full, nobody came in + /// time, or the program is ending). + /// + [RubyMethod("__fiber_park__", RubyMethodAttributes.PublicSingleton)] + public static object ParkFiberThread(object self) { + Thread current = Thread.CurrentThread; + IronRuby.StandardLibrary.Threading.RubyMutex.ReleaseLocksOf(current); + RubyUtils.GetPendingAsyncException(current); + RubyThreadInfo info = RubyThreadInfo.FromThread(current); + info.ClearLocals(); + _currentFiber = null; + _currentFiberContext = null; + + FiberWorker worker = _fiberWorker ?? (_fiberWorker = new FiberWorker(current)); + lock (_idleFiberWorkers) { + if (_terminating || _idleFiberWorkers.Count >= MaxIdleFiberWorkers) { + return null; + } + info.IsIdleFiberWorker = true; + _idleFiberWorkers.Add(worker); + } + + bool parked = true; + try { + object fiber; + if (worker.Jobs.TryTake(FiberWorkerIdleMilliseconds, out fiber)) { + parked = false; + return fiber; + } + lock (_idleFiberWorkers) { + if (_idleFiberWorkers.Remove(worker)) { + // retired; stays hidden until the thread is gone + parked = false; + return null; + } + } + // ReuseFiberThread took this thread just now: its fiber is on the way + parked = false; + return worker.Jobs.Take(); + } finally { + if (parked) { + // left by an exception (Thread#kill): nobody may hand this thread a fiber now + lock (_idleFiberWorkers) { + _idleFiberWorkers.Remove(worker); + } + } + } + } + + /// + /// Releases the mutexes the fiber running on this thread still holds, as the end of the + /// thread would. Called by a fiber that has finished, before it hands control back: the + /// fiber it hands it to may run (and lock) right away, while this thread is still alive. + /// + [RubyMethod("__fiber_release_locks__", RubyMethodAttributes.PublicSingleton)] + public static object ReleaseFiberLocks(object self) { + IronRuby.StandardLibrary.Threading.RubyMutex.ReleaseLocksOf(Thread.CurrentThread); + return null; + } + + #endregion + + [RubyMethod("__fiber_mailbox__", RubyMethodAttributes.PublicSingleton)] + public static FiberMailbox/*!*/ CreateFiberMailbox(object self) { + return new FiberMailbox(); + } + + [RubyMethod("__fiber_current__", RubyMethodAttributes.PublicSingleton)] + public static object GetCurrentFiber(RubyContext/*!*/ context, object self) { + return ReferenceEquals(_currentFiberContext, context) ? _currentFiber : null; + } + + [RubyMethod("__fiber_current_set__", RubyMethodAttributes.PublicSingleton)] + public static object SetCurrentFiber(RubyContext/*!*/ context, object self, object fiber) { + RubyThreadInfo.FromThread(Thread.CurrentThread)[context.CreateAsciiSymbol("__ir_fiber_current__")] = fiber; + _currentFiber = fiber; + _currentFiberContext = context; + return fiber; + } + + [RubyMethod("__fiber_post__", RubyMethodAttributes.PublicSingleton)] + public static object FiberPost(object self, [NotNull]FiberMailbox/*!*/ mailbox, object message) { + mailbox.Post(message); + return null; + } + + /// + /// Waits for the next message to the fiber running on this thread, then makes this thread + /// the one that runs on behalf of (what __set_fiber_owner__ does). + /// + [RubyMethod("__fiber_take__", RubyMethodAttributes.PublicSingleton)] + public static object FiberTake(object self, [NotNull]FiberMailbox/*!*/ mailbox, [NotNull]Thread/*!*/ owner) { + object message = mailbox.Take(); + RubyUtils.SetFiberOwnerThread(owner); + mailbox.GetOwnerInfo(owner).ActiveFiberThread = Thread.CurrentThread; + return message; + } + } +} +#endif diff --git a/Src/Libraries/Builtins/ThreadOps.cs b/Src/Libraries/Builtins/ThreadOps.cs index 473800598..fe783c5ab 100644 --- a/Src/Libraries/Builtins/ThreadOps.cs +++ b/Src/Libraries/Builtins/ThreadOps.cs @@ -39,7 +39,7 @@ namespace IronRuby.Builtins { /// and this will not be an issue then. /// [RubyClass("Thread", Extends = typeof(Thread), Inherits = typeof(object), BuildConfig = "FEATURE_THREAD")] - public static class ThreadOps { + public static partial class ThreadOps { static bool _globalAbortOnException; static bool _globalReportOnException = true; @@ -137,6 +137,13 @@ internal object this[RubySymbol/*!*/ key] { } } + /// Forgets every fiber-local, for a fiber thread that goes back to the pool. + internal void ClearLocals() { + lock (_threadLocalStorage) { + _threadLocalStorage.Clear(); + } + } + internal bool HasKey(RubySymbol/*!*/ key) { lock (_threadLocalStorage) { return _threadLocalStorage.ContainsKey(key); @@ -188,6 +195,12 @@ internal Thread Thread { /// internal volatile string BlockedLabel; internal volatile Thread ActiveFiberThread; + + /// + /// A fiber thread parked in the pool between two fibers (ThreadOps.Fiber.cs). It is no + /// Ruby thread of anybody's while it waits, so the thread lists leave it out. + /// + internal volatile bool IsIdleFiberWorker; /// /// MRI's Thread#priority is a small integer clamped to -3..3 that outlives the thread; @@ -267,7 +280,7 @@ internal static RubyThreadInfo[] Threads { lock (_mapping) { List result = new List(_mapping.Count); foreach (KeyValuePair entry in _mapping) { - if (entry.Value.Thread.IsAlive) { + if (entry.Value.Thread.IsAlive && !entry.Value.IsIdleFiberWorker) { result.Add(entry.Value); } } diff --git a/Src/Libraries/Extensions/IDictionaryOps.cs b/Src/Libraries/Extensions/IDictionaryOps.cs index 3c5383c78..b88bffb03 100644 --- a/Src/Libraries/Extensions/IDictionaryOps.cs +++ b/Src/Libraries/Extensions/IDictionaryOps.cs @@ -251,13 +251,43 @@ public static object DeleteIf(BlockParam block, IDictionary/*!*/ [RubyMethod("each_pair")] public static object Each(RubyContext/*!*/ context, [NotNull]BlockParam/*!*/ block, IDictionary/*!*/ self) { if (self.Count > 0) { + bool yieldPair = IsPairYieldOptimizable(block); + + // A Ruby Hash: keys and values come out of one pass over its entries, and each value + // is used as is while the block leaves the hash alone; once it mutates it, the rest is + // looked up as below. That is the same answer without hashing every key a second + // time - which, for a key of a Ruby class, was a #hash and an #eql? call per entry. + var hash = self as Hash; + if (hash != null) { + int count = hash.Count; + object[] hashKeys = new object[count]; + object[] hashValues = new object[count]; + hash.CopyKeysAndValues(hashKeys, hashValues); + int stamp = hash.MutationStamp; + for (int i = 0; i < count; i++) { + object key = hashKeys[i]; + object value; + if (hash.MutationStamp == stamp) { + value = hashValues[i]; + } else if (!hash.TryGetValue(key, out value)) { + // deleted by the block before its turn came: MRI skips it + continue; + } + object result; + if (yieldPair + ? block.Yield(CustomStringDictionary.ObjToNull(key), value, out result) + : block.Yield(MakeArray(key, value), out result)) { + return result; + } + } + return self; + } + // Must make a copy of the Keys array so that we can iterate over a static set of keys. Remember // that the block can modify the hash, hence the need for a copy of the keys object[] keys = new object[self.Count]; self.Keys.CopyTo(keys, 0); - bool yieldPair = IsPairYieldOptimizable(block); - // TODO: what are all the scenarios where the block can mutate the hash? can it remove keys? if so, what happens? for (int i = 0; i < keys.Length; i++) { object result; diff --git a/Src/Libraries/Extensions/IListOps.cs b/Src/Libraries/Extensions/IListOps.cs index 69d275037..92f32559c 100644 --- a/Src/Libraries/Extensions/IListOps.cs +++ b/Src/Libraries/Extensions/IListOps.cs @@ -233,15 +233,9 @@ internal static void AddRange(IList/*!*/ collection, IList/*!*/ items) { throw RubyExceptions.CreateTypeError("{0}#allocate should return IList", cls.Name); } - internal static IEnumerable/*!*/ ReverseEnumerateIndexes(IList/*!*/ collection) { - for (int originalSize = collection.Count, i = originalSize - 1; i >= 0; i--) { - yield return i; - if (collection.Count < originalSize) { - i = originalSize - (originalSize - collection.Count); - originalSize = collection.Count; - } - } - } + // The reverse walks below are MRI's (rb_ary_reverse_each, rb_ary_rindex): index Count - 1 + // down to 0, and when the block has shrunk the array below the index just visited, on from + // its new end - elements appended meanwhile are not visited, and none is visited twice. #endregion @@ -1127,6 +1121,18 @@ private static object DeleteIf(BlockParam/*!*/ block, IList/*!*/ self, out bool [RubyMethod("each")] public static object Each([NotNull]BlockParam/*!*/ block, IList/*!*/ self) { + var array = self as RubyArray; + if (array != null) { + // the same loop, without an interface call for Count and the element every time round + for (int i = 0; i < array.Count; i++) { + object result; + if (block.Yield(array[i], out result)) { + return result; + } + } + return self; + } + for (int i = 0; i < self.Count; i++) { object result; if (block.Yield(self[i], out result)) { @@ -1136,6 +1142,60 @@ public static object Each([NotNull]BlockParam/*!*/ block, IList/*!*/ self) { return self; } + #region map, collect, select + + // Array's own, as in MRI (rb_ary_collect, rb_ary_select): they walk the array rather than + // run Enumerable's versions, which call #each with a block of their own that yields to the + // given block - two block invocations and a packed argument array per element. Like MRI's, + // they do not call an #each that a subclass defines. + + [RubyMethod("collect")] + [RubyMethod("map")] + public static Enumerator/*!*/ GetMapEnumerator(IList/*!*/ self) { + return new Enumerator(self, "map"); + } + + [RubyMethod("collect")] + [RubyMethod("map")] + public static object Map([NotNull]BlockParam/*!*/ collector, IList/*!*/ self) { + var result = new RubyArray(self.Count); + var array = self as RubyArray; + for (int i = 0; i < (array != null ? array.Count : self.Count); i++) { + object blockResult; + if (collector.Yield(array != null ? array[i] : self[i], out blockResult)) { + return blockResult; + } + result.Add(blockResult); + } + return result; + } + + [RubyMethod("filter")] + [RubyMethod("select")] + public static Enumerator/*!*/ GetSelectEnumerator(IList/*!*/ self) { + return new Enumerator(self, "select"); + } + + [RubyMethod("filter")] + [RubyMethod("select")] + public static object Select([NotNull]BlockParam/*!*/ predicate, IList/*!*/ self) { + var result = new RubyArray(); + var array = self as RubyArray; + for (int i = 0; i < (array != null ? array.Count : self.Count); i++) { + object blockResult; + if (predicate.Yield(array != null ? array[i] : self[i], out blockResult)) { + return blockResult; + } + if (Protocols.IsTrue(blockResult)) { + // MRI adds the element as it is once the block has run (rb_ary_elt) + result.Add(i < self.Count ? self[i] : null); + } + } + return result; + } + + #endregion + [RubyMethod("each_index")] public static Enumerator/*!*/ EachIndex(IList/*!*/ self) { return new Enumerator(self, "each_index"); @@ -1162,11 +1222,15 @@ public static object EachIndex([NotNull]BlockParam/*!*/ block, IList/*!*/ self) [RubyMethod("reverse_each")] public static object ReverseEach([NotNull]BlockParam/*!*/ block, RubyArray/*!*/ self) { - foreach (int index in IListOps.ReverseEnumerateIndexes(self)) { + for (int i = self.Count; i-- > 0; ) { object result; - if (block.Yield(self[index], out result)) { + if (block.Yield(self[i], out result)) { return result; } + int count = self.Count; + if (count < i) { + i = count; + } } return self; } @@ -1636,7 +1700,7 @@ public static object FindIndex(BinaryOpStorage/*!*/ equals, BlockParam predicate [RubyMethod("rindex")] public static object ReverseIndex([NotNull]BlockParam/*!*/ predicate, IList/*!*/ self) { - foreach (int i in IListOps.ReverseEnumerateIndexes(self)) { + for (int i = self.Count; i-- > 0; ) { object blockResult; if (predicate.Yield(self[i], out blockResult)) { return blockResult; @@ -1645,6 +1709,10 @@ public static object ReverseIndex([NotNull]BlockParam/*!*/ predicate, IList/*!*/ if (Protocols.IsTrue(blockResult)) { return ScriptingRuntimeHelpers.Int32ToObject(i); } + int count = self.Count; + if (count < i) { + i = count; + } } return null; } @@ -1655,10 +1723,14 @@ public static object ReverseIndex(BinaryOpStorage/*!*/ equals, BlockParam predic equals.Context.ReportWarning("given block not used"); } - foreach (int i in IListOps.ReverseEnumerateIndexes(self)) { + for (int i = self.Count; i-- > 0; ) { if (Protocols.IsEqual(equals, self[i], item)) { return ScriptingRuntimeHelpers.Int32ToObject(i); } + // an == that shrank the array below the next index ends the search, as in MRI + if (i > self.Count) { + break; + } } return null; } diff --git a/Src/Libraries/Initializers.Generated.cs b/Src/Libraries/Initializers.Generated.cs index 9013034c6..39150012d 100644 --- a/Src/Libraries/Initializers.Generated.cs +++ b/Src/Libraries/Initializers.Generated.cs @@ -569,18 +569,18 @@ private static void LoadBasicObject_Instance(IronRuby.Builtins.RubyModule/*!*/ m ); DefineLibraryMethod(module, "__send__", 0x51, - new[] { 0x00000000U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x80020004U, 0x80040008U}, + new[] { 0x00000000U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x80040008U, 0x80080010U}, new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage), - new Func(IronRuby.Builtins.BasicObjectOps.SendMessage) + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object[], System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object[], System.Object>(IronRuby.Builtins.BasicObjectOps.SendMessage) ); DefineLibraryMethod(module, "!", 0x51, @@ -1503,6 +1503,21 @@ private static void LoadEnumerator_Instance(IronRuby.Builtins.RubyModule/*!*/ mo new Func(IronRuby.Builtins.Enumerator.GetTarget) ); + DefineLibraryMethod(module, "__ir_array_cursor__", 0x52, + 0x00000000U, + new Func(IronRuby.Builtins.Enumerator.GetArrayCursor) + ); + + DefineLibraryMethod(module, "__ir_array_cursor_next__", 0x52, + 0x00000002U, + new Func(IronRuby.Builtins.Enumerator.ArrayCursorNext) + ); + + DefineLibraryMethod(module, "__ir_array_cursor_source__", 0x52, + 0x00000002U, + new Func(IronRuby.Builtins.Enumerator.ArrayCursorSource) + ); + DefineLibraryMethod(module, "each", 0x51, 0x80000000U, new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, IronRuby.Builtins.Enumerator, System.Object[], System.Object>(IronRuby.Builtins.Enumerator.EachFromCaller) @@ -4949,7 +4964,7 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module DefineLibraryMethod(module, "public_send", 0x51, 0x00000000U, 0x80000000U, new Func(IronRuby.Builtins.KernelOps.PublicSendMessage), - new Func, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.Object, System.Object[], System.Object>(IronRuby.Builtins.KernelOps.PublicSendMessage) + new Func, IronRuby.Runtime.SendSiteStorage>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.Object, System.Object[], System.Object>(IronRuby.Builtins.KernelOps.PublicSendMessage) ); DefineLibraryMethod(module, "putc", 0x52, @@ -5000,7 +5015,7 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module DefineLibraryMethod(module, "respond_to?", 0x51, 0x00040008U, - new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Boolean, System.Boolean>(IronRuby.Builtins.KernelOps.RespondTo) + new Func(IronRuby.Builtins.KernelOps.RespondTo) ); DefineLibraryMethod(module, "select", 0x52, @@ -5009,18 +5024,18 @@ private static void LoadKernel_Instance(IronRuby.Builtins.RubyModule/*!*/ module ); DefineLibraryMethod(module, "send", 0x51, - new[] { 0x00000000U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x00020004U, 0x00040008U, 0x80020004U, 0x80040008U}, + new[] { 0x00000000U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x00040008U, 0x00080010U, 0x80040008U, 0x80080010U}, new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage), - new Func(IronRuby.Builtins.KernelOps.SendMessage) + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, System.Object, System.String, System.Object[], System.Object>(IronRuby.Builtins.KernelOps.SendMessage), + new Func>, IronRuby.Runtime.RubyScope, IronRuby.Runtime.BlockParam, System.Object, System.String, System.Object[], System.Object>(IronRuby.Builtins.KernelOps.SendMessage) ); DefineLibraryMethod(module, "set_trace_func", 0x52, @@ -5627,8 +5642,8 @@ private static void LoadMath_Constants(IronRuby.Builtins.RubyModule/*!*/ module) private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) { DefineLibraryMethod(module, "acos", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Acos) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Acos) ); DefineLibraryMethod(module, "acosh", 0x52, @@ -5637,23 +5652,23 @@ private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "asin", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Asin) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Asin) ); DefineLibraryMethod(module, "asinh", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Asinh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Asinh) ); DefineLibraryMethod(module, "atan", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Atan) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Atan) ); DefineLibraryMethod(module, "atan2", 0x52, - 0x00030000U, - new Func(IronRuby.Builtins.RubyMath.Atan2) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Atan2) ); DefineLibraryMethod(module, "atanh", 0x52, @@ -5662,18 +5677,18 @@ private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "cbrt", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.CubeRoot) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.CubeRoot) ); DefineLibraryMethod(module, "cos", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Cos) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Cos) ); DefineLibraryMethod(module, "cosh", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Cosh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Cosh) ); DefineLibraryMethod(module, "erf", 0x52, @@ -5687,8 +5702,8 @@ private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "exp", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Exp) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Exp) ); DefineLibraryMethod(module, "frexp", 0x52, @@ -5702,8 +5717,8 @@ private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "hypot", 0x52, - 0x00030000U, - new Func(IronRuby.Builtins.RubyMath.Hypot) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Hypot) ); DefineLibraryMethod(module, "ldexp", 0x52, @@ -5732,36 +5747,36 @@ private static void LoadMath_Instance(IronRuby.Builtins.RubyModule/*!*/ module) ); DefineLibraryMethod(module, "sin", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sin) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sin) ); DefineLibraryMethod(module, "sinh", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sinh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sinh) ); DefineLibraryMethod(module, "sqrt", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sqrt) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sqrt) ); DefineLibraryMethod(module, "tan", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Tan) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Tan) ); DefineLibraryMethod(module, "tanh", 0x52, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Tanh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Tanh) ); } private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { DefineLibraryMethod(module, "acos", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Acos) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Acos) ); DefineLibraryMethod(module, "acosh", 0x61, @@ -5770,23 +5785,23 @@ private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "asin", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Asin) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Asin) ); DefineLibraryMethod(module, "asinh", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Asinh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Asinh) ); DefineLibraryMethod(module, "atan", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Atan) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Atan) ); DefineLibraryMethod(module, "atan2", 0x61, - 0x00030000U, - new Func(IronRuby.Builtins.RubyMath.Atan2) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Atan2) ); DefineLibraryMethod(module, "atanh", 0x61, @@ -5795,18 +5810,18 @@ private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "cbrt", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.CubeRoot) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.CubeRoot) ); DefineLibraryMethod(module, "cos", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Cos) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Cos) ); DefineLibraryMethod(module, "cosh", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Cosh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Cosh) ); DefineLibraryMethod(module, "erf", 0x61, @@ -5820,8 +5835,8 @@ private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "exp", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Exp) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Exp) ); DefineLibraryMethod(module, "frexp", 0x61, @@ -5835,8 +5850,8 @@ private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "hypot", 0x61, - 0x00030000U, - new Func(IronRuby.Builtins.RubyMath.Hypot) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Hypot) ); DefineLibraryMethod(module, "ldexp", 0x61, @@ -5865,28 +5880,28 @@ private static void LoadMath_Class(IronRuby.Builtins.RubyModule/*!*/ module) { ); DefineLibraryMethod(module, "sin", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sin) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sin) ); DefineLibraryMethod(module, "sinh", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sinh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sinh) ); DefineLibraryMethod(module, "sqrt", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Sqrt) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Sqrt) ); DefineLibraryMethod(module, "tan", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Tan) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Tan) ); DefineLibraryMethod(module, "tanh", 0x61, - 0x00010000U, - new Func(IronRuby.Builtins.RubyMath.Tanh) + 0x00000000U, + new Func>, System.Object, System.Object, System.Object>(IronRuby.Builtins.RubyMath.Tanh) ); } @@ -8403,6 +8418,11 @@ private static void LoadStruct_Constants(IronRuby.Builtins.RubyModule/*!*/ modul } private static void LoadStruct_Instance(IronRuby.Builtins.RubyModule/*!*/ module) { + DefineLibraryMethod(module, "__struct_initialize__", 0x52, + 0x80000000U, + new Action(IronRuby.Builtins.RubyStructOps.Reinitialize) + ); + DefineLibraryMethod(module, "[]", 0x51, 0x00000000U, 0x00000002U, 0x00000002U, 0x00000000U, new Func(IronRuby.Builtins.RubyStructOps.GetValue), @@ -8446,7 +8466,7 @@ private static void LoadStruct_Instance(IronRuby.Builtins.RubyModule/*!*/ module DefineLibraryMethod(module, "initialize", 0x52, 0x80000000U, - new Action(IronRuby.Builtins.RubyStructOps.Reinitialize) + new Func>, IronRuby.Builtins.RubyStruct, System.Object[], System.Object>(IronRuby.Builtins.RubyStructOps.Initialize) ); DefineLibraryMethod(module, "initialize_copy", 0x52, @@ -9073,6 +9093,12 @@ private static void LoadSystem__Collections__IList_Instance(IronRuby.Builtins.Ru new Func(IronRuby.Builtins.IListOps.Clear) ); + DefineLibraryMethod(module, "collect", 0x51, + 0x00000000U, 0x00000001U, + new Func(IronRuby.Builtins.IListOps.GetMapEnumerator), + new Func(IronRuby.Builtins.IListOps.Map) + ); + DefineLibraryMethod(module, "collect!", 0x51, 0x00000000U, new Func(IronRuby.Builtins.IListOps.CollectInPlace) @@ -9166,6 +9192,12 @@ private static void LoadSystem__Collections__IList_Instance(IronRuby.Builtins.Ru new Func, IronRuby.Runtime.BlockParam, System.Collections.IList, IronRuby.Builtins.Range, System.Object>(IronRuby.Builtins.IListOps.Fill) ); + DefineLibraryMethod(module, "filter", 0x51, + 0x00000000U, 0x00000001U, + new Func(IronRuby.Builtins.IListOps.GetSelectEnumerator), + new Func(IronRuby.Builtins.IListOps.Select) + ); + DefineLibraryMethod(module, "find_index", 0x51, 0x00000000U, 0x00000001U, 0x00000000U, new Func(IronRuby.Builtins.IListOps.GetFindIndexEnumerator), @@ -9249,6 +9281,12 @@ private static void LoadSystem__Collections__IList_Instance(IronRuby.Builtins.Ru new Func(IronRuby.Builtins.IListOps.Length) ); + DefineLibraryMethod(module, "map", 0x51, + 0x00000000U, 0x00000001U, + new Func(IronRuby.Builtins.IListOps.GetMapEnumerator), + new Func(IronRuby.Builtins.IListOps.Map) + ); + DefineLibraryMethod(module, "map!", 0x51, 0x00000000U, new Func(IronRuby.Builtins.IListOps.CollectInPlace) @@ -9335,6 +9373,12 @@ private static void LoadSystem__Collections__IList_Instance(IronRuby.Builtins.Ru new Func(IronRuby.Builtins.IListOps.ReverseIndex) ); + DefineLibraryMethod(module, "select", 0x51, + 0x00000000U, 0x00000001U, + new Func(IronRuby.Builtins.IListOps.GetSelectEnumerator), + new Func(IronRuby.Builtins.IListOps.Select) + ); + DefineLibraryMethod(module, "shift", 0x51, 0x00000000U, new Func(IronRuby.Builtins.IListOps.Shift) @@ -10007,6 +10051,46 @@ private static void LoadThread_Class(IronRuby.Builtins.RubyModule/*!*/ module) { new Func(IronRuby.Builtins.ThreadOps.GetBacktraceLimit) ); + DefineLibraryMethod(module, "__fiber_current__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.GetCurrentFiber) + ); + + DefineLibraryMethod(module, "__fiber_current_set__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.SetCurrentFiber) + ); + + DefineLibraryMethod(module, "__fiber_mailbox__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.CreateFiberMailbox) + ); + + DefineLibraryMethod(module, "__fiber_park__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.ParkFiberThread) + ); + + DefineLibraryMethod(module, "__fiber_post__", 0x61, + 0x00000002U, + new Func(IronRuby.Builtins.ThreadOps.FiberPost) + ); + + DefineLibraryMethod(module, "__fiber_release_locks__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.ReleaseFiberLocks) + ); + + DefineLibraryMethod(module, "__fiber_reuse__", 0x61, + 0x00000000U, + new Func(IronRuby.Builtins.ThreadOps.ReuseFiberThread) + ); + + DefineLibraryMethod(module, "__fiber_take__", 0x61, + 0x00000006U, + new Func(IronRuby.Builtins.ThreadOps.FiberTake) + ); + DefineLibraryMethod(module, "__set_fiber_owner__", 0x61, 0x00000002U, new Func(IronRuby.Builtins.ThreadOps.SetFiberOwner) diff --git a/Src/Libraries/StringScanner/StringScanner.cs b/Src/Libraries/StringScanner/StringScanner.cs index 5b8db7b72..e8e50e4ed 100644 --- a/Src/Libraries/StringScanner/StringScanner.cs +++ b/Src/Libraries/StringScanner/StringScanner.cs @@ -65,6 +65,13 @@ public Error(string message, Exception inner) : base(message ?? "Error", inner) /// private MatchData _match; + /// + /// A regexp match whose MatchData is only made when its groups are asked for (see + /// CurrentMatch): the operations that answer a length or a position (#skip, #match?, + /// #exist?, #skip_until) rarely have their groups read. + /// + private RubyRegex.PendingMatch _pendingMatch; + /// The byte offset _match's own offsets are relative to. private int _matchBase; @@ -100,6 +107,7 @@ private void InitializeFrom(StringScanner/*!*/ other) { _matchStart = other._matchStart; _matchEnd = other._matchEnd; _match = other._match; + _pendingMatch = other._pendingMatch; _matchBase = other._matchBase; } @@ -243,6 +251,7 @@ private void ResetScanner() { private void ClearMatch() { _matched = false; _match = null; + _pendingMatch = default(RubyRegex.PendingMatch); _matchBase = 0; _matchStart = 0; _matchEnd = 0; @@ -275,15 +284,49 @@ private void ClearMatch() { } private bool Match(object/*!*/ pattern, bool anchored, bool advance) { + return Match(pattern, anchored, advance, false); + } + + /// The caller answers a length or a position, not text: the + /// MatchData can wait until something asks for a group. + private bool Match(object/*!*/ pattern, bool anchored, bool advance, bool lazyGroups) { var regex = pattern as RubyRegex; return (regex != null) - ? MatchRegex(regex, anchored, advance) + ? MatchRegex(regex, anchored, advance, lazyGroups) : MatchLiteral((MutableString)pattern, anchored, advance); } - private bool MatchRegex(RubyRegex/*!*/ pattern, bool anchored, bool advance) { + /// + /// The MatchData of the last regexp match, made now if the match left it pending; null + /// when the last match had no pattern. + /// + private MatchData CurrentMatch { + get { + if (_match == null && !_pendingMatch.IsEmpty) { + _match = _pendingMatch.Complete(); + _pendingMatch = default(RubyRegex.PendingMatch); + } + return _match; + } + } + + private bool MatchRegex(RubyRegex/*!*/ pattern, bool anchored, bool advance, bool lazyGroups) { ClearMatch(); + if (lazyGroups && !_fixedAnchor) { + int boundsStart, boundsEnd; + RubyRegex.PendingMatch pending; + if (!pattern.MatchWindowBounds(_scanString, CharIndexOf(_position), anchored, out boundsStart, out boundsEnd, out pending)) { + return false; + } + if (anchored && boundsStart != _position) { + return false; + } + _pendingMatch = pending; + _matchBase = 0; + return Matched(boundsStart, boundsEnd, advance); + } + MatchData match; int matchBase = 0; if (_fixedAnchor) { @@ -302,24 +345,39 @@ private bool MatchRegex(RubyRegex/*!*/ pattern, bool anchored, bool advance) { return false; } - int[] offsets = match.GetGroupOffsets(0, true); - if (offsets == null) { + int matchStart, matchEnd; + if (!match.TryGetGroupByteOffsets(0, out matchStart, out matchEnd)) { return false; } - int start = matchBase + offsets[0]; + int start = matchBase + matchStart; if (anchored && start != _position) { return false; } _match = match; _matchBase = matchBase; - return Matched(start, matchBase + offsets[1], advance); + return Matched(start, matchBase + matchEnd, advance); } private bool MatchLiteral(MutableString/*!*/ pattern, bool anchored, bool advance) { ClearMatch(); + if (anchored && pattern.IsAscii()) { + // The usual #skip("{"): an ASCII pattern's characters are its bytes, so it can be + // compared where it stands instead of being copied out to a byte array first. + int length = pattern.GetByteCount(); + if (_position + length > ByteLength) { + return false; + } + for (int i = 0; i < length; i++) { + if (_scanString.GetByte(_position + i) != pattern.GetByte(i)) { + return false; + } + } + return Matched(_position, _position + length, advance); + } + byte[] needle = pattern.ToByteArray(); int start = -1; if (anchored) { @@ -385,30 +443,33 @@ public static MutableString GetMatchSubgroup(ConversionStorage/*!*/ fixnumC } // A match with no pattern behind it has no named groups at all, so every name // is undefined rather than merely unmatched. - if (self._match == null || !self._match.HasNamedGroup(name)) { + if (self.CurrentMatch == null || !self.CurrentMatch.HasNamedGroup(name)) { throw RubyExceptions.CreateIndexError("undefined group name reference: {0}", name); } - int[] named = self._match.GetGroupOffsets(name, true); + int[] named = self.CurrentMatch.GetGroupOffsets(name, true); return (named == null) ? null : self.ByteSlice(self._matchBase + named[0], named[1] - named[0]); } - int subgroup = Protocols.CastToFixnum(fixnumCast, index); + // An Integer index is the usual case; it converts to itself. + int subgroup = (index is int) ? (int)index : Protocols.CastToFixnum(fixnumCast, index); if (!self._matched) { return null; } - if (self._match == null) { + if (self.CurrentMatch == null) { return (subgroup == 0) ? self.ByteSlice(self._matchStart, self._matchEnd - self._matchStart) : null; } - int count = self._match.GroupCount; + int count = self.CurrentMatch.GroupCount; if (subgroup < 0) { subgroup += count; } if (subgroup < 0 || subgroup >= count) { return null; } - int[] offsets = self._match.GetGroupOffsets(subgroup, true); - return (offsets == null) ? null : self.ByteSlice(self._matchBase + offsets[0], offsets[1] - offsets[0]); + int start, end; + return self.CurrentMatch.TryGetGroupByteOffsets(subgroup, out start, out end) + ? self.ByteSlice(self._matchBase + start, end - start) + : null; } private static string GroupName(object index) { @@ -426,11 +487,11 @@ public static RubyArray Captures(StringScanner/*!*/ self) { return null; } var result = new RubyArray(); - if (self._match == null) { + if (self.CurrentMatch == null) { return result; } - for (int i = 1; i < self._match.GroupCount; i++) { - int[] offsets = self._match.GetGroupOffsets(i, true); + for (int i = 1; i < self.CurrentMatch.GroupCount; i++) { + int[] offsets = self.CurrentMatch.GetGroupOffsets(i, true); result.Add((offsets == null) ? null : self.ByteSlice(self._matchBase + offsets[0], offsets[1] - offsets[0])); } return result; @@ -451,11 +512,11 @@ public static RubyArray ValuesAt(ConversionStorage/*!*/ fixnumCast, StringS [RubyMethod("named_captures")] public static Hash/*!*/ NamedCaptures(RubyContext/*!*/ context, StringScanner/*!*/ self) { var result = new Hash(context); - if (!self._matched || self._match == null) { + if (!self._matched || self.CurrentMatch == null) { return result; } - foreach (var name in self._match.GetGroupNames()) { - int[] offsets = self._match.GetGroupOffsets(name, true); + foreach (var name in self.CurrentMatch.GetGroupNames()) { + int[] offsets = self.CurrentMatch.GetGroupOffsets(name, true); result[MutableString.CreateAscii(name).Freeze()] = (offsets == null) ? null : self.ByteSlice(self._matchBase + offsets[0], offsets[1] - offsets[0]); } @@ -467,7 +528,7 @@ public static RubyArray ValuesAt(ConversionStorage/*!*/ fixnumCast, StringS if (!self._matched) { return null; } - return (self._match != null) ? self._match.GroupCount : 1; + return (self.CurrentMatch != null) ? self.CurrentMatch.GroupCount : 1; } [RubyMethod("fixed_anchor?")] @@ -499,7 +560,7 @@ public static bool EndOfLine(StringScanner/*!*/ self) { [RubyMethod("exist?")] public static int? Exist(ConversionStorage/*!*/ stringCast, StringScanner/*!*/ self, object pattern) { - if (!self.Match(ToPattern(stringCast, pattern), false, false)) { + if (!self.Match(ToPattern(stringCast, pattern), false, false, true)) { return null; } return self._matchEnd - self._position; @@ -560,7 +621,7 @@ public static MutableString ToString(StringScanner/*!*/ self) { [RubyMethod("match?")] public static int? Match(ConversionStorage/*!*/ stringCast, StringScanner/*!*/ self, object pattern) { - if (!self.Match(ToPattern(stringCast, pattern), true, false)) { + if (!self.Match(ToPattern(stringCast, pattern), true, false, true)) { return null; } return self._matchEnd - self._matchStart; @@ -699,7 +760,7 @@ public static object SearchFull(ConversionStorage/*!*/ stringCast [RubyMethod("skip")] public static int? Skip(ConversionStorage/*!*/ stringCast, StringScanner/*!*/ self, object pattern) { - if (!self.Match(ToPattern(stringCast, pattern), true, true)) { + if (!self.Match(ToPattern(stringCast, pattern), true, true, true)) { return null; } return self._position - self._previousPosition; @@ -707,7 +768,7 @@ public static object SearchFull(ConversionStorage/*!*/ stringCast [RubyMethod("skip_until")] public static int? SkipUntil(ConversionStorage/*!*/ stringCast, StringScanner/*!*/ self, object pattern) { - if (!self.Match(ToPattern(stringCast, pattern), false, true)) { + if (!self.Match(ToPattern(stringCast, pattern), false, true, true)) { return null; } return self._position - self._previousPosition; diff --git a/Src/Prism/Generated/PrismLoader.Generated.cs b/Src/Prism/Generated/PrismLoader.Generated.cs index b1a6c43c8..d896af17f 100644 --- a/Src/Prism/Generated/PrismLoader.Generated.cs +++ b/Src/Prism/Generated/PrismLoader.Generated.cs @@ -444,22 +444,31 @@ private PmNode LoadNode() { } case 45: { LoadRawUInt32(); - return new DefNode { + var def = new DefNode { NodeId = nodeId, StartOffset = location.Start, Length = location.Length, Flags = LoadVarUInt(), Name = LoadConstant(), NameLoc = LoadLocation(), Receiver = LoadOptionalNode(), Parameters = LoadOptionalNode(), - Body = LoadOptionalNode(), - Locals = LoadConstantList(), - DefKeywordLoc = LoadLocation(), - OperatorLoc = LoadOptionalLocation(), - LparenLoc = LoadOptionalLocation(), - RparenLoc = LoadOptionalLocation(), - EqualLoc = LoadOptionalLocation(), - EndKeywordLoc = LoadOptionalLocation(), }; + if (_lazyDefBodies) { + def.DeferredBodyLoader = this; + def.DeferredBodyOffset = _pos; + _contents = 0; + SkimOptionalNode(); + def.BodyContents = _contents; + } else { + def.Body = LoadOptionalNode(); + } + def.Locals = LoadConstantList(); + def.DefKeywordLoc = LoadLocation(); + def.OperatorLoc = LoadOptionalLocation(); + def.LparenLoc = LoadOptionalLocation(); + def.RparenLoc = LoadOptionalLocation(); + def.EqualLoc = LoadOptionalLocation(); + def.EndKeywordLoc = LoadOptionalLocation(); + return def; } case 46: { return new DefinedNode { @@ -1432,6 +1441,1144 @@ private PmNode LoadNode() { RparenLoc = LoadOptionalLocation(), }; } + default: + throw new System.InvalidOperationException("Unknown prism node type: " + type); + } +} + +// Reads past a node without building it, noting in _contents what the bridge has to see +// at parse time (see PrismLoader.Contents*). Mirrors LoadNode field for field. +private void SkimNode() { + byte type = LoadByte(); + LoadVarUInt(); // node id + LoadLocation(); + switch (type) { + case 1: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 2: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 3: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 4: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 5: { + LoadVarUInt(); // flags + SkimNodeList(); + return; + } + case 6: { + LoadVarUInt(); // flags + SkimNodeList(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 7: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNodeList(); + SkimOptionalNode(); + SkimNodeList(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 8: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadOptionalLocation(); + return; + } + case 9: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + return; + } + case 10: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 11: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + SkimOptionalNode(); + SkimOptionalNode(); + LoadOptionalLocation(); + return; + } + case 12: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + return; + } + case 13: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 14: { + LoadVarUInt(); // flags + SkipVarUIntList(); + SkimOptionalNode(); + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + return; + } + case 15: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadOptionalLocation(); + LoadLocation(); + return; + } + case 16: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNodeList(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 17: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + return; + } + case 18: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadVarUInt(); + LoadVarUInt(); + LoadLocation(); + SkimNode(); + return; + } + case 19: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadVarUInt(); + LoadOptionalLocation(); + LoadOptionalLocation(); + SkimOptionalNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + SkimOptionalNode(); + return; + } + case 20: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadVarUInt(); + LoadVarUInt(); + LoadVarUInt(); + LoadLocation(); + SkimNode(); + return; + } + case 21: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadVarUInt(); + LoadVarUInt(); + LoadLocation(); + SkimNode(); + return; + } + case 22: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + LoadVarUInt(); + LoadLocation(); + return; + } + case 23: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 24: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNodeList(); + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + return; + } + case 25: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNodeList(); + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + return; + } + case 26: { + LoadVarUInt(); // flags + _contents |= ContentsEager; + SkipVarUIntList(); + LoadLocation(); + SkimNode(); + LoadOptionalLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + LoadLocation(); + LoadVarUInt(); + return; + } + case 27: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 28: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + return; + } + case 29: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 30: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 31: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 32: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimNode(); + LoadLocation(); + return; + } + case 33: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 34: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + return; + } + case 35: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 36: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimNode(); + return; + } + case 37: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + return; + } + case 38: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + return; + } + case 39: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimNode(); + return; + } + case 40: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + return; + } + case 41: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimNode(); + return; + } + case 42: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 43: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 44: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimNode(); + LoadLocation(); + return; + } + case 45: { + _pos += 4; + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimOptionalNode(); + uint outer = _contents; + _contents = 0; + SkimOptionalNode(); + SkimOptionalNode(); + _contents = outer | (_contents & ContentsEager); + SkipVarUIntList(); + LoadLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 46: { + LoadVarUInt(); // flags + _contents |= ContentsEager; + LoadOptionalLocation(); + SkimNode(); + LoadOptionalLocation(); + LoadLocation(); + return; + } + case 47: { + LoadVarUInt(); // flags + LoadLocation(); + SkimOptionalNode(); + LoadOptionalLocation(); + return; + } + case 48: { + LoadVarUInt(); // flags + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + return; + } + case 49: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNode(); + return; + } + case 50: { + LoadVarUInt(); // flags + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + return; + } + case 51: { + LoadVarUInt(); // flags + SkimOptionalNode(); + return; + } + case 52: { + LoadVarUInt(); // flags + return; + } + case 53: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNode(); + SkimNodeList(); + SkimNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 54: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimOptionalNode(); + LoadLocation(); + return; + } + case 55: { + LoadVarUInt(); // flags + _pos += 8; + return; + } + case 56: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + LoadOptionalLocation(); + LoadLocation(); + return; + } + case 57: { + LoadVarUInt(); // flags + return; + } + case 58: { + LoadVarUInt(); // flags + return; + } + case 59: { + LoadVarUInt(); // flags + LoadLocation(); + if (_buffer[_pos] == 0) _contents |= ContentsUsesBlock; + SkimOptionalNode(); + return; + } + case 60: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 61: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + return; + } + case 62: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 63: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 64: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 65: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimNode(); + LoadLocation(); + return; + } + case 66: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadLocation(); + return; + } + case 67: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimNodeList(); + SkimOptionalNode(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 68: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + SkimNode(); + LoadOptionalLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + LoadOptionalLocation(); + return; + } + case 69: { + LoadVarUInt(); // flags + SkimNode(); + return; + } + case 70: { + LoadVarUInt(); // flags + SkimNode(); + return; + } + case 71: { + LoadVarUInt(); // flags + return; + } + case 72: { + LoadVarUInt(); // flags + SkimNode(); + SkimOptionalNode(); + LoadLocation(); + LoadOptionalLocation(); + return; + } + case 73: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimNode(); + return; + } + case 74: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimOptionalNode(); + LoadVarUInt(); + LoadLocation(); + SkimNode(); + return; + } + case 75: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadOptionalLocation(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimNode(); + return; + } + case 76: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimOptionalNode(); + LoadLocation(); + SkimOptionalNode(); + return; + } + case 77: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 78: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + return; + } + case 79: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 80: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 81: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 82: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimNode(); + LoadLocation(); + return; + } + case 83: { + LoadVarUInt(); // flags + LoadInteger(); + return; + } + case 84: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadLocation(); + return; + } + case 85: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadLocation(); + return; + } + case 86: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + SkimNodeList(); + LoadOptionalLocation(); + return; + } + case 87: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + SkimNodeList(); + LoadOptionalLocation(); + return; + } + case 88: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadLocation(); + return; + } + case 89: { + LoadVarUInt(); // flags + return; + } + case 90: { + LoadVarUInt(); // flags + return; + } + case 91: { + LoadVarUInt(); // flags + SkimNodeList(); + return; + } + case 92: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadOptionalLocation(); + LoadLocation(); + return; + } + case 93: { + LoadVarUInt(); // flags + SkipVarUIntList(); + LoadLocation(); + LoadLocation(); + LoadLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + return; + } + case 94: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + LoadVarUInt(); + return; + } + case 95: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + LoadVarUInt(); + LoadVarUInt(); + return; + } + case 96: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + SkimNode(); + LoadVarUInt(); + LoadVarUInt(); + return; + } + case 97: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadVarUInt(); + return; + } + case 98: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadVarUInt(); + return; + } + case 99: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadVarUInt(); + LoadLocation(); + SkimNode(); + LoadLocation(); + return; + } + case 100: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + LoadLocation(); + SkipRegexpSource(); + return; + } + case 101: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 102: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 103: { + LoadVarUInt(); // flags + SkimNode(); + SkimNodeList(); + return; + } + case 104: { + LoadVarUInt(); // flags + _contents |= ContentsEager; + SkipVarUIntList(); + LoadLocation(); + SkimNode(); + SkimOptionalNode(); + LoadLocation(); + LoadVarUInt(); + return; + } + case 105: { + LoadVarUInt(); // flags + SkimNodeList(); + SkimOptionalNode(); + SkimNodeList(); + LoadOptionalLocation(); + LoadOptionalLocation(); + return; + } + case 106: { + LoadVarUInt(); // flags + SkimNodeList(); + SkimOptionalNode(); + SkimNodeList(); + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 107: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + return; + } + case 108: { + LoadVarUInt(); // flags + return; + } + case 109: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + return; + } + case 110: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + return; + } + case 111: { + LoadVarUInt(); // flags + _pos++; + return; + } + case 112: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 113: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + SkimNode(); + return; + } + case 114: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + LoadLocation(); + SkimNode(); + return; + } + case 115: { + LoadVarUInt(); // flags + SkimNode(); + SkimNode(); + LoadLocation(); + return; + } + case 116: { + LoadVarUInt(); // flags + SkimNodeList(); + SkimNodeList(); + SkimOptionalNode(); + SkimNodeList(); + SkimNodeList(); + SkimOptionalNode(); + SkimOptionalNode(); + return; + } + case 117: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + return; + } + case 118: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + LoadLocation(); + LoadLocation(); + return; + } + case 119: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + return; + } + case 120: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + LoadLocation(); + return; + } + case 121: { + LoadVarUInt(); // flags + SkimOptionalNode(); + LoadLocation(); + LoadLocation(); + LoadLocation(); + return; + } + case 122: { + LoadVarUInt(); // flags + SkipVarUIntList(); + SkimNode(); + return; + } + case 123: { + LoadVarUInt(); // flags + SkimOptionalNode(); + SkimOptionalNode(); + LoadLocation(); + return; + } + case 124: { + LoadVarUInt(); // flags + LoadInteger(); + LoadInteger(); + return; + } + case 125: { + LoadVarUInt(); // flags + return; + } + case 126: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + LoadLocation(); + SkipRegexpSource(); + return; + } + case 127: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadLocation(); + return; + } + case 128: { + LoadVarUInt(); // flags + LoadVarUInt(); + return; + } + case 129: { + LoadVarUInt(); // flags + SkimNode(); + LoadLocation(); + SkimNode(); + return; + } + case 130: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadOptionalLocation(); + SkimOptionalNode(); + LoadOptionalLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + return; + } + case 131: { + LoadVarUInt(); // flags + LoadVarUInt(); + LoadOptionalLocation(); + LoadLocation(); + return; + } + case 132: { + LoadVarUInt(); // flags + return; + } + case 133: { + LoadVarUInt(); // flags + LoadLocation(); + SkimOptionalNode(); + return; + } + case 134: { + LoadVarUInt(); // flags + return; + } + case 135: { + LoadVarUInt(); // flags + SkimNode(); + return; + } + case 136: { + LoadVarUInt(); // flags + _contents |= ContentsEager; + SkipVarUIntList(); + LoadLocation(); + LoadLocation(); + SkimNode(); + SkimOptionalNode(); + LoadLocation(); + return; + } + case 137: { + LoadVarUInt(); // flags + return; + } + case 138: { + LoadVarUInt(); // flags + SkipString(); + return; + } + case 139: { + LoadVarUInt(); // flags + return; + } + case 140: { + LoadVarUInt(); // flags + LoadLocation(); + SkimOptionalNode(); + return; + } + case 141: { + LoadVarUInt(); // flags + SkimNodeList(); + return; + } + case 142: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + LoadLocation(); + LoadOptionalLocation(); + SkipString(); + return; + } + case 143: { + LoadVarUInt(); // flags + LoadLocation(); + LoadOptionalLocation(); + SkimOptionalNode(); + LoadOptionalLocation(); + if (_buffer[_pos] == 0) _contents |= ContentsUsesBlock; + SkimOptionalNode(); + return; + } + case 144: { + LoadVarUInt(); // flags + LoadOptionalLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + SkipString(); + return; + } + case 145: { + LoadVarUInt(); // flags + return; + } + case 146: { + LoadVarUInt(); // flags + SkimNodeList(); + LoadLocation(); + return; + } + case 147: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNode(); + LoadOptionalLocation(); + SkimOptionalNode(); + SkimOptionalNode(); + LoadOptionalLocation(); + return; + } + case 148: { + LoadVarUInt(); // flags + LoadLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + SkimNode(); + SkimOptionalNode(); + return; + } + case 149: { + LoadVarUInt(); // flags + LoadLocation(); + SkimNodeList(); + LoadOptionalLocation(); + SkimOptionalNode(); + return; + } + case 150: { + LoadVarUInt(); // flags + LoadLocation(); + LoadOptionalLocation(); + LoadOptionalLocation(); + SkimNode(); + SkimOptionalNode(); + return; + } + case 151: { + LoadVarUInt(); // flags + LoadLocation(); + LoadLocation(); + LoadLocation(); + SkipString(); + return; + } + case 152: { + LoadVarUInt(); // flags + _contents |= ContentsUsesBlock; + LoadLocation(); + LoadOptionalLocation(); + SkimOptionalNode(); + LoadOptionalLocation(); + return; + } default: throw new System.InvalidOperationException("Unknown prism node type: " + type); } diff --git a/Src/Prism/Generated/PrismNodes.Generated.cs b/Src/Prism/Generated/PrismNodes.Generated.cs index fa03bb968..21515f18d 100644 --- a/Src/Prism/Generated/PrismNodes.Generated.cs +++ b/Src/Prism/Generated/PrismNodes.Generated.cs @@ -417,6 +417,12 @@ public sealed class DefNode : PmNode { public PmLocation? RparenLoc; public PmLocation? EqualLoc; public PmLocation? EndKeywordLoc; + // Set by a loader asked for lazy method bodies: Body is null and the body is still in the + // serialized buffer, for PrismLoader.LoadDeferredBody; BodyContents is what the body holds + // (PrismLoader.Contents* bits). + public IronRuby.Prism.PrismLoader DeferredBodyLoader; + public int DeferredBodyOffset; + public uint BodyContents; } public sealed class DefinedNode : PmNode { diff --git a/Src/Prism/PrismAstBridge.cs b/Src/Prism/PrismAstBridge.cs index a801b8897..2993e26a3 100644 --- a/Src/Prism/PrismAstBridge.cs +++ b/Src/Prism/PrismAstBridge.cs @@ -22,7 +22,12 @@ public sealed class PrismAstBridge { private readonly string/*!*/ _source; private readonly string _path; private readonly int _lineOffset; - private readonly List/*!*/ _lineStarts; + // the offset each line starts at; the first _lineCount entries are used + private readonly int[]/*!*/ _lineStarts; + private readonly int _lineCount; + // the line the last location was found on: nodes come in source order, so the next + // lookup is almost always on that line or a few lines further down + private int _lineHint; // The line the first line of this source is reported as. 1 for a file; whatever // eval/module_eval/Binding#eval was told for a string, which is what makes __LINE__ @@ -52,10 +57,12 @@ private PrismAstBridge(string/*!*/ source, string path, RubyEncoding/*!*/ encodi _lineOffset = RubyUtils.DecodeEvalLineOffset(ref path); _path = path; _encoding = encoding; - _lineStarts = new List { 0 }; - for (int i = 0; i < source.Length; i++) { - if (source[i] == '\n') _lineStarts.Add(i + 1); + var lineStarts = new List { 0 }; + for (int i = source.IndexOf('\n'); i >= 0; i = source.IndexOf('\n', i + 1)) { + lineStarts.Add(i + 1); } + _lineStarts = lineStarts.ToArray(); + _lineCount = _lineStarts.Length; } public static SourceUnitTree Parse(SourceUnit/*!*/ sourceUnit, RubyCompilerOptions/*!*/ options, ErrorSink/*!*/ errorSink) { @@ -142,11 +149,16 @@ public static SourceUnitTree ParseText(string/*!*/ code, string path, List _source.Length) index = _source.Length; return new SourceLocation(index, line + _startLine, index - _lineStarts[line] + 1); } + /// + /// The last line that starts at or before (what a binary search of + /// the line starts finds), looked for from the line the previous lookup ended on. + /// + private int FindLine(int index) { + int hint = _lineHint; + int line = (_lineStarts[hint] <= index) ? FindLineFrom(index, hint) : FindLineFrom(index, 0); + _lineHint = line; + return line; + } + + // the same, given a line that starts at or before index + private int FindLineFrom(int index, int line) { + int[] starts = _lineStarts; + int count = _lineCount; + for (int i = 0; i < 4; i++) { + if (line + 1 >= count || starts[line + 1] > index) { + return line; + } + line++; + } + int lo = line, hi = count - 1; + while (lo < hi) { + int mid = lo + ((hi - lo + 1) >> 1); + if (starts[mid] <= index) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; + } + private Exception Unsupported(Pm.PmNode node) { var span = Span(node); return new NotSupportedException( @@ -970,6 +1025,9 @@ private void WarnRegexp(byte[]/*!*/ pattern, SourceSpan span) { return; } string text = System.Text.Encoding.UTF8.GetString(pattern); + if (!MayHaveNestedQuantifier(text)) { + return; + } var warnings = RegexpTransformer.GetWarnings(text); if (warnings != null) { foreach (var warning in warnings) { @@ -978,6 +1036,27 @@ private void WarnRegexp(byte[]/*!*/ pattern, SourceSpan span) { } } + /// + /// The only warnings RegexpTransformer reports are about a quantifier applied directly to + /// another (`a**`, `a{2}*`, `a*{0,1}`), which needs two quantifier characters in a row. Whitespace + /// or a comment may sit between them in extended mode, so a pattern with either is always checked. + /// + private static bool MayHaveNestedQuantifier(string/*!*/ pattern) { + for (int i = 0; i < pattern.Length; i++) { + char c = pattern[i]; + if (c == '#' || Char.IsWhiteSpace(c)) { + return true; + } + if ((c == '?' || c == '*' || c == '+' || c == '}') && i + 1 < pattern.Length) { + char d = pattern[i + 1]; + if (d == '?' || d == '*' || d == '+' || d == '{') { + return true; + } + } + } + return false; + } + private Expression/*!*/ BigIntegerLiteral(BigInteger value, SourceSpan span) { if (value >= int.MinValue && value <= int.MaxValue) return Literal.Integer((int)value, span); return Literal.BigInteger(value, span); @@ -1269,6 +1348,10 @@ private Expression DefinednessGuard(Pm.PmNode/*!*/ receiver, SourceSpan span) { case Pm.AssocNode assoc: maplets.Add(new Maplet(Expr(assoc.Key), Expr(assoc.Value), Span(assoc))); break; + case Pm.AssocSplatNode splat when isKeywordArguments: + result = MergeKeywordSplat(MergeHash(result, maplets, isKeywordArguments, span) ?? new HashConstructor(new Maplet[0], true, span), + splat.Value != null ? Expr(splat.Value) : AnonymousParameter(AnonymousKeywordRestName, splat), span); + break; case Pm.AssocSplatNode splat: result = MergeHash(result, maplets, isKeywordArguments, span); // `**nil` contributes nothing (Ruby 3.4); it is only known at run time @@ -1291,9 +1374,25 @@ private Expression MergeHash(Expression result, List/*!*/ maplets, bool if (maplets.Count == 0) return result; var ctor = new HashConstructor(maplets.ToArray(), isKeywordArguments, span); maplets.Clear(); + if (result != null && isKeywordArguments) { + return MergeKeywordSplat(result, ctor, span); + } return result == null ? (Expression)ctor : new MethodCall(result, "merge", new Arguments(ctor), span); } + /// + /// `hash.merge(splatted || {})` for keyword arguments, where the hash is the call's own: a + /// Hash (or nil) is copied into it in place, and only anything else takes the merge, which + /// applies #to_hash - see KeywordSplatMerge. + /// + private Expression/*!*/ MergeKeywordSplat(Expression/*!*/ hash, Expression/*!*/ splatted, SourceSpan span) { + var hashTemp = NewTemp(hash, span, out var hashAssignment); + var splatTemp = NewTemp(splatted, span, out var splatAssignment); + return new KeywordSplatMerge(hashAssignment, splatAssignment, + new MethodCall(hashTemp, "merge", new Arguments( + new OrExpression(splatTemp, new HashConstructor(new Maplet[0], span), span)), span), span); + } + // ---- calls ---- private Expression/*!*/ Call(Pm.CallNode/*!*/ node, SourceSpan span) { @@ -1427,24 +1526,36 @@ private Block OptionalBlock(Pm.PmNode block) { /// private Arguments/*!*/ BuildArguments(Pm.PmNode argumentsNode, ref Block block) { if (argumentsNode == null) return new Arguments(); - var exprs = new List(); - foreach (var arg in ((Pm.ArgumentsNode)argumentsNode).Arguments) { + var arguments = ((Pm.ArgumentsNode)argumentsNode).Arguments; + // each argument maps to exactly one expression + var exprs = new Expression[arguments.Length]; + for (int i = 0; i < arguments.Length; i++) { + var arg = arguments[i]; if (arg is Pm.ForwardingArgumentsNode) { var rest = CurrentScope.ResolveVariable(ForwardingRestName); var fwdBlock = CurrentScope.ResolveVariable(ForwardingBlockName); if (rest == null) throw Unsupported(arg); - exprs.Add(new SplattedArgument(rest)); + exprs[i] = new SplattedArgument(rest); if (block == null && fwdBlock != null) { block = new BlockReference(fwdBlock, Span(arg)); } + } else if (arg is Pm.KeywordHashNode single && single.Elements.Length == 1 && + single.Elements[0] is Pm.AssocSplatNode singleSplat) { + // `f(**opts)`: a Hash (or nil) is splatted as it is; only anything else goes + // through the `{}.merge(opts)` HashExpression would build, for #to_hash + var span = Span(arg); + var temp = NewTemp(singleSplat.Value != null ? Expr(singleSplat.Value) : AnonymousParameter(AnonymousKeywordRestName, singleSplat), + span, out var assignment); + exprs[i] = new KeywordArgumentSplat(assignment, new MethodCall(new HashConstructor(new Maplet[0], true, span), "merge", + new Arguments(new OrExpression(temp, new HashConstructor(new Maplet[0], span), span)), span)); } else if (IsSplattedKeywordHash(arg)) { // `f(**opts)` must pass nothing when opts is empty - see KeywordArgumentSplat. - exprs.Add(new KeywordArgumentSplat(Argument(arg))); + exprs[i] = new KeywordArgumentSplat(Argument(arg)); } else { - exprs.Add(Argument(arg)); + exprs[i] = Argument(arg); } } - return new Arguments(exprs.ToArray()); + return new Arguments(exprs); } /// @@ -1716,6 +1827,18 @@ private Parameters BlockParameters(Pm.PmNode parametersNode, bool autoSplat, out } parameters = BuildParameters(parametersNode, false, true, out prologue); } + if (node.DeferredBodyLoader != null) { + // A body that holds nothing reported at parse time is mapped when first asked for. + // What mapping it now would have set _usesBlock to the skim has noted. + if ((node.BodyContents & PrismLoader.ContentsEager) == 0) { + var deferred = new DeferredMethodBody(this, node, scope, span, prologue, _zsuperArguments); + return new MethodDefinition(scope, target, node.Name, parameters, deferred.Materialize, span) { + UsesBlock = _usesBlock || (node.BodyContents & PrismLoader.ContentsUsesBlock) != 0 + }; + } + node.Body = node.DeferredBodyLoader.LoadDeferredBody(node); + node.DeferredBodyLoader = null; + } var body = DefinitionBody(node.Body, span, prologue); return new MethodDefinition(scope, target, node.Name, parameters, body, span) { UsesBlock = _usesBlock }; } finally { @@ -1727,6 +1850,57 @@ private Parameters BlockParameters(Pm.PmNode parametersNode, bool autoSplat, out } } + /// + /// A method body left in prism's buffer, mapped the first time the method's AST body is asked + /// for - normally when the method is first called and compiled. It is mapped as Def would have + /// mapped it: in the method's own scope, after its parameters, with the same super arguments. + /// + private sealed class DeferredMethodBody { + private readonly PrismAstBridge/*!*/ _bridge; + private readonly Pm.DefNode/*!*/ _node; + private readonly LexicalScope/*!*/ _scope; + private readonly SourceSpan _span; + private readonly Statements _prologue; + private readonly Arguments _zsuperArguments; + + public DeferredMethodBody(PrismAstBridge/*!*/ bridge, Pm.DefNode/*!*/ node, LexicalScope/*!*/ scope, SourceSpan span, + Statements prologue, Arguments zsuperArguments) { + _bridge = bridge; + _node = node; + _scope = scope; + _span = span; + _prologue = prologue; + _zsuperArguments = zsuperArguments; + } + + public Body/*!*/ Materialize() { + return _bridge.MaterializeBody(_node, _scope, _span, _prologue, _zsuperArguments); + } + } + + private Body/*!*/ MaterializeBody(Pm.DefNode/*!*/ node, LexicalScope/*!*/ scope, SourceSpan span, Statements prologue, + Arguments zsuperArguments) { + // the bridge keeps state while it maps; methods of one file may be first called on different threads + lock (this) { + Pm.PmNode bodyNode = node.DeferredBodyLoader.LoadDeferredBody(node); + var enclosingUsesBlock = _usesBlock; + var enclosingZSuperArguments = _zsuperArguments; + _scopes.Push(scope); + _usesBlock = false; + _zsuperArguments = zsuperArguments; + try { + var body = DefinitionBody(bodyNode, span, prologue); + // the skim has to have seen what mapping the body sees + System.Diagnostics.Debug.Assert(_usesBlock == ((node.BodyContents & PrismLoader.ContentsUsesBlock) != 0)); + return body; + } finally { + _scopes.Pop(); + _usesBlock = enclosingUsesBlock; + _zsuperArguments = enclosingZSuperArguments; + } + } + } + /// /// MRI rejects a yield whose nearest enclosing method-or-class body is a class, module or /// singleton class body, or the top level. Blocks and lambdas are transparent. @@ -2124,33 +2298,30 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List // Everything below consumes ?args? destructively, so a parameterless `super` // inside the body would find nothing left to forward. Keep the list as it // arrived - trailing keyword hash included - and let SuperCall splat that. - var superArgs = CurrentScope.AddVariable("?superargs?", span); - statements.Add(new SimpleAssignmentExpression(superArgs, - new MethodCall(args, "dup", null, span), null, span)); + // Only a method needs it: a `super` in a block forwards the arguments of the + // method the block is in (SuperCall reads gen.CurrentMethod.Parameters). + LocalVariable superArgs = null; + if (isMethod) { + superArgs = CurrentScope.AddVariable("?superargs?", span); + statements.Add(new SimpleAssignmentExpression(superArgs, + SplatParameterRead.Copy(args, span), null, span)); + } // A trailing Hash is how a keyword call arrives here, since keywords have no // calling-convention slot of their own. But when a block auto-splats a single // Array argument no keywords were passed, so the Hash stays positional // (Ruby 3 keyword separation). var popKeywords = new SimpleAssignmentExpression(kwVar, - new ConditionalExpression( - new KeywordArgumentsTest(new MethodCall(args, "last", null, span), span), - new MethodCall(args, "pop", null, span), - new HashConstructor(new Maplet[0], span), span), - null, span); + SplatParameterRead.PopKeywords(args, span), null, span); int formalCount = node.Requireds.Length + node.Optionals.Length + postCount + (node.Rest != null ? 1 : 0); if (autoSplat && formalCount > 1) { + // the Array is copied: taking it apart must not empty the caller's array statements.Add(new IfExpression( - new AndExpression( - new MethodCall(new MethodCall(args, "size", null, span), "==", - new Arguments(Literal.Integer(1, span)), span), - CaseEqual(new ConstantVariable("Array", span), - new MethodCall(args, "[]", new Arguments(Literal.Integer(0, span)), span), span), - span), + SplatParameterRead.IsAutoSplat(args, span), MakeStatements( new SimpleAssignmentExpression(args, - new MethodCall(args, "[]", new Arguments(Literal.Integer(0, span)), span), null, span), + SplatParameterRead.AutoSplat(args, span), null, span), new SimpleAssignmentExpression(kwVar, new HashConstructor(new Maplet[0], span), null, span)), new List { new ElseIfClause(null, MakeStatements(popKeywords), span) }, span)); @@ -2179,26 +2350,18 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List var optParam = (Pm.OptionalParameterNode)opt; var local = CurrentScope.ResolveOrAddVariable(optParam.Name, Span(opt)); statements.Add(new SimpleAssignmentExpression(local, - new ConditionalExpression( - new MethodCall(new MethodCall(args, "size", null, span), ">", - new Arguments(Literal.Integer(postCount, span)), span), - new MethodCall(args, "shift", null, span), - Expr(optParam.Value), span), + SplatParameterRead.ShiftOptional(args, postCount, Expr(optParam.Value), span), null, span)); } if (node.Rest is Pm.RestParameterNode rest && (rest.Name != null || isMethod)) { var local = CurrentScope.ResolveOrAddVariable(rest.Name ?? Symbols.RestArgsLocal, Span(node.Rest)); statements.Add(new SimpleAssignmentExpression(local, - new MethodCall(args, "shift", new Arguments( - new MethodCall(new MethodCall(args, "size", null, span), "-", - new Arguments(Literal.Integer(postCount, span)), span)), span), + SplatParameterRead.ShiftRest(args, postCount, span), null, span)); } else if (node.Rest != null && postCount > 0) { // anonymous rest still has to consume what the posts do not take - statements.Add(new MethodCall(args, "shift", new Arguments( - new MethodCall(new MethodCall(args, "size", null, span), "-", - new Arguments(Literal.Integer(postCount, span)), span)), span)); + statements.Add(SplatParameterRead.ShiftRest(args, postCount, span)); } foreach (var post in node.Posts) { @@ -2230,7 +2393,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List /// take; handing one to SimpleAssignmentExpression trips its Debug.Assert. /// private Expression/*!*/ AssignNext(Pm.PmNode/*!*/ target, LocalVariable/*!*/ args, SourceSpan span) { - var shift = new MethodCall(args, "shift", null, span); + var shift = SplatParameterRead.Shift(args, span); var lhs = Target(target); return lhs is CompoundLeftValue compound ? (Expression)new DestructuringAssignmentExpression(compound, shift, span) @@ -2254,9 +2417,13 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List var prologue = new Statements(); // blocks and lambdas pass nil for parameters the caller omitted, so the - // optional-parameter default above does not fire for them - prologue.Add(new SimpleAssignmentExpression(kwVar, - new HashConstructor(new Maplet[0], true, span), "||", span)); + // optional-parameter default above does not fire for them. A method gets the + // default whenever the caller omitted it, so a nil there is one the caller passed + // positionally - `m(nil)' - which the separation check below has to refuse. + if (!isMethod) { + prologue.Add(new SimpleAssignmentExpression(kwVar, + new HashConstructor(new Maplet[0], true, span), "||", span)); + } if (strict) { foreach (var check in KeywordSeparationChecks(kwVar, mandatory, true, span)) { @@ -2331,27 +2498,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List /// took the cap off, which MRI reports as "expected N+". /// private Expression/*!*/ ArityCheck(LocalVariable/*!*/ args, int minimum, int maximum, SourceSpan span) { - Expression condition = new MethodCall(new MethodCall(args, "size", null, span), "<", - new Arguments(Literal.Integer(minimum, span)), span); - - if (maximum >= 0) { - condition = new OrExpression(condition, - new MethodCall(new MethodCall(args, "size", null, span), ">", - new Arguments(Literal.Integer(maximum, span)), span), span); - } - - string expected = maximum < 0 ? minimum + "+" : - minimum == maximum ? minimum.ToString() : minimum + ".." + maximum; - - Expression message = new MethodCall( - new MethodCall(new StringLiteral("wrong number of arguments (given ", _encoding, span), "+", - new Arguments(new MethodCall(new MethodCall(args, "size", null, span), "to_s", null, span)), span), - "+", new Arguments(new StringLiteral(", expected " + expected + ")", _encoding, span)), span); - - return new IfExpression(condition, new Statements( - new MethodCall(null, "raise", new Arguments(new Expression[] { - new ConstantVariable("ArgumentError", span), message - }), span)), new List(), span); + return SplatParameterRead.CheckArity(args, minimum, maximum, span); } private Expression/*!*/ RaiseArityError(int given, int expected, SourceSpan span) { @@ -2368,6 +2515,7 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List var prologue = new List(); var names = new List(); var requiredNames = new List(); + var optionalNames = new List(); foreach (var keyword in node.Keywords) { var kwSpan = Span(keyword); @@ -2377,18 +2525,16 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List requiredNames.Add(required.Name); var local = CurrentScope.ResolveOrAddVariable(required.Name, kwSpan); prologue.Add(new SimpleAssignmentExpression(local, - new MethodCall(kwVar, "[]", new Arguments(new SymbolLiteral(required.Name, _encoding, kwSpan)), kwSpan), + KeywordParameterRead.Value(kwVar, required.Name, names.Count - 1, null, _encoding, kwSpan), null, kwSpan)); break; } case Pm.OptionalKeywordParameterNode optKeyword: { names.Add(optKeyword.Name); + optionalNames.Add(optKeyword.Name); var local = CurrentScope.ResolveOrAddVariable(optKeyword.Name, kwSpan); prologue.Add(new SimpleAssignmentExpression(local, - new ConditionalExpression( - new MethodCall(kwVar, "key?", new Arguments(new SymbolLiteral(optKeyword.Name, _encoding, kwSpan)), kwSpan), - new MethodCall(kwVar, "[]", new Arguments(new SymbolLiteral(optKeyword.Name, _encoding, kwSpan)), kwSpan), - Expr(optKeyword.Value), kwSpan), + KeywordParameterRead.Value(kwVar, optKeyword.Name, names.Count - 1, Expr(optKeyword.Value), _encoding, kwSpan), null, kwSpan)); break; } @@ -2401,21 +2547,17 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List var kwSpan = Span(node.KeywordRest); var restLocal = CurrentScope.ResolveOrAddVariable(keywordRest.Name ?? AnonymousKeywordRestName, kwSpan); prologue.Add(new SimpleAssignmentExpression(restLocal, - new MethodCall(kwVar, "dup", null, kwSpan), null, kwSpan)); - foreach (var name in names) { - prologue.Add(new MethodCall(restLocal, "delete", - new Arguments(new SymbolLiteral(name, _encoding, kwSpan)), kwSpan)); - } + KeywordParameterRead.Rest(kwVar, names.ToArray(), _encoding, kwSpan), null, kwSpan)); } // MRI reports every missing keyword in one error, and does so before complaining // about keywords it does not know; a signature with **rest knows them all. // // Both errors are spelled with an array difference, which allocates three arrays and - // builds a set to say "nothing is wrong" - on every call. Each one therefore sits - // behind a test that costs nothing and can only be true when there is nothing to + // builds a set to say "nothing is wrong" - on every call. So they sit behind one + // runtime call that allocates nothing and is true exactly when there is nothing to // report; the array difference runs only to word the error. - var checks = new List(); + var checks = new Statements(); if (requiredNames.Count > 0) { // Every required keyword is there, or something is missing and the list says what. Expression allPresent = null; @@ -2449,8 +2591,18 @@ private Arguments ZSuperArguments(Pm.ParametersNode/*!*/ node, List new Arguments(SymbolArray(names, span)), span), "unknown keyword: ", "unknown keywords: ", span)), null, span)); } - checks.AddRange(prologue); - return checks; + + var result = new List(); + if (checks.Count > 0) { + var ordered = new List(requiredNames); + ordered.AddRange(optionalNames); + result.Add(new UnlessExpression( + KeywordParameterRead.Match(kwVar, ordered.ToArray(), requiredNames.Count, + node.KeywordRest is Pm.KeywordRestParameterNode, _encoding, span), + checks, null, span)); + } + result.AddRange(prologue); + return result; } private Expression/*!*/ SymbolArray(List/*!*/ names, SourceSpan span) { diff --git a/Src/Prism/PrismLoader.cs b/Src/Prism/PrismLoader.cs index c384bf515..e54fe7c57 100644 --- a/Src/Prism/PrismLoader.cs +++ b/Src/Prism/PrismLoader.cs @@ -42,15 +42,57 @@ public sealed partial class PrismLoader { private uint _cpoolBase; private string[] _constants; - private PrismLoader(byte[]/*!*/ buffer) { + // Lazy method bodies: a DefNode's body is skimmed rather than built (prism prefixes each + // DefNode with its length for this) and loaded from the buffer when it is asked for. + private readonly bool _lazyDefBodies; + + // What the body being skimmed holds, as Contents* bits. + private uint _contents; + + /// The method uses its block: a yield, or a super that passes the block on. + public const uint ContentsUsesBlock = 1; + + /// + /// Something the AST bridge reports while it maps it - a warning or an error that belongs to + /// parse time - so a body holding it is not put off: `defined?' (void-context warning), a + /// regexp literal that may draw a nested-quantifier warning, `class << x' (yield in it is an + /// error), class and module. + /// + public const uint ContentsEager = 2; + + private PrismLoader(byte[]/*!*/ buffer, bool lazyDefBodies) { _buffer = buffer; + _lazyDefBodies = lazyDefBodies; } public static PrismParseResult/*!*/ LoadParse(byte[]/*!*/ serialized) { - var loader = new PrismLoader(serialized); + return LoadParse(serialized, false); + } + + /// + /// With every DefNode comes back with a null Body and its + /// DeferredBodyLoader set: builds the body when it is needed. + /// + public static PrismParseResult/*!*/ LoadParse(byte[]/*!*/ serialized, bool lazyDefBodies) { + var loader = new PrismLoader(serialized, lazyDefBodies); return loader.Load(); } + /// + /// Builds the body a lazy load left in the buffer. Safe to call from any thread. + /// + public PmNode LoadDeferredBody(DefNode/*!*/ node) { + lock (this) { + int saved = _pos; + _pos = node.DeferredBodyOffset; + try { + return LoadOptionalNode(); + } finally { + _pos = saved; + } + } + } + private PrismParseResult/*!*/ Load() { if (_buffer.Length < 8 || _buffer[0] != 'P' || _buffer[1] != 'R' || _buffer[2] != 'I' || _buffer[3] != 'S' || _buffer[4] != 'M') { throw new InvalidOperationException("Invalid prism serialization (bad magic)"); @@ -211,6 +253,56 @@ private string LoadOptionalConstant() { return result; } + // ---- skimming (see SkimNode) ---- + + private void SkimOptionalNode() { + if (_buffer[_pos] == 0) { + _pos++; + return; + } + SkimNode(); + } + + private void SkimNodeList() { + uint count = LoadVarUInt(); + for (uint i = 0; i < count; i++) SkimNode(); + } + + private void SkipString() { + int length = (int)LoadVarUInt(); + _pos += length; + } + + private void SkipVarUIntList() { + uint count = LoadVarUInt(); + for (uint i = 0; i < count; i++) LoadVarUInt(); + } + + /// + /// A regexp literal's source. RegexpTransformer only warns about a quantifier applied directly + /// to another, which takes two quantifier characters in a row; extended mode lets whitespace or a + /// comment sit between them, and non-ASCII bytes may be whitespace, so any of those counts too. + /// + private void SkipRegexpSource() { + int length = (int)LoadVarUInt(); + int end = _pos + length; + for (int i = _pos; i < end; i++) { + byte c = _buffer[i]; + if (c == (byte)'#' || c == (byte)' ' || (c >= 9 && c <= 13) || c >= 0x80) { + _contents |= ContentsEager; + break; + } + if ((c == (byte)'?' || c == (byte)'*' || c == (byte)'+' || c == (byte)'}') && i + 1 < end) { + byte d = _buffer[i + 1]; + if (d == (byte)'?' || d == (byte)'*' || d == (byte)'+' || d == (byte)'{') { + _contents |= ContentsEager; + break; + } + } + } + _pos = end; + } + private BigInteger LoadInteger() { bool negative = LoadByte() != 0; uint words = LoadVarUInt(); diff --git a/Src/Prism/PrismParser.cs b/Src/Prism/PrismParser.cs index fe75b85f4..1cd276d0c 100644 --- a/Src/Prism/PrismParser.cs +++ b/Src/Prism/PrismParser.cs @@ -57,11 +57,21 @@ public static class PrismParser { public static PrismParseResult/*!*/ Parse(string/*!*/ source, string path, int startLine, IList outerLocals, int frozenStringLiteral, Encoding/*!*/ sourceEncoding, string encodingName, byte commandLine, bool mainScript, bool encodingLocked) { + return Parse(source, path, startLine, outerLocals, frozenStringLiteral, sourceEncoding, encodingName, commandLine, + mainScript, encodingLocked, false); + } + + /// + /// leaves method bodies in prism's buffer (see PrismLoader.LoadParse). + /// + public static PrismParseResult/*!*/ Parse(string/*!*/ source, string path, int startLine, IList outerLocals, + int frozenStringLiteral, Encoding/*!*/ sourceEncoding, string encodingName, byte commandLine, bool mainScript, + bool encodingLocked, bool lazyDefBodies) { byte[] sourceBytes = sourceEncoding.GetBytes(source); PrismParseResult result = PrismLoader.LoadParse( ParseSerializedBytes(sourceBytes, BuildOptionsData(path, startLine, outerLocals, frozenStringLiteral, encodingName, - commandLine, mainScript, encodingLocked))); + commandLine, mainScript, encodingLocked)), lazyDefBodies); result.DataOffset = ComputeDataOffset(result.DataLocation, sourceBytes); return result; } diff --git a/Src/Prism/generate.rb b/Src/Prism/generate.rb index cca08842c..5c0821fde 100644 --- a/Src/Prism/generate.rb +++ b/Src/Prism/generate.rb @@ -97,6 +97,16 @@ def field_name(node, field) (node["fields"] || []).each do |field| nodes_cs << " public #{cs_type(field)} #{field_name(node, field)};\n" end + if node.fetch("name") == "DefNode" + nodes_cs << <<-CS + // Set by a loader asked for lazy method bodies: Body is null and the body is still in the + // serialized buffer, for PrismLoader.LoadDeferredBody; BodyContents is what the body holds + // (PrismLoader.Contents* bits). + public IronRuby.Prism.PrismLoader DeferredBodyLoader; + public int DeferredBodyOffset; + public uint BodyContents; + CS + end nodes_cs << " }\n\n" end nodes_cs << "}\n" @@ -121,7 +131,34 @@ def field_name(node, field) CS nodes.each_with_index do |node, index| loader_cs << " case #{index + 1}: {\n" - loader_cs << " LoadRawUInt32();\n" if node.fetch("name") == "DefNode" + if node.fetch("name") == "DefNode" + # The body can be left in the buffer (lazy method bodies): everything else is read as usual. + fields = node["fields"] + body_index = fields.index { |f| f.fetch("name") == "body" } + loader_cs << " LoadRawUInt32();\n" + loader_cs << " var def = new DefNode {\n" + loader_cs << " NodeId = nodeId, StartOffset = location.Start, Length = location.Length,\n" + loader_cs << " Flags = LoadVarUInt(),\n" + fields[0...body_index].each do |field| + loader_cs << " #{field_name(node, field)} = #{cs_reader(field)},\n" + end + loader_cs << " };\n" + loader_cs << " if (_lazyDefBodies) {\n" + loader_cs << " def.DeferredBodyLoader = this;\n" + loader_cs << " def.DeferredBodyOffset = _pos;\n" + loader_cs << " _contents = 0;\n" + loader_cs << " SkimOptionalNode();\n" + loader_cs << " def.BodyContents = _contents;\n" + loader_cs << " } else {\n" + loader_cs << " def.Body = LoadOptionalNode();\n" + loader_cs << " }\n" + fields[(body_index + 1)..].each do |field| + loader_cs << " def.#{field_name(node, field)} = #{cs_reader(field)};\n" + end + loader_cs << " return def;\n" + loader_cs << " }\n" + next + end loader_cs << " return new #{node.fetch("name")} {\n" loader_cs << " NodeId = nodeId, StartOffset = location.Start, Length = location.Length,\n" loader_cs << " Flags = LoadVarUInt(),\n" @@ -131,6 +168,72 @@ def field_name(node, field) loader_cs << " };\n" loader_cs << " }\n" end +loader_cs << <<~CS + default: + throw new System.InvalidOperationException("Unknown prism node type: " + type); + } + } + + // Reads past a node without building it, noting in _contents what the bridge has to see + // at parse time (see PrismLoader.Contents*). Mirrors LoadNode field for field. + private void SkimNode() { + byte type = LoadByte(); + LoadVarUInt(); // node id + LoadLocation(); + switch (type) { +CS + +def cs_skimmer(field) + case field.fetch("type") + when "node" then "SkimNode();" + when "node?" then "SkimOptionalNode();" + when "node[]" then "SkimNodeList();" + when "string" then "SkipString();" + when "constant", "constant?", "uint32" then "LoadVarUInt();" + when "constant[]" then "SkipVarUIntList();" + when "location" then "LoadLocation();" + when "location?" then "LoadOptionalLocation();" + when "uint8" then "_pos++;" + when "integer" then "LoadInteger();" + when "double" then "_pos += 8;" + end +end + +# what the bridge must not put off: bridged later, these would warn (or fail) late +eager_nodes = %w[DefinedNode SingletonClassNode ClassNode ModuleNode] +nodes.each_with_index do |node, index| + name = node.fetch("name") + fields = node["fields"] || [] + loader_cs << " case #{index + 1}: {\n" + loader_cs << " _pos += 4;\n" if name == "DefNode" + loader_cs << " LoadVarUInt(); // flags\n" + loader_cs << " _contents |= ContentsEager;\n" if eager_nodes.include?(name) + loader_cs << " _contents |= ContentsUsesBlock;\n" if name == "YieldNode" + saved = false + fields.each do |field| + fname = field.fetch("name") + if name == "DefNode" && fname == "parameters" + # a nested method's use of its block is its own; what forces the outer one eager is not + loader_cs << " uint outer = _contents;\n" + loader_cs << " _contents = 0;\n" + saved = true + end + if (name == "SuperNode" || name == "ForwardingSuperNode") && fname == "block" + # super with no block of its own passes the method's block on + loader_cs << " if (_buffer[_pos] == 0) _contents |= ContentsUsesBlock;\n" + end + if (name == "RegularExpressionNode" || name == "MatchLastLineNode") && fname == "unescaped" + loader_cs << " SkipRegexpSource();\n" + next + end + loader_cs << " #{cs_skimmer(field)}\n" + if name == "DefNode" && fname == "body" + loader_cs << " _contents = outer | (_contents & ContentsEager);\n" + end + end + loader_cs << " return;\n" + loader_cs << " }\n" +end loader_cs << <<~CS default: throw new System.InvalidOperationException("Unknown prism node type: " + type); diff --git a/Src/Ruby/Builtins/Binding.cs b/Src/Ruby/Builtins/Binding.cs index 392c93416..a2d2ad262 100644 --- a/Src/Ruby/Builtins/Binding.cs +++ b/Src/Ruby/Builtins/Binding.cs @@ -61,6 +61,7 @@ public Binding(RubyScope/*!*/ localScope) public Binding(RubyScope/*!*/ localScope, object self) { Assert.NotNull(localScope); + localScope.MarkCaptured(); _localScope = localScope; _self = self; } diff --git a/Src/Ruby/Builtins/Hash.Keywords.cs b/Src/Ruby/Builtins/Hash.Keywords.cs new file mode 100644 index 000000000..fa95e5091 --- /dev/null +++ b/Src/Ruby/Builtins/Hash.Keywords.cs @@ -0,0 +1,173 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace IronRuby.Builtins { + + /// + /// What binding keyword parameters needs from the hash a keyword call passed (see + /// RubyOps.KeywordArgumentsMatch). The keys looked for are Symbols, and a Symbol is equal to + /// nothing but itself, so an entry holds one exactly when its key is that very object. A + /// keyword hash is small, and scanning a handful of entries for a reference is cheaper than + /// hashing the key and probing for it; a larger hash is probed as usual. + /// + public partial class Hash { + private const int IdentityScanLimit = 8; + + /// + /// An empty keyword-call hash with room for Symbol keys, to be + /// filled with . Binding keyword parameters only scans the + /// entries (, ), so such a hash + /// gets no bucket array unless something probes it later (LinkPendingBuckets). + /// + internal static Hash/*!*/ CreateKeywordArguments(IEqualityComparer/*!*/ comparer, int capacity) { + var result = new Hash(comparer, 0); + int size = GetPrime(capacity); + result._entries = new Entry[size]; + result._fastModMultiplier = UInt64.MaxValue / (uint)size + 1; + result.IsKeywordArguments = true; + return result; + } + + /// + /// Adds a Symbol key to a hash made by , within the + /// capacity it was made with. A Symbol is equal only to itself, so a repeated key is found by + /// identity and, as an insert would, keeps its position and takes the new value. The hash + /// code is taken now, as an insert would take it, so linking the buckets later asks nothing + /// of the keys. + /// + internal void AppendUnlinked(object/*!*/ key, object value) { + Debug.Assert(_buckets == null && _count < _entries.Length); + Entry[] entries = _entries; + for (int i = 0; i < _count; i++) { + if (entries[i].Key == key) { + entries[i].Value = value; + _stamp++; + return; + } + } + ref Entry e = ref entries[_count++]; + e.HashCode = _comparer.GetHashCode(key); + e.Next = -1; + e.Key = key; + e.Value = value; + _liveCount++; + _version++; + _stamp++; + } + + /// + /// A copy of this hash - default value or proc and comparer included, as the copy + /// constructor makes it - without the keys among , which are + /// compared by identity (see ): a **rest parameter. + /// + internal Hash/*!*/ CopyWithoutKeywords(object/*!*/[]/*!*/ names) { + var result = new Hash(_comparer, _liveCount); + result._defaultProc = _defaultProc; + result._defaultValue = DefaultValue; + result._comparesByIdentity = _comparesByIdentity; + + Entry[] entries = _entries; + int count = _count; + for (int i = _start; i < count; i++) { + if (entries[i].Next == TombstoneNext) { + continue; + } + object key = entries[i].Key; + int j = 0; + while (j < names.Length && names[j] != key) { + j++; + } + if (j == names.Length) { + result[key] = entries[i].Value; + } + } + return result; + } + + /// + /// The value stored under , which must be a key that is equal only to + /// itself (a Symbol). Entry is tried first: callers mostly pass + /// keywords in the order they are declared in. + /// + internal bool TryGetValueByIdentity(object/*!*/ key, int hint, out object value) { + Entry[] entries = _entries; + if (entries != null && _liveCount <= IdentityScanLimit) { + int count = _count; + if ((uint)hint < (uint)count && entries[hint].Key == key && entries[hint].Next != TombstoneNext) { + value = entries[hint].Value; + return true; + } + for (int i = _start; i < count; i++) { + if (entries[i].Key == key && entries[i].Next != TombstoneNext) { + value = entries[i].Value; + return true; + } + } + value = null; + return false; + } + return TryGetValue(key, out value); + } + + /// + /// True when the first of are all keys + /// of this hash and, unless , it has no key but those names. + /// The names are compared by identity, see . + /// + internal bool HasKeywords(object/*!*/[]/*!*/ names, int requiredCount, bool acceptsOthers) { + int live = _liveCount; + if (live == 0) { + return requiredCount == 0; + } + + if (live <= IdentityScanLimit && names.Length <= 64) { + Entry[] entries = _entries; + int count = _count; + ulong seen = 0; + for (int i = _start; i < count; i++) { + if (entries[i].Next == TombstoneNext) { + continue; + } + object key = entries[i].Key; + int j = 0; + while (j < names.Length && names[j] != key) { + j++; + } + if (j < names.Length) { + seen |= 1UL << j; + } else if (!acceptsOthers) { + return false; + } + } + ulong required = (requiredCount >= 64) ? ulong.MaxValue : (1UL << requiredCount) - 1; + return (seen & required) == required; + } + + int found = 0; + for (int i = 0; i < names.Length; i++) { + if (ContainsKey(names[i])) { + found++; + } else if (i < requiredCount) { + return false; + } + } + return acceptsOthers || found == live; + } + } +} diff --git a/Src/Ruby/Builtins/Hash.Storage.cs b/Src/Ruby/Builtins/Hash.Storage.cs index 09b766ed5..d7c7482de 100644 --- a/Src/Ruby/Builtins/Hash.Storage.cs +++ b/Src/Ruby/Builtins/Hash.Storage.cs @@ -89,6 +89,9 @@ private struct Entry { /// private int _start; private int _version; + // Changes with every mutation, an overwrite of an existing key's value included (which + // _version leaves alone, so that enumerators don't fail on it). See MutationStamp. + private int _stamp; private IEqualityComparer/*!*/ _comparer; private KeyCollection _keys; @@ -250,6 +253,29 @@ private void RebuildBuckets() { LinkBuckets(); } + /// + /// Builds the bucket array of a hash whose entries were laid out without one (see + /// AppendUnlinked), on the first operation that probes. The entry array is already + /// prime-sized and the multiplier set, and the array is published only once every entry is + /// linked, so a reader racing with this sees either no buckets (and links its own, writing + /// the same chains) or complete ones. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private int[]/*!*/ LinkPendingBuckets() { + Entry[] entries = _entries; + var buckets = new int[entries.Length]; + for (int i = _start; i < _count; i++) { + ref Entry e = ref entries[i]; + if (e.Next != TombstoneNext) { + int bucket = (int)FastMod((uint)e.HashCode, (uint)buckets.Length, _fastModMultiplier); + e.Next = buckets[bucket] - 1; + buckets[bucket] = i + 1; + } + } + System.Threading.Volatile.Write(ref _buckets, buckets); + return buckets; + } + private void LinkBuckets() { int[] buckets = _buckets; Entry[] entries = _entries; @@ -318,7 +344,8 @@ private ref Entry FindEntryRef(object key, out bool found) { if (entries != null) { IEqualityComparer comparer = _comparer; int hashCode = comparer.GetHashCode(key); - int i = _buckets[BucketIndex(hashCode)] - 1; + int[] buckets = _buckets ?? LinkPendingBuckets(); + int i = buckets[(int)FastMod((uint)hashCode, (uint)buckets.Length, _fastModMultiplier)] - 1; int collisions = 0; while ((uint)i < (uint)entries.Length) { ref Entry e = ref entries[i]; @@ -411,7 +438,11 @@ private bool Insert(object key, object value, int behavior) { throw new ArgumentNullException("key"); } if (_buckets == null) { - Initialize(0); + if (_entries == null) { + Initialize(0); + } else { + LinkPendingBuckets(); + } } IEqualityComparer comparer = _comparer; @@ -426,6 +457,7 @@ private bool Insert(object key, object value, int behavior) { if (existing.HashCode == hashCode && comparer.Equals(existing.Key, key)) { if (behavior == InsertOverwrite) { existing.Value = value; + _stamp++; return false; } if (behavior == InsertThrow) { @@ -455,6 +487,7 @@ private bool Insert(object key, object value, int behavior) { b = index + 1; _liveCount++; _version++; + _stamp++; return true; } @@ -476,8 +509,11 @@ public bool Remove(object key, out object value) { throw new ArgumentNullException("key"); } if (_buckets == null) { - value = null; - return false; + if (_entries == null) { + value = null; + return false; + } + LinkPendingBuckets(); } IEqualityComparer comparer = _comparer; @@ -503,6 +539,7 @@ public bool Remove(object key, out object value) { found.Value = null; _liveCount--; _version++; + _stamp++; // Removing the newest entry needs no tombstone at all; dropping it (and any // tombstones it was sitting on) keeps push/pop patterns from growing the // entry array. Removing the oldest one moves the enumeration start instead, @@ -543,13 +580,16 @@ public bool Remove(object key, out object value) { public void Clear() { if (_count > 0) { - Array.Clear(_buckets, 0, _buckets.Length); + if (_buckets != null) { + Array.Clear(_buckets, 0, _buckets.Length); + } Array.Clear(_entries, 0, _count); _count = 0; _liveCount = 0; _start = 0; } _version++; + _stamp++; } #endregion @@ -611,6 +651,28 @@ bool ICollection>.Remove(KeyValuePair + /// Changes with every mutation of the hash, value overwrites included: an iteration that + /// took a snapshot of the entries can use it for as long as this stays the same. + /// + public int MutationStamp { + get { return _stamp; } + } + + /// Keys and values in insertion order, in one pass over the entries. + public void CopyKeysAndValues(object[]/*!*/ keys, object[]/*!*/ values) { + Entry[] entries = _entries; + int count = _count; + int index = 0; + for (int i = _start; i < count; i++) { + ref Entry e = ref entries[i]; + if (e.Next != TombstoneNext) { + keys[index] = e.Key; + values[index++] = e.Value; + } + } + } + public void CopyTo(KeyValuePair[]/*!*/ array, int index) { CheckCopyToArgs(array != null ? array.Length : 0, array == null, index); Entry[] entries = _entries; diff --git a/Src/Ruby/Builtins/MatchData.cs b/Src/Ruby/Builtins/MatchData.cs index 4bc9c9986..acbcfe5fd 100644 --- a/Src/Ruby/Builtins/MatchData.cs +++ b/Src/Ruby/Builtins/MatchData.cs @@ -55,6 +55,11 @@ public partial class MatchData : IDuplicable, IRubyObjectState { // #string answers the same frozen String every time it is called, so it is made once. private MutableString _frozenInput; + // GroupCount, worked out once per match: it is asked for by every group access, and + // telling a hidden group from a Ruby one parses group names. Zero means not known yet + // (there is always group 0). + private int _groupCount; + public MutableString/*!*/ OriginalString { get { return _originalString; } } public RubyRegex Regexp { get { return _regexp; } } @@ -77,12 +82,16 @@ public MutableString/*!*/ FrozenOriginalString { /// public int GroupCount { get { + int count = _groupCount; + if (count > 0) { + return count; + } var groups = _match.Groups; - int count = groups.Count; + count = groups.Count; while (count > 1 && IsHiddenGroup(groups[count - 1])) { count--; } - return count; + return _groupCount = count; } } @@ -166,6 +175,7 @@ object IDuplicable.Duplicate(RubyContext/*!*/ context, bool copySingletonMembers public void InitializeFrom(MatchData/*!*/ other) { _match = other._match; + _groupCount = 0; _originalString = other._originalString; _encodedInput = other._encodedInput; _encoding = other._encoding; @@ -284,6 +294,26 @@ public int[] GetGroupOffsets(int groupIndex, bool inBytes) { return OffsetsOf(_match.Groups[groupIndex], inBytes); } + /// + /// What answers in bytes, without an array: false + /// when the group did not take part in the match. For a scanner that asks for an offset + /// pair on every token. + /// + public bool TryGetGroupByteOffsets(int groupIndex, out int start, out int end) { + if (groupIndex != 0) { // group 0 always exists + RequireExistingGroup(groupIndex); + } + // group 0 is the Match itself, which spares the group collection + Group group = (groupIndex == 0) ? _match : _match.Groups[groupIndex]; + if (!group.Success) { + start = end = 0; + return false; + } + start = ToByteIndex(group.Index); + end = ToByteIndex(group.Index + group.Length); + return true; + } + public int[] GetGroupOffsets(string/*!*/ name, bool inBytes) { if (!HasNamedGroup(name)) { throw RubyExceptions.CreateIndexError("undefined group name reference: {0}", name); diff --git a/Src/Ruby/Builtins/Proc.cs b/Src/Ruby/Builtins/Proc.cs index d4d202eb2..01ba5c279 100644 --- a/Src/Ruby/Builtins/Proc.cs +++ b/Src/Ruby/Builtins/Proc.cs @@ -50,7 +50,13 @@ public enum ProcKind { Lambda } - public partial class Proc : IDuplicable { + /// + /// A Proc is also a : a literal block passed to a call is a new Proc + /// that only that call converts, so the Proc itself stands for the converting frame (see + /// and ) + /// instead of a separate RuntimeFlowControl object allocated for every such call. + /// + public partial class Proc : RuntimeFlowControl, IDuplicable { // Self object captured by the block definition, if any. // Although we could load self from scope in Ruby defined blocks, we cannot do so when we don't have a scope. private readonly object _self; @@ -61,20 +67,64 @@ public partial class Proc : IDuplicable { private readonly BlockDispatcher/*!*/ _dispatcher; // TODO: can we remove _kind and use _method and Converter fields intead? - private ProcKind _kind; - private RubyLambdaMethodInfo _method; + // The ProcKind in the low bits, and SelfConverterFlag. + private int _kind; + private const int KindMask = 3; - // The frame that converted this block to a proc: - internal RuntimeFlowControl Converter { get; set; } + // The proc is its own converter: a call it was the literal block of converted it (see Converter). + private const int SelfConverterFlag = 4; + + /// + /// The state few procs have, out of line: a Proc is allocated for every call with a literal block. + /// + private sealed class RareState { + public RubyLambdaMethodInfo Method; + public RefinementActivation RefinementOverride; + public string SymbolName; + } + + private RareState _rare; + + private RareState/*!*/ Rare { + get { return _rare ?? (_rare = new RareState()); } + } + + /// + /// The frame that converted this block to a proc. No field of its own: a proc is only ever active as a + /// frame (RuntimeFlowControl) while it is its own converter, so the base class's field holds any other + /// converter, and SelfConverterFlag says the converter is this proc - active or not. + /// + internal RuntimeFlowControl Converter { + get { + var converter = _activeFlowControlScope; + return (converter != null) ? converter : ((_kind & SelfConverterFlag) != 0) ? this : null; + } + set { + if (value == this) { + _kind |= SelfConverterFlag; + } else { + _kind &= ~SelfConverterFlag; + _activeFlowControlScope = value; + } + } + } + + /// + /// The call this literal block is passed to converts it: the proc becomes that call's active frame. + /// + internal void ConvertAsFrame() { + _kind = (int)ProcKind.Proc | SelfConverterFlag; + _activeFlowControlScope = this; + } public ProcKind Kind { - get { return _kind; } + get { return (ProcKind)(_kind & KindMask); } // friend: RuntimeFlowControl - internal set { _kind = value; } + internal set { _kind = (_kind & ~KindMask) | (int)value; } } internal RubyLambdaMethodInfo Method { - get { return _method; } + get { var r = _rare; return (r != null) ? r.Method : null; } } public BlockDispatcher/*!*/ Dispatcher { @@ -95,13 +145,19 @@ public RubyScope/*!*/ LocalScope { /// it is being defined in, and only for the duration of the block - which is a lexical extent that /// exists only at runtime, so it rides on the Proc rather than on a scope object. /// - internal RefinementActivation RefinementOverride; + internal RefinementActivation RefinementOverride { + get { var r = _rare; return (r != null) ? r.RefinementOverride : null; } + set { if (value != null || _rare != null) { Rare.RefinementOverride = value; } } + } /// /// The symbol this proc came from, for a proc Symbol#to_proc built; null otherwise. /// Proc#to_s prints it as "(&:name)". /// - public string SymbolName { get; internal set; } + public string SymbolName { + get { var r = _rare; return (r != null) ? r.SymbolName : null; } + internal set { if (value != null || _rare != null) { Rare.SymbolName = value; } } + } public string SourcePath { get { return _dispatcher.SourcePath; } @@ -116,10 +172,12 @@ public int SourceLine { internal Proc(ProcKind kind, object self, RubyScope/*!*/ scope, BlockDispatcher/*!*/ dispatcher) { Assert.NotNull(scope, dispatcher); Debug.Assert(dispatcher.Method != null); - _kind = kind; + _kind = (int)kind; _self = self; _scope = scope; _dispatcher = dispatcher; + // (no RubyScope.MarkCaptured here: a literal block makes a Proc over its enclosing scope on every + // call, and a block with a nested block literal never reuses its scope - see BlockDefinition) } protected Proc(Proc/*!*/ proc) @@ -142,7 +200,9 @@ protected Proc(Proc/*!*/ proc) public Proc/*!*/ ToLambda(RubyLambdaMethodInfo method) { Proc result = new Proc(this); result.Kind = ProcKind.Lambda; - result._method = method; + if (method != null) { + result.Rare.Method = method; + } return result; } @@ -245,7 +305,7 @@ public object Call(Proc procArg, object arg1) { var blockParam = RubyOps.CreateBfcForProcCall(this); try { // lambda calls are weird: - var result = (_kind == ProcKind.Lambda) ? + var result = (Kind == ProcKind.Lambda) ? RubyOps.YieldNoAutoSplat1(arg1, procArg, _self, blockParam) : RubyOps.Yield1(arg1, procArg, _self, blockParam); @@ -398,6 +458,7 @@ public object CallN(Proc procArg, object[]/*!*/ args) { // MRI's symbol proc is a lambda: it reports #lambda? true, arity -2 and // [[:req], [:rest]] for #parameters + scope.MarkCaptured(); return new Proc(ProcKind.Lambda, scope.SelfObject, scope, procDispatcher) { SymbolName = methodName }; } @@ -434,7 +495,7 @@ public object CallN(Proc procArg, object[]/*!*/ args) { #endregion public override string/*!*/ ToString() { - return String.Format("{0}: {1}:{2}", _kind, SourcePath ?? "(unknown)", SourceLine); + return String.Format("{0}: {1}:{2}", Kind, SourcePath ?? "(unknown)", SourceLine); } } } diff --git a/Src/Ruby/Builtins/RubyArray.cs b/Src/Ruby/Builtins/RubyArray.cs index fcaea669c..609f4a383 100644 --- a/Src/Ruby/Builtins/RubyArray.cs +++ b/Src/Ruby/Builtins/RubyArray.cs @@ -573,6 +573,50 @@ public void RemoveAt(int index) { RemoveRange(index, 1); } + // Taking an array apart from the ends without RemoveRange's reallocation, which it does to + // shrink a small array on nearly every removal. For the splat of a lowered parameter list, + // which is consumed and then dropped (RubyOps.ShiftSplatArgument and its neighbours). + + /// Removes and returns the first element; nil if there is none. + internal object TakeFirst() { + if (_count == 0) { + return null; + } + Mutate(); + object result = _content[_start]; + _content[_start] = null; + _start++; + _count--; + ObjectInvariant(); + return result; + } + + /// Removes the first elements and returns them as a new array. + internal RubyArray/*!*/ TakeFirst(int count) { + ContractUtils.RequiresArrayRange(_count, 0, count, "index", "count"); + var result = new RubyArray(this, 0, count); + if (count > 0) { + Mutate(); + Utils.Fill(_content, _start, null, count); + _start += count; + _count -= count; + ObjectInvariant(); + } + return result; + } + + /// Removes and returns the last element, of which there must be one. + internal object TakeLast() { + Debug.Assert(_count > 0); + Mutate(); + int last = _start + _count - 1; + object result = _content[last]; + _content[last] = null; + _count--; + ObjectInvariant(); + return result; + } + public bool Remove(object item) { int index = IndexOf(item); if (index >= 0) { diff --git a/Src/Ruby/Builtins/RubyMethod.cs b/Src/Ruby/Builtins/RubyMethod.cs index 7cd421d2d..a051fa822 100644 --- a/Src/Ruby/Builtins/RubyMethod.cs +++ b/Src/Ruby/Builtins/RubyMethod.cs @@ -158,6 +158,7 @@ object IDuplicable.Duplicate(RubyContext/*!*/ context, bool copySingletonMembers // method, both of which are lambda behaviour, so MRI's Method#to_proc answers // #lambda? with true. Its self is the method's receiver rather than whatever self // the to_proc call happened under, which is what #binding hands back. + scope.MarkCaptured(); return new Proc(ProcKind.Lambda, _target, scope, _procDispatcher); } @@ -167,6 +168,33 @@ internal void BuildInvoke(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ Assert.NotNull(metaBuilder, args); Debug.Assert(args.Target == this); + // A call to these methods depends on the method, the name it is called by and the arguments (self included), not on + // which Method object holds them. Each `instance_method(:m).bind(obj).call` makes a new Method, so a rule that took + // the Method and its receiver as constants was bound and compiled anew for every call of such a site. + if (_methodMissingName == null && IsReceiverIndependent(_info)) { + var method = Ast.Convert(args.TargetExpression, typeof(RubyMethod)); + var receiver = Ast.Property(method, TargetProperty); + + // The receiver's CLR type is restricted here: the overload resolver takes self for granted. + metaBuilder.AddRestriction(Ast.AndAlso( + Ast.TypeIs(args.TargetExpression, typeof(RubyMethod)), + Ast.AndAlso( + Ast.AndAlso( + Ast.Equal(Ast.Property(method, InfoProperty), AstUtils.Constant(_info, typeof(RubyMemberInfo))), + Ast.Equal(Ast.Property(method, NameProperty), AstUtils.Constant(_name)) + ), + Ast.AndAlso( + Ast.Not(Ast.Property(method, IsMethodMissingProperty)), + (_target == null) ? (Expression)Ast.Equal(receiver, AstUtils.Constant(null)) : Ast.TypeEqual(receiver, _target.GetType()) + ) + ) + )); + + args.SetTarget(receiver, _target); + _info.BuildCall(metaBuilder, args, _name); + return; + } + // first argument must be this method: metaBuilder.AddRestriction(Ast.Equal(args.TargetExpression, AstUtils.Constant(this))); @@ -192,6 +220,16 @@ internal void BuildInvoke(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ } } + private static bool IsReceiverIndependent(RubyMemberInfo/*!*/ info) { + Type type = info.GetType(); + return type == typeof(RubyLibraryMethodInfo) || type == typeof(RubyMethodInfo) || type == typeof(RubyLambdaMethodInfo); + } + + private static readonly System.Reflection.PropertyInfo/*!*/ InfoProperty = typeof(RubyMethod).GetProperty("Info"); + private static readonly System.Reflection.PropertyInfo/*!*/ NameProperty = typeof(RubyMethod).GetProperty("Name"); + private static readonly System.Reflection.PropertyInfo/*!*/ TargetProperty = typeof(RubyMethod).GetProperty("Target"); + private static readonly System.Reflection.PropertyInfo/*!*/ IsMethodMissingProperty = typeof(RubyMethod).GetProperty("IsMethodMissing"); + #endregion #region Debug View diff --git a/Src/Ruby/Builtins/RubyModule.MethodLookupCache.cs b/Src/Ruby/Builtins/RubyModule.MethodLookupCache.cs index 0fcb1a3b3..81da0843d 100644 --- a/Src/Ruby/Builtins/RubyModule.MethodLookupCache.cs +++ b/Src/Ruby/Builtins/RubyModule.MethodLookupCache.cs @@ -89,15 +89,27 @@ internal MethodLookupTable(int version, Dictionary/* /// All-visible lookup of , ignoring refinements. Lock-free once warm for a class. /// public CachedMethodLookup/*!*/ GetMethodLookup(string/*!*/ name) { + int version; + return GetMethodLookup(name, out version); + } + + /// + /// , and the method version the answer belongs to, for a caller + /// that keeps the answer itself (a call site's storage): it stays right while .Method + /// equals . -1 when the answer must not be kept. + /// + public CachedMethodLookup/*!*/ GetMethodLookup(string/*!*/ name, out int version) { var table = Volatile.Read(ref _methodLookups); CachedMethodLookup entry; if (table != null && table.Version == Version.Method && table.Entries.TryGetValue(name, out entry)) { + version = table.Version; return entry; } - return FillMethodLookup(name); + return FillMethodLookup(name, out version); } - private CachedMethodLookup/*!*/ FillMethodLookup(string/*!*/ name) { + private CachedMethodLookup/*!*/ FillMethodLookup(string/*!*/ name, out int publishedVersion) { + publishedVersion = -1; using (Context.ClassHierarchyLocker()) { int version = Version.Method; var entry = ResolveMethodLookupNoLock(name); @@ -125,6 +137,7 @@ internal MethodLookupTable(int version, Dictionary/* entries[name] = entry; Volatile.Write(ref _methodLookups, new MethodLookupTable(version, entries)); + publishedVersion = version; return entry; } } diff --git a/Src/Ruby/Builtins/RubyModule.cs b/Src/Ruby/Builtins/RubyModule.cs index c36e09122..ef7f35563 100644 --- a/Src/Ruby/Builtins/RubyModule.cs +++ b/Src/Ruby/Builtins/RubyModule.cs @@ -91,13 +91,19 @@ public partial class RubyModule : IDuplicable, IRubyObject { // interlocked internal static int _globalMethodVersion = 0; + // interlocked; bumped only where a class's method version is (IncrementMethodVersion) + private static int _methodTableVersion = 0; + /// - /// Read by JIT-compiled code as its one global guard: every method table change anywhere - /// bumps it, so a specialization that snapshots it is invalidated by any def, undef, - /// alias, include, extend or singleton-method definition. + /// Read by JIT-compiled code as its one global guard: every method table change that can + /// alter a resolution call sites depend on bumps it, so a specialization that snapshots it is + /// invalidated by any def, undef, alias, include, extend or singleton-method definition that + /// overrides something. Creating a class or module does not: a new module changes no existing + /// class's resolution, and Class.new, Struct.new or a fresh singleton class after a method + /// had been specialized would otherwise send that method down the generic path for good. /// public static int GlobalMethodVersion { - get { return System.Threading.Volatile.Read(ref _globalMethodVersion); } + get { return System.Threading.Volatile.Read(ref _methodTableVersion); } } internal static int _globalModuleId = 0; @@ -986,6 +992,7 @@ internal void AddDependentClass(RubyClass/*!*/ dependentClass) { private void IncrementMethodVersion() { if (IsClass) { Version.Method = Interlocked.Increment(ref _globalMethodVersion); + Interlocked.Increment(ref _methodTableVersion); } } @@ -2939,10 +2946,32 @@ public static bool IsModuleType(Type/*!*/ type) { } // thread-safe: + // thread-safe without the class hierarchy lock: is_a?, kind_of? and Module#=== call this on + // every check, and the lock plus the closure ForEachAncestor needs were most of their cost. + // The walk only reads _prepends and _mixins, which are replaced, never mutated in place, under + // the lock, and the superclass chain; an include/prepend running concurrently is seen or not, + // just as with the lock, which only orders this check before or after it. public bool HasAncestor(RubyModule/*!*/ module) { - using (Context.ClassHierarchyLocker()) { - return HasAncestorNoLock(module); + RubyClass cls = this as RubyClass; + if (cls == null) { + return IsDeclaredAncestor(module); + } + for (RubyClass c = cls; c != null; c = c.SuperClass) { + if (c.IsDeclaredAncestor(module)) return true; } + return false; + } + + // The modules ForEachDeclaredAncestor visits, read without the lock (see HasAncestor). + private bool IsDeclaredAncestor(RubyModule/*!*/ module) { + if (this == module) return true; + foreach (RubyModule p in _prepends) { + if (p == module) return true; + } + foreach (RubyModule m in _mixins) { + if (m == module) return true; + } + return false; } public bool HasAncestorNoLock(RubyModule/*!*/ module) { diff --git a/Src/Ruby/Builtins/RubyRegex.cs b/Src/Ruby/Builtins/RubyRegex.cs index 19b3894ea..b2032cacd 100644 --- a/Src/Ruby/Builtins/RubyRegex.cs +++ b/Src/Ruby/Builtins/RubyRegex.cs @@ -28,6 +28,9 @@ public partial class RubyRegex : IEquatable, IDuplicable, IRubyObject // 1.9: correctly encoded, switched to characters // 1.8: k-coded binary data, if _options specify encoding, or raw binary data otherwise. private MutableString/*!*/ _pattern; + // HasNonAsciiEscape(_pattern) - the pattern is frozen, so it is worked out once: -1 not yet, + // 0 none, 1 a byte escape, 2 a \u escape. Encoding and IsFixedEncoding ask on every match. + private int _nonAsciiEscape = -1; private RubyRegexOptions _options; private bool _hasGAnchor; @@ -50,6 +53,11 @@ public partial class RubyRegex : IEquatable, IDuplicable, IRubyObject private Regex _cachedAnchoredRegex; private Regex _cachedAnchoredSource; + // How many matches _cachedRegex and _cachedAnchoredRegex have run while still interpreted. + // A regex that keeps being used is rebuilt with RegexOptions.Compiled (see TierUp). + private int _cachedRegexUses; + private int _cachedAnchoredUses; + // Ruby 1.8: match operations use KCODE encoding so we need to remember the one for which we have cached CLR Regex. private RubyRegexOptions _cachedKCode; @@ -160,6 +168,7 @@ public void Set(MutableString/*!*/ pattern, RubyRegexOptions options) { _options = options & ~RubyRegexOptions.Once; _cachedRegex = null; _cachedAstralRegex = null; + _nonAsciiEscape = -1; RubyEncoding encoding = RubyEncoding.GetRegexEncoding(options); if (encoding != null) { @@ -225,6 +234,15 @@ private void RequireMatchableEncoding(MutableString/*!*/ input) { /// a whole pair. /// private Regex/*!*/ Transform(ref RubyEncoding encoding, MutableString/*!*/ input, int start, out string strInput, out bool astral) { + return Transform(ref encoding, input, start, out strInput, out astral, true); + } + + /// + /// is false when the caller is not going to match with the + /// regex it gets back (MatchWindow, which derives an anchored one from it), so that only + /// the regex that actually runs works its way up to being compiled. + /// + private Regex/*!*/ Transform(ref RubyEncoding encoding, MutableString/*!*/ input, int start, out string strInput, out bool astral, bool countUse) { ContractUtils.RequiresNotNull(input, "input"); RequireMatchableEncoding(input); @@ -285,7 +303,7 @@ private void RequireMatchableEncoding(MutableString/*!*/ input) { astral = input.HasSurrogatePairs(); } - return astral ? TransformAstralPattern(encoding, kc) : TransformPattern(encoding, kc); + return astral ? TransformAstralPattern(encoding, kc) : TransformPattern(encoding, kc, countUse); } private Regex/*!*/ TransformAstralPattern(RubyEncoding encoding, RubyRegexOptions kc) { @@ -315,11 +333,19 @@ private void RequireMatchableEncoding(MutableString/*!*/ input) { } private Regex/*!*/ TransformPattern(RubyEncoding encoding, RubyRegexOptions kc) { + return TransformPattern(encoding, kc, false); + } + + private Regex/*!*/ TransformPattern(RubyEncoding encoding, RubyRegexOptions kc, bool countUse) { // We can reuse cached CLR regex if it was created for the same k-coding and the // same timeout: TimeSpan timeout = EffectiveTimeout; - if (_cachedRegex != null && kc == _cachedKCode && timeout == _cachedTimeout) { - return _cachedRegex; + Regex cached = _cachedRegex; + if (cached != null && kc == _cachedKCode && timeout == _cachedTimeout) { + if (countUse && (cached.Options & RegexOptions.Compiled) == 0 && ++_cachedRegexUses >= CompileThreshold) { + _cachedRegex = cached = TierUp(cached); + } + return cached; } string pattern = GetClrPatternSource(encoding, kc); @@ -333,10 +359,32 @@ private void RequireMatchableEncoding(MutableString/*!*/ input) { _cachedTimeout = timeout; _cachedKCode = kc; + _cachedRegexUses = 0; _cachedRegex = result; return result; } + /// + /// Matches a regex runs before it is compiled. The CLR interprets a Regex unless told to + /// compile it, and compiling costs a millisecond or more - about what the compiled code + /// then saves over ten thousand matches of a typical pattern - so only a regex that has + /// already shown it is hot pays for it: one in a loop over a large input, a tokenizer's. + /// + private const int CompileThreshold = 10000; + + /// + /// The same CLR regex compiled to IL. Compiled and interpreted regexes are the same + /// engine and the same pattern tree and match identically; only the speed differs. If it + /// cannot be built for any reason, the interpreted one is kept. + /// + private static Regex/*!*/ TierUp(Regex/*!*/ regex) { + try { + return new Regex(regex.ToString(), regex.Options | RegexOptions.Compiled, regex.MatchTimeout); + } catch (Exception) { + return regex; + } + } + /// /// Searches the pattern for hexadecimal and octal character escapes that represent a non-ASCII character. /// @@ -435,14 +483,14 @@ public RubyEncoding/*!*/ Encoding { // above 7 bits without /n is "invalid multibyte escape" in MRI; we don't raise, // and the pattern's own encoding is as good an answer as any.) bool isUnicode; - if (HasNonAsciiEscape(_pattern, out isUnicode)) { + if (PatternHasNonAsciiEscape(out isUnicode)) { return isUnicode ? RubyEncoding.UTF8 : _pattern.Encoding; } return RubyEncoding.Ascii; } - if (encodingOptions == RubyRegexOptions.FIXED && _pattern.IsAscii() && !HasNonAsciiEscape(_pattern)) { + if (encodingOptions == RubyRegexOptions.FIXED && _pattern.IsAscii() && !PatternHasNonAsciiEscape(out _)) { return RubyEncoding.Ascii; } @@ -570,6 +618,16 @@ public static bool HasBackReference(MutableString/*!*/ pattern) { /// written with byte escapes would silently lose its encoding when unioned with a plain /// ASCII string. /// + private bool PatternHasNonAsciiEscape(out bool isUnicode) { + int cached = _nonAsciiEscape; + if (cached < 0) { + cached = HasNonAsciiEscape(_pattern, out isUnicode) ? (isUnicode ? 2 : 1) : 0; + _nonAsciiEscape = cached; + } + isUnicode = cached == 2; + return cached != 0; + } + public static bool HasNonAsciiEscape(MutableString/*!*/ pattern) { bool isUnicode; return HasNonAsciiEscape(pattern, out isUnicode); @@ -656,7 +714,8 @@ public bool IsFixedEncoding { // /n on its own says nothing - /abc/n matches any ASCII compatible string. It // pins the encoding only once the pattern really compiles to non-ASCII bytes, // which it does for a byte escape as much as for a literal non-ASCII character. - return !_pattern.IsAscii() || HasNonAsciiEscape(_pattern); + bool isUnicode; + return !_pattern.IsAscii() || PatternHasNonAsciiEscape(out isUnicode); } return (_options & (RubyRegexOptions.EUC | RubyRegexOptions.SJIS | RubyRegexOptions.UTF8 | RubyRegexOptions.FixedEncoding)) != 0 @@ -894,7 +953,7 @@ public MatchData MatchWindow(MutableString/*!*/ input, int start, bool anchored, string str; RubyEncoding kcode = null; bool astral; - Regex regex = Transform(ref kcode, input, 0, out str, out astral); + Regex regex = Transform(ref kcode, input, 0, out str, out astral, !anchored); // Under a k-coding the offsets are bytes, and "start" only indexes the converted // string directly when that coding spells every character in one byte - which the @@ -902,7 +961,7 @@ public MatchData MatchWindow(MutableString/*!*/ input, int start, bool anchored, int offset = 0; if (kcode != null && !kcode.IsSingleByteCharacterSet) { kcode = null; - regex = Transform(ref kcode, input, start, out str, out astral); + regex = Transform(ref kcode, input, start, out str, out astral, !anchored); offset = (start < 0) ? start + input.GetByteCount() : start; start = 0; } @@ -925,6 +984,103 @@ public MatchData MatchWindow(MutableString/*!*/ input, int start, bool anchored, return MatchData.Create(match, input, freezeInput, str, kcode, offset, this, astral); } + /// + /// A match MatchWindowBounds found, whose MatchData has not been made yet. Most scanning + /// operations only want to know where the match is, and building the CLR Match with its + /// groups and the MatchData around it costs more than finding the match does. + /// makes the MatchData MatchWindow would have made, by running the + /// same CLR regex over the same immutable text again - so a later change to the subject + /// does not change the groups, as it does not in MRI either. + /// + public struct PendingMatch { + internal RubyRegex _owner; + internal Regex _regex; + internal MutableString _input; + internal string _text; + internal RubyEncoding _kcode; + internal int _start; + internal MatchData _data; + + public bool IsEmpty { + get { return _owner == null; } + } + + public MatchData/*!*/ Complete() { + if (_data != null) { + return _data; + } + Match match = _regex.Match(_text, _start, _text.Length - _start); + Debug.Assert(match.Success); + return MatchData.Create(match, _input, false, _text, _kcode, 0, _owner, false); + } + } + + /// + /// MatchWindow without a frozen copy of the input, answering the byte offsets of the whole + /// match and a for its groups. Where one CLR character stands + /// for one byte of the input - a binary or single-byte k-coding, or a subject whose + /// characters are all single bytes - the match is found without building any match + /// objects at all; anything else goes through MatchWindow. + /// + public bool MatchWindowBounds(MutableString/*!*/ input, int start, bool anchored, + out int matchStart, out int matchEnd, out PendingMatch pending) { + + matchStart = matchEnd = 0; + pending = default(PendingMatch); + + string str; + RubyEncoding kcode = null; + bool astral; + Regex regex = Transform(ref kcode, input, 0, out str, out astral, !anchored); + + bool bytesAreChars = (kcode != null) ? kcode.IsSingleByteCharacterSet : input.DetectSingleByteCharacters(); + if (astral || !bytesAreChars) { + MatchData data = MatchWindow(input, start, anchored, false); + if (data == null || !data.TryGetGroupByteOffsets(0, out matchStart, out matchEnd)) { + return false; + } + pending._owner = this; + pending._data = data; + return true; + } + + if (str == null) { + return false; + } + if (start < 0) { + start += str.Length; + } + if (start < 0 || start > str.Length) { + return false; + } + + if (anchored) { + regex = AnchorAtSearchStart(regex); + } + + // A span cut at "start" is the window Match(str, start, length) searches: \A, ^, \G + // and lookbehind all stop at its first character. + foreach (ValueMatch match in regex.EnumerateMatches(str.AsSpan(start))) { + int clrStart = start + match.Index; + // MatchData's offsets: the CLR offset is the byte offset, capped at the subject + matchStart = Math.Min(clrStart, str.Length); + matchEnd = Math.Min(clrStart + match.Length, str.Length); + if (kcode == null) { + int byteCount = input.GetByteCount(); + matchStart = Math.Min(matchStart, byteCount); + matchEnd = Math.Min(matchEnd, byteCount); + } + pending._owner = this; + pending._regex = regex; + pending._input = input; + pending._text = str; + pending._kcode = kcode; + pending._start = start; + return true; + } + return false; + } + /// /// The same pattern with a \G in front of it, which in a forward CLR match stands for the /// position the search was told to start at. The body goes in a non-capturing group so that @@ -938,8 +1094,12 @@ public MatchData MatchWindow(MutableString/*!*/ input, int start, bool anchored, /// the caller falls back to searching: slower, never wrong. /// private Regex/*!*/ AnchorAtSearchStart(Regex/*!*/ regex) { - if (_cachedAnchoredSource == regex && _cachedAnchoredRegex != null) { - return _cachedAnchoredRegex; + Regex cached = _cachedAnchoredRegex; + if (_cachedAnchoredSource == regex && cached != null) { + if ((cached.Options & RegexOptions.Compiled) == 0 && ++_cachedAnchoredUses >= CompileThreshold) { + _cachedAnchoredRegex = cached = TierUp(cached); + } + return cached; } Regex result; @@ -954,6 +1114,7 @@ public MatchData MatchWindow(MutableString/*!*/ input, int start, bool anchored, } _cachedAnchoredSource = regex; + _cachedAnchoredUses = 0; _cachedAnchoredRegex = result; return result; } @@ -1080,7 +1241,24 @@ private static Match LastMatch(Regex/*!*/ regex, string/*!*/ input, int start, b RubyEncoding kcode = null; bool astral; Regex regex = Transform(ref kcode, input, 0, out str, out astral); - IList matches = astral ? MatchesOverPairs(regex, str) : (IList)regex.Matches(str); + IList matches; + if (astral) { + matches = MatchesOverPairs(regex, str); + } else { + // Match and NextMatch are what Regex.Matches is defined as; asking for the first + // one directly answers "no match" - the usual outcome of a #gsub over a short + // string - without the collection. + Match match = regex.Match(str); + if (!match.Success) { + return Array.Empty(); + } + var list = new List(); + do { + list.Add(match); + match = match.NextMatch(); + } while (match.Success); + matches = list; + } var result = new MatchData[matches.Count]; if (result.Length > 0 && inputMayMutate) { diff --git a/Src/Ruby/Builtins/RubyStruct.cs b/Src/Ruby/Builtins/RubyStruct.cs index cd1e5d806..a2816d35a 100644 --- a/Src/Ruby/Builtins/RubyStruct.cs +++ b/Src/Ruby/Builtins/RubyStruct.cs @@ -333,5 +333,18 @@ private void RequireNotFrozen() { public int ItemCount { get { return _data.Length; } } + + /// + /// Whether the struct's class was built by Struct.new(..., keyword_init: true): ruby4.rb's + /// Struct.new keeps the option in the class's @__keyword_init__, which is what the class's + /// #keyword_init? answers. A subclass has none of its own. + /// + public bool IsKeywordInit { + get { + var data = ImmediateClass.GetNonSingletonClass().TryGetInstanceData(); + object value; + return data != null && data.TryGetInstanceVariable("@__keyword_init__", out value) && RubyOps.IsTrue(value); + } + } } } diff --git a/Src/Ruby/Builtins/RubySymbol.cs b/Src/Ruby/Builtins/RubySymbol.cs index 14ca5271c..34072a582 100644 --- a/Src/Ruby/Builtins/RubySymbol.cs +++ b/Src/Ruby/Builtins/RubySymbol.cs @@ -26,6 +26,11 @@ public class RubySymbol : IComparable, IComparable, IEquatable different hash? different symbols? - return _string.GetHashCode(); + return _hashCode; } public bool Equals(RubySymbol other) { diff --git a/Src/Ruby/Builtins/TracePoint.cs b/Src/Ruby/Builtins/TracePoint.cs index 0d057d1bc..6a8874975 100644 --- a/Src/Ruby/Builtins/TracePoint.cs +++ b/Src/Ruby/Builtins/TracePoint.cs @@ -212,6 +212,11 @@ public static void Deliver(TraceEventInfo/*!*/ info) { return; } + // a hook can keep the scope (TracePoint#binding): it is not reused for another block call + if (info.Scope != null) { + info.Scope.MarkCaptured(); + } + var tracePoints = _enabledTracePoints; Thread thread = null; diff --git a/Src/Ruby/Compiler/Ast/BlockDefinition.cs b/Src/Ruby/Compiler/Ast/BlockDefinition.cs index 8d735f1e0..7d8e7bd76 100644 --- a/Src/Ruby/Compiler/Ast/BlockDefinition.cs +++ b/Src/Ruby/Compiler/Ast/BlockDefinition.cs @@ -190,6 +190,27 @@ public BlockDefinition(LexicalScope/*!*/ definedScope, Parameters parameters, St MSA.Expression statements = gen.TransformStatements(_body, ResultOperation.Return); bool hasNestedReturn = gen.CurrentBlock.HasNestedReturn; + // A body that never uses its scope - { }, { |x| x } - has no use for the RubyBlockScope + // every invocation would allocate, except to hand to the TracePoint hooks, which only + // evaluate their scope while a TracePoint listens: there it is made on demand. + MSA.Expression lazyScope = null, filterScope = scopeVariable; + if (!hasNestedReturn && !gen.TraceEnabled && !gen.CompilerOptions.IsEval && !scope.HasClosures && IsScopeFreeBody()) { + var names = scope.GetVariableNamesExpression(); + Func makeScope = () => Methods.CreateBlockScopeLazy.OpCall( + scope.LocalsTupleVariable, names, blockParameter, selfParameter, EnterInterpretedFrameExpression.Instance + ); + var lazy = Ast.Coalesce(scopeVariable, Ast.Assign(scopeVariable, makeScope())); + var rewriter = new LazyScopeRewriter(scopeVariable, lazy); + var lazyParamInit = rewriter.Visit(paramInit); + var lazyStatements = rewriter.Visit(statements); + if (!rewriter.NeedsScope) { + paramInit = lazyParamInit; + statements = lazyStatements; + lazyScope = lazy; + filterScope = Ast.Coalesce(scopeVariable, makeScope()); + } + } + var bodyTry = AstUtils.Try( statements ).Catch(blockUnwinder = Ast.Parameter(typeof(BlockUnwinder), "#u"), @@ -221,25 +242,64 @@ public BlockDefinition(LexicalScope/*!*/ definedScope, Parameters parameters, St Ast.Label(redoLabel), bodyTry ).Filter(filterVariable = Ast.Parameter(typeof(Exception), "#e"), - Methods.FilterBlockException.OpCall(scopeVariable, filterVariable) + Methods.FilterBlockException.OpCall(filterScope, filterVariable) ).Finally( traceReturn, leaveNestedReturn, Ast.Empty() ); - body = scope.CreateScope( - scopeVariable, - Methods.CreateBlockScope.OpCall(new AstExpressions { + // A body that needs its scope but defines no closure over it can reuse the scope of the + // previous call of the block, unless something captured that (see RubyScope.IsCaptured). + // A nested block literal is a Proc over the scope on every call: nothing to reuse. + bool reuseScope = lazyScope == null && !hasNestedReturn && !gen.CurrentBlock.HasNestedBlock && + !gen.TraceEnabled && !gen.CompilerOptions.IsEval; + + body = gen.Traceable + ? new TraceReturnExpression(lazyScope ?? scopeVariable, gen.AddReturnTarget(body), TraceEvents.BReturn | TraceEvents.Return, gen.SourcePath, Location.End.Line) + : gen.AddReturnTarget(body); + + if (reuseScope) { + // the body stays in statement position: as a call argument it would have to be spilled + var result = scope.DefineHiddenVariable("#result", typeof(object)); + bool keepLocals = scope.HasReusableLocals; + MSA.Expression leave = Methods.LeaveReusableBlockScope.OpCall(scopeVariable, AstUtils.Constant(keepLocals)); + if (keepLocals) { + leave = Ast.IfThen(leave, scope.MakeClearLocals()); + } + body = Ast.Block( + Ast.Assign(result, AstUtils.Convert(body, typeof(object))), + leave, + result + ); + } + + MSA.Expression enterScope; + if (reuseScope) { + // the scope to reuse is taken first, its tuple from it (see RubyOps.TakeReusableBlockScope) + var reusableScope = scope.DefineHiddenVariable("#reusable", typeof(RubyBlockScope)); + enterScope = Methods.CreateReusableBlockScope.OpCall(new AstExpressions { + scope.MakeReusableLocalsStorage(blockParameter, reusableScope), + scope.GetVariableNamesExpression(), + blockParameter, + selfParameter, + EnterInterpretedFrameExpression.Instance, + reusableScope + }); + } else { + enterScope = (lazyScope != null ? Methods.CreateBlockScopeIfTraced : Methods.CreateBlockScope).OpCall(new AstExpressions { scope.MakeLocalsStorage(), scope.GetVariableNamesExpression(), blockParameter, selfParameter, EnterInterpretedFrameExpression.Instance - }), - gen.Traceable - ? new TraceReturnExpression(scopeVariable, gen.AddReturnTarget(body), TraceEvents.BReturn | TraceEvents.Return, gen.SourcePath, Location.End.Line) - : gen.AddReturnTarget(body) + }); + } + + body = scope.CreateScope( + scopeVariable, + enterScope, + body ); gen.LeaveBlockDefinition(); @@ -295,6 +355,82 @@ public BlockDefinition(LexicalScope/*!*/ definedScope, Parameters parameters, St return AstGenerator.FormatBlockLabel(enclosing, levels); } + /// + /// Nothing in the parameters or the body needs the block's scope at runtime: plain + /// parameters, and statements that only read and assign the block's own locals, literals and + /// self. Every call site takes the scope, and so do constants, instance and global variables, + /// so this is the call-free body: { }, { |x| x }, { |a, b| a }. + /// The transformed body is still checked (LazyScopeRewriter); this is the cheap filter. + /// + private bool IsScopeFreeBody() { + if (_parameters.Optional.Length > 0 || _parameters.Unsplat != null || _parameters.Block != null || HasPostParameters) { + return false; + } + var signature = _parameters.Signature; + if (signature != null && (signature.TakesKeywords || signature.RefusesKeywords)) { + return false; + } + foreach (var parameter in _parameters.Mandatory) { + if (!(parameter is LocalVariable) && !(parameter is Placeholder)) { + return false; + } + } + foreach (var statement in _body) { + if (!IsScopeFree(statement)) { + return false; + } + } + return true; + } + + private static bool IsScopeFree(Expression/*!*/ node) { + if (node is LocalVariable || node is Literal || node is SelfReference) { + return true; + } + var assignment = node as SimpleAssignmentExpression; + return assignment != null && assignment.Operation == null && assignment.Left is LocalVariable && IsScopeFree(assignment.Right); + } + + /// + /// Points the TracePoint hooks of a scope-free body at the scope made on demand. Any other use + /// of the scope, or a node it cannot look into, means the body needs its scope after all. + /// + private sealed class LazyScopeRewriter : MSA.ExpressionVisitor { + private readonly MSA.ParameterExpression/*!*/ _scope; + private readonly MSA.Expression/*!*/ _lazyScope; + private int _inTraceHook; + + internal bool NeedsScope; + + internal LazyScopeRewriter(MSA.ParameterExpression/*!*/ scope, MSA.Expression/*!*/ lazyScope) { + _scope = scope; + _lazyScope = lazyScope; + } + + protected override MSA.Expression/*!*/ VisitParameter(MSA.ParameterExpression/*!*/ node) { + if (node == _scope) { + if (_inTraceHook > 0) { + return _lazyScope; + } + NeedsScope = true; + } + return node; + } + + protected override MSA.Expression/*!*/ VisitExtension(MSA.Expression/*!*/ node) { + if (node is TraceLineExpression) { + _inTraceHook++; + try { + return base.VisitExtension(node); + } finally { + _inTraceHook--; + } + } + NeedsScope = true; + return node; + } + } + private MSA.Expression/*!*/ GetParameterAccess(AstParameters/*!*/ parameters, MSA.Expression paramsArray, int i) { return (paramsArray != null) ? (MSA.Expression)Ast.ArrayAccess(paramsArray, AstUtils.Constant(i)) : parameters[HiddenParameterCount + i]; } diff --git a/Src/Ruby/Compiler/Ast/Definitions/MethodDefinition.cs b/Src/Ruby/Compiler/Ast/Definitions/MethodDefinition.cs index e3112e38e..19a8be5f9 100644 --- a/Src/Ruby/Compiler/Ast/Definitions/MethodDefinition.cs +++ b/Src/Ruby/Compiler/Ast/Definitions/MethodDefinition.cs @@ -79,6 +79,21 @@ public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, strin _parameters = parameters ?? Parameters.Empty; } + /// + /// A method whose body the front end builds when it is first asked for - normally when the method + /// is first called. Building it adds the body's locals to . + /// + public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, string/*!*/ name, Parameters parameters, + Func/*!*/ bodyFactory, SourceSpan location) + : base(definedScope, bodyFactory, location) { + Assert.NotNull(name); + Debug.Assert(parameters == null || parameters.Unsplat == null || parameters.Unsplat is LocalVariable); + + _target = target; + _name = name; + _parameters = parameters ?? Parameters.Empty; + } + private ScopeBuilder/*!*/ DefineLocals(out AstParameters/*!*/ parameters) { // Method signature: // , &, , , , * @@ -156,10 +171,21 @@ public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, strin internal MSA.LambdaExpression/*!*/ TransformBody(AstGenerator/*!*/ gen, RubyScope/*!*/ declaringScope, RubyModule/*!*/ declaringModule, StrongBox definitionRefinements) { - return TransformBody(gen, Ast.Constant(declaringScope, typeof(RubyScope)), Ast.Constant(declaringModule, typeof(RubyModule)), + return TransformBody(gen, ExactConstant(declaringScope, typeof(RubyScope)), ExactConstant(declaringModule, typeof(RubyModule)), QualifyFrameLabel(_name, declaringModule), definitionRefinements); } + /// + /// A compiled lambda keeps its object constants in an object[] and casts each one back on + /// every load. Typed as their base class the declaring scope (a top-level, module or method + /// scope) and module (a class, mostly) miss the inline exact-type check and go to the + /// runtime's class-cast helper, twice per call of the method; typed as what they are, the + /// cast is one compare, and the up-cast to the parameter type is free. + /// + private static MSA.Expression/*!*/ ExactConstant(object/*!*/ value, Type/*!*/ type) { + return AstUtils.Convert(Ast.Constant(value, value.GetType()), type); + } + /// /// The body with the declaring scope and module given as expressions rather than baked-in /// objects. Ahead-of-time compilation (Util/aot) uses it: there, both are only known when @@ -169,6 +195,9 @@ public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, strin string/*!*/ frameLabel, StrongBox definitionRefinements) { string encodedName = RubyStackTraceBuilder.EncodeMethodName(frameLabel, gen.SourcePath, Location, gen.DebugMode); + // a body built on demand adds its locals to the scope: build it before they are allocated + Body methodBody = Body; + AstParameters parameters; ScopeBuilder scope = DefineLocals(out parameters); @@ -232,7 +261,7 @@ public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, strin // see RubyScope.GetActiveRefinements: MSA.Expression recordRefinements = (definitionRefinements != null) ? (MSA.Expression)Ast.Assign( - Ast.Field(scopeVariable, typeof(RubyMethodScope).GetField("DefinitionRefinements")), + Ast.Property(scopeVariable, typeof(RubyMethodScope).GetProperty("DefinitionRefinements")), Ast.Constant(definitionRefinements, typeof(StrongBox))) : AstUtils.Empty(); @@ -251,7 +280,7 @@ public MethodDefinition(LexicalScope/*!*/ definedScope, Expression target, strin profileStart, _parameters.TransformOptionalsInitialization(gen), traceCall, - Body.TransformResult(gen, ResultOperation.Return) + methodBody.TransformResult(gen, ResultOperation.Return) ).Filter(unwinder = Ast.Parameter(typeof(Exception), "#u"), Methods.IsMethodUnwinderTargetFrame.OpCall(scopeVariable, unwinder), Ast.Return(gen.ReturnLabel, Methods.GetMethodUnwinderReturnValue.OpCall(unwinder)) ).Finally( diff --git a/Src/Ruby/Compiler/Ast/Expressions/DefinitionExpression.cs b/Src/Ruby/Compiler/Ast/Expressions/DefinitionExpression.cs index bd1aa6999..a1c18c5a2 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/DefinitionExpression.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/DefinitionExpression.cs @@ -20,7 +20,9 @@ namespace IronRuby.Compiler.Ast { public abstract class DefinitionExpression : Expression { private readonly LexicalScope/*!*/ _definedScope; - private readonly Body/*!*/ _body; + private volatile Body _body; + // A body the front end builds when it is first asked for (see MethodDefinition), or null. + private System.Func _bodyFactory; public LexicalScope/*!*/ DefinedScope { get { @@ -30,6 +32,18 @@ public LexicalScope/*!*/ DefinedScope { public Body/*!*/ Body { get { + return _body ?? BuildBody(); + } + } + + private Body/*!*/ BuildBody() { + lock (this) { + if (_body == null) { + var body = _bodyFactory(); + ContractUtils.RequiresNotNull(body, "body"); + _body = body; + _bodyFactory = null; + } return _body; } } @@ -42,5 +56,14 @@ protected DefinitionExpression(LexicalScope/*!*/ definedScope, Body/*!*/ body, S _definedScope = definedScope; _body = body; } + + protected DefinitionExpression(LexicalScope/*!*/ definedScope, System.Func/*!*/ bodyFactory, SourceSpan location) + : base(location) { + ContractUtils.RequiresNotNull(definedScope, "definedScope"); + ContractUtils.RequiresNotNull(bodyFactory, "bodyFactory"); + + _definedScope = definedScope; + _bodyFactory = bodyFactory; + } } } diff --git a/Src/Ruby/Compiler/Ast/Expressions/HashConstructor.cs b/Src/Ruby/Compiler/Ast/Expressions/HashConstructor.cs index 0c35b5ba7..b05fdbc9a 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/HashConstructor.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/HashConstructor.cs @@ -20,8 +20,10 @@ #endif using System.Collections.Generic; +using System.Runtime.CompilerServices; using Microsoft.Scripting; using Microsoft.Scripting.Utils; +using AstUtils = Microsoft.Scripting.Ast.Utils; namespace IronRuby.Compiler.Ast { using Ast = Expression; @@ -55,8 +57,38 @@ public HashConstructor(Maplet/*!*/[]/*!*/ maplets, bool isKeywordArguments, Sour } internal override MSA.Expression/*!*/ TransformRead(AstGenerator/*!*/ gen) { + if (_isKeywordArguments && _maplets.Length <= 3 && HasSymbolLiteralKeys()) { + return TransformKeywordArguments(gen); + } var items = gen.TransformMapletsToExpressions(_maplets); return _isKeywordArguments ? gen.MakeKeywordArgumentsHashOpCall(items) : gen.MakeHashOpCall(items); } + + private bool HasSymbolLiteralKeys() { + foreach (var maplet in _maplets) { + if (!(maplet.Key is SymbolLiteral)) { + return false; + } + } + return true; + } + + // `f(a: x, b: y)`: the keyword hash is built by a helper of its own arity (keys and values + // still evaluated in order), see RubyOps.MakeKeywordArgumentsHash2. + private MSA.Expression/*!*/ TransformKeywordArguments(AstGenerator/*!*/ gen) { + var args = new ReadOnlyCollectionBuilder(1 + 2 * _maplets.Length); + args.Add(gen.CurrentScopeVariable); + foreach (var maplet in _maplets) { + args.Add(AstUtils.Box(maplet.Key.TransformRead(gen))); + args.Add(AstUtils.Box(maplet.Value.TransformRead(gen))); + } + switch (_maplets.Length) { + // the default of the hidden keyword parameter: no keywords passed + case 0: return Methods.MakeKeywordArgumentsHash0.OpCall(gen.CurrentScopeVariable); + case 1: return Methods.MakeKeywordArgumentsHash1.OpCall(args); + case 2: return Methods.MakeKeywordArgumentsHash2.OpCall(args); + default: return Methods.MakeKeywordArgumentsHash3.OpCall(args); + } + } } } diff --git a/Src/Ruby/Compiler/Ast/Expressions/KeywordParameterRead.cs b/Src/Ruby/Compiler/Ast/Expressions/KeywordParameterRead.cs new file mode 100644 index 000000000..cdeaf493d --- /dev/null +++ b/Src/Ruby/Compiler/Ast/Expressions/KeywordParameterRead.cs @@ -0,0 +1,162 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +#if FEATURE_CORE_DLR +using MSA = System.Linq.Expressions; +#else +using MSA = Microsoft.Scripting.Ast; +#endif + +using Microsoft.Scripting; +using Microsoft.Scripting.Utils; +using IronRuby.Builtins; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Compiler.Ast { + using Ast = MSA.Expression; + + /// + /// The parts of a keyword-parameter prologue (PrismAstBridge.BindKeywordsFrom) that read the + /// hash a keyword call passed: whether it matches the declared keywords, the value of one + /// keyword parameter, and the **rest parameter. Each one is a single runtime call (see + /// ) where the prologue used to + /// make a handful of dynamic Hash calls per keyword. + /// + public partial class KeywordParameterRead : Expression { + private enum ReadKind { + Match, + Value, + Rest + } + + private readonly ReadKind _kind; + // the hidden local holding the keyword hash + private readonly Expression/*!*/ _keywords; + private readonly string/*!*/[]/*!*/ _names; + private readonly RubyEncoding/*!*/ _encoding; + // Match: how many of the names are required; Value: where the keyword is declared + private readonly int _requiredCount; + private readonly bool _acceptsRest; + private readonly Expression _defaultValue; + + public Expression/*!*/ Keywords { + get { return _keywords; } + } + + public Expression DefaultValue { + get { return _defaultValue; } + } + + // What the method JIT (Runtime/Jit/JitKeywords.cs) needs to recognize a keyword prologue. + internal bool IsMatch { get { return _kind == ReadKind.Match; } } + internal bool IsValue { get { return _kind == ReadKind.Value; } } + internal string/*!*/[]/*!*/ Names { get { return _names; } } + internal RubyEncoding/*!*/ Encoding { get { return _encoding; } } + // Match: how many of the names are required; Value: where the keyword is declared + internal int RequiredCountOrPosition { get { return _requiredCount; } } + internal bool AcceptsRest { get { return _acceptsRest; } } + + private KeywordParameterRead(ReadKind kind, Expression/*!*/ keywords, string/*!*/[]/*!*/ names, RubyEncoding/*!*/ encoding, + int requiredCount, bool acceptsRest, Expression defaultValue, SourceSpan location) + : base(location) { + Assert.NotNull(keywords, names, encoding); + _kind = kind; + _keywords = keywords; + _names = names; + _encoding = encoding; + _requiredCount = requiredCount; + _acceptsRest = acceptsRest; + _defaultValue = defaultValue; + } + + /// + /// True when the hash holds the first of + /// and, unless , no other key. + /// + public static KeywordParameterRead/*!*/ Match(Expression/*!*/ keywords, string/*!*/[]/*!*/ names, int requiredCount, + bool acceptsRest, RubyEncoding/*!*/ encoding, SourceSpan location) { + return new KeywordParameterRead(ReadKind.Match, keywords, names, encoding, requiredCount, acceptsRest, null, location); + } + + /// + /// The value the call passed for keyword , the method's keyword number + /// . If it passed none, is evaluated + /// instead, or nil if there is none. + /// + public static KeywordParameterRead/*!*/ Value(Expression/*!*/ keywords, string/*!*/ name, int position, Expression defaultValue, + RubyEncoding/*!*/ encoding, SourceSpan location) { + return new KeywordParameterRead(ReadKind.Value, keywords, new[] { name }, encoding, position, false, defaultValue, location); + } + + /// + /// A copy of the hash without the declared : the **rest parameter. + /// + public static KeywordParameterRead/*!*/ Rest(Expression/*!*/ keywords, string/*!*/[]/*!*/ names, + RubyEncoding/*!*/ encoding, SourceSpan location) { + return new KeywordParameterRead(ReadKind.Rest, keywords, names, encoding, 0, true, null, location); + } + + private RubySymbol/*!*/[]/*!*/ GetSymbols(AstGenerator/*!*/ gen) { + var result = new RubySymbol[_names.Length]; + for (int i = 0; i < result.Length; i++) { + result[i] = gen.Context.CreateSymbol(_names[i], _encoding); + } + return result; + } + + private MSA.Expression/*!*/ TransformMatch(AstGenerator/*!*/ gen) { + return Methods.KeywordArgumentsMatch.OpCall( + AstUtils.Box(_keywords.TransformRead(gen)), + AstUtils.Constant(GetSymbols(gen)), + AstUtils.Constant(_requiredCount), + AstUtils.Constant(_acceptsRest) + ); + } + + internal override MSA.Expression/*!*/ TransformRead(AstGenerator/*!*/ gen) { + switch (_kind) { + case ReadKind.Match: + return AstUtils.Box(TransformMatch(gen)); + + case ReadKind.Rest: + return Methods.MakeKeywordRest.OpCall(AstUtils.Box(_keywords.TransformRead(gen)), AstUtils.Constant(GetSymbols(gen))); + + default: + // value = GetKeywordArgument(kw, :name); value == DefaultArgument ? : value + var value = gen.CurrentScope.DefineHiddenVariable("#keyword", typeof(object)); + return Ast.Condition( + Ast.Equal( + Ast.Assign(value, Methods.GetKeywordArgument.OpCall( + AstUtils.Box(_keywords.TransformRead(gen)), + AstUtils.Constant(gen.Context.CreateSymbol(_names[0], _encoding)), + AstUtils.Constant(_requiredCount) + )), + Ast.Field(null, Fields.DefaultArgument) + ), + (_defaultValue != null) ? AstUtils.Box(_defaultValue.TransformRead(gen)) : AstUtils.Constant(null, typeof(object)), + value + ); + } + } + + internal override MSA.Expression/*!*/ TransformReadBoolean(AstGenerator/*!*/ gen, bool positive) { + if (_kind == ReadKind.Match) { + var match = TransformMatch(gen); + return positive ? match : Ast.Not(match); + } + return base.TransformReadBoolean(gen, positive); + } + } +} diff --git a/Src/Ruby/Compiler/Ast/Expressions/KeywordSplatMerge.cs b/Src/Ruby/Compiler/Ast/Expressions/KeywordSplatMerge.cs new file mode 100644 index 000000000..6af5148b2 --- /dev/null +++ b/Src/Ruby/Compiler/Ast/Expressions/KeywordSplatMerge.cs @@ -0,0 +1,69 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +#if FEATURE_CORE_DLR +using MSA = System.Linq.Expressions; +#else +using MSA = Microsoft.Scripting.Ast; +#endif + +using Microsoft.Scripting; +using Microsoft.Scripting.Utils; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Compiler.Ast { + using Ast = MSA.Expression; + + /// + /// One `**h` of keyword arguments that also has `k: v` pairs or other splats, e.g. `f(k: v, **h)`: + /// h's entries go into the keyword hash built so far (see RubyOps.MergeKeywordSplat) when h is + /// a Hash or nil. Anything else takes - `hash.merge(h || {})`, which + /// applies #to_hash - as every `**h` used to, at the price of a Ruby-level Hash#merge. + /// + public partial class KeywordSplatMerge : Expression { + // `temp = ` and `temp2 = `, the temporaries Converted reads + private readonly Expression/*!*/ _hash; + private readonly Expression/*!*/ _splatted; + private readonly Expression/*!*/ _converted; + + public Expression/*!*/ Hash { + get { return _hash; } + } + + public Expression/*!*/ Splatted { + get { return _splatted; } + } + + public Expression/*!*/ Converted { + get { return _converted; } + } + + public KeywordSplatMerge(Expression/*!*/ hash, Expression/*!*/ splatted, Expression/*!*/ converted, SourceSpan location) + : base(location) { + Assert.NotNull(hash, splatted, converted); + _hash = hash; + _splatted = splatted; + _converted = converted; + } + + internal override MSA.Expression/*!*/ TransformRead(AstGenerator/*!*/ gen) { + return Ast.Coalesce( + Methods.MergeKeywordSplat.OpCall(gen.CurrentScopeVariable, + AstUtils.Box(_hash.TransformRead(gen)), AstUtils.Box(_splatted.TransformRead(gen))), + AstUtils.Box(_converted.TransformRead(gen)) + ); + } + } +} diff --git a/Src/Ruby/Compiler/Ast/Expressions/MethodCall.cs b/Src/Ruby/Compiler/Ast/Expressions/MethodCall.cs index 3cad4e8ac..be63ae060 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/MethodCall.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/MethodCall.cs @@ -96,6 +96,14 @@ public MethodCall(Expression target, string/*!*/ methodName, Arguments args, Blo Assert.NotNull(gen, transformedTarget); Assert.NotEmpty(methodName); + // -X:JIT: `m(x, k: v)' may go straight into m's specialized body (Runtime/Jit/JitKeywordCall.cs) + if (hasImplicitSelf && block == null && singleArgument == null && assignmentRhsArgument == null && node is MethodCall) { + var direct = IronRuby.Runtime.Jit.JitKeywordCall.TryTransform(gen, methodName, transformedTarget, arguments); + if (direct != null) { + return direct; + } + } + // Pass args in this order: // 1. instance // 2. block (if present) diff --git a/Src/Ruby/Compiler/Ast/Expressions/SplatParameterRead.cs b/Src/Ruby/Compiler/Ast/Expressions/SplatParameterRead.cs new file mode 100644 index 000000000..473b85af3 --- /dev/null +++ b/Src/Ruby/Compiler/Ast/Expressions/SplatParameterRead.cs @@ -0,0 +1,164 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +#if FEATURE_CORE_DLR +using MSA = System.Linq.Expressions; +#else +using MSA = Microsoft.Scripting.Ast; +#endif + +using Microsoft.Scripting; +using Microsoft.Scripting.Utils; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Compiler.Ast { + using Ast = MSA.Expression; + + /// + /// The steps of the prologue PrismAstBridge.LowerGeneralParameters emits to take apart the + /// single splat a signature mixing keywords with optional, rest or post parameters is lowered + /// to. Each is one runtime call (see + /// and its neighbours) where the prologue used to make one or more dynamic Array calls. + /// + public partial class SplatParameterRead : Expression { + private enum ReadKind { + Copy, + PopKeywords, + IsAutoSplat, + AutoSplat, + CheckArity, + Shift, + ShiftOptional, + ShiftRest + } + + private readonly ReadKind _kind; + // the hidden local holding the splat + private readonly Expression/*!*/ _arguments; + private readonly int _count1, _count2; + private readonly Expression _defaultValue; + + public Expression/*!*/ Arguments { + get { return _arguments; } + } + + public Expression DefaultValue { + get { return _defaultValue; } + } + + private SplatParameterRead(ReadKind kind, Expression/*!*/ arguments, int count1, int count2, Expression defaultValue, SourceSpan location) + : base(location) { + Assert.NotNull(arguments); + _kind = kind; + _arguments = arguments; + _count1 = count1; + _count2 = count2; + _defaultValue = defaultValue; + } + + /// A copy of the arguments as they arrived (for a parameterless `super`). + public static SplatParameterRead/*!*/ Copy(Expression/*!*/ arguments, SourceSpan location) { + return new SplatParameterRead(ReadKind.Copy, arguments, 0, 0, null, location); + } + + /// The keyword hash taken off the end of the arguments, or an empty hash. + public static SplatParameterRead/*!*/ PopKeywords(Expression/*!*/ arguments, SourceSpan location) { + return new SplatParameterRead(ReadKind.PopKeywords, arguments, 0, 0, null, location); + } + + /// True if the arguments are a single Array, which a block auto-splats. + public static SplatParameterRead/*!*/ IsAutoSplat(Expression/*!*/ arguments, SourceSpan location) { + return new SplatParameterRead(ReadKind.IsAutoSplat, arguments, 0, 0, null, location); + } + + /// A copy of the single Array argument a block auto-splats. + public static SplatParameterRead/*!*/ AutoSplat(Expression/*!*/ arguments, SourceSpan location) { + return new SplatParameterRead(ReadKind.AutoSplat, arguments, 0, 0, null, location); + } + + /// Raises ArgumentError unless there are minimum..maximum arguments (maximum < 0: no cap). + public static SplatParameterRead/*!*/ CheckArity(Expression/*!*/ arguments, int minimum, int maximum, SourceSpan location) { + return new SplatParameterRead(ReadKind.CheckArity, arguments, minimum, maximum, null, location); + } + + /// The next argument, nil if none is left. + public static SplatParameterRead/*!*/ Shift(Expression/*!*/ arguments, SourceSpan location) { + return new SplatParameterRead(ReadKind.Shift, arguments, 0, 0, null, location); + } + + /// + /// The next argument if the post parameters leave one, + /// otherwise the value of . + /// + public static SplatParameterRead/*!*/ ShiftOptional(Expression/*!*/ arguments, int postCount, Expression/*!*/ defaultValue, SourceSpan location) { + Assert.NotNull(defaultValue); + return new SplatParameterRead(ReadKind.ShiftOptional, arguments, postCount, 0, defaultValue, location); + } + + /// The arguments the post parameters leave, as an Array. + public static SplatParameterRead/*!*/ ShiftRest(Expression/*!*/ arguments, int postCount, SourceSpan location) { + return new SplatParameterRead(ReadKind.ShiftRest, arguments, postCount, 0, null, location); + } + + internal override MSA.Expression/*!*/ TransformRead(AstGenerator/*!*/ gen) { + var arguments = AstUtils.Box(_arguments.TransformRead(gen)); + switch (_kind) { + case ReadKind.Copy: + return Methods.CopySplatArguments.OpCall(arguments); + + case ReadKind.PopKeywords: + return Methods.PopKeywordArguments.OpCall(gen.CurrentScopeVariable, arguments); + + case ReadKind.IsAutoSplat: + return AstUtils.Box(Methods.IsAutoSplatArgument.OpCall(arguments)); + + case ReadKind.AutoSplat: + return Methods.AutoSplatArgument.OpCall(arguments); + + case ReadKind.CheckArity: + return Ast.Block( + Methods.CheckSplatArity.OpCall(arguments, AstUtils.Constant(_count1), AstUtils.Constant(_count2)), + AstUtils.Constant(null, typeof(object)) + ); + + case ReadKind.Shift: + return Methods.ShiftSplatArgument.OpCall(arguments); + + case ReadKind.ShiftRest: + return Methods.ShiftRestSplatArguments.OpCall(arguments, AstUtils.Constant(_count1)); + + default: + // value = ShiftOptionalSplatArgument(args, postCount); value == DefaultArgument ? : value + var value = gen.CurrentScope.DefineHiddenVariable("#optional", typeof(object)); + return Ast.Condition( + Ast.Equal( + Ast.Assign(value, Methods.ShiftOptionalSplatArgument.OpCall(arguments, AstUtils.Constant(_count1))), + Ast.Field(null, Fields.DefaultArgument) + ), + AstUtils.Box(_defaultValue.TransformRead(gen)), + value + ); + } + } + + internal override MSA.Expression/*!*/ TransformReadBoolean(AstGenerator/*!*/ gen, bool positive) { + if (_kind == ReadKind.IsAutoSplat) { + var test = Methods.IsAutoSplatArgument.OpCall(AstUtils.Box(_arguments.TransformRead(gen))); + return positive ? test : Ast.Not(test); + } + return base.TransformReadBoolean(gen, positive); + } + } +} diff --git a/Src/Ruby/Compiler/Ast/Expressions/StringConstructor.cs b/Src/Ruby/Compiler/Ast/Expressions/StringConstructor.cs index 0e1a8ec70..4bb6646c7 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/StringConstructor.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/StringConstructor.cs @@ -292,7 +292,7 @@ public bool Add(StringLiteral/*!*/ literal) { // finish trailing literals: if (concat.Count > 0) { - merged.Add(StringLiteral.Transform(concat.GetValue(), concat.Encoding)); + merged.Add(TransformLiteralPart(concat.GetValue(), concat.Encoding)); } if (merged.Count <= RubyOps.MakeStringParamCount) { @@ -307,6 +307,16 @@ public bool Add(StringLiteral/*!*/ literal) { } } + // The literal text between interpolations. Every CreateMutableString*, CreateSymbol* and + // CreateRegex* helper the parts go to copies out of them, so one instance made here serves + // every evaluation instead of a new string each time; frozen, so that a helper which ever + // kept a part rather than copying it would fail loudly instead of sharing it. + private static MSA.Expression/*!*/ TransformLiteralPart(object/*!*/ value, RubyEncoding/*!*/ encoding) { + var str = value as string; + var part = (str != null) ? MutableString.Create(str, encoding) : MutableString.CreateBinary((byte[])value, encoding); + return AstUtils.Constant(part.Freeze(), typeof(MutableString)); + } + // // Traverses expressions in "parts" and concats all contiguous literal strings/bytes. // Notes: @@ -336,7 +346,7 @@ private static bool ConcatLiteralsAndTransformRecursive(AstGenerator/*!*/ gen, L } } else { if (concat.Count > 0) { - result.Add(StringLiteral.Transform(concat.GetValue(), concat.Encoding)); + result.Add(TransformLiteralPart(concat.GetValue(), concat.Encoding)); concat.Clear(); } diff --git a/Src/Ruby/Compiler/Ast/Expressions/WhileLoopExpression.cs b/Src/Ruby/Compiler/Ast/Expressions/WhileLoopExpression.cs index 3815d1e74..caa1da82d 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/WhileLoopExpression.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/WhileLoopExpression.cs @@ -130,6 +130,9 @@ public WhileLoopExpression(Expression/*!*/ condition, bool isWhileLoop, bool isP } } osrSite.TupleArgIndex = tupleArgIndex; + // and self, which the instance variables the loop reads belong to + osrSite.SelfArgIndex = tupleArguments.Count; + tupleArguments.Add(AstUtils.Box(gen.CurrentSelfVariable)); osrArguments = Ast.NewArrayInit(typeof(object), tupleArguments); } diff --git a/Src/Ruby/Compiler/Ast/Expressions/YieldCall.cs b/Src/Ruby/Compiler/Ast/Expressions/YieldCall.cs index 4a31afddb..b37284f63 100644 --- a/Src/Ruby/Compiler/Ast/Expressions/YieldCall.cs +++ b/Src/Ruby/Compiler/Ast/Expressions/YieldCall.cs @@ -59,7 +59,7 @@ public YieldCall(Arguments args, SourceSpan location) return new AstBlock { gen.DebugMarker("#RB: yield begin"), - Ast.Assign(bfcVariable, Methods.CreateBfcForYield.OpCall(gen.MakeMethodBlockParameterRead())), + Ast.Assign(bfcVariable, Methods.ReuseBfcForYield.OpCall(bfcVariable, gen.MakeMethodBlockParameterRead())), Ast.Assign( resultVariable, diff --git a/Src/Ruby/Compiler/Ast/KeywordArgumentSplat.cs b/Src/Ruby/Compiler/Ast/KeywordArgumentSplat.cs index 4ab03a85b..3bdbea76b 100644 --- a/Src/Ruby/Compiler/Ast/KeywordArgumentSplat.cs +++ b/Src/Ruby/Compiler/Ast/KeywordArgumentSplat.cs @@ -39,11 +39,32 @@ namespace IronRuby.Compiler.Ast { /// Arguments.IndexOfSplatted see it, so no new NodeTypes entry is needed. /// public partial class KeywordArgumentSplat : SplattedArgument { + // `f(**h)` alone: Argument stores h in a temporary, and this is the hash to splat instead + // when h is neither a Hash nor nil - `{}.merge(temporary)`, which applies #to_hash. + private readonly Expression _converted; + public KeywordArgumentSplat(Expression/*!*/ hash) : base(hash) { } + /// + /// `f(**h)`: evaluates h (and keeps it for ), + /// which is splatted as it is when it is a Hash or nil - a copy is made either way - and + /// through otherwise. Building `{}.merge(h)` for every call only + /// to copy the result again cost a Ruby-level Hash#merge per call. + /// + public KeywordArgumentSplat(Expression/*!*/ hash, Expression/*!*/ converted) + : base(hash) { + _converted = converted; + } + internal override MSA.Expression/*!*/ TransformRead(AstGenerator/*!*/ gen) { + if (_converted != null) { + return MSA.Expression.Coalesce( + Methods.SplatKeywordHashIfHash.OpCall(gen.CurrentScopeVariable, AstUtils.Box(Argument.TransformRead(gen))), + Methods.SplatKeywordHash.OpCall(gen.CurrentScopeVariable, AstUtils.Box(_converted.TransformRead(gen))) + ); + } return Methods.SplatKeywordHash.OpCall(gen.CurrentScopeVariable, AstUtils.Box(Argument.TransformRead(gen))); } } diff --git a/Src/Ruby/Compiler/Ast/LeftValues/GlobalVariable.cs b/Src/Ruby/Compiler/Ast/LeftValues/GlobalVariable.cs index 6214f75d5..3ec2bf88c 100644 --- a/Src/Ruby/Compiler/Ast/LeftValues/GlobalVariable.cs +++ b/Src/Ruby/Compiler/Ast/LeftValues/GlobalVariable.cs @@ -38,11 +38,21 @@ public GlobalVariable(string/*!*/ name, SourceSpan location) } internal override MSA.Expression/*!*/ TransformReadVariable(AstGenerator/*!*/ gen, bool tryRead) { - return (tryRead ? Methods.GetGlobalVariable : Methods.ReadGlobalVariable).OpCall(gen.CurrentScopeVariable, TransformName(gen)); + if (gen.SavingToDisk) { + return (tryRead ? Methods.GetGlobalVariable : Methods.ReadGlobalVariable).OpCall(gen.CurrentScopeVariable, TransformName(gen)); + } + return (tryRead ? Methods.GetCachedGlobalVariable : Methods.ReadCachedGlobalVariable).OpCall( + gen.CurrentScopeVariable, AstUtils.Constant(new IronRuby.Runtime.GlobalVariableSite(Name)) + ); } internal override MSA.Expression/*!*/ TransformWriteVariable(AstGenerator/*!*/ gen, MSA.Expression/*!*/ rightValue) { - return Methods.SetGlobalVariable.OpCall(AstUtils.Box(rightValue), gen.CurrentScopeVariable, TransformName(gen)); + if (gen.SavingToDisk) { + return Methods.SetGlobalVariable.OpCall(AstUtils.Box(rightValue), gen.CurrentScopeVariable, TransformName(gen)); + } + return Methods.SetCachedGlobalVariable.OpCall( + AstUtils.Box(rightValue), gen.CurrentScopeVariable, AstUtils.Constant(new IronRuby.Runtime.GlobalVariableSite(Name)) + ); } internal override MSA.Expression TransformDefinedCondition(AstGenerator/*!*/ gen) { diff --git a/Src/Ruby/Compiler/Ast/ScopeBuilder.cs b/Src/Ruby/Compiler/Ast/ScopeBuilder.cs index 23d3e7c02..b6298357f 100644 --- a/Src/Ruby/Compiler/Ast/ScopeBuilder.cs +++ b/Src/Ruby/Compiler/Ast/ScopeBuilder.cs @@ -110,6 +110,15 @@ internal int LexicalDepth { get { return _lexicalScope.Depth; } } + // whether anything in the scope reads an outer scope's locals (see GetClosure) + internal bool HasClosures { + get { return _closures != null; } + } + + internal MSA.ParameterExpression/*!*/ LocalsTupleVariable { + get { return _localsTuple; } + } + internal ScopeBuilder Parent { get { return _parent; } } @@ -210,6 +219,52 @@ private int LiftedVisibleVariableCount { return result; } + /// + /// Whether a block that reuses its scope (see RubyOps.TakeReusableBlockScope) reuses the + /// scope's locals tuple too: one of up to 8 locals, cleared when the call finishes. + /// + internal bool HasReusableLocals { + get { + int count = LiftedVisibleVariableCount; + return count > 0 && count <= 8 && _parameters == null; + } + } + + /// + /// Takes the scope the last call of the block kept (into , null if there is + /// none) and then makes the locals storage: the taken scope's tuple, if the block reuses it, or a new one. + /// A tuple is only ever reused together with the scope it came from. + /// + public MSA.Expression/*!*/ MakeReusableLocalsStorage(MSA.Expression/*!*/ blockParameter, MSA.ParameterExpression/*!*/ reusableScope) { + var take = Ast.Assign(reusableScope, Methods.TakeReusableBlockScope.OpCall(blockParameter)); + if (!HasReusableLocals) { + return Ast.Block(take, MakeLocalsStorage()); + } + // (the type test only when there is a tuple to reuse: a block called once has none) + return Ast.Block( + take, + Ast.Assign(_localsTuple, Ast.Coalesce( + Ast.Condition( + Ast.Equal(reusableScope, AstUtils.Constant(null)), + AstUtils.Constant(null, _localsTuple.Type), + Ast.TypeAs(Methods.GetReusableLocals.OpCall(reusableScope), _localsTuple.Type) + ), + Ast.New(_localsTuple.Type) + )) + ); + } + + public MSA.Expression/*!*/ MakeClearLocals() { + Debug.Assert(HasReusableLocals); + int count = LiftedVisibleVariableCount; + var result = new AstExpressions(count + 1); + for (int i = 0; i < count; i++) { + result.Add(Ast.Assign(GetVariableAccessor(_localsTuple, i), AstUtils.Constant(null))); + } + result.Add(AstUtils.Empty()); + return Ast.Block(result); + } + public MSA.Expression/*!*/ GetVariableNamesExpression() { if (LiftedVisibleVariableCount == 0) { return Ast.Constant(null, typeof(string[])); diff --git a/Src/Ruby/Compiler/Ast/Walkers/NodeTypes.cs b/Src/Ruby/Compiler/Ast/Walkers/NodeTypes.cs index 32e9398b0..fd8b3da5d 100644 --- a/Src/Ruby/Compiler/Ast/Walkers/NodeTypes.cs +++ b/Src/Ruby/Compiler/Ast/Walkers/NodeTypes.cs @@ -30,6 +30,9 @@ public enum NodeTypes { Parameters, SplattedArgument, KeywordArgumentsTest, + KeywordParameterRead, + SplatParameterRead, + KeywordSplatMerge, // definitions: ClassDefinition, @@ -119,6 +122,9 @@ public enum NodeTypes { public partial class Parameters { public override NodeTypes NodeType { get { return NodeTypes.Parameters; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } public partial class SplattedArgument { public override NodeTypes NodeType { get { return NodeTypes.SplattedArgument; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } public partial class KeywordArgumentsTest { public override NodeTypes NodeType { get { return NodeTypes.KeywordArgumentsTest; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } + public partial class KeywordParameterRead { public override NodeTypes NodeType { get { return NodeTypes.KeywordParameterRead; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } + public partial class SplatParameterRead { public override NodeTypes NodeType { get { return NodeTypes.SplatParameterRead; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } + public partial class KeywordSplatMerge { public override NodeTypes NodeType { get { return NodeTypes.KeywordSplatMerge; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } public partial class ClassDefinition { public override NodeTypes NodeType { get { return NodeTypes.ClassDefinition; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } public partial class ModuleDefinition { public override NodeTypes NodeType { get { return NodeTypes.ModuleDefinition; } } internal protected override void Walk(Walker/*!*/ walker) { walker.Walk(this); } } diff --git a/Src/Ruby/Compiler/Ast/Walkers/Walker.Generated.cs b/Src/Ruby/Compiler/Ast/Walkers/Walker.Generated.cs index 4eb6968ad..58a7bd16a 100644 --- a/Src/Ruby/Compiler/Ast/Walkers/Walker.Generated.cs +++ b/Src/Ruby/Compiler/Ast/Walkers/Walker.Generated.cs @@ -41,6 +41,12 @@ public virtual void Exit(Arguments/*!*/ node) { } public virtual void Exit(SplattedArgument/*!*/ node) { } public virtual bool Enter(KeywordArgumentsTest/*!*/ node) { return true; } public virtual void Exit(KeywordArgumentsTest/*!*/ node) { } + public virtual bool Enter(KeywordParameterRead/*!*/ node) { return true; } + public virtual void Exit(KeywordParameterRead/*!*/ node) { } + public virtual bool Enter(SplatParameterRead/*!*/ node) { return true; } + public virtual void Exit(SplatParameterRead/*!*/ node) { } + public virtual bool Enter(KeywordSplatMerge/*!*/ node) { return true; } + public virtual void Exit(KeywordSplatMerge/*!*/ node) { } // declarations: public virtual bool Enter(ClassDefinition/*!*/ node) { return true; } diff --git a/Src/Ruby/Compiler/Ast/Walkers/Walker.cs b/Src/Ruby/Compiler/Ast/Walkers/Walker.cs index 1ff3936e7..d3fb75d7f 100644 --- a/Src/Ruby/Compiler/Ast/Walkers/Walker.cs +++ b/Src/Ruby/Compiler/Ast/Walkers/Walker.cs @@ -67,6 +67,35 @@ internal protected virtual void Walk(KeywordArgumentsTest/*!*/ node) { Exit(node); } + internal protected virtual void Walk(KeywordParameterRead/*!*/ node) { + if (Enter(node)) { + node.Keywords.Walk(this); + if (node.DefaultValue != null) { + node.DefaultValue.Walk(this); + } + } + Exit(node); + } + + internal protected virtual void Walk(KeywordSplatMerge/*!*/ node) { + if (Enter(node)) { + node.Hash.Walk(this); + node.Splatted.Walk(this); + node.Converted.Walk(this); + } + Exit(node); + } + + internal protected virtual void Walk(SplatParameterRead/*!*/ node) { + if (Enter(node)) { + node.Arguments.Walk(this); + if (node.DefaultValue != null) { + node.DefaultValue.Walk(this); + } + } + Exit(node); + } + internal protected virtual void Walk(BlockReference/*!*/ node) { if (Enter(node)) { node.Expression.Walk(this); diff --git a/Src/Ruby/Compiler/AstGenerator.cs b/Src/Ruby/Compiler/AstGenerator.cs index dad45a0ad..89c51072d 100644 --- a/Src/Ruby/Compiler/AstGenerator.cs +++ b/Src/Ruby/Compiler/AstGenerator.cs @@ -341,6 +341,9 @@ public MSA.LabelTarget/*!*/ RedoLabel { // that return leaves it, so its body catches the LambdaUnwinder. public bool HasNestedReturn; + // A block literal (or lambda) is nested in this one: its Proc captures this block's scope. + public bool HasNestedBlock; + public BlockScope(ScopeBuilder/*!*/ builder, MSA.Expression/*!*/ selfVariable, MSA.ParameterExpression/*!*/ runtimeScopeVariable, MSA.Expression/*!*/ bfcVariable, MSA.LabelTarget/*!*/ redoLabel) : base(builder, selfVariable, runtimeScopeVariable) { @@ -604,6 +607,9 @@ public void EnterBlockDefinition( Assert.NotNull(redoLabel); BlockScope block = new BlockScope(locals, selfVariable, runtimeScopeVariable, bfcVariable, redoLabel); + if (_currentBlock != null) { + _currentBlock.HasNestedBlock = true; + } block.Parent = _currentElement; block.ParentRescue = _currentRescue; block.ParentLoop = _currentLoop; diff --git a/Src/Ruby/Compiler/ReflectionCache.Generated.cs b/Src/Ruby/Compiler/ReflectionCache.Generated.cs index 407bf2a0c..bdf504ad5 100644 --- a/Src/Ruby/Compiler/ReflectionCache.Generated.cs +++ b/Src/Ruby/Compiler/ReflectionCache.Generated.cs @@ -42,6 +42,8 @@ public static partial class Methods { private static MethodInfo _AliasMethod; public static MethodInfo/*!*/ AllocateStructInstance { get { return _AllocateStructInstance ?? (_AllocateStructInstance = CallInstruction.CacheFunc(RubyOps.AllocateStructInstance)); } } private static MethodInfo _AllocateStructInstance; + public static MethodInfo/*!*/ AutoSplatArgument { get { return _AutoSplatArgument ?? (_AutoSplatArgument = CallInstruction.CacheFunc(RubyOps.AutoSplatArgument)); } } + private static MethodInfo _AutoSplatArgument; public static MethodInfo/*!*/ BlockBreak { get { return _BlockBreak ?? (_BlockBreak = CallInstruction.CacheFunc(RubyOps.BlockBreak)); } } private static MethodInfo _BlockBreak; public static MethodInfo/*!*/ BlockPropagateReturn { get { return _BlockPropagateReturn ?? (_BlockPropagateReturn = CallInstruction.CacheFunc(RubyOps.BlockPropagateReturn)); } } @@ -54,6 +56,8 @@ public static partial class Methods { private static MethodInfo _BlockYield; public static MethodInfo/*!*/ CanRescue { get { return _CanRescue ?? (_CanRescue = CallInstruction.CacheFunc(RubyOps.CanRescue)); } } private static MethodInfo _CanRescue; + public static MethodInfo/*!*/ CheckSplatArity { get { return _CheckSplatArity ?? (_CheckSplatArity = CallInstruction.CacheAction(RubyOps.CheckSplatArity)); } } + private static MethodInfo _CheckSplatArity; public static MethodInfo/*!*/ ClearKeywordArguments { get { return _ClearKeywordArguments ?? (_ClearKeywordArguments = CallInstruction.CacheFunc(RubyOps.ClearKeywordArguments)); } } private static MethodInfo _ClearKeywordArguments; public static MethodInfo/*!*/ CompareDefaultException { get { return _CompareDefaultException ?? (_CompareDefaultException = CallInstruction.CacheFunc(RubyOps.CompareDefaultException)); } } @@ -82,6 +86,8 @@ public static partial class Methods { private static MethodInfo _ConvertSymbolToClrString; public static MethodInfo/*!*/ ConvertSymbolToMutableString { get { return _ConvertSymbolToMutableString ?? (_ConvertSymbolToMutableString = CallInstruction.CacheFunc(RubyOps.ConvertSymbolToMutableString)); } } private static MethodInfo _ConvertSymbolToMutableString; + public static MethodInfo/*!*/ CopySplatArguments { get { return _CopySplatArguments ?? (_CopySplatArguments = CallInstruction.CacheFunc(RubyOps.CopySplatArguments)); } } + private static MethodInfo _CopySplatArguments; public static MethodInfo/*!*/ CreateArgumentsError { get { return _CreateArgumentsError ?? (_CreateArgumentsError = CallInstruction.CacheFunc(RubyOps.CreateArgumentsError)); } } private static MethodInfo _CreateArgumentsError; public static MethodInfo/*!*/ CreateArgumentsErrorForMissingBlock { get { return _CreateArgumentsErrorForMissingBlock ?? (_CreateArgumentsErrorForMissingBlock = CallInstruction.CacheFunc(RubyOps.CreateArgumentsErrorForMissingBlock)); } } @@ -96,6 +102,12 @@ public static partial class Methods { private static MethodInfo _CreateBfcForYield; public static MethodInfo/*!*/ CreateBlockScope { get { return _CreateBlockScope ?? (_CreateBlockScope = CallInstruction.CacheFunc(RubyOps.CreateBlockScope)); } } private static MethodInfo _CreateBlockScope; + public static MethodInfo/*!*/ CreateBlockScopeIfTraced { get { return _CreateBlockScopeIfTraced ?? (_CreateBlockScopeIfTraced = CallInstruction.CacheFunc(RubyOps.CreateBlockScopeIfTraced)); } } + private static MethodInfo _CreateBlockScopeIfTraced; + public static MethodInfo/*!*/ CreateBlockScopeLazy { get { return _CreateBlockScopeLazy ?? (_CreateBlockScopeLazy = CallInstruction.CacheFunc(RubyOps.CreateBlockScopeLazy)); } } + private static MethodInfo _CreateBlockScopeLazy; + public static MethodInfo/*!*/ CreateReusableBlockScope { get { return _CreateReusableBlockScope ?? (_CreateReusableBlockScope = CallInstruction.CacheFunc(RubyOps.CreateReusableBlockScope)); } } + private static MethodInfo _CreateReusableBlockScope; public static MethodInfo/*!*/ CreateBoundMember { get { return _CreateBoundMember ?? (_CreateBoundMember = CallInstruction.CacheFunc(RubyOps.CreateBoundMember)); } } private static MethodInfo _CreateBoundMember; public static MethodInfo/*!*/ CreateBoundMissingMember { get { return _CreateBoundMissingMember ?? (_CreateBoundMissingMember = CallInstruction.CacheFunc(RubyOps.CreateBoundMissingMember)); } } @@ -270,12 +282,16 @@ public static partial class Methods { private static MethodInfo _GetGlobalMissingConstant; public static MethodInfo/*!*/ GetGlobalScopeFromScope { get { return _GetGlobalScopeFromScope ?? (_GetGlobalScopeFromScope = CallInstruction.CacheFunc(RubyOps.GetGlobalScopeFromScope)); } } private static MethodInfo _GetGlobalScopeFromScope; + public static MethodInfo/*!*/ GetCachedGlobalVariable { get { return _GetCachedGlobalVariable ?? (_GetCachedGlobalVariable = CallInstruction.CacheFunc(RubyOps.GetCachedGlobalVariable)); } } + private static MethodInfo _GetCachedGlobalVariable; public static MethodInfo/*!*/ GetGlobalVariable { get { return _GetGlobalVariable ?? (_GetGlobalVariable = CallInstruction.CacheFunc(RubyOps.GetGlobalVariable)); } } private static MethodInfo _GetGlobalVariable; public static MethodInfo/*!*/ GetInstanceData { get { return _GetInstanceData ?? (_GetInstanceData = GetMethod(typeof(RubyOps), "GetInstanceData")); } } private static MethodInfo _GetInstanceData; public static MethodInfo/*!*/ GetInstanceVariable { get { return _GetInstanceVariable ?? (_GetInstanceVariable = CallInstruction.CacheFunc(RubyOps.GetInstanceVariable)); } } private static MethodInfo _GetInstanceVariable; + public static MethodInfo/*!*/ GetKeywordArgument { get { return _GetKeywordArgument ?? (_GetKeywordArgument = CallInstruction.CacheFunc(RubyOps.GetKeywordArgument)); } } + private static MethodInfo _GetKeywordArgument; public static MethodInfo/*!*/ GetLambdaUnwinderReturnValue { get { return _GetLambdaUnwinderReturnValue ?? (_GetLambdaUnwinderReturnValue = CallInstruction.CacheFunc(RubyOps.GetLambdaUnwinderReturnValue)); } } private static MethodInfo _GetLambdaUnwinderReturnValue; public static MethodInfo/*!*/ GetLocals { get { return _GetLocals ?? (_GetLocals = CallInstruction.CacheFunc(RubyOps.GetLocals)); } } @@ -356,6 +372,8 @@ public static partial class Methods { private static MethodInfo _IRubyObjectState_set_IsUntrusted; public static MethodInfo/*!*/ IsAdoptedClrRuleValid { get { return _IsAdoptedClrRuleValid ?? (_IsAdoptedClrRuleValid = CallInstruction.CacheFunc(RubyOps.IsAdoptedClrRuleValid)); } } private static MethodInfo _IsAdoptedClrRuleValid; + public static MethodInfo/*!*/ IsAutoSplatArgument { get { return _IsAutoSplatArgument ?? (_IsAutoSplatArgument = CallInstruction.CacheFunc(RubyOps.IsAutoSplatArgument)); } } + private static MethodInfo _IsAutoSplatArgument; public static MethodInfo/*!*/ IsClrNonSingletonRuleValid { get { return _IsClrNonSingletonRuleValid ?? (_IsClrNonSingletonRuleValid = CallInstruction.CacheFunc(RubyOps.IsClrNonSingletonRuleValid)); } } private static MethodInfo _IsClrNonSingletonRuleValid; public static MethodInfo/*!*/ IsClrSingletonRuleValid { get { return _IsClrSingletonRuleValid ?? (_IsClrSingletonRuleValid = CallInstruction.CacheFunc(RubyOps.IsClrSingletonRuleValid)); } } @@ -400,10 +418,18 @@ public static partial class Methods { private static MethodInfo _IsTopLevelReturn; public static MethodInfo/*!*/ IsTrue { get { return _IsTrue ?? (_IsTrue = CallInstruction.CacheFunc(RubyOps.IsTrue)); } } private static MethodInfo _IsTrue; + public static MethodInfo/*!*/ KeywordArgumentsMatch { get { return _KeywordArgumentsMatch ?? (_KeywordArgumentsMatch = CallInstruction.CacheFunc(RubyOps.KeywordArgumentsMatch)); } } + private static MethodInfo _KeywordArgumentsMatch; public static MethodInfo/*!*/ LeaveLambdaReturnScope { get { return _LeaveLambdaReturnScope ?? (_LeaveLambdaReturnScope = CallInstruction.CacheAction(RubyOps.LeaveLambdaReturnScope)); } } private static MethodInfo _LeaveLambdaReturnScope; public static MethodInfo/*!*/ LeaveLoop { get { return _LeaveLoop ?? (_LeaveLoop = CallInstruction.CacheAction(RubyOps.LeaveLoop)); } } private static MethodInfo _LeaveLoop; + public static MethodInfo/*!*/ LeaveReusableBlockScope { get { return _LeaveReusableBlockScope ?? (_LeaveReusableBlockScope = CallInstruction.CacheFunc(RubyOps.LeaveReusableBlockScope)); } } + private static MethodInfo _LeaveReusableBlockScope; + public static MethodInfo/*!*/ TakeReusableBlockScope { get { return _TakeReusableBlockScope ?? (_TakeReusableBlockScope = CallInstruction.CacheFunc(RubyOps.TakeReusableBlockScope)); } } + private static MethodInfo _TakeReusableBlockScope; + public static MethodInfo/*!*/ GetReusableLocals { get { return _GetReusableLocals ?? (_GetReusableLocals = CallInstruction.CacheFunc(RubyOps.GetReusableLocals)); } } + private static MethodInfo _GetReusableLocals; public static MethodInfo/*!*/ LeaveMethodFrame { get { return _LeaveMethodFrame ?? (_LeaveMethodFrame = CallInstruction.CacheAction(RubyOps.LeaveMethodFrame)); } } private static MethodInfo _LeaveMethodFrame; public static MethodInfo/*!*/ LeaveProcCall { get { return _LeaveProcCall ?? (_LeaveProcCall = CallInstruction.CacheAction(RubyOps.LeaveProcCall)); } } @@ -432,6 +458,8 @@ public static partial class Methods { private static MethodInfo _MakeArray5; public static MethodInfo/*!*/ MakeArrayN { get { return _MakeArrayN ?? (_MakeArrayN = CallInstruction.CacheFunc(RubyOps.MakeArrayN)); } } private static MethodInfo _MakeArrayN; + public static MethodInfo/*!*/ MakeArrayOwned { get { return _MakeArrayOwned ?? (_MakeArrayOwned = CallInstruction.CacheFunc(RubyOps.MakeArrayOwned)); } } + private static MethodInfo _MakeArrayOwned; public static MethodInfo/*!*/ MakeClrProtectedMethodCalledError { get { return _MakeClrProtectedMethodCalledError ?? (_MakeClrProtectedMethodCalledError = CallInstruction.CacheFunc(RubyOps.MakeClrProtectedMethodCalledError)); } } private static MethodInfo _MakeClrProtectedMethodCalledError; public static MethodInfo/*!*/ MakeClrVirtualMethodCalledError { get { return _MakeClrVirtualMethodCalledError ?? (_MakeClrVirtualMethodCalledError = CallInstruction.CacheFunc(RubyOps.MakeClrVirtualMethodCalledError)); } } @@ -450,6 +478,14 @@ public static partial class Methods { private static MethodInfo _MakeKeywordArgumentsHash; public static MethodInfo/*!*/ MakeKeywordArgumentsHash0 { get { return _MakeKeywordArgumentsHash0 ?? (_MakeKeywordArgumentsHash0 = CallInstruction.CacheFunc(RubyOps.MakeKeywordArgumentsHash0)); } } private static MethodInfo _MakeKeywordArgumentsHash0; + public static MethodInfo/*!*/ MakeKeywordArgumentsHash1 { get { return _MakeKeywordArgumentsHash1 ?? (_MakeKeywordArgumentsHash1 = CallInstruction.CacheFunc(RubyOps.MakeKeywordArgumentsHash1)); } } + private static MethodInfo _MakeKeywordArgumentsHash1; + public static MethodInfo/*!*/ MakeKeywordArgumentsHash2 { get { return _MakeKeywordArgumentsHash2 ?? (_MakeKeywordArgumentsHash2 = CallInstruction.CacheFunc(RubyOps.MakeKeywordArgumentsHash2)); } } + private static MethodInfo _MakeKeywordArgumentsHash2; + public static MethodInfo/*!*/ MakeKeywordArgumentsHash3 { get { return _MakeKeywordArgumentsHash3 ?? (_MakeKeywordArgumentsHash3 = CallInstruction.CacheFunc(RubyOps.MakeKeywordArgumentsHash3)); } } + private static MethodInfo _MakeKeywordArgumentsHash3; + public static MethodInfo/*!*/ MakeKeywordRest { get { return _MakeKeywordRest ?? (_MakeKeywordRest = CallInstruction.CacheFunc(RubyOps.MakeKeywordRest)); } } + private static MethodInfo _MakeKeywordRest; public static MethodInfo/*!*/ MakeMissingDefaultConstructorError { get { return _MakeMissingDefaultConstructorError ?? (_MakeMissingDefaultConstructorError = CallInstruction.CacheFunc(RubyOps.MakeMissingDefaultConstructorError)); } } private static MethodInfo _MakeMissingDefaultConstructorError; public static MethodInfo/*!*/ MakeMissingMemberError { get { return _MakeMissingMemberError ?? (_MakeMissingMemberError = CallInstruction.CacheFunc(RubyOps.MakeMissingMemberError)); } } @@ -486,6 +522,8 @@ public static partial class Methods { private static MethodInfo _MatchLastInputLine; public static MethodInfo/*!*/ MatchString { get { return _MatchString ?? (_MatchString = CallInstruction.CacheFunc(RubyOps.MatchString)); } } private static MethodInfo _MatchString; + public static MethodInfo/*!*/ MergeKeywordSplat { get { return _MergeKeywordSplat ?? (_MergeKeywordSplat = CallInstruction.CacheFunc(RubyOps.MergeKeywordSplat)); } } + private static MethodInfo _MergeKeywordSplat; public static MethodInfo/*!*/ MethodBreak { get { return _MethodBreak ?? (_MethodBreak = CallInstruction.CacheAction(RubyOps.MethodBreak)); } } private static MethodInfo _MethodBreak; public static MethodInfo/*!*/ MethodNext { get { return _MethodNext ?? (_MethodNext = CallInstruction.CacheAction(RubyOps.MethodNext)); } } @@ -510,14 +548,20 @@ public static partial class Methods { private static MethodInfo _ObjectToMutableString; public static MethodInfo/*!*/ ObjectToString { get { return _ObjectToString ?? (_ObjectToString = CallInstruction.CacheFunc(RubyOps.ObjectToString)); } } private static MethodInfo _ObjectToString; + public static MethodInfo/*!*/ PopKeywordArguments { get { return _PopKeywordArguments ?? (_PopKeywordArguments = CallInstruction.CacheFunc(RubyOps.PopKeywordArguments)); } } + private static MethodInfo _PopKeywordArguments; public static MethodInfo/*!*/ PrintInteractiveResult { get { return _PrintInteractiveResult ?? (_PrintInteractiveResult = CallInstruction.CacheAction(RubyOps.PrintInteractiveResult)); } } private static MethodInfo _PrintInteractiveResult; public static MethodInfo/*!*/ PropagateRetrySingleton { get { return _PropagateRetrySingleton ?? (_PropagateRetrySingleton = CallInstruction.CacheFunc(RubyOps.PropagateRetrySingleton)); } } private static MethodInfo _PropagateRetrySingleton; + public static MethodInfo/*!*/ ReadCachedGlobalVariable { get { return _ReadCachedGlobalVariable ?? (_ReadCachedGlobalVariable = CallInstruction.CacheFunc(RubyOps.ReadCachedGlobalVariable)); } } + private static MethodInfo _ReadCachedGlobalVariable; public static MethodInfo/*!*/ ReadGlobalVariable { get { return _ReadGlobalVariable ?? (_ReadGlobalVariable = CallInstruction.CacheFunc(RubyOps.ReadGlobalVariable)); } } private static MethodInfo _ReadGlobalVariable; public static MethodInfo/*!*/ RegisterShutdownHandler { get { return _RegisterShutdownHandler ?? (_RegisterShutdownHandler = CallInstruction.CacheAction(RubyOps.RegisterShutdownHandler)); } } private static MethodInfo _RegisterShutdownHandler; + public static MethodInfo/*!*/ ReuseBfcForYield { get { return _ReuseBfcForYield ?? (_ReuseBfcForYield = CallInstruction.CacheFunc(RubyOps.ReuseBfcForYield)); } } + private static MethodInfo _ReuseBfcForYield; public static MethodInfo/*!*/ RubyStruct_GetValue { get { return _RubyStruct_GetValue ?? (_RubyStruct_GetValue = CallInstruction.CacheFunc(RubyStruct.GetValue)); } } private static MethodInfo _RubyStruct_GetValue; public static MethodInfo/*!*/ RubyStruct_SetValue { get { return _RubyStruct_SetValue ?? (_RubyStruct_SetValue = CallInstruction.CacheFunc(RubyStruct.SetValue)); } } @@ -532,6 +576,8 @@ public static partial class Methods { private static MethodInfo _SetDataConstant; public static MethodInfo/*!*/ SetGlobalConstant { get { return _SetGlobalConstant ?? (_SetGlobalConstant = CallInstruction.CacheFunc(RubyOps.SetGlobalConstant)); } } private static MethodInfo _SetGlobalConstant; + public static MethodInfo/*!*/ SetCachedGlobalVariable { get { return _SetCachedGlobalVariable ?? (_SetCachedGlobalVariable = CallInstruction.CacheFunc(RubyOps.SetCachedGlobalVariable)); } } + private static MethodInfo _SetCachedGlobalVariable; public static MethodInfo/*!*/ SetGlobalVariable { get { return _SetGlobalVariable ?? (_SetGlobalVariable = CallInstruction.CacheFunc(RubyOps.SetGlobalVariable)); } } private static MethodInfo _SetGlobalVariable; public static MethodInfo/*!*/ SetInstanceVariable { get { return _SetInstanceVariable ?? (_SetInstanceVariable = CallInstruction.CacheFunc(RubyOps.SetInstanceVariable)); } } @@ -548,12 +594,20 @@ public static partial class Methods { private static MethodInfo _SetQualifiedConstant; public static MethodInfo/*!*/ SetUnqualifiedConstant { get { return _SetUnqualifiedConstant ?? (_SetUnqualifiedConstant = CallInstruction.CacheFunc(RubyOps.SetUnqualifiedConstant)); } } private static MethodInfo _SetUnqualifiedConstant; + public static MethodInfo/*!*/ ShiftOptionalSplatArgument { get { return _ShiftOptionalSplatArgument ?? (_ShiftOptionalSplatArgument = CallInstruction.CacheFunc(RubyOps.ShiftOptionalSplatArgument)); } } + private static MethodInfo _ShiftOptionalSplatArgument; + public static MethodInfo/*!*/ ShiftRestSplatArguments { get { return _ShiftRestSplatArguments ?? (_ShiftRestSplatArguments = CallInstruction.CacheFunc(RubyOps.ShiftRestSplatArguments)); } } + private static MethodInfo _ShiftRestSplatArguments; + public static MethodInfo/*!*/ ShiftSplatArgument { get { return _ShiftSplatArgument ?? (_ShiftSplatArgument = CallInstruction.CacheFunc(RubyOps.ShiftSplatArgument)); } } + private static MethodInfo _ShiftSplatArgument; public static MethodInfo/*!*/ Splat { get { return _Splat ?? (_Splat = CallInstruction.CacheFunc(RubyOps.Splat)); } } private static MethodInfo _Splat; public static MethodInfo/*!*/ SplatAppend { get { return _SplatAppend ?? (_SplatAppend = CallInstruction.CacheFunc(RubyOps.SplatAppend)); } } private static MethodInfo _SplatAppend; public static MethodInfo/*!*/ SplatKeywordHash { get { return _SplatKeywordHash ?? (_SplatKeywordHash = CallInstruction.CacheFunc(RubyOps.SplatKeywordHash)); } } private static MethodInfo _SplatKeywordHash; + public static MethodInfo/*!*/ SplatKeywordHashIfHash { get { return _SplatKeywordHashIfHash ?? (_SplatKeywordHashIfHash = CallInstruction.CacheFunc(RubyOps.SplatKeywordHashIfHash)); } } + private static MethodInfo _SplatKeywordHashIfHash; public static MethodInfo/*!*/ SplatPair { get { return _SplatPair ?? (_SplatPair = CallInstruction.CacheFunc(RubyOps.SplatPair)); } } private static MethodInfo _SplatPair; public static MethodInfo/*!*/ SplatRuby2Keywords { get { return _SplatRuby2Keywords ?? (_SplatRuby2Keywords = CallInstruction.CacheFunc(RubyOps.SplatRuby2Keywords)); } } diff --git a/Src/Ruby/Compiler/ReflectionCache.cs b/Src/Ruby/Compiler/ReflectionCache.cs index ad56e9297..975ba8998 100644 --- a/Src/Ruby/Compiler/ReflectionCache.cs +++ b/Src/Ruby/Compiler/ReflectionCache.cs @@ -169,7 +169,7 @@ public static MethodInfo/*!*/ MutableString_Freeze { default: Debug.Assert(args.Count > Runtime.RubyOps.OptimizedOpCallParamCount); - return Methods.MakeArrayN.OpCall(AstUtils.NewArrayHelper(typeof(object), args)); + return Methods.MakeArrayOwned.OpCall(AstUtils.NewArrayHelper(typeof(object), args)); } } diff --git a/Src/Ruby/Runtime/BlockParam.cs b/Src/Ruby/Runtime/BlockParam.cs index 60645cd15..fff83f5bc 100644 --- a/Src/Ruby/Runtime/BlockParam.cs +++ b/Src/Ruby/Runtime/BlockParam.cs @@ -93,37 +93,84 @@ public DynamicMetaObject Restrict(Type/*!*/ type) { public sealed partial class BlockParam { // -- in -- private readonly Proc/*!*/ _proc; - private readonly BlockCallerKind _callerKind; + // The small state is packed into bytes, which keeps a BlockParam at 48 bytes: one is allocated for every + // call of a library method with a block, and for every yield and Proc#call. + // bit 0: BlockCallerKind.Call; bit 1: the library method call taking this BlockParam is a proc converter. + private readonly byte _callerFlags; + private const int CallFlag = 1; + private const int LibProcConverterFlag = 2; // filled by module_eval: if not null than method definition uses the module private RubyModule _methodLookupModule; - // Is the library method call taking this BlockParam a proc converter? - // Used only for BlockParams that are passed to library method calls. - // Friend: RubyOps - internal readonly bool _isLibProcConverter; - - // the thread the call taking the block runs on; a break cannot reach that call from another + // the thread the call taking the block runs on; a break cannot reach that call from another. + // 0 for a yield or a Proc#call, which run the block on the thread that made the BlockParam. private readonly int _threadId; // the block is the body of a Thread (see RubyBlockScope.IsClosureScope) public bool IsThreadRoot { get; set; } // -- out -- - private BlockReturnReason _returnReason; + // bits 0-1: BlockReturnReason; bits 2-3: the ProcKind of the block that set it; + // bit 4: a call of the block through this BlockParam has finished (see ParkScope) + private byte _flow; + private const int CalledFlag = 16; + + // The break target while ReturnReason is Break. While it is Undefined: null, or the scope of the last + // call of the block that nothing captured, for the next call through this BlockParam to reuse (see + // ReusableScope) - no field of its own, a BlockParam is allocated for every call with a block. private RuntimeFlowControl _targetFrame; - private ProcKind _sourceProcKind; - internal BlockCallerKind CallerKind { get { return _callerKind; } } + internal BlockCallerKind CallerKind { get { return (_callerFlags & CallFlag) != 0 ? BlockCallerKind.Call : BlockCallerKind.Yield; } } + + /// + /// The scope ParkScope kept, or null. + /// + internal RubyBlockScope ReusableScope { + get { return ((_flow & 3) == (int)BlockReturnReason.Undefined) ? (RubyBlockScope)_targetFrame : null; } + } + + // False if there is no ReusableScope; a quick test for the common case. + internal bool MayHaveReusableScope { + get { return _targetFrame != null; } + } + + // The scope is taken for a call of the block (it was the ReusableScope). + internal void TakeReusableScope() { + _targetFrame = null; + } + + /// + /// Keeps the scope of a call of the block that finished normally, for the next call to reuse; not on the + /// first call through this BlockParam, which most often is the only one (a library method calling its + /// block once), nor after a break/return/retry, which use the target frame, nor if something captured it. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal bool ParkScope(RubyBlockScope/*!*/ scope) { + int flow = _flow; + if (flow == CalledFlag) { + if (scope.IsCaptured) { + return false; + } + _targetFrame = scope; + return true; + } + if (flow == 0) { + _flow = CalledFlag; + } + return false; + } // True while a lambda called through Proc#call (or as a define_method body) is running, so // a return in a block nested in it can unwind to that call. internal bool IsActiveLambdaCall; - internal ProcKind SourceProcKind { get { return _sourceProcKind; } } - internal BlockReturnReason ReturnReason { get { return _returnReason; } set { _returnReason = value; } } + internal ProcKind SourceProcKind { get { return (ProcKind)((_flow >> 2) & 3); } } + internal BlockReturnReason ReturnReason { get { return (BlockReturnReason)(_flow & 3); } set { _flow = (byte)((_flow & ~3) | (int)value); } } internal RuntimeFlowControl TargetFrame { get { return _targetFrame; } } internal RubyModule MethodLookupModule { get { return _methodLookupModule; } set { _methodLookupModule = value; } } - internal bool IsLibProcConverter { get { return _isLibProcConverter; } } + // Is the library method call taking this BlockParam a proc converter? + // Used only for BlockParams that are passed to library method calls. + internal bool IsLibProcConverter { get { return (_callerFlags & LibProcConverterFlag) != 0; } } /// /// True if the library method got a literal block rather than a Proc object passed with &: @@ -134,11 +181,12 @@ public sealed partial class BlockParam { /// public bool IsLiteralBlock { get { - if (_isLibProcConverter) { + if (IsLibProcConverter) { return true; } + // a call that converted a literal block stands for its frame by that Proc (see CreateRfcForMethod): var converter = _proc.Converter; - return converter != null && converter.GetType() == typeof(RuntimeFlowControl) && converter.IsActiveMethod; + return converter is Proc && converter.IsActiveMethod; } } @@ -160,17 +208,30 @@ public bool IsMethod { // friend: RubyOps internal BlockParam(Proc/*!*/ proc, BlockCallerKind callerKind, bool isLibProcConverter) { _threadId = Environment.CurrentManagedThreadId; - _callerKind = callerKind; + _callerFlags = (byte)((callerKind == BlockCallerKind.Call ? CallFlag : 0) | (isLibProcConverter ? LibProcConverterFlag : 0)); _proc = proc; - _isLibProcConverter = isLibProcConverter; + } + + // A BlockParam that is only ever used on the thread creating it, for the duration of a call: + // a yield or a Proc#call. There the thread check in Break always passes, so skip it. + internal BlockParam(Proc/*!*/ proc, BlockCallerKind callerKind) { + _callerFlags = (byte)(callerKind == BlockCallerKind.Call ? CallFlag : 0); + _proc = proc; + } + + // ReuseBfcForYield: the flow-control state is cleared, but the scope the last call kept (see ParkScope) stays. + internal void ResetForNextYield() { + if ((_flow & ~CalledFlag) != 0) { + _targetFrame = null; + } + _flow = CalledFlag; } internal void SetFlowControl(BlockReturnReason reason, RuntimeFlowControl targetFrame, ProcKind sourceProcKind) { Debug.Assert((reason == BlockReturnReason.Break) == (targetFrame != null)); - _returnReason = reason; + _flow = (byte)((int)reason | ((int)sourceProcKind << 2)); _targetFrame = targetFrame; - _sourceProcKind = sourceProcKind; } internal object GetUnwinderResult(EvalUnwinder/*!*/ unwinder) { @@ -189,8 +250,13 @@ internal object GetUnwinderResult(EvalUnwinder/*!*/ unwinder) { /// A library method that creates a block yet doesn't take one needs to manage RFC on its own. /// internal bool BlockJumped(object returnValue) { + // the block finished normally (no break/return/retry), which is what nearly every yield does: + if ((_flow & 3) == (int)BlockReturnReason.Undefined) { + return false; + } + // if this method is a proc converter then the current frame is Proc.Converter, otherwise it is not available: - return RubyOps.MethodYieldRfc(_isLibProcConverter ? _proc.Converter : null, this, returnValue); + return RubyOps.MethodYieldRfc(IsLibProcConverter ? _proc.Converter : null, this, returnValue); } public bool Returning(object returnValue, out object result) { @@ -209,7 +275,7 @@ public object PropagateFlow(BlockParam/*!*/ yieldedBlock, object returnValue) { if (yieldedBlock.ReturnReason == BlockReturnReason.Break) { return Break(returnValue); } else { - _returnReason = yieldedBlock.ReturnReason; + ReturnReason = yieldedBlock.ReturnReason; return returnValue; } } @@ -226,7 +292,7 @@ public object Break(object returnValue) { // proc whose home frame is gone (e.g. Proc#call on an orphaned block), or one running // on another thread than the call it would break out of (Thread.new { break }): - if (_proc.Converter == null || _threadId != Environment.CurrentManagedThreadId) { + if (_proc.Converter == null || (_threadId != 0 && _threadId != Environment.CurrentManagedThreadId)) { throw new LocalJumpError("break from proc-closure", "break", returnValue); } @@ -307,16 +373,31 @@ public static bool IsProcConverterTarget(BlockParam/*!*/ bfc, MethodUnwinder/*!* [Emitted] public static BlockParam/*!*/ CreateBfcForYield(Proc proc) { if (proc != null) { - return new BlockParam(proc, BlockCallerKind.Yield, false); + return new BlockParam(proc, BlockCallerKind.Yield); } else { throw RubyExceptions.NoBlockGiven(); } } + /// + /// A yield site that runs again in the same frame - a loop around `yield` - yields to the same + /// proc every time, and its BlockParam is only looked at right after each yield returns + /// (MethodYield/BlockYield), so the site hands the previous one back with its flow-control + /// state cleared instead of allocating another. + /// + [Emitted] + public static BlockParam/*!*/ ReuseBfcForYield(BlockParam previous, Proc proc) { + if (previous != null && previous.Proc == proc) { + previous.ResetForNextYield(); + return previous; + } + return CreateBfcForYield(proc); + } + [Emitted] public static BlockParam/*!*/ CreateBfcForProcCall(Proc/*!*/ proc) { Assert.NotNull(proc); - return new BlockParam(proc, BlockCallerKind.Call, false) { IsActiveLambdaCall = proc.Kind == ProcKind.Lambda }; + return new BlockParam(proc, BlockCallerKind.Call) { IsActiveLambdaCall = proc.Kind == ProcKind.Lambda }; } [Emitted] @@ -325,10 +406,8 @@ public static bool IsProcConverterTarget(BlockParam/*!*/ bfc, MethodUnwinder/*!* bool isProcConverter; if (proc.Kind == ProcKind.Block) { - var rfc = new RuntimeFlowControl(); - rfc._activeFlowControlScope = rfc; - proc.Converter = rfc; - proc.Kind = ProcKind.Proc; + // the literal block is new for this call: it is the call's flow-control frame (see CreateRfcForMethod) + proc.ConvertAsFrame(); isProcConverter = true; } else { isProcConverter = false; @@ -340,7 +419,7 @@ public static bool IsProcConverterTarget(BlockParam/*!*/ bfc, MethodUnwinder/*!* [Emitted] public static void LeaveProcConverter(BlockParam/*!*/ bfc) { Debug.Assert(bfc.Proc != null); - if (bfc._isLibProcConverter) { + if (bfc.IsLibProcConverter) { Debug.Assert(bfc.Proc.Converter != null); bfc.Proc.Converter.LeaveMethod(); } diff --git a/Src/Ruby/Runtime/Calls/BlockDispatcher.cs b/Src/Ruby/Runtime/Calls/BlockDispatcher.cs index 6b2c22883..8b835bb97 100644 --- a/Src/Ruby/Runtime/Calls/BlockDispatcher.cs +++ b/Src/Ruby/Runtime/Calls/BlockDispatcher.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using Microsoft.Scripting; using Microsoft.Scripting.Actions; +using Microsoft.Scripting.Interpreter; using Microsoft.Scripting.Runtime; using Microsoft.Scripting.Utils; using System.Reflection; @@ -30,6 +31,7 @@ using System.Collections.ObjectModel; using System.Collections; using System.Diagnostics; +using System.Threading; namespace IronRuby.Runtime.Calls { using Ast = Expression; @@ -73,8 +75,25 @@ public override Delegate/*!*/ Method { // Note: this might potentially be executed by multiple threads. So we can assert that _block == null here. // It's ok if the delegate is overwritten multiple times since all the target methods are equivalent. _block = (T)method; + Volatile.Write(ref _hasMethod, true); + + // A block defined in interpreted code gets the interpreter's delegate, which keeps going + // through LightLambda.RunN - a check and a cast - on every call even once the lambda has + // compiled itself, and every later Proc of this block reuses that delegate. Call the + // compiled one directly as soon as there is one: it is the very delegate RunN would call. + var light = ((Delegate)method).Target as LightLambda; + if (light != null) { + light.Compile += OnLightLambdaCompiled; + } return this; } + + private void OnLightLambdaCompiled(object sender, LightLambdaCompileEventArgs/*!*/ e) { + var compiled = e.Compiled as T; + if (compiled != null) { + _block = compiled; + } + } } internal abstract class BlockDispatcherN : BlockDispatcher where T : class { @@ -176,7 +195,18 @@ public bool Ruby2Keywords { /// public RubyParameterSignature ParameterSignature { get { return _parameterSignature; } - set { _parameterSignature = value; } + set { + _parameterSignature = value; + _checksKeywords = value != null && (value.TakesKeywords || value.RefusesKeywords); + } + } + + // the signature declares keywords or **nil, so even a proc's arguments are checked on a yield + // (see RubyOps.RequireLambdaArity); cached because RefusesKeywords walks the parameter list + private bool _checksKeywords; + + internal bool ChecksKeywords { + get { return _checksKeywords; } } public bool HasUnsplatParameter { @@ -209,6 +239,13 @@ public static BlockSignatureAttributes MakeAttributes(BlockSignatureAttributes a // Includes anonymous parameter. public abstract int ParameterCount { get; } public abstract Delegate/*!*/ Method { get; } + + // Method != null, without the virtual call: InstantiateBlock asks on every call with a literal block + private protected bool _hasMethod; + + internal bool HasMethod { + get { return Volatile.Read(ref _hasMethod); } + } internal abstract BlockDispatcher/*!*/ SetMethod(object/*!*/ method); public string SourcePath { get { return _sourcePath; } } diff --git a/Src/Ruby/Runtime/Calls/CallArguments.cs b/Src/Ruby/Runtime/Calls/CallArguments.cs index 5a533631a..85f168946 100644 --- a/Src/Ruby/Runtime/Calls/CallArguments.cs +++ b/Src/Ruby/Runtime/Calls/CallArguments.cs @@ -311,6 +311,16 @@ public void InsertSimple(int index, DynamicMetaObject/*!*/ arg) { _signature = new RubyCallSignature(_signature.ArgumentCount + 1, _signature.Flags); } + /// + /// The call #send makes: the method name dropped, and visibility ignored as for a call with implicit self. + /// + internal void RemoveSendMethodName() { + _args = ArrayUtils.RemoveAt(_args, GetSimpleArgumentIndex(0)); + _copyArgsOnWrite = false; + _signature = new RubyCallSignature(_signature.ArgumentCount - 1, + (_signature.Flags & ~(RubyCallFlags.HasSourceBlock | RubyCallFlags.IsVariableCall)) | RubyCallFlags.HasImplicitSelf); + } + internal void InsertMethodName(string/*!*/ methodName) { // insert the method name argument into the args object symbol = RubyContext.EncodeIdentifier(methodName); diff --git a/Src/Ruby/Runtime/Calls/DispatchIntrinsics.cs b/Src/Ruby/Runtime/Calls/DispatchIntrinsics.cs new file mode 100644 index 000000000..b34a47358 --- /dev/null +++ b/Src/Ruby/Runtime/Calls/DispatchIntrinsics.cs @@ -0,0 +1,181 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +#if FEATURE_CORE_DLR +using System.Linq.Expressions; +#else +using Microsoft.Scripting.Ast; +#endif + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using IronRuby.Builtins; +using IronRuby.Compiler; +using Microsoft.Scripting.Runtime; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Runtime.Calls { + using Ast = Expression; + + /// + /// Rules the call binder builds itself for calls of Kernel's dynamic-dispatch helpers. + /// + internal static class DispatchIntrinsics { + // The method infos last found to be Kernel#respond_to? and Kernel#respond_to_missing? (or aliases of them). + private static RubyMemberInfo _respondTo, _respondToMissing; + + // The method infos last found to be Kernel#send and BasicObject#__send__ (or aliases of them). + private static RubyMemberInfo _send, _basicSend; + + // Rules specialized on a name, per receiver class, so that a site sending or asking about computed + // names does not bind a rule per name: past the limit the class's sites get the general rules. + private sealed class FoldCounts { + internal int RespondTo; + internal int Send; + } + + private const int MaxFoldsPerClass = 32; + private static readonly ConditionalWeakTable/*!*/ _folds = new ConditionalWeakTable(); + + private static bool IsKernelMethod(RubyMemberInfo/*!*/ method, ref RubyMemberInfo last, string/*!*/ clrName) { + if (ReferenceEquals(method, last)) { + return true; + } + + var libraryMethod = method as RubyLibraryMethodInfo; + if (libraryMethod == null) { + return false; + } + + var members = libraryMethod.GetMembers(); + if (members.Length == 1 && members[0].Name == clrName && members[0].DeclaringType.FullName == "IronRuby.Builtins.KernelOps") { + last = method; + return true; + } + return false; + } + + /// + /// `obj.respond_to?(:name)' answered by method lookups alone - no refinements anywhere, the name's + /// method or Kernel#respond_to_missing? found - is a constant for as long as the receiver class's + /// method version stays the same, which the rule's receiver test already checks: the lookups are + /// registered for invalidation the way a call site's own resolution is (RubyModule.GetMethodLookup). + /// The symbol is part of the rule's test, so each literal site gets a rule of its own. + /// + internal static bool TryBuildRespondTo(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ args, RubyMemberInfo/*!*/ method) { + var signature = args.Signature; + if (signature.HasBlock || signature.HasSplattedArgument || signature.HasRhsArgument || signature.ResolveOnly || + args.SimpleArgumentCount != 1 || !IsKernelMethod(method, ref _respondTo, "RespondTo")) { + return false; + } + + // Only a Ruby object's rule tests its exact class; a CLR object's may cover an instance with singleton methods. + var symbol = args.GetSimpleArgument(0) as RubySymbol; + var targetClass = args.TargetClass; + if (symbol == null || !(args.Target is IRubyObject) || targetClass.Context.HasRefinements) { + return false; + } + + string name = symbol.ToString(); + int version; + var lookup = targetClass.GetMethodLookup(name, out version); + if (version < 0) { + return false; + } + + bool answer; + if (lookup.Found && lookup.Visibility == RubyMethodVisibility.Public) { + var libraryMethod = lookup.Info as RubyLibraryMethodInfo; + answer = libraryMethod == null || !libraryMethod.IsNotImplemented; + } else { + // a user-defined respond_to_missing? is called every time: + int missingVersion; + var missing = targetClass.GetMethodLookup(Symbols.RespondToMissing, out missingVersion); + if (missingVersion != version || missing.Found && !IsKernelMethod(missing.Info, ref _respondToMissing, "RespondToMissing")) { + return false; + } + answer = false; + } + + if (Interlocked.Increment(ref _folds.GetOrCreateValue(targetClass).RespondTo) > MaxFoldsPerClass) { + return false; + } + + metaBuilder.AddCondition(Ast.Equal(AstUtils.Convert(args.GetSimpleArgumentExpression(0), typeof(object)), Ast.Constant(symbol, typeof(object)))); + metaBuilder.Result = Ast.Constant(ScriptingRuntimeHelpers.BooleanToObject(answer), typeof(object)); + return true; + } + + private static bool IsSendMethod(RubyMemberInfo/*!*/ method) { + if (ReferenceEquals(method, _send) || ReferenceEquals(method, _basicSend)) { + return true; + } + + var libraryMethod = method as RubyLibraryMethodInfo; + if (libraryMethod == null) { + return false; + } + + var members = libraryMethod.GetMembers(); + if (members.Length == 0 || members[0].Name != "SendMessage") { + return false; + } + + switch (members[0].DeclaringType.FullName) { + case "IronRuby.Builtins.KernelOps": _send = method; return true; + case "IronRuby.Builtins.BasicObjectOps": _basicSend = method; return true; + default: return false; + } + } + + /// + /// `obj.send(:name, ...)' (or __send__) is bound as the call of name it makes - the call with implicit + /// self, scope and block that Kernel#send's own site makes - so that it costs one dispatch rather than + /// two, as MRI's optimized send does (no send frame appears in a backtrace there either). The symbol is + /// part of the rule's test; the caller binds the call of on the rewritten + /// . + /// + internal static bool TryBuildSend(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ args, RubyMemberInfo/*!*/ method, + out string sentName) { + + sentName = null; + var signature = args.Signature; + if (!signature.HasScope || signature.HasSplattedArgument || signature.HasRhsArgument || signature.ResolveOnly || + signature.IsVirtualCall || signature.IsSuperCall || signature.IsInteropCall || args.SimpleArgumentCount < 1 || + !IsSendMethod(method)) { + return false; + } + + var symbol = args.GetSimpleArgument(0) as RubySymbol; + if (symbol == null || Interlocked.Increment(ref _folds.GetOrCreateValue(args.TargetClass).Send) > MaxFoldsPerClass) { + return false; + } + + metaBuilder.AddCondition(Ast.Equal(AstUtils.Convert(args.GetSimpleArgumentExpression(0), typeof(object)), Ast.Constant(symbol, typeof(object)))); + args.RemoveSendMethodName(); + sentName = symbol.ToString(); + return true; + } + + /// + /// The rules TryBuildRespondTo and TryBuildSend build depend on the receiver's class, not only on the + /// method, so RubyCallAction.BindShared must not hand them to receivers of other classes. + /// + internal static bool MaySpecialize(RubyMemberInfo/*!*/ method) { + return IsKernelMethod(method, ref _respondTo, "RespondTo") || IsSendMethod(method); + } + } +} diff --git a/Src/Ruby/Runtime/Calls/MetaObjectBuilder.cs b/Src/Ruby/Runtime/Calls/MetaObjectBuilder.cs index 21fae0f2e..726fa0eb9 100644 --- a/Src/Ruby/Runtime/Calls/MetaObjectBuilder.cs +++ b/Src/Ruby/Runtime/Calls/MetaObjectBuilder.cs @@ -92,6 +92,12 @@ public Expression Result { public ParameterExpression BfcVariable { get; set; } + /// + /// Set if the rule being built is to be shared by receivers of several classes (see ). + /// Taken by the first target type test of a method call. + /// + internal RuleVersionSet TargetVersionSet { get; set; } + /// /// A rule builder sets this up if the resulting rule is required to be wrapped in a non-local control flow handler. /// This delegate must be called exactly once ( method). @@ -246,6 +252,15 @@ public void AddCondition(Expression/*!*/ condition) { public void AddTargetTypeTest(object target, RubyClass/*!*/ targetClass, Expression/*!*/ targetParameter, DynamicMetaObject/*!*/ metaContext, IEnumerable/*!*/ resolvedNames) { + AddTargetTypeTest(target, targetClass, targetParameter, metaContext, resolvedNames, null); + } + + /// + /// If is given and the target is a Ruby object the rule tests the class version against the set + /// (and the current version is added to it) instead of against a constant: see . + /// + internal void AddTargetTypeTest(object target, RubyClass/*!*/ targetClass, Expression/*!*/ targetParameter, DynamicMetaObject/*!*/ metaContext, + IEnumerable/*!*/ resolvedNames, RuleVersionSet versionSet) { // no changes to the module's class hierarchy while building the test: targetClass.Context.RequiresClassHierarchyLock(); @@ -262,6 +277,14 @@ public void AddTargetTypeTest(object target, RubyClass/*!*/ targetClass, Express // Ruby objects (get the method directly to prevent interface dispatch): MethodInfo classGetter = type.GetMethod(Methods.IRubyObject_get_ImmediateClass.Name, BindingFlags.Public | BindingFlags.Instance); if (type.IsVisible && classGetter != null && classGetter.ReturnType == typeof(RubyClass)) { + if (versionSet != null && versionSet.TryAdd(targetClass.Version.Method)) { + // (#{type})target.ImmediateClass.Version.Method in #{versionSet} + AddCondition(versionSet.MakeTest( + Ast.Field(Ast.Field(Ast.Call(Ast.Convert(targetParameter, type), classGetter), Fields.RubyModule_Version), Fields.VersionHandle_Method) + )); + return; + } + AddCondition( // (#{type})target.ImmediateClass.Version.Method == #{immediateClass.Version.Method} Ast.Equal( @@ -287,7 +310,7 @@ public void AddTargetTypeTest(object target, RubyClass/*!*/ targetClass, Express // singleton nil: if (target == null) { AddRestriction(Ast.Equal(targetParameter, AstUtils.Constant(null))); - AddVersionTest(context.NilClass); + AddVersionTest(context.NilClass, versionSet); return; } @@ -325,7 +348,7 @@ public void AddTargetTypeTest(object target, RubyClass/*!*/ targetClass, Express // no: there is no singleton subclass of target class that defines any method being called: AddTypeRestriction(target.GetType(), targetParameter); - AddVersionTest(targetClass); + AddVersionTest(targetClass, versionSet); } else if (targetClass.IsSingletonClass) { @@ -369,6 +392,15 @@ internal void AddVersionTest(RubyClass/*!*/ cls) { AddCondition(Ast.Equal(Ast.Field(AstUtils.Constant(cls.Version), Fields.VersionHandle_Method), AstUtils.Constant(cls.Version.Method))); } + private void AddVersionTest(RubyClass/*!*/ cls, RuleVersionSet versionSet) { + if (versionSet != null && versionSet.TryAdd(cls.Version.Method)) { + // #{cls.Version}.Method in #{versionSet} + AddCondition(versionSet.MakeTest(Ast.Field(AstUtils.Constant(cls.Version), Fields.VersionHandle_Method))); + } else { + AddVersionTest(cls); + } + } + internal void AddSplattedArgumentTest(IList/*!*/ value, Expression/*!*/ expression, out int listLength, out ParameterExpression/*!*/ listVariable) { // Every splatted call comes through here, whoever built the list - `f(*args)' in Ruby // source, Kernel#send, an Enumerator replaying the arguments it was made with. A hash diff --git a/Src/Ruby/Runtime/Calls/MethodDispatcher.Generated.cs b/Src/Ruby/Runtime/Calls/MethodDispatcher.Generated.cs index 4980cde3d..b99556372 100644 --- a/Src/Ruby/Runtime/Calls/MethodDispatcher.Generated.cs +++ b/Src/Ruby/Runtime/Calls/MethodDispatcher.Generated.cs @@ -171,8 +171,7 @@ public class RubyObjectMethodDispatcher/*$GenericDecl*/ : MethodDispatcher>)callSite). @@ -189,8 +188,7 @@ public class RubyObjectMethodDispatcherWithScope/*$GenericDecl*/ : MethodDispatc } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self /*$Parameters*/) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null /*$Arguments*/); } else { return ((CallSite>)callSite). @@ -207,8 +205,7 @@ public class RubyObjectMethodDispatcherWithBlock/*$GenericDecl*/ : MethodDispatc } public object Invoke(CallSite/*!*/ callSite, object self, TProc proc /*$Parameters*/) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc /*$Arguments*/); } else { @@ -227,8 +224,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock/*$GenericDecl*/ : Metho } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc /*$Parameters*/) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc /*$Arguments*/); } else { @@ -246,8 +242,7 @@ public class RubyObjectMethodDispatcher : MethodDispatcher>)callSite). @@ -264,8 +259,7 @@ public class RubyObjectMethodDispatcherWithScope : MethodDispatcher(CallSite/*!*/ callSite, TScope/*!*/ scope, object self ,T0 arg0) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null ,arg0); } else { return ((CallSite>)callSite). @@ -282,8 +276,7 @@ public class RubyObjectMethodDispatcherWithBlock : MethodDispatcher(CallSite/*!*/ callSite, object self, TProc proc ,T0 arg0) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0); } else { @@ -302,8 +295,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock : MethodDispatcher< } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc ,T0 arg0) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0); } else { @@ -318,8 +310,7 @@ public class RubyObjectMethodDispatcher : MethodDispatcher>)callSite). @@ -336,8 +327,7 @@ public class RubyObjectMethodDispatcherWithScope : MethodDispatcher(CallSite/*!*/ callSite, TScope/*!*/ scope, object self ,T0 arg0, T1 arg1) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null ,arg0, arg1); } else { return ((CallSite>)callSite). @@ -354,8 +344,7 @@ public class RubyObjectMethodDispatcherWithBlock : MethodDispatcher(CallSite/*!*/ callSite, object self, TProc proc ,T0 arg0, T1 arg1) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1); } else { @@ -374,8 +363,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock : MethodDispatc } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc ,T0 arg0, T1 arg1) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1); } else { @@ -390,8 +378,7 @@ public class RubyObjectMethodDispatcher : MethodDispatcher>)callSite). @@ -408,8 +395,7 @@ public class RubyObjectMethodDispatcherWithScope : MethodDispatcher< } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self ,T0 arg0, T1 arg1, T2 arg2) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null ,arg0, arg1, arg2); } else { return ((CallSite>)callSite). @@ -426,8 +412,7 @@ public class RubyObjectMethodDispatcherWithBlock : MethodDispatcher< } public object Invoke(CallSite/*!*/ callSite, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2); } else { @@ -446,8 +431,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock : MethodDis } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2); } else { @@ -462,8 +446,7 @@ public class RubyObjectMethodDispatcher : MethodDispatcher>)callSite). @@ -480,8 +463,7 @@ public class RubyObjectMethodDispatcherWithScope : MethodDispatc } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self ,T0 arg0, T1 arg1, T2 arg2, T3 arg3) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null ,arg0, arg1, arg2, arg3); } else { return ((CallSite>)callSite). @@ -498,8 +480,7 @@ public class RubyObjectMethodDispatcherWithBlock : MethodDispatc } public object Invoke(CallSite/*!*/ callSite, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2, T3 arg3) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2, arg3); } else { @@ -518,8 +499,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock : Metho } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2, T3 arg3) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2, arg3); } else { @@ -534,8 +514,7 @@ public class RubyObjectMethodDispatcher : MethodDispatcher>)callSite). @@ -552,8 +531,7 @@ public class RubyObjectMethodDispatcherWithScope : MethodDis } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self ,T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { return Method(self, null ,arg0, arg1, arg2, arg3, arg4); } else { return ((CallSite>)callSite). @@ -570,8 +548,7 @@ public class RubyObjectMethodDispatcherWithBlock : MethodDis } public object Invoke(CallSite/*!*/ callSite, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2, arg3, arg4); } else { @@ -590,8 +567,7 @@ public class RubyObjectMethodDispatcherWithScopeAndBlock : M } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, TProc proc ,T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { + if (IsCurrentFor(self)) { // Dispatching to a Ruby method - the method scope sets the proc's converter to itself, hence we don't need to do it here: return Method(self, (Proc)(object)proc ,arg0, arg1, arg2, arg3, arg4); } else { diff --git a/Src/Ruby/Runtime/Calls/MethodDispatcher.cs b/Src/Ruby/Runtime/Calls/MethodDispatcher.cs index 3074200b1..1382972ec 100644 --- a/Src/Ruby/Runtime/Calls/MethodDispatcher.cs +++ b/Src/Ruby/Runtime/Calls/MethodDispatcher.cs @@ -41,6 +41,29 @@ public abstract partial class MemberDispatcher { public abstract object/*!*/ CreateDelegate(bool isUntyped); + /// + /// The guard a precompiled dispatcher runs on every call: the receiver is a Ruby object + /// whose class has not had a method table change since the dispatcher was made. + /// RubyObject is the class of nearly every such receiver, and an exact type test is two + /// instructions inline, where the IRubyObject cast it saves is a call to the runtime's + /// interface-cast helper. Any other IRubyObject goes the old way. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool IsCurrentFor(object self) { + if (self != null) { + Type type = self.GetType(); + if (type == typeof(RubyObject)) { + return Unsafe.As(self).ImmediateClass.Version.Method == Version; + } + // a class or module receiver - Foo.new, Math.sqrt, any class method: + if (type == typeof(RubyClass) || type == typeof(RubyModule)) { + return Unsafe.As(self).ImmediateClass.Version.Method == Version; + } + } + IRubyObject obj = self as IRubyObject; + return obj != null && obj.ImmediateClass.Version.Method == Version; + } + internal static object CreateDispatcher(Type/*!*/ func, int mandatoryParamCount, bool hasScope, bool hasBlock, int version, Func parameterlessFactory, Type[] genericFactories) { Type[] funcArgs = func.GetGenericArguments(); @@ -102,6 +125,13 @@ internal static MethodDispatcher CreateRubyObjectDispatcher(Type/*!*/ func, Dele } internal abstract void Initialize(Delegate/*!*/ method, int version); + + /// + /// Points the dispatcher at the compiled form of the body it was made for (see + /// RubyMethodBody.RegisterDispatcher). Either delegate runs the same body, so a call racing + /// with this may use either one. + /// + internal abstract void SetMethod(Delegate/*!*/ method); } public abstract class MethodDispatcher : MethodDispatcher { @@ -112,6 +142,12 @@ internal override void Initialize(Delegate/*!*/ method, int version) { Method = (TRubyFunc)(object)method; Version = version; } + + internal override void SetMethod(Delegate/*!*/ method) { + if (method is TRubyFunc typed) { + Method = typed; + } + } } public abstract class AttributeDispatcher : MemberDispatcher { @@ -129,6 +165,27 @@ internal static AttributeDispatcher CreateRubyObjectWriterDispatcher(Type/*!*/ d } internal abstract void Initialize(string/*!*/ name, int version); + + /// + /// The receiver as an IRubyObject and its class, or null: an exact RubyObject is told apart + /// inline, without the interface cast (see MemberDispatcher.IsCurrentFor). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IRubyObject GetRubyObject(object self, out RubyClass cls) { + if (self != null && self.GetType() == typeof(RubyObject)) { + var obj = Unsafe.As(self); + cls = obj.ImmediateClass; + return obj; + } + if (self != null && (self.GetType() == typeof(RubyClass) || self.GetType() == typeof(RubyModule))) { + var module = Unsafe.As(self); + cls = module.ImmediateClass; + return module; + } + IRubyObject result = self as IRubyObject; + cls = (result != null) ? result.ImmediateClass : null; + return result; + } } public sealed class RubyObjectAttributeReaderDispatcherWithScope : AttributeDispatcher { @@ -145,9 +202,10 @@ internal override void Initialize(string/*!*/ name, int version) { } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { - return RubyOps.ReadInstanceVariable(obj.ImmediateClass.Context, obj, Site); + RubyClass cls; + IRubyObject obj = GetRubyObject(self, out cls); + if (obj != null && cls.Version.Method == Version) { + return RubyOps.ReadInstanceVariable(cls.Context, obj, Site); } else { return ((CallSite>)callSite).Update(callSite, scope, self); } @@ -168,9 +226,10 @@ internal override void Initialize(string/*!*/ name, int version) { } public object Invoke(CallSite/*!*/ callSite, TScope/*!*/ scope, object self, T0 arg0) { - IRubyObject obj = self as IRubyObject; - if (obj != null && obj.ImmediateClass.Version.Method == Version) { - return RubyOps.WriteInstanceVariable(obj.ImmediateClass.Context, obj, (object)arg0, Site); + RubyClass cls; + IRubyObject obj = GetRubyObject(self, out cls); + if (obj != null && cls.Version.Method == Version) { + return RubyOps.WriteInstanceVariable(cls.Context, obj, (object)arg0, Site); } else { return ((CallSite>)callSite).Update(callSite, scope, self, arg0); } diff --git a/Src/Ruby/Runtime/Calls/RubyCallAction.cs b/Src/Ruby/Runtime/Calls/RubyCallAction.cs index 07afd8c5a..c40b24d51 100644 --- a/Src/Ruby/Runtime/Calls/RubyCallAction.cs +++ b/Src/Ruby/Runtime/Calls/RubyCallAction.cs @@ -108,9 +108,12 @@ internal protected RubyCallAction(RubyContext context, string/*!*/ methodName, R #region Precompiled Rules protected override object BindPrecompiled(Type/*!*/ delegateType, object[]/*!*/ args) { - if (Context == null || + // IsVariableCall only decides which error a call that finds no method raises; a dispatcher + // is only made for a method that was found, and hands every other case back to the + // binder. Leaving it out of the mask sent every bare `foo` to a rule of its own. + if (Context == null || Signature.ResolveOnly || - (Signature.Flags & ~(RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasScope | RubyCallFlags.HasBlock | RubyCallFlags.HasRhsArgument | RubyCallFlags.HasSourceBlock)) != 0) { + (Signature.Flags & ~(RubyCallFlags.HasImplicitSelf | RubyCallFlags.HasScope | RubyCallFlags.HasBlock | RubyCallFlags.HasRhsArgument | RubyCallFlags.HasSourceBlock | RubyCallFlags.IsVariableCall)) != 0) { return null; } @@ -123,10 +126,13 @@ protected override object BindPrecompiled(Type/*!*/ delegateType, object[]/*!*/ // Nor can it hold RubyMethodInfo.BuildCallNoFlow's unmarking of a keyword-arguments // hash handed to a method that declares no keywords. A call site's syntax decides // whether it passes keywords, so seeing one here means every call at this site - // passes one: hand the site to the rule builder instead of precompiling it. + // passes one: unless the method takes keywords (checked below), hand the site to the + // rule builder instead of precompiling it. + bool passesKeywords = false; for (int i = 0; i < args.Length; i++) { if (RubyOps.IsKeywordArgumentsHash(args[i])) { - return null; + passesKeywords = true; + break; } } @@ -163,6 +169,11 @@ protected override object BindPrecompiled(Type/*!*/ delegateType, object[]/*!*/ return null; } + // a method that takes or refuses keywords reads the hash as it is - nothing to unmark + if (passesKeywords && (rubyMethod == null || !rubyMethod.TakesOrRefusesKeywords)) { + return null; + } + var dispatcher = method.Info.GetDispatcher(delegateType, Signature, target, version); if (dispatcher != null) { object result = dispatcher.CreateDelegate(MethodDispatcher.UntypedFuncs.Contains(delegateType)); @@ -175,12 +186,192 @@ protected override object BindPrecompiled(Type/*!*/ delegateType, object[]/*!*/ #endregion + #region Shared Rules + + // A rule that serves receivers of several classes, or of one class in several states, that resolve the call to the same method. + // The first rule bound for a method is an ordinary one (tests a constant version); the second class (version) to resolve the call + // to that method gets a rule that tests the class version against a set (see RuleVersionSet), and every further one is added to + // the set once the call has been resolved for it again - with no rule built or compiled. + private sealed class SharedRule { + internal readonly RubyMemberInfo/*!*/ Member; + internal readonly Type/*!*/ TargetType; + internal readonly Type/*!*/ DelegateType; + internal readonly int FirstVersion; + + // null until the shared rule is built: + internal RuleVersionSet Versions; + internal volatile object Rule; + + internal SharedRule(RubyMemberInfo/*!*/ member, Type/*!*/ targetType, Type/*!*/ delegateType, int firstVersion) { + Member = member; + TargetType = targetType; + DelegateType = delegateType; + FirstVersion = firstVersion; + } + } + + private const int MaxSharedRules = 16; + + private const RubyCallFlags SharableCallFlags = RubyCallFlags.HasScope | RubyCallFlags.HasSplattedArgument | RubyCallFlags.HasRhsArgument | + RubyCallFlags.HasBlock | RubyCallFlags.HasImplicitSelf | RubyCallFlags.IsVariableCall | RubyCallFlags.HasSourceBlock; + + private static readonly object _SharedRulesLock = new object(); + private SharedRule[] _sharedRules; + + // Handed from BindSharedRule to Build of the same binder: + [ThreadStatic] + private static RuleVersionSet _PendingVersionSet; + [ThreadStatic] + private static RubyCallAction _PendingVersionSetOwner; + + internal override T BindShared(object[]/*!*/ args) { + RubyContext context = Context; + if (context == null || Signature.ResolveOnly || (Signature.Flags & ~SharableCallFlags) != 0 || + context.HasRefinements || TracePoint.CCallTracing) { + return null; + } + + RubyScope scope; + object target; + if (Signature.HasScope) { + scope = (RubyScope)args[0]; + target = args[1]; + } else { + scope = context.EmptyScope; + target = args[0]; + } + + // The type test of a Ruby object is only a version check (see MetaObjectBuilder.AddTargetTypeTest), and so is that of nil + // and of a CLR object whose class has neither singletons nor instances of a Ruby subclass. Not true/false: they are + // told apart by value. + Type targetType; + if (target == null) { + targetType = typeof(DynamicNull); + } else if (target is bool) { + return null; + } else { + targetType = target.GetType(); + } + + // an object of another runtime: + RubyClass targetClass = context.GetImmediateClassOf(target); + if (targetClass.Context != context) { + return null; + } + + int version; + MethodResolutionResult method; + using (context.ClassHierarchyLocker()) { + targetClass.InitializeMethodsNoLock(); + if (target != null && !(target is IRubyObject) && !HasPlainClrTypeTest(targetClass, _methodName)) { + return null; + } + version = targetClass.Version.Method; + method = targetClass.ResolveMethodForSiteNoLock(_methodName, GetVisibilityContext(Signature, scope), MethodLookup.Default); + } + + // Rules calling these methods depend on nothing but the method, the call signature and the arguments. + // A protected method's rule also tests the caller's class. + if (!method.Found || method.Info.IsProtected || + method.Info.GetType() != typeof(RubyLibraryMethodInfo) && method.Info.GetType() != typeof(RubyMethodInfo)) { + return null; + } + + // respond_to?(:sym) and send(:sym) rules are specialized on the receiver's class (DispatchIntrinsics): + if (DispatchIntrinsics.MaySpecialize(method.Info)) { + return null; + } + + SharedRule shared = null; + var sharedRules = _sharedRules; + if (sharedRules != null) { + foreach (var rule in sharedRules) { + if (rule.Member == method.Info && rule.TargetType == targetType && rule.DelegateType == typeof(T)) { + shared = rule; + break; + } + } + } + + if (shared == null) { + lock (_SharedRulesLock) { + sharedRules = _sharedRules; + if (sharedRules == null || sharedRules.Length < MaxSharedRules) { + _sharedRules = ArrayUtils.Append(sharedRules ?? new SharedRule[0], new SharedRule(method.Info, targetType, typeof(T), version)); + } + } + return null; + } + + object sharedRule = shared.Rule; + if (sharedRule == null) { + // the first rule bound for the method didn't match for some other reason than its class version: + if (version == shared.FirstVersion) { + return null; + } + + return BindSharedRule(shared, args); + } + + // The shared rule is handed out for a version it does not hold yet, and only then: a call that comes here although + // the rule holds its version didn't match the rule (its arguments are of other types, say) and gets a rule of its own. + if (!shared.Versions.TryAdd(version)) { + return null; + } + + return (T)sharedRule; + } + + // MetaObjectBuilder.AddTargetTypeTest tests a CLR object's type and its class version - and nothing else - in this case: + private static bool HasPlainClrTypeTest(RubyClass/*!*/ targetClass, string/*!*/ methodName) { + if (targetClass.IsSingletonClass || targetClass.GetNonSingletonClass().IsRubyClass) { + return false; + } + var nominalClass = targetClass.NominalClass; + var singletonMethods = nominalClass.ClrSingletonMethods; + return !nominalClass.HasAdoptedInstances && (singletonMethods == null || + !singletonMethods.ContainsKey(methodName) && !singletonMethods.ContainsKey(Symbols.MethodMissing)); + } + + private T/*!*/ BindSharedRule(SharedRule/*!*/ shared, object[]/*!*/ args) where T : class { + var versionSet = new RuleVersionSet(); + T result; + + _PendingVersionSet = versionSet; + _PendingVersionSetOwner = this; + try { + result = this.LightBind(args, Context.Options.CompilationThreshold); + } finally { + _PendingVersionSet = null; + _PendingVersionSetOwner = null; + } + CacheTarget(result); + + // The rule tests the version set iff Build has taken it and bound it to a version the method was resolved for: + if (versionSet.Member == shared.Member) { + lock (_SharedRulesLock) { + if (shared.Rule == null) { + shared.Versions = versionSet; + shared.Rule = result; + } + } + } + return result; + } + + #endregion + private static bool RequiresUnusedBlockCheck(RubyMemberInfo/*!*/ method) { var rubyMethod = method as RubyMethodInfo; return rubyMethod != null && !rubyMethod.UsesBlock; } protected override bool Build(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ args, bool defaultFallback) { + if (_PendingVersionSetOwner == this) { + metaBuilder.TargetVersionSet = _PendingVersionSet; + _PendingVersionSet = null; + _PendingVersionSetOwner = null; + } return BuildCall(metaBuilder, _methodName, args, defaultFallback, true); } @@ -206,6 +397,15 @@ internal static bool BuildCall(MetaObjectBuilder/*!*/ metaBuilder, string/*!*/ m return true; } + if (DispatchIntrinsics.TryBuildRespondTo(metaBuilder, args, method.Info)) { + return true; + } + + string sentName; + if (defaultFallback && callClrMethods && DispatchIntrinsics.TryBuildSend(metaBuilder, args, method.Info, out sentName)) { + return BuildCall(metaBuilder, sentName, args, defaultFallback, callClrMethods); + } + method.Info.BuildCall(metaBuilder, args, methodName); // MRI 3.4+: a block passed to a method that never uses it may be ignored @@ -287,8 +487,11 @@ internal static MethodResolutionResult Resolve(MetaObjectBuilder/*!*/ metaBuilde var visibilityContext = GetVisibilityContext(args.Signature, args.Scope); RefinementActivation refinements = null; using (targetClass.Context.ClassHierarchyLocker()) { + var versionSet = metaBuilder.TargetVersionSet; + metaBuilder.TargetVersionSet = null; + metaBuilder.AddTargetTypeTest(args.Target, targetClass, args.TargetExpression, args.MetaContext, - new[] { methodName, Symbols.MethodMissing } + new[] { methodName, Symbols.MethodMissing }, versionSet ); // Refinements are lexically scoped, so resolution has to consult the scope the call site was @@ -312,6 +515,11 @@ internal static MethodResolutionResult Resolve(MetaObjectBuilder/*!*/ metaBuilde } else { methodMissing = null; } + + // the version the test was bound against resolves to this method: + if (versionSet != null && versionSet.Count > 0) { + versionSet.Member = method.Found ? method.Info : null; + } } // The rule is only valid while the caller's lexical activation is the one it was bound against. diff --git a/Src/Ruby/Runtime/Calls/RubyMetaBinder.cs b/Src/Ruby/Runtime/Calls/RubyMetaBinder.cs index 2c7860e58..bfade91c3 100644 --- a/Src/Ruby/Runtime/Calls/RubyMetaBinder.cs +++ b/Src/Ruby/Runtime/Calls/RubyMetaBinder.cs @@ -102,6 +102,10 @@ public sealed override T BindDelegate(CallSite site, object[] args) { ), "BIND"); } #endif + if (argArray == null && (result = BindShared(args)) != null) { + return result; + } + result = this.LightBind(args, context.Options.CompilationThreshold); CacheTarget(result); return result; @@ -114,6 +118,13 @@ protected virtual object BindPrecompiled(Type/*!*/ delegateType, object[]/*!*/ a return null; } + /// + /// Returns a rule that is shared with receivers of other classes the call resolves the same for, or null (see ). + /// + internal virtual T BindShared(object[]/*!*/ args) where T : class { + return null; + } + public override DynamicMetaObject/*!*/ Bind(DynamicMetaObject/*!*/ scopeOrContextOrTargetOrArgArray, DynamicMetaObject/*!*/[]/*!*/ args) { PerfTrack.NoteEvent(PerfTrack.Categories.Binding, "Ruby: " + GetType().Name + Signature.ToString() + ": Bind"); diff --git a/Src/Ruby/Runtime/Calls/RubyMethodBody.cs b/Src/Ruby/Runtime/Calls/RubyMethodBody.cs index 65bfd1fa8..99e77d1f1 100644 --- a/Src/Ruby/Runtime/Calls/RubyMethodBody.cs +++ b/Src/Ruby/Runtime/Calls/RubyMethodBody.cs @@ -106,6 +106,7 @@ internal Delegate GetDelegate(RubyScope/*!*/ declaringScope, RubyModule/*!*/ dec _singletonLexicalModules = GetObjectSingletonLexicalModules(declaringScope); _delegateModule = declaringModule; _delegate = primary = Compile(declaringScope, declaringModule); + WatchLightCompilation(primary); } } } @@ -153,6 +154,70 @@ internal Delegate GetDelegate(RubyScope/*!*/ declaringScope, RubyModule/*!*/ dec return primary; } + #region Compiled-body forwarding + + // The primary delegate starts out as the light compiler's interpreted LightLambda, which goes + // on standing in front of the IL it compiles after a few dozen calls: every call through it is + // one more delegate hop plus a cast of the compiled delegate. Precompiled dispatchers made + // while the body was interpreted are recorded here and pointed at the compiled delegate when + // it arrives; later binds get it from GetDelegate. Rules already built keep the LightLambda, + // which forwards correctly - just not as cheaply. + private Delegate _interpretedDelegate; + private List _interpretedDispatchers; + private Delegate _compiledFrom, _compiledTo; + + private void WatchLightCompilation(Delegate/*!*/ primary) { + var light = primary.Target as Microsoft.Scripting.Interpreter.LightLambda; + if (light != null) { + _interpretedDelegate = primary; + light.Compile += (sender, e) => OnLightCompiled(primary, e.Compiled); + } + } + + private void OnLightCompiled(Delegate/*!*/ interpreted, Delegate/*!*/ compiled) { + List dispatchers; + lock (this) { + if (_delegate != interpreted || compiled.GetType() != interpreted.GetType()) { + return; + } + _delegate = compiled; + _interpretedDelegate = null; + _compiledFrom = interpreted; + _compiledTo = compiled; + dispatchers = _interpretedDispatchers; + _interpretedDispatchers = null; + } + if (dispatchers != null) { + foreach (var dispatcher in dispatchers) { + dispatcher.SetMethod(compiled); + } + } + } + + /// + /// Called for each dispatcher made over , a delegate GetDelegate returned. + /// + internal void RegisterDispatcher(MethodDispatcher/*!*/ dispatcher, Delegate/*!*/ method) { + Delegate compiled; + lock (this) { + if (method == _interpretedDelegate) { + if (_interpretedDispatchers == null) { + _interpretedDispatchers = new List(); + } + _interpretedDispatchers.Add(dispatcher); + return; + } + if (method != _compiledFrom) { + return; + } + compiled = _compiledTo; + } + // compiled between GetDelegate and here: + dispatcher.SetMethod(compiled); + } + + #endregion + private Delegate _moduleFunctionDelegate; // The refinements that were active where the body was last defined for a module, which is @@ -181,6 +246,9 @@ internal void SetDefinitionRefinements(RubyModule/*!*/ declaringModule, Refineme _singletonLexicalModules = null; _lexicalVariants = null; _moduleFunctionDelegate = null; + _interpretedDelegate = null; + _interpretedDispatchers = null; + _compiledFrom = _compiledTo = null; } GetDefinitionRefinementsNoLock(declaringModule).Value = refinements; } diff --git a/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs b/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs index c1970bfaa..8ec2ad109 100644 --- a/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs +++ b/Src/Ruby/Runtime/Calls/RubyMethodInfo.cs @@ -60,6 +60,7 @@ internal RubyMethodInfo(RubyMethodBody/*!*/ body, RubyScope/*!*/ declaringScope, _body = body; _declaringScope = declaringScope; + declaringScope.MarkCaptured(); } protected internal override RubyMemberInfo/*!*/ Copy(RubyMemberFlags flags, RubyModule/*!*/ module) { @@ -209,17 +210,36 @@ public override int GetArity() { #region Dynamic Sites internal override MemberDispatcher GetDispatcher(Type/*!*/ delegateType, RubyCallSignature signature, object target, int version) { - if (Parameters.Unsplat != null || Parameters.Optional.Length > 0) { + if (Parameters.Unsplat != null) { return null; } + // The keywords of a method that declares nothing but mandatory positionals besides them + // were lowered onto one trailing optional parameter (see PrismAstBridge.LowerKeywords and + // RefuseKeywords). A call that fills it - a keyword call - passes the keyword hash to it + // unchanged, which is all the rule would do (BuildCallNoFlow leaves the trailing hash of a + // method that takes or refuses keywords alone). A call that omits it still gets a rule: + // CreateDispatcher refuses a site whose argument count differs. + int parameterCount = Parameters.Mandatory.Length; + if (Parameters.Optional.Length > 0) { + if (!TakesOrRefusesKeywords) { + return null; + } + parameterCount += Parameters.Optional.Length; + } + if (!(target is IRubyObject)) { return null; } - return MethodDispatcher.CreateRubyObjectDispatcher( - delegateType, GetDelegate(), Parameters.Mandatory.Length, signature.HasScope, signature.HasBlock, version + var method = GetDelegate(); + var dispatcher = MethodDispatcher.CreateRubyObjectDispatcher( + delegateType, method, parameterCount, signature.HasScope, signature.HasBlock, version ); + if (dispatcher != null && !_isModuleFunctionCopy) { + _body.RegisterDispatcher(dispatcher, method); + } + return dispatcher; } internal override void BuildCallNoFlow(MetaObjectBuilder/*!*/ metaBuilder, CallArguments/*!*/ args, string/*!*/ name) { diff --git a/Src/Ruby/Runtime/Calls/RuleVersionSet.cs b/Src/Ruby/Runtime/Calls/RuleVersionSet.cs new file mode 100644 index 000000000..7b915749a --- /dev/null +++ b/Src/Ruby/Runtime/Calls/RuleVersionSet.cs @@ -0,0 +1,148 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +using System.Linq.Expressions; + +using System; +using System.Reflection; +using System.Threading; +using Microsoft.Scripting.Interpreter; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Runtime.Calls { + /// + /// The class versions () a shared call-site rule holds for. + /// + /// A rule bound for a receiver whose class is an is valid as long as the + /// receiver's immediate class has the version it was bound against. Method versions are unique + /// across all classes and never reused, so a version identifies a class in one particular state. + /// When receivers of many classes - or one class in many states - resolve a call to the same method, + /// one rule can serve all of them: its test checks the receiver's class version against this set, + /// and every version is added only after the method has been resolved again for that class + /// (see RubyCallAction.BindShared). This saves building and compiling a rule per class, which is + /// what calls in class bodies (private, attr_reader, alias_method, const_set...) and calls on + /// classes whose version was bumped by a method definition elsewhere would otherwise cost. + /// + public sealed class RuleVersionSet { + // open addressing, 0 marks an empty slot (method versions start at 1); + // readers see either the old or the new array, an insert writes a single slot: + private int[] _slots; + private int _count; + + /// + /// The version added last. A rule that runs hot mostly runs for the class (state) it was last bound for, + /// so its test compares with this field before it looks in the set. + /// + public int LastVersion; + + // stop growing: further classes get rules of their own + private const int MaxCount = 4096; + + /// + /// The method the rule calls; set while the rule is built (under the class hierarchy lock). + /// + internal RubyMemberInfo Member; + + internal RuleVersionSet() { + _slots = new int[8]; + } + + internal int Count { + get { return _count; } + } + + public bool Contains(int version) { + int[] slots = Volatile.Read(ref _slots); + int mask = slots.Length - 1; + int i = version & mask; + while (true) { + int slot = slots[i]; + if (slot == version) { + return slot != 0; + } + if (slot == 0) { + return false; + } + i = (i + 1) & mask; + } + } + + /// + /// Adds a version the rule has been verified to hold for. Returns false if the set already holds it or is full. + /// + internal bool TryAdd(int version) { + if (version == 0) { + return false; + } + + lock (this) { + if (Contains(version)) { + return false; + } + + if (_count >= MaxCount) { + return false; + } + + int[] slots = _slots; + if ((_count + 1) * 2 > slots.Length) { + var newSlots = new int[slots.Length * 2]; + foreach (int v in slots) { + if (v != 0) { + Insert(newSlots, v); + } + } + Insert(newSlots, version); + Volatile.Write(ref _slots, newSlots); + } else { + Insert(slots, version); + } + _count++; + LastVersion = version; + return true; + } + } + + private static void Insert(int[]/*!*/ slots, int version) { + int mask = slots.Length - 1; + int i = version & mask; + while (slots[i] != 0) { + i = (i + 1) & mask; + } + slots[i] = version; + } + + public static bool IsValid(RuleVersionSet/*!*/ set, int version) { + return set.Contains(version); + } + + private static MethodInfo _IsValidMethod; + + private static FieldInfo _LastVersionField; + + /// + /// #{versionExpression} == #{set}.LastVersion || (#{versionExpression}) in #{set} + /// + internal Expression/*!*/ MakeTest(Expression/*!*/ versionExpression) { + var method = _IsValidMethod ?? (_IsValidMethod = CallInstruction.CacheFunc(IsValid)); + var lastVersion = _LastVersionField ?? (_LastVersionField = typeof(RuleVersionSet).GetField("LastVersion")); + var set = AstUtils.Constant(this); + return Expression.OrElse( + Expression.Equal(versionExpression, Expression.Field(set, lastVersion)), + Expression.Call(method, set, versionExpression) + ); + } + } +} diff --git a/Src/Ruby/Runtime/Conversions/ProtocolConversionAction.cs b/Src/Ruby/Runtime/Conversions/ProtocolConversionAction.cs index 31e7fc278..f6dd87908 100644 --- a/Src/Ruby/Runtime/Conversions/ProtocolConversionAction.cs +++ b/Src/Ruby/Runtime/Conversions/ProtocolConversionAction.cs @@ -441,8 +441,18 @@ protected virtual void SetError(MetaObjectBuilder/*!*/ metaBuilder, CallArgument public abstract class ProtocolConversionAction : ProtocolConversionAction where TSelf : ProtocolConversionAction, new() { + // The action Make handed out last. Library methods ask for their conversion on every + // call (Protocols.CastToFixnum and friends), and the factory's locked, type-keyed lookup + // costs far more than the conversion itself. One action per runtime, so a single slot + // answers every call of a process that has one runtime; a second runtime just misses. + private static TSelf _lastMade; + public static TSelf/*!*/ Make(RubyContext/*!*/ context) { - return context.MetaBinderFactory.Conversion(); + TSelf last = _lastMade; + if (last != null && ReferenceEquals(last.Context, context)) { + return last; + } + return _lastMade = context.MetaBinderFactory.Conversion(); } [Emitted] diff --git a/Src/Ruby/Runtime/DispatchStorages.cs b/Src/Ruby/Runtime/DispatchStorages.cs new file mode 100644 index 000000000..91c60e3e6 --- /dev/null +++ b/Src/Ruby/Runtime/DispatchStorages.cs @@ -0,0 +1,188 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using IronRuby.Builtins; +using IronRuby.Compiler.Generation; +using IronRuby.Runtime.Calls; + +namespace IronRuby.Runtime { + + // Storages of the dynamic-dispatch helpers (#send, #__send__, #public_send, #respond_to?). + // + // A library method's storage is created for the rule that binds the call, so it is a per-site + // cache - shared only with the other sites of the same binder that the binder's rule cache + // hands the same rule (sites of the same arity that saw a receiver of the same class). That is + // why each of these remembers a few names rather than one. + + /// + /// The call sites #send and friends dispatch through, by method name. A send of a literal name + /// finds its site by comparing one reference instead of taking the context's lock and hashing the + /// name (), and each name gets a site of its own, as + /// monomorphic as the send site itself is. + /// + public sealed class SendSiteStorage : RubyCallSiteStorage where TSiteFunc : class { + private sealed class Entry { + internal readonly string/*!*/ Name; + internal readonly RubyCallSignature Signature; + internal readonly CallSite/*!*/ Site; + + internal Entry(string/*!*/ name, RubyCallSignature signature, CallSite/*!*/ site) { + Name = name; + Signature = signature; + Site = site; + } + } + + // Past this many names a site sends computed names; those share the context's sites. + private const int Capacity = 8; + + // Filled in order and never replaced, so a reader needs no lock. + private readonly Entry[]/*!*/ _entries = new Entry[Capacity]; + private int _count; + + [Emitted] + public SendSiteStorage(RubyContext/*!*/ context) : base(context) { } + + public CallSite/*!*/ GetCallSite(string/*!*/ methodName, RubyCallSignature signature) { + var entries = _entries; + for (int i = 0; i < entries.Length; i++) { + var entry = entries[i]; + if (entry == null) { + break; + } + if ((object)entry.Name == (object)methodName && entry.Signature.Equals(signature)) { + return entry.Site; + } + } + return GetCallSiteSlow(methodName, signature); + } + + private CallSite/*!*/ GetCallSiteSlow(string/*!*/ methodName, RubyCallSignature signature) { + // the same name in another string instance: + var entries = _entries; + for (int i = 0; i < entries.Length; i++) { + var entry = entries[i]; + if (entry == null) { + break; + } + if (entry.Name == methodName && entry.Signature.Equals(signature)) { + return entry.Site; + } + } + + lock (entries) { + int count = _count; + for (int i = 0; i < count; i++) { + var entry = entries[i]; + if (entry.Name == methodName && entry.Signature.Equals(signature)) { + return entry.Site; + } + } + + if (count < Capacity) { + var site = CallSite.Create(RubyCallAction.Make(Context, methodName, signature)); + Volatile.Write(ref entries[count], new Entry(methodName, signature, site)); + _count = count + 1; + return site; + } + } + + return Context.GetOrCreateSendSite(methodName, signature); + } + } + + /// + /// #respond_to?'s answers at one site, and the site it calls a user-defined respond_to_missing? through. + /// An answer is kept only when it came from method lookups alone, and only for as long as the + /// receiver class's method version stays where it was when they were made - the validity the + /// class's own lookup cache has (), which + /// registers its lookups for invalidation exactly as call sites do. + /// + public sealed class RespondToCacheStorage : CallSiteStorage> { + private sealed class Answer { + internal readonly RubyClass/*!*/ Class; + internal readonly int Version; + internal readonly string/*!*/ Name; + internal readonly bool IncludePrivate; + internal readonly bool Result; + + internal Answer(RubyClass/*!*/ cls, int version, string/*!*/ name, bool includePrivate, bool result) { + Class = cls; + Version = version; + Name = name; + IncludePrivate = includePrivate; + Result = result; + } + } + + private const int Capacity = 8; + + // Each slot is replaced as a whole, so a reader needs no lock. + private readonly Answer[]/*!*/ _answers = new Answer[Capacity]; + private int _next; + + [Emitted] + public RespondToCacheStorage(RubyContext/*!*/ context) : base(context) { } + + public bool TryGetAnswer(RubyClass/*!*/ cls, string/*!*/ name, bool includePrivate, out bool result) { + var answers = _answers; + for (int i = 0; i < answers.Length; i++) { + var answer = answers[i]; + if (answer == null) { + break; + } + if ((object)answer.Name == (object)name && answer.Class == cls && answer.IncludePrivate == includePrivate + && answer.Version == cls.Version.Method) { + result = answer.Result; + return true; + } + } + result = false; + return false; + } + + /// + /// Keeps an answer computed from lookups made at method version of + /// (-1: lookups that must not be kept). + /// + public void SetAnswer(RubyClass/*!*/ cls, int version, string/*!*/ name, bool includePrivate, bool result) { + if (version < 0) { + return; + } + + var answers = _answers; + var newAnswer = new Answer(cls, version, name, includePrivate, result); + + // an answer for the same question made at an older version is replaced in place: + for (int i = 0; i < answers.Length; i++) { + var answer = answers[i]; + if (answer == null) { + break; + } + if ((object)answer.Name == (object)name && answer.Class == cls && answer.IncludePrivate == includePrivate) { + Volatile.Write(ref answers[i], newAnswer); + return; + } + } + + int index = _next; + _next = (index + 1) % Capacity; + Volatile.Write(ref answers[index], newAnswer); + } + } +} diff --git a/Src/Ruby/Runtime/Globals/GlobalVariableInfo.cs b/Src/Ruby/Runtime/Globals/GlobalVariableInfo.cs index bdcf49787..a6f5e29d5 100644 --- a/Src/Ruby/Runtime/Globals/GlobalVariableInfo.cs +++ b/Src/Ruby/Runtime/Globals/GlobalVariableInfo.cs @@ -40,6 +40,15 @@ public override object GetValue(RubyContext/*!*/ context, RubyScope scope) { } public override void SetValue(RubyContext/*!*/ context, RubyScope scope, string/*!*/ name, object value) { + Assign(value); + } + + // What a GlobalVariableSite reads and writes directly: an ordinary global is a slot. + internal object Value { + get { return _value; } + } + + internal void Assign(object value) { _value = value; _isDefined = true; } diff --git a/Src/Ruby/Runtime/Globals/GlobalVariableSite.cs b/Src/Ruby/Runtime/Globals/GlobalVariableSite.cs new file mode 100644 index 000000000..83fededae --- /dev/null +++ b/Src/Ruby/Runtime/Globals/GlobalVariableSite.cs @@ -0,0 +1,73 @@ +/* **************************************************************************** + * + * Copyright (c) Microsoft Corporation. + * + * This source code is subject to terms and conditions of the Apache License, Version 2.0. A + * copy of the license can be found in the License.html file at the root of this distribution. If + * you cannot locate the Apache License, Version 2.0, please send an email to + * ironruby@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + * by the terms of the Apache License, Version 2.0. + * + * You must not remove this notice, or any other, from this software. + * + * + * ***************************************************************************/ + +using System; +using System.Threading; + +namespace IronRuby.Runtime { + /// + /// One per global variable reference in compiled code (`$x', `$x = v'). Remembers the + /// GlobalVariable object the name denoted, stamped with the version of the context's name + /// table it was looked up at, so a read or write of the global needs neither the dictionary + /// nor the GlobalVariablesLock while no global has been defined, aliased or removed since. + /// + [CLSCompliant(false)] + public sealed class GlobalVariableSite { + private sealed class Binding { + internal readonly int Version; + // null: the name denotes no global at this version + internal readonly GlobalVariable Variable; + + internal Binding(int version, GlobalVariable variable) { + Version = version; + Variable = variable; + } + } + + internal readonly string/*!*/ Name; + + // Version and variable travel together in one immutable object, so a reader never pairs + // a version with the variable of another one. + private volatile Binding _binding; + + public GlobalVariableSite(string/*!*/ name) { + Name = name; + } + + /// The variable the name denotes in the context, or null if it denotes none. + internal GlobalVariable GetVariable(RubyContext/*!*/ context) { + Binding binding = _binding; + if (binding != null && binding.Version == context.GlobalVariablesVersion) { + return binding.Variable; + } + + GlobalVariable variable; + int version; + context.TryGetGlobalVariable(Name, out variable, out version); + _binding = new Binding(version, variable); + return variable; + } + + /// + /// Versions come from one process-wide counter, so a version number belongs to exactly + /// one context and a site can never take another context's binding for its own. + /// + private static int _lastVersion; + + internal static int NextVersion() { + return Interlocked.Increment(ref _lastVersion); + } + } +} diff --git a/Src/Ruby/Runtime/Jit/JitCompiler.cs b/Src/Ruby/Runtime/Jit/JitCompiler.cs index 108a3b8e1..c7ae4ffa6 100644 --- a/Src/Ruby/Runtime/Jit/JitCompiler.cs +++ b/Src/Ruby/Runtime/Jit/JitCompiler.cs @@ -87,6 +87,10 @@ internal sealed class JitCompiler { // _canDeopt: the body contains an operation that can signal a deopt. private bool _pure = true; private bool _canDeopt; + // The body writes an instance variable. A write to a frozen object deopts (JitRuntime.SetIVar) + // so that the generic body raises the FrozenError from a real frame; it is always the first + // write that does, before anything observable, so the restart is safe whatever _pure says. + private bool _ivarWrite; private bool _selfCall; private int _nodes; @@ -108,23 +112,43 @@ internal sealed class JitCompiler { return JitBailout.Instance; } - private JitCompiler(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/*!*/ paramTypes) { + // A method with keyword parameters: its keyword locals are typed parameters too, after the + // positional ones, and the body starts after the prologue that binds them (JitKeywords). + private readonly JitKeywords _keywords; + + private JitCompiler(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/*!*/ paramTypes, JitKeywords keywords) { _ast = ast; _context = context; _paramTypes = paramTypes; + _keywords = keywords; _scopeDepth = ast.DefinedScope.Depth; _self = Ast.Parameter(typeof(object), "#self"); PushScope(); + // The typed body's parameters are the generic delegate's: every mandatory one, then + // the optional ones, whose defaults the entry has already filled in (JitStub). var mandatory = ast.Parameters.Mandatory; - _paramNames = new string[mandatory.Length]; - for (int i = 0; i < mandatory.Length; i++) { - var lv = mandatory[i] as LocalVariable; - if (lv == null) { throw JitBailout.Instance; } - _paramNames[i] = lv.Name; - var p = Ast.Parameter(ClrType(paramTypes[i]), lv.Name); - _vars[0].Add(lv.Name, p); - _types[0].Add(lv.Name, paramTypes[i]); + // The typed body's parameters are the generic delegate's positional ones - every mandatory + // one, then the optional ones, whose defaults the entry has already filled in (JitStub) - + // or, for a method with keywords, the mandatory ones and then its keyword locals. + var optional = ast.Parameters.Optional; + int expected = mandatory.Length + ((keywords != null) ? keywords.Count : optional.Length); + _paramNames = new string[paramTypes.Length]; + if (paramTypes.Length != expected) { throw JitBailout.Instance; } + for (int i = 0; i < paramTypes.Length; i++) { + string name; + if (i < mandatory.Length || keywords == null) { + var lv = (i < mandatory.Length) ? mandatory[i] as LocalVariable : optional[i - mandatory.Length].Left as LocalVariable; + if (lv == null) { throw JitBailout.Instance; } + name = lv.Name; + } else { + name = keywords.LocalNames[i - mandatory.Length]; + } + if (_vars[0].ContainsKey(name)) { throw JitBailout.Instance; } + _paramNames[i] = name; + var p = Ast.Parameter(ClrType(paramTypes[i]), name); + _vars[0].Add(name, p); + _types[0].Add(name, paramTypes[i]); } } @@ -139,8 +163,13 @@ private JitCompiler(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/* private readonly List _slots; private MSA.LabelTarget _breakLabel, _continueLabel; + // Loop mode: what an instance variable of the loop's self holds right now, as a type + // (see NumericIVar). Null when the loop was entered without its self, which is then + // unbound and the loop may not touch self or an instance variable. + private readonly Func _ivarProfile; + private JitCompiler(WhileLoopExpression/*!*/ loop, RubyContext/*!*/ context, int scopeDepth, - Func/*!*/ bindSlot) { + Func/*!*/ bindSlot, MSA.ParameterExpression self, Func ivarProfile) { _ast = null; _loop = loop; _context = context; @@ -149,21 +178,30 @@ private JitCompiler(WhileLoopExpression/*!*/ loop, RubyContext/*!*/ context, int _slots = new List(); _paramTypes = new JT[0]; _paramNames = new string[0]; - _self = Ast.Parameter(typeof(object), "#self"); + _self = self ?? Ast.Parameter(typeof(object), "#self"); + _ivarProfile = (self != null) ? ivarProfile : null; PushScope(); } /// /// Emits a typed body for one `while' loop. The slots it bound - the scope locals it /// touched, and the type each of them held when the loop turned hot - come back in - /// ; the caller loads and stores them. + /// ; the caller loads and stores them. is + /// the variable the caller loads the loop's self into (null if it has none), and + /// what self's instance variables hold right now. /// internal static MSA.Expression TryCompileLoop(WhileLoopExpression/*!*/ loop, RubyContext/*!*/ context, int scopeDepth, Func/*!*/ bindSlot, out List slots, out bool canDeopt) { + return TryCompileLoop(loop, context, scopeDepth, bindSlot, null, null, out slots, out canDeopt); + } + + internal static MSA.Expression TryCompileLoop(WhileLoopExpression/*!*/ loop, RubyContext/*!*/ context, + int scopeDepth, Func/*!*/ bindSlot, MSA.ParameterExpression self, + Func ivarProfile, out List slots, out bool canDeopt) { slots = null; canDeopt = false; - var c = new JitCompiler(loop, context, scopeDepth, bindSlot); + var c = new JitCompiler(loop, context, scopeDepth, bindSlot, self, ivarProfile); MSA.Expression body; try { JT type; @@ -301,13 +339,17 @@ private bool TryLookup(string/*!*/ name, out MSA.ParameterExpression p, out JT t /// cannot be specialized. /// internal static JitCode TryCompile(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/*!*/ paramTypes) { + return TryCompile(ast, context, paramTypes, null); + } + + internal static JitCode TryCompile(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, JT[]/*!*/ paramTypes, JitKeywords keywords) { // The self-recursive call needs the method's return type before the body has been // typed. Guess, compile, and re-run once if the guess was wrong. // Rounds: each retry widens the guess (Int -> Lng -> Dbl/Obj), so the lattice bounds // how many there can be; a guess that does not settle is rejected. JT guess = JT.None; for (int round = 0; round < 5; round++) { - var c = new JitCompiler(ast, context, paramTypes); + var c = new JitCompiler(ast, context, paramTypes, keywords); c._firstRound = (guess == JT.None); c._selfReturn = c._firstRound ? JT.Int : guess; c._selfDelegateType = DelegateType(paramTypes, c._selfReturn); @@ -366,7 +408,7 @@ internal static JitCode TryCompile(MethodDeclaration/*!*/ ast, RubyContext/*!*/ SelfCell = c._selfCell, ReturnType = actual, ParameterTypes = paramTypes, - CanDeopt = c._canDeopt, + CanDeopt = c._canDeopt || c._ivarWrite, EmittedSelfCall = c._selfCall, }; } @@ -384,7 +426,11 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { case 1: return typeof(Func<,,>).MakeGenericType(types); case 2: return typeof(Func<,,,>).MakeGenericType(types); case 3: return typeof(Func<,,,,>).MakeGenericType(types); - default: return null; + case 4: return typeof(Func<,,,,,>).MakeGenericType(types); + case 5: return typeof(Func<,,,,,,>).MakeGenericType(types); + case 6: return typeof(Func<,,,,,,,>).MakeGenericType(types); + // positionals plus keywords (JitStub.MaxArity - 1 + JitKeywords.MaxKeywords) + default: return (ps.Length <= 14) ? Ast.GetFuncType(types) : null; } } @@ -395,7 +441,111 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { if (body.RescueClauses != null || body.ElseStatements != null || body.EnsureStatements != null) { throw JitBailout.Instance; } - return EmitStatements(body.Statements, out type); + if (_keywords != null) { + // the keyword prologue is what the entry did; the parameters hold its result + return EmitStatements(body.Statements, _keywords.PrologueLength, out type); + } + return EmitInitialIVars(body.Statements, out type) ?? EmitStatements(body.Statements, out type); + } + + // ---- an initializer's opening run of `@x = v' -------------------------------------- + // `def initialize(a, b) @a = a; @b = b; @n = 0 ...': a fresh object gets every one of these + // variables at once - one slot array, already in its final shape, published with one CAS - + // rather than one shape transition (and interlocked operation) per variable. Only for a run + // of writes of distinct variables whose values have no effect and read no instance variable + // (a parameter, a literal, self), so that doing all the stores together is the same as doing + // them one by one. Any object that is not a fresh RubyObject takes the writes one by one. + + private const int MinInitialIVars = 2; + + private MSA.Expression EmitInitialIVars(Statements statements, out JT type) { + type = JT.None; + if (_loop != null || statements == null) { return null; } + + var run = new List(); + var names = new List(); + foreach (var statement in statements) { + var assign = statement as SimpleAssignmentExpression; + var ivar = (assign != null && assign.Operation == null) ? assign.Left as InstanceVariable : null; + if (ivar == null || names.Contains(ivar.Name) || !IsInitialIVarValue(assign.Right)) { break; } + run.Add(assign); + names.Add(ivar.Name); + } + if (run.Count < MinInitialIVars) { return null; } + + var site = JitRuntime.InitialIVarsSite.TryCreate(names.ToArray()); + if (site == null) { return null; } + + var temps = new MSA.ParameterExpression[run.Count]; + var parts = new List(); + var stores = new List(); + var oneByOne = new List(); + var slots = Ast.Variable(typeof(object[]), "#slots"); + for (int i = 0; i < run.Count; i++) { + ++_nodes; + JT vt; + var v = Emit(run[i].Right, out vt); + temps[i] = Ast.Variable(typeof(object), "#iv" + i); + parts.Add(Ast.Assign(temps[i], Coerce(v, vt, JT.Obj))); + stores.Add(Ast.Assign(Ast.ArrayAccess(slots, Ast.Constant(i + 1)), temps[i])); + oneByOne.Add(Ast.Call(JitRuntime.M("SetIVar"), Ast.Constant(_context), _self, temps[i], Ast.Constant(new InstanceVariableSite(names[i])))); + } + _pure = false; + _ivarWrite = true; + + stores.Add(Ast.Call(JitRuntime.M("CommitInitialIVars"), _self, slots)); + parts.Add(Ast.Assign(slots, Ast.Call(JitRuntime.M("BeginInitialIVars"), _self, Ast.Constant(site)))); + parts.Add(Ast.IfThen( + Ast.Not(Ast.AndAlso(Ast.NotEqual(slots, Ast.Constant(null, typeof(object[]))), Ast.Block(typeof(bool), stores))), + Ast.Block(typeof(void), oneByOne) + )); + + int rest = statements.Count - run.Count; + if (rest == 0) { + // the value of the last assignment is the method's + type = JT.Obj; + parts.Add(temps[run.Count - 1]); + } else { + int skip = run.Count; + JT t = JT.Obj; + int k = 0; + foreach (var statement in statements) { + if (k++ < skip) { continue; } + parts.Add(Emit(statement, out t)); + } + type = t; + } + + var variables = new List(temps); + variables.Add(slots); + return Ast.Block(ClrType(type), variables, parts); + } + + /// A value an opening run of instance variable writes may have: see EmitInitialIVars. + private bool IsInitialIVarValue(RExpr/*!*/ value) { + if (value is SelfReference) { return true; } + var literal = value as Literal; + if (literal != null) { + object v = literal.Value; + return v == null || v is bool || v is int || v is long || v is double; + } + // Nothing before the run assigns a local, so a parameter still holds its argument. + var local = value as LocalVariable; + return local != null && local.DefinitionLexicalDepth == _scopeDepth && Array.IndexOf(_paramNames, local.Name) >= 0; + } + + private MSA.Expression/*!*/ EmitStatements(Statements/*!*/ statements, int start, out JT type) { + int count = statements.Count - start; + if (count <= 0) { + type = JT.Obj; + return Ast.Constant(null, typeof(object)); + } + var parts = new List(count); + type = JT.None; + for (int i = start; i < statements.Count; i++) { + parts.Add(Emit(statements[i], out type)); + } + return (parts.Count == 1) ? parts[0] : Ast.Block(ClrType(type), parts); } private MSA.Expression/*!*/ EmitStatements(Statements statements, out JT type) { @@ -478,12 +628,14 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { var ivar = node as InstanceVariable; if (ivar != null) { + if (_loop != null && _ivarProfile == null) { throw Bail("instance variable in a loop entered without self"); } type = JT.Obj; return Ast.Call(JitRuntime.M("GetIVar"), Ast.Constant(_context), _self, Ast.Constant(new InstanceVariableSite(ivar.Name))); } var self = node as SelfReference; if (self != null) { + if (_loop != null && _ivarProfile == null) { throw Bail("self in a loop entered without it"); } type = JT.Obj; return _self; } @@ -509,6 +661,9 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { var ret = node as ReturnStatement; if (ret != null) { return EmitReturn(ret, out type); } + var arrayLiteral = node as ArrayConstructor; + if (arrayLiteral != null) { return EmitArrayLiteral(arrayLiteral, out type); } + var body = node as Body; if (body != null) { if (body.RescueClauses != null || body.ElseStatements != null || body.EnsureStatements != null) { @@ -520,6 +675,26 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { throw Bail("node: " + node.GetType().Name); } + /// + /// `[a, b]': a new Array of boxed values, made by the helper the generic body calls. Building + /// one has no effect anything could observe, so it is no obstacle to a deopt. A splat (or + /// anything else Emit does not know) among the elements bails as usual. + /// + internal const int MaxArrayLiteral = 5; + + private MSA.Expression/*!*/ EmitArrayLiteral(ArrayConstructor/*!*/ node, out JT type) { + var items = node.Arguments.Expressions; + if (items.Length > MaxArrayLiteral) { throw Bail("array literal of " + items.Length); } + var values = new List(items.Length); + foreach (var item in items) { + JT t; + var e = Emit(item, out t); + values.Add(Coerce(e, t, JT.Obj)); + } + type = JT.Obj; + return IronRuby.Compiler.Methods.MakeArrayOpCall(values); + } + private MSA.Expression/*!*/ EmitLiteral(Literal/*!*/ literal, out JT type) { object v = literal.Value; if (v is int) { type = JT.Int; return Ast.Constant((int)v, typeof(int)); } @@ -551,9 +726,13 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { var ivarTarget = node.Left as InstanceVariable; if (ivarTarget != null) { if (op != null) { throw JitBailout.Instance; } + // A write can raise (FrozenError), and an exception leaving an outlined loop + // part way through would leave the scope's locals as they were on entry. + if (_loop != null) { throw Bail("instance variable write in an outlined loop"); } JT vt; var v = Emit(node.Right, out vt); _pure = false; // an ivar write is observable, so the body may not deopt + _ivarWrite = true; type = JT.Obj; return Ast.Call(JitRuntime.M("SetIVar"), Ast.Constant(_context), _self, Coerce(v, vt, JT.Obj), Ast.Constant(new InstanceVariableSite(ivarTarget.Name))); } @@ -576,6 +755,7 @@ private static Type DelegateType(JT[]/*!*/ ps, JT ret) { if (op != null) { // `x op= v' -> `x = x op v' if (!known) { throw JitBailout.Instance; } + rhs = NumericIVar(node.Right, rhs, ref rhsType); if (!IsNumeric(lt) || !IsNumeric(rhsType)) { throw JitBailout.Instance; } rhs = EmitBinary(op, (_loop != null) ? LoadLoopSlot(p) : p, lt, rhs, rhsType, out rhsType); } @@ -796,7 +976,12 @@ private static JT Unify(JT a, JT b) { // A long leaves through the representation funnel: a value that fits in an Int32 // IS an Int32, which is what #hash, #eql? and Hash keys are defined on. if (from == JT.Lng) { return Ast.Call(JitRuntime.M("NarrowLong"), e); } - // Fixnum is a boxed int, Float a boxed double, true/false boxed bools. + // Fixnum is a boxed int, Float a boxed double, true/false boxed bools. A literal + // is boxed once, here, as the generic body does, rather than on every evaluation + // (`@count = 0' in an initializer would otherwise allocate per object). + var constant = e as MSA.ConstantExpression; + if (constant != null) { return Ast.Constant(constant.Value, typeof(object)); } + if (from == JT.Bool) { return Ast.Call(typeof(Microsoft.Scripting.Runtime.ScriptingRuntimeHelpers).GetMethod("BooleanToObject"), e); } return Ast.Convert(e, typeof(object)); } throw JitBailout.Instance; @@ -831,7 +1016,7 @@ private static JT Unify(JT a, JT b) { // Self-recursion: `fib(n - 1)' inside `def fib'. Sound because the entry stub pins // the receiver class and the global method version, so no override can slip in. - if (_ast != null && node.Target == null && !node.IsVariableCall && node.MethodName == _ast.Name && argc == _paramNames.Length) { + if (_ast != null && _keywords == null && node.Target == null && !node.IsVariableCall && node.MethodName == _ast.Name && argc == _paramNames.Length) { var callArgs = new MSA.Expression[argc + 1]; callArgs[0] = _self; for (int i = 0; i < argc; i++) { @@ -852,6 +1037,8 @@ private static JT Unify(JT a, JT b) { JT lt, rt; var l = Emit(node.Target, out lt); var r = Emit(args.Expressions[0], out rt); + l = NumericIVar(node.Target, l, ref lt); + r = NumericIVar(args.Expressions[0], r, ref rt); if (IsNumeric(lt) && IsNumeric(rt)) { return EmitBinary(node.MethodName, l, lt, r, rt, out type); } @@ -864,6 +1051,28 @@ private static bool IsNumeric(JT t) { return t == JT.Int || t == JT.Lng || t == JT.Dbl; } + /// + /// An instance variable read used as a numeric operand - `sum += @x', `@n * 2'. An + /// outlined loop is compiled with its self at hand, so what the variable holds right now + /// is a type profile: an Integer or a Float there is read through a guard that unboxes it + /// while it still is one and deopts when it is not, and the generic loop then does + /// whatever the operator does with whatever the variable holds. Reads are not + /// observable, so this keeps a body restartable. Anything else stays an object. + /// + private MSA.Expression/*!*/ NumericIVar(RExpr/*!*/ node, MSA.Expression/*!*/ e, ref JT type) { + var ivar = node as InstanceVariable; + if (type != JT.Obj || ivar == null || _ivarProfile == null) { return e; } + string guard; + switch (_ivarProfile(ivar.Name)) { + case JT.Int: type = JT.Int; guard = "GuardInt"; break; + case JT.Lng: type = JT.Lng; guard = "GuardLong"; break; + case JT.Dbl: type = JT.Dbl; guard = "GuardDouble"; break; + default: return e; + } + _canDeopt = true; + return Ast.Call(JitRuntime.M(guard), e); + } + /// The two representations of a Ruby Integer the specialized code carries. private static bool IsInteger(JT t) { return t == JT.Int || t == JT.Lng; @@ -1062,10 +1271,19 @@ internal sealed class JitScreen { /// The node kind that said no, for the histogram. private string _rejectedBy; - private JitScreen(MethodDeclaration/*!*/ ast) { + // Locals read anywhere other than straight into an instance variable (`@a = a'), or + // assigned. A parameter not in here is only ever stored, so it is best carried boxed: + // unboxing it on entry would only mean boxing it again for the store. + private readonly HashSet/*!*/ _otherUses = new HashSet(); + + private readonly JitKeywords _keywords; + + private JitScreen(MethodDeclaration/*!*/ ast, JitKeywords keywords) { _scopeDepth = ast.DefinedScope.Depth; _methodName = ast.Name; - _arity = ast.Parameters.Mandatory.Length; + // a method with keywords is never called directly from its typed body (EmitCall) + _arity = (keywords != null) ? -1 : ast.Parameters.Mandatory.Length + ast.Parameters.Optional.Length; + _keywords = keywords; } /// @@ -1073,10 +1291,20 @@ private JitScreen(MethodDeclaration/*!*/ ast) { /// this body. names the node kind that decided it. /// internal static bool CanEverCompile(MethodDeclaration/*!*/ ast, out string rejectedBy) { - var s = new JitScreen(ast); + HashSet otherUses; + return CanEverCompile(ast, null, out rejectedBy, out otherUses); + } + + /// + /// : the locals the body reads other than as the value of an + /// instance variable assignment, or assigns. + /// + internal static bool CanEverCompile(MethodDeclaration/*!*/ ast, JitKeywords keywords, out string rejectedBy, out HashSet otherUses) { + var s = new JitScreen(ast, keywords); + otherUses = s._otherUses; bool ok; try { - ok = s.WalkBody(ast.Body); + ok = (keywords != null) ? s.WalkMethodBody(ast.Body, keywords.PrologueLength) : s.WalkBody(ast.Body); } catch (Exception) { // A screen that throws must not take the process down with it; it just means the // method is not wrapped. @@ -1100,6 +1328,17 @@ private bool WalkBody(Body/*!*/ body) { return WalkStatements(body.Statements); } + /// Mirrors JitCompiler.EmitBody of a method with keywords: the prologue is not emitted. + private bool WalkMethodBody(Body/*!*/ body, int prologueLength) { + if (body.RescueClauses != null || body.ElseStatements != null || body.EnsureStatements != null) { + return No(body); + } + for (int i = prologueLength; i < body.Statements.Count; i++) { + if (!Walk(body.Statements[i])) { return false; } + } + return true; + } + private bool WalkStatements(Statements statements) { if (statements == null) { return true; } foreach (var s in statements) { @@ -1151,6 +1390,7 @@ private bool Walk(RExpr/*!*/ node) { if (local != null) { // A local from an enclosing scope is never read from a typed body. (Emit also // bails on a read before any assignment, which is a flow property: say yes.) + _otherUses.Add(local.Name); return local.DefinitionLexicalDepth == _scopeDepth || No(node); } @@ -1163,10 +1403,14 @@ private bool Walk(RExpr/*!*/ node) { var ivarTarget = assign.Left as InstanceVariable; if (ivarTarget != null) { - return (op == null) ? Walk(assign.Right) : No(node); + if (op != null) { return No(node); } + var stored = assign.Right as LocalVariable; + if (stored != null && stored.DefinitionLexicalDepth == _scopeDepth) { ++_nodes; return true; } + return Walk(assign.Right); } var target = assign.Left as LocalVariable; if (target == null || target.DefinitionLexicalDepth != _scopeDepth) { return No(node); } + _otherUses.Add(target.Name); return (op != null) ? WalkOperand(assign.Right) : Walk(assign.Right); } @@ -1235,6 +1479,16 @@ private bool Walk(RExpr/*!*/ node) { return Walk(retArgs.Expressions[0]); } + var arrayLiteral = node as ArrayConstructor; + if (arrayLiteral != null) { + var items = arrayLiteral.Arguments.Expressions; + if (items.Length > JitCompiler.MaxArrayLiteral) { return No(node); } + foreach (var item in items) { + if (!Walk(item)) { return false; } + } + return true; + } + // break/next are only emitted in loop mode, which a method body never is, so they // fall through to the rejection below along with every other unknown node. diff --git a/Src/Ruby/Runtime/Jit/JitKeywordCall.cs b/Src/Ruby/Runtime/Jit/JitKeywordCall.cs new file mode 100644 index 000000000..48e1f4028 --- /dev/null +++ b/Src/Ruby/Runtime/Jit/JitKeywordCall.cs @@ -0,0 +1,261 @@ +/* **************************************************************************** + * + * -X:JIT - an experimental ZJIT-style method JIT for IronRuby. + * + * Keyword calls straight into a specialized body. + * + * A keyword call builds a Hash for its keywords, and the entry of a specialized keyword + * method (see JitKeywords) only reads the values back out of it. For the common shape - + * a receiverless call, Symbol-literal keywords, a few positional arguments and nothing + * else - the call is compiled with an inline cache in front of the dynamic call site: + * + * r = JitKeywordCall.Invoke(cache, self, p0, p1, k0, k1, k2) + * r != JitKeywordCall.NoDirect ? r : + * + * The cache remembers, per receiver class and global method version, which method the + * name resolves to. When that is a method the JIT has specialized, and the keywords the + * call passes are ones its prologue would bind without complaint, the values go straight + * into the specialized body's "direct" entry (JitStub.Direct) in declaration order, with + * the defaults of the keywords the call leaves out. Anything else - another kind of method, + * a method not specialized (yet), a type guard or the method version failing, a deopt - + * answers NoDirect and the call runs exactly as it did before, so the generic body raises + * whatever ArgumentError there is to raise. + * + * A receiverless call may call a method of any visibility, so resolving the name on the + * receiver's class with every visibility allowed is what the call site does too. Calls + * where refinements could be in play are never sent this way (JitRuntime.Disabled, + * RubyContext.HasRefinements), and neither is anything once tracing is on. + * + * ***************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Reflection; +using IronRuby.Builtins; +using IronRuby.Compiler; +using IronRuby.Compiler.Ast; +using IronRuby.Runtime.Calls; +using AstUtils = Microsoft.Scripting.Ast.Utils; + +namespace IronRuby.Runtime.Jit { + using MSA = System.Linq.Expressions; + using Ast = System.Linq.Expressions.Expression; + using RExpr = IronRuby.Compiler.Ast.Expression; + + public sealed class JitKeywordCall { + internal const int MaxPositional = 2; + internal const int MaxKeywords = 3; + + // A site whose receiver keeps changing class (or that keeps meeting new method versions) + // stops looking: resolving a name is far dearer than the hash it would save. + private const int MaxMisses = 8; + + /// "Not called here: make the call the ordinary way." + public static readonly object NoDirect = new object(); + + private readonly RubyContext/*!*/ _context; + private readonly string/*!*/ _name; + private readonly int _positional; + private readonly RubySymbol/*!*/[]/*!*/ _keys; // in the order the call passes them + + private sealed class State { + internal RubyClass Class; + internal int Version; + internal JitStub Stub; // null: the name does not resolve to a method this site can call directly + internal int[] Slots; // per declared keyword: the call's argument holding it, or -1 for its default + } + + private volatile State _state; + private int _misses; + private volatile bool _megamorphic; + + private JitKeywordCall(RubyContext/*!*/ context, string/*!*/ name, int positional, RubySymbol/*!*/[]/*!*/ keys) { + _context = context; + _name = name; + _positional = positional; + _keys = keys; + } + + private static readonly MethodInfo/*!*/ InvokeMethod = typeof(JitKeywordCall).GetMethod("Invoke"); + private static readonly FieldInfo/*!*/ NoDirectField = typeof(JitKeywordCall).GetField("NoDirect"); + + public static object Invoke(object/*!*/ site, object self, object p0, object p1, object k0, object k1, object k2) { + return ((JitKeywordCall)site).Call(self, p0, p1, k0, k1, k2); + } + + private object Call(object self, object p0, object p1, object k0, object k1, object k2) { + if (_megamorphic || JitRuntime.Disabled || _context.HasRefinements) { return NoDirect; } + + RubyClass cls = JitRuntime.ClassOf(_context, self); + int version = RubyModule.GlobalMethodVersion; + State state = _state; + if (state == null || state.Class != cls || state.Version != version) { + if (++_misses > MaxMisses) { + _megamorphic = true; + return NoDirect; + } + _state = state = Resolve(cls, version); + } + + JitStub stub = state.Stub; + if (stub == null) { return NoDirect; } + var direct = stub.Direct; + if (direct == null) { return NoDirect; } + + int[] slots = state.Slots; + object d0 = null, d1 = null, d2 = null; + switch (slots.Length) { + case 3: d2 = Pick(stub, 2, slots[2], k0, k1, k2); goto case 2; + case 2: d1 = Pick(stub, 1, slots[1], k0, k1, k2); goto case 1; + case 1: d0 = Pick(stub, 0, slots[0], k0, k1, k2); break; + } + return direct(self, p0, p1, d0, d1, d2); + } + + private static object Pick(JitStub/*!*/ stub, int declared, int slot, object k0, object k1, object k2) { + switch (slot) { + case 0: return k0; + case 1: return k1; + case 2: return k2; + default: return stub.Keywords.Defaults[declared]; + } + } + + /// + /// What means on an instance of , under method + /// version , for this site. + /// + private State/*!*/ Resolve(RubyClass/*!*/ cls, int version) { + var state = new State { Class = cls, Version = version }; + try { + var resolved = cls.ResolveMethod(_name, VisibilityContext.AllVisible); + var info = resolved.Found ? resolved.Info as RubyMethodInfo : null; + if (info == null) { return state; } + + var stub = info.GetDelegate().Target as JitStub; + if (stub == null || stub.Keywords == null || stub.Positional != _positional) { return state; } + + // The keywords this call passes must be ones the method's prologue binds without + // raising: each a declared one, and every required one among them. + var keywords = stub.Keywords; + var slots = new int[keywords.Count]; + int matched = 0; + for (int j = 0; j < slots.Length; j++) { + slots[j] = Array.IndexOf(_keys, keywords.Symbols[j]); + if (slots[j] >= 0) { + matched++; + } else if (keywords.Required[j]) { + return state; + } + } + if (matched != _keys.Length) { return state; } + + state.Stub = stub; + state.Slots = slots; + } catch (Exception) { + state.Stub = null; + } + return state; + } + + // ---- compilation of the call --------------------------------------------------------- + + /// + /// The call `name(p0, p1, k: v, ...)' with the inline cache in front of it, or null when the + /// call does not have that shape. + /// + internal static MSA.Expression TryTransform(AstGenerator/*!*/ gen, string/*!*/ methodName, MSA.Expression/*!*/ self, + Arguments arguments) { + + var options = gen.Context.RubyOptions; + if (!options.Jit || options.EnableTracing || options.Profile || JitRuntime.Disabled) { return null; } + if (arguments == null) { return null; } + + var exprs = arguments.Expressions; + int count = exprs.Length; + if (count == 0 || count - 1 > MaxPositional) { return null; } + var hash = exprs[count - 1] as HashConstructor; + if (hash == null || !hash.IsKeywordArguments) { return null; } + var maplets = hash.Maplets; + if (maplets.Length == 0 || maplets.Length > MaxKeywords) { return null; } + + var keys = new RubySymbol[maplets.Length]; + for (int i = 0; i < maplets.Length; i++) { + var key = maplets[i].Key as SymbolLiteral; + if (key == null || !(key.Value is string)) { return null; } + keys[i] = gen.Context.CreateSymbol((string)key.Value, key.Encoding); + // a repeated key is the hash's business + if (Array.IndexOf(keys, keys[i], 0, i) >= 0) { return null; } + } + int positional = count - 1; + for (int i = 0; i < positional; i++) { + if (exprs[i] is SplattedArgument || exprs[i] is HashConstructor) { return null; } + } + + var site = new JitKeywordCall(gen.Context, methodName, positional, keys); + + // The arguments are evaluated once, in source order, into temporaries both paths share. + var temps = new List(); + var body = new List(); + var selfTemp = Ast.Variable(typeof(object), "#kwself"); + temps.Add(selfTemp); + body.Add(Ast.Assign(selfTemp, AstUtils.Box(self))); + + var p = new MSA.Expression[MaxPositional]; + for (int i = 0; i < MaxPositional; i++) { + if (i < positional) { + var t = Ast.Variable(typeof(object), "#kwp" + i); + temps.Add(t); + body.Add(Ast.Assign(t, AstUtils.Box(exprs[i].TransformRead(gen)))); + p[i] = t; + } else { + p[i] = Ast.Constant(null, typeof(object)); + } + } + var k = new MSA.Expression[MaxKeywords]; + for (int i = 0; i < MaxKeywords; i++) { + if (i < maplets.Length) { + var t = Ast.Variable(typeof(object), "#kwv" + i); + temps.Add(t); + body.Add(Ast.Assign(t, AstUtils.Box(maplets[i].Value.TransformRead(gen)))); + k[i] = t; + } else { + k[i] = Ast.Constant(null, typeof(object)); + } + } + + // the ordinary call: the keyword hash, then the dynamic site + var hashArgs = new List(1 + 2 * maplets.Length); + hashArgs.Add(gen.CurrentScopeVariable); + for (int i = 0; i < maplets.Length; i++) { + hashArgs.Add(AstUtils.Box(Ast.Constant(keys[i]))); + hashArgs.Add(k[i]); + } + MethodInfo make; + switch (maplets.Length) { + case 1: make = Methods.MakeKeywordArgumentsHash1; break; + case 2: make = Methods.MakeKeywordArgumentsHash2; break; + default: make = Methods.MakeKeywordArgumentsHash3; break; + } + var makeHash = Ast.Call(make, hashArgs); + var siteBuilder = new CallSiteBuilder(gen, selfTemp, null); + for (int i = 0; i < positional; i++) { + siteBuilder.Add(p[i]); + } + siteBuilder.Add(makeHash); + var ordinary = siteBuilder.MakeCallAction(methodName, true); + + var result = Ast.Variable(typeof(object), "#kwresult"); + temps.Add(result); + body.Add(Ast.Assign(result, Ast.Call(InvokeMethod, + Ast.Constant(site, typeof(object)), selfTemp, p[0], p[1], k[0], k[1], k[2]))); + body.Add(Ast.Condition( + Ast.ReferenceNotEqual(result, Ast.Field(null, NoDirectField)), + result, + AstUtils.Convert(ordinary, typeof(object)), + typeof(object) + )); + return Ast.Block(typeof(object), temps, body); + } + } +} diff --git a/Src/Ruby/Runtime/Jit/JitKeywords.cs b/Src/Ruby/Runtime/Jit/JitKeywords.cs new file mode 100644 index 000000000..cc6811275 --- /dev/null +++ b/Src/Ruby/Runtime/Jit/JitKeywords.cs @@ -0,0 +1,251 @@ +/* **************************************************************************** + * + * -X:JIT - an experimental ZJIT-style method JIT for IronRuby. + * + * Keyword parameters. + * + * PrismAstBridge.LowerKeywords turns `def m(a, k:, j: 1)' into `def m(a, ?kw? = {})' plus a + * prologue that binds the keyword locals out of that hash: + * + * raise ArgumentError, ... if KeywordArgumentsTest(a) # separation checks + * raise ArgumentError, ... unless KeywordArgumentsTest(?kw?) + * unless KeywordArgumentsMatch(?kw?, [:k, :j], 1, false) # missing / unknown + * ...word the ArgumentError the slow way... + * end + * k = GetKeywordArgument(?kw?, :k, 0) + * j = (v = GetKeywordArgument(?kw?, :j, 1)) == DefaultArgument ? 1 : v + * + * + * This class recognizes exactly that shape. A recognized method is specialized with the + * keyword locals as typed parameters of its own, after the positional ones, and the entry + * stub reads them out of the hash - the very helpers the prologue uses - after checking that + * the prologue would have bound them without raising. Any call the prologue would complain + * about (a missing or unknown keyword, a Hash passed positionally, keywords landing in the + * last positional) fails that check and runs the generic body, which raises the error MRI + * raises. A default that is not a plain literal (it could have side effects, or depend on + * another parameter), and **rest, are not recognized: such a method is never wrapped. + * + * ***************************************************************************/ + +using System; +using System.Collections.Generic; +using IronRuby.Builtins; +using IronRuby.Compiler.Ast; + +namespace IronRuby.Runtime.Jit { + using RExpr = IronRuby.Compiler.Ast.Expression; + using MethodDeclaration = IronRuby.Compiler.Ast.MethodDefinition; + + internal sealed class JitKeywords { + internal const int MaxKeywords = 6; + + // How many leading statements of the body are the prologue; the typed body starts after them. + internal int PrologueLength; + + // KeywordArgumentsMatch's arguments: required names first, then the optional ones. + internal RubySymbol/*!*/[]/*!*/ MatchNames; + internal int RequiredCount; + + // Per keyword, in declaration order: its local, its symbol, its position, its default. + internal string/*!*/[]/*!*/ LocalNames; + internal RubySymbol/*!*/[]/*!*/ Symbols; + internal int[]/*!*/ Positions; + internal object[]/*!*/ Defaults; + internal bool[]/*!*/ Required; + + internal int Count { get { return LocalNames.Length; } } + + /// + /// The keyword signature of a method whose parameters are mandatory positionals plus + /// keywords lowered by LowerKeywords, or null if the method is anything else. + /// + internal static JitKeywords TryRecognize(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context) { + var ps = ast.Parameters; + if (ps.Optional.Length != 1 || ps.Unsplat != null || ps.Block != null) { return null; } + var kwVar = ps.Optional[0].Left as LocalVariable; + if (kwVar == null || kwVar.Name != "?kw?") { return null; } + + var body = ast.Body; + if (body == null || body.Statements == null) { return null; } + var statements = body.Statements; + int n = statements.Count; + int i = 0; + + // ?kw? ||= {} (a block's prologue starts with it; a method's does not) + if (i >= n) { return null; } + var reset = statements[i] as SimpleAssignmentExpression; + if (reset != null && reset.Operation == "||" && IsVar(reset.Left, kwVar) && reset.Right is HashConstructor) { + i++; + } + + // The separation checks: an if/unless on KeywordArgumentsTest whose body only raises. + while (i < n) { + var ifExpr = statements[i] as IfExpression; + if (ifExpr != null && ifExpr.Condition is KeywordArgumentsTest && + (ifExpr.ElseIfClauses == null || ifExpr.ElseIfClauses.Count == 0)) { + i++; + continue; + } + var unlessExpr = statements[i] as UnlessExpression; + if (unlessExpr != null && unlessExpr.ElseClause == null) { + var test = unlessExpr.Condition as KeywordArgumentsTest; + if (test != null && IsVar(test.Value, kwVar)) { + i++; + continue; + } + } + break; + } + + // unless KeywordArgumentsMatch(?kw?, names, required, false) ... end + if (i >= n) { return null; } + var matchStmt = statements[i] as UnlessExpression; + if (matchStmt == null || matchStmt.ElseClause != null) { return null; } + var match = matchStmt.Condition as KeywordParameterRead; + if (match == null || !match.IsMatch || match.AcceptsRest || !IsVar(match.Keywords, kwVar)) { return null; } + i++; + + var names = match.Names; + if (names.Length == 0 || names.Length > MaxKeywords) { return null; } + + var result = new JitKeywords(); + result.RequiredCount = match.RequiredCountOrPosition; + result.MatchNames = new RubySymbol[names.Length]; + for (int j = 0; j < names.Length; j++) { + result.MatchNames[j] = context.CreateSymbol(names[j], match.Encoding); + } + + // k = , one per declared keyword, in declaration order + var locals = new List(); + var symbols = new List(); + var positions = new List(); + var defaults = new List(); + var required = new List(); + while (i < n) { + var bind = statements[i] as SimpleAssignmentExpression; + if (bind == null || bind.Operation != null) { break; } + var read = bind.Right as KeywordParameterRead; + if (read == null || !read.IsValue || !IsVar(read.Keywords, kwVar)) { break; } + var local = bind.Left as LocalVariable; + if (local == null || local.DefinitionLexicalDepth != ast.DefinedScope.Depth) { return null; } + if (read.RequiredCountOrPosition != locals.Count) { return null; } + + object defaultValue = null; + if (read.DefaultValue != null) { + // A literal is the same value every time and evaluating it does nothing. + var literal = read.DefaultValue as Literal; + if (literal == null) { return null; } + object v = literal.Value; + if (!(v == null || v is int || v is double || v is bool)) { return null; } + defaultValue = v; + } + + locals.Add(local.Name); + symbols.Add(context.CreateSymbol(read.Names[0], read.Encoding)); + positions.Add(read.RequiredCountOrPosition); + defaults.Add(defaultValue); + required.Add(read.DefaultValue == null); + i++; + } + if (locals.Count != names.Length) { return null; } + + // every name Match checks is one a binding reads, and vice versa + for (int j = 0; j < names.Length; j++) { + if (!symbols.Contains(result.MatchNames[j])) { return null; } + } + + result.PrologueLength = i; + result.LocalNames = locals.ToArray(); + result.Symbols = symbols.ToArray(); + result.Positions = positions.ToArray(); + result.Defaults = defaults.ToArray(); + result.Required = required.ToArray(); + return result; + } + + private static bool IsVar(RExpr node, LocalVariable/*!*/ kwVar) { + var lv = node as LocalVariable; + return lv != null && lv.Name == kwVar.Name && lv.DefinitionLexicalDepth == kwVar.DefinitionLexicalDepth; + } + + /// + /// True when the prologue would bind every keyword from without + /// raising: it is the hash a keyword call made (or the call passed no keywords at all), + /// it has every required keyword and no other. + /// + internal bool Matches(object kw) { + if (kw == RubyOps.DefaultArgument) { return RequiredCount == 0; } + var hash = kw as Hash; + return hash != null && hash.IsKeywordArguments && hash.HasKeywords(MatchNames, RequiredCount, false); + } + + /// What the prologue binds keyword to, given . + internal object Value(object kw, int index) { + object v = RubyOps.GetKeywordArgument(kw, Symbols[index], Positions[index]); + return (v == RubyOps.DefaultArgument) ? Defaults[index] : v; + } + + // ---- Matches and Value in one go: what a specialized entry calls ---------------------- + // + // A keyword call's hash is small, and Hash.TryGetValueByIdentity scans a small hash for the + // Symbol rather than probing for it (8 entries, Hash.IdentityScanLimit). Looking each declared + // keyword up once both binds it and counts the keys that are declared ones, so the hash has + // no key the method does not know exactly when that count is all the keys it has - which is + // what Matches answers with a scan of its own. Anything else takes the two steps above. + + private const int ScanLimit = 8; + + private bool Bind(Hash/*!*/ hash, int index, ref int found, out object value) { + if (hash.TryGetValueByIdentity(Symbols[index], Positions[index], out value)) { + found++; + return true; + } + value = Defaults[index]; + return !Required[index]; + } + + private static Hash SmallKeywordHash(object kw) { + var hash = kw as Hash; + return (hash != null && hash.IsKeywordArguments && hash.Count <= ScanLimit) ? hash : null; + } + + internal bool Read(object kw, out object v0) { + var hash = SmallKeywordHash(kw); + if (hash != null) { + int found = 0; + return Bind(hash, 0, ref found, out v0) && found == hash.Count; + } + bool ok = Matches(kw); + v0 = ok ? Value(kw, 0) : null; + return ok; + } + + internal bool Read(object kw, out object v0, out object v1) { + var hash = SmallKeywordHash(kw); + if (hash != null) { + int found = 0; + v1 = null; + return Bind(hash, 0, ref found, out v0) && Bind(hash, 1, ref found, out v1) && found == hash.Count; + } + bool ok = Matches(kw); + v0 = ok ? Value(kw, 0) : null; + v1 = ok ? Value(kw, 1) : null; + return ok; + } + + internal bool Read(object kw, out object v0, out object v1, out object v2) { + var hash = SmallKeywordHash(kw); + if (hash != null) { + int found = 0; + v1 = v2 = null; + return Bind(hash, 0, ref found, out v0) && Bind(hash, 1, ref found, out v1) && Bind(hash, 2, ref found, out v2) && + found == hash.Count; + } + bool ok = Matches(kw); + v0 = ok ? Value(kw, 0) : null; + v1 = ok ? Value(kw, 1) : null; + v2 = ok ? Value(kw, 2) : null; + return ok; + } + } +} diff --git a/Src/Ruby/Runtime/Jit/JitLoopCompiler.cs b/Src/Ruby/Runtime/Jit/JitLoopCompiler.cs index 5dbb9a1b4..ccf6761b4 100644 --- a/Src/Ruby/Runtime/Jit/JitLoopCompiler.cs +++ b/Src/Ruby/Runtime/Jit/JitLoopCompiler.cs @@ -69,10 +69,11 @@ internal static class JitLoopCompiler { /// Builds a typed entry for a loop, or returns null when the loop is not the shape /// this can specialize. are the locals tuples the site passes, /// one per lexical depth; they carry the live values the types are read from, which is - /// a type profile a loop that is already running gets for free. + /// a type profile a loop that is already running gets for free. The loop's self is + /// args[], or there is none if that is negative. /// internal static Func TryCompile(WhileLoopExpression/*!*/ loop, RubyContext/*!*/ context, - int scopeDepth, Dictionary/*!*/ tupleArgIndex, object[]/*!*/ args) { + int scopeDepth, Dictionary/*!*/ tupleArgIndex, object[]/*!*/ args, int selfArgIndex) { if (JitRuntime.Disabled || context.HasRefinements) { return null; } @@ -149,17 +150,42 @@ internal static Func TryCompile(WhileLoopExpression/*!*/ loop, }; }; + // The loop's self, which its instance variable reads go through. The live object is a + // profile as well: what one of its variables holds when the loop turns hot is the type + // a numeric use of the variable is specialized for (JitCompiler.NumericIVar). + MSA.ParameterExpression self = null; + Func ivarProfile = null; + if (selfArgIndex >= 0 && selfArgIndex < args.Length) { + object liveSelf = args[selfArgIndex]; + self = Ast.Parameter(typeof(object), "#self"); + ivarProfile = name => { + object value; + if (!context.TryGetInstanceVariable(liveSelf, name, out value)) { return JT.Obj; } + return (value is int) ? JT.Int : (value is long) ? JT.Lng : (value is double) ? JT.Dbl : JT.Obj; + }; + } + List slots; bool canDeopt; - MSA.Expression body = JitCompiler.TryCompileLoop(loop, context, scopeDepth, bind, out slots, out canDeopt); + MSA.Expression body = JitCompiler.TryCompileLoop(loop, context, scopeDepth, bind, self, ivarProfile, out slots, out canDeopt); if (body == null || slots.Count == 0) { if (OsrLoopSite.Verbose) { Console.Error.WriteLine("[osr] emitter gave up (body={0} slots={1})", body != null, slots == null ? -1 : slots.Count); } return null; } + // Bind self only if the body reads it (an instance variable, `self'): an unused #self + // local loaded in the prologue cost the JIT's register allocation of the loop itself - + // `while i < m; i += 1; end' ran 1.4x slower per iteration with it. + if (self != null && !UsesParameter.In(body, self)) { + self = null; + } + var locals = new List(tuples.Values); var probe = Ast.Parameter(typeof(object), "#probe"); locals.Add(probe); + if (self != null) { + locals.Add(self); + } // The scope this loop belongs to always makes tuples of the same shape, but nothing // in the type system says so: check before the cast rather than let an unexpected @@ -169,12 +195,17 @@ internal static Func TryCompile(WhileLoopExpression/*!*/ loop, prologue.Add(Ast.IfThen(guard, Ast.Return(exit, Ast.Constant(OsrLoopSite.Retry, typeof(object))))); } prologue.AddRange(tupleInit); + if (self != null) { + prologue.Add(Ast.Assign(self, Ast.ArrayIndex(argsParameter, Ast.Constant(selfArgIndex)))); + } // Nothing may have redefined Integer#+ (or anything else) since the types were - // read: one global version covers every def, undef, alias and include. + // read: one global version covers every def, undef, alias and include. A copy that + // fails it fails it on every later entry, so it asks to be rebuilt under the new + // version rather than counting as one more deopt towards retiring the loop. prologue.Add(Ast.IfThen( Ast.Not(Ast.Call(JitRuntime.M("VersionOk"), Ast.Constant(RubyModule.GlobalMethodVersion))), - Ast.Return(exit, Ast.Call(JitRuntime.M("OsrGuardFailed"), Ast.Constant("method version"))) + Ast.Return(exit, Ast.Call(JitRuntime.M("OsrVersionStale"))) )); var storeCurrent = new List(); @@ -251,4 +282,31 @@ internal static Func TryCompile(WhileLoopExpression/*!*/ loop, } } } + + /// Whether an expression tree references a given parameter or variable. + internal sealed class UsesParameter : MSA.ExpressionVisitor { + private readonly MSA.ParameterExpression/*!*/ _parameter; + private bool _found; + + private UsesParameter(MSA.ParameterExpression/*!*/ parameter) { + _parameter = parameter; + } + + internal static bool In(MSA.Expression/*!*/ tree, MSA.ParameterExpression/*!*/ parameter) { + var finder = new UsesParameter(parameter); + finder.Visit(tree); + return finder._found; + } + + public override MSA.Expression Visit(MSA.Expression node) { + return _found ? node : base.Visit(node); + } + + protected override MSA.Expression VisitParameter(MSA.ParameterExpression/*!*/ node) { + if (node == _parameter) { + _found = true; + } + return node; + } + } } diff --git a/Src/Ruby/Runtime/Jit/JitRuntime.cs b/Src/Ruby/Runtime/Jit/JitRuntime.cs index 06df34e4a..ad8ec162c 100644 --- a/Src/Ruby/Runtime/Jit/JitRuntime.cs +++ b/Src/Ruby/Runtime/Jit/JitRuntime.cs @@ -278,6 +278,17 @@ public static bool VersionOk(int snapshot) { return !Disabled && RubyModule.GlobalMethodVersion == snapshot; } + /// A method entry's VersionOk failed: it is stale for good (see JitStub.VersionChanged). + public static void EntryStale(object/*!*/ stub) { + ((JitStub)stub).VersionChanged(); + } + + /// An outlined loop was compiled under an older method version; build it again. + public static object OsrVersionStale() { + if (Osr.OsrLoopSite.Verbose) { Console.Error.WriteLine("[osr] guard failed: method version"); } + return Osr.OsrLoopSite.Retype; + } + /// An entry guard of an outlined loop did not hold; hand the loop back. public static object OsrGuardFailed(string/*!*/ what) { if (Osr.OsrLoopSite.Verbose) { Console.Error.WriteLine("[osr] guard failed: {0}", what); } @@ -300,6 +311,32 @@ public static object OsrTypeGuardFailed(string/*!*/ what) { return Deopt(); } + // ---- keyword parameters (see JitKeywords) --------------------------------------- + + /// The entry guard of a method with keywords: the prologue would bind them without raising. + public static bool KeywordsMatch(object/*!*/ keywords, object kw) { + return ((JitKeywords)keywords).Matches(kw); + } + + /// The value the prologue binds keyword number to. + public static object KeywordValue(object/*!*/ keywords, object kw, int index) { + return ((JitKeywords)keywords).Value(kw, index); + } + + // KeywordsMatch and KeywordValue in one call, for a method with up to three keywords. + + public static bool KeywordsRead1(object/*!*/ keywords, object kw, out object v0) { + return ((JitKeywords)keywords).Read(kw, out v0); + } + + public static bool KeywordsRead2(object/*!*/ keywords, object kw, out object v0, out object v1) { + return ((JitKeywords)keywords).Read(kw, out v0, out v1); + } + + public static bool KeywordsRead3(object/*!*/ keywords, object kw, out object v0, out object v1, out object v2) { + return ((JitKeywords)keywords).Read(kw, out v0, out v1, out v2); + } + public static RubyClass/*!*/ ClassOf(RubyContext/*!*/ context, object self) { IRubyObject obj = self as IRubyObject; return obj != null ? obj.ImmediateClass : context.GetImmediateClassOf(self); @@ -314,7 +351,101 @@ public static object GetIVar(RubyContext/*!*/ context, object self, InstanceVari } public static object SetIVar(RubyContext/*!*/ context, object self, object value, InstanceVariableSite/*!*/ site) { - return RubyOps.WriteInstanceVariable(context, self, value, site); + var obj = self as RubyObject; + if (obj != null) { + var data = obj._instanceData ?? RubyOps.GetInstanceData(ref obj._instanceData); + if (!data._frozen && data.TrySetCached(site.Cache, value, obj)) { + return value; + } + } + // A frozen receiver raises FrozenError, and a typed body has no frame to raise it from: + // let the generic body run instead (see JitCompiler._ivarWrite). + if (context.IsObjectFrozen(self)) { + throw Deopt(); + } + context.SetInstanceVariable(self, value, site); + return value; + } + + /// + /// The opening run of instance variable writes of a specialized method (JitCompiler. + /// EmitInitialIVars): the shape a fresh object has after all of them. + /// + public sealed class InitialIVarsSite { + internal readonly InstanceVariableShape/*!*/ Shape; + + private InitialIVarsSite(InstanceVariableShape/*!*/ shape) { + Shape = shape; + } + + /// Null if the variables would make an object too complex for shapes. + internal static InitialIVarsSite TryCreate(string/*!*/[]/*!*/ names) { + var shape = InstanceVariableShape.Root; + foreach (var name in names) { + shape = shape.GetChild(name); + if (shape == null) { return null; } + } + return new InitialIVarsSite(shape); + } + } + + /// + /// The slot array a fresh object would get the run's variables in - element 0 already the + /// final shape, the values to be stored by the caller - or null if self is not a RubyObject + /// without instance variables, or is frozen: then the writes are made one by one. + /// + public static object[] BeginInitialIVars(object self, InitialIVarsSite/*!*/ site) { + var obj = self as RubyObject; + if (obj == null) { return null; } + var data = obj._instanceData; + if (data != null && (data._frozen || data._ivars != InstanceVariableShape.EmptySlots)) { return null; } + int count = site.Shape.Count; + var result = new object[Math.Max(count, obj.ImmediateClass.InstanceVariableCapacity) + 1]; + result[0] = site.Shape; + return result; + } + + /// + /// Publishes the array BeginInitialIVars made and the caller filled. False if the object + /// got variables, or was frozen, in the meantime: the caller then makes the writes one by one. + /// + public static bool CommitInitialIVars(object self, object[]/*!*/ slots) { + var obj = (RubyObject)self; + var data = obj._instanceData; + if (data == null) { + var fresh = new RubyInstanceData(); + fresh._ivars = slots; + if (System.Threading.Interlocked.CompareExchange(ref obj._instanceData, fresh, null) == null) { + RubyInstanceData.UpdateCapacityHint(obj.ImmediateClass, ((InstanceVariableShape)slots[0]).Count); + return true; + } + data = obj._instanceData; + } + if (data._frozen) { return false; } + if (System.Threading.Interlocked.CompareExchange(ref data._ivars, slots, InstanceVariableShape.EmptySlots) == InstanceVariableShape.EmptySlots) { + RubyInstanceData.UpdateCapacityHint(obj.ImmediateClass, ((InstanceVariableShape)slots[0]).Count); + return true; + } + return false; + } + + // An instance variable used as a numeric operand of an outlined loop (JitCompiler.NumericIVar): + // unboxed while it holds the type it held when the loop was compiled, a deopt once it does not. + + public static int GuardInt(object value) { + if (value is int) { return (int)value; } + throw Deopt(); + } + + public static long GuardLong(object value) { + if (value is int) { return (int)value; } + if (value is long) { return (long)value; } + throw Deopt(); + } + + public static double GuardDouble(object value) { + if (value is double) { return (double)value; } + throw Deopt(); } // ---- reflection handles the compiler emits --------------------------------------- diff --git a/Src/Ruby/Runtime/Jit/JitStub.cs b/Src/Ruby/Runtime/Jit/JitStub.cs index f11224ef8..768c24229 100644 --- a/Src/Ruby/Runtime/Jit/JitStub.cs +++ b/Src/Ruby/Runtime/Jit/JitStub.cs @@ -43,13 +43,42 @@ namespace IronRuby.Runtime.Jit { internal abstract class JitStub { internal const int Threshold = 40; - internal const int MaxArity = 3; + internal const int MaxArity = 6; + internal const string EntryNamePrefix = "jitentry$"; + internal const string DirectEntryNamePrefix = "jitdirect$"; + + // What a typed body's call evaluates to in the entry when it deopted. + private static readonly object Deopted = new object(); protected readonly MethodDeclaration/*!*/ _ast; protected readonly RubyContext/*!*/ _context; protected readonly RubyModule/*!*/ _declaringModule; protected readonly Delegate/*!*/ _generic; protected readonly int _arity; + // A method with keyword parameters (see JitKeywords): its last delegate parameter is the + // keyword hash, and the typed body takes the keyword values instead, after the positionals. + protected readonly JitKeywords _keywords; + private readonly int _positional; // delegate parameters that are positional parameters + private readonly int _typedCount; // parameters of the typed body + + // A specialized keyword method's entry for JitKeywordCall: (self, p0, p1, k0, k1, k2) with the + // keyword values in declaration order and unused slots null. Answers JitKeywordCall.NoDirect + // when a guard fails. Null when the method is not specialized. + private volatile Func _direct; + + internal Func Direct { get { return _direct; } } + internal JitKeywords Keywords { get { return _keywords; } } + internal int Positional { get { return _positional; } } + + // Optional parameters (after the mandatory ones, as in the generic delegate) have a literal + // default; a call that omits one passes RubyOps.DefaultArgument in its place, which the + // entry and the profiler replace with the value. _hasDefault is null with no optionals. + private bool[] _hasDefault; + private object[] _defaults; + + // Parameters the body only ever stores into an instance variable: kept as the boxed + // objects they arrived as, whatever their type (JitScreen's otherUses). + private bool[] _storedOnly; // profiling feedback private int _calls; @@ -58,13 +87,17 @@ internal abstract class JitStub { private bool _polymorphicReceiver; private volatile bool _givenUp; - protected JitStub(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, RubyModule/*!*/ declaringModule, Delegate/*!*/ generic, int arity) { + protected JitStub(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, RubyModule/*!*/ declaringModule, Delegate/*!*/ generic, int arity, + JitKeywords keywords) { _ast = ast; _context = context; _declaringModule = declaringModule; _generic = generic; _arity = arity; - _argKind = new int[arity]; + _keywords = keywords; + _positional = (keywords != null) ? arity - 1 : arity; + _typedCount = (keywords != null) ? _positional + keywords.Count : arity; + _argKind = new int[_typedCount]; } protected abstract void Install(Delegate/*!*/ entry); @@ -74,16 +107,20 @@ protected JitStub(MethodDeclaration/*!*/ ast, RubyContext/*!*/ context, RubyModu protected void Profile(object self, object[] args) { if (_givenUp) { return; } - for (int i = 0; i < _arity; i++) { - int k = (args[i] is int) ? 1 : (args[i] is double) ? 2 : (args[i] is long) ? 4 : 3; - if (_argKind[i] == 0) { - _argKind[i] = k; - } else if (_argKind[i] != k) { - // int and long are one Ruby class seen in two representations, so a - // parameter that has been both is still an Integer: carry it as a long. - bool bothIntegral = (k == 1 || k == 4) && (_argKind[i] == 1 || _argKind[i] == 4); - _argKind[i] = bothIntegral ? 4 : 3; + if (_keywords != null) { + // what the prologue binds the keywords to, if it binds them at all + object kw = args[_positional]; + if (_keywords.Matches(kw)) { + for (int j = 0; j < _keywords.Count; j++) { + Observe(_positional + j, _keywords.Value(kw, j)); + } + } + } + for (int i = 0; i < _positional; i++) { + if (_hasDefault != null && _hasDefault[i] && args[i] == RubyOps.DefaultArgument) { + args[i] = _defaults[i]; } + Observe(i, args[i]); } RubyClass cls = JitRuntime.ClassOf(_context, self); @@ -101,6 +138,18 @@ protected void Profile(object self, object[] args) { } } + private void Observe(int i, object arg) { + int k = (arg is int) ? 1 : (arg is double) ? 2 : (arg is long) ? 4 : 3; + if (_argKind[i] == 0) { + _argKind[i] = k; + } else if (_argKind[i] != k) { + // int and long are one Ruby class seen in two representations, so a + // parameter that has been both is still an Integer: carry it as a long. + bool bothIntegral = (k == 1 || k == 4) && (_argKind[i] == 1 || _argKind[i] == 4); + _argKind[i] = bothIntegral ? 4 : 3; + } + } + /// /// Stop profiling this method for good, and put the generic body back in the fast slot. /// Leaving the slot empty would be much worse than doing nothing: the trampoline would @@ -118,22 +167,48 @@ private void GiveUp() { Console.Error.WriteLine("[jit] declined {0}/{1} arity {2} (passed the pre-screen)", _declaringModule.Name ?? "main", _ast.Name, _arity); } + _direct = null; Install(_generic); } /// Deoptimize: drop the specialized entry and let profiling start over. internal void Invalidate() { JitRuntime.Invalidations++; + _direct = null; // A method that was already declined must not fall back into profiling here. Install(_givenUp ? _generic : null); _calls = 0; } + // How many times a method is specialized again after a method table change made its entry + // stale. A program that keeps defining methods (a `def obj.x' per object, define_method in + // a loop) would otherwise pay a compile every Threshold calls for as long as it runs. + private const int MaxRespecializations = 8; + private int _respecializations; + + /// + /// Called by the entry when the global method version no longer matches its snapshot. That + /// guard can never pass again, so leaving the entry installed would send every later call + /// through the failed guard into the generic body: profile again and specialize under the + /// new version instead, or settle on the generic body once that has happened too often. + /// + internal void VersionChanged() { + if (_givenUp) { return; } + if (++_respecializations > MaxRespecializations) { + _givenUp = true; + _direct = null; + Install(_generic); + return; + } + Invalidate(); + } + private bool TrySpecialize() { if (JitRuntime.Disabled || _context.HasRefinements) { return false; } - var types = new JT[_arity]; - for (int i = 0; i < _arity; i++) { + var types = new JT[_typedCount]; + for (int i = 0; i < _typedCount; i++) { + if (_storedOnly != null && _storedOnly[i]) { types[i] = JT.Obj; continue; } switch (_argKind[i]) { case 1: types[i] = JT.Int; break; case 2: types[i] = JT.Dbl; break; @@ -143,7 +218,7 @@ private bool TrySpecialize() { } long start = Stopwatch.GetTimestamp(); - var code = JitCompiler.TryCompile(_ast, _context, types); + var code = JitCompiler.TryCompile(_ast, _context, types, _keywords); if (code == null) { return false; } bool needsClassGuard = code.EmittedSelfCall; @@ -169,6 +244,10 @@ private bool TrySpecialize() { var entry = BuildEntry(code, callee, version, needsClassGuard ? _receiverClass : null); Install(entry.Compile()); + if (_keywords != null && !needsClassGuard && _positional <= JitKeywordCall.MaxPositional && + _keywords.Count <= JitKeywordCall.MaxKeywords) { + _direct = BuildDirectEntry(code, callee, version).Compile(); + } JitRuntime.CompileTicks += Stopwatch.GetTimestamp() - start; JitRuntime.Specialized++; @@ -200,30 +279,81 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls) { args[i] = Ast.Parameter(typeof(object), "a" + i); } - MSA.Expression test = Ast.Call(JitRuntime.M("VersionOk"), Ast.Constant(version)); + // The values the typed parameters are made of: the positional arguments - an omitted + // optional one being its default - then, for a method with keywords, what the prologue + // would bind each keyword to. The generic body is still handed the arguments as they came. + var values = new MSA.Expression[_typedCount]; + var defaulted = new List(); + var fillDefaults = new List(); + for (int i = 0; i < _positional; i++) { + if (_hasDefault != null && _hasDefault[i]) { + var v = Ast.Variable(typeof(object), "v" + i); + values[i] = v; + defaulted.Add(v); + fillDefaults.Add(Ast.Assign(v, Ast.Condition( + Ast.ReferenceEqual(args[i], Ast.Field(null, typeof(RubyOps).GetField("DefaultArgument"))), + Ast.Constant(_defaults[i], typeof(object)), + args[i] + ))); + } else { + values[i] = args[i]; + } + } + + MSA.Expression test = null; if (pinnedClass != null) { - test = Ast.AndAlso(test, Ast.ReferenceEqual( + test = And(test, Ast.ReferenceEqual( Ast.Call(JitRuntime.M("ClassOf"), Ast.Constant(_context), self), Ast.Constant(pinnedClass, typeof(RubyClass)))); } - for (int i = 0; i < _arity; i++) { + + MSA.ParameterExpression[] keywordValues = null; + if (_keywords != null) { + MSA.Expression kw = args[_positional]; + // the separation check on the last positional: `m("a" => 1)' gave it the keywords + if (_positional > 0 && code.ParameterTypes[_positional - 1] == JT.Obj) { + test = And(test, Ast.Not(Ast.Call(typeof(RubyOps).GetMethod("IsKeywordArgumentsHash"), args[_positional - 1]))); + } + keywordValues = new MSA.ParameterExpression[_keywords.Count]; + for (int j = 0; j < keywordValues.Length; j++) { + values[_positional + j] = keywordValues[j] = Ast.Variable(typeof(object), "kw" + j); + } + var info = Ast.Constant(_keywords, typeof(object)); + if (keywordValues.Length <= 3) { + // match and bind in one call, the values coming back through out parameters + var readArgs = new MSA.Expression[keywordValues.Length + 2]; + readArgs[0] = info; + readArgs[1] = kw; + Array.Copy(keywordValues, 0, readArgs, 2, keywordValues.Length); + test = And(test, Ast.Call(JitRuntime.M("KeywordsRead" + keywordValues.Length), readArgs)); + } else { + test = And(test, Ast.Call(JitRuntime.M("KeywordsMatch"), info, kw)); + for (int j = 0; j < keywordValues.Length; j++) { + test = And(test, Ast.Block( + Ast.Assign(keywordValues[j], Ast.Call(JitRuntime.M("KeywordValue"), info, kw, Ast.Constant(j))), + Ast.Constant(true))); + } + } + } + + for (int i = 0; i < _typedCount; i++) { switch (code.ParameterTypes[i]) { - case JT.Int: test = Ast.AndAlso(test, Ast.TypeIs(args[i], typeof(int))); break; - case JT.Dbl: test = Ast.AndAlso(test, Ast.TypeIs(args[i], typeof(double))); break; + case JT.Int: test = And(test, Ast.TypeIs(values[i], typeof(int))); break; + case JT.Dbl: test = And(test, Ast.TypeIs(values[i], typeof(double))); break; // An Integer parameter carried as a long accepts either representation: a // value in Int32 range arrives as an Int32 and only a larger one as an Int64. - case JT.Lng: test = Ast.AndAlso(test, Ast.Call(JitRuntime.M("IsIntegral"), args[i])); break; + case JT.Lng: test = And(test, Ast.Call(JitRuntime.M("IsIntegral"), values[i])); break; } } - var typedArgs = new MSA.Expression[_arity + 1]; + var typedArgs = new MSA.Expression[_typedCount + 1]; typedArgs[0] = self; - for (int i = 0; i < _arity; i++) { + for (int i = 0; i < _typedCount; i++) { switch (code.ParameterTypes[i]) { - case JT.Int: typedArgs[i + 1] = Ast.Unbox(args[i], typeof(int)); break; - case JT.Dbl: typedArgs[i + 1] = Ast.Unbox(args[i], typeof(double)); break; - case JT.Lng: typedArgs[i + 1] = Ast.Call(JitRuntime.M("ToLong"), args[i]); break; - default: typedArgs[i + 1] = args[i]; break; + case JT.Int: typedArgs[i + 1] = Ast.Unbox(values[i], typeof(int)); break; + case JT.Dbl: typedArgs[i + 1] = Ast.Unbox(values[i], typeof(double)); break; + case JT.Lng: typedArgs[i + 1] = Ast.Call(JitRuntime.M("ToLong"), values[i]); break; + default: typedArgs[i + 1] = values[i]; break; } } @@ -237,17 +367,116 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls) { } if (code.CanDeopt) { - fast = Ast.TryCatch(fast, - Ast.Catch(typeof(JitDeoptException), GenericCall(self, blk, args))); + // The generic body runs after the handler, not inside it: an exception it raises + // from within a catch block would leave the frames outside this one out of its + // backtrace. + var result = Ast.Variable(typeof(object), "#result"); + var deopted = Ast.Constant(Deopted, typeof(object)); + fast = Ast.Block(typeof(object), new[] { result }, + Ast.TryCatch( + Ast.Assign(result, fast), + Ast.Catch(typeof(JitDeoptException), Ast.Assign(result, deopted)) + ), + Ast.Condition(Ast.ReferenceEqual(result, deopted), GenericCall(self, blk, args), result, typeof(object)) + ); } - var body = Ast.Condition(test, fast, GenericCall(self, blk, args), typeof(object)); + MSA.Expression body = (test != null) ? Ast.Condition(test, fast, GenericCall(self, blk, args), typeof(object)) : fast; + if (keywordValues != null) { + body = Ast.Block(typeof(object), keywordValues, body); + } + if (defaulted.Count > 0) { + fillDefaults.Add(body); + body = Ast.Block(typeof(object), defaulted, fillDefaults); + } + + // A stale version fails this guard on every later call; the stub takes the entry down + // (JitStub.VersionChanged) and this one call runs the generic body. + body = Ast.Condition( + Ast.Call(JitRuntime.M("VersionOk"), Ast.Constant(version)), + body, + Ast.Block( + Ast.Call(JitRuntime.M("EntryStale"), Ast.Constant(this, typeof(object))), + GenericCall(self, blk, args) + ), + typeof(object) + ); var all = new MSA.ParameterExpression[_arity + 2]; all[0] = self; all[1] = blk; Array.Copy(args, 0, all, 2, _arity); - return Ast.Lambda(_generic.GetType(), body, "jitentry$" + _ast.Name, all); + return Ast.Lambda(_generic.GetType(), body, EntryNamePrefix + _ast.Name, all); + } + + /// + /// The entry JitKeywordCall calls with the keyword values already bound (and the defaults of the + /// ones the call left out filled in): the same guards as BuildEntry's, but a failed one - or a + /// deopt - answers NoDirect, and the caller then makes the call the ordinary way. + /// + private MSA.Expression>/*!*/ BuildDirectEntry(JitCode/*!*/ code, + MSA.Expression/*!*/ callee, int version) { + + var self = Ast.Parameter(typeof(object), "self"); + var slots = new MSA.ParameterExpression[JitKeywordCall.MaxPositional + JitKeywordCall.MaxKeywords]; + for (int i = 0; i < slots.Length; i++) { + slots[i] = Ast.Parameter(typeof(object), "s" + i); + } + var values = new MSA.Expression[_typedCount]; + for (int i = 0; i < _positional; i++) { + values[i] = slots[i]; + } + for (int j = 0; j < _keywords.Count; j++) { + values[_positional + j] = slots[JitKeywordCall.MaxPositional + j]; + } + + MSA.Expression test = Ast.Call(JitRuntime.M("VersionOk"), Ast.Constant(version)); + // the separation check on the last positional, as in BuildEntry + if (_positional > 0 && code.ParameterTypes[_positional - 1] == JT.Obj) { + test = And(test, Ast.Not(Ast.Call(typeof(RubyOps).GetMethod("IsKeywordArgumentsHash"), values[_positional - 1]))); + } + var typedArgs = new MSA.Expression[_typedCount + 1]; + typedArgs[0] = self; + for (int i = 0; i < _typedCount; i++) { + switch (code.ParameterTypes[i]) { + case JT.Int: + test = And(test, Ast.TypeIs(values[i], typeof(int))); + typedArgs[i + 1] = Ast.Unbox(values[i], typeof(int)); + break; + case JT.Dbl: + test = And(test, Ast.TypeIs(values[i], typeof(double))); + typedArgs[i + 1] = Ast.Unbox(values[i], typeof(double)); + break; + case JT.Lng: + test = And(test, Ast.Call(JitRuntime.M("IsIntegral"), values[i])); + typedArgs[i + 1] = Ast.Call(JitRuntime.M("ToLong"), values[i]); + break; + default: + typedArgs[i + 1] = values[i]; + break; + } + } + + MSA.Expression fast = Ast.Invoke(callee, typedArgs); + if (code.ReturnType == JT.Lng) { + fast = Ast.Call(JitRuntime.M("NarrowLong"), fast); + } else if (fast.Type != typeof(object)) { + fast = Ast.Convert(fast, typeof(object)); + } + var noDirect = Ast.Field(null, typeof(JitKeywordCall).GetField("NoDirect")); + if (code.CanDeopt) { + fast = Ast.TryCatch(fast, Ast.Catch(typeof(JitDeoptException), noDirect)); + } + var body = Ast.Condition(test, fast, noDirect, typeof(object)); + + var all = new MSA.ParameterExpression[slots.Length + 1]; + all[0] = self; + Array.Copy(slots, 0, all, 1, slots.Length); + return Ast.Lambda>(body, DirectEntryNamePrefix + _ast.Name, all); + } + + private static MSA.Expression/*!*/ And(MSA.Expression left, MSA.Expression/*!*/ right) { + return (left != null) ? Ast.AndAlso(left, right) : right; } private MSA.Expression/*!*/ GenericCall(MSA.ParameterExpression/*!*/ self, MSA.ParameterExpression/*!*/ blk, MSA.ParameterExpression[]/*!*/ args) { @@ -270,12 +499,35 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls) { JitRuntime.HookStats(); var ps = ast.Parameters; - if (ps.Optional.Length != 0 || ps.Unsplat != null || ps.Block != null || ast.UsesBlock) { return generic; } - int arity = ps.Mandatory.Length; + if (ps.Unsplat != null || ps.Block != null || ast.UsesBlock) { return generic; } + int mandatoryCount = ps.Mandatory.Length; + int arity = mandatoryCount + ps.Optional.Length; if (arity > MaxArity) { return generic; } - for (int i = 0; i < arity; i++) { + for (int i = 0; i < mandatoryCount; i++) { if (!(ps.Mandatory[i] is LocalVariable)) { return generic; } } + + // An optional parameter is taken when its default is a literal the typed body could + // have produced itself - nil, true, false, a Fixnum or a Float: evaluating it has no + // effect and cannot see the other parameters, so the entry may fill it in up front. + // Keyword parameters arrive in one trailing optional parameter (see JitKeywords); a method + // with keywords has no other optional one (PrismAstBridge lowers that mix to a splat). + JitKeywords keywords = null; + bool[] hasDefault = null; + object[] defaults = null; + if (ps.Optional.Length > 0 && (keywords = JitKeywords.TryRecognize(ast, context)) == null) { + hasDefault = new bool[arity]; + defaults = new object[arity]; + for (int i = 0; i < ps.Optional.Length; i++) { + var opt = ps.Optional[i]; + var literal = opt.Right as Literal; + if (!(opt.Left is LocalVariable) || literal == null) { return generic; } + object v = literal.Value; + if (!(v == null || v is bool || v is int || v is double)) { return generic; } + hasDefault[mandatoryCount + i] = true; + defaults[mandatoryCount + i] = v; + } + } // Tracing and coverage need the real frame. if (context.RubyOptions.EnableTracing || context.RubyOptions.Profile) { return generic; } @@ -284,18 +536,38 @@ private bool SelfCallResolvesHere(RubyClass/*!*/ cls) { // into call site rules, so a method declined later still pays one extra indirect // call on every call for the life of the process. string rejectedBy; - if (!JitScreen.CanEverCompile(ast, out rejectedBy)) { + HashSet otherUses; + if (!JitScreen.CanEverCompile(ast, keywords, out rejectedBy, out otherUses)) { JitRuntime.RecordScreenRejection(rejectedBy); return generic; } + // over the typed body's parameters: the positionals, then a keyword method's keyword locals + int typedCount = mandatoryCount + ((keywords != null) ? keywords.Count : ps.Optional.Length); + bool[] storedOnly = null; + for (int i = 0; i < typedCount; i++) { + string name = (i < mandatoryCount) ? ((LocalVariable)ps.Mandatory[i]).Name + : (keywords != null) ? keywords.LocalNames[i - mandatoryCount] + : ((LocalVariable)ps.Optional[i - mandatoryCount].Left).Name; + if (!otherUses.Contains(name)) { + if (storedOnly == null) { storedOnly = new bool[typedCount]; } + storedOnly[i] = true; + } + } + JitStub stub; switch (arity) { - case 0: stub = new JitStub0(ast, context, declaringModule, generic); break; - case 1: stub = new JitStub1(ast, context, declaringModule, generic); break; - case 2: stub = new JitStub2(ast, context, declaringModule, generic); break; - default: stub = new JitStub3(ast, context, declaringModule, generic); break; + case 0: stub = new JitStub0(ast, context, declaringModule, generic, keywords); break; + case 1: stub = new JitStub1(ast, context, declaringModule, generic, keywords); break; + case 2: stub = new JitStub2(ast, context, declaringModule, generic, keywords); break; + case 3: stub = new JitStub3(ast, context, declaringModule, generic, keywords); break; + case 4: stub = new JitStub4(ast, context, declaringModule, generic, keywords); break; + case 5: stub = new JitStub5(ast, context, declaringModule, generic, keywords); break; + default: stub = new JitStub6(ast, context, declaringModule, generic, keywords); break; } + stub._hasDefault = hasDefault; + stub._defaults = defaults; + stub._storedOnly = storedOnly; return stub.Trampoline; } @@ -310,8 +582,8 @@ internal sealed class JitStub0 : JitStub { private readonly Func/*!*/ _gen; private readonly Func/*!*/ _tramp; - internal JitStub0(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic) - : base(ast, context, m, generic, 0) { + internal JitStub0(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 0, keywords) { _gen = (Func)generic; _tramp = Call; } @@ -333,8 +605,8 @@ internal sealed class JitStub1 : JitStub { private readonly Func/*!*/ _gen; private readonly Func/*!*/ _tramp; - internal JitStub1(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic) - : base(ast, context, m, generic, 1) { + internal JitStub1(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 1, keywords) { _gen = (Func)generic; _tramp = Call; } @@ -356,8 +628,8 @@ internal sealed class JitStub2 : JitStub { private readonly Func/*!*/ _gen; private readonly Func/*!*/ _tramp; - internal JitStub2(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic) - : base(ast, context, m, generic, 2) { + internal JitStub2(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 2, keywords) { _gen = (Func)generic; _tramp = Call; } @@ -379,8 +651,8 @@ internal sealed class JitStub3 : JitStub { private readonly Func/*!*/ _gen; private readonly Func/*!*/ _tramp; - internal JitStub3(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic) - : base(ast, context, m, generic, 3) { + internal JitStub3(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 3, keywords) { _gen = (Func)generic; _tramp = Call; } @@ -397,4 +669,73 @@ private object Call(object self, Proc block, object a0, object a1, object a2) { } } + internal sealed class JitStub4 : JitStub { + private volatile Func _fast; + private readonly Func/*!*/ _gen; + private readonly Func/*!*/ _tramp; + + internal JitStub4(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 4, keywords) { + _gen = (Func)generic; + _tramp = Call; + } + + internal override Delegate/*!*/ Trampoline { get { return _tramp; } } + protected override void Install(Delegate entry) { _fast = (Func)entry; } + + private object Call(object self, Proc block, object a0, object a1, object a2, object a3) { + var f = _fast; + if (f != null) { return f(self, block, a0, a1, a2, a3); } + Profile(self, new[] { a0, a1, a2, a3 }); + f = _fast; + return (f != null) ? f(self, block, a0, a1, a2, a3) : _gen(self, block, a0, a1, a2, a3); + } + } + + internal sealed class JitStub5 : JitStub { + private volatile Func _fast; + private readonly Func/*!*/ _gen; + private readonly Func/*!*/ _tramp; + + internal JitStub5(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 5, keywords) { + _gen = (Func)generic; + _tramp = Call; + } + + internal override Delegate/*!*/ Trampoline { get { return _tramp; } } + protected override void Install(Delegate entry) { _fast = (Func)entry; } + + private object Call(object self, Proc block, object a0, object a1, object a2, object a3, object a4) { + var f = _fast; + if (f != null) { return f(self, block, a0, a1, a2, a3, a4); } + Profile(self, new[] { a0, a1, a2, a3, a4 }); + f = _fast; + return (f != null) ? f(self, block, a0, a1, a2, a3, a4) : _gen(self, block, a0, a1, a2, a3, a4); + } + } + + internal sealed class JitStub6 : JitStub { + private volatile Func _fast; + private readonly Func/*!*/ _gen; + private readonly Func/*!*/ _tramp; + + internal JitStub6(MethodDeclaration ast, RubyContext context, RubyModule m, Delegate generic, JitKeywords keywords) + : base(ast, context, m, generic, 6, keywords) { + _gen = (Func)generic; + _tramp = Call; + } + + internal override Delegate/*!*/ Trampoline { get { return _tramp; } } + protected override void Install(Delegate entry) { _fast = (Func)entry; } + + private object Call(object self, Proc block, object a0, object a1, object a2, object a3, object a4, object a5) { + var f = _fast; + if (f != null) { return f(self, block, a0, a1, a2, a3, a4, a5); } + Profile(self, new[] { a0, a1, a2, a3, a4, a5 }); + f = _fast; + return (f != null) ? f(self, block, a0, a1, a2, a3, a4, a5) : _gen(self, block, a0, a1, a2, a3, a4, a5); + } + } + } diff --git a/Src/Ruby/Runtime/Loader.cs b/Src/Ruby/Runtime/Loader.cs index 491ccac30..27571f698 100644 --- a/Src/Ruby/Runtime/Loader.cs +++ b/Src/Ruby/Runtime/Loader.cs @@ -1208,6 +1208,38 @@ private void EndAutoloads(List autoloads, string/ #endregion + // The thread running ruby4.rb as the host's core prelude (--disable-gems), while it does. The + // only Ruby code on its stack then is the prelude's own, which backtraces leave out. + private Thread _corePreludeThread; + + /// + /// True while this thread runs ruby4.rb as the core prelude, loaded by the host itself: a + /// backtrace taken now has no frame in it (they are all the prelude's, which backtraces + /// skip), so a caller that only wants the innermost location need not walk the CLR stack. + /// + internal bool IsLoadingCorePrelude { + get { return _corePreludeThread == Thread.CurrentThread; } + } + + /// + /// Requires the core prelude - gem_prelude.rb, or ruby4.rb under --disable-gems - and then + /// lists the features MRI has in $" from the start. + /// + internal void LoadCorePrelude(Scope/*!*/ globalScope, object self, string/*!*/ path) { + bool direct = path == "ruby4.rb"; + if (direct) { + _corePreludeThread = Thread.CurrentThread; + } + try { + LoadFile(globalScope, self, MutableString.Create(path, RubyEncoding.UTF8), LoadFlags.Require); + } finally { + if (direct) { + _corePreludeThread = null; + } + } + ProvideCoreFeatures(); + } + // Features the core prelude loaded: kept out of $" (MRI's own Ruby-defined core is not in // it either) but still loaded, so that requiring one again does nothing. private readonly HashSet/*!*/ _hiddenLoadedFiles = new HashSet(StringComparer.Ordinal); diff --git a/Src/Ruby/Runtime/ObjectTracking.cs b/Src/Ruby/Runtime/ObjectTracking.cs index a544dd602..0615d1842 100644 --- a/Src/Ruby/Runtime/ObjectTracking.cs +++ b/Src/Ruby/Runtime/ObjectTracking.cs @@ -162,6 +162,9 @@ internal static void SetCurrentLine(RubyScope scope, string path, int line) { _path = path; _line = line; _scope = scope; + if (scope != null) { + scope.MarkCaptured(); + } _cachedSite = null; } diff --git a/Src/Ruby/Runtime/Osr/OsrLoopSite.cs b/Src/Ruby/Runtime/Osr/OsrLoopSite.cs index 7b03c1fe3..cae0f8c80 100644 --- a/Src/Ruby/Runtime/Osr/OsrLoopSite.cs +++ b/Src/Ruby/Runtime/Osr/OsrLoopSite.cs @@ -103,6 +103,8 @@ private static long ReadThreshold() { internal RubyContext Context; internal int ScopeDepth; internal Dictionary TupleArgIndex; + // Where in the argument array the loop's self is; -1 if it is not passed. + internal int SelfArgIndex = -1; private readonly object/*!*/ _lock = new object(); private readonly string/*!*/ _name; @@ -194,7 +196,7 @@ private Func Compile(object[]/*!*/ tuples) { long start = Stopwatch.GetTimestamp(); try { - _typed = JitLoopCompiler.TryCompile(LoopAst, Context, ScopeDepth, TupleArgIndex, tuples); + _typed = JitLoopCompiler.TryCompile(LoopAst, Context, ScopeDepth, TupleArgIndex, tuples, SelfArgIndex); } catch (Exception e) { if (Verbose) { Console.Error.WriteLine("[osr] {0}: specialization failed: {1}", _name, e.Message); diff --git a/Src/Ruby/Runtime/RubyContext.cs b/Src/Ruby/Runtime/RubyContext.cs index 2a324d82c..767dfdb0e 100644 --- a/Src/Ruby/Runtime/RubyContext.cs +++ b/Src/Ruby/Runtime/RubyContext.cs @@ -20,6 +20,7 @@ #endif using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; @@ -161,6 +162,19 @@ public void SetNativeProcessTitle(MutableString/*!*/ title) { private readonly Dictionary/*!*/ _globalVariables; public object/*!*/ GlobalVariablesLock { get { return _globalVariables; } } + /// + /// Changes whenever a name starts or stops denoting a GlobalVariable object - a define, + /// an alias, a delete, the first assignment of a new global - but not when a value is + /// assigned. A GlobalVariableSite keeps the object it found until this moves. + /// Written under GlobalVariablesLock (or before any Ruby code runs). + /// + private volatile int _globalVariablesVersion = GlobalVariableSite.NextVersion(); + internal int GlobalVariablesVersion { get { return _globalVariablesVersion; } } + + private void GlobalVariablesChanged() { + _globalVariablesVersion = GlobalVariableSite.NextVersion(); + } + // not thread safe: use GlobalVariablesLock to synchronize access to the variables: public IEnumerable>/*!*/ GlobalVariables { get { return _globalVariables; } @@ -258,6 +272,11 @@ public Random/*!*/ RandomNumberGenerator { private readonly Dictionary/*!*/ _valueTypeInstanceData; private object/*!*/ ValueTypeInstanceDataLock { get { return _valueTypeInstanceData; } } + // The number of entries in _valueTypeInstanceData, published after each add. Entries are + // never removed, so zero means there is nothing to find: then a lookup - every + // `1.is_a?(Hash)` asks for the immediate class of an Integer - takes no lock. + private int _valueTypeInstanceDataCount; + // not thread-safe: private readonly RubyInstanceData/*!*/ _nilInstanceData = new RubyInstanceData(RubyUtils.NilObjectId); @@ -811,6 +830,7 @@ private void InitializeGlobalConstants() { } else { _globalVariables["-i"] = new GlobalVariableInfo(null); } + GlobalVariablesChanged(); // Hash // SCRIPT_LINES__ @@ -1035,6 +1055,7 @@ internal void AddModuleToCacheNoLock(Type/*!*/ type, RubyModule/*!*/ module) { internal void ReplaceModuleInCacheNoLock(Type/*!*/ type, RubyModule/*!*/ module) { Assert.NotNull(type, module); _moduleCache[type] = module; + _classReadCache.TryRemove(type, out _); } internal void AddNamespaceToCacheNoLock(NamespaceTracker/*!*/ namespaceTracker, RubyModule/*!*/ module) { @@ -1089,9 +1110,21 @@ internal bool TryGetClassNoLock(Type/*!*/ type, out RubyClass result) { } } + // Lock-free read side of GetOrCreateClass. Every call on an Integer, Float or other CLR value + // asks for its class (GetClassOf), and taking ModuleCacheLock for a dictionary probe each time + // was a third or more of numeric code. A CLR type's class, once created, is only replaced by + // ReplaceModuleInCacheNoLock, which evicts the type here; entries are written under the lock. + private readonly ConcurrentDictionary/*!*/ _classReadCache = new ConcurrentDictionary(); + internal RubyClass/*!*/ GetOrCreateClass(Type/*!*/ type) { + RubyClass result; + if (_classReadCache.TryGetValue(type, out result)) { + return result; + } lock (ModuleCacheLock) { - return GetOrCreateClassNoLock(type); + result = GetOrCreateClassNoLock(type); + _classReadCache[type] = result; + return result; } } @@ -2110,6 +2143,9 @@ internal RubyInstanceData TryGetInstanceData(object obj) { RubyInstanceData result; if (!RubyUtils.HasObjectState(obj) && !(obj is BigInteger)) { + if (Volatile.Read(ref _valueTypeInstanceDataCount) == 0) { + return null; + } lock (ValueTypeInstanceDataLock) { _valueTypeInstanceData.TryGetValue(obj, out result); } @@ -2176,6 +2212,7 @@ public void ReplaceClrObjectClass(object/*!*/ obj, RubyClass/*!*/ cls) { lock (ValueTypeInstanceDataLock) { if (!_valueTypeInstanceData.TryGetValue(obj, out result)) { _valueTypeInstanceData.Add(obj, result = new RubyInstanceData()); + Volatile.Write(ref _valueTypeInstanceDataCount, _valueTypeInstanceData.Count); } } return result; @@ -2499,12 +2536,14 @@ public object GetGlobalVariable(string/*!*/ name) { public void DefineGlobalVariable(string/*!*/ name, object value) { lock (GlobalVariablesLock) { _globalVariables[name] = new GlobalVariableInfo(value); + GlobalVariablesChanged(); } } public void DefineReadOnlyGlobalVariable(string/*!*/ name, object value) { lock (GlobalVariablesLock) { _globalVariables[name] = new ReadOnlyGlobalVariableInfo(value); + GlobalVariablesChanged(); } } @@ -2512,15 +2551,18 @@ public void DefineGlobalVariable(string/*!*/ name, GlobalVariable/*!*/ variable) ContractUtils.RequiresNotNull(variable, "variable"); lock (GlobalVariablesLock) { _globalVariables[name] = variable; + GlobalVariablesChanged(); } } internal void DefineGlobalVariableNoLock(string/*!*/ name, GlobalVariable/*!*/ variable) { _globalVariables[name] = variable; + GlobalVariablesChanged(); } public bool DeleteGlobalVariable(string/*!*/ name) { lock (GlobalVariablesLock) { + GlobalVariablesChanged(); return _globalVariables.Remove(name); } } @@ -2532,6 +2574,7 @@ public void AliasGlobalVariable(string/*!*/ newName, string/*!*/ oldName) { DefineGlobalVariableNoLock(oldName, existing = new GlobalVariableInfo(null, false)); } _globalVariables[newName] = existing; + GlobalVariablesChanged(); } } @@ -2544,6 +2587,7 @@ public void SetGlobalVariable(RubyScope scope, string/*!*/ name, object value) { global.SetValue(this, scope, name, value); } else { _globalVariables[name] = new GlobalVariableInfo(value); + GlobalVariablesChanged(); } } @@ -2575,6 +2619,36 @@ internal bool TryGetGlobalVariable(string/*!*/ name, out GlobalVariable variable } } + /// The variable the name denotes and the version of the name table it was found at. + internal bool TryGetGlobalVariable(string/*!*/ name, out GlobalVariable variable, out int version) { + lock (GlobalVariablesLock) { + version = _globalVariablesVersion; + return _globalVariables.TryGetValue(name, out variable); + } + } + + /// + /// The value of a variable a GlobalVariableSite found: what TryGetGlobalVariable would + /// answer for its name, minus the lookup. An ordinary global is a field read; anything + /// else (a special or read-only variable, or one a library defined) keeps running under + /// the lock. + /// + internal object GetGlobalVariableValue(GlobalVariable/*!*/ variable, RubyScope scope) { + lock (GlobalVariablesLock) { + return variable.GetValue(this, scope); + } + } + + /// + /// Runs the Kernel#trace_var handlers of a global that has just been assigned outside + /// SetGlobalVariable (which runs them itself). + /// + internal void GlobalVariableAssigned(RubyScope scope, string/*!*/ name, object value) { + if (_anyGlobalVariableTraced) { + FireGlobalVariableTraces(scope, name, value); + } + } + #endregion #region Global Variables: Kernel#trace_var handlers (thread-safe) @@ -2596,6 +2670,9 @@ public sealed class GlobalVariableTraceCommand { public GlobalVariableTraceCommand(MutableString/*!*/ code, RubyScope scope, object self) { Code = code; Scope = scope; + if (scope != null) { + scope.MarkCaptured(); + } Self = self; } } diff --git a/Src/Ruby/Runtime/RubyExceptionData.cs b/Src/Ruby/Runtime/RubyExceptionData.cs index 0564ddb7f..b545fd1db 100644 --- a/Src/Ruby/Runtime/RubyExceptionData.cs +++ b/Src/Ruby/Runtime/RubyExceptionData.cs @@ -64,6 +64,19 @@ public sealed class RubyExceptionData { // can be set explicitly by the user (even to nil): private RubyArray _backtrace; +#if FEATURE_STACK_TRACE + // The backtrace the runtime captured when the exception was raised, until someone asks for + // it (see CaptureExceptionTrace). Only ever set while _backtrace is null. + [NonSerialized] + private RubyStackTraceBuilder.CapturedTrace _capturedTrace; + + // The catch-site walk of the interpreted frame that last caught an exception on this thread. + [ThreadStatic] + private static WeakReference _lastCatchFrame; + [ThreadStatic] + private static StackTrace _lastCatchSiteTrace; +#endif + // Exception#cause; _hasCause tells "decided" from "not raised yet", see TrySetCause. private Exception _cause; private bool _hasCause; @@ -136,16 +149,68 @@ public static RubyArray CreateBacktrace(RubyContext/*!*/ context, Thread/*!*/ th /// Called from compiled code. /// internal void CaptureExceptionTrace(RubyScope/*!*/ scope) { - if (_backtrace == null) { + if (!HasBacktrace) { + var context = scope.RubyContext; #if FEATURE_STACK_TRACE - StackTrace catchSiteTrace = RubyStackTraceBuilder.GetClrStackTrace(null, - scope.RubyContext.DomainManager.Configuration.DebugMode); - _backtrace = new RubyStackTraceBuilder(scope.RubyContext, _exception, catchSiteTrace, scope.InterpretedFrame != null).RubyTrace; + bool debugMode = context.DomainManager.Configuration.DebugMode; + + // The catch-site walk is the expensive part of a raise: the whole CLR stack, every frame's + // method resolved. All but its top - the frames this one sees the exception from - lies + // below the catching frame, and nothing below a frame changes while the frame lives. So an + // interpreted frame that keeps catching (a loop around begin/rescue) reuses the walk it + // took the first time. Interpreted frames because their identity is that of a single + // invocation (a scope is not: eval code runs in the scope of its binding); not in the modes + // that report CLR frames, which would show the top of the walk too. + StackTrace catchSiteTrace = null; + InterpretedFrame frame = scope.InterpretedFrame; + WeakReference lastFrame = _lastCatchFrame; + bool reusable = frame != null && !debugMode && !context.Options.ExceptionDetail && + frame == InterpretedFrame.CurrentFrame.Value; + + InterpretedFrame last; + if (reusable && lastFrame != null && lastFrame.TryGetTarget(out last) && last == frame) { + catchSiteTrace = _lastCatchSiteTrace; + } + + if (catchSiteTrace == null) { + catchSiteTrace = RubyStackTraceBuilder.GetClrStackTrace(null, debugMode); + if (reusable && RubyStackTraceBuilder.IsReusableCatchSiteTrace(catchSiteTrace)) { + _lastCatchSiteTrace = catchSiteTrace; + if (lastFrame != null) { + lastFrame.SetTarget(frame); + } else { + _lastCatchFrame = new WeakReference(frame); + } + } + } + + // formatted on first use, see Backtrace: + _capturedTrace = new RubyStackTraceBuilder.CapturedTrace(context, _exception, catchSiteTrace, scope.InterpretedFrame != null); #else _backtrace = new RubyArray(); #endif - DynamicSetBacktrace(scope.RubyContext, _backtrace); + // MRI (setup_exception) only calls #set_backtrace when it has been redefined: + if (!HasBuiltinSetBacktrace(context)) { + DynamicSetBacktrace(context, Backtrace); + } + } + } + + /// + /// Whether #set_backtrace of the exception is IronRuby's own: the library method, or the + /// prelude's (Src/StdLib/ironruby/ruby4.rb) which keeps Thread::Backtrace::Locations around + /// and passes strings on to the library method. Called with the backtrace the runtime has just + /// captured, the prelude's would only parse the strings into Locations eagerly - which its + /// #backtrace_locations does on demand all the same. + /// + private bool HasBuiltinSetBacktrace(RubyContext/*!*/ context) { + var method = context.GetImmediateClassOf(_exception).ResolveMethod("set_backtrace", VisibilityContext.AllVisible).Info; + if (method is RubyLibraryMethodInfo) { + return true; } + + var rubyMethod = method as RubyMethodInfo; + return rubyMethod != null && rubyMethod.Document != null && RubyStackTraceBuilder.IsInternalFile(rubyMethod.Document.FileName); } /// @@ -227,13 +292,51 @@ public object Message { public RubyArray Backtrace { get { +#if FEATURE_STACK_TRACE + if (_capturedTrace != null) { + FormatCapturedTrace(); + } +#endif return _backtrace; } set { +#if FEATURE_STACK_TRACE + _capturedTrace = null; +#endif _backtrace = value; } } + /// + /// Whether the exception has a backtrace, without formatting a captured one. + /// + public bool HasBacktrace { + get { +#if FEATURE_STACK_TRACE + return _backtrace != null || _capturedTrace != null; +#else + return _backtrace != null; +#endif + } + } + +#if FEATURE_STACK_TRACE + private void FormatCapturedTrace() { + var captured = _capturedTrace; + if (captured == null) { + return; + } + + // another thread may ask for the same exception's backtrace (Thread#value, #join) + lock (captured) { + if (_capturedTrace == captured) { + _backtrace = captured.Build(); + _capturedTrace = null; + } + } + } +#endif + /// /// Exception#cause. MRI fixes the cause the first time an exception is raised and never /// changes it afterwards, so re-raising a rescued exception keeps the original chain. @@ -372,6 +475,9 @@ public void SetCause(Exception cause) { newInstanceData.Handled = true; newInstanceData._backtrace = instanceData._backtrace; +#if FEATURE_STACK_TRACE + newInstanceData._capturedTrace = instanceData._capturedTrace; +#endif return newException; } diff --git a/Src/Ruby/Runtime/RubyInstanceData.cs b/Src/Ruby/Runtime/RubyInstanceData.cs index 861d02450..33bc4c061 100644 --- a/Src/Ruby/Runtime/RubyInstanceData.cs +++ b/Src/Ruby/Runtime/RubyInstanceData.cs @@ -275,7 +275,7 @@ private static int GetCapacity(RubyClass hintClass, int needed, int current) { return Math.Max(capacity, needed); } - private static void UpdateCapacityHint(RubyClass hintClass, int count) { + internal static void UpdateCapacityHint(RubyClass hintClass, int count) { if (hintClass != null && count > hintClass.InstanceVariableCapacity && count <= MaxCapacityHint) { // racy but benign: a lost update only costs a later object a move hintClass.InstanceVariableCapacity = count; diff --git a/Src/Ruby/Runtime/RubyOps.FlowControl.cs b/Src/Ruby/Runtime/RubyOps.FlowControl.cs index bbddd2572..7f6d794be 100644 --- a/Src/Ruby/Runtime/RubyOps.FlowControl.cs +++ b/Src/Ruby/Runtime/RubyOps.FlowControl.cs @@ -26,14 +26,23 @@ namespace IronRuby.Runtime { public static partial class RubyOps { #region RFC Flags + /// + /// The flow-control frame of a call to a Ruby method with a block: a break from the block unwinds to it. + /// A literal block (ProcKind.Block) is a Proc made for this call alone, so the Proc itself is that frame - + /// active until LeaveMethodFrame - and becomes the block's converter. Any other block has been converted + /// already, so nothing can target this call: it shares one frame that is never active and never a converter. + /// [Emitted] public static RuntimeFlowControl/*!*/ CreateRfcForMethod(Proc proc) { - var result = new RuntimeFlowControl(); - result._activeFlowControlScope = result; - result.InitializeRfc(proc); - return result; + if (proc != null && proc.Kind == ProcKind.Block) { + proc.ConvertAsFrame(); + return proc; + } + return InactiveRfc; } + private static readonly RuntimeFlowControl/*!*/ InactiveRfc = new RuntimeFlowControl(); + [Emitted] public static void EnterLoop(RubyScope/*!*/ scope) { scope.InLoop = true; @@ -377,6 +386,9 @@ public static bool BlockYield(RubyScope/*!*/ scope, BlockParam/*!*/ ownerBlockFl /// True if the method should terminate returning the result of yield. [Emitted] public static bool MethodYield(RubyScope/*!*/ scope, BlockParam/*!*/ yieldedBlockFlowControl, object returnValue) { + if (yieldedBlockFlowControl.ReturnReason == BlockReturnReason.Undefined) { + return false; + } return MethodYieldRfc(scope.FlowControlScope, yieldedBlockFlowControl, returnValue); } diff --git a/Src/Ruby/Runtime/RubyOps.cs b/Src/Ruby/Runtime/RubyOps.cs index 33e68e2dd..6250e8800 100644 --- a/Src/Ruby/Runtime/RubyOps.cs +++ b/Src/Ruby/Runtime/RubyOps.cs @@ -238,9 +238,105 @@ public static object MarkAliasCall(object lastArgument, string/*!*/ calleeName, return scope; } - // TracePoint hooks; emitted code only calls them when TracePoint.ActiveEvents has the event. + /// + /// The entry of a block whose body never uses its scope (see BlockDefinition.IsScopeFreeBody): + /// the safe point, and the scope only if a TracePoint wants the call event. Null otherwise; a + /// TracePoint hook enabled later makes it then (). + /// + [Emitted] + public static RubyBlockScope CreateBlockScopeIfTraced(MutableTuple locals, string[] variableNames, + BlockParam/*!*/ blockParam, object selfObject, InterpretedFrame interpretedFrame) { + + RubyUtils.SafePoint(); + if ((TracePoint.ActiveEvents & (int)(TraceEvents.BCall | TraceEvents.Call)) != 0) { + var scope = new RubyBlockScope(locals, variableNames ?? ArrayUtils.EmptyStrings, blockParam, selfObject, interpretedFrame); + TracePoint.OnBlockCall(scope); + return scope; + } + return null; + } + + /// + /// The first step of the entry of a block whose body needs its scope but does not define a closure + /// over it: the safe point, then the scope the last finished call through this BlockParam left there + /// (each, map, times, a yield in a loop; see BlockParam.ParkScope) unless something captured it, or null. + /// The scope comes off the BlockParam here, after the safe point (which can run a trap handler or a + /// finalizer) and together with its locals tuple: a call that starts in between finds nothing kept and + /// makes a scope and tuple of its own, and a call that does not finish normally keeps nothing. + /// The block reuses the scope's tuple (see ) and hands the scope to + /// ; parks it again. + /// + [Emitted, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RubyBlockScope TakeReusableBlockScope(BlockParam/*!*/ blockParam) { + // Block entry too: `n.times { }` and `each { }` loop in C#, where no back edge is emitted. + RubyUtils.SafePoint(); + + if (blockParam.MayHaveReusableScope) { + var scope = blockParam.ReusableScope; + if (scope != null && !scope.IsCaptured) { + blockParam.TakeReusableScope(); + return scope; + } + } + return null; + } + + /// + /// The locals tuple of a scope took, for the block to reuse (the + /// last call cleared it); null if it took none. + /// + [Emitted, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MutableTuple GetReusableLocals(RubyBlockScope reusable) { + return (reusable != null) ? reusable._locals : null; + } + + /// + /// The scope of a block call that began: the scope it took, made the + /// scope of this call, or a new one. + /// + [Emitted, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RubyBlockScope/*!*/ CreateReusableBlockScope(MutableTuple locals, string[] variableNames, + BlockParam/*!*/ blockParam, object selfObject, InterpretedFrame interpretedFrame, RubyBlockScope reusable) { + + RubyBlockScope scope; + if (reusable != null) { + scope = reusable; + scope.Reinitialize(locals, blockParam, selfObject, interpretedFrame); + } else { + scope = new RubyBlockScope(locals, variableNames ?? ArrayUtils.EmptyStrings, blockParam, selfObject, interpretedFrame); + } + + if ((TracePoint.ActiveEvents & (int)(TraceEvents.BCall | TraceEvents.Call)) != 0) { + scope.MarkCaptured(); + TracePoint.OnBlockCall(scope); + } + return scope; + } + + /// + /// The normal exit of a block that made its scope by . + /// An exception leaving the body skips this, and the scope is simply not reused. True if the + /// scope was kept on the BlockParam, with its locals tuple when : the + /// block then clears the tuple, so that the next call finds its locals nil. The first call through + /// a BlockParam keeps nothing (see BlockParam.ParkScope): a block called once pays no more than a flag. + /// + [Emitted, MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool LeaveReusableBlockScope(RubyBlockScope/*!*/ scope, bool keepLocals) { + return scope.BlockFlowControl.ParkScope(scope); + } [Emitted] + public static RubyBlockScope/*!*/ CreateBlockScopeLazy(MutableTuple locals, string[] variableNames, + BlockParam/*!*/ blockParam, object selfObject, InterpretedFrame interpretedFrame) { + return new RubyBlockScope(locals, variableNames ?? ArrayUtils.EmptyStrings, blockParam, selfObject, interpretedFrame); + } + + // TracePoint hooks; emitted code only calls them when TracePoint.ActiveEvents has the event. + + // Emitted behind a test of TracePoint.ActiveEvents in front of every statement: kept out of + // line, the never-taken branch is one call; inlined, it was a few hundred bytes of machine + // code per statement in every compiled method body. + [Emitted, MethodImpl(MethodImplOptions.NoInlining)] public static void TraceLineEvent(RubyScope scope, string path, int line) { // objspace's allocation tracing forces this event on and takes the allocation site of // every object created by the statement from here. @@ -438,11 +534,11 @@ private static bool IsRefinedHere(RubyScope/*!*/ scope, object value, string/*!* [Emitted] public static Proc InstantiateBlock(RubyScope/*!*/ scope, object self, BlockDispatcher/*!*/ dispatcher) { - return (dispatcher.Method != null) ? new Proc(ProcKind.Block, self, scope, dispatcher) : null; + return dispatcher.HasMethod ? new Proc(ProcKind.Block, self, scope, dispatcher) : null; } [Emitted] public static Proc InstantiateLambda(RubyScope/*!*/ scope, object self, BlockDispatcher/*!*/ dispatcher) { - return (dispatcher.Method != null) ? new Proc(ProcKind.Lambda, self, scope, dispatcher) : null; + return dispatcher.HasMethod ? new Proc(ProcKind.Lambda, self, scope, dispatcher) : null; } [Emitted] @@ -513,9 +609,19 @@ public static void RequireLambdaArity(Proc/*!*/ proc, int argCount) { /// only the positional arguments against a lambda, and `|**nil|` refuses keywords in a /// proc as well as in a lambda. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RequireLambdaArity(Proc/*!*/ proc, int argCount, object lastArg) { + // A plain block takes any arguments - the common case, so it does not make the call: + // only a lambda, or a signature with keywords or **nil, has anything to check. + if (proc.Kind == ProcKind.Lambda || proc.Dispatcher.ChecksKeywords) { + CheckLambdaArity(proc, argCount, lastArg); + } + } + + private static void CheckLambdaArity(Proc/*!*/ proc, int argCount, object lastArg) { var signature = proc.Dispatcher.ParameterSignature; - if (signature != null && (signature.TakesKeywords || signature.RefusesKeywords)) { + // = signature != null && (signature.TakesKeywords || signature.RefusesKeywords), cached + if (proc.Dispatcher.ChecksKeywords) { var keywords = argCount > 0 ? lastArg as Hash : null; if (keywords != null && !keywords.IsKeywordArguments) { keywords = null; @@ -1578,6 +1684,36 @@ public static object SetQualifiedConstant(object value, object target, RubyScope /// is only emitted for argument hashes that actually contain a `**` splat. /// The result is fed to the ordinary splatting machinery, so it is either [] or [hash]. /// + /// + /// `f(k: v, **h)`: h's entries added to the keyword hash being built for the call - the + /// call's own, so it is filled in place - which is returned; nil adds nothing. Null if h is + /// anything else: that goes through Hash#merge, for #to_hash. + /// + [Emitted] + public static object MergeKeywordSplat(RubyScope/*!*/ scope, object keywords, object splatted) { + if (splatted == null) { + return keywords; + } + var source = splatted as Hash; + var target = keywords as Hash; + if (source == null || target == null) { + return null; + } + foreach (var entry in source) { + RubyUtils.SetHashElement(scope.RubyContext, target, entry.Key, entry.Value); + } + return target; + } + + /// + /// `f(**h)` with h a Hash or nil: what makes of it. Null for + /// anything else, which the call site first converts with #to_hash (by way of Hash#merge). + /// + [Emitted] + public static RubyArray SplatKeywordHashIfHash(RubyScope/*!*/ scope, object hash) { + return (hash == null || hash is Hash) ? SplatKeywordHash(scope, hash) : null; + } + [Emitted] public static RubyArray/*!*/ SplatKeywordHash(RubyScope/*!*/ scope, object hash) { // `f(**nil)` passes no keywords at all @@ -1608,47 +1744,35 @@ public static object SetQualifiedConstant(object value, object target, RubyScope [Emitted] public static RubyArray/*!*/ MakeArray1(object item1) { - RubyArray result = new RubyArray(1); - result.Add(item1); - return result; + return new RubyArray(new object[] { item1, null, null, null }, 0, 1); } [Emitted] public static RubyArray/*!*/ MakeArray2(object item1, object item2) { - RubyArray result = new RubyArray(2); - result.Add(item1); - result.Add(item2); - return result; + return new RubyArray(new object[] { item1, item2, null, null }, 0, 2); } [Emitted] public static RubyArray/*!*/ MakeArray3(object item1, object item2, object item3) { - RubyArray result = new RubyArray(3); - result.Add(item1); - result.Add(item2); - result.Add(item3); - return result; + return new RubyArray(new object[] { item1, item2, item3, null }, 0, 3); } [Emitted] public static RubyArray/*!*/ MakeArray4(object item1, object item2, object item3, object item4) { - RubyArray result = new RubyArray(4); - result.Add(item1); - result.Add(item2); - result.Add(item3); - result.Add(item4); - return result; + return new RubyArray(new object[] { item1, item2, item3, item4 }, 0, 4); } [Emitted] public static RubyArray/*!*/ MakeArray5(object item1, object item2, object item3, object item4, object item5) { - RubyArray result = new RubyArray(5); - result.Add(item1); - result.Add(item2); - result.Add(item3); - result.Add(item4); - result.Add(item5); - return result; + return new RubyArray(new object[] { item1, item2, item3, item4, item5 }, 0, 5); + } + + // The elements of an array literal of more than five, in an array built for it alone + // (ReflectionCache.MakeArrayOpCall): the Array takes it over instead of copying it. + [Emitted] + public static RubyArray/*!*/ MakeArrayOwned(object[]/*!*/ items) { + Debug.Assert(items.Length >= Utils.MinListSize); + return new RubyArray(items, 0, items.Length); } [Emitted] @@ -1691,6 +1815,36 @@ public static object SetQualifiedConstant(object value, object target, RubyScope return result; } + // `f(a: x)`, `f(a: x, b: y)` and `f(a: x, b: y, c: z)` with Symbol literals for keys, the + // usual keyword call: no item array to allocate, and none of the conversions + // SetHashElement applies to a String or nil key. A repeated key keeps the last value, as + // in MRI (and SetHashElements). + + [Emitted] + public static Hash/*!*/ MakeKeywordArgumentsHash1(RubyScope/*!*/ scope, object key1, object value1) { + var result = Hash.CreateKeywordArguments(scope.RubyContext.EqualityComparer, 1); + result.AppendUnlinked(key1, value1); + return result; + } + + [Emitted] + public static Hash/*!*/ MakeKeywordArgumentsHash2(RubyScope/*!*/ scope, object key1, object value1, object key2, object value2) { + var result = Hash.CreateKeywordArguments(scope.RubyContext.EqualityComparer, 2); + result.AppendUnlinked(key1, value1); + result.AppendUnlinked(key2, value2); + return result; + } + + [Emitted] + public static Hash/*!*/ MakeKeywordArgumentsHash3(RubyScope/*!*/ scope, object key1, object value1, object key2, object value2, + object key3, object value3) { + var result = Hash.CreateKeywordArguments(scope.RubyContext.EqualityComparer, 3); + result.AppendUnlinked(key1, value1); + result.AppendUnlinked(key2, value2); + result.AppendUnlinked(key3, value3); + return result; + } + /// True for the hash a call site built out of keyword syntax. [Emitted] public static bool IsKeywordArgumentsHash(object obj) { @@ -1698,6 +1852,127 @@ public static bool IsKeywordArgumentsHash(object obj) { return hash != null && hash.IsKeywordArguments; } + // The keyword-parameter prologue (see PrismAstBridge.BindKeywordsFrom) reads the hash a + // keyword call passed with the helpers below rather than with Ruby-level Hash calls. MRI + // binds keywords without dispatching to Hash methods either, so a redefined Hash#[] or + // Hash#key? has no say in it. The hash is the one the call site built (or a copy of a + // splatted one), so it is a plain Hash with the default comparer. + + /// + /// True when holds every required keyword - the first + /// of - and, unless the method + /// takes **rest, no keyword it does not declare. False means the prologue has an + /// ArgumentError to word, which it does the slow way. + /// + [Emitted] + public static bool KeywordArgumentsMatch(object keywords, RubySymbol/*!*/[]/*!*/ names, int requiredCount, bool acceptsRest) { + var hash = keywords as Hash; + return hash != null && hash.HasKeywords(names, requiredCount, acceptsRest); + } + + /// + /// The value the call passed for keyword , or + /// if it passed none. is where the + /// method declares the keyword, which is where a call usually puts it too. + /// + [Emitted] + public static object GetKeywordArgument(object keywords, RubySymbol/*!*/ name, int position) { + var hash = keywords as Hash; + object value; + return (hash != null && hash.TryGetValueByIdentity(name, position, out value)) ? value : DefaultArgument; + } + + // A signature that mixes keywords with optional, rest or post positionals is lowered to a + // single splat that its prologue takes apart (PrismAstBridge.LowerGeneralParameters) with + // the helpers below rather than with Ruby-level Array calls. The splat is the array the + // call made for this call - or, for a block that auto-splats an Array argument, a copy of + // it (see AutoSplatArgument) - so consuming it changes nothing anyone else can see. + + /// The arguments as they arrived, kept aside for a parameterless `super`. + [Emitted] + public static RubyArray/*!*/ CopySplatArguments(object args) { + return new RubyArray((RubyArray)args); + } + + /// + /// Takes the hash a keyword call passed off the end of the arguments, or makes an empty one + /// if the call passed no keywords. + /// + [Emitted] + public static Hash/*!*/ PopKeywordArguments(RubyScope/*!*/ scope, object args) { + var list = (RubyArray)args; + int last = list.Count - 1; + if (last >= 0 && IsKeywordArgumentsHash(list[last])) { + return (Hash)list.TakeLast(); + } + return MakeHash0(scope); + } + + /// A block's single Array argument, which it auto-splats. + [Emitted] + public static bool IsAutoSplatArgument(object args) { + var list = (RubyArray)args; + return list.Count == 1 && list[0] is RubyArray; + } + + /// + /// The arguments a block that auto-splats its single Array argument binds: a copy of the + /// array, which the prologue consumes - the caller's array is left as it was. + /// + [Emitted] + public static RubyArray/*!*/ AutoSplatArgument(object args) { + return new RubyArray((RubyArray)((RubyArray)args)[0]); + } + + /// + /// The arity check of a method or lambda whose positionals are in the splat. A negative + /// means a rest parameter took the cap off. + /// + [Emitted] + public static void CheckSplatArity(object args, int minimum, int maximum) { + int count = ((RubyArray)args).Count; + if (count < minimum || (maximum >= 0 && count > maximum)) { + var inv = System.Globalization.CultureInfo.InvariantCulture; + throw (minimum == maximum) ? MakeWrongNumberOfArgumentsError(count, minimum) + : MakeWrongNumberOfArgumentsErrorN(count, minimum.ToString(inv) + (maximum < 0 ? "+" : ".." + maximum.ToString(inv))); + } + } + + /// The next positional argument; nil if there is none left (a block's). + [Emitted] + public static object ShiftSplatArgument(object args) { + return ((RubyArray)args).TakeFirst(); + } + + /// + /// The argument of an optional parameter: the next one if the + /// post parameters leave one, otherwise. + /// + [Emitted] + public static object ShiftOptionalSplatArgument(object args, int postCount) { + var list = (RubyArray)args; + return (list.Count <= postCount) ? DefaultArgument : list.TakeFirst(); + } + + /// + /// What the rest parameter takes: everything the post parameters + /// leave - nothing if a block was given too few arguments to fill even those. + /// + [Emitted] + public static RubyArray/*!*/ ShiftRestSplatArguments(object args, int postCount) { + var list = (RubyArray)args; + return list.TakeFirst(Math.Max(0, list.Count - postCount)); + } + + /// + /// The **rest parameter: a new hash with whatever the call passed beyond the declared + /// . It is a copy, so the method may change it freely. + /// + [Emitted] + public static Hash/*!*/ MakeKeywordRest(object keywords, RubySymbol/*!*/[]/*!*/ names) { + return ((Hash)keywords).CopyWithoutKeywords(names); + } + /// /// The argument a parameter of a method that declares no keyword parameters is about to /// be given. `f(a: 1)` there is a call that passes a positional Hash - Ruby 3 has no @@ -2023,6 +2298,57 @@ public static object ReadGlobalVariable(RubyScope/*!*/ scope, string/*!*/ name) return value; } + /// + /// GetGlobalVariable through the site's cached variable: an ordinary global is one + /// version check and a field read, with no lookup and no lock. + /// + [Emitted] + public static object GetCachedGlobalVariable(RubyScope/*!*/ scope, GlobalVariableSite/*!*/ site) { + var context = scope.RubyContext; + var variable = site.GetVariable(context); + var plain = variable as GlobalVariableInfo; + if (plain != null) { + return plain.Value; + } + return (variable != null) ? context.GetGlobalVariableValue(variable, scope) : null; + } + + /// ReadGlobalVariable through the site's cached variable. + [Emitted] + public static object ReadCachedGlobalVariable(RubyScope/*!*/ scope, GlobalVariableSite/*!*/ site) { + var context = scope.RubyContext; + var variable = site.GetVariable(context); + var plain = variable as GlobalVariableInfo; + if (plain != null) { + return plain.Value; + } + if (variable != null) { + return context.GetGlobalVariableValue(variable, scope); + } + if (context.Verbose is bool && (bool)context.Verbose) { + context.ReportWarning(String.Format("global variable '${0}' not initialized", site.Name), true); + } + return null; + } + + /// + /// SetGlobalVariable through the site's cached variable. An ordinary global is assigned + /// in place; everything else - a new global, a special or read-only one - takes the + /// general path, which validates and creates. + /// + [Emitted] + public static object SetCachedGlobalVariable(object value, RubyScope/*!*/ scope, GlobalVariableSite/*!*/ site) { + var context = scope.RubyContext; + var plain = site.GetVariable(context) as GlobalVariableInfo; + if (plain != null) { + plain.Assign(value); + context.GlobalVariableAssigned(scope, site.Name, value); + } else { + context.SetGlobalVariable(scope, site.Name, value); + } + return value; + } + [Emitted] public static bool IsDefinedGlobalVariable(RubyScope/*!*/ scope, string/*!*/ name) { GlobalVariable variable; @@ -2249,14 +2575,26 @@ public static object MatchString(MutableString str, RubyRegex/*!*/ regex, RubySc return result; } + // A literal without substitutions is built once and cached in its site's box, so every + // later evaluation only reads the box. The construction is split off: the delegate it + // makes captures the parameters, and a closure over parameters is allocated on entry to + // the method that declares them - on every evaluation, even one that never needs it. [Emitted] public static RubyRegex/*!*/ CreateRegexB(byte[]/*!*/ bytes, RubyEncoding/*!*/ encoding, RubyRegexOptions options, StrongBox regexpCache) { + return regexpCache.Value ?? CreateRegexBWorker(bytes, encoding, options, regexpCache); + } + + private static RubyRegex/*!*/ CreateRegexBWorker(byte[]/*!*/ bytes, RubyEncoding/*!*/ encoding, RubyRegexOptions options, StrongBox regexpCache) { Func createRegex = delegate { return new RubyRegex(CreateMutableStringB(bytes, encoding), options); }; return CreateRegexWorker(options, regexpCache, true, createRegex); } [Emitted] public static RubyRegex/*!*/ CreateRegexL(string/*!*/ str1, RubyEncoding/*!*/ encoding, RubyRegexOptions options, StrongBox regexpCache) { + return regexpCache.Value ?? CreateRegexLWorker(str1, encoding, options, regexpCache); + } + + private static RubyRegex/*!*/ CreateRegexLWorker(string/*!*/ str1, RubyEncoding/*!*/ encoding, RubyRegexOptions options, StrongBox regexpCache) { Func createRegex = delegate { return new RubyRegex(CreateMutableStringL(str1, encoding), options); }; return CreateRegexWorker(options, regexpCache, true, createRegex); } @@ -2418,17 +2756,42 @@ public int GetHashCode(MutableString str) { [Emitted] public static MutableString/*!*/ CreateMutableStringMM(MutableString str1, MutableString str2, RubyEncoding/*!*/ encoding) { - return MutableString.CreateInternal(str1, encoding).Append(str2); + var result = MutableString.CreateInternal(str1, encoding); + AppendInterpolatedPart(result, str2); + return result; + } + + /// + /// MRI's rb_str_concat_literals: append, and a result that is still US-ASCII takes the encoding + /// of the first part that is not - "#{1}a" in a UTF-8 file is UTF-8, "#{y}#{bin}" is binary. + /// + private static void AppendInterpolatedPart(MutableString/*!*/ result, MutableString part) { + if (part != null) { + result.Append(part); + if (part.Encoding != RubyEncoding.Ascii && result.Encoding == RubyEncoding.Ascii) { + result.ForceEncoding(part.Encoding); + } + } } // TODO: we should emit Append calls directly, and not create an array first [Emitted] public static MutableString/*!*/ CreateMutableStringN(MutableString/*!*/[]/*!*/ parts) { Debug.Assert(parts.Length > 0); - var result = MutableString.CreateMutable(RubyEncoding.Ascii); + // room for all of it up front rather than a regrowth per part (a part counts its + // characters or its bytes, whichever it holds, which is what a capacity hint needs) + int capacity = 0; + for (int i = 0; i < parts.Length; i++) { + if (parts[i] != null) { + capacity += parts[i].Length; + } + } + // MRI's rb_str_concat_literals starts from the first part's encoding: "a#{1}b" in a UTF-8 + // file is UTF-8, though every part is ASCII and the Integer's is US-ASCII + var result = MutableString.CreateMutable(capacity, (parts[0] != null) ? parts[0].Encoding : RubyEncoding.Ascii); for (int i = 0; i < parts.Length; i++) { - result.Append(parts[i]); + AppendInterpolatedPart(result, parts[i]); } return result; @@ -2519,7 +2882,11 @@ public static object NullIfTrue(object obj) { /// [Emitted] public static bool FilterBlockException(RubyScope/*!*/ scope, Exception/*!*/ exception) { - RubyExceptionData.GetInstance(exception).CaptureExceptionTrace(scope); + // Flow control (throw/catch, retry, Thread#kill) is never seen by Ruby code, so it needs no + // backtrace. One that escaped anyway would still get one at the top level. + if (!(exception is StackUnwinder)) { + RubyExceptionData.GetInstance(exception).CaptureExceptionTrace(scope); + } return false; } @@ -2589,7 +2956,10 @@ public static bool IsTopLevelReturn(RubyScope/*!*/ scope, Exception/*!*/ excepti public static bool IsMethodUnwinderTargetFrame(RubyScope/*!*/ scope, Exception/*!*/ exception) { var unwinder = exception as MethodUnwinder; if (unwinder == null) { - RubyExceptionData.GetInstance(exception).CaptureExceptionTrace(scope); + // see FilterBlockException + if (!(exception is StackUnwinder)) { + RubyExceptionData.GetInstance(exception).CaptureExceptionTrace(scope); + } return false; } else { return unwinder.TargetFrame == scope.FlowControlScope; @@ -2603,7 +2973,10 @@ public static object GetMethodUnwinderReturnValue(Exception/*!*/ exception) { [Emitted] public static void LeaveMethodFrame(RuntimeFlowControl/*!*/ rfc) { - rfc.LeaveMethod(); + // no write to a frame that is not active: CreateRfcForMethod's shared inactive frame stays untouched + if (rfc._activeFlowControlScope != null) { + rfc.LeaveMethod(); + } } /// diff --git a/Src/Ruby/Runtime/RubyScope.cs b/Src/Ruby/Runtime/RubyScope.cs index 38d200077..bf917e0d8 100644 --- a/Src/Ruby/Runtime/RubyScope.cs +++ b/Src/Ruby/Runtime/RubyScope.cs @@ -91,9 +91,19 @@ public abstract class RubyScope : RuntimeFlowControl { internal bool InLoop; internal bool InRescue; + // Set once something may hold on to this scope after the call it belongs to has returned: a + // Proc, Binding, method or nested scope made in it, a TracePoint event. A block scope that is + // not captured is reused for the next call of its block (see RubyOps.TakeReusableBlockScope). + internal bool IsCaptured; + + // Libraries call this for a scope they keep after returning. + public void MarkCaptured() { + if (!IsCaptured) { + IsCaptured = true; + } + } + // closure: - private Dictionary _staticLocalMapping; - private Dictionary _dynamicLocals; internal /*and protected*/ MutableTuple _locals; // null if there are no variables internal /*and protected*/ string/*!*/[] _variableNames; // empty if there are no variables @@ -108,15 +118,57 @@ public abstract class RubyScope : RuntimeFlowControl { // set by private/public/protected/module_function internal /*and protected*/ RubyMethodAttributes _methodAttributes; - // Refinements activated by `using' in this scope, in activation order; null until `using' runs here. - private List _usedModules; - // Parallel to _usedModules: what each `using' saw of the module's refinements (null = all, live). - private List> _usedModuleSnapshots; + /// + /// The state only eval, Binding and refinements need, out of line: a scope is allocated for + /// every method call and every block call that needs one, and each field here would be paid + /// for by all of them. Null until one of these is first set. + /// + internal sealed class RareState { + // closure: name -> slot of the compiled locals, built for eval and Binding on demand + public Dictionary StaticLocalMapping; + // closure: locals an eval or Binding#local_variable_set introduced + public Dictionary DynamicLocals; + + // Refinements activated by `using' in this scope, in activation order; null until `using' runs here. + public List UsedModules; + // Parallel to UsedModules: what each `using' saw of the module's refinements (null = all, live). + public List> UsedModuleSnapshots; + + // Memoized effective activation table for this scope (own + everything lexically enclosing), + // stamped with RubyContext.RefinementVersion so that a `using' anywhere forces a recompute. + public RefinementActivation Refinements; + public int RefinementsVersion; + + // see RubyMethodScope.DefinitionRefinements + public StrongBox MethodDefinitionRefinements; + } - // Memoized effective activation table for this scope (own + everything lexically enclosing), - // stamped with RubyContext.RefinementVersion so that a `using' anywhere forces a recompute. - private RefinementActivation _refinements; - private int _refinementsVersion; + private RareState _rare; + + internal RareState/*!*/ Rare { + get { return _rare ?? Interlocked.CompareExchange(ref _rare, new RareState(), null) ?? _rare; } + } + + internal RareState RareOrNull { + get { return _rare; } + } + + private Dictionary StaticLocalMapping { + get { var r = _rare; return (r != null) ? r.StaticLocalMapping : null; } + set { Rare.StaticLocalMapping = value; } + } + + private Dictionary DynamicLocals { + get { var r = _rare; return (r != null) ? r.DynamicLocals : null; } + } + + private List UsedModules { + get { var r = _rare; return (r != null) ? r.UsedModules : null; } + } + + private List> UsedModuleSnapshots { + get { var r = _rare; return (r != null) ? r.UsedModuleSnapshots : null; } + } internal InterpretedFrame InterpretedFrame { get; set; } @@ -187,6 +239,19 @@ internal RubyScope() { // Perf note: Initialization is performed completely in the most derived scope classes to improve perf. } + // The per-call state of a scope reused for another call of its block, as a new scope has it. + internal void ResetForReuse(MutableTuple locals, object selfObject, InterpretedFrame interpretedFrame) { + _activeFlowControlScope = _parent.FlowControlScope; + InLoop = false; + InRescue = false; + _locals = locals; + _selfObject = selfObject; + _selfImmediateClass = null; + _methodAttributes = RubyMethodAttributes.PublicInstance; + _rare = null; + InterpretedFrame = interpretedFrame; + } + public virtual RubyModule Module { get { return null; } } @@ -270,7 +335,7 @@ internal void SetEmptyLocals() { } private void EnsureBoxes() { - if (_staticLocalMapping == null) { + if (StaticLocalMapping == null) { if (_variableNames == null) { // Locals not installed yet (see GetDeclaredLocalVariables). Don't cache an // empty mapping - it would permanently hide the real locals once SetLocals runs. @@ -281,7 +346,7 @@ private void EnsureBoxes() { for (int i = 0; i < count; i++) { boxes[_variableNames[i]] = i; } - _staticLocalMapping = boxes; + StaticLocalMapping = boxes; } } @@ -297,7 +362,7 @@ public object GetImplicitParameterValue(string/*!*/ name) { EnsureBoxes(); int index; - if (_staticLocalMapping != null && _staticLocalMapping.TryGetValue(name, out index)) { + if (StaticLocalMapping != null && StaticLocalMapping.TryGetValue(name, out index)) { return _locals.GetValue(index); } return null; @@ -307,19 +372,19 @@ private bool TryGetLocal(string/*!*/ name, out object value) { EnsureBoxes(); int index; - if (_staticLocalMapping != null && _staticLocalMapping.TryGetValue(name, out index) && !IsImplicitParameter(name)) { + if (StaticLocalMapping != null && StaticLocalMapping.TryGetValue(name, out index) && !IsImplicitParameter(name)) { Debug.Assert(_locals != null); value = _locals.GetValue(index); return true; } - if (_dynamicLocals == null) { + if (DynamicLocals == null) { value = null; return false; } - lock (_dynamicLocals) { - return _dynamicLocals.TryGetValue(name, out value); + lock (DynamicLocals) { + return DynamicLocals.TryGetValue(name, out value); } } @@ -327,22 +392,22 @@ private bool TrySetLocal(string/*!*/ name, object value) { EnsureBoxes(); int index; - if (_staticLocalMapping != null && _staticLocalMapping.TryGetValue(name, out index) && !IsImplicitParameter(name)) { + if (StaticLocalMapping != null && StaticLocalMapping.TryGetValue(name, out index) && !IsImplicitParameter(name)) { Debug.Assert(_locals != null); _locals.SetValue(index, value); return true; } - if (_dynamicLocals == null) { + if (DynamicLocals == null) { return false; } - lock (_dynamicLocals) { - if (!_dynamicLocals.ContainsKey(name)) { + lock (DynamicLocals) { + if (!DynamicLocals.ContainsKey(name)) { return false; } - _dynamicLocals[name] = value; + DynamicLocals[name] = value; } return true; } @@ -357,9 +422,9 @@ private bool TrySetLocal(string/*!*/ name, object value) { } } - if (_dynamicLocals != null) { - lock (_dynamicLocals) { - foreach (var entry in _dynamicLocals) { + if (DynamicLocals != null) { + lock (DynamicLocals) { + foreach (var entry in DynamicLocals) { yield return entry; } } @@ -369,9 +434,9 @@ private bool TrySetLocal(string/*!*/ name, object value) { private IEnumerable/*!*/ GetDeclaredLocalSymbols() { // a variable an eval or Binding#local_variable_set introduced is newer than the ones // the scope was compiled with, and MRI lists it first - if (_dynamicLocals != null) { - lock (_dynamicLocals) { - foreach (string name in _dynamicLocals.Keys) { + if (DynamicLocals != null) { + lock (DynamicLocals) { + foreach (string name in DynamicLocals.Keys) { yield return name; } } @@ -478,12 +543,12 @@ internal object ResolveAndSetLocalVariable(string/*!*/ name, object value) { /// module-eval scopes need to override this since they should set the variable in their parent scope. /// internal virtual void DefineDynamicVariable(string/*!*/ name, object value) { - if (_dynamicLocals == null) { - Interlocked.CompareExchange(ref _dynamicLocals, new Dictionary(), null); + if (DynamicLocals == null) { + Interlocked.CompareExchange(ref Rare.DynamicLocals, new Dictionary(), null); } - lock (_dynamicLocals) { - _dynamicLocals[name] = value; + lock (DynamicLocals) { + DynamicLocals[name] = value; } } @@ -514,19 +579,20 @@ public void ActivateRefinements(RubyModule/*!*/ module, bool snapshot) { refinements = new HashSet(list); } - if (_usedModules == null) { - _usedModules = new List(); - _usedModuleSnapshots = new List>(); + if (UsedModules == null) { + var rare = Rare; + rare.UsedModules = new List(); + rare.UsedModuleSnapshots = new List>(); } else { // re-activating moves it to the front of this scope's precedence order - int index = _usedModules.IndexOf(module); + int index = UsedModules.IndexOf(module); if (index >= 0) { - _usedModules.RemoveAt(index); - _usedModuleSnapshots.RemoveAt(index); + UsedModules.RemoveAt(index); + UsedModuleSnapshots.RemoveAt(index); } } - _usedModules.Add(module); - _usedModuleSnapshots.Add(refinements); + UsedModules.Add(module); + UsedModuleSnapshots.Add(refinements); // Invalidate every memoized table and, through the rule guards built on them, every cached // call site that could be affected. @@ -546,8 +612,9 @@ public void ActivateRefinements(RubyModule/*!*/ module, bool snapshot) { } int version = context.RefinementVersion; - if (_refinements != null && _refinementsVersion == version) { - return _refinements; + var cached = _rare; + if (cached != null && cached.Refinements != null && cached.RefinementsVersion == version) { + return cached.Refinements; } RefinementActivation outer; @@ -572,10 +639,11 @@ public void ActivateRefinements(RubyModule/*!*/ module, bool snapshot) { } else { outer = (_parent != null) ? _parent.GetActiveRefinements() : RefinementActivation.Empty; } - RefinementActivation result = (_usedModules != null) ? RefinementActivation.Create(outer, _usedModules, _usedModuleSnapshots) : outer; + RefinementActivation result = (UsedModules != null) ? RefinementActivation.Create(outer, UsedModules, UsedModuleSnapshots) : outer; - _refinements = result; - _refinementsVersion = version; + var rare = Rare; + rare.Refinements = result; + rare.RefinementsVersion = version; return result; } @@ -1097,7 +1165,10 @@ public Proc BlockParameter { /// it creates this scope; null for a body compiled before anything was refined. Public because /// the method body assigns it directly. /// - public StrongBox DefinitionRefinements; + public StrongBox DefinitionRefinements { + get { var r = RareOrNull; return (r != null) ? r.MethodDefinitionRefinements : null; } + set { Rare.MethodDefinitionRefinements = value; } + } internal RubyMethodScope(MutableTuple locals, string/*!*/[]/*!*/ variableNames, int visibleParameterCountAndSignatureFlags, RubyScope/*!*/ parent, RubyModule/*!*/ declaringModule, string/*!*/ definitionName, @@ -1156,6 +1227,7 @@ public sealed class RubyModuleScope : RubyClosureScope { internal RubyModuleScope(RubyScope/*!*/ parent, RubyModule/*!*/ module) { Assert.NotNull(parent, module); + parent.MarkCaptured(); // RuntimeFlowControl: _activeFlowControlScope = parent.FlowControlScope; @@ -1211,6 +1283,7 @@ internal void SetLexicalConstantModule(RubyModule module) { internal RubyModuleEvalScope(RubyScope/*!*/ parent, RubyModule module, object selfObject) { Assert.NotNull(parent); + parent.MarkCaptured(); // RuntimeFlowControl: _activeFlowControlScope = parent.FlowControlScope; @@ -1238,6 +1311,8 @@ public sealed class RubyFileInitializerScope : RubyClosureScope { public override bool InheritsLocalVariables { get { return false; } } internal RubyFileInitializerScope(MutableTuple locals, string[]/*!*/ variableNames, RubyScope/*!*/ parent) { + parent.MarkCaptured(); + // RuntimeFlowControl: _activeFlowControlScope = parent.FlowControlScope; @@ -1258,7 +1333,7 @@ internal override void DefineDynamicVariable(string/*!*/ name, object value) { } public sealed class RubyBlockScope : RubyClosureScope { - private readonly BlockParam/*!*/ _blockFlowControl; + private BlockParam/*!*/ _blockFlowControl; // The block a thread runs keeps its own $~ and $_, as MRI's thread root frame does; any // other block shares them with the method it is in. @@ -1318,6 +1393,15 @@ internal RubyBlockScope(MutableTuple locals, string/*!*/[]/*!*/ variableNames, // RubyBlockScope: _blockFlowControl = blockFlowControl; } + + // Makes this scope, which nothing has captured, the scope of another call of its block. + internal void Reinitialize(MutableTuple locals, BlockParam/*!*/ blockFlowControl, object selfObject, InterpretedFrame interpretedFrame) { + _blockFlowControl = blockFlowControl; + ResetForReuse(locals, selfObject, interpretedFrame); + CurrentMatch = null; + LastInputLine = null; + LambdaReturnState = 0; + } } /// @@ -1334,6 +1418,8 @@ public sealed class RubyBindingCopyScope : RubyScope { // No module of its own: lexical walks (Module.nesting, constant lookup) pass through to the parent. public RubyBindingCopyScope(RubyScope/*!*/ parent, object selfObject) { + parent.MarkCaptured(); + // RuntimeFlowControl: _activeFlowControlScope = parent.FlowControlScope; @@ -1445,10 +1531,11 @@ internal RubyTopLevelScope(RubyGlobalScope/*!*/ globalScope, RubyModule scopeMod if (context.RubyOptions.RequirePaths != null) { bool first = true; foreach (var path in context.RubyOptions.RequirePaths) { - context.Loader.LoadFile(globalScope, rubyGlobalScope.MainObject, MutableString.Create(path, RubyEncoding.UTF8), LoadFlags.Require); // the first is the core prelude (gem_prelude.rb or ruby4.rb) if (first && (path == "gem_prelude.rb" || path == "ruby4.rb")) { - context.Loader.ProvideCoreFeatures(); + context.Loader.LoadCorePrelude(globalScope, rubyGlobalScope.MainObject, path); + } else { + context.Loader.LoadFile(globalScope, rubyGlobalScope.MainObject, MutableString.Create(path, RubyEncoding.UTF8), LoadFlags.Require); } first = false; } diff --git a/Src/Ruby/Runtime/RubyStackTraceBuilder.cs b/Src/Ruby/Runtime/RubyStackTraceBuilder.cs index fb67f6d44..38883ad12 100644 --- a/Src/Ruby/Runtime/RubyStackTraceBuilder.cs +++ b/Src/Ruby/Runtime/RubyStackTraceBuilder.cs @@ -46,21 +46,34 @@ public sealed class RubyStackTraceBuilder { // core method written in Ruby is that method's own, not its caller's. private bool _keepInternalFrames; - private RubyStackTraceBuilder(RubyContext/*!*/ context) { + private RubyStackTraceBuilder(RubyContext/*!*/ context) + : this(context, context.GetPathEncoding()) { + } + + private RubyStackTraceBuilder(RubyContext/*!*/ context, RubyEncoding/*!*/ encoding) { _context = context; _needsClrFileInfo = NeedsClrFileInfo(context); _hasFileAccessPermission = _needsClrFileInfo && DetectFileAccessPermissions(); _exceptionDetail = context.Options.ExceptionDetail; - _encoding = context.GetPathEncoding(); + _encoding = encoding; _trace = new RubyArray(); } - - internal RubyStackTraceBuilder(RubyContext/*!*/ context, Exception/*!*/ exception, StackTrace catchSiteTrace, bool isCatchSiteInterpreted) + + internal RubyStackTraceBuilder(RubyContext/*!*/ context, Exception/*!*/ exception, StackTrace catchSiteTrace, bool isCatchSiteInterpreted) : this(context) { // Compiled trace: contains frames starting with the throw site up to the first filter/catch that the exception was caught by: StackTrace throwSiteTrace = GetClrStackTrace(exception, _needsClrFileInfo); _interpretedFrames = InterpretedFrame.GetExceptionStackTrace(exception); + AddExceptionBacktrace(throwSiteTrace, catchSiteTrace, isCatchSiteInterpreted); + } + + private RubyStackTraceBuilder(CapturedTrace/*!*/ captured) + : this(captured.Context, captured.Encoding) { + _interpretedFrames = captured.InterpretedFrames; + AddExceptionBacktrace(captured.ThrowSiteTrace, captured.CatchSiteTrace, captured.IsCatchSiteInterpreted); + } + private void AddExceptionBacktrace(StackTrace/*!*/ throwSiteTrace, StackTrace catchSiteTrace, bool isCatchSiteInterpreted) { AddBacktrace(throwSiteTrace.GetFrames(), 0, false); // Compiled trace: contains frames above and including the first Ruby filter/catch site that the exception was caught by: @@ -71,6 +84,39 @@ internal RubyStackTraceBuilder(RubyContext/*!*/ context, Exception/*!*/ exceptio } } + /// + /// What the backtrace of a raised exception is made of, taken where the first Ruby frame saw + /// the exception go by - the stack walks, and the interpreted frames the interpreter saved on + /// the exception - but not yet turned into strings. Most exceptions are rescued and dropped + /// without anyone asking for their backtrace, and formatting it (resolving every frame's + /// method, parsing the encoded names, building a string per frame) is most of what a raise + /// used to cost. does it on first use and yields exactly what building it + /// eagerly yielded: everything it reads is either captured here or fixed for the context. + /// + internal sealed class CapturedTrace { + internal readonly RubyContext/*!*/ Context; + internal readonly RubyEncoding/*!*/ Encoding; + internal readonly StackTrace/*!*/ ThrowSiteTrace; + internal readonly StackTrace CatchSiteTrace; + internal readonly IList InterpretedFrames; + internal readonly bool IsCatchSiteInterpreted; + + internal CapturedTrace(RubyContext/*!*/ context, Exception/*!*/ exception, StackTrace catchSiteTrace, bool isCatchSiteInterpreted) { + Context = context; + Encoding = context.GetPathEncoding(); + // the exception's own trace grows as it is rethrown, and a raise of it starts it over: + ThrowSiteTrace = GetClrStackTrace(exception, NeedsClrFileInfo(context)); + // the interpreter's frames are saved on the exception once, and raise clears them: + InterpretedFrames = InterpretedFrame.GetExceptionStackTrace(exception); + CatchSiteTrace = catchSiteTrace; + IsCatchSiteInterpreted = isCatchSiteInterpreted; + } + + internal RubyArray/*!*/ Build() { + return new RubyStackTraceBuilder(this).RubyTrace; + } + } + internal RubyStackTraceBuilder(RubyContext/*!*/ context, int skipFrames) : this(context, skipFrames, false) { } @@ -216,6 +262,37 @@ internal static StackTrace GetClrStackTrace(Exception exception, bool needFileIn return exception != null ? new StackTrace(exception, needFileInfo) : new StackTrace(needFileInfo); } + /// + /// Whether a catch-site walk taken in an interpreted frame (see RubyExceptionData.CaptureExceptionTrace) + /// can stand for the walk of any other exception the frame catches: whether it shows the frame + /// once. While the interpreter runs a frame's finally blocks on the way out of an exception it + /// re-enters the frame's Run method from HandleException, and the walk then has two runs of it. + /// + internal static bool IsReusableCatchSiteTrace(StackTrace/*!*/ trace) { + var frames = trace.GetFrames(); + int i = 0; + while (i < frames.Length && !IsInterpreterRunFrame(frames[i])) { + i++; + } + if (i == frames.Length) { + return false; + } + var interpreterType = frames[i].GetMethod().DeclaringType; + while (i < frames.Length && IsInterpreterRunFrame(frames[i])) { + i++; + } + if (i == frames.Length) { + return false; + } + var next = frames[i].GetMethod(); + return next != null && !(next.DeclaringType == interpreterType && next.Name == "HandleException"); + } + + private static bool IsInterpreterRunFrame(StackFrame/*!*/ frame) { + var method = frame.GetMethod(); + return method != null && InterpretedFrame.IsInterpretedFrame(method); + } + [MethodImpl(MethodImplOptions.NoInlining)] // CF private static string GetFileName(StackFrame/*!*/ frame) { return frame.GetFileName(); @@ -484,6 +561,14 @@ private static bool IsVisibleClrFrame(MethodBase/*!*/ method) { return false; } + // The entry of a JIT-specialized method (JitStub.BuildEntry, BuildDirectEntry): its fast + // path raises nothing, and its slow path calls the method's generic body, which is the + // frame that stands for the method. + if (method.DeclaringType == null && (method.Name.StartsWith(Jit.JitStub.EntryNamePrefix, StringComparison.Ordinal) || + method.Name.StartsWith(Jit.JitStub.DirectEntryNamePrefix, StringComparison.Ordinal))) { + return false; + } + Type type = method.DeclaringType; if (type != null) { if (type.Assembly == typeof(RubyOps).Assembly || type.Namespace != null && diff --git a/Src/Ruby/Runtime/RubyUtils.cs b/Src/Ruby/Runtime/RubyUtils.cs index 468a724d1..da89b3da1 100644 --- a/Src/Ruby/Runtime/RubyUtils.cs +++ b/Src/Ruby/Runtime/RubyUtils.cs @@ -808,6 +808,13 @@ public static bool TryGetCallerSourceLocation(RubyContext/*!*/ context, out stri sourcePath = null; sourceLine = 0; + // Nothing but the core prelude is running, and its frames are not in a backtrace: the + // walk below would come back empty, after ~1ms, for each of the prelude's const_set calls + // (Errno and Encoding make hundreds). + if (context.Loader.IsLoadingCorePrelude) { + return false; + } + RubyArray trace; try { trace = RubyExceptionData.CreateBacktrace(context, 0); @@ -1316,6 +1323,9 @@ public static object Evaluate(MutableString/*!*/ code, RubyScope/*!*/ targetScop RubyModule lexicalConstantModule, MutableString file, int line) { Assert.NotNull(code, targetScope); + // the code can make a Proc, Binding or method over the scope (a block scope is then not reused) + targetScope.MarkCaptured(); + RubyContext context = targetScope.RubyContext; RubyMethodScope methodScope = targetScope.GetInnerMostMethodScope(); diff --git a/Src/StdLib/ironruby/ruby4.rb b/Src/StdLib/ironruby/ruby4.rb index 40ab1c477..ffb17f430 100644 --- a/Src/StdLib/ironruby/ruby4.rb +++ b/Src/StdLib/ironruby/ruby4.rb @@ -91,7 +91,7 @@ def yield_self(&block) return to_enum(:yield_self) { 1 } unless block yield self end unless method_defined?(:yield_self) - alias_method :then, :yield_self unless method_defined?(:then) + alias :then :yield_self unless method_defined?(:then) # Written in Ruby, as MRI's is (): its frame is reported with its caller's # location, and it has a source_location. @@ -298,7 +298,7 @@ def sum(init = 0) compensating ? acc + compensation : acc end unless method_defined?(:sum) - alias_method :filter, :select unless method_defined?(:filter) + alias :filter :select unless method_defined?(:filter) # A `yield` with several values is one element, packed into an Array; a yield # with none is nil. Everything below goes through this so that @@ -321,7 +321,7 @@ def each_entry(*args, &block) end unless method_defined?(:each_entry) unless method_defined?(:each_with_index_without_args) - alias_method :each_with_index_without_args, :each_with_index + alias :each_with_index_without_args :each_with_index def each_with_index(*args, &block) return to_enum(:each_with_index, *args) { size if respond_to?(:size) } unless block @@ -339,10 +339,10 @@ def each_with_index(*args, &block) # The built-ins only know the block form, and because they are defined on # Enumerable they shadow rather than extend, so wrap them. unless method_defined?(:all_without_pattern?) - alias_method :all_without_pattern?, :all? - alias_method :any_without_pattern?, :any? - alias_method :none_without_pattern?, :none? - alias_method :one_without_pattern?, :one? + alias :all_without_pattern? :all? + alias :any_without_pattern? :any? + alias :none_without_pattern? :none? + alias :one_without_pattern? :one? def __check_pattern_args__(args, block = nil) if args.size > 1 @@ -398,7 +398,7 @@ def one?(*args, &block) # inspecting the object rather than by naming its class - and takes at most # two arguments. A block alongside a method name is never called. unless method_defined?(:inject_without_name_check) - alias_method :inject_without_name_check, :inject + alias :inject_without_name_check :inject def inject(*args, &block) if args.size > 2 @@ -421,13 +421,13 @@ def inject(*args, &block) end inject_without_name_check(*args, &block) end - alias_method :reduce, :inject + alias :reduce :inject end # min/max also grew an `n` form. unless method_defined?(:min_without_count) - alias_method :min_without_count, :min - alias_method :max_without_count, :max + alias :min_without_count :min + alias :max_without_count :max def min(*args, &block) n = args.empty? ? nil : __count_arg__(args[0]) @@ -549,7 +549,7 @@ def flat_map end result end unless method_defined?(:flat_map) - alias_method :collect_concat, :flat_map unless method_defined?(:collect_concat) + alias :collect_concat :flat_map unless method_defined?(:collect_concat) def uniq seen = {} @@ -723,7 +723,7 @@ def chunk(&block) # #sort_by has to walk the receiver exactly once - the block may be expensive # or have side effects - which the decorate/sort/undecorate helper does. unless method_defined?(:sort_by_without_enumerator) - alias_method :sort_by_without_enumerator, :sort_by + alias :sort_by_without_enumerator :sort_by def sort_by(&block) return to_enum(:sort_by) { size if respond_to?(:size) } unless block @@ -734,8 +734,8 @@ def sort_by(&block) # Neither of these can know how long the answer will be without running the # block, so their Enumerators report no size even when the receiver has one. unless method_defined?(:take_while_without_enumerator) - alias_method :take_while_without_enumerator, :take_while - alias_method :drop_while_without_enumerator, :drop_while + alias :take_while_without_enumerator :take_while + alias :drop_while_without_enumerator :drop_while def take_while(&block) return to_enum(:take_while) { nil } unless block @@ -751,8 +751,8 @@ def drop_while(&block) # Both return the receiver when they are given a block (they returned nil), # and both reject a slice size that cannot produce any slice at all. unless method_defined?(:each_cons_without_self) - alias_method :each_cons_without_self, :each_cons - alias_method :each_slice_without_self, :each_slice + alias :each_cons_without_self :each_cons + alias :each_slice_without_self :each_slice def __slice_size__(n, message) size = n @@ -782,7 +782,7 @@ def each_slice(n, &block) # otherwise iterated with #each - a Range or a lazy Enumerator zips fine. # Only something that can do neither is an error. unless method_defined?(:zip_without_conversion) - alias_method :zip_without_conversion, :zip + alias :zip_without_conversion :zip def __zip_source__(other) return other if other.is_a?(Array) @@ -894,8 +894,8 @@ def __reverse_each_size__ # are defined on Range they hide Enumerable#first(n) rather than falling # through to it - so `(0..Float::INFINITY).first(3)` was an ArgumentError. unless method_defined?(:first_without_count) - alias_method :first_without_count, :first - alias_method :last_without_count, :last + alias :first_without_count :first + alias :last_without_count :last def first(*args) if args.empty? @@ -1323,10 +1323,10 @@ def __array_repeat__(count, sorted, &block) public - alias_method :filter, :select unless method_defined?(:filter) - alias_method :filter!, :select! if method_defined?(:select!) && !method_defined?(:filter!) - alias_method :append, :push unless method_defined?(:append) - alias_method :prepend, :unshift unless method_defined?(:prepend) + alias :filter :select unless method_defined?(:filter) + alias :filter! :select! if method_defined?(:select!) && !method_defined?(:filter!) + alias :append :push unless method_defined?(:append) + alias :prepend :unshift unless method_defined?(:prepend) end class Hash @@ -1432,7 +1432,7 @@ def compact # method_defined? is true and Hash would be left returning the wrong class. end unless instance_methods(false).include?(:compact) - alias_method :filter, :select unless method_defined?(:filter) + alias :filter :select unless method_defined?(:filter) end class String @@ -1506,7 +1506,7 @@ module Math class DomainError < StandardError; end unless const_defined?(:DomainError) class << self - alias_method :__ir_frexp__, :frexp + alias :__ir_frexp__ :frexp # frexp answered the magnitude for a negative argument - Math.frexp(-0.5) # came back [0.5, 0] where MRI says [-0.5, 0] - and 0.0 came back with the @@ -1653,7 +1653,7 @@ class IO # Keyword arguments and a Hash in the last positional slot are different things, # and only a Ruby-level signature can tell them apart: the library method behind # this one takes options as an ordinary parameter and so sees them as the same. - alias_method :__ir_initialize__, :initialize + alias :__ir_initialize__ :initialize private :__ir_initialize__ def initialize(*args, **opts) @@ -1665,7 +1665,7 @@ def initialize(*args, **opts) end class << self - alias_method :__ir_new__, :new + alias :__ir_new__ :new # IO.open is the form that takes a block; IO.new ignores one and says so. def new(*args, **opts, &block) @@ -1678,7 +1678,7 @@ def new(*args, **opts, &block) end class File - alias_method :__ir_initialize__, :initialize + alias :__ir_initialize__ :initialize private :__ir_initialize__ # File takes a permission argument between the mode and the options. @@ -1819,7 +1819,7 @@ def self.binread(name, length = nil, offset = 0) # they return :wait_readable / :wait_writable / nil. net/protocol drives its # read buffer through this form. unless method_defined?(:__read_nonblock_raising__) - alias_method :__read_nonblock_raising__, :read_nonblock + alias :__read_nonblock_raising__ :read_nonblock def read_nonblock(len, buf = nil, exception: true) unless exception == true || exception == false @@ -1846,7 +1846,7 @@ def read_nonblock(len, buf = nil, exception: true) end unless method_defined?(:__write_nonblock_raising__) - alias_method :__write_nonblock_raising__, :write_nonblock + alias :__write_nonblock_raising__ :write_nonblock def write_nonblock(buf, exception: true) unless exception == true || exception == false @@ -2132,7 +2132,7 @@ def world_readable? class Array # 1.9 added the count form of shift; the bundled core only has the no-arg one unless (begin; [1].shift(1); true; rescue ArgumentError; false; end) - alias_method :shift_without_count, :shift + alias :shift_without_count :shift def shift(*args) if args.size > 1 @@ -2170,7 +2170,7 @@ def shift(*args) class Module # (method_defined? and its public_/private_/protected_ forms take `inherit' natively.) unless (begin; Module.const_defined?(:Module, true); true; rescue ArgumentError; false; end) - alias_method :const_defined_without_inherit?, :const_defined? + alias :const_defined_without_inherit? :const_defined? def const_defined?(name, inherit = true) return const_defined_without_inherit?(name) if inherit @@ -2179,7 +2179,7 @@ def const_defined?(name, inherit = true) end unless (begin; Module.const_get(:Module, true); true; rescue ArgumentError; false; end) - alias_method :const_get_without_inherit, :const_get + alias :const_get_without_inherit :const_get def const_get(name, inherit = true) return const_get_without_inherit(name) if inherit @@ -2281,29 +2281,145 @@ class Encoding end end - begin - Encoding.name_list.each { |__n__| __add__.call(__n__) } - rescue StandardError - end - - # Names CRuby knows that are missing from (or spelled differently in) the - # runtime's own name list. Unsupported ones are silently skipped. - %w[ - ISO-8859-1 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 - ISO-8859-7 ISO-8859-8 ISO-8859-9 ISO-8859-10 ISO-8859-11 ISO-8859-13 - ISO-8859-14 ISO-8859-15 ISO-8859-16 - Windows-31J Windows-874 - Windows-1250 Windows-1251 Windows-1252 Windows-1253 Windows-1254 - Windows-1255 Windows-1256 Windows-1257 Windows-1258 - CP437 CP737 CP775 CP850 CP852 CP855 CP857 CP860 CP861 CP862 CP863 - CP864 CP865 CP866 CP869 CP932 CP936 CP949 CP950 CP1252 - IBM437 IBM737 IBM775 IBM852 IBM857 IBM861 IBM862 IBM866 IBM869 - KOI8-R KOI8-U GB18030 GBK Big5 EUC-JP EUC-KR EUC-CN - macRoman macCentEuro macCroatian macCyrillic macGreek macIceland - macRomania macThai macTurkish macUkraine - ASCII-8BIT BINARY US-ASCII UTF-8 UTF-16 UTF-32 UTF-16BE UTF-16LE - UTF-32BE UTF-32LE Shift_JIS SJIS - ].each { |__n__| __add__.call(__n__) } + # What those loops make of the names depends on nothing but the names the runtime + # knows and the constants Encoding starts out with, yet working it out - a few + # regexp matches, a gsub and two blocks for each of some 300 names - is a tenth of a + # second of every start. So the outcome is written down here, as name=CONSTANT in + # the order the loops set them, for the runtime this file ships with; it was taken + # from the loops themselves. A runtime that knows other names, or starts out with + # other constants, takes the loops as before - a stale table costs time, nothing more. + __known__ = %i[ + ANSI_X3_4_1968 ASCII ASCII_8BIT Big5 BIG5 BINARY CESU_8 CP50221 CP874 Emacs_Mule EMACS_MULE + EUC_JP ISO_2022_JP ISO_8859_15 ISO_8859_9 ISO2022_JP ISO8859_15 ISO8859_9 KOI8_R Shift_JIS + SHIFT_JIS TIS_620 US_ASCII UTF_16 UTF_16BE UTF_16LE UTF_32 UTF_32BE UTF_32LE UTF_7 UTF_8 + Windows_874 WINDOWS_874 Converter CompatibilityError ConverterNotFoundError + InvalidByteSequenceError UndefinedConversionError + ] + __names__ = <<~'NAMES'.split + ASCII-8BIT UTF-8 US-ASCII Windows-31J EUC-JP UTF-16 UTF-32 UTF-16BE UTF-16LE UTF-32BE UTF-32LE + CESU-8 TIS-620 Emacs-Mule GB2312 EUC-KR GB18030 ISO-2022-JP CP50221 Shift_JIS UTF8-MAC CP51932 + eucJP-ms stateless-ISO-2022-JP ISO-2022-JP-2 GB12345 EUC-TW GB1988 ISO-8859-10 ISO-8859-14 + ISO-8859-16 IBM860 IBM861 ibm880 IBM862 IBM863 GBK IBM864 IBM865 IBM866 KOI8-U IBM037 IBM869 + ibm500 macIceland ibm01140 ibm01141 ibm01142 ibm273 ibm01143 ibm01144 ibm01145 Windows-1250 + ibm01146 Windows-1251 ibm01147 macRoman Windows-1252 IBM720 ibm277 ibm01148 MacJapanese + Windows-1253 IBM437 ibm278 ibm01149 x-mac-chinesetrad Windows-1254 Windows-1255 johab + Windows-1256 x-mac-arabic Windows-1257 x-mac-hebrew Windows-1258 macGreek macCyrillic ibm00924 + ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 ibm870 ISO-8859-7 ISO-8859-8 ISO-8859-9 + macTurkish macCroatian Windows-874 cp875 ibm420 CP949 ibm423 ibm424 ibm280 ibm01047 ibm284 + ibm285 macRomania CP20932 macUkraine x-europa IBM737 x-ia5 Big5 x-cp20936 x-ia5-german + x-ia5-swedish x-ia5-norwegian KOI8-R IBM775 ISO-8859-13 ibm290 ISO-8859-15 x-chinese-cns + asmo-708 ibm297 macThai x-cp20001 ibm905 x-chinese-eten x-ebcdic-koreanextended x-cp20003 + x-cp20004 x-cp20005 CP850 ibm-thai IBM852 ibm871 macCentEuro IBM855 cp1025 x-cp20949 IBM857 + ibm00858 x-cp20261 ibm1026 x-cp20269 ISO-8859-1 UTF-7 646 ASCII ANSI_X3.4-1968 BINARY CP437 + CP737 CP775 CP857 CP860 CP861 CP862 CP863 CP864 CP865 CP866 CP869 CP874 CP878 CP932 CP936 CP720 + EUC-CN eucCN MacJapan ebcdic-cp-us CP950 CP951 CP1258 CP1252 CP1250 CP1256 CP1251 CP1253 CP1255 + CP1254 CP1257 CP65000 CP65001 IBM850 eucJP eucKR eucTW ISO2022-JP ISO2022-JP2 ISO8859-1 + ISO8859-2 ISO8859-3 ISO8859-4 ISO8859-5 ISO8859-6 ISO8859-7 ISO8859-8 ISO8859-9 ISO8859-10 + ISO8859-11 ISO8859-13 ISO8859-14 ISO8859-15 ISO8859-16 SJIS PCK euc-jp-ms csWindows31J + UTF-8-MAC UTF-8-HFS UCS-2BE UCS-4BE UCS-4LE locale external filesystem + NAMES + __table__ = <<~'TABLE'.split + Windows-31J=Windows_31J Windows-31J=WINDOWS_31J GB2312=GB2312 EUC-KR=EUC_KR GB18030=GB18030 + UTF8-MAC=UTF8_MAC CP51932=CP51932 eucJP-ms=EucJP_ms eucJP-ms=EUCJP_MS + stateless-ISO-2022-JP=Stateless_ISO_2022_JP stateless-ISO-2022-JP=STATELESS_ISO_2022_JP + ISO-2022-JP-2=ISO_2022_JP_2 GB12345=GB12345 EUC-TW=EUC_TW GB1988=GB1988 ISO-8859-10=ISO_8859_10 + ISO-8859-14=ISO_8859_14 ISO-8859-16=ISO_8859_16 IBM860=IBM860 IBM861=IBM861 ibm880=Ibm880 + ibm880=IBM880 IBM862=IBM862 IBM863=IBM863 GBK=GBK IBM864=IBM864 IBM865=IBM865 IBM866=IBM866 + KOI8-U=KOI8_U IBM037=IBM037 IBM869=IBM869 ibm500=Ibm500 ibm500=IBM500 macIceland=MacIceland + macIceland=MACICELAND ibm01140=Ibm01140 ibm01140=IBM01140 ibm01141=Ibm01141 ibm01141=IBM01141 + ibm01142=Ibm01142 ibm01142=IBM01142 ibm273=Ibm273 ibm273=IBM273 ibm01143=Ibm01143 + ibm01143=IBM01143 ibm01144=Ibm01144 ibm01144=IBM01144 ibm01145=Ibm01145 ibm01145=IBM01145 + Windows-1250=Windows_1250 Windows-1250=WINDOWS_1250 ibm01146=Ibm01146 ibm01146=IBM01146 + Windows-1251=Windows_1251 Windows-1251=WINDOWS_1251 ibm01147=Ibm01147 ibm01147=IBM01147 + macRoman=MacRoman macRoman=MACROMAN Windows-1252=Windows_1252 Windows-1252=WINDOWS_1252 + IBM720=IBM720 ibm277=Ibm277 ibm277=IBM277 ibm01148=Ibm01148 ibm01148=IBM01148 + MacJapanese=MacJapanese MacJapanese=MACJAPANESE Windows-1253=Windows_1253 + Windows-1253=WINDOWS_1253 IBM437=IBM437 ibm278=Ibm278 ibm278=IBM278 ibm01149=Ibm01149 + ibm01149=IBM01149 x-mac-chinesetrad=X_mac_chinesetrad x-mac-chinesetrad=X_MAC_CHINESETRAD + Windows-1254=Windows_1254 Windows-1254=WINDOWS_1254 Windows-1255=Windows_1255 + Windows-1255=WINDOWS_1255 johab=Johab johab=JOHAB Windows-1256=Windows_1256 + Windows-1256=WINDOWS_1256 x-mac-arabic=X_mac_arabic x-mac-arabic=X_MAC_ARABIC + Windows-1257=Windows_1257 Windows-1257=WINDOWS_1257 x-mac-hebrew=X_mac_hebrew + x-mac-hebrew=X_MAC_HEBREW Windows-1258=Windows_1258 Windows-1258=WINDOWS_1258 macGreek=MacGreek + macGreek=MACGREEK macCyrillic=MacCyrillic macCyrillic=MACCYRILLIC ibm00924=Ibm00924 + ibm00924=IBM00924 ISO-8859-2=ISO_8859_2 ISO-8859-3=ISO_8859_3 ISO-8859-4=ISO_8859_4 + ISO-8859-5=ISO_8859_5 ISO-8859-6=ISO_8859_6 ibm870=Ibm870 ibm870=IBM870 ISO-8859-7=ISO_8859_7 + ISO-8859-8=ISO_8859_8 macTurkish=MacTurkish macTurkish=MACTURKISH macCroatian=MacCroatian + macCroatian=MACCROATIAN cp875=Cp875 cp875=CP875 ibm420=Ibm420 ibm420=IBM420 CP949=CP949 + ibm423=Ibm423 ibm423=IBM423 ibm424=Ibm424 ibm424=IBM424 ibm280=Ibm280 ibm280=IBM280 + ibm01047=Ibm01047 ibm01047=IBM01047 ibm284=Ibm284 ibm284=IBM284 ibm285=Ibm285 ibm285=IBM285 + macRomania=MacRomania macRomania=MACROMANIA CP20932=CP20932 macUkraine=MacUkraine + macUkraine=MACUKRAINE x-europa=X_europa x-europa=X_EUROPA IBM737=IBM737 x-ia5=X_ia5 x-ia5=X_IA5 + x-cp20936=X_cp20936 x-cp20936=X_CP20936 x-ia5-german=X_ia5_german x-ia5-german=X_IA5_GERMAN + x-ia5-swedish=X_ia5_swedish x-ia5-swedish=X_IA5_SWEDISH x-ia5-norwegian=X_ia5_norwegian + x-ia5-norwegian=X_IA5_NORWEGIAN IBM775=IBM775 ISO-8859-13=ISO_8859_13 ibm290=Ibm290 + ibm290=IBM290 x-chinese-cns=X_chinese_cns x-chinese-cns=X_CHINESE_CNS asmo-708=Asmo_708 + asmo-708=ASMO_708 ibm297=Ibm297 ibm297=IBM297 macThai=MacThai macThai=MACTHAI + x-cp20001=X_cp20001 x-cp20001=X_CP20001 ibm905=Ibm905 ibm905=IBM905 + x-chinese-eten=X_chinese_eten x-chinese-eten=X_CHINESE_ETEN + x-ebcdic-koreanextended=X_ebcdic_koreanextended x-ebcdic-koreanextended=X_EBCDIC_KOREANEXTENDED + x-cp20003=X_cp20003 x-cp20003=X_CP20003 x-cp20004=X_cp20004 x-cp20004=X_CP20004 + x-cp20005=X_cp20005 x-cp20005=X_CP20005 CP850=CP850 ibm-thai=Ibm_thai ibm-thai=IBM_THAI + IBM852=IBM852 ibm871=Ibm871 ibm871=IBM871 macCentEuro=MacCentEuro macCentEuro=MACCENTEURO + IBM855=IBM855 cp1025=Cp1025 cp1025=CP1025 x-cp20949=X_cp20949 x-cp20949=X_CP20949 IBM857=IBM857 + ibm00858=Ibm00858 ibm00858=IBM00858 x-cp20261=X_cp20261 x-cp20261=X_CP20261 ibm1026=Ibm1026 + ibm1026=IBM1026 x-cp20269=X_cp20269 x-cp20269=X_CP20269 ISO-8859-1=ISO_8859_1 CP437=CP437 + CP737=CP737 CP775=CP775 CP857=CP857 CP860=CP860 CP861=CP861 CP862=CP862 CP863=CP863 CP864=CP864 + CP865=CP865 CP866=CP866 CP869=CP869 CP878=CP878 CP932=CP932 CP936=CP936 CP720=CP720 + EUC-CN=EUC_CN eucCN=EucCN eucCN=EUCCN MacJapan=MacJapan MacJapan=MACJAPAN + ebcdic-cp-us=Ebcdic_cp_us ebcdic-cp-us=EBCDIC_CP_US CP950=CP950 CP951=CP951 CP1258=CP1258 + CP1252=CP1252 CP1250=CP1250 CP1256=CP1256 CP1251=CP1251 CP1253=CP1253 CP1255=CP1255 + CP1254=CP1254 CP1257=CP1257 CP65000=CP65000 CP65001=CP65001 IBM850=IBM850 eucJP=EucJP + eucJP=EUCJP eucKR=EucKR eucKR=EUCKR eucTW=EucTW eucTW=EUCTW ISO2022-JP2=ISO2022_JP2 + ISO8859-1=ISO8859_1 ISO8859-2=ISO8859_2 ISO8859-3=ISO8859_3 ISO8859-4=ISO8859_4 + ISO8859-5=ISO8859_5 ISO8859-6=ISO8859_6 ISO8859-7=ISO8859_7 ISO8859-8=ISO8859_8 + ISO8859-10=ISO8859_10 ISO8859-11=ISO8859_11 ISO8859-13=ISO8859_13 ISO8859-14=ISO8859_14 + ISO8859-16=ISO8859_16 SJIS=SJIS PCK=PCK euc-jp-ms=Euc_jp_ms euc-jp-ms=EUC_JP_MS + csWindows31J=CsWindows31J csWindows31J=CSWINDOWS31J UTF-8-MAC=UTF_8_MAC UTF-8-HFS=UTF_8_HFS + UCS-2BE=UCS_2BE UCS-4BE=UCS_4BE UCS-4LE=UCS_4LE locale=Locale locale=LOCALE external=External + external=EXTERNAL filesystem=Filesystem filesystem=FILESYSTEM ISO-8859-11=ISO_8859_11 + CP852=CP852 CP855=CP855 + TABLE + + if (begin; constants(false) == __known__ && Encoding.name_list == __names__; rescue StandardError; false; end) + __table__.each do |__entry__| + __name__, __c__ = __entry__.split("=", 2) + __enc__ = begin + Encoding.find(__name__) + rescue StandardError + nil + end + next unless __enc__.is_a?(Encoding) + begin + const_set(__c__, __enc__) + rescue StandardError + end + end + else + begin + Encoding.name_list.each { |__n__| __add__.call(__n__) } + rescue StandardError + end + + # Names CRuby knows that are missing from (or spelled differently in) the + # runtime's own name list. Unsupported ones are silently skipped. + %w[ + ISO-8859-1 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6 + ISO-8859-7 ISO-8859-8 ISO-8859-9 ISO-8859-10 ISO-8859-11 ISO-8859-13 + ISO-8859-14 ISO-8859-15 ISO-8859-16 + Windows-31J Windows-874 + Windows-1250 Windows-1251 Windows-1252 Windows-1253 Windows-1254 + Windows-1255 Windows-1256 Windows-1257 Windows-1258 + CP437 CP737 CP775 CP850 CP852 CP855 CP857 CP860 CP861 CP862 CP863 + CP864 CP865 CP866 CP869 CP932 CP936 CP949 CP950 CP1252 + IBM437 IBM737 IBM775 IBM852 IBM857 IBM861 IBM862 IBM866 IBM869 + KOI8-R KOI8-U GB18030 GBK Big5 EUC-JP EUC-KR EUC-CN + macRoman macCentEuro macCroatian macCyrillic macGreek macIceland + macRomania macThai macTurkish macUkraine + ASCII-8BIT BINARY US-ASCII UTF-8 UTF-16 UTF-32 UTF-16BE UTF-16LE + UTF-32BE UTF-32LE Shift_JIS SJIS + ].each { |__n__| __add__.call(__n__) } + end end # -------------------------------------------------------------------------- @@ -2358,7 +2474,7 @@ def $LOAD_PATH.resolve_feature_path(feature) # argument-less #clone and refuse clone(freeze: false). module Kernel unless (begin; Object.new.clone(:freeze => nil); true; rescue ArgumentError; false; end) - alias_method :clone_without_options, :clone + alias :clone_without_options :clone # `clone(freeze: nil)` is the default (copy the frozen state), `freeze: true` # always freezes, `freeze: false` never does. The runtime's #clone always @@ -2397,9 +2513,10 @@ def clone(opts = nil) # .NET has no coroutines, so every Fiber is backed by a dedicated thread plus a # one-slot mailbox. Thread.new marks CLR threads IsBackground, so an abandoned # fiber can never keep the process alive. Exactly one fiber of a group ever -# runs: switching means "post to the target's mailbox, then block on my own", -# which is the SemaphoreSlim handshake spelled with Queue (Queue is a -# Monitor-based blocking queue here, so the wait is a real kernel wait). +# runs: switching means "post to the target's mailbox, then block on my own". +# The mailbox is a C# FiberMailbox (Src/Libraries/Builtins/ThreadOps.Fiber.cs), +# which spins for a few microseconds before it falls back to a Monitor wait, so +# a quick resume / Fiber.yield round trip does not cost two kernel wake-ups. # # resume / Fiber.yield / transfer / kill / raise / storage / scheduler follow # MRI, including the rule that a fiber which was entered by #transfer returns, @@ -2430,9 +2547,10 @@ def self.__err__(cls, msg) ::Kernel.raise(cls, msg) end + # A C# FiberMailbox (Src/Libraries/Builtins/ThreadOps.Fiber.cs): a one-slot handoff + # whose take spins briefly before it blocks. def self.__mailbox__ - require 'thread' unless defined?(::Queue) - ::Queue.new + ::Thread.__fiber_mailbox__ end # Storage keys are Symbols; anything with #to_str (but not #to_sym) is @@ -2473,23 +2591,28 @@ def self.__init_storage__(storage, parent) # ---- the root fiber of a thread -------------------------------------- + # Thread.__fiber_current__ is a thread-static copy of Thread.current[:__ir_fiber_current__] + # (a fiber never leaves its CLR thread); __fiber_current_set__ writes both. def self.current - Thread.current[:__ir_fiber_current__] || __root__ + ::Thread.__fiber_current__ || __root__ end def self.__root__ t = Thread.current - f = t[:__ir_fiber_root__] + f = t[:__ir_fiber_current__] unless f - f = allocate - f.__init_root__ - # A thread's root fiber starts with the storage of the fiber that created the - # thread; Thread.new leaves that fiber here for us. - parent = t[:__ir_fiber_parent__] - f.__inherit_storage__(parent) if parent - t[:__ir_fiber_root__] = f - t[:__ir_fiber_current__] = f + f = t[:__ir_fiber_root__] + unless f + f = allocate + f.__init_root__ + # A thread's root fiber starts with the storage of the fiber that created the + # thread; Thread.new leaves that fiber here for us. + parent = t[:__ir_fiber_parent__] + f.__inherit_storage__(parent) if parent + t[:__ir_fiber_root__] = f + end end + ::Thread.__fiber_current_set__(f) f end @@ -2552,7 +2675,7 @@ def __set_resuming__(f); @resuming = f; end def __set_status__(s); @status = s; end def __set_blocking__(b); @blocking = b; end def __set_transferred__; @transferred = true; end - def __post__(msg); @mailbox.push(msg); end + def __post__(msg); ::Thread.__fiber_post__(@mailbox, msg); end def __scheduler_get__; @scheduler; end def __scheduler_set__(s); @scheduler = s; end @@ -2588,7 +2711,7 @@ def inspect @location ? "#" : "#" end - alias_method :to_s, :inspect + alias :to_s :inspect def storage unless Fiber.current.equal?(self) @@ -2754,22 +2877,31 @@ def self.__switch__(cur, target, msg) cur.__act__(cur.__await__) end + # A fiber runs on a thread of its own: one whose last fiber has finished if one is + # parked (Thread.__fiber_reuse__, ThreadOps.Fiber.cs), a new one otherwise. A thread + # goes back to the pool when its fiber is done (Thread.__fiber_park__), clean of that + # fiber's locals, and ends when nobody wants it for a while. def __start__ return self if @thread fiber = self - owner = @root_fiber.__thread__ - @thread = Thread.new do - Thread.current[:__ir_fiber_current__] = fiber - # Ruby ownership (Mutex, deadlock detection) is per thread, not per fiber - Thread.__set_fiber_owner__(owner) if owner - fiber.__run__ + @thread = ::Thread.__fiber_reuse__(fiber) || Thread.new do + while fiber + ::Thread.__fiber_current_set__(fiber) + # Ruby ownership (Mutex, deadlock detection) is per thread, not per fiber + owner = fiber.__root_fiber__.__thread__ + Thread.__set_fiber_owner__(owner) if owner + fiber.__run__ + fiber = nil + fiber = ::Thread.__fiber_park__ + end end self end + # Takes the next message and makes this CLR thread the one that runs on behalf of the + # group's owner (Thread.__set_fiber_owner__). def __await__ - msg = @mailbox.pop - Thread.__set_fiber_owner__(@root_fiber.__thread__) + msg = ::Thread.__fiber_take__(@mailbox, @root_fiber.__thread__) @status = :resumed msg end @@ -2860,6 +2992,9 @@ def __finish__(msg) # The program has ended and this fiber was killed with its thread: the # fiber that resumed it is suspended, and stays that way (as in MRI). return if ::Thread.__terminating__ + # What the end of this thread does for the mutexes the fiber still holds, + # done before the fiber we hand control to can run and lock. + ::Thread.__fiber_release_locks__ target = @prev if target @prev = nil @@ -2912,10 +3047,16 @@ def __opts_exception__(value) # String is parsed, and anything else is asked for #to_int, then #to_i, with # #to_str consulted first when the object offers one. if private_method_defined?(:Integer) || method_defined?(:Integer) - alias_method :__ir_Integer__, :Integer + alias :__ir_Integer__ :Integer private :__ir_Integer__ def Integer(arg, base = nil, exception: true) + # The common case - a String, no base, failures raised - is what the block below + # does first anyway; going straight there skips the block and its rescue frame, + # which cost many times the parse itself. + if base.nil? && true.equal?(exception) && !arg.nil? && arg.is_a?(::String) + return __ir_Integer__(arg) + end exception = ::Kernel.__opts_exception__(exception) __numeric_conversion__(exception) do if arg.nil? @@ -2973,10 +3114,14 @@ def Integer(arg, base = nil, exception: true) end if private_method_defined?(:Float) || method_defined?(:Float) - alias_method :__ir_Float__, :Float + alias :__ir_Float__ :Float private :__ir_Float__ def Float(arg, exception: true) + # A String with failures raised goes to the C# parser below in any case; see Integer. + if true.equal?(exception) && !arg.nil? && ::String === arg + return __ir_Float__(arg) + end exception = ::Kernel.__opts_exception__(exception) __numeric_conversion__(exception) do raise ::TypeError, "can't convert nil into Float" if arg.nil? @@ -3038,7 +3183,7 @@ def Rational(*args, exception: true) # process for ever - spec/core/kernel/sleep_spec.rb stops dead there. module Kernel if private_method_defined?(:sleep) || method_defined?(:sleep) - alias_method :__ir_sleep__, :sleep + alias :__ir_sleep__ :sleep private :__ir_sleep__ def sleep(*args) @@ -3082,7 +3227,7 @@ class Integer # ordinary code use. class Float unless instance_method(:round).arity == -1 - alias_method :__ir_round__, :round + alias :__ir_round__ :round def round(ndigits = 0) n = ndigits.to_int @@ -3128,7 +3273,7 @@ def truncate(ndigits = 0) class Integer unless instance_method(:round).arity == -1 - alias_method :__ir_round__, :round + alias :__ir_round__ :round def round(ndigits = 0) n = ndigits.to_int @@ -3185,7 +3330,7 @@ def div(other) def modulo(other) self - other * div(other) end - alias_method :%, :modulo + alias :% :modulo def divmod(other) [div(other), modulo(other)] @@ -3223,8 +3368,8 @@ def quo(other) def arg self < 0 ? ::Math::PI : 0 end - alias_method :angle, :arg - alias_method :phase, :arg + alias :angle :arg + alias :phase :arg # A number is already what a copy of it would be. def clone(freeze: nil) @@ -3246,8 +3391,8 @@ def arg return self if nan? (self < 0 || (zero? && (1.0 / self) < 0)) ? ::Math::PI : 0 end - alias_method :angle, :arg - alias_method :phase, :arg + alias :angle :arg + alias :phase :arg end if defined?(Rational) && Rational.instance_method(:round).arity == 0 @@ -3420,7 +3565,7 @@ module Kernel # The built-in warn takes exactly one message and no keywords; 2.5 added # multiple messages plus uplevel:, and 3.0 added category:. if private_method_defined?(:warn) && instance_method(:warn).arity == 1 - alias_method :__ir_warn__, :warn + alias :__ir_warn__ :warn private :__ir_warn__ def warn(*messages, **options) @@ -4067,9 +4212,9 @@ def __selected__(ch, selectors) end private :__selected__ - alias_method :__ir_strip__, :strip - alias_method :__ir_lstrip__, :lstrip - alias_method :__ir_rstrip__, :rstrip + alias :__ir_strip__ :strip + alias :__ir_lstrip__ :lstrip + alias :__ir_rstrip__ :rstrip private :__ir_strip__, :__ir_lstrip__, :__ir_rstrip__ # #count is asked once per character, so the selectors are converted up front: @@ -4320,19 +4465,19 @@ def fdiv(other) to_f / other end unless method_defined?(:fdiv) - alias_method :magnitude, :abs unless method_defined?(:magnitude) + alias :magnitude :abs unless method_defined?(:magnitude) # #imag and #imaginary are one method in MRI, and so are Float#quo and Float#fdiv. - alias_method :imaginary, :imag if method_defined?(:imag) + alias :imaginary :imag if method_defined?(:imag) end class Float - alias_method :quo, :fdiv + alias :quo :fdiv end class Rational - alias_method :magnitude, :abs - alias_method :quo, :/ + alias :magnitude :abs + alias :quo :/ end class Float @@ -4438,12 +4583,12 @@ def real def imaginary 0 end unless method_defined?(:imaginary) - alias_method :imag, :imaginary unless method_defined?(:imag) + alias :imag :imaginary unless method_defined?(:imag) def conjugate self end unless method_defined?(:conjugate) - alias_method :conj, :conjugate unless method_defined?(:conj) + alias :conj :conjugate unless method_defined?(:conj) def abs2 self * self @@ -4452,13 +4597,13 @@ def abs2 def rectangular [self, 0] end unless method_defined?(:rectangular) - alias_method :rect, :rectangular unless method_defined?(:rect) + alias :rect :rectangular unless method_defined?(:rect) def arg self < 0 ? Math::PI : 0 end unless method_defined?(:arg) - alias_method :angle, :arg unless method_defined?(:angle) - alias_method :phase, :arg unless method_defined?(:phase) + alias :angle :arg unless method_defined?(:angle) + alias :phase :arg unless method_defined?(:phase) def polar [abs, arg] @@ -4528,7 +4673,7 @@ def inspect # One method under two names, which is what `Complex.instance_method(:imag) == # Complex.instance_method(:imaginary)` compares - a second method that called the # first answered the same number but was not the same method. - alias_method :imaginary, :imag + alias :imaginary :imag def finite? real.finite? && imag.finite? @@ -4541,7 +4686,7 @@ def infinite? def rectangular [real, imag] end - alias_method :rect, :rectangular + alias :rect :rectangular end # The rest of Complex, on top of 1.8's complex.rb. @@ -4605,7 +4750,7 @@ def self.rectangular(real, imag = 0) __canon__(real, imag) end class << self - alias_method :rect, :rectangular + alias :rect :rectangular end def self.polar(abs, arg = 0) @@ -4686,7 +4831,7 @@ def /(other) __coerce_bin__(other, :quo) end end - alias_method :quo, :/ + alias :quo :/ def fdiv(other) if other.is_a?(::Complex) @@ -4766,7 +4911,7 @@ def abs ::Math.hypot(r, i) end end - alias_method :magnitude, :abs + alias :magnitude :abs def abs2 real * real + imag * imag @@ -4775,8 +4920,8 @@ def abs2 def arg ::Math.atan2(imag, real) end - alias_method :angle, :arg - alias_method :phase, :arg + alias :angle :arg + alias :phase :arg def polar [abs, arg] @@ -4785,7 +4930,7 @@ def polar def conjugate ::Complex.__raw__(real, -imag) end - alias_method :conj, :conjugate + alias :conj :conjugate def ==(other) if other.is_a?(::Complex) @@ -5233,7 +5378,7 @@ def clone(freeze: nil) class Rational # rational18's Rational.reduce still builds instances behind our back, so the # freeze goes on #initialize rather than only on the constructors below. - alias_method :__ir_rational_init__, :initialize + alias :__ir_rational_init__ :initialize def initialize(num, den) __ir_rational_init__(num, den) freeze @@ -5582,7 +5727,7 @@ def each_child(path, *args, &block) end unless respond_to?(:__clr_foreach__, true) - alias_method :__clr_foreach__, :foreach + alias :__clr_foreach__ :foreach private :__clr_foreach__ def foreach(path, *args, &block) @@ -5603,7 +5748,7 @@ def empty?(path) # #each_child and #chdir, and an Enumerator from #each when no block is given. class Dir unless method_defined?(:__clr_each__) - alias_method :__clr_each__, :each + alias :__clr_each__ :each private :__clr_each__ def each(&block) @@ -5724,14 +5869,19 @@ module Errno ERRNO_TABLE.each do |errno, (name, message)| if const_defined?(name, false) klass = const_get(name) + # const_defined? without the second argument would find the enclosing Errno + # module through the lexical scope, so restrict the lookup to the class. + klass.const_set(:Errno, errno) unless klass.const_defined?(:Errno, false) + klass.const_set(:Message, message) unless klass.const_defined?(:Message, false) else klass = Class.new(SystemCallError) const_set(name, klass) + # A class made just now has no constants of its own, so there is nothing to ask it + # first - and asking is not free: every class is a new receiver for these call + # sites, so each question binds a rule of its own. + klass.const_set(:Errno, errno) + klass.const_set(:Message, message) end - # const_defined? without the second argument would find the enclosing Errno - # module through the lexical scope, so restrict the lookup to the class. - klass.const_set(:Errno, errno) unless klass.const_defined?(:Errno, false) - klass.const_set(:Message, message) unless klass.const_defined?(:Message, false) ERRNO_CLASSES[errno] ||= klass end @@ -6006,7 +6156,7 @@ def select!(&block) size == before ? nil : self end unless method_defined?(:select!) - alias_method :filter!, :select! unless method_defined?(:filter!) + alias :filter! :select! unless method_defined?(:filter!) def keep_if(&block) return ::Enumerator.new { |y| each { |x| y << x } } unless block @@ -6039,8 +6189,8 @@ def to_h # #uniq and #uniq! compare the block's result, not the element itself; the # core versions ignore the block entirely, so they are kept for the no-block # case and wrapped here. - alias_method :__uniq_without_block__, :uniq - alias_method :__uniq_in_place_without_block__, :uniq! + alias :__uniq_without_block__ :uniq + alias :__uniq_in_place_without_block__ :uniq! def uniq(&block) return __uniq_without_block__ unless block @@ -6071,7 +6221,7 @@ def uniq!(&block) # #zip takes anything that responds to #each, not just arrays: an Enumerator # (including an infinite one, which is why this reads lazily and stops at the # receiver's size) or any object with #each. - alias_method :__zip_arrays__, :zip + alias :__zip_arrays__ :zip def zip(*others, &block) length = size @@ -6251,7 +6401,7 @@ def self.start(full_mark: true, immediate_mark: true, immediate_sweep: true) end class << self - alias_method :garbage_collect, :start + alias :garbage_collect :start end def self.total_time @@ -6493,7 +6643,7 @@ def with_object(memo) end unless method_defined?(:with_object) # Same story as #each_with_index: Enumerable's version would shadow this one, # and the specs check that the two really are the same method. - alias_method :each_with_object, :with_object + alias :each_with_object :with_object # --- #size ----------------------------------------------------------- # MRI gives every enumerator a size function: an Integer, a Proc, or nil when @@ -6651,7 +6801,7 @@ def inspect detail += "(#{args.map { |a| a.inspect }.join(', ')})" unless args.empty? "#<#{self.class}: #{detail}>" end - alias_method :to_s, :inspect + alias :to_s :inspect # --- external iteration ------------------------------------------------ # #next has to suspend `each` half way through, which needs a coroutine. @@ -6683,6 +6833,14 @@ def __iter_stop__ def __iter_advance__ return if @__peeked__ __iter_stop__ if @__iter_done__ + # The built-in Array#each over an Array needs no fiber: a cursor walks it by + # index the way #each would (Enumerator#__ir_array_cursor__ says when). + cursor = @__iter_cursor__ + if cursor.nil? && @__fiber__.nil? + cursor = @__iter_cursor__ = __ir_array_cursor__ + @__iter_started__ = false + end + return __iter_cursor_advance__(cursor) if cursor fiber = __iter_fiber__ fed = nil # A fed value is what the suspended yield returns, so it waits while the @@ -6712,6 +6870,27 @@ def __iter_advance__ end private :__iter_advance__ + def __iter_cursor_advance__(cursor) + values = __ir_array_cursor_next__(cursor) + # false: the iteration belongs to another thread, as a fiber would + ::Kernel.raise(::FiberError, "fiber called across threads") if values.equal?(false) + if @__iter_started__ + @__feed__ = nil + @__has_feed__ = false + end + @__iter_started__ = true + if values + @__peek__ = values + @__peeked__ = true + else + @__iter_done__ = true + @__iter_result__ = __ir_array_cursor_source__(cursor) + @__iter_cursor__ = nil + __iter_stop__ + end + end + private :__iter_cursor_advance__ + def next_values __iter_advance__ values = @__peek__ @@ -6746,6 +6925,7 @@ def feed(value) def rewind fiber = @__fiber__ @__fiber__ = nil + @__iter_cursor__ = nil @__iter_done__ = false @__iter_result__ = nil @__peeked__ = false @@ -6834,14 +7014,14 @@ def inspect return "#<#{self.class}: uninitialized>" if @generator.nil? "#<#{self.class}: ...>" end - alias_method :to_s, :inspect + alias :to_s :inspect def map(&block) __need_block__("map", block) source = self __chain__(size) { |y| source.each { |*values| y << block.call(*values) } } end - alias_method :collect, :map + alias :collect :map def flat_map(&block) __need_block__("flat_map", block) @@ -6863,15 +7043,15 @@ def flat_map(&block) end end end - alias_method :collect_concat, :flat_map + alias :collect_concat :flat_map def select(&block) __need_block__("select", block) source = self __chain__ { |y| source.each { |*values| item = __value__(values); y << item if block.call(item) } } end - alias_method :filter, :select - alias_method :find_all, :select + alias :filter :select + alias :find_all :select def filter_map(&block) __need_block__("filter_map", block) @@ -6904,8 +7084,8 @@ def __ir_lazy_grep_impl__(pattern, invert, caller_frame, block) end private :__ir_lazy_grep_impl__ - alias_method :grep, :__ir_lazy_grep__ - alias_method :grep_v, :__ir_lazy_grep_v__ + alias :grep :__ir_lazy_grep__ + alias :grep_v :__ir_lazy_grep_v__ public :grep, :grep_v def compact @@ -7029,7 +7209,7 @@ def with_object(memo) source.each { |*values| y << [values.size <= 1 ? values[0] : values, memo] } end end - alias_method :each_with_object, :with_object + alias :each_with_object :with_object def zip(*others, &block) # Only the block form falls back to the eager Enumerable#zip. Everything @@ -7204,7 +7384,7 @@ def to_enum(method = :each, *args, &size_block) end end end - alias_method :enum_for, :to_enum + alias :enum_for :to_enum def first(n = nil) if n.nil? @@ -7313,7 +7493,7 @@ def inspect return "#" if @__enums__.nil? "#" end - alias_method :to_s, :inspect + alias :to_s :inspect def +(other) Chain.new(self, other) @@ -7409,7 +7589,7 @@ def inspect ::Thread.current[:__enumerator_product_inspect__] = nil if seen.empty? end end - alias_method :to_s, :inspect + alias :to_s :inspect def __check_frozen__ return unless frozen? @@ -7511,7 +7691,7 @@ module Kernel enum.__set_size__(size_block) if size_block enum end - alias_method :enum_for, :to_enum + alias :enum_for :to_enum # The C# Kernel#loop does not know about StopIteration, which is what makes # `loop { e.next }` terminate instead of blowing up. @@ -7563,7 +7743,7 @@ def unpack1(format, offset: 0) # #chars and #bytes build the Array in C# (MutableStringOps); only #lines is still # an Enumerator there. unless "a".lines.is_a?(Array) - alias_method :lines_enumerator, :lines + alias :lines_enumerator :lines def lines(*args, &block) return lines_enumerator(*args, &block) if block @@ -7582,7 +7762,7 @@ def lines(*args, &block) # splitter so encodings and the String-instances-for-subclasses rule keep # working; only the grouping and the trimming are redone. class String - alias_method :__ir_each_line__, :each_line + alias :__ir_each_line__ :each_line def each_line(*args, chomp: false, &block) lines = __ir_split_lines__(args, chomp) @@ -7694,7 +7874,7 @@ def __ir_chomp_line__(line, separator, chomp) class Hash # the core raises IndexError; Ruby 1.9 introduced KeyError for this unless (begin; {}.fetch(:missing); rescue KeyError; true; rescue IndexError; false; end) - alias_method :fetch_raising_index_error, :fetch + alias :fetch_raising_index_error :fetch def fetch(key, *default, &block) return fetch_raising_index_error(key, *default, &block) if !default.empty? || block @@ -7733,7 +7913,7 @@ def inspect stack.pop end end - alias_method :to_s, :inspect + alias :to_s :inspect end # Mutex/Queue/SizedQueue/ConditionVariable needed `require "thread"` in 1.9; they @@ -7762,7 +7942,7 @@ def match?(str, pos = 0) # three plain CLR methods added to MatchData.cs; everything else is built on # top of them and of #string, which is the subject the offsets refer to. class MatchData - alias_method :__ir_index__, :[] + alias :__ir_index__ :[] def [](*args) if args.size == 1 @@ -7807,7 +7987,7 @@ def named_captures(symbolize_names: false) # #deconstruct is not merely equivalent to #captures, it is the same method: # MatchData.instance_method(:deconstruct) == MatchData.instance_method(:captures). - alias_method :deconstruct, :captures unless method_defined?(:deconstruct) + alias :deconstruct :captures unless method_defined?(:deconstruct) def deconstruct_keys(keys) all = names @@ -7841,7 +8021,7 @@ def match_length(n) m && m.length end unless method_defined?(:match_length) - alias_method :__ir_values_at__, :values_at + alias :__ir_values_at__ :values_at # Ranges, names and Integers can be mixed. Array#values_at already has the # Range rules - nil fill past the end, RangeError for a start that is negative @@ -7947,12 +8127,12 @@ def -@ __ir_fstring__ end unless method_defined?(:-@) - alias_method :dedup, :-@ + alias :dedup :-@ # The C# #upto is Range#each in disguise, which loses the numeric-sequence # case ("8".upto("11")), the exclusive-end argument, the "stop is shorter than # self" cut-off and the type and encoding checks. - alias_method :__ir_upto__, :upto + alias :__ir_upto__ :upto private :__ir_upto__ def upto(stop, exclusive = false, &block) @@ -8162,7 +8342,7 @@ def self.__step_each__(from, to, unit, excl, &block) # Only Range#step, Range#% and Numeric#step make one: .new is undefined and .allocate refuses. class << self - alias_method :__allocate__, :allocate + alias :__allocate__ :allocate private :__allocate__ undef_method :new @@ -8222,8 +8402,8 @@ def to_a __arith_each__ { |v| result << v } result end - alias_method :entries, :to_a - alias_method :force, :to_a + alias :entries :to_a + alias :force :to_a def size return ::Float::INFINITY if @end.nil? && @step != 0 @@ -8240,7 +8420,7 @@ def ==(other) self.begin == other.begin && self.end == other.end && step == other.step && exclude_end? == other.exclude_end? end - alias_method :eql?, :== + alias :eql? :== def hash [self.begin, self.end, step, exclude_end?].hash @@ -8250,7 +8430,7 @@ def inspect return @inspect_str if @inspect_str "((#{@source.inspect}).%(#{@step.inspect}))" end - alias_method :to_s, :inspect + alias :to_s :inspect end end @@ -8303,7 +8483,7 @@ def new(*args, **kwargs, &block) instance end - alias_method :[], :new + alias :[] :new def inspect ::Module.instance_method(:to_s).bind(self).call @@ -8481,7 +8661,7 @@ def to_s stack.pop end end - alias_method :inspect, :to_s + alias :inspect :to_s private @@ -8580,7 +8760,7 @@ def filename __stream__ @current_path || "-" end - alias_method :path, :filename + alias :path :filename def to_io file @@ -8589,12 +8769,12 @@ def to_io def to_s "ARGF" end - alias_method :inspect, :to_s + alias :inspect :to_s def fileno file.fileno end - alias_method :to_i, :fileno + alias :to_i :fileno def binmode @binmode = true @@ -8633,7 +8813,7 @@ def eof? end s.nil? || s.eof? end - alias_method :eof, :eof? + alias :eof :eof? def gets(*args) loop do @@ -8666,7 +8846,7 @@ def each_line(*args) end self end - alias_method :each, :each_line + alias :each :each_line def readlines(*args) result = [] @@ -8675,7 +8855,7 @@ def readlines(*args) end result end - alias_method :to_a, :readlines + alias :to_a :readlines def read(length = nil, buffer = nil) result = +"" @@ -8728,19 +8908,19 @@ def each_char end self end - alias_method :chars, :each_char + alias :chars :each_char def each_byte return ::Enumerator.new { |y| each_byte { |b| y << b } } unless block_given? each_char { |c| c.each_byte { |b| yield b } } self end - alias_method :bytes, :each_byte + alias :bytes :each_byte def pos file.pos end - alias_method :tell, :pos + alias :tell :pos def pos=(value) file.pos = value @@ -8805,8 +8985,8 @@ def __env_check_name__(name) private :__env_check_name__ unless respond_to?(:__clr_store__, true) - alias_method :__clr_store__, :[]= - alias_method :__clr_fetch__, :[] + alias :__clr_store__ :[]= + alias :__clr_fetch__ :[] private :__clr_store__ private :__clr_fetch__ @@ -8823,7 +9003,7 @@ def []=(name, value) value end - alias_method :store, :[]= + alias :store :[]= end def [](name) @@ -8875,7 +9055,7 @@ def each(&block) to_hash.each(&block) self end - alias_method :each_pair, :each + alias :each_pair :each def each_key return to_enum(:each_key) { size } unless block_given? @@ -8915,9 +9095,9 @@ def select(&block) return to_enum(:select) { size } unless block to_hash.select(&block) end - alias_method :filter, :select + alias :filter :select - alias_method :member?, :include? + alias :member? :include? # Pairs are applied one at a time and in order, so a bad pair leaves everything # before it in place -- which is what MRI's own specs pin down. @@ -8934,7 +9114,7 @@ def merge!(*others) self end - alias_method :update, :merge! + alias :update :merge! # replace is all-or-nothing: everything is converted and checked before the first # variable is touched, so a TypeError halfway through leaves ENV untouched. @@ -8981,7 +9161,7 @@ def select! changed ? self : nil end unless respond_to?(:select!) - alias_method :filter!, :select! unless respond_to?(:filter!) + alias :filter! :select! unless respond_to?(:filter!) # MRI keeps the objects it was handed as the result keys, not their #to_str. def slice(*keys) @@ -9020,7 +9200,7 @@ def value?(value) to_hash.each_value { |v| return true if v == value } false end - alias_method :has_value?, :value? + alias :has_value? :value? def to_h(&block) to_hash.to_h(&block) @@ -9091,10 +9271,10 @@ def bind_call(receiver, *args, &block) module Math class DomainError < StandardError; end unless const_defined?(:DomainError, false) - alias_method :__ir_gamma__, :gamma - alias_method :__ir_lgamma__, :lgamma - alias_method :__ir_erf__, :erf - alias_method :__ir_erfc__, :erfc + alias :__ir_gamma__ :gamma + alias :__ir_lgamma__ :lgamma + alias :__ir_erf__ :erf + alias :__ir_erfc__ :erfc module_function :__ir_gamma__, :__ir_lgamma__, :__ir_erf__, :__ir_erfc__ # MRI's rb_to_float: Numeric only. An object that merely answers #to_f is a @@ -9116,25 +9296,25 @@ def __dom__(name) module_function :__flt__, :__dom__ private_class_method :__flt__, :__dom__ - def acos(x) + def __ir_acos__(x) x = __flt__(x) return x if x.nan? __dom__("acos") if x < -1.0 || x > 1.0 ::System::Math.Acos(x) end - def asin(x) + def __ir_asin__(x) x = __flt__(x) return x if x.nan? __dom__("asin") if x < -1.0 || x > 1.0 ::System::Math.Asin(x) end - def atan(x) + def __ir_atan__(x) ::System::Math.Atan(__flt__(x)) end - def atan2(y, x) + def __ir_atan2__(y, x) y = __flt__(y) x = __flt__(x) return y if y.nan? @@ -9149,7 +9329,7 @@ def acosh(x) ::System::Math.Acosh(x) end - def asinh(x) + def __ir_asinh__(x) ::System::Math.Asinh(__flt__(x)) end @@ -9162,40 +9342,40 @@ def atanh(x) ::System::Math.Atanh(x) end - def cos(x) + def __ir_cos__(x) ::System::Math.Cos(__flt__(x)) end - def sin(x) + def __ir_sin__(x) ::System::Math.Sin(__flt__(x)) end - def tan(x) + def __ir_tan__(x) ::System::Math.Tan(__flt__(x)) end - def cosh(x) + def __ir_cosh__(x) ::System::Math.Cosh(__flt__(x)) end - def sinh(x) + def __ir_sinh__(x) ::System::Math.Sinh(__flt__(x)) end - def tanh(x) + def __ir_tanh__(x) ::System::Math.Tanh(__flt__(x)) end - def exp(x) + def __ir_exp__(x) ::System::Math.Exp(__flt__(x)) end # pow(x, 1/3.0) is NaN for every negative x; cbrt is defined there. - def cbrt(x) + def __ir_cbrt__(x) ::System::Math.Cbrt(__flt__(x)) end - def sqrt(x) + def __ir_sqrt__(x) x = __flt__(x) return x if x.nan? __dom__("sqrt") if x < 0.0 @@ -9291,7 +9471,7 @@ def erfc(x) __ir_erfc__(__flt__(x)) end - def hypot(x, y) + def __ir_hypot__(x, y) x = __flt__(x).abs y = __flt__(y).abs # An infinity wins over a NaN, which is what C's hypot() promises. @@ -9378,10 +9558,20 @@ def frexp(x) end module_function :frexp - module_function :acos, :acosh, :asin, :asinh, :atan, :atan2, :atanh, :cbrt, - :cos, :cosh, :erf, :erfc, :exp, :expm1, :gamma, :hypot, - :ldexp, :lgamma, :log, :log10, :log1p, :log2, :sin, :sinh, - :sqrt, :tan, :tanh + module_function :acosh, :atanh, :erf, :erfc, :expm1, :gamma, + :ldexp, :lgamma, :log, :log10, :log1p, :log2 + # complex18.rb (the 1.9 Complex still loaded at boot) redefined most of these to return + # Complex numbers and kept the C# originals as name!. Put the C# methods - RubyMath, whose + # Float/Fixnum fast path falls back to the __ir___ definitions above - back in place. + %i[acos asin atan atan2 cos sin tan cosh sinh tanh asinh exp sqrt].each do |name| + bang = :"#{name}!" + alias_method name, bang if private_method_defined?(bang) || method_defined?(bang) + end + module_function :acos, :asin, :atan, :atan2, :cos, :sin, :tan, :cosh, :sinh, :tanh, :asinh, :exp, :cbrt, + :sqrt, :hypot + # The Float/Fixnum fast path of these is C# (RubyMath); their general Ruby half: + module_function :__ir_acos__, :__ir_asin__, :__ir_atan__, :__ir_atan2__, :__ir_cos__, :__ir_sin__, :__ir_tan__, :__ir_cosh__, :__ir_sinh__, :__ir_tanh__, :__ir_asinh__, :__ir_exp__, :__ir_cbrt__, :__ir_sqrt__, :__ir_hypot__ + private_class_method :__ir_acos__, :__ir_asin__, :__ir_atan__, :__ir_atan2__, :__ir_cos__, :__ir_sin__, :__ir_tan__, :__ir_cosh__, :__ir_sinh__, :__ir_tanh__, :__ir_asinh__, :__ir_exp__, :__ir_cbrt__, :__ir_sqrt__, :__ir_hypot__ end # `ruby2_keywords' written at the top level is Object.ruby2_keywords. MRI defines it on @@ -10023,7 +10213,7 @@ def self.__check__(result, message = nil) # The CLR half of kill hands back -errno rather than throwing, for the same reason. class << self unless method_defined?(:__clr_kill__) || private_method_defined?(:__clr_kill__) - alias_method :__clr_kill__, :kill + alias :__clr_kill__ :kill def kill(signal, *pids) Process.__check__(__clr_kill__(signal, *pids)) @@ -10236,8 +10426,8 @@ def self.wait2(pid = -1, flags = 0) # Not four methods that call one another: MRI defines two and aliases them, and a spec # compares Process.method(:waitpid) with Process.method(:wait). class << self - alias_method :waitpid, :wait - alias_method :waitpid2, :wait2 + alias :waitpid :wait + alias :waitpid2 :wait2 end def self.waitall @@ -10651,7 +10841,7 @@ module Kernel # includes every Float below 1 -- means "give me a Float" rather than an error. # The builtin only ever understood a positive Integer. unless private_method_defined?(:__ir_rand__) - alias_method :__ir_rand__, :rand + alias :__ir_rand__ :rand private :__ir_rand__ # rb_f_rand draws from the same default generator as Random.rand, which is @@ -10736,8 +10926,8 @@ def getkey(key) def key?(key) @table.key?(Key.new(key)) end - alias_method :member?, :key? - alias_method :include?, :key? + alias :member? :key? + alias :include? :key? def clear @table.clear @@ -11291,7 +11481,7 @@ def get_string(offset = 0, length = nil, encoding = ::Encoding::BINARY) result.force_encoding(encoding) if result.respond_to?(:force_encoding) result end - alias_method :to_str, :get_string + alias :to_str :get_string # The write goes into the allocation in place, so a String-backed buffer and # every slice sharing the allocation see it. @@ -11545,7 +11735,7 @@ def ~ # Same for IO.read: the text form is tagged with the encoding asked for, # the length form is bytes. IO.binread stays binary, which is its whole job. class << self - alias_method :__ir_class_read__, :read + alias :__ir_class_read__ :read private :__ir_class_read__ # IO.read opens the file itself, so mode:, encoding: and :open_args all get @@ -11608,7 +11798,7 @@ def read(name, *args, **options) # read with no length answers text in the stream's external encoding; read # with a length answers bytes, and is ASCII-8BIT. The C# read always handed # back ASCII-8BIT, so File.read(path, encoding: "utf-8") came out binary. - alias_method :__ir_read__, :read + alias :__ir_read__ :read private :__ir_read__ def read(*args) @@ -11630,7 +11820,7 @@ def read(*args) # set_encoding takes "external:internal" in one string as well as the two # separately; the C# one only understood a single encoding name and answered # "unknown encoding name - utf-8:ISO-8859-1". - alias_method :__ir_set_encoding__, :set_encoding + alias :__ir_set_encoding__ :set_encoding def set_encoding(*args, **opts) args = args + [opts] unless opts.empty? @@ -11729,7 +11919,7 @@ def __readable_stream__? # a socket, the standard streams. On this platform it has nothing to do but # say "these are bytes", so record that and set the external encoding, rather # than throwing a CLR exception at anything that is not a File. - alias_method :__ir_binmode__, :binmode + alias :__ir_binmode__ :binmode def binmode ::Kernel.raise(::IOError, "closed stream") if closed? @@ -11826,7 +12016,7 @@ def __encode_for_write__(text) end private :__encode_for_write__ - alias_method :__ir_write__, :write + alias :__ir_write__ :write def write(*args) total = 0 @@ -11885,7 +12075,7 @@ def __chomp_line__(line, args) # #gets and #readline do chomp: and the transcoding in the built-ins (IoOps.cs): # they set $_ for their caller, and a Ruby wrapper would set its own instead. - alias_method :__ir_readlines__, :readlines + alias :__ir_readlines__ :readlines def readlines(*args, **opts) args, chomp = __take_chomp__(args, opts) @@ -11893,7 +12083,7 @@ def readlines(*args, **opts) chomp ? lines.map { |l| __chomp_line__(l, args) } : lines end - alias_method :__ir_each_line__, :each_line + alias :__ir_each_line__ :each_line def each_line(*args, **opts, &block) args, chomp = __take_chomp__(args, opts) @@ -11909,12 +12099,12 @@ def each_line(*args, **opts, &block) # #each is #each_line under another name, so it is aliased rather than defined to # call it: the two are one method in MRI and a spec compares them. if method_defined?(:each) - alias_method :__ir_each__, :each - alias_method :each, :each_line + alias :__ir_each__ :each + alias :each :each_line end class << self - alias_method :__ir_class_readlines__, :readlines + alias :__ir_class_readlines__ :readlines # IO.readlines and IO.foreach are the instance methods on a stream opened for # the occasion, which is how the mode: and encoding options in the trailing @@ -11983,7 +12173,7 @@ class << self # pair, closes both afterwards and answers what the block answered. The # built-in took no arguments at all and gave the pair back from the block # form instead of the block's value. - alias_method :__ir_pipe__, :pipe + alias :__ir_pipe__ :pipe def pipe(*args) options = args.last.is_a?(::Hash) ? args.pop : nil @@ -12067,7 +12257,7 @@ def __ir_skip_bom__ # answered a one-character String since 1.9, reading as many bytes as the # character needs. if instance_method(:getc).arity == 0 - alias_method :__ir_getc__, :getc + alias :__ir_getc__ :getc private :__ir_getc__ def getc @@ -12100,14 +12290,14 @@ def __seek_whence__(whence) end private :__seek_whence__ - alias_method :__ir_seek__, :seek + alias :__ir_seek__ :seek private :__ir_seek__ def seek(amount, whence = 0) __ir_seek__(amount, __seek_whence__(whence)) end - alias_method :__ir_sysseek__, :sysseek + alias :__ir_sysseek__ :sysseek private :__ir_sysseek__ def sysseek(amount, whence = 0) @@ -12137,7 +12327,7 @@ def readbyte # ungetc takes a String as well as a codepoint, and a codepoint is a character # in the stream's external encoding rather than a single byte. The built-in only # ever pushed one byte back. - alias_method :__ir_ungetc__, :ungetc + alias :__ir_ungetc__ :ungetc private :__ir_ungetc__ def ungetc(value) @@ -12190,7 +12380,7 @@ def each_codepoint self end unless method_defined?(:each_codepoint) - alias_method :codepoints, :each_codepoint unless method_defined?(:codepoints) + alias :codepoints :each_codepoint unless method_defined?(:codepoints) # ---- descriptor flags --------------------------------------------------- # @@ -12216,7 +12406,7 @@ def close_on_exec? # because the built-in binmode throws on one. Fall back to the flag that # the binmode above records. if method_defined?(:binmode?) - alias_method :__ir_binmode_p__, :binmode? + alias :__ir_binmode_p__ :binmode? private :__ir_binmode_p__ end @@ -12277,7 +12467,7 @@ def path end end unless method_defined?(:path) - alias_method :to_path, :path unless method_defined?(:to_path) + alias :to_path :path unless method_defined?(:to_path) # Positional read/write, done by saving and restoring the file position since # there is no pread/pwrite underneath. @@ -12633,8 +12823,8 @@ def default_proc=(proc) end unless method_defined?(:default_proc=) # Hash#merge/#merge! take any number of hashes since 2.6 (zero included). - alias_method :__merge_one__, :merge - alias_method :__update_one__, :merge! + alias :__merge_one__ :merge + alias :__update_one__ :merge! def merge(*others, &block) result = dup @@ -12647,16 +12837,16 @@ def merge!(*others, &block) others.each { |other| __update_one__(other, &block) } self end - alias_method :update, :merge! + alias :update :merge! # CRuby's #filter/#filter! are genuine aliases of #select/#select!, and # Hash#select is not Enumerable#select, so re-alias them here. - alias_method :filter, :select - alias_method :filter!, :select! + alias :filter :select + alias :filter! :select! # #delete_if / #keep_if / #reject / #reject! return an Enumerator when called # without a block; the 1.9 core raised LocalJumpError instead. - alias_method :__delete_if_block__, :delete_if + alias :__delete_if_block__ :delete_if def delete_if(&block) raise FrozenError.new("can't modify frozen Hash: #{inspect}", receiver: self) if frozen? && block return to_enum(:delete_if) unless block @@ -12669,7 +12859,7 @@ def keep_if(&block) delete_if { |k, v| !block.call(k, v) } end - alias_method :__reject_bang_block__, :reject! + alias :__reject_bang_block__ :reject! def reject!(&block) return to_enum(:reject!) unless block raise FrozenError.new("can't modify frozen Hash: #{inspect}", receiver: self) if frozen? @@ -12974,7 +13164,7 @@ def values_at(*args) end # #each / #each_pair / #select return an Enumerator when no block is given. - alias_method :__struct_each__, :each + alias :__struct_each__ :each def each(&block) return to_enum(:each) unless block __struct_each__(&block) @@ -12989,13 +13179,13 @@ def each_pair(&block) self end - alias_method :deconstruct, :to_a - alias_method :filter, :select + alias :deconstruct :to_a + alias :filter :select end # Struct.new(..., keyword_init: true) (2.5) and StructClass#keyword_init? (3.1). class << Struct - alias_method :__struct_new__, :new + alias :__struct_new__ :new def new(*args, &block) keyword_init = nil @@ -13027,9 +13217,10 @@ def new(*args, &block) end class Struct - alias_method :__struct_initialize__, :initialize - - def initialize(*args) + # Struct#initialize is RubyStructOps.Initialize: it stores one positional value per member + # itself and hands every other call here, the arguments in one array. __struct_initialize__ + # is the plain store. + def __struct_initialize_slow__(args) # self.class.members, not members: a struct may have a member called "members" klass = self.class names = klass.members @@ -13070,7 +13261,7 @@ def initialize(*args) __struct_initialize__(*values) end end - private :initialize + private :__struct_initialize_slow__ end # -------------------------------------------------------------------------- @@ -13094,7 +13285,7 @@ class Exception # locations either, which is MRI's answer as well. The one case where there are real # Locations to begin with is `raise Klass, message, caller_locations`: set_backtrace # was handed them, so they are kept as they are. - alias_method :__core_set_backtrace__, :set_backtrace + alias :__core_set_backtrace__ :set_backtrace def set_backtrace(value) if ::Array === value && !value.empty? && @@ -13318,7 +13509,7 @@ def include?(value) each { |x| return true if x == value } false end - alias_method :member?, :include? + alias :member? :include? def __linear__(x) x.is_a?(::Numeric) || x.is_a?(::Time) @@ -13395,7 +13586,7 @@ def __empty_range__(r) # and MRI searches those too - bisecting the interval rather than the # integers in it. if method_defined?(:bsearch) - alias_method :__ir_bsearch__, :bsearch + alias :__ir_bsearch__ :bsearch private :__ir_bsearch__ end @@ -13730,7 +13921,7 @@ def to_a raise RangeError, "cannot convert endless range to an array" if self.end.nil? super end - alias_method :entries, :to_a + alias :entries :to_a end # Rebind ARGF to a real instance so ARGF.class is a class. Done at the very end diff --git a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb index 1fa2ad915..4a5f827d8 100644 --- a/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb +++ b/Src/StdLib/ruby/4.0/rubygems/defaults/ironruby.rb @@ -535,8 +535,15 @@ def build_extensions # :nodoc: # # Prepended rather than reopened: rubygems/installer.rb is loaded long after # this file and would overwrite a plain definition. - - class Installer; end + # + # And prepended only once that file has created the class (const_added below), + # however it gets there - Gem's autoload or one of the plain requires of + # rubygems/installer scattered through RubyGems and Bundler. Reopening + # Gem::Installer here would trigger the autoload at boot, and with it the + # whole installer stack - package, the tar readers, the extension builders, + # security and the openssl it requires - which is a third of the cost of + # `require "rubygems"`, and which CRuby does not load until something + # actually installs a gem. module IronRubyNoExtensionBuild # :nodoc: def build_extensions @@ -548,7 +555,13 @@ def build_extensions end end - Installer.prepend(IronRubyNoExtensionBuild) + def self.const_added(name) # :nodoc: + super + # Registering an autoload fires this too; only a real class is prepended to. + Installer.prepend(IronRubyNoExtensionBuild) if name == :Installer && !autoload?(:Installer) + end + + Installer.prepend(IronRubyNoExtensionBuild) if const_defined?(:Installer, false) && !autoload?(:Installer) class BasicSpecification; end diff --git a/Util/bench/bench.rb b/Util/bench/bench.rb index 1ba573ad3..abe23affe 100644 --- a/Util/bench/bench.rb +++ b/Util/bench/bench.rb @@ -324,6 +324,29 @@ def o.sing; 1; end while i < m; f.resume; i += 1; end end +# Enumerator#next over an Array, over a generator block, and Fiber creation. +bench("enum_next_array") do + a = Array.new(1_000) { |k| k } + e = a.each + i = 0; m = n(100_000) + while i < m + e.next + i += 1 + e.rewind if i % 1_000 == 0 + end +end + +bench("enum_next_gen") do + e = Enumerator.new { |y| k = 0; loop { y << k; k += 1 } } + i = 0; m = n(50_000) + while i < m; e.next; i += 1; end +end + +bench("fiber_new") do + i = 0; m = n(5_000) + while i < m; Fiber.new { i }.resume; i += 1; end +end + bench("proc_new_call") do i = 0; m = n(500_000) while i < m; (proc { |x| x }).call(i); i += 1; end @@ -391,6 +414,100 @@ def ack(mm, k) end end +bench("map_loop") do + a = (0...1000).to_a; r = n(2_000) + r.times { a.map { |x| x + 1 } } +end + +bench("select_loop") do + a = (0...1000).to_a; r = n(2_000) + r.times { a.select { |x| x > 500 } } +end + +bench("any_loop") do + a = (0...1000).to_a; r = n(2_000) + r.times { a.any? { |x| x > 5000 } } +end + +class CallTarget + def yield_upto(k) + i = 0 + while i < k; yield i; i += 1; end + end +end + +bench("yield_loop") do + o = CallTarget.new; s = 0; r = n(2_000) + r.times { o.yield_upto(1000) { |x| s += x } } +end + +# A tokenizer's inner loop: StringScanner scan/skip with Regexps and Strings, group access. +bench("strscan_tokens") do + require "strscan" + src = (%q({"name": "Team \\"A\\"", "score": [1, 22.5, -3e2], "ok": true, "x": null}, ) * 200).b + r = n(60) + r.times do + s = StringScanner.new(src) + until s.eos? + if s.skip(/\s+/) then next + elsif s.scan(/"((?:[^"\\]|\\.)*)"/n) then s[1] + elsif s.scan(/-?\d+(\.\d+)?([eE][-+]?\d+)?/) then (s[1] || s[2]) ? Float(s[0]) : Integer(s[0]) + elsif s.skip("true") || s.skip("null") then nil + else s.getch + end + end + end +end + +# a literal block handed to a library method that takes a BlockParam (ruby-bench send_cfunc_block) +bench("block_literal_cfunc") do + a = []; i = 0; m = n(2_000_000) + while i < m; a.reverse_each { }; i += 1; end +end + +class NewTarget + def initialize(left = nil, right = nil) + @left = left; @right = right; @i = 0; @j = 0 + end +end + +bench("object_new_init") do + i = 0; m = n(2_000_000); x = nil + while i < m; x = NewTarget.new(x, nil); x = nil if i % 64 == 0; i += 1; end +end + +# ---------------- keyword parameters ---------------- + +class KwPoint + def initialize(x:, y: 0) + @x = x + @y = y + end +end + +bench("new_kwargs") do + i = 0; m = n(1_000_000) + while i < m; KwPoint.new(x: i, y: 2); i += 1; end +end + +# ---------------- single-call blocks whose body makes a call ---------------- + +class YieldOnce + def one(x); yield x; end +end + +# a Ruby method yielding once to a new literal block (block scope reuse must cost it nothing) +bench("yield_once_call") do + o = YieldOnce.new; i = 0; m = n(2_000_000) + while i < m; o.one(i) { |y| y * 2 }; i += 1; end +end + +# a library method calling a new literal block once +bench("then_call") do + i = 0; m = n(2_000_000) + while i < m; i.then { |y| y * 2 }; i += 1; end +end + name = ARGV[0] unless BENCH.key?(name) if name == "--list" diff --git a/Util/block-scope-reuse-blockpass.rb b/Util/block-scope-reuse-blockpass.rb new file mode 100644 index 000000000..3089a7df1 --- /dev/null +++ b/Util/block-scope-reuse-blockpass.rb @@ -0,0 +1,128 @@ +# Differential checks for block scope reuse together with literal-block procs (block-pass): +# break/next/return through reused scopes, scopes kept on the BlockParam (in its break-target +# slot), separate yields of one block (a BlockParam each), proc identity, escaped procs and bindings, +# threads and fibers. Every block body here makes calls, so it takes the reuse path; the output +# must match CRuby, with and without -X:NoJIT -X:NoOSR: +# +# ruby Util/block-scope-reuse-blockpass.rb > /tmp/cruby.txt +# ./ir.sh Util/block-scope-reuse-blockpass.rb > /tmp/ir.txt +# diff /tmp/cruby.txt /tmp/ir.txt + +def show(label, v) = puts("#{label}: #{v.inspect}") +# block-pass interactions with block scope reuse: every body makes calls +def loop_yield(n) i = 0; r = []; while i < n; r << yield(i); i += 1; end; r; end +def two_yields; [yield(1), yield(2)]; end +def three_yields; [yield(1), yield(2), yield(3)]; end + +show :next_mid, loop_yield(6) { |x| y = x * 2; next(-y) if x == 2; y.to_s } +show :break_mid, loop_yield(6) { |x| y = x * 3; break [:brk, y] if x == 3; y } +show :break_then_again, 3.times.map { |k| loop_yield(5) { |x| z = x + k; break z if x == k + 1; z.to_s } } +def ret_from_block; loop_yield(5) { |x| w = x.succ; return [:ret, w] if x == 2; w }; :no; end +show :return_through, ret_from_block +show :two_yields, two_yields { |x| a = x * 10; a.to_s } +show :three_yields, three_yields { |x| b = defined?(b).inspect; b = x; [b, x.succ] } +show :locals_nil, loop_yield(4) { |x| q = (q.nil? ? :fresh : q); q = x; [q, x.pred] } + +# proc identity / escaped procs +def keep(&b) b end +pr = keep { |x| v = x * 2; v + 1 } +show :pr_identity, [pr.equal?(keep(&pr)), pr.lambda?, 5.times.map { |i| pr.call(i) }] +procs = []; loop_yield(4) { |x| t = x * 2; procs << proc { [x, t] } if x.odd?; t } +show :escaped, procs.map(&:call) +bs = []; 4.times { |x| m = x.to_s; bs << binding if x == 2; m } +show :binding_mid, bs.map { |b| [b.local_variable_get(:m), b.local_variable_get(:x)] } +pr2 = proc { |x| s = x.to_s; break s * 2 if x == 3; s } +show :proc_break, (begin; 5.times.map { |i| pr2.call(i) }; rescue LocalJumpError => e; [e.class, e.reason]; end) +l = lambda { |x| u = x * 2; return u if x > 1; u.succ } +show :lambda_return, 4.times.map { |i| l.call(i) } +f = proc { |n| t = n * 1; n == 0 ? 0 : t + f.call(n - 1) } +show :recursion, [f.call(10), f.call(5), f.call(20)] +class DM; define_method(:dm) { |x| k = x * 2; k.to_s }; define_method(:rec) { |n| j = n; n == 0 ? [] : [j] + rec(n - 1) }; end +show :define_method, [5.times.map { |i| DM.new.dm(i) }, DM.new.rec(4)] +# then/tap/synchronize single-call library blocks, repeated +mu = Mutex.new +show :then_tap, 4.times.map { |i| [i.then { |y| y * 2 }, i.tap { |y| y * 3 }, mu.synchronize { z = i.succ; z.to_s }] } +show :then_break, 3.times.map { |i| i.then { |y| break y * 100 if y == 1; y.to_s } } +# threads: one proc called on several threads, one blockparam per thread +pt = proc { |x| a = x * 2; Thread.pass; [a, x] } +show :threads, 4.times.map { |t| Thread.new { 300.times.map { |i| pt.call(i + t)[0] }.sum } }.map(&:value) +show :thread_each, 3.times.map { |t| Thread.new { r = 0; 2000.times { |i| c = i + t; r += c } ; r } }.map(&:value) +# fibers: each driven externally, the same block across fiber switches +e = [1, 2, 3, 4].each_with_index.lazy.map { |x, i| s = x * i; s.to_s } +show :lazy, e.first(3) +en = Enumerator.new { |y| 5.times { |i| w = i * i; y << w } } +show :ext_next, [en.next, en.next, en.peek, en.next, en.take(2)] +fb = Fiber.new { 3.times { |i| v = i * 7; Fiber.yield v }; :done } +show :fiber, 4.times.map { fb.resume } +fs = 2.times.map { |k| Fiber.new { loop_yield(3) { |x| Fiber.yield(x + k); x * 10 } } } +show :fibers_interleaved, 4.times.flat_map { fs.map { |f| f.alive? ? f.resume : nil } } +# exception through a reused scope, then reuse again +show :raise_mid, loop_yield(5) { |x| begin; g = x * 2; raise "e#{g}" if x == 2; g; rescue => ex; ex.message; end } +show :raise_out, (loop_yield(5) { |x| h = x; raise ArgumentError, h.to_s if x == 3; h } rescue $!.message) +show :catch_throw, catch(:t) { loop_yield(5) { |x| c = x * 4; throw :t, c if x == 2; c } } +# block with &blk passed further (block-pass) in a loop +def pass_on(&b) [1, 2, 3].map(&b) end +show :pass_on, 3.times.map { |k| pass_on { |x| d = x + k; d.to_s } } +def yield_pass(&b) loop_yield(3, &b) end +show :yield_pass, yield_pass { |x| e2 = x * 5; e2 } +# $~ and $_ scoping in reused scopes +show :match, loop_yield(4) { |x| m = "a#{x}b" =~ /\d/; [m, $~ && $~[0]] } +show :match_nil, loop_yield(3) { |x| x.even? ? ("q" =~ /z/) : 1; $~.inspect } +# local_variables / eval in reused scopes +show :eval, loop_yield(3) { |x| eval("ev = x * 2; ev") + (defined?(ev) ? 1 : 0) } +show :lv, loop_yield(2) { |x| r1 = local_variables.sort; nn = 1; [r1, local_variables.sort] } +def four(x) [yield(x), yield(x + 1), yield(x + 2), yield(x + 3)]; end +def five_sep(x) a = yield(x); b = yield(x + 1); c = yield(x + 2); d = yield(x + 3); e = yield(x + 4); [a, b, c, d, e]; end +# separate yields of one block: each yield has a BlockParam of its own +show :four, four(1) { |y| q = defined?(q).inspect; z = y * 2; [q, z] } +show :five_sep, five_sep(1) { |y| w = (w || :nil); w2 = y; [w, w2.to_s] } +bnd = [] +show :five_bind, five_sep(10) { |y| t = y * 3; bnd << binding if y == 12; t } +show :five_bind_vals, bnd.map { |b| [b.local_variable_get(:t), b.local_variable_get(:y)] } +pr = proc { |x| y = x * 2; $b = binding if x == 3; y } +show :pr_calls, 6.times.map { |i| pr.call(i) } +show :pr_binding_kept, [$b.local_variable_get(:y), $b.local_variable_get(:x)] +pq = proc { |x| u = x; u } +6.times { |i| pq.call(i) } +show :pr_threads, 3.times.map { |t| Thread.new { 50.times.map { |i| pq.call(i * t) }.sum } }.map(&:value) +show :pr_after_threads, 4.times.map { |i| pq.call(i) } +# breaks/returns after a scope was parked on the BlockParam +show :break_late, loop_yield(8) { |x| v = x * 5; break [:late, v] if x == 5; v } +def ret_late; loop_yield(8) { |x| v = x.succ; return [:ret_late, v] if x == 6; v }; :none; end +show :ret_late, ret_late +show :lib_break_late, (1..10).each { |x| y = x * x; break y if x == 7 } +show :lib_next_late, (1..6).map { |x| y = x * x; next -y if x > 3; y } +show :lib_after_break, 3.times.map { (1..10).each { |x| y = x; break y if x == 4 } } +# break target vs parked scope: the break from a later call targets the right frame +def outer; [1, 2, 3].each { |a| loop_yield(5) { |x| c = x + a; break c if x == 3 } }; :done; end +show :nested_break, outer +r = [] +def inner_break; [1, 2].map { |k| loop_yield(6) { |x| m = x * k; break m if x == 4; m } }; end +show :inner_break, inner_break +# a lambda yielded repeatedly by a library method, returns +lam = lambda { |x| l2 = x * 2; return l2 if x > 2; -l2 } +show :lambda_each, [1, 2, 3, 4].map(&lam) +# exceptions after parking leave the scope behind, then reuse resumes +show :raise_late, loop_yield(7) { |x| begin; e = x; raise "x" if x == 4; e; rescue; :resc; end } +show :raise_through, (begin; loop_yield(7) { |x| e = x; raise "out#{e}" if x == 5; e }; rescue => ex; ex.message; end) +show :after_raise, loop_yield(4) { |x| e = x * 2; e } +# recursion through one BlockParam (each_with_object with recursive lambda) +fib = proc { |n| a = n; a < 2 ? a : fib.call(a - 1) + fib.call(a - 2) } +show :fib, (0..12).map { |i| fib.call(i) } +def walk(t, &b) t.is_a?(Array) ? t.map { |c| walk(c, &b) } : b.call(t) end +show :walk, walk([1, [2, [3, 4]], 5, [6]]) { |v| s = v * 10; s } +# Enumerator over a reused block via external next across fibers +en = (1..8).each_slice(3) +show :ext, [en.next, en.next, en.next, (en.next rescue :stop)] +e2 = Enumerator.new { |y| i = 0; loop { k = i * 2; y << k; i += 1 } } +show :ext2, [e2.next, e2.next, e2.next, e2.take(4)] +# $~ in reused scopes across calls +show :match_reuse, loop_yield(5) { |x| ("ab#{x}" =~ /b(\d)/) && $1 } +show :match_after, loop_yield(4) { |x| x.even? ? ("z" =~ /q/) : ("q#{x}" =~ /q(.)/); $~ && $1 } +# ObjectSpace / trace_var / TracePoint capturing late +tps = [] +tp = TracePoint.new(:b_call) { |t| tps << t.binding.local_variables.size if tps.size < 3 } +loop_yield(3) { |x| y = x; y } +tp.enable { loop_yield(3) { |x| y = x; y } } +show :tracepoint, tps +show :after_tp, loop_yield(4) { |x| g = x; local_variables.size + g } diff --git a/Util/block-scope-reuse-matrix.rb b/Util/block-scope-reuse-matrix.rb new file mode 100644 index 000000000..6e7943d9a --- /dev/null +++ b/Util/block-scope-reuse-matrix.rb @@ -0,0 +1,375 @@ +# Differential checks for block scope reuse. +# +# A block whose body needs its RubyScope but defines no closure reuses the scope (and its locals +# tuple) of the previous call through the same BlockParam, unless something captured the scope: +# a Binding, a Proc or method defined in it, a nested scope (eval, class body), a TracePoint +# event. Every block body below makes calls, so it takes that path; the output must match CRuby: +# +# ruby Util/block-scope-reuse-matrix.rb > /tmp/cruby.txt +# ./ir.sh Util/block-scope-reuse-matrix.rb > /tmp/ir.txt +# diff /tmp/cruby.txt /tmp/ir.txt +# +# (also with -X:NoJIT -X:NoOSR, which keeps the blocks in the interpreter) + +# Differential checks for block scope reuse (every block body here makes calls) +# Differential checks for block scope reuse (every block body here makes calls) +bs = [] +[1, 2, 3].each { |x| y = x * 2; bs << binding } +p bs.map { |b| b.local_variable_get(:y) } +p bs.map { |b| b.local_variable_get(:x) } + +r = [] +[1, 2, 3].each { |x| r << eval("defined?(zz)").inspect; r << eval("zz = x * 10; zz") } +p r + +r = [] +[1, 2].each { |x| r << local_variables.inspect; w = x.succ; r << local_variables.inspect } +p r + +r = [] +[1, 2, 3].each { |x| y ||= x.succ; r << y } +p r + +procs = [] +[1, 2, 3].each { |x| y = x * 3; procs << -> { y } } +p procs.map(&:call) + +ps = [] +[1, 2, 3].each { |x| y = x + 100; ps << eval("proc { y }") } +p ps.map(&:call) + +ps = [] +[1, 2, 3].each { |x| y = x.to_s; ps << binding.local_variable_get(:y) } +p ps + +class DefIn + [1, 2].each { |x| define_method("m#{x}") { x } } + [3, 4].each { |x| y = x * 2; class_eval "def k#{x}; #{y}; end" } +end +p [DefIn.new.m1, DefIn.new.m2, DefIn.new.k3, DefIn.new.k4] + +$b = [] +tp = TracePoint.new(:line) { |t| $b << t.binding if t.path == __FILE__ } +tp.enable +[5, 6].each { |x| + q = x * 7 + q.to_s +} +tp.disable +p $b.map { |b| b.local_variable_defined?(:q) ? b.local_variable_get(:q) : :undef } + +r = [] +%w[a1 b2 c3].each { |s| s =~ /\d/; r << $~[0] } +p r, $~ && $~[0] + +def m_given; r = []; [1, 2].each { |x| r << [block_given?, __method__, x.succ] }; r; end +p m_given, m_given {} + +r = [] +[1, 2, 3, 4].each { |x| next if x.even?; r << x * 2 } +p r +p [1, 2, 3, 4].each { |x| break x * 100 if x == 3; x.succ } + +r = [] +cnt = 0 +[1, 2].each { |x| cnt += 1; r << x * 2; redo if cnt == 1 } +p r + +r = [] +[1, 2, 3].each { |x| + begin + raise ArgumentError, "x#{x}" if x.odd? + r << x.succ + rescue => e + r << e.message + end +} +p r + +p Thread.new { [1, 2, 3].map { |x| x * 2 } }.value + +def tw; yield 1; yield 2; yield 3; end +r = [] +tw { |x| r << x * 2 } +p r + +def rec(n, &b); return if n == 0; yield n; rec(n - 1, &b); end +r = [] +rec(4) { |x| r << x * 3 } +p r + +fib = Hash.new { |h, n| h[n] = n < 2 ? n : h[n - 1] + h[n - 2] } +p fib[40] + +e = [1, 2, 3].each +r = [] +loop { v = e.next; r << v * 2 } +p r +p [1, 2, 3].map.with_index { |x, i| x * i } +p [1, 2, 3].each_slice(2).map { |a| a.sum } + +o = Object.new +r = [] +[1, 2].each { |x| o.instance_exec(x) { |y| @v = y * 2 }; r << o.instance_variable_get(:@v) } +p r + +class Dm + define_method(:dm) { |x| x * 2 + 1 } +end +p (1..3).map { |i| Dm.new.dm(i) } + +p [3, 1, 2].sort { |a, b| b <=> a } +p [1, 2, 3].inject { |s, x| s + x * 2 } +p catch(:t) { [1, 2, 3].each { |x| throw :t, x * 9 if x == 2 } } + +r = [] +[1, 2].each { |x| r << self.to_s; r << x.frozen? } +p r + +r = [] +[[1, 2], [3, 4]].each { |a, b| c = a * b; r << c } +p r + +r = [] +[1, 2].each { |x| [10, 20].each { |y| r << x * y } } +p r + +# a binding taken in iteration 1, iteration 2 must not overwrite it +bb = nil +[1, 2].each { |x| z = x * 5; bb ||= binding } +p bb.local_variable_get(:z) + +# lambda with calls +l = ->(x) { x * 2 } +p (1..3).map { |i| l.(i) } +p (1..3).map(&l) + +# Symbol#to_proc inside a block +p [[1, 2], [3]].map { |a| a.map(&:to_s) } + +# eval with binding from the block, then reuse +bnd = [] +[1, 2].each { |x| bnd << binding if x == 1; y = x.succ } +p bnd[0].local_variable_get(:y) + +# raise inside, caller info +r = [] +[1, 2].each { |x| r << caller(0).first[/in .*/] } +p r.uniq.size + +# instance_variable / global in body +$g = 0 +[1, 2, 3].each { |x| $g += x * 2; @iv = $g } +p $g, @iv + +# Proc#call reuse +pr = proc { |x| x * 2 + (x > 1 ? 1 : 0) } +p (1..4).map { |i| pr.call(i) } + +r = [] +[1, 2].each { |x| q = x * 4; r << send(:binding).local_variable_get(:q) } +p r + +def each2; i = 0; while i < 3; yield i; i += 1; end; end +r = [] +each2 { |x| r << x * 2 } +p r +def walk(n, &b); i = 0; while i < 2; yield n; walk(n - 1, &b) if n > 0; i += 1; end; end +r = [] +walk(2) { |x| r << x * 10 } +p r +# block via yield in loop capturing binding on some iterations +bs = [] +each2 { |x| y = x * 3; bs << binding if x != 1 } +p bs.map { |b| b.local_variable_get(:y) } +# external enumeration over a yielding method +e = to_enum(:each2) +p [e.next * 2, e.next * 2, e.next * 2] +gen = Enumerator.new { |y| [1, 2, 3].each { |x| y << x * 5 } } +p gen.next, gen.next, gen.to_a +# lazy +p (1..Float::INFINITY).lazy.map { |x| x * 2 }.select { |x| x % 3 == 0 }.first(3) +# zip/each_with_index etc. +p [1, 2, 3].each_with_index.map { |x, i| x * i } +p [1, 2, 3].each_with_object([]) { |x, a| a << x * 2 } +# fibers +f = Fiber.new { [1, 2, 3].each { |x| Fiber.yield x * 7 }; :done } +p [f.resume, f.resume, f.resume, f.resume] +# threads using same proc +pr = proc { |x| x * 2 } +p 4.times.map { |i| Thread.new { (1..1000).map(&pr).sum } }.map(&:value) +# block params with defaults/splat/kw +p [[1, 2], [3, 4]].map { |a, b = 5, *c| [a * 1, b, c] } +p [1, 2].map { |x, k: 3| x * k } +# numbered/it params +p [1, 2, 3].map { _1 * 2 }, [1, 2, 3].map { it * 3 } +# block-local vars +y = 10 +p [1, 2].map { |x; y| y = x * 2; y }, y +# $_ in block +r = [] +["a\n", "b\n"].each { |l| $_ = l; r << $_.chomp.upcase } +p r +# self switching +class K; def run; [1, 2].map { |x| self.class.name + x.to_s }; end; end +p K.new.run +p 3.times.map { |i| instance_exec(i) { |j| j * 2 } } +# __method__ / block_given? in define_method +class K2; define_method(:z) { [1, 2].map { |x| [__method__, x * 2] } }; end +p K2.new.z +# eval of local after previous iteration's eval +r = [] +[1, 2].each { |x| r << (eval("defined?(qq)") || :none); eval("qq = 1"); } +p r +# ObjectSpace / GC +p [1, 2].map { |x| GC.start if x == 1; x * 2 } +# exception backtrace in reused scope +def raiser(x) = raise("e#{x}") +p [1, 2].map { |x| begin; raiser(x); rescue => e; e.backtrace[1][/block.*/]; end } +# local_variable_set via binding then next iteration +r = [] +[1, 2].each { |x| b = binding; b.local_variable_set(:nn, x * 2); r << b.local_variable_get(:nn) } +p r +# proc from method(:...) in block +p [1, 2].map { |x| method(:Integer).call(x.to_s) * 2 } +# retry inside block +cnt = 0 +p [1, 2].map { |x| begin; cnt += 1; raise "x" if cnt == 1; x * 2; rescue; retry; end } + +# locals of a reused scope start out nil again (in a method: its own locals) +def locals_part + r = [] + [1, 2, 3].each { |x| z = (z || 0) + x; r << z } + p r + r = [] + [1, 2, 3].each { |x| if x.odd?; w = x * 2; end; r << w } + p r + r = [] + [1, 2, 3].each { |x| a = (a || []) << x.succ; b ||= x; r << [a, b] } + p r + r = [] + [1, 2, 3].each { |x| a1 = x; a2 = a1 + 1; a3 = a2 + 1; a4 = a3 + 1; a5 = a4 + 1; a6 = a5 + 1; a7 = a6 + 1; a8 = a7 + 1; a9 = (a9 || 0) + a8; r << a9 } + p r + r = [] + [[1, 2], [3, 4]].each { |a, (b, c)| d = (d || 10) + a.to_i; r << [a, b, c, d] } + p r + r = [] + [1, 2].each { |x, y = ((y || 5) + 1)| r << [x, y.inspect] } + p r + r = [] + [1, 2, 3].each_with_index { |x, i| t = (t.to_i) + x * i; r << t } + p r + r = [] + [1, 2].each { |x| i = 0; while i < 2; k = (k || 0) + 1; i += 1; end; r << k } + p r + cnt = 0 + r = [] + [1, 2].each { |x| q = (q || 0) + 1; cnt += 1; r << q; redo if cnt == 1 } + p r + r = [] + h = {a: 1, b: 2} + h.each { |k, v| s = (s || "") + k.to_s * v; r << s } + p r + r = [] + 3.times { |i| case i when 0 then m = :zero when 2 then r << m.inspect end; r << m.inspect } + p r + r = [] + [1, 2].each { |x| begin; e = (e || 0) + x; raise "x" if x == 1; rescue; f = (f || 0) + 1; end; r << [e, f] } + p r +end +locals_part + +# one proc called through many BlockParams (each call makes its own scope) +class ReuseD + define_method(:dbl) { |x| y = x * 2; y + (z ||= 0) } + define_method(:bnd) { |x| y = x * 3; binding } +end +class ReuseE + include Enumerable + def initialize(a); @a = a; end + def each; @a.each { |x| yield x }; self; end +end + +def proc_part + # one proc, called through many BlockParams + pr = proc { |x| y = x * 2; Thread.pass if x % 97 == 0; y + 1 } + p 4.times.map { |t| Thread.new { s = 0; 20000.times { |i| s += pr.call(i + t) }; s } }.map(&:value) + + fact = proc { |n| n <= 1 ? 1 : n * fact.call(n - 1) } + p fact.call(25) + + d = ReuseD.new + p (1..5).map { |i| d.dbl(i) } + bs = (1..3).map { |i| d.bnd(i) } + p bs.map { |b| b.local_variable_get(:y) } + + l = ->(x) { t = x.to_s; t * 2 } + p (1..3).map { |i| l.(i) }, (1..3).map(&l) + + pr = proc { |k| to_s + k.to_s } + p [1, 2, 3].map { |i| i.instance_exec(i * 10, &pr) } + + $bs = [] + pr2 = proc { |n| v = n * 7; $bs << binding if n.odd?; pr2.call(n - 1) if n > 0; v } + p pr2.call(4), $bs.map { |b| b.local_variable_get(:v) } + + pr3 = proc { |x| a = x; Fiber.yield a if x == 1; a * 10 } + f = Fiber.new { pr3.call(1) } + p f.resume + p pr3.call(2), pr3.call(3) + p f.resume + + e = ReuseE.new([3, 1, 2]) + p e.map { |x| x * 2 }, e.select { |x| x.odd? }, e.sort_by { |x| -x }, e.inject { |s, x| s + x } + r = [] + e.each { |x| w = (w || 0) + x; r << w } + p r + p e.each_slice(2).map { |a| a.sum } + + # a proc whose call raises: its scope is not kept, the next call works + pr4 = proc { |x| q = x.to_s; raise "boom" if x == 2; q * 2 } + p [1, 2, 3].map { |i| begin; pr4.call(i); rescue => ex; ex.message; end } + + # break / next out of a proc-called block + p [1, 2, 3, 4].each { |x| y = x * 2; break y if x == 3 } + pr5 = proc { |x| next x * 100 if x.even?; x.to_s } + p (1..4).map { |i| pr5.call(i) } + + # same proc used as block of different methods + blk = proc { |x| x.to_s * 2 } + p [1, 2].map(&blk), { a: 1 }.map(&blk), (1..2).each_with_object([]) { |x, acc| acc << blk.call(x) } + + # lambda with nested return + lr = ->(x) { [1, 2].each { |y| return y * x if y == 2 }; :none } + p lr.(5), lr.(6) + + # a proc that captures its scope only on some calls + $keep = [] + pk = proc { |x| zz = x * 9; $keep << binding if x == 2; zz } + p (1..4).map { |i| pk.call(i) }, $keep.map { |b| b.local_variable_get(:zz) } +end +proc_part + +# procs over a block's scope made without a block literal in it: eval, Method#to_proc, +# Symbol#to_proc, instance_eval/class_eval strings, a define_method'd method's binding +def capture_part + r = [] + [1, 2, 3].each { |x| y = x * 4; r << eval("proc { [x, y] }") } + p r.map(&:call) + r = [] + [1, 2, 3].each { |x| y = x * 5; r << [method(:p).to_proc, y] } + p r.map { |m, y| [m.lambda?, y] } + r = [] + [1, 2, 3].each { |x| y = x.to_s; r << [y.method(:upcase).to_proc, :succ.to_proc, y] } + p r.map { |up, su, y| [up.call, su.call(y), y] } + r = [] + %w[a b c].each { |s| t = s * 2; r << s.instance_eval("proc { [self, t] }") } + p r.map(&:call) + r = [] + [1, 2, 3].each { |x| w = x * 6; r << Object.class_eval("lambda { w + x }") } + p r.map(&:call) + r = [] + [1, 2, 3].each { |x| v = x * 7; r << eval("binding") } + p r.map { |b| [b.local_variable_get(:x), b.local_variable_get(:v)] } +end +capture_part diff --git a/Util/block-scope-reuse-race.rb b/Util/block-scope-reuse-race.rb new file mode 100644 index 000000000..e837f22bf --- /dev/null +++ b/Util/block-scope-reuse-race.rb @@ -0,0 +1,101 @@ +# Regression checks for block scope reuse against re-entrant calls of the same block from a +# Signal.trap handler or an ObjectSpace finalizer. Both run at a safe point - a block entry is +# one - so they can start a call of a block while another call of it is being entered. A scope +# and its locals tuple must only ever be reused together: a call must never see the locals of +# another call, nor share its tuple with a Binding that another call captured. +# +# ruby Util/block-scope-reuse-race.rb +# ./ir.sh Util/block-scope-reuse-race.rb (also with -X:NoJIT -X:NoOSR) +# +# Every line must end in "stale 0 leaked 0" (CRuby prints the same); the exit status is 1 otherwise. +# ROUNDS=n scales the work (CRuby runs a trap handler only at its next thread switch: fewer rounds). + +ROUNDS = (ENV['ROUNDS'] || (RUBY_ENGINE == 'ruby' ? 20 : 200)).to_i +$failed = false +def report(name, stale, leaked = 0) + $failed = true unless stale == 0 && leaked == 0 + puts "#{name}: stale #{stale} leaked #{leaked}" +end + +# Fires a trap handler (or, with :gc, a finalizer) while the block runs over and over, and returns +# whether the handler ran. +def interrupt_while(kind) + $hit = false + th = Thread.new do + if kind == :gc + ObjectSpace.define_finalizer(Object.new, FIN) + GC.start + else + sleep 0.002 + Process.kill(:USR1, Process.pid) + end + end + t0 = Time.now + yield until $hit || Time.now - t0 > 2 + th.join + $hit +end + +# a local of the block that every call reads before assigning it: always nil +PR = proc { |m| z = 0 if false; r = z; z = m; $b = binding if m == :cap; r } +5.times { PR.call(1) } + +class Svc + define_method(:step) { |m| seen = nil if false; prev = seen; seen = m; raise "fail" if m == :h; prev } +end +SVC = Svc.new +5.times { SVC.step(1) } + +$stale = 0 +EACH = proc { |m| w = 0 if false; r = w; w = m; $b = binding if m == :cap; $stale += 1 unless r.nil?; r } +ARR = [1, 2, 3, 4, 5, 6, 7, 8] +ARR.each(&EACH) + +FIN = proc { |_| $hit = true; PR.call(:cap) } + +if Signal.list.key?('USR1') + # Proc#call: the handler's call captures its scope in a Binding + Signal.trap(:USR1) { $hit = true; PR.call(:cap) } + stale = leaked = 0 + ROUNDS.times do + $b = nil + interrupt_while(:signal) { stale += 1 unless PR.call(2).nil? } + next unless $b + 4.times { PR.call(3) } + leaked += 1 unless $b.local_variable_get(:z) == :cap + end + report :trap_proc_call, stale, leaked + + # define_method: the handler's call raises after assigning a local + Signal.trap(:USR1) { $hit = true; SVC.step(:h) rescue nil } + stale = 0 + ROUNDS.times { interrupt_while(:signal) { v = SVC.step(2); stale += 1 unless v.nil? } } + report :trap_define_method, stale + + # each: repeated calls through one BlockParam, interrupted by calls of the same block + Signal.trap(:USR1) { $hit = true; EACH.call(:cap) } + $stale = 0 + leaked = 0 + ROUNDS.times do + $b = nil + interrupt_while(:signal) { ARR.each(&EACH) } + next unless $b + ARR.each(&EACH) + leaked += 1 unless $b.local_variable_get(:w) == :cap + end + report :trap_each, $stale, leaked + Signal.trap(:USR1, 'DEFAULT') +end + +# a finalizer calling the proc +stale = leaked = 0 +(ROUNDS / 2).times do + $b = nil + interrupt_while(:gc) { stale += 1 unless PR.call(2).nil? } + next unless $b + 4.times { PR.call(3) } + leaked += 1 unless $b.local_variable_get(:z) == :cap +end +report :finalizer_proc_call, stale, leaked + +exit 1 if $failed diff --git a/Util/jit-matrix.rb b/Util/jit-matrix.rb index 1075b08c6..babf18632 100644 --- a/Util/jit-matrix.rb +++ b/Util/jit-matrix.rb @@ -400,6 +400,212 @@ def no_osr_return(n) t('return of a splat') { no_splat_return(4) } t('return from inside a hot loop (OSR)') { [no_osr_return(100000), no_osr_return(10)] } +# ---------------------------------------------------------------- 8b. keyword parameters + +# Like t, but an ArgumentError's message is part of the answer: a keyword method's entry hands +# every call the prologue would complain about to the generic body, and the wording of the +# complaint is what shows that it did. (The few probes that use t instead are arity errors +# IronRuby words differently from CRuby with the JIT off as well: MRI appends the required +# keywords to them.) +def kt(label) + print label.ljust(56), ' => ' + begin + v = yield + puts "#{v.inspect} (#{v.class})" + rescue ArgumentError => e + puts "ArgumentError: #{e.message}" + rescue Exception => e + puts e.class + end +end + +def sp_kw_add(left:, right:) + left + right +end + +def sp_kw_opt(a: 1, b: 2.5) + a * b +end + +def sp_kw_mixed(x, k:, j: 10) + x - k * j +end + +def sp_kw_obj(a, k: nil) + k +end + +def sp_kw_ret(n:, m: 0) + return 0 if n < m + n * 2 +end + +def sp_kw_assign(a:, b: 1) + a = a + b + a * 2 +end + +def no_kw_rest(a:, **rest) + a + rest.size +end + +def no_kw_default_expr(a:, b: a + 1) + a + b +end + +def no_kw_default_string(a:, b: "s") + a +end + +puts +puts '--- 8b. keyword parameters ---' +warm { |i| sp_kw_add(left: i, right: 1) } +kt('kw add') { sp_kw_add(left: 2, right: 3) } +kt('kw add, keywords in the other order') { sp_kw_add(right: 3, left: 20) } +kt('kw add Int32 max + 1') { sp_kw_add(left: 2**31 - 1, right: 1) } +kt('kw add Int64 max + 1') { sp_kw_add(left: 2**63 - 1, right: 1) } +kt('kw add Float') { sp_kw_add(left: 1.5, right: 1) } +kt('kw add String') { sp_kw_add(left: 'a', right: 'b') } +kt('kw add nil') { sp_kw_add(left: nil, right: 1) } +kt('kw add missing one') { sp_kw_add(left: 1) } +kt('kw add missing both') { sp_kw_add } +kt('kw add unknown') { sp_kw_add(left: 1, right: 2, up: 3) } +kt('kw add two unknown') { sp_kw_add(left: 1, right: 2, up: 3, down: 4) } +kt('kw add missing and unknown') { sp_kw_add(left: 1, up: 3) } +kt('kw add positional Hash') { sp_kw_add({ left: 1, right: 2 }) rescue $!.class } +kt('kw add String key') { sp_kw_add(left: 1, right: 2, 'x' => 3) } +kt('kw add **hash') { h = { left: 4, right: 5 }; [sp_kw_add(**h), h] } +kt('kw add **hash + literal') { h = { left: 4 }; [sp_kw_add(**h, right: 6), h] } +kt('kw add **{} extra') { sp_kw_add(left: 1, right: 2, **{}) } +kt('kw add **hash with a default proc') { h = Hash.new { 99 }; h[:left] = 1; sp_kw_add(**h, right: 2) } +kt('kw add via send') { send(:sp_kw_add, left: 7, right: 8) } +kt('kw add via Method#call') { method(:sp_kw_add).call(left: 7, right: 9) } +kt('kw add via public_send splat') { a = [{ left: 1, right: 1 }]; sp_kw_add(*a) rescue $!.class } +warm { |i| sp_kw_add(left: i * 0.5, right: 0.25) } +kt('kw add re-profiled on Float') { sp_kw_add(left: 1.5, right: 0.25) } +kt('kw add re-profiled on Float, Integer') { sp_kw_add(left: 1, right: 2) } + +warm { |i| sp_kw_opt(a: i) } +kt('kw opt none') { sp_kw_opt } +kt('kw opt a') { sp_kw_opt(a: 4) } +kt('kw opt b') { sp_kw_opt(b: 3) } +kt('kw opt both') { sp_kw_opt(b: 2, a: 7) } +kt('kw opt a Float') { sp_kw_opt(a: 0.5) } +kt('kw opt a nil') { sp_kw_opt(a: nil) } +kt('kw opt unknown') { sp_kw_opt(c: 1) } +kt('kw opt positional') { sp_kw_opt(1) } +kt('kw opt positional Hash') { sp_kw_opt({ a: 1 }) } +kt('kw opt nil') { sp_kw_opt(nil) } +warm { |i| sp_kw_opt } +kt('kw opt hot without keywords') { sp_kw_opt } +kt('kw opt hot without keywords, given one') { sp_kw_opt(b: 4) } + +warm { |i| sp_kw_mixed(i, k: 2) } +kt('kw mixed') { sp_kw_mixed(5, k: 1) } +kt('kw mixed with optional') { sp_kw_mixed(5, k: 1, j: 2) } +t('kw mixed missing positional') { sp_kw_mixed(k: 1) } +kt('kw mixed missing keyword') { sp_kw_mixed(5) } +t('kw mixed extra positional') { sp_kw_mixed(5, 6, k: 1) } +t('kw mixed positional Hash') { sp_kw_mixed(5, { k: 1 }) } +t('kw mixed nil for the keywords') { sp_kw_mixed(5, nil) } +kt('kw mixed overflow') { sp_kw_mixed(-2**62, k: 2**62, j: 2) } + +warm { |i| sp_kw_obj('x', k: i) } +kt('kw obj') { sp_kw_obj('y', k: 5) } +kt('kw obj no keyword') { sp_kw_obj('y') } +kt('kw obj string-keyed hash') { sp_kw_obj('a' => 1) } +kt('kw obj hash and keyword') { sp_kw_obj({ 'a' => 1 }, k: 2) } + +warm { |i| sp_kw_ret(n: i - 60) } +kt('kw return') { [sp_kw_ret(n: -1), sp_kw_ret(n: 4), sp_kw_ret(n: 4, m: 5)] } +warm { |i| sp_kw_assign(a: i) } +kt('kw local reassigned') { [sp_kw_assign(a: 3), sp_kw_assign(a: 3, b: 4)] } + +warm { |i| no_kw_rest(a: i, z: 1) } +kt('kw **rest') { [no_kw_rest(a: 1), no_kw_rest(a: 1, b: 2, c: 3)] } +warm { |i| no_kw_default_expr(a: i) } +kt('kw default reads a parameter') { [no_kw_default_expr(a: 1), no_kw_default_expr(a: 1, b: 5)] } +warm { |i| no_kw_default_string(a: i) } +kt('kw String default') { no_kw_default_string(a: 1) } + +# A keyword initializer: an instance variable store is a typed body too. +class KwIvars + def initialize(x:, y: 0) + @x = x + @y = y + end + + def pair + [@x, @y] + end +end +class KwIvarsSub < KwIvars + def initialize(x:, z: 5) + super(x: x, y: z) + end +end + +warm { |i| KwIvars.new(x: i) } +kt('kw initialize') { KwIvars.new(x: 1, y: 2).pair } +kt('kw initialize with the default') { KwIvars.new(x: 1).pair } +kt('kw initialize with Float and String') { KwIvars.new(x: 1.5, y: 'b').pair } +kt('kw initialize missing') { KwIvars.new(y: 1) } +kt('kw initialize unknown') { KwIvars.new(x: 1, w: 1) } +kt('kw initialize of a frozen object') { o = KwIvars.new(x: 1).freeze; o.send(:initialize, x: 2) } +kt('kw initialize via super') { KwIvarsSub.new(x: 3).pair } + +# A receiverless keyword call goes straight into the specialized body when it can +# (JitKeywordCall): what it resolves to has to follow the receiver's class. +class KwCaller + def sp_kw_scale(x, by: 2) + x * by + end + private :sp_kw_scale + + def go(x) + sp_kw_scale(x, by: 3) + end + + def go_missing(x) + sp_kw_not_here(x, by: 3) + end + + def method_missing(name, *args, **kw) + [name, args, kw] + end +end +class KwCallerSub < KwCaller + def sp_kw_scale(x, by: 2) + [:sub, x, by] + end +end + +kc = KwCaller.new +warm { |i| kc.go(i) } +kt('kw call into a private method') { kc.go(5) } +kt('kw call, a Float') { kc.go(0.5) } +kt('kw call, overflow') { kc.go(2**62) } +kt('kw call, a String') { kc.go('ab') } +kt('kw call, overridden in a subclass') { KwCallerSub.new.go(5) } +kt('kw call, the superclass again') { kc.go(5) } +kt('kw call to method_missing') { kc.go_missing(1) } +def kc.sp_kw_scale(x, by: 2) + :singleton +end +kt('kw call, a singleton method') { kc.go(5) } +kt('kw call, another instance') { KwCaller.new.go(5) } + +def sp_kw_add(left:, right:) + left - right +end +kt('kw add redefined') { sp_kw_add(left: 2, right: 3) } +warm { |i| sp_kw_add(left: i, right: 1) } +kt('kw add redefined, hot again') { sp_kw_add(left: 2, right: 3) } +def sp_kw_add(left:, right: 100) + left + right +end +kt('kw add redefined with a default') { sp_kw_add(left: 2) } + # ---------------------------------------------------------------- 8. Integer#+ redefined def sp_plus(a, b)