diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart index 56a9deac1e8..4c4e4e9cb48 100644 --- a/pkg/dev_compiler/lib/src/kernel/compiler.dart +++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart @@ -5180,7 +5180,6 @@ class ProgramCompiler extends ComputeOnceConstantVisitor js_ast.Expression _emitSuperPropertyGet(Member target) { if (_reifyTearoff(target)) { if (_superAllowed) { - var jsTarget = _emitSuperTarget(target); var jsName = _declareMemberName(target); var enclosingClass = target.enclosingClass!; js_ast.Expression? supertypeReference = @@ -5197,9 +5196,9 @@ class ProgramCompiler extends ComputeOnceConstantVisitor } } return _runtimeCall( - 'bind(this, #, #, #)', [supertypeReference, jsName, jsTarget]); + 'superTearoff(this, #, #)', [supertypeReference, jsName]); } else { - return _emitSuperTearoff(target); + return _emitSuperTearoffFromDisallowedContext(target); } } return _emitSuperTarget(target); @@ -5896,7 +5895,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor // // NOTE: This is intended to help in the cases of calling a `super` getter, // setter, or method. For the case of tearing off a `super` method in - // contexts where `super` isn't allowed, see [_emitSuperTearoff]. + // contexts where `super` isn't allowed, see + // [_emitSuperTearoffFromDisallowedContext]. var name = member.name.text; var getter = (member is Field && !setter) || (member is Procedure && member.isGetter); @@ -5964,17 +5964,15 @@ class ProgramCompiler extends ComputeOnceConstantVisitor /// This method assumes `super` is not allowed in the current context. // TODO(nshahan) Replace with a kernel transform and synthetic method filters // for devtools. - js_ast.Expression _emitSuperTearoff(Member member) { + js_ast.Expression _emitSuperTearoffFromDisallowedContext(Member member) { var jsName = _declareMemberName(member); var name = '_#super#tearOff#${member.name.text}'; var jsMethod = _superHelpers.putIfAbsent(name, () { - var superclass = member.enclosingClass?.superclass; - var supertypeReference = superclass == null - ? js_ast.LiteralNull() - : _mixinSuperclassCache[member.enclosingClass!] ?? - _emitTopLevelNameNoExternalInterop(superclass); + var superclass = member.enclosingClass!; + var supertypeReference = _mixinSuperclassCache[superclass] ?? + _emitTopLevelNameNoExternalInterop(superclass); var jsReturnValue = _runtimeCall( - 'bind(this, #, #, super[#])', [supertypeReference, jsName, jsName]); + 'superTearoff(this, #, #)', [supertypeReference, jsName]); var fn = js.fun('function() { return #; }', [jsReturnValue]); name = js_ast.friendlyNameForDartOperator[name] ?? name; return js_ast.Method(_emitScopedId(name), fn); diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart index 23441fb2722..69441333d2f 100644 --- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart +++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart @@ -5630,7 +5630,6 @@ class LibraryCompiler extends ComputeOnceConstantVisitor js_ast.Expression _emitSuperPropertyGet(Member target) { if (_reifyTearoff(target)) { if (_superAllowed) { - var jsTarget = _emitSuperTarget(target); var jsName = _declareMemberName(target); var enclosingClass = target.enclosingClass!; js_ast.Expression? supertypeReference = @@ -5647,9 +5646,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor } } return _runtimeCall( - 'bind(this, #, #, #)', [supertypeReference, jsName, jsTarget]); + 'superTearoff(this, #, #)', [supertypeReference, jsName]); } else { - return _emitSuperTearoff(target); + return _emitSuperTearoffFromDisallowedContext(target); } } return _emitSuperTarget(target); @@ -6345,7 +6344,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor // // NOTE: This is intended to help in the cases of calling a `super` getter, // setter, or method. For the case of tearing off a `super` method in - // contexts where `super` isn't allowed, see [_emitSuperTearoff]. + // contexts where `super` isn't allowed, see + // [_emitSuperTearoffFromDisallowedContext]. var name = member.name.text; var getter = (member is Field && !setter) || (member is Procedure && member.isGetter); @@ -6413,17 +6413,17 @@ class LibraryCompiler extends ComputeOnceConstantVisitor /// This method assumes `super` is not allowed in the current context. // TODO(nshahan) Replace with a kernel transform and synthetic method filters // for devtools. - js_ast.Expression _emitSuperTearoff(Member member) { + js_ast.Expression _emitSuperTearoffFromDisallowedContext(Member member) { var jsName = _declareMemberName(member); var name = '_#super#tearOff#${member.name.text}'; var jsMethod = _superHelpers.putIfAbsent(name, () { - var superclass = member.enclosingClass?.superclass; - var supertypeReference = superclass == null - ? js_ast.LiteralNull() - : _mixinSuperclassCache[member.enclosingClass!] ?? - _emitTopLevelNameNoExternalInterop(superclass); - var jsReturnValue = _runtimeCall( - 'bind(this, #, #, super[#])', [supertypeReference, jsName, jsName]); + var superclass = member.enclosingClass!; + var supertypeReference = _mixinSuperclassCache[superclass] ?? + _emitTopLevelNameNoExternalInterop(superclass); + var jsReturnValue = _runtimeCall('superTearoff(this, #, #)', [ + supertypeReference, + jsName, + ]); var fn = js.fun('function() { return #; }', [jsReturnValue]); name = js_ast.friendlyNameForDartOperator[name] ?? name; return js_ast.Method(_emitScopedId(name), fn); diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart index bdb3f6309e2..9482efa187e 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart @@ -97,21 +97,25 @@ staticTearoff(context, String immediateMethodTargetLabel, property) { /// Constructs a new tearoff, on `context[property]`. Tearoffs are represented /// as a closure that resolves its underlying member late. /// -/// [immediateMethodTargetLabel] uniquely identifies the class from which this -/// method is torn off. Static tearoffs provide this at tearoff time. This is -/// directly provided for static tearoffs. If null (such as in dynamic/instance -/// tearoffs), we resolve this via this tearoff's method signature. +/// [immediateMethodTargetOrLabel] is either 'null', a method label string, or +/// an object that resolves to a method label (via [getMethodImmediateTarget]). +/// A method label uniquely identifies the class from which this method is +/// torn off. Static tearoffs pass in a label when the tearoff is created. If +/// null (such as in dynamic/instance tearoffs), we resolve the label via this +/// tearoff's method signature. /// /// Note: We do not canonicalize instance tearoffs to be consistent with /// Dart2JS, but we should update this if the spec changes. See #3612. -tearoff(context, String? immediateMethodTargetLabel, property) { +@notNull +Object tearoff( + Object? context, + Object? immediateMethodTargetOrLabel, + @notNull Object property, +) { if (context == null) context = jsNull; property = _canonicalMember(context, property); - var tear = JS('', '(...args) => #[#](...args)', context, property); + var tear = JS('!', '(...args) => #[#](...args)', context, property); var rtiName = JS_GET_NAME(JsGetName.SIGNATURE_NAME); - // Type-resolving members on tearoffs must be resolved late. Static tearoffs - // are tagged with their RTIs ahead of time. Runtime/instance tearoffs must - // access them through `getMethodType` and `getMethodDefaultTypeArgs`. defineAccessor( tear, rtiName, @@ -122,6 +126,88 @@ tearoff(context, String? immediateMethodTargetLabel, property) { configurable: true, enumerable: false, ); + defineAccessor( + tear, + '_boundMethodTarget', + get: () { + if (JS('', '# == null', immediateMethodTargetOrLabel)) { + return getMethodImmediateTarget(context, null, property); + } + if (JS('', 'typeof # == "string"', immediateMethodTargetOrLabel)) { + return JS('!', '#', immediateMethodTargetOrLabel); + } + return getMethodImmediateTarget( + context, + immediateMethodTargetOrLabel, + property, + ); + }, + configurable: true, + enumerable: false, + ); + JS('', '#._boundObject = #', tear, context); + return _finishTearoff(tear, context, property); +} + +/// Constructs a tearoff on `super.property` from [context]. +/// +/// [context] is the object whose super member is being torn off. +/// [superclass] is the class definition at the point in [context]'s hierarchy +/// where [property] should be torn off. +/// [property] is the property (string name or symbol) used to access the +/// member being torn off. +@notNull +Object superTearoff( + @notNull Object context, + @notNull Object superclass, + @notNull Object property, +) { + var superContext = JS('!', '#.prototype', superclass); + property = _canonicalMember(superContext, property); + var tear = JS( + '!', + '(...args) => #[#].bind(#)(...args)', + superContext, + property, + context, + ); + var rtiName = JS_GET_NAME(JsGetName.SIGNATURE_NAME); + defineAccessor( + tear, + rtiName, + get: () { + var existingRti = JS('', '#[#][#]', context, property, rtiName); + return existingRti ?? getMethodType(context, property); + }, + configurable: true, + enumerable: false, + ); + defineAccessor( + tear, + '_boundMethodTarget', + get: () { + return getMethodImmediateTarget(superContext, superclass, property); + }, + configurable: true, + enumerable: false, + ); + JS('', '#._boundObject = #', tear, context); + return _finishTearoff(tear, superContext, property); +} + +/// Appends hidden members to a tearoff required for correctness. +/// +/// Does not append '_boundMethodTarget' and '_boundObject', as these have +/// special handling logic. +@notNull +Object _finishTearoff( + @notNull Object tear, + Object? context, + @notNull Object property, +) { + // Type-resolving members on tearoffs must be resolved late. Static tearoffs + // are tagged with their RTIs ahead of time. Runtime/instance tearoffs must + // access them through `getMethodType` and `getMethodDefaultTypeArgs`. defineAccessor( tear, '_defaultTypeArgs', @@ -148,97 +234,10 @@ tearoff(context, String? immediateMethodTargetLabel, property) { configurable: true, enumerable: false, ); - JS('', '#._boundObject = #', tear, context); JS('', '#._boundName = #', tear, stringNameForProperty(property)); - JS( - '', - '#._boundMethodTarget = #', - tear, - immediateMethodTargetLabel ?? - getMethodImmediateTarget(context, null, property), - ); return tear; } -/// Given an object and a method name, tear off the method. -/// Sets the runtime type of the torn off method appropriately, -/// and also binds the object. -/// -/// [immediateMethodTarget] is the class at the exact point in the [obj]'s -/// hierarchy where [name] is torn off. This field is only used when the -/// immediate target cannot be resolved on [obj] (such as in super tearoffs). -/// -/// If the optional `f` argument is passed in, it will be used as the method. -/// This supports cases like `super.foo` where we need to tear off the method -/// from the superclass, not from the `obj` directly. -// TODO(60297): This function currently binds super tearoffs too early. This -// should be updated to receive obj's supertype at runtime like we do for -// mixin classes. -bind(obj, immediateMethodTarget, name, method) { - if (obj == null) obj = jsNull; - var property = _canonicalMember(obj, name); - if (method == null) method = JS('', '#[#]', obj, property); - var f = JS('', '#.bind(#)', method, obj); - // TODO(jmesserly): canonicalize tearoffs. - JS('', '#._boundObject = #', f, obj); - JS('', '#._boundName = #', f, stringNameForProperty(name)); - JS('', '#._boundMethod = #', f, method); - JS( - '', - '#._boundMethodTarget = #', - f, - getMethodImmediateTarget(obj, immediateMethodTarget, property), - ); - var methodType = getMethodType(obj, property); - // Native JavaScript methods do not have Dart signatures attached that need - // to be copied. - if (methodType != null) { - if (rti.isGenericFunctionType(methodType)) { - // Attach the default type argument values to the new function in case - // they are needed for a dynamic call. - var defaultTypeArgs = getMethodDefaultTypeArgs(obj, property); - JS('', '#._defaultTypeArgs = #', f, defaultTypeArgs); - } - JS('', '#[#] = #', f, JS_GET_NAME(JsGetName.SIGNATURE_NAME), methodType); - } - return f; -} - -/// Binds the `call` method of an interface type, handling null. -/// -/// Essentially this works like `obj?.call`. It also handles the needs of -/// [dsend]/[dcall], returning `null` if no method was found with the given -/// canonical member [name]. -/// -/// [name] is typically `"call"` but it could be the [extensionSymbol] for -/// `call`, if we define it on a native type, and [obj] is known statically to -/// be a native type/interface with `call`. -bindCall(obj, name) { - if (obj == null) return null; - var ftype = getMethodType(obj, name); - if (ftype == null) return null; - var method = JS('', '#[#]', obj, name); - var f = JS('', '#.bind(#)', method, obj); - // TODO(jmesserly): canonicalize tearoffs. - JS('', '#._boundObject = #', f, obj); - JS('', '#._boundMethod = #', f, method); - JS('', '#._boundName = #', f, stringNameForProperty(name)); - JS( - '', - '#._boundMethodTarget = #', - f, - getMethodImmediateTarget(obj, obj, name), - ); - JS('', '#[#] = #', f, JS_GET_NAME(JsGetName.SIGNATURE_NAME), ftype); - if (rti.isGenericFunctionType(ftype)) { - // Attach the default type argument values to the new function in case - // they are needed for a dynamic call. - var defaultTypeArgs = getMethodDefaultTypeArgs(obj, name); - JS('', '#._defaultTypeArgs = #', f, defaultTypeArgs); - } - return f; -} - /// Instantiate a generic method. /// /// We need to apply the type arguments both to the function, as well as its @@ -540,11 +539,14 @@ _checkAndCall(f, ftype, obj, typeArgs, args, named, displayName) { // (we're now trying `call()` on `f`, so we want to call its nSM rather // than the original target's nSM). originalTarget = f; - f = bindCall(f, _canonicalMember(f, 'call')); + // Use [getMethodType] to determine if 'call' is allowed to be dynamically + // torn off on this object. + f = getMethodType(f, 'call') == null ? null : tearoff(f, null, 'call'); ftype = null; displayName = 'call'; } - if (f == null) { + if (f == null || + JS('', '#._boundObject[#._boundName] == null', f, f)) { return callNSM("Dynamic call of object has no instance method 'call'."); } } @@ -1029,12 +1031,12 @@ String _toString(obj) { /// statically known to have one attached to its prototype (null or a JavaScript /// interop value). @notNull -String Function() toStringTearoff(obj) { +Object toStringTearoff(obj) { if (obj == null) obj = jsNull; if (JS('!', '#[#] !== void 0', obj, extensionSymbol('toString'))) { // The bind helper can handle finding the toString method for null or Dart // Objects. - return bind(obj, null, extensionSymbol('toString'), null); + return tearoff(obj, null, extensionSymbol('toString')); } // Otherwise bind the native JavaScript toString method. // This differs from dart2js to provide a more useful toString at development @@ -1042,7 +1044,7 @@ String Function() toStringTearoff(obj) { // If obj does not have a native toString method this will throw but that // matches the behavior of dart2js and it would be misleading to make this // work at development time but allow it to fail in production. - return bind(obj, null, 'toString', null); + return tearoff(obj, null, 'toString'); } /// Converts to a non-null [String], equivalent to @@ -1103,26 +1105,27 @@ noSuchMethod(obj, Invocation invocation) { /// isn't statically known to have one attached to its prototype (null or a /// JavaScript interop value). @notNull -dynamic Function(Invocation) noSuchMethodTearoff(obj) { - if (obj == null) obj = jsNull; - if (JS('!', '#[#] !== void 0', obj, extensionSymbol('noSuchMethod'))) { - // The bind helper can handle finding the toString method for null or Dart - // Objects. - return bind(obj, null, extensionSymbol('noSuchMethod'), null); +Object noSuchMethodTearoff(context) { + if (context == null) context = jsNull; + if (JS( + '!', + '#[#] !== void 0', + context, + extensionSymbol('noSuchMethod'), + )) { + // The bind helper can handle finding the noSuchMethod method for null or + // Dart Objects. + return tearoff(context, null, extensionSymbol('noSuchMethod')); } - // Otherwise, manually pass the Dart Core Object noSuchMethod to the bind - // helper. - return bind( - obj, + // Otherwise, tear off the Dart Core Object's noSuchMethod. + var tear = tearoff( + JS_CLASS_REF(Object), null, - 'noSuchMethod', - JS( - '!', - '#.prototype[#]', - JS_CLASS_REF(Object), - extensionSymbol('noSuchMethod'), - ), + extensionSymbol('noSuchMethod'), ); + // Update the bound object for equality correctness. + JS('', '#._boundObject = #', tear, context); + return tear; } /// The default implementation of `noSuchMethod` to match `Object.noSuchMethod`. @@ -1167,20 +1170,26 @@ final JsIterator = JS('', ''' _canonicalMember(obj, name) { // Private names are symbols and are already canonical. - if (JS('!', 'typeof # === "symbol"', name)) return name; + if (JS('!', 'typeof # === "symbol"', name)) return name; - // 'toString' and 'noSuchMethod' use their extension symbol when available. - if (obj != null && - JS('!', '# === "toString" || # === "noSuchMethod"', name, name)) { - if (JS('!', '#[#] !== void 0', obj, extensionSymbol(name))) { + if (obj != null) { + // 'toString', 'call', and 'noSuchMethod' use their extension symbol when + // available but default to their string names. + if (JS( + '!', + '# === "toString" || # === "noSuchMethod" || # === "call"', + name, + name, + name, + ) && + JS('!', '#[#] != null', obj, extensionSymbol(name))) { + return extensionSymbol(name) ?? name; + } + if (JS('!', '#[#] != null', obj, _extensionType)) { return extensionSymbol(name); } } - if (obj != null && JS('!', '#[#] != null', obj, _extensionType)) { - return extensionSymbol(name); - } - // Check for certain names that we can't use in JS if (JS('!', '# == "constructor" || # == "prototype"', name, name)) { JS('', '# = "+" + #', name, name); diff --git a/tests/hot_reload/tear_off_generic_super_getter/main.0.dart b/tests/hot_reload/tear_off_generic_super_getter/main.0.dart new file mode 100644 index 00000000000..80b6a670c22 --- /dev/null +++ b/tests/hot_reload/tear_off_generic_super_getter/main.0.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/expect.dart'; +import 'package:reload_test/reload_test_utils.dart'; + +// Tests reload succeeds when super getter are updated. + +class Bar { + method() { + return 42; + } +} + +class Foo extends Bar { + get tearoff => super.method; + get tearoff2 => super.method; +} + +Future main() async { + var tearoff = Foo().tearoff; + var tearoff2 = Foo().tearoff2; + Expect.equals(42, tearoff()); + Expect.equals(42, tearoff2()); + await hotReload(); + + Expect.equals(100, tearoff()); + Expect.equals(100, tearoff2()); +} diff --git a/tests/hot_reload/tear_off_generic_super_getter/main.1.dart b/tests/hot_reload/tear_off_generic_super_getter/main.1.dart new file mode 100644 index 00000000000..1df7c5c63a0 --- /dev/null +++ b/tests/hot_reload/tear_off_generic_super_getter/main.1.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/expect.dart'; +import 'package:reload_test/reload_test_utils.dart'; + +// Tests reload succeeds when super getter are updated. + +class Bar { + method() { + return 100; + } +} + +class Foo extends Bar { + get tearoff => super.method; +} + +Future main() async { + var tearoff = Foo().tearoff; + Expect.equals(42, tearoff()); + await hotReload(); + + Expect.equals(100, tearoff()); +} + +/** DIFF **/ +/* + // Tests reload succeeds when super getter are updated. + + class Bar { +- method() { +- return 42; ++ method() { ++ return 100; + } + } + + class Foo extends Bar { +- get tearoff => super.method; +- get tearoff2 => super.method; ++ get tearoff => super.method; + } + + Future main() async { + var tearoff = Foo().tearoff; +- var tearoff2 = Foo().tearoff2; + Expect.equals(42, tearoff()); +- Expect.equals(42, tearoff2()); + await hotReload(); + + Expect.equals(100, tearoff()); +- Expect.equals(100, tearoff2()); + } +*/ diff --git a/tests/language/closure/identity_equality_nosuchmethod_tearoff_test.dart b/tests/language/closure/identity_equality_nosuchmethod_tearoff_test.dart new file mode 100644 index 00000000000..c1c72058977 --- /dev/null +++ b/tests/language/closure/identity_equality_nosuchmethod_tearoff_test.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/expect.dart'; + +// Test identity and equality of 'noSuchMethod' tearoffs. + +void checkIdentical(Object o1, Object o2) { + Expect.isTrue(identical(o1, o2)); + Expect.isTrue(o1 == o2); + Expect.isTrue(o2 == o1); +} + +void checkEqual(Object o1, Object o2) { + Expect.isTrue(o1 == o2); + Expect.isTrue(o2 == o1); + // The behavior of `identical` is unspecified, optimizations could + // make a difference and should be allowed: Do not expect anything. +} + +void checkUnequal(Object o1, Object o2) { + Expect.isTrue(o1 != o2); + Expect.isTrue(o2 != o1); + // We expect that `identical` is never true when `==` yields false. + Expect.isFalse(identical(o1, o2)); +} + +class CheckIdentical { + const CheckIdentical(Object o1, Object o2) : assert(identical(o1, o2)); +} + +class CheckNotIdentical { + const CheckNotIdentical(Object o1, Object o2) : assert(!identical(o1, o2)); +} + +class A { + // Enable a mixed-in method in `M` that has a superinvocation. + noSuchMethod(Invocation i) => 'A'; +} + +mixin M on A { + noSuchMethod(Invocation i) => super.noSuchMethod(i) + ' M on A'; +} + +class AM extends A with M { + Function(Invocation i) get tearoffSuperMethod => super.noSuchMethod; +} + +class AMM extends AM with M { + // Tear off the second copy of M.noSuchMethod + // (`tearoffSuperMethod` still tears off the first copy). + Function(Invocation i) get tearoffSuperMethodSecond => super.noSuchMethod; + // In this case, `super.` should not make a difference. + Function(Invocation i) get tearoffSuperMethodSecondNoSuper => noSuchMethod; +} + +void main() { + var amm = AMM(); + Function(Invocation i) vMixedInSuperMethod1 = amm.tearoffSuperMethod; + Function(Invocation i) vMixedInSuperMethod2 = amm.tearoffSuperMethod; + Function(Invocation i) vMixedInSuperMethodSecond1 = + amm.tearoffSuperMethodSecond; + Function(Invocation i) vMixedInSuperMethodSecond2 = + amm.tearoffSuperMethodSecond; + Function(Invocation i) vMixedInSuperMethodSecondNoSuper1 = + amm.tearoffSuperMethodSecondNoSuper; + Function(Invocation i) vMixedInSuperMethodSecondNoSuper2 = + amm.tearoffSuperMethodSecondNoSuper; + + checkEqual(amm.tearoffSuperMethod, amm.tearoffSuperMethod); + checkEqual(vMixedInSuperMethod1, vMixedInSuperMethod2); + checkEqual(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethodSecond); + checkEqual(vMixedInSuperMethodSecond1, vMixedInSuperMethodSecond2); + checkUnequal(amm.tearoffSuperMethod, amm.tearoffSuperMethodSecond); + checkUnequal(vMixedInSuperMethod1, vMixedInSuperMethodSecond2); + checkUnequal(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethod); + checkUnequal(vMixedInSuperMethodSecond1, vMixedInSuperMethod2); + + checkEqual( + amm.tearoffSuperMethodSecondNoSuper, + amm.tearoffSuperMethodSecondNoSuper, + ); + checkEqual( + vMixedInSuperMethodSecondNoSuper1, + vMixedInSuperMethodSecondNoSuper2, + ); + checkUnequal(amm.tearoffSuperMethod, amm.tearoffSuperMethodSecondNoSuper); + checkUnequal(vMixedInSuperMethod1, vMixedInSuperMethodSecondNoSuper2); + checkUnequal(amm.tearoffSuperMethodSecondNoSuper, amm.tearoffSuperMethod); + checkUnequal(vMixedInSuperMethodSecondNoSuper1, vMixedInSuperMethod2); + + checkEqual(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethodSecondNoSuper); +} diff --git a/tests/language/closure/identity_equality_tostring_tearoff_test.dart b/tests/language/closure/identity_equality_tostring_tearoff_test.dart new file mode 100644 index 00000000000..1eb8e0b6f0d --- /dev/null +++ b/tests/language/closure/identity_equality_tostring_tearoff_test.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/expect.dart'; + +// Test identity and equality of 'toString' tearoffs. + +void checkIdentical(Object o1, Object o2) { + Expect.isTrue(identical(o1, o2)); + Expect.isTrue(o1 == o2); + Expect.isTrue(o2 == o1); +} + +void checkEqual(Object o1, Object o2) { + Expect.isTrue(o1 == o2); + Expect.isTrue(o2 == o1); + // The behavior of `identical` is unspecified, optimizations could + // make a difference and should be allowed: Do not expect anything. +} + +void checkUnequal(Object o1, Object o2) { + Expect.isTrue(o1 != o2); + Expect.isTrue(o2 != o1); + // We expect that `identical` is never true when `==` yields false. + Expect.isFalse(identical(o1, o2)); +} + +class CheckIdentical { + const CheckIdentical(Object o1, Object o2) : assert(identical(o1, o2)); +} + +class CheckNotIdentical { + const CheckNotIdentical(Object o1, Object o2) : assert(!identical(o1, o2)); +} + +class A { + // Enable a mixed-in method in `M` that has a superinvocation. + String toString() => 'A'; +} + +mixin M on A { + String toString() => super.toString() + ' M on A'; +} + +class AM extends A with M { + String Function() get tearoffSuperMethod => super.toString; +} + +class AMM extends AM with M { + // Tear off the second copy of M.toString + // (`tearoffSuperMethod` still tears off the first copy). + String Function() get tearoffSuperMethodSecond => super.toString; + // In this case, `super.` should not make a difference. + String Function() get tearoffSuperMethodSecondNoSuper => toString; +} + +void main() { + var amm = AMM(); + String Function() vMixedInSuperMethod1 = amm.tearoffSuperMethod; + String Function() vMixedInSuperMethod2 = amm.tearoffSuperMethod; + String Function() vMixedInSuperMethodSecond1 = amm.tearoffSuperMethodSecond; + String Function() vMixedInSuperMethodSecond2 = amm.tearoffSuperMethodSecond; + String Function() vMixedInSuperMethodSecondNoSuper1 = + amm.tearoffSuperMethodSecondNoSuper; + String Function() vMixedInSuperMethodSecondNoSuper2 = + amm.tearoffSuperMethodSecondNoSuper; + + checkEqual(amm.tearoffSuperMethod, amm.tearoffSuperMethod); + checkEqual(vMixedInSuperMethod1, vMixedInSuperMethod2); + checkEqual(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethodSecond); + checkEqual(vMixedInSuperMethodSecond1, vMixedInSuperMethodSecond2); + checkUnequal(amm.tearoffSuperMethod, amm.tearoffSuperMethodSecond); + checkUnequal(vMixedInSuperMethod1, vMixedInSuperMethodSecond2); + checkUnequal(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethod); + checkUnequal(vMixedInSuperMethodSecond1, vMixedInSuperMethod2); + + checkEqual( + amm.tearoffSuperMethodSecondNoSuper, + amm.tearoffSuperMethodSecondNoSuper, + ); + checkEqual( + vMixedInSuperMethodSecondNoSuper1, + vMixedInSuperMethodSecondNoSuper2, + ); + checkUnequal(amm.tearoffSuperMethod, amm.tearoffSuperMethodSecondNoSuper); + checkUnequal(vMixedInSuperMethod1, vMixedInSuperMethodSecondNoSuper2); + checkUnequal(amm.tearoffSuperMethodSecondNoSuper, amm.tearoffSuperMethod); + checkUnequal(vMixedInSuperMethodSecondNoSuper1, vMixedInSuperMethod2); + + checkEqual(amm.tearoffSuperMethodSecond, amm.tearoffSuperMethodSecondNoSuper); +} diff --git a/tests/language/mixin/super_tearoff_test.dart b/tests/language/mixin/super_tearoff_test.dart new file mode 100644 index 00000000000..0c529b4c9f9 --- /dev/null +++ b/tests/language/mixin/super_tearoff_test.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Checks correctness of super tearoff invocation order across complex mixin +// hierarchies. + +import "package:expect/expect.dart"; + +var superTearoffCallOrder = []; + +class C extends Super1 with M1, M2 {} + +mixin M1 on Super2 {} + +mixin M2 on Super1 {} + +class Super1 extends Super2 { + paint() { + superTearoffCallOrder.add('Super1'); + return super.paint; + } +} + +class Super2 { + paint() { + superTearoffCallOrder.add('Super2'); + return this.paint; + } +} + +main() { + var tearoff = C().paint(); + tearoff(); + Expect.listEquals(['Super1', 'Super2'], superTearoffCallOrder); +}