From db8563d4d62e19e34d6f177044191e7cc0274536 Mon Sep 17 00:00:00 2001 From: Tess Strickland Date: Mon, 9 Mar 2026 10:16:02 -0700 Subject: [PATCH] [vm,dyn_modules] Recognize the vm:invisible pragma. Adds a new isInvisible flag for both FunctionDeclarations and ClosureDeclarations and sets it if the function or closure declaration is annotated with @pragma('vm:invisible'). This way, function visibility is appropriately recorded even if options.emitAnnotations is false. The bytecode reader checks for the isInvisible flag when reading FunctionDeclarations and ClosureDeclarations and appropriately sets the is_visible flag for the Function object accordingly. TEST=pkg/dart2bytecode/test/bytecode_generator_test vm/dart/invisible_function_pragma_test Cq-Include-Trybots: luci.dart.try:vm-dyn-linux-debug-x64-try Change-Id: If435afbe5e74adc022ce064784b6b3e5e8a88164 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/486381 Commit-Queue: Tess Strickland Reviewed-by: Alexander Markov --- pkg/dart2bytecode/docs/bytecode.md | 4 +- pkg/dart2bytecode/lib/bytecode_generator.dart | 20 +- pkg/dart2bytecode/lib/declarations.dart | 8 + .../test/bytecode_generator_test.dart | 1 + pkg/dart2bytecode/testcases/invisible.dart | 48 ++++ .../testcases/invisible.dart.expect | 252 ++++++++++++++++++ pkg/vm/lib/transformations/pragma.dart | 7 + runtime/vm/bytecode_reader.cc | 4 + 8 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 pkg/dart2bytecode/testcases/invisible.dart create mode 100644 pkg/dart2bytecode/testcases/invisible.dart.expect diff --git a/pkg/dart2bytecode/docs/bytecode.md b/pkg/dart2bytecode/docs/bytecode.md index ca23d4c18d4..ccd6b1c5c14 100644 --- a/pkg/dart2bytecode/docs/bytecode.md +++ b/pkg/dart2bytecode/docs/bytecode.md @@ -557,7 +557,7 @@ type FunctionDeclaration { isAsync, isAsyncStar, isSyncStar, isNoSuchMethodForwarder, isExternal, isNative, hasSourcePositions, hasAnnotations, hasPragma, - hasCustomScript, isExtensionTypeMember); + hasCustomScript, isExtensionTypeMember, isInvisible); PackedObject name; @@ -647,7 +647,7 @@ type ClosureDeclaration { UInt flags = (hasOptionalPositionalParams, hasOptionalNamedParams, hasTypeParams, hasSourcePositions, isAsync, isAsyncStar, isSyncStar, isDebuggable, - hasParameterFlags, hasAnnotations, hasPragma) + hasParameterFlags, hasAnnotations, hasPragma, isInvisible) // Member or Closure. PackedObject parent; diff --git a/pkg/dart2bytecode/lib/bytecode_generator.dart b/pkg/dart2bytecode/lib/bytecode_generator.dart index 66592527479..2420a74eec4 100644 --- a/pkg/dart2bytecode/lib/bytecode_generator.dart +++ b/pkg/dart2bytecode/lib/bytecode_generator.dart @@ -679,6 +679,11 @@ class BytecodeGenerator extends RecursiveVisitor { flags |= FunctionDeclaration.hasAnnotationsFlag; if (annotations.hasPragma) { flags |= FunctionDeclaration.hasPragmaFlag; + if (pragmaParser + .parsedPragmas(member.annotations) + .isNotEmpty) { + flags |= FunctionDeclaration.isInvisibleFlag; + } if (pragmaParser .parsedPragmas(member.annotations) .isNotEmpty) { @@ -2621,15 +2626,20 @@ class BytecodeGenerator extends RecursiveVisitor { flags |= ClosureDeclaration.hasParameterFlagsFlag; } - final Annotations annotations = getFunctionAnnotations( - node is ast.FunctionDeclaration - ? node.variable.annotations - : const [], - function); + final List astAnnotations = node is ast.FunctionDeclaration + ? node.variable.annotations + : const []; + final Annotations annotations = + getFunctionAnnotations(astAnnotations, function); if (annotations.object != null) { flags |= ClosureDeclaration.hasAnnotationsFlag; if (annotations.hasPragma) { flags |= ClosureDeclaration.hasPragmaFlag; + if (pragmaParser + .parsedPragmas(astAnnotations) + .isNotEmpty) { + flags |= ClosureDeclaration.isInvisibleFlag; + } } } diff --git a/pkg/dart2bytecode/lib/declarations.dart b/pkg/dart2bytecode/lib/declarations.dart index 3dcf32b312e..99b2b0b5da1 100644 --- a/pkg/dart2bytecode/lib/declarations.dart +++ b/pkg/dart2bytecode/lib/declarations.dart @@ -539,6 +539,7 @@ class FunctionDeclaration { static const hasPragmaFlag = 1 << 22; static const hasCustomScriptFlag = 1 << 23; static const isExtensionTypeMemberFlag = 1 << 24; + static const isInvisibleFlag = 1 << 25; final int flags; final ObjectHandle name; @@ -724,6 +725,9 @@ class FunctionDeclaration { if ((flags & isExternalFlag) != 0) { sb.write(', external'); } + if ((flags & isInvisibleFlag) != 0) { + sb.write(', invisible'); + } if ((flags & hasPragmaFlag) != 0) { sb.write(', has-pragma'); } @@ -984,6 +988,7 @@ class ClosureDeclaration { static const hasParameterFlagsFlag = 1 << 8; static const hasAnnotationsFlag = 1 << 9; static const hasPragmaFlag = 1 << 10; + static const isInvisibleFlag = 1 << 11; int flags; final ObjectHandle parent; @@ -1122,6 +1127,9 @@ class ClosureDeclaration { String toString() { final StringBuffer sb = new StringBuffer(); sb.write('Closure $parent::$name'); + if ((flags & isInvisibleFlag) != 0) { + sb.write(' invisible'); + } if ((flags & isAsyncFlag) != 0) { sb.write(' async'); } diff --git a/pkg/dart2bytecode/test/bytecode_generator_test.dart b/pkg/dart2bytecode/test/bytecode_generator_test.dart index c81ffaaf723..66eb57ef83f 100644 --- a/pkg/dart2bytecode/test/bytecode_generator_test.dart +++ b/pkg/dart2bytecode/test/bytecode_generator_test.dart @@ -201,6 +201,7 @@ main() { 'optional_params.dart', 'bootstrapping.dart', 'ffi.dart', + 'invisible.dart', }; group('gen-bytecode-with-closure-context-lowering', () { diff --git a/pkg/dart2bytecode/testcases/invisible.dart b/pkg/dart2bytecode/testcases/invisible.dart new file mode 100644 index 00000000000..8be35b1f8e5 --- /dev/null +++ b/pkg/dart2bytecode/testcases/invisible.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2026, 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. + +class A { + A.visible(void Function() fun) { + print('A.visible'); + fun(); + } + + @pragma('vm:invisible') + A.invisible(void Function() fun) { + print('A.invisible'); + fun(); + } +} + +void visible(void Function() fun) { + print('visible()'); + fun(); +} + +@pragma('vm:invisible') +void invisible(void Function() fun) { + print('invisible()'); + fun(); +} + +void visibleClosure(void Function() fun) { + visibleInner() { + print('visibleInner'); + fun(); + } + + visibleInner(); +} + +void invisibleClosure(void Function() fun) { + @pragma('vm:invisible') + invisibleInner() { + print('invisibleInner'); + fun(); + } + + invisibleInner(); +} + +main() {} diff --git a/pkg/dart2bytecode/testcases/invisible.dart.expect b/pkg/dart2bytecode/testcases/invisible.dart.expect new file mode 100644 index 00000000000..07283fe9360 --- /dev/null +++ b/pkg/dart2bytecode/testcases/invisible.dart.expect @@ -0,0 +1,252 @@ +Bytecode +Dynamic Module Entry Point: DART_SDK/pkg/dart2bytecode/testcases/invisible.dart::main +Library 'DART_SDK/pkg/dart2bytecode/testcases/invisible.dart' + name '#lib' + script 'DART_SDK/pkg/dart2bytecode/testcases/invisible.dart' + +Class '', script = 'DART_SDK/pkg/dart2bytecode/testcases/invisible.dart' + + +Function 'visible', static, reflectable, debuggable + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type void + +Bytecode { + Entry 1 + CheckStack 0 + PushConstant CP#0 + DirectCall CP#1, 1 + Drop1 + Push FP[-5] + StoreLocal r0 + Push r0 + UncheckedClosureCall CP#3, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = ObjectRef 'visible()' + [1] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [2] = Reserved + [3] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] +} + + +Function 'invisible', static, reflectable, debuggable, invisible, has-pragma + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type void + annotations const List [const dart:core::pragma {dart:core::pragma::name (field): 'vm:invisible', dart:core::pragma::options (field): null}] + +Bytecode { + Entry 1 + CheckStack 0 + PushConstant CP#0 + DirectCall CP#1, 1 + Drop1 + Push FP[-5] + StoreLocal r0 + Push r0 + UncheckedClosureCall CP#3, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = ObjectRef 'invisible()' + [1] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [2] = Reserved + [3] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] +} + + +Function 'visibleClosure', static, reflectable, debuggable + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type void + +Bytecode { + Entry 4 + CheckStack 0 + AllocateContext 0, 1 + PopLocal r0 + Push r0 + Push FP[-5] + StoreContextVar 0, 0 + PushConstant CP#0 + Push r0 + PushNull + AllocateClosure + PopLocal r2 + Push r2 + Push r2 + UncheckedClosureCall CP#6, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = ClosureFunction 0 + [1] = InstanceField dart:core::_Closure::_context (field) + [2] = Reserved + [3] = ObjectRef 'visibleInner' + [4] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [5] = Reserved + [6] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] + [7] = EndClosureFunctionScope +} +Closure DART_SDK/pkg/dart2bytecode/testcases/invisible.dart::visibleClosure::'visibleInner' () -> Null +ClosureCode { + Entry 3 + Push FP[-5] + LoadFieldTOS CP#1 + PopLocal r0 + CheckStack 0 + PushConstant CP#3 + DirectCall CP#4, 1 + Drop1 + Push r0 + LoadContextVar 0, 0 + StoreLocal r2 + Push r2 + UncheckedClosureCall CP#6, 1 + Drop1 + PushNull + ReturnTOS +} + + +Function 'invisibleClosure', static, reflectable, debuggable + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type void + +Bytecode { + Entry 4 + CheckStack 0 + AllocateContext 0, 1 + PopLocal r0 + Push r0 + Push FP[-5] + StoreContextVar 0, 0 + PushConstant CP#0 + Push r0 + PushNull + AllocateClosure + PopLocal r2 + Push r2 + Push r2 + UncheckedClosureCall CP#6, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = ClosureFunction 0 + [1] = InstanceField dart:core::_Closure::_context (field) + [2] = Reserved + [3] = ObjectRef 'invisibleInner' + [4] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [5] = Reserved + [6] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] + [7] = EndClosureFunctionScope +} +Closure DART_SDK/pkg/dart2bytecode/testcases/invisible.dart::invisibleClosure::'invisibleInner' invisible annotations const List [const dart:core::pragma {dart:core::pragma::name (field): 'vm:invisible', dart:core::pragma::options (field): null}] + () -> Null +ClosureCode { + Entry 3 + Push FP[-5] + LoadFieldTOS CP#1 + PopLocal r0 + CheckStack 0 + PushConstant CP#3 + DirectCall CP#4, 1 + Drop1 + Push r0 + LoadContextVar 0, 0 + StoreLocal r2 + Push r2 + UncheckedClosureCall CP#6, 1 + Drop1 + PushNull + ReturnTOS +} + + +Function 'main', static, reflectable, debuggable + parameters [] (required: 0) + return-type dynamic + +Bytecode { + Entry 0 + CheckStack 0 + PushNull + ReturnTOS +} +ConstantPool { +} + +Class 'A', script = 'DART_SDK/pkg/dart2bytecode/testcases/invisible.dart' + extends dart:core::Object + + +Function 'visible', constructor, reflectable, debuggable + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type DART_SDK/pkg/dart2bytecode/testcases/invisible.dart::A + +Bytecode { + Entry 1 + CheckStack 0 + Push FP[-6] + DirectCall CP#0, 1 + Drop1 + PushConstant CP#2 + DirectCall CP#3, 1 + Drop1 + Push FP[-5] + StoreLocal r0 + Push r0 + UncheckedClosureCall CP#5, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = DirectCall 'dart:core::Object:: (constructor)', ArgDesc num-args 1, num-type-args 0, names [] + [1] = Reserved + [2] = ObjectRef 'A.visible' + [3] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [4] = Reserved + [5] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] +} + + +Function 'invisible', constructor, reflectable, debuggable, invisible, has-pragma + parameters [FunctionType () -> void 'fun'] (required: 1) + return-type DART_SDK/pkg/dart2bytecode/testcases/invisible.dart::A + annotations const List [const dart:core::pragma {dart:core::pragma::name (field): 'vm:invisible', dart:core::pragma::options (field): null}] + +Bytecode { + Entry 1 + CheckStack 0 + Push FP[-6] + DirectCall CP#0, 1 + Drop1 + PushConstant CP#2 + DirectCall CP#3, 1 + Drop1 + Push FP[-5] + StoreLocal r0 + Push r0 + UncheckedClosureCall CP#5, 1 + Drop1 + PushNull + ReturnTOS +} +ConstantPool { + [0] = DirectCall 'dart:core::Object:: (constructor)', ArgDesc num-args 1, num-type-args 0, names [] + [1] = Reserved + [2] = ObjectRef 'A.invisible' + [3] = DirectCall 'dart:core::print', ArgDesc num-args 1, num-type-args 0, names [] + [4] = Reserved + [5] = ObjectRef ArgDesc num-args 1, num-type-args 0, names [] +} + diff --git a/pkg/vm/lib/transformations/pragma.dart b/pkg/vm/lib/transformations/pragma.dart index 0108a67d651..ab67474d496 100644 --- a/pkg/vm/lib/transformations/pragma.dart +++ b/pkg/vm/lib/transformations/pragma.dart @@ -19,6 +19,7 @@ const kVmPlatformConstIfPragmaName = "vm:platform-const-if"; const kVmFfiNative = "vm:ffi:native"; const kVmSharedPragmaName = "vm:shared"; const kVmDeeplyImmutablePragmaName = "vm:deeply-immutable"; +const kVmInvisiblePragmaName = "vm:invisible"; // Pragmas recognized by dart2wasm const kWasmEntryPointPragmaName = "wasm:entry-point"; @@ -101,6 +102,10 @@ class ParsedVmDeeplyImmutablePragma implements ParsedPragma { const ParsedVmDeeplyImmutablePragma(); } +class ParsedVmInvisiblePragma implements ParsedPragma { + const ParsedVmInvisiblePragma(); +} + abstract class PragmaAnnotationParser { /// May return 'null' if the annotation does not represent a recognized /// @pragma. @@ -248,6 +253,8 @@ class ConstantPragmaAnnotationParser implements PragmaAnnotationParser { return const ParsedVmSharedPragma(); case kVmDeeplyImmutablePragmaName: return const ParsedVmDeeplyImmutablePragma(); + case kVmInvisiblePragmaName: + return const ParsedVmInvisiblePragma(); default: return null; } diff --git a/runtime/vm/bytecode_reader.cc b/runtime/vm/bytecode_reader.cc index 51536862007..dc3181f51eb 100644 --- a/runtime/vm/bytecode_reader.cc +++ b/runtime/vm/bytecode_reader.cc @@ -372,6 +372,7 @@ void BytecodeReaderHelper::ReadClosureDeclaration(const Function& function, const int kHasParameterFlagsFlag = 1 << 8; const int kHasAnnotationsFlag = 1 << 9; const int kHasPragmaFlag = 1 << 10; + const int kIsInvisibleFlag = 1 << 11; const intptr_t flags = reader_.ReadUInt(); const bool has_pragma = (flags & kHasPragmaFlag) != 0; @@ -410,6 +411,7 @@ void BytecodeReaderHelper::ReadClosureDeclaration(const Function& function, closure.set_is_inlinable(false); } closure.set_is_debuggable((flags & kIsDebuggableFlag) != 0); + closure.set_is_visible((flags & kIsInvisibleFlag) == 0); closure.set_has_pragma(has_pragma); closures_->SetAt(closureIndex, closure); @@ -2072,6 +2074,7 @@ void BytecodeReaderHelper::ReadFunctionDeclarations(const Class& cls) { const int kHasPragmaFlag = 1 << 22; const int kHasCustomScriptFlag = 1 << 23; const int kIsExtensionTypeMemberFlag = 1 << 24; + const int kIsInvisibleFlag = 1 << 25; const intptr_t num_functions = reader_.ReadListLength(); ASSERT(function_index_ + num_functions == functions_->Length()); @@ -2154,6 +2157,7 @@ void BytecodeReaderHelper::ReadFunctionDeclarations(const Class& cls) { function.set_is_synthetic((flags & kIsNoSuchMethodForwarderFlag) != 0); function.set_is_reflectable((flags & kIsReflectableFlag) != 0); function.set_is_debuggable((flags & kIsDebuggableFlag) != 0); + function.set_is_visible((flags & kIsInvisibleFlag) == 0); function.set_is_extension_member(is_extension_member); function.set_is_extension_type_member(is_extension_type_member);