diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart index bf10199e342..61a549d7c80 100644 --- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart +++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart @@ -248,6 +248,29 @@ class LibraryBundleCompiler implements old.Compiler { } } +/// Tracks the state of which branch the compiler is on for hot reload checks. +/// +/// This allows hot reload generation checks to be batched into a single +/// branch statement. +/// +/// This batched branching allows us to avoid exponential behavior when +/// recursing on deeply nested checked calls. Otherwise each branch makes 2 +/// copies of all the sub-branches leading to 2^n checks. +enum HotReloadBranchState { + /// The compiler is not along any hot reload check branch yet. + none, + + /// The compiler is along the branch with no extra checks. The check rewrite + /// logic will be skipped and the normal call will be generated. The root of + /// this branch will include a hot reload generation check. + uncheckedBranch, + + /// The compiler is along the branch with extra checks. The check rewrite + /// logic will be applied for every call that needs it. The root of this + /// branch will include a hot reload generation check. + checkedBranch, +} + class LibraryCompiler extends ComputeOnceConstantVisitor with OnceConstantVisitorDefaultMixin implements @@ -256,6 +279,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor final Options _options; final SymbolData _symbolData; + HotReloadBranchState hotReloadCheckedBranch = HotReloadBranchState.none; + /// Maps each `Class` node compiled in the module to the `Identifier`s used to /// name the class in JavaScript. /// @@ -5207,7 +5232,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor // the sub-expressions will have the correct mapping applied. return jsExpression.toStatement()..sourceInformation = continueSourceMap; } - return _visitExpression(expr).toStatement(); + return jsExpression.toStatement(); } @override @@ -6176,10 +6201,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor // Since there are no arguments (unlike methods) the dynamic get path can // be reused for the hot reload checks on a getter. var checkedGet = _emitCast( - _emitDynamicGet( - _visitExpression(receiver), - _emitMemberName(memberName), - ), + _emitDynamicGet(jsReceiver, _emitMemberName(memberName)), node.resultType, )..sourceInformation = _nodeStart(node); return _emitHotReloadSafeInvocation(instanceGet, checkedGet); @@ -6527,11 +6549,13 @@ class LibraryCompiler extends ComputeOnceConstantVisitor } var receiver = node.receiver; var jsReceiver = _visitExpression(receiver); - var jsArguments = _emitArgumentList(node.arguments, target: target); if (node.isNativeListInvariantAddInvocation(_coreTypes.listClass)) { // TODO(nshahan): If this code is retained, can it become invalid after a // hot reload? - return js.call('#.push(#)', [jsReceiver, jsArguments]); + return js.call('#.push(#)', [ + jsReceiver, + _emitArgumentList(node.arguments, target: target), + ]); } var name = node.name.text; if (name == 'call') { @@ -6548,12 +6572,23 @@ class LibraryCompiler extends ComputeOnceConstantVisitor // names of the Object instance members. // TODO(nshahan): What should be checked after a hot reload. I think only // the return type of the NSM can change. - return _runtimeCall('#(#, #)', [name, jsReceiver, jsArguments]); + return _runtimeCall('#(#, #)', [ + name, + jsReceiver, + _emitArgumentList(node.arguments, target: target), + ]); } // Otherwise generate this as a normal typed method call. var jsName = _emitMemberName(name, member: target); - var invocation = js.call('#.#(#)', [jsReceiver, jsName, jsArguments]); - if (_shouldRewriteInvocationWithHotReloadChecks(target)) { + + js_ast.Expression generateCall() { + var args = _emitArgumentList(node.arguments, target: target); + return js.call('#.#(#)', [jsReceiver, jsName, args]); + } + + // Only consider checks if we're not in the unchecked branch. + if (_shouldRewriteInvocationWithHotReloadChecks(target) && + hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) { var checkedInvocation = _rewriteInvocationWithHotReloadChecks( jsReceiver, jsName, @@ -6562,10 +6597,25 @@ class LibraryCompiler extends ComputeOnceConstantVisitor node.getStaticType(_staticTypeContext), _nodeStart(node), ); + + // If we're within the checked branch (i.e. not at the root) then return + // the checked call as-is. + if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) { + return checkedInvocation; + } + + // We're at the root of the branch so we need to generate the unchecked + // branch as well. + hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch; + final invocation = generateCall(); + hotReloadCheckedBranch = HotReloadBranchState.none; + // As an optimization, avoid extra checks when the invocation code was // compiled in the same generation that it is running. return _emitHotReloadSafeInvocation(invocation, checkedInvocation); } + + final invocation = generateCall(); return _isNullCheckableJsInterop(node.interfaceTarget) ? _wrapWithJsInteropNullCheck(invocation) : invocation; @@ -7641,12 +7691,17 @@ class LibraryCompiler extends ComputeOnceConstantVisitor } } - var fn = _emitStaticTarget(target); - var args = _emitArgumentList(node.arguments, target: target); - var staticCall = js_ast.Call(fn, args) - ..sourceInformation = _nodeStart(node); - if (_shouldRewriteInvocationWithHotReloadChecks(target)) { - var checkedCall = _rewriteInvocationWithHotReloadChecks( + js_ast.Call generateCall(js_ast.PropertyAccess fn) { + var args = _emitArgumentList(node.arguments, target: target); + return js_ast.Call(fn, args)..sourceInformation = _nodeStart(node); + } + + // Only consider checks if we're not in the unchecked branch. + if (_shouldRewriteInvocationWithHotReloadChecks(target) && + hotReloadCheckedBranch != HotReloadBranchState.uncheckedBranch) { + final fn = _emitStaticTarget(target); + + var checkedInvocation = _rewriteInvocationWithHotReloadChecks( fn.receiver, fn.selector, target, @@ -7654,10 +7709,25 @@ class LibraryCompiler extends ComputeOnceConstantVisitor node.getStaticType(_staticTypeContext), _nodeStart(node), ); + + // If we're within the checked branch (i.e. not at the root) then return + // the checked call as-is. + if (hotReloadCheckedBranch == HotReloadBranchState.checkedBranch) { + return checkedInvocation; + } + + // We're at the root of the branch so we need to generate the unchecked + // branch as well. + hotReloadCheckedBranch = HotReloadBranchState.uncheckedBranch; + final invocation = generateCall(fn); + hotReloadCheckedBranch = HotReloadBranchState.none; + // As an optimization, avoid extra checks when the invocation code was // compiled in the same generation that it is running. - return _emitHotReloadSafeInvocation(staticCall, checkedCall); + return _emitHotReloadSafeInvocation(invocation, checkedInvocation); } + + final staticCall = generateCall(_emitStaticTarget(target)); return _isNullCheckableJsInterop(target) ? _wrapWithJsInteropNullCheck(staticCall) : staticCall; @@ -7713,6 +7783,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor DartType expectedReturnType, SourceLocation? originalCallSiteSourceLocation, ) { + final savedCheckedBranch = hotReloadCheckedBranch; + hotReloadCheckedBranch = HotReloadBranchState.checkedBranch; var hoistedPositionalVariables = []; var hoistedNamedVariables = {}; js_ast.Expression? letAssignments; @@ -7799,7 +7871,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor ])..sourceInformation = originalCallSiteSourceLocation; // Cast the result of the checked call or the value returned from a // `NoSuchMethod` invocation. - return js_ast.Binary( + final result = js_ast.Binary( ',', letAssignments, js.call('# == # ? # : #', [ @@ -7809,6 +7881,9 @@ class LibraryCompiler extends ComputeOnceConstantVisitor _emitCast(checkResult, expectedReturnType), ]), ); + + hotReloadCheckedBranch = savedCheckedBranch; + return result; } js_ast.Expression _emitJSObjectGetPrototypeOf( diff --git a/tests/web/nested_closure_invocations_test.dart b/tests/web/nested_closure_invocations_test.dart new file mode 100644 index 00000000000..573120d2460 --- /dev/null +++ b/tests/web/nested_closure_invocations_test.dart @@ -0,0 +1,225 @@ +// 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. + +abstract class Either { + Either(); + + B fold(B ifLeft(L l), B ifRight(R r)); + + static Either map20< + L, + A, + A2 extends A, + B, + B2 extends B, + C, + C2 extends C, + D, + D2 extends D, + E, + E2 extends E, + F, + F2 extends F, + G, + G2 extends G, + H, + H2 extends H, + I, + I2 extends I, + J, + J2 extends J, + K, + K2 extends K, + LL, + LL2 extends LL, + M, + M2 extends M, + N, + N2 extends N, + O, + O2 extends O, + P, + P2 extends P, + Q, + Q2 extends Q, + R, + R2 extends R, + S, + S2 extends S, + T, + T2 extends T, + U + >( + Either fa, + Either fb, + Either fc, + Either fd, + Either fe, + Either ff, + Either fg, + Either fh, + Either fi, + Either fj, + Either fk, + Either fl, + Either fm, + Either fn, + Either fo, + Either fp, + Either fq, + Either fr, + Either fs, + Either ft, + U fun( + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + LL l, + M m, + N n, + O o, + P p, + Q q, + R r, + S s, + T t, + ), + ) => fa.fold( + left, + (a) => fb.fold( + left, + (b) => fc.fold( + left, + (c) => fd.fold( + left, + (d) => fe.fold( + left, + (e) => ff.fold( + left, + (f) => fg.fold( + left, + (g) => fh.fold( + left, + (h) => fi.fold( + left, + (i) => fj.fold( + left, + (j) => fk.fold( + left, + (k) => fl.fold( + left, + (l) => fm.fold( + left, + (m) => fn.fold( + left, + (n) => fo.fold( + left, + (o) => fp.fold( + left, + (p) => fq.fold( + left, + (q) => fr.fold( + left, + (r) => fs.fold( + left, + (s) => ft.fold( + left, + (t) => right( + fun( + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + q, + r, + s, + t, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); +} + +Either left(L l) => new Left(l); +Either right(R r) => new Right(r); + +class Left extends Either { + final L _l; + + B fold(B ifLeft(L l), B ifRight(R r)) => ifLeft(_l); + + Left(this._l); +} + +class Right extends Either { + final R _r; + + B fold(B ifLeft(L l), B ifRight(R r)) => ifRight(_r); + + Right(this._r); +} + +void main() { + Either.map20( + left(0), + left(1), + left(2), + left(3), + left(4), + left(5), + left(6), + left(7), + left(8), + left(9), + left(10), + left(11), + left(12), + left(13), + left(14), + left(15), + left(16), + left(17), + left(18), + left(19), + (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) => 20, + ); +}