[JVM IR] Add support for exposing boxed inline classes for use from Java

Added a new annotation @JvmExposeBoxed to expose boxed forms of inline
classes for effective use from Java. For functions and constructors
that take or return inline classes, a wrapper declaration for Java is
created where inline classes are boxed. Fixed IR transformation
to properly handle this annotation. Added the required checker to
handle misuse.
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt
index 223427a..ddb1ebc 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt
@@ -4857,6 +4857,12 @@
             token,
         )
     }
+    add(FirJvmErrors.JVM_EXPOSE_BOXED_WITHOUT_INLINE) { firDiagnostic ->
+        JvmExposeBoxedWithoutInlineImpl(
+            firDiagnostic as KtPsiDiagnostic,
+            token,
+        )
+    }
     add(FirJvmErrors.JAVA_TYPE_MISMATCH) { firDiagnostic ->
         JavaTypeMismatchImpl(
             firSymbolBuilder.typeBuilder.buildKtType(firDiagnostic.a),
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt
index 64682fe..c20b756 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt
@@ -3386,6 +3386,10 @@
         override val diagnosticClass get() = JvmInlineWithoutValueClass::class
     }
 
+    interface JvmExposeBoxedWithoutInline : KtFirDiagnostic<KtAnnotationEntry> {
+        override val diagnosticClass get() = JvmExposeBoxedWithoutInline::class
+    }
+
     interface JavaTypeMismatch : KtFirDiagnostic<KtExpression> {
         override val diagnosticClass get() = JavaTypeMismatch::class
         val expectedType: KtType
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt
index 9816fac..4efef2e 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt
@@ -4088,6 +4088,11 @@
     token: KtLifetimeToken,
 ) : KtAbstractFirDiagnostic<PsiElement>(firDiagnostic, token), KtFirDiagnostic.JvmInlineWithoutValueClass
 
+internal class JvmExposeBoxedWithoutInlineImpl(
+    firDiagnostic: KtPsiDiagnostic,
+    token: KtLifetimeToken,
+) : KtAbstractFirDiagnostic<KtAnnotationEntry>(firDiagnostic, token), KtFirDiagnostic.JvmExposeBoxedWithoutInline
+
 internal class JavaTypeMismatchImpl(
     override val expectedType: KtType,
     override val actualType: KtType,
diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/inlineCodegenUtils.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/inlineCodegenUtils.kt
index 8da1c92..8eccaef 100644
--- a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/inlineCodegenUtils.kt
+++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/inlineCodegenUtils.kt
@@ -559,7 +559,7 @@
 fun generateResumePathUnboxing(v: InstructionAdapter, inlineClass: KotlinTypeMarker, typeMapper: KotlinTypeMapperBase) {
     addBeforeUnboxInlineClassMarker(v)
     StackValue.unboxInlineClass(AsmTypes.OBJECT_TYPE, inlineClass, v, typeMapper)
-    // Suspend functions always returns Any?, but the unboxing disrupts type analysis of the bytecode.
+    // Suspend functions always returns Any?, but   the unboxing disrupts type analysis of the bytecode.
     // For example, if the underlying type is String, CHECKCAST String is removed.
     // However, the unboxing is moved to the resume path, the direct path still has Any?, but now, without the CHECKCAST.
     // Thus, we add CHECKCAST Object, which we remove, after we copy the unboxing to the resume path.
diff --git a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJvmDiagnosticsList.kt b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJvmDiagnosticsList.kt
index fb73a56..41c6d7f 100644
--- a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJvmDiagnosticsList.kt
+++ b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJvmDiagnosticsList.kt
@@ -40,6 +40,7 @@
 
         val VALUE_CLASS_WITHOUT_JVM_INLINE_ANNOTATION by error<PsiElement>()
         val JVM_INLINE_WITHOUT_VALUE_CLASS by error<PsiElement>()
+        val JVM_EXPOSE_BOXED_WITHOUT_INLINE by error<KtAnnotationEntry>()
     }
 
     val TYPES by object : DiagnosticGroup("Types") {
diff --git a/compiler/fir/checkers/checkers.jvm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrors.kt b/compiler/fir/checkers/checkers.jvm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrors.kt
index 4654f69..072a8be 100644
--- a/compiler/fir/checkers/checkers.jvm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrors.kt
+++ b/compiler/fir/checkers/checkers.jvm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrors.kt
@@ -44,6 +44,7 @@
     val FUNCTION_DELEGATE_MEMBER_NAME_CLASH by error0<PsiElement>(SourceElementPositioningStrategies.DECLARATION_NAME)
     val VALUE_CLASS_WITHOUT_JVM_INLINE_ANNOTATION by error0<PsiElement>()
     val JVM_INLINE_WITHOUT_VALUE_CLASS by error0<PsiElement>()
+    val JVM_EXPOSE_BOXED_WITHOUT_INLINE by error0<KtAnnotationEntry>()
 
     // Types
     val JAVA_TYPE_MISMATCH by error2<KtExpression, ConeKotlinType, ConeKotlinType>()
diff --git a/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrorsDefaultMessages.kt b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrorsDefaultMessages.kt
index bab9352..cd14c28 100644
--- a/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrorsDefaultMessages.kt
+++ b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/jvm/FirJvmErrorsDefaultMessages.kt
@@ -34,6 +34,7 @@
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_DEFAULT_IN_DECLARATION
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_DEFAULT_WITH_COMPATIBILITY_IN_DECLARATION
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_DEFAULT_WITH_COMPATIBILITY_NOT_ON_INTERFACE
+import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_EXPOSE_BOXED_WITHOUT_INLINE
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_INLINE_WITHOUT_VALUE_CLASS
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_PACKAGE_NAME_CANNOT_BE_EMPTY
 import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.JVM_PACKAGE_NAME_MUST_BE_VALID_NAME
@@ -113,42 +114,42 @@
         )
         map.put(
             SUPER_CALL_WITH_DEFAULT_PARAMETERS,
-            "Super-calls with default arguments are prohibited. Please specify all arguments of ''super.{0}'' explicitly.",
+            "Super-calls with default arguments are not allowed. Please specify all arguments of ''super.{0}'' explicitly",
             TO_STRING
         )
 
-        map.put(LOCAL_JVM_RECORD, "Local '@JvmRecord' classes are prohibited.")
-        map.put(NON_FINAL_JVM_RECORD, "'@JvmRecord' class should be final.")
-        map.put(ENUM_JVM_RECORD, "'@JvmRecord' class should not be an enum.")
+        map.put(LOCAL_JVM_RECORD, "Local @JvmRecord classes are not allowed")
+        map.put(NON_FINAL_JVM_RECORD, "@JvmRecord class should be final")
+        map.put(ENUM_JVM_RECORD, "@JvmRecord class should not be an enum")
         map.put(
             JVM_RECORD_WITHOUT_PRIMARY_CONSTRUCTOR_PARAMETERS,
-            "Primary constructor with parameters is required for '@JvmRecord' class."
+            "Primary constructor with parameters is required for @JvmRecord class"
         )
-        map.put(JVM_RECORD_NOT_VAL_PARAMETER, "Constructor parameter of '@JvmRecord' class should be a 'val'.")
-        map.put(JVM_RECORD_NOT_LAST_VARARG_PARAMETER, "Only the last constructor parameter of '@JvmRecord' can be a vararg.")
-        map.put(JVM_RECORD_EXTENDS_CLASS, "Record cannot extend a class.", RENDER_TYPE)
-        map.put(INNER_JVM_RECORD, "'@JvmRecord' class should not be inner.")
-        map.put(FIELD_IN_JVM_RECORD, "Non-constructor properties with backing field in '@JvmRecord' class are prohibited.")
-        map.put(DELEGATION_BY_IN_JVM_RECORD, "Delegation is prohibited for '@JvmRecord' classes.")
-        map.put(NON_DATA_CLASS_JVM_RECORD, "Only data classes are allowed to be marked as '@JvmRecord'.")
-        map.put(ILLEGAL_JAVA_LANG_RECORD_SUPERTYPE, "Classes cannot have explicit 'java.lang.Record' supertype.")
+        map.put(JVM_RECORD_NOT_VAL_PARAMETER, "Constructor parameter of @JvmRecord class should be a val")
+        map.put(JVM_RECORD_NOT_LAST_VARARG_PARAMETER, "Only the last constructor parameter of @JvmRecord may be a vararg")
+        map.put(JVM_RECORD_EXTENDS_CLASS, "Record cannot inherit a class", RENDER_TYPE)
+        map.put(INNER_JVM_RECORD, "@JvmRecord class should not be inner")
+        map.put(FIELD_IN_JVM_RECORD, "It's not allowed to have non-constructor properties with backing field in @JvmRecord class")
+        map.put(DELEGATION_BY_IN_JVM_RECORD, "Delegation is not allowed for @JvmRecord classes")
+        map.put(NON_DATA_CLASS_JVM_RECORD, "Only data classes are allowed to be marked as @JvmRecord")
+        map.put(ILLEGAL_JAVA_LANG_RECORD_SUPERTYPE, "Classes cannot have explicit 'java.lang.Record' supertype")
 
-        map.put(OVERRIDE_CANNOT_BE_STATIC, "Override member cannot be '@JvmStatic' in an object.")
+        map.put(OVERRIDE_CANNOT_BE_STATIC, "Override member cannot be '@JvmStatic' in object")
         map.put(
             JVM_STATIC_NOT_IN_OBJECT_OR_CLASS_COMPANION,
-            "Only members in named objects and companion objects of classes can be annotated with '@JvmStatic'."
+            "Only members in named objects and companion objects of classes can be annotated with '@JvmStatic'"
         )
         map.put(
             JVM_STATIC_NOT_IN_OBJECT_OR_COMPANION,
-            "Only members in named objects and companion objects can be annotated with '@JvmStatic'."
+            "Only members in named objects and companion objects can be annotated with '@JvmStatic'"
         )
         map.put(
             JVM_STATIC_ON_NON_PUBLIC_MEMBER,
-            "Only public members in interface companion objects can be annotated with '@JvmStatic'."
+            "Only public members in interface companion objects can be annotated with '@JvmStatic'"
         )
         map.put(
             JVM_STATIC_ON_CONST_OR_JVM_FIELD,
-            "'@JvmStatic' annotation is useless for const or '@JvmField' properties.",
+            "'@JvmStatic' annotation is useless for const or '@JvmField' properties",
         )
         map.put(
             JVM_STATIC_ON_EXTERNAL_IN_INTERFACE,
@@ -162,6 +163,7 @@
 
         map.put(VALUE_CLASS_WITHOUT_JVM_INLINE_ANNOTATION, "Value classes without '@JvmInline' annotation are not yet supported.")
         map.put(JVM_INLINE_WITHOUT_VALUE_CLASS, "'@JvmInline' annotation is applicable only to value classes.")
+        map.put(JVM_EXPOSE_BOXED_WITHOUT_INLINE, "'@JvmExposeBoxed' annotation is only applicable to '@JvmInline' value classes.")
 
         map.put(JVM_DEFAULT_IN_DECLARATION, "Usage of ''@{0}'' is only allowed with ''-Xjvm-default'' option.", STRING)
         map.put(
diff --git a/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/JvmDeclarationCheckers.kt b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/JvmDeclarationCheckers.kt
index 2477f22..6c88b56 100644
--- a/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/JvmDeclarationCheckers.kt
+++ b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/JvmDeclarationCheckers.kt
@@ -30,6 +30,7 @@
         get() = setOf(
             FirJvmRecordChecker,
             FirJvmInlineApplicabilityChecker,
+            FirJvmExposeBoxedChecker,
             FirJvmConflictsChecker,
             FirInlineBodyRegularClassChecker,
         )
diff --git a/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/declaration/FirJvmExposeBoxedChecker.kt b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/declaration/FirJvmExposeBoxedChecker.kt
new file mode 100644
index 0000000..e5331f2
--- /dev/null
+++ b/compiler/fir/checkers/checkers.jvm/src/org/jetbrains/kotlin/fir/analysis/jvm/checkers/declaration/FirJvmExposeBoxedChecker.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.fir.analysis.jvm.checkers.declaration
+
+import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
+import org.jetbrains.kotlin.diagnostics.reportOn
+import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
+import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirRegularClassChecker
+import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors
+import org.jetbrains.kotlin.fir.declarations.*
+import org.jetbrains.kotlin.fir.declarations.utils.*
+import org.jetbrains.kotlin.resolve.JVM_EXPOSE_BOXED_ANNOTATION_CLASS_ID
+import org.jetbrains.kotlin.resolve.JVM_INLINE_ANNOTATION_CLASS_ID
+
+object FirJvmExposeBoxedChecker : FirRegularClassChecker() {
+    override fun check(declaration: FirRegularClass, context: CheckerContext, reporter: DiagnosticReporter) {
+        val jvmInlineAnnotation = declaration.getAnnotationByClassId(JVM_INLINE_ANNOTATION_CLASS_ID, context.session)
+        val jvmExposeBoxedAnnotation = declaration.getAnnotationByClassId(JVM_EXPOSE_BOXED_ANNOTATION_CLASS_ID, context.session)
+
+        if ((jvmInlineAnnotation == null || !declaration.isInline) && jvmExposeBoxedAnnotation != null) {
+            reporter.reportOn(jvmExposeBoxedAnnotation.source, FirJvmErrors.JVM_EXPOSE_BOXED_WITHOUT_INLINE, context)
+        }
+    }
+}
diff --git a/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirNonSuppressibleErrorNames.kt b/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirNonSuppressibleErrorNames.kt
index 7c781a6..7118bed3 100644
--- a/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirNonSuppressibleErrorNames.kt
+++ b/compiler/fir/checkers/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/FirNonSuppressibleErrorNames.kt
@@ -573,6 +573,7 @@
     "FUNCTION_DELEGATE_MEMBER_NAME_CLASH",
     "VALUE_CLASS_WITHOUT_JVM_INLINE_ANNOTATION",
     "JVM_INLINE_WITHOUT_VALUE_CLASS",
+    "JVM_EXPOSE_BOXED_WITHOUT_INLINE",
     "JAVA_TYPE_MISMATCH",
     "UPPER_BOUND_CANNOT_BE_ARRAY",
     "STRICTFP_ON_CLASS",
diff --git a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirLightTreeBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirLightTreeBlackBoxCodegenTestGenerated.java
index 933d303..7087617 100644
--- a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirLightTreeBlackBoxCodegenTestGenerated.java
+++ b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirLightTreeBlackBoxCodegenTestGenerated.java
@@ -53188,6 +53188,12 @@
         }
 
         @Test
+        @TestMetadata("exposedInlineClass.kt")
+        public void testExposedInlineClass() throws Exception {
+            runTest("compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt");
+        }
+
+        @Test
         @TestMetadata("fakeOverrideCall.kt")
         public void testFakeOverrideCall() throws Exception {
             runTest("compiler/testData/codegen/box/valueClasses/fakeOverrideCall.kt");
diff --git a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirPsiBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirPsiBlackBoxCodegenTestGenerated.java
index 037082a..2819c6e 100644
--- a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirPsiBlackBoxCodegenTestGenerated.java
+++ b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirPsiBlackBoxCodegenTestGenerated.java
@@ -53188,6 +53188,12 @@
         }
 
         @Test
+        @TestMetadata("exposedInlineClass.kt")
+        public void testExposedInlineClass() throws Exception {
+            runTest("compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt");
+        }
+
+        @Test
         @TestMetadata("fakeOverrideCall.kt")
         public void testFakeOverrideCall() throws Exception {
             runTest("compiler/testData/codegen/box/valueClasses/fakeOverrideCall.kt");
diff --git a/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/codegen/FunctionCodegen.kt b/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/codegen/FunctionCodegen.kt
index 7629160..ebfef16 100644
--- a/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/codegen/FunctionCodegen.kt
+++ b/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/codegen/FunctionCodegen.kt
@@ -251,14 +251,14 @@
         }
         val contextReceivers = valueParameters.subList(0, contextReceiverParametersCount)
         for (contextReceiver in contextReceivers) {
-            frameMap.enter(contextReceiver, classCodegen.typeMapper.mapType(contextReceiver.type))
+            frameMap.enter(contextReceiver, classCodegen.typeMapper.mapType(contextReceiver))
         }
         extensionReceiverParameter?.let {
             frameMap.enter(it, classCodegen.typeMapper.mapType(it))
         }
         val regularParameters = valueParameters.subList(contextReceiverParametersCount, valueParameters.size)
         for (parameter in regularParameters) {
-            frameMap.enter(parameter, classCodegen.typeMapper.mapType(parameter.type))
+            frameMap.enter(parameter, classCodegen.typeMapper.mapType(parameter))
         }
         return frameMap
     }
diff --git a/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/intrinsics/CompareTo.kt b/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/intrinsics/CompareTo.kt
index 7aeac31..a873c19 100644
--- a/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/intrinsics/CompareTo.kt
+++ b/compiler/ir/backend.jvm/codegen/src/org/jetbrains/kotlin/backend/jvm/intrinsics/CompareTo.kt
@@ -21,6 +21,7 @@
 import org.jetbrains.kotlin.backend.jvm.codegen.*
 import org.jetbrains.kotlin.backend.jvm.ir.isSmartcastFromHigherThanNullable
 import org.jetbrains.kotlin.backend.jvm.ir.receiverAndArgs
+import org.jetbrains.kotlin.backend.jvm.mapping.mapType
 import org.jetbrains.kotlin.builtins.PrimitiveType
 import org.jetbrains.kotlin.codegen.AsmUtil
 import org.jetbrains.kotlin.codegen.AsmUtil.comparisonOperandType
@@ -58,7 +59,7 @@
         val callee = expression.symbol.owner
         val calleeParameter = callee.dispatchReceiverParameter ?: callee.extensionReceiverParameter!!
         val parameterType = comparisonOperandType(
-            classCodegen.typeMapper.mapType(calleeParameter.type),
+            classCodegen.typeMapper.mapType(calleeParameter),
             signature.valueParameters.single().asmType,
         )
         return IntrinsicFunction.create(expression, signature, classCodegen, listOf(parameterType, parameterType)) {
diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/BridgeLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/BridgeLowering.kt
index 798359c..60053f8 100644
--- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/BridgeLowering.kt
+++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/BridgeLowering.kt
@@ -15,6 +15,7 @@
 import org.jetbrains.kotlin.backend.jvm.MemoizedMultiFieldValueClassReplacements
 import org.jetbrains.kotlin.backend.jvm.SpecialBridge
 import org.jetbrains.kotlin.backend.jvm.ir.*
+import org.jetbrains.kotlin.backend.jvm.mapping.mapType
 import org.jetbrains.kotlin.codegen.AsmUtil
 import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
 import org.jetbrains.kotlin.descriptors.Modality
@@ -171,6 +172,9 @@
         if (isMethodOfAny())
             return false
 
+        if (origin == JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS)
+            return false
+
         // We don't produce bridges for abstract functions in interfaces.
         if (isJvmAbstract(context.config.jvmDefaultMode)) {
             if (parentAsClass.isJvmInterface) {
@@ -552,7 +556,7 @@
             // If the signature of this method will be changed in the output to take a boxed argument instead of a primitive,
             // rewrite the argument so that code will be generated for a boxed argument and not a primitive.
             valueParameters.forEachIndexed { i, p ->
-                if (AsmUtil.isPrimitive(context.defaultTypeMapper.mapType(p.type)) && ourSignature.argumentTypes[i].sort == Type.OBJECT) {
+                if (AsmUtil.isPrimitive(context.defaultTypeMapper.mapType(p)) && ourSignature.argumentTypes[i].sort == Type.OBJECT) {
                     p.type = p.type.makeNullable()
                 }
             }
diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt
index 226f86f..a047aa2 100644
--- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt
+++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmInlineClassLowering.kt
@@ -8,16 +8,21 @@
 import org.jetbrains.kotlin.backend.common.ScopeWithIr
 import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
 import org.jetbrains.kotlin.backend.common.lower.irBlockBody
+import org.jetbrains.kotlin.backend.common.pop
+import org.jetbrains.kotlin.backend.common.push
 import org.jetbrains.kotlin.backend.jvm.*
 import org.jetbrains.kotlin.backend.jvm.ir.erasedUpperBound
+import org.jetbrains.kotlin.backend.jvm.ir.isExposedSingleFieldValueClass
 import org.jetbrains.kotlin.builtins.StandardNames
 import org.jetbrains.kotlin.config.ApiVersion
 import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
+import org.jetbrains.kotlin.descriptors.Modality
 import org.jetbrains.kotlin.ir.IrStatement
 import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
 import org.jetbrains.kotlin.ir.builders.*
 import org.jetbrains.kotlin.ir.builders.declarations.addConstructor
 import org.jetbrains.kotlin.ir.builders.declarations.buildFun
+import org.jetbrains.kotlin.ir.builders.declarations.buildValueParameter
 import org.jetbrains.kotlin.ir.declarations.*
 import org.jetbrains.kotlin.ir.expressions.*
 import org.jetbrains.kotlin.ir.expressions.impl.*
@@ -26,6 +31,7 @@
 import org.jetbrains.kotlin.ir.types.*
 import org.jetbrains.kotlin.ir.util.*
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.load.java.JvmAbi
 import org.jetbrains.kotlin.name.Name
 import org.jetbrains.kotlin.resolve.JVM_INLINE_ANNOTATION_FQ_NAME
 
@@ -71,6 +77,7 @@
 
     override val specificMangle: SpecificMangle
         get() = SpecificMangle.Inline
+
     override fun visitClassNewDeclarationsWhenParallel(declaration: IrDeclaration) = Unit
 
     override fun visitClassNew(declaration: IrClass): IrClass {
@@ -99,12 +106,13 @@
     }
 
     override fun handleSpecificNewClass(declaration: IrClass) {
-        val irConstructor = declaration.primaryConstructor!!
+        val primaryConstructor = declaration.primaryConstructor!!
         // The field getter is used by reflection and cannot be removed here unless it is internal.
-        declaration.declarations.removeIf {
-            it == irConstructor || (it is IrFunction && it.isInlineClassFieldGetter && !it.visibility.isPublicAPI)
+        declaration.declarations.removeAll {
+            it == primaryConstructor || (it is IrFunction && it.isInlineClassFieldGetter && !it.visibility.isPublicAPI)
         }
-        buildPrimaryInlineClassConstructor(declaration, irConstructor)
+
+        buildInlineClassConstructors(declaration, primaryConstructor)
         buildBoxFunction(declaration)
         buildUnboxFunction(declaration)
         buildSpecializedEqualsMethodIfNeeded(declaration)
@@ -131,11 +139,96 @@
         }
     }
 
+    override fun transformSimpleFunctionFlat(function: IrSimpleFunction, replacement: IrSimpleFunction): List<IrDeclaration> {
+        replacement.valueParameters.forEach {
+            visitParameter(it)
+            it.defaultValue?.patchDeclarationParents(replacement)
+        }
+
+        allScopes.push(createScope(replacement))
+        replacement.body = function.body?.transform(this, null)?.patchDeclarationParents(replacement)
+        allScopes.pop()
+
+        val result = ArrayList<IrDeclaration>(3)
+        result += replacement
+
+        val declaredOverrideWithDispatchReceiver = function.overriddenSymbols.isNotEmpty() && replacement.dispatchReceiverParameter == null
+
+        val takesOrReturnsExposeBoxedClass = function.explicitParameters
+            .any { it.type.classOrNull?.owner?.isExposedSingleFieldValueClass == true }
+                || function.returnType.classOrNull?.owner?.isExposedSingleFieldValueClass == true
+
+        if (declaredOverrideWithDispatchReceiver) {
+            result += createBridgeFunction(function, replacement)
+        }
+
+        // Replaces the original with function taking/returning boxed inline class, intended for use from Java.
+        // The function body just calls the replacement.
+        if (takesOrReturnsExposeBoxedClass && !function.isEquals() && !function.isToString() && !function.isHashCode()) {
+            val functionForJava = function.deepCopyWithSymbols(initialParent = function.parent)
+
+            functionForJava.origin = JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS
+
+            function.correspondingPropertySymbol?.let { property ->
+                val propertyName = property.owner.name.asString()
+                functionForJava.name = Name.identifier(
+                    if (function.isGetter) {
+                        JvmAbi.getterName(propertyName)
+                    } else {
+                        JvmAbi.setterName(propertyName)
+                    }
+                )
+            }
+
+            // Don't create a default argument stub
+            for (param in functionForJava.valueParameters) {
+                param.defaultValue = null
+            }
+
+            if (replacement.modality != Modality.ABSTRACT) {
+                functionForJava.body = context.createIrBuilder(functionForJava.symbol).irBlockBody(functionForJava) {
+                    +irReturn(irCall(replacement).also { access ->
+                        var paramOffset = 0
+
+                        if (functionForJava.parentClassOrNull?.isExposedSingleFieldValueClass == true) {
+                            functionForJava.dispatchReceiverParameter?.let {
+                                access.putValueArgument(paramOffset, irGet(it))
+                                paramOffset++
+                            }
+
+                            functionForJava.extensionReceiverParameter?.let {
+                                access.putValueArgument(paramOffset, irGet(it))
+                                paramOffset++
+                            }
+                        } else {
+                            functionForJava.dispatchReceiverParameter?.let {
+                                access.dispatchReceiver = irGet(it)
+                            }
+
+                            functionForJava.extensionReceiverParameter?.let {
+                                access.extensionReceiver = irGet(it)
+                            }
+                        }
+
+                        for ((idx, parameter) in functionForJava.valueParameters.withIndex()) {
+                            access.putValueArgument(paramOffset + idx, irGet(parameter))
+                        }
+                    })
+                }
+            }
+
+            result += functionForJava
+        }
+
+        return result
+    }
+
     // Secondary constructors for boxed types get translated to static functions returning
     // unboxed arguments. We remove the original constructor.
     // Primary constructors' case is handled at the start of transformFunctionFlat
     override fun transformSecondaryConstructorFlat(constructor: IrConstructor, replacement: IrSimpleFunction): List<IrDeclaration> {
         replacement.valueParameters.forEach { it.transformChildrenVoid() }
+
         replacement.body = context.createIrBuilder(replacement.symbol, replacement.startOffset, replacement.endOffset).irBlockBody(
             replacement
         ) {
@@ -146,9 +239,7 @@
                 +statement
                     .transformStatement(object : IrElementTransformerVoid() {
                         // Don't recurse under nested class declarations
-                        override fun visitClass(declaration: IrClass): IrStatement {
-                            return declaration
-                        }
+                        override fun visitClass(declaration: IrClass): IrStatement = declaration
 
                         // Capture the result of a delegating constructor call in a temporary variable "thisVar".
                         //
@@ -189,13 +280,49 @@
             +irReturn(irGet(thisVar))
         }
 
+        if (constructor.parentAsClass.isExposedSingleFieldValueClass) {
+            val valueClass = constructor.parentAsClass
+
+            constructor.valueParameters = constructor.valueParameters.map {
+                it.deepCopyWithSymbols(initialParent = constructor)
+            }
+
+            constructor.body = context.createIrBuilder(constructor.symbol).irBlockBody(constructor) {
+                +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single())
+                val firstArgument = constructor.valueParameters[0]
+                val field = getInlineClassBackingField(valueClass)
+
+                +irSetField(
+                    irGet(valueClass.thisReceiver!!),
+                    field,
+                    coerceInlineClasses(
+                        irCall(replacement.symbol).also { access ->
+                            access.putValueArgument(0, irGet(firstArgument))
+                            val valueParameterMap = constructor.explicitParameters.zip(replacement.explicitParameters).toMap()
+
+                            for ((parameter, argument) in typedArgumentList(constructor, access)) {
+                                if (argument == null) continue
+                                val newParameter = valueParameterMap.getValue(parameter)
+                                access.putArgument(replacement, newParameter, argument.transform(this@JvmInlineClassLowering, null))
+                            }
+                        },
+                        replacement.returnType,
+                        field.type,
+                        true,
+                    )
+                )
+            }
+
+            return listOf(constructor, replacement)
+        }
+
         return listOf(replacement)
     }
 
     private fun IrMemberAccessExpression<*>.buildReplacement(
         originalFunction: IrFunction,
         original: IrMemberAccessExpression<*>,
-        replacement: IrSimpleFunction
+        replacement: IrSimpleFunction,
     ) {
         copyTypeArgumentsFrom(original)
         val valueParameterMap = originalFunction.explicitParameters.zip(replacement.explicitParameters).toMap()
@@ -347,8 +474,7 @@
                     .specializeEqualsCall(leftOp, rightOp)
                     ?: expression
             }
-            else ->
-                super.visitCall(expression)
+            else -> super.visitCall(expression)
         }
 
     private val IrCall.isEqualsMethodCallOnInlineClass: Boolean
@@ -413,59 +539,110 @@
         return super.visitSetValue(expression)
     }
 
-    private fun buildPrimaryInlineClassConstructor(valueClass: IrClass, irConstructor: IrConstructor) {
-        // Add the default primary constructor
+    private fun buildInlineClassConstructors(valueClass: IrClass, constructor: IrConstructor) {
+        // Add the primary synthetic constructor for boxing
         valueClass.addConstructor {
-            updateFrom(irConstructor)
+            updateFrom(constructor)
             visibility = DescriptorVisibilities.PRIVATE
             origin = JvmLoweredDeclarationOrigin.SYNTHETIC_INLINE_CLASS_MEMBER
-            returnType = irConstructor.returnType
+            returnType = constructor.returnType
         }.apply {
-            // Don't create a default argument stub for the primary constructor
-            irConstructor.valueParameters.forEach { it.defaultValue = null }
-            copyParameterDeclarationsFrom(irConstructor)
-            annotations = irConstructor.annotations
-            body = context.createIrBuilder(this.symbol).irBlockBody(this) {
+            copyParameterDeclarationsFrom(constructor)
+
+            // Don't create a default argument stub for the primary constructor for boxing
+            for (param in valueParameters) {
+                param.defaultValue = null
+            }
+
+            if (valueClass.isExposedSingleFieldValueClass) {
+                valueParameters += buildValueParameter(this) {
+                    type = context.irBuiltIns.nothingNType
+                    name = Name.identifier("boxingConstructorMarker")
+                    index = 1
+                }
+            }
+            annotations = constructor.annotations
+            body = context.createIrBuilder(symbol).irBlockBody(this) {
                 +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single())
                 +irSetField(
                     irGet(valueClass.thisReceiver!!),
                     getInlineClassBackingField(valueClass),
-                    irGet(this@apply.valueParameters[0])
+                    irGet(this@apply.valueParameters[0]),
                 )
             }
         }
 
         // Add a static bridge method to the primary constructor. This contains
         // null-checks, default arguments, and anonymous initializers.
-        val function = context.inlineClassReplacements.getReplacementFunction(irConstructor)!!
+        val constructorImpl = checkNotNull(context.inlineClassReplacements.getReplacementFunction(constructor)) {
+            "No replacement function present for ${constructor.dump()}"
+        }
 
         val initBlocks = valueClass.declarations.filterIsInstance<IrAnonymousInitializer>()
             .filterNot { it.isStatic }
 
-        function.valueParameters.forEach { it.transformChildrenVoid() }
-        function.body = context.createIrBuilder(function.symbol).irBlockBody {
-            val argument = function.valueParameters[0]
-            val thisValue = irTemporary(coerceInlineClasses(irGet(argument), argument.type, function.returnType, skipCast = true))
+        constructorImpl.valueParameters.forEach { it.transformChildrenVoid() }
+        constructorImpl.body = context.createIrBuilder(constructorImpl.symbol).irBlockBody {
+            val argument = constructorImpl.valueParameters[0]
+            val thisValue = irTemporary(coerceInlineClasses(irGet(argument), argument.type, constructorImpl.returnType, skipCast = true))
             valueMap[valueClass.thisReceiver!!.symbol] = thisValue
             for (initBlock in initBlocks) {
                 for (stmt in initBlock.body.statements) {
-                    +stmt.transformStatement(this@JvmInlineClassLowering).patchDeclarationParents(function)
+                    +stmt.transformStatement(this@JvmInlineClassLowering).patchDeclarationParents(constructorImpl)
                 }
             }
             +irReturn(irGet(thisValue))
         }
 
+        if (valueClass.isExposedSingleFieldValueClass) {
+            // Build primary constructor as a public secondary constructor for Java
+            valueClass.addConstructor {
+                updateFrom(constructor)
+                returnType = constructor.returnType
+                isPrimary = false
+            }.apply constructor@{
+                copyParameterDeclarationsFrom(constructor)
+                copyAnnotationsFrom(constructor)
+
+                // Don't create a default argument stub
+                for (param in valueParameters) {
+                    param.defaultValue = null
+                }
+
+                body = context.createIrBuilder(symbol).irBlockBody(this) {
+                    +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single())
+                    +irSetField(
+                        irGet(valueClass.thisReceiver!!),
+                        getInlineClassBackingField(valueClass),
+                        irGet(valueParameters[0])
+                    )
+                    val argument = valueParameters[0]
+                    val thisValue =
+                        irTemporary(coerceInlineClasses(irGet(argument), argument.type, constructorImpl.returnType, skipCast = true))
+                    valueMap[valueClass.thisReceiver!!.symbol] = thisValue
+                    +irCall(constructorImpl.symbol).apply {
+                        putValueArgument(0, irGet(valueParameters[0]))
+                    }
+                }
+            }
+        }
+
         valueClass.declarations.removeAll(initBlocks)
-        valueClass.declarations += function
+        valueClass.declarations += constructorImpl
     }
 
     private fun buildBoxFunction(valueClass: IrClass) {
         val function = context.inlineClassReplacements.getBoxFunction(valueClass)
         with(context.createIrBuilder(function.symbol)) {
+            val constructor = checkNotNull(valueClass.primaryConstructor) {
+                "Building box function in value class without a primary constructor"
+            }
             function.body = irExprBody(
-                irCall(valueClass.primaryConstructor!!.symbol).apply {
+                irCall(constructor.symbol).apply {
                     passTypeArgumentsFrom(function)
                     putValueArgument(0, irGet(function.valueParameters[0]))
+                    if (valueClass.isExposedSingleFieldValueClass)
+                        putValueArgument(1, irNull())
                 }
             )
         }
@@ -527,4 +704,33 @@
         valueClass.declarations += function
     }
 
+    override fun handleRegularClassConstructor(constructor: IrConstructor) {
+        if (constructor.valueParameters.none { it.type.classOrNull?.owner?.isExposedSingleFieldValueClass == true }) return
+        if (constructor.origin == JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS) return
+        if (DescriptorVisibilities.isPrivate(constructor.visibility)) return
+        val valueClass = constructor.parentAsClass
+        // Adding secondary constructor for Java with boxed exposed value class parameters
+        valueClass.addConstructor {
+            updateFrom(constructor)
+            isPrimary = false
+            origin = JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS
+            returnType = constructor.returnType
+        }.apply {
+            copyParameterDeclarationsFrom(constructor)
+
+            // Don't create a default argument stub for the constructor for Java
+            for (param in valueParameters) {
+                param.defaultValue = null
+            }
+
+            copyAnnotationsFrom(constructor)
+            body = context.createIrBuilder(symbol).irBlockBody(this) {
+                +irDelegatingConstructorCall(constructor).also {
+                    for ((idx, param) in this@apply.valueParameters.withIndex()) {
+                        it.putValueArgument(idx, irGet(param))
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmMultiFieldValueClassLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmMultiFieldValueClassLowering.kt
index b2d4574..24317c0 100644
--- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmMultiFieldValueClassLowering.kt
+++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmMultiFieldValueClassLowering.kt
@@ -443,6 +443,26 @@
         }
     }
 
+    override fun transformSimpleFunctionFlat(function: IrSimpleFunction, replacement: IrSimpleFunction): List<IrDeclaration> {
+        replacement.valueParameters.forEach {
+            visitParameter(it)
+            it.defaultValue?.patchDeclarationParents(replacement)
+        }
+
+        allScopes.push(createScope(replacement))
+        replacement.body = function.body?.transform(this, null)?.patchDeclarationParents(replacement)
+        allScopes.pop()
+        replacement.copyAttributes(function)
+
+        // Don't create a wrapper for functions which are only used in an unboxed context
+        if (function.overriddenSymbols.isEmpty() || replacement.dispatchReceiverParameter != null)
+            return listOf(replacement)
+
+        val bridgeFunction = createBridgeFunction(function, replacement)
+        return listOf(replacement, bridgeFunction)
+    }
+
+
     override fun transformSecondaryConstructorFlat(constructor: IrConstructor, replacement: IrSimpleFunction): List<IrDeclaration> {
         for (param in replacement.valueParameters) {
             param.defaultValue?.patchDeclarationParents(replacement)
diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmValueClassAbstractLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmValueClassAbstractLowering.kt
index 23e7474..1a6c11a 100644
--- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmValueClassAbstractLowering.kt
+++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/JvmValueClassAbstractLowering.kt
@@ -40,6 +40,7 @@
 
         if (replacement == null) {
             if (function is IrConstructor) {
+                handleRegularClassConstructor(function)
                 val constructorReplacement = replacements.getReplacementForRegularClassConstructor(function)
                 if (constructorReplacement != null) {
                     addBindingsFor(function, constructorReplacement)
@@ -77,6 +78,9 @@
         }
     }
 
+    protected open fun handleRegularClassConstructor(constructor: IrConstructor) {
+    }
+
     private fun transformFlattenedConstructor(function: IrConstructor, replacement: IrConstructor): List<IrDeclaration> {
         replacement.valueParameters.forEach {
             it.defaultValue?.patchDeclarationParents(replacement)
@@ -105,24 +109,7 @@
         return declaration
     }
 
-    private fun transformSimpleFunctionFlat(function: IrSimpleFunction, replacement: IrSimpleFunction): List<IrDeclaration> {
-        replacement.valueParameters.forEach {
-            it.defaultValue?.patchDeclarationParents(replacement)
-            visitParameter(it)
-        }
-        allScopes.push(createScope(replacement))
-        replacement.body = function.body?.transform(this, null)?.patchDeclarationParents(replacement)
-        allScopes.pop()
-        replacement.copyAttributes(function)
-
-        // Don't create a wrapper for functions which are only used in an unboxed context
-        if (function.overriddenSymbols.isEmpty() || replacement.dispatchReceiverParameter != null)
-            return listOf(replacement)
-
-        val bridgeFunction = createBridgeFunction(function, replacement)
-
-        return listOf(replacement, bridgeFunction)
-    }
+    protected abstract fun transformSimpleFunctionFlat(function: IrSimpleFunction, replacement: IrSimpleFunction): List<IrDeclaration>
 
     final override fun visitReturn(expression: IrReturn): IrExpression {
         (expression.returnTargetSymbol.owner as? IrFunction)?.let { target ->
@@ -177,7 +164,7 @@
     protected enum class SpecificMangle { Inline, MultiField }
 
     protected abstract val specificMangle: SpecificMangle
-    private fun createBridgeFunction(
+    protected fun createBridgeFunction(
         function: IrSimpleFunction,
         replacement: IrSimpleFunction
     ): IrSimpleFunction {
diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SuspendLambdaLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SuspendLambdaLowering.kt
index d5ebb37..6baf9ee 100644
--- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SuspendLambdaLowering.kt
+++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/SuspendLambdaLowering.kt
@@ -16,6 +16,7 @@
 import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
 import org.jetbrains.kotlin.backend.jvm.ir.hasChild
 import org.jetbrains.kotlin.backend.jvm.ir.isReadOfCrossinline
+import org.jetbrains.kotlin.backend.jvm.mapping.mapType
 import org.jetbrains.kotlin.codegen.coroutines.COROUTINE_LABEL_FIELD_NAME
 import org.jetbrains.kotlin.codegen.coroutines.INVOKE_SUSPEND_METHOD_NAME
 import org.jetbrains.kotlin.codegen.coroutines.SUSPEND_FUNCTION_COMPLETION_PARAMETER_NAME
@@ -177,7 +178,7 @@
 
             val parametersFields = function.explicitParameters.map {
                 val field = if (it in usedParams) addField {
-                    val normalizedType = context.defaultTypeMapper.mapType(it.type).normalize()
+                    val normalizedType = context.defaultTypeMapper.mapType(it).normalize()
                     val index = varsCountByType[normalizedType]?.plus(1) ?: 0
                     varsCountByType[normalizedType] = index
                     // Rename `$this` to avoid being caught by inlineCodegenUtils.isCapturedFieldName()
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/InlineClassAbi.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/InlineClassAbi.kt
index 6ffbfe7..d586d66 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/InlineClassAbi.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/InlineClassAbi.kt
@@ -7,6 +7,7 @@
 
 import org.jetbrains.kotlin.backend.jvm.MemoizedMultiFieldValueClassReplacements.RemappedParameter.MultiFieldValueClassMapping
 import org.jetbrains.kotlin.backend.jvm.ir.erasedUpperBound
+import org.jetbrains.kotlin.backend.jvm.ir.isExposedSingleFieldValueClass
 import org.jetbrains.kotlin.backend.jvm.ir.isInlineClassType
 import org.jetbrains.kotlin.backend.jvm.ir.isValueClassType
 import org.jetbrains.kotlin.builtins.StandardNames
@@ -20,6 +21,7 @@
 import org.jetbrains.kotlin.load.java.JvmAbi
 import org.jetbrains.kotlin.name.FqNameUnsafe
 import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
 
 /**
  * Replace inline classes by their underlying types.
@@ -65,7 +67,7 @@
             assert(irFunction.constructedClass.isValue) {
                 "Should not mangle names of non-inline class constructors: ${irFunction.render()}"
             }
-            return Name.identifier("constructor-impl")
+            return Name.identifier("constructor${JvmAbi.IMPL_SUFFIX_FOR_INLINE_CLASS_MEMBERS}")
         }
         if (irFunction.isAlreadyMangledMfvcFunction(context)) {
             return irFunction.name
@@ -87,7 +89,7 @@
                 irFunction.name.asString()
         }
 
-        return Name.identifier("$base-${suffix ?: "impl"}")
+        return Name.identifier("$base${if (suffix == null) JvmAbi.IMPL_SUFFIX_FOR_INLINE_CLASS_MEMBERS else "-$suffix"}")
     }
 
     private fun IrFunction.isAlreadyMangledMfvcFunction(context: JvmBackendContext) =
@@ -144,16 +146,21 @@
 val IrFunction.fullValueParameterList: List<IrValueParameter>
     get() = listOfNotNull(extensionReceiverParameter) + valueParameters
 
-fun IrFunction.hasMangledParameters(includeInline: Boolean = true, includeMFVC: Boolean = true): Boolean =
-    (dispatchReceiverParameter != null && when {
-        parentAsClass.isSingleFieldValueClass -> includeInline
+fun IrFunction.hasMangledParameters(includeInline: Boolean = true, includeMFVC: Boolean = true): Boolean {
+    val includesInlineAndNotExposed = includeInline && origin != JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS
+
+    return (dispatchReceiverParameter != null && when {
+        parentAsClass.isSingleFieldValueClass -> includesInlineAndNotExposed
         parentAsClass.isMultiFieldValueClass -> includeMFVC
         else -> false
-    }) || fullValueParameterList.any { it.type.getRequiresMangling(includeInline, includeMFVC) } || (this is IrConstructor && when {
-        constructedClass.isSingleFieldValueClass -> includeInline
+    }) || fullValueParameterList.any {
+        it.type.getRequiresMangling(includesInlineAndNotExposed, includeMFVC)
+    } || (this is IrConstructor && when {
+        constructedClass.isSingleFieldValueClass -> includesInlineAndNotExposed
         constructedClass.isMultiFieldValueClass -> includeMFVC
         else -> false
     })
+}
 
 val IrFunction.hasMangledReturnType: Boolean
     get() = returnType.isInlineClassType() && parentClassOrNull?.isFileClass != true
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt
index c4529ac..a4d983a 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt
@@ -29,9 +29,10 @@
     object ENUM_MAPPINGS_FOR_WHEN : IrDeclarationOriginImpl("ENUM_MAPPINGS_FOR_WHEN", isSynthetic = true)
     object ENUM_MAPPINGS_FOR_ENTRIES : IrDeclarationOriginImpl("ENUM_MAPPINGS_FOR_ENTRIES", isSynthetic = true)
     object SYNTHETIC_INLINE_CLASS_MEMBER : IrDeclarationOriginImpl("SYNTHETIC_INLINE_CLASS_MEMBER", isSynthetic = true)
-    object SYNTHETIC_MULTI_FIELD_VALUE_CLASS_MEMBER : 
+    object SYNTHETIC_MULTI_FIELD_VALUE_CLASS_MEMBER :
         IrDeclarationOriginImpl("SYNTHETIC_MULTI_FIELD_VALUE_CLASS_MEMBER", isSynthetic = true)
     object INLINE_CLASS_GENERATED_IMPL_METHOD : IrDeclarationOriginImpl("INLINE_CLASS_GENERATED_IMPL_METHOD")
+    object FUNCTION_WITH_EXPOSED_INLINE_CLASS : IrDeclarationOriginImpl("FUNCTION_WITH_EXPOSED_INLINE_CLASS")
     object MULTI_FIELD_VALUE_CLASS_GENERATED_IMPL_METHOD : IrDeclarationOriginImpl("MULTI_FIELD_VALUE_CLASS_GENERATED_IMPL_METHOD")
     object STATIC_INLINE_CLASS_REPLACEMENT : IrDeclarationOriginImpl("STATIC_INLINE_CLASS_REPLACEMENT")
     object STATIC_MULTI_FIELD_VALUE_CLASS_REPLACEMENT : IrDeclarationOriginImpl("STATIC_MULTI_FIELD_VALUE_CLASS_REPLACEMENT")
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/MemoizedInlineClassReplacements.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/MemoizedInlineClassReplacements.kt
index 18d540e..6145911 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/MemoizedInlineClassReplacements.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/MemoizedInlineClassReplacements.kt
@@ -135,7 +135,7 @@
 
     private val specializedEqualsCache = storageManager.createCacheWithNotNullValues<IrClass, IrSimpleFunction>()
     fun getSpecializedEqualsMethod(irClass: IrClass, irBuiltIns: IrBuiltIns): IrSimpleFunction {
-        require(irClass.isSingleFieldValueClass)
+        require(irClass.isSingleFieldValueClass) { "Not a single-field inline class" }
         return specializedEqualsCache.computeIfAbsent(irClass) {
             irFactory.buildFun {
                 name = InlineClassDescriptorResolver.SPECIALIZED_EQUALS_NAME
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrCoroutineUtils.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrCoroutineUtils.kt
index 48d0592..23ec74a 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrCoroutineUtils.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrCoroutineUtils.kt
@@ -95,6 +95,7 @@
         isSuspend && shouldContainSuspendMarkers() &&
         // These are templates for the inliner; the continuation is borrowed from the caller method.
         !isEffectivelyInlineOnly() &&
+        origin != JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS &&
         origin != JvmLoweredDeclarationOrigin.INLINE_LAMBDA &&
         origin != JvmLoweredDeclarationOrigin.FOR_INLINE_STATE_MACHINE_TEMPLATE &&
         origin != JvmLoweredDeclarationOrigin.FOR_INLINE_STATE_MACHINE_TEMPLATE_CAPTURES_CROSSINLINE
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrUtils.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrUtils.kt
index 1fb747c..42e663c 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrUtils.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/ir/JvmIrUtils.kt
@@ -55,6 +55,7 @@
 import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource
 import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
 import org.jetbrains.kotlin.name.ClassId
+import org.jetbrains.kotlin.name.JvmNames
 import org.jetbrains.kotlin.name.JvmNames.JVM_DEFAULT_FQ_NAME
 import org.jetbrains.kotlin.name.JvmNames.JVM_DEFAULT_NO_COMPATIBILITY_FQ_NAME
 import org.jetbrains.kotlin.name.JvmNames.JVM_DEFAULT_WITH_COMPATIBILITY_FQ_NAME
@@ -69,6 +70,9 @@
 import org.jetbrains.org.objectweb.asm.commons.Method
 import java.io.File
 
+val IrClass.isExposedSingleFieldValueClass: Boolean
+    get() = isSingleFieldValueClass && hasAnnotation(JvmNames.JVM_EXPOSE_BOXED)
+
 fun IrDeclaration.getJvmNameFromAnnotation(): String? {
     // TODO lower @JvmName?
     val const = getAnnotation(DescriptorUtils.JVM_NAME)?.getValueArgument(0) as? IrConst<*> ?: return null
@@ -316,6 +320,7 @@
 fun IrSimpleFunction.suspendFunctionOriginal(): IrSimpleFunction =
     if (isSuspend &&
         !isStaticValueClassReplacement &&
+        origin != JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS &&
         !isOrOverridesDefaultParameterStub() &&
         parentClassOrNull?.origin != JvmLoweredDeclarationOrigin.DEFAULT_IMPLS
     )
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/IrTypeMapping.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/IrTypeMapping.kt
index 4dd3258..b19fffe 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/IrTypeMapping.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/IrTypeMapping.kt
@@ -5,13 +5,11 @@
 
 package org.jetbrains.kotlin.backend.jvm.mapping
 
+import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
 import org.jetbrains.kotlin.builtins.jvm.JavaToKotlinClassMap
 import org.jetbrains.kotlin.codegen.signature.JvmSignatureWriter
 import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
-import org.jetbrains.kotlin.ir.declarations.IrClass
-import org.jetbrains.kotlin.ir.declarations.IrField
-import org.jetbrains.kotlin.ir.declarations.IrValueParameter
-import org.jetbrains.kotlin.ir.declarations.IrVariable
+import org.jetbrains.kotlin.ir.declarations.*
 import org.jetbrains.kotlin.ir.types.IrSimpleType
 import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.types.IrTypeArgument
@@ -27,8 +25,14 @@
 fun IrTypeMapper.mapType(irVariable: IrVariable): Type =
     mapType(irVariable.type)
 
-fun IrTypeMapper.mapType(irValueParameter: IrValueParameter): Type =
-    mapType(irValueParameter.type)
+fun IrTypeMapper.mapType(irValueParameter: IrValueParameter): Type {
+    val mode = if ((irValueParameter.parent as? IrFunction)?.origin == JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS)
+        TypeMappingMode.DEFAULT.wrapInlineClassesMode()
+    else
+        TypeMappingMode.DEFAULT
+
+    return mapType(irValueParameter.type, mode)
+}
 
 fun IrTypeMapper.mapType(irField: IrField): Type =
     mapType(irField.type)
diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/MethodSignatureMapper.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/MethodSignatureMapper.kt
index 82df40c..53df3f7 100644
--- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/MethodSignatureMapper.kt
+++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/mapping/MethodSignatureMapper.kt
@@ -36,6 +36,7 @@
 import org.jetbrains.kotlin.metadata.deserialization.getExtensionOrNull
 import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf
 import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
+import org.jetbrains.kotlin.name.JvmNames
 import org.jetbrains.kotlin.name.NameUtils
 import org.jetbrains.kotlin.resolve.jvm.JAVA_LANG_RECORD_FQ_NAME
 import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodGenericSignature
@@ -195,6 +196,7 @@
     // See also: KotlinTypeMapper.forceBoxedReturnType
     private fun forceBoxedReturnType(function: IrFunction): Boolean =
         isBoxMethodForInlineClass(function) ||
+                function.origin == JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS ||
                 forceFoxedReturnTypeOnOverride(function) ||
                 forceBoxedReturnTypeOnDefaultImplFun(function) ||
                 function.isFromJava() && function.returnType.isInlineClassType()
@@ -345,24 +347,31 @@
     }
 
     private fun writeParameterType(sw: JvmSignatureWriter, type: IrType, declaration: IrDeclaration, materialized: Boolean = true) {
-        if (sw.skipGenericSignature()) {
+        var mode = if (sw.skipGenericSignature()) {
             if (type.isInlineClassType() && declaration.isFromJava()) {
-                typeMapper.mapType(type, TypeMappingMode.GENERIC_ARGUMENT, sw, materialized)
+                TypeMappingMode.GENERIC_ARGUMENT
             } else {
-                typeMapper.mapType(type, TypeMappingMode.DEFAULT, sw, materialized)
+                TypeMappingMode.DEFAULT
             }
-            return
+        } else {
+            with(typeSystem) {
+                val extractTypeMappingModeFromAnnotation = extractTypeMappingModeFromAnnotation(
+                    declaration.suppressWildcardsMode(), type, isForAnnotationParameter = false, mapTypeAliases = false
+                )
+                when {
+                    extractTypeMappingModeFromAnnotation != null -> extractTypeMappingModeFromAnnotation
+                    declaration.isMethodWithDeclarationSiteWildcards && !declaration.isStaticInlineClassReplacement && type.argumentsCount() != 0 ->
+                        TypeMappingMode.GENERIC_ARGUMENT // Render all wildcards
+                    else -> typeSystem.getOptimalModeForValueParameter(type)
+                }
+            }
         }
 
-        val mode = with(typeSystem) {
-            extractTypeMappingModeFromAnnotation(
-                declaration.suppressWildcardsMode(), type, isForAnnotationParameter = false, mapTypeAliases = false
-            )
-                ?: if (declaration.isMethodWithDeclarationSiteWildcards && !declaration.isStaticInlineClassReplacement && type.argumentsCount() != 0) {
-                    TypeMappingMode.GENERIC_ARGUMENT // Render all wildcards
-                } else {
-                    typeSystem.getOptimalModeForValueParameter(type)
-                }
+
+        if (type.classOrNull?.owner?.isExposedSingleFieldValueClass == true &&
+            declaration.origin == JvmLoweredDeclarationOrigin.FUNCTION_WITH_EXPOSED_INLINE_CLASS
+        ) {
+            mode = mode.wrapInlineClassesMode()
         }
 
         typeMapper.mapType(type, mode, sw, materialized)
diff --git a/compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt b/compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt
new file mode 100644
index 0000000..6977f08
--- /dev/null
+++ b/compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt
@@ -0,0 +1,101 @@
+// WITH_STDLIB
+// !LANGUAGE: +ContextReceivers
+// TARGET_BACKEND: JVM_IR
+// CHECK_BYTECODE_LISTING
+// FILE: Example.kt
+@file:OptIn(ExperimentalStdlibApi::class)
+
+package example
+
+import interop.*
+
+@Target(AnnotationTarget.CONSTRUCTOR)
+annotation class A
+
+interface I<T> {
+    fun virtualFunction(another: T): T
+}
+
+@JvmInline
+@JvmExposeBoxed
+value class StringWrapper(val s: String) : I<StringWrapper> {
+    @A
+    constructor(i: Int) : this(i.toString())
+
+    init {
+        require(s != "")
+    }
+
+    fun plainFunction(): Unit = println(s)
+    override fun virtualFunction(another: StringWrapper): StringWrapper = StringWrapper(s + another.s)
+}
+
+fun topLevelFunction(e: StringWrapper): StringWrapper = e.virtualFunction(e)
+
+suspend fun suspendFunction(e: StringWrapper): StringWrapper = e.virtualFunction(e)
+
+@JvmInline
+@JvmExposeBoxed
+value class IntWrapper(val i: Int) : I<IntWrapper> {
+    constructor(l: Long) : this(l.toInt())
+
+    init {
+        require(i != 0)
+    }
+
+    fun plainFunction(another: IntWrapper): IntWrapper = IntWrapper(i + another.i)
+
+    fun IntWrapper.extensionFunction(another: IntWrapper): IntWrapper = IntWrapper(this.i + this@IntWrapper.i + another.i)
+
+    fun StringWrapper.extensionFunction(another: IntWrapper): IntWrapper = IntWrapper(another.i)
+
+    override fun virtualFunction(another: IntWrapper): IntWrapper = IntWrapper(i + another.i)
+}
+
+fun topLevelFunction(e: IntWrapper): IntWrapper = e.plainFunction(e)
+
+fun IntWrapper.topLevelFunction(e: IntWrapper): IntWrapper = when {
+    e.i > 100 -> IntWrapper(42)
+    else -> e.plainFunction(e)
+}
+
+fun assertEquals(value1: Any?, value2: Any?) {
+    if (value1 != value2)
+        throw AssertionError("Expected $value1, got $value2")
+}
+
+data class HasWrappers(val i: IntWrapper = IntWrapper(42), val j: StringWrapper = StringWrapper(84))
+
+fun box(): String {
+    val intWrapper = IntWrapper(42L)
+    assertEquals(IntWrapper(84), intWrapper.plainFunction(intWrapper))
+    val stringWrapper = StringWrapper(42)
+    assertEquals(StringWrapper("4242"), stringWrapper.virtualFunction(stringWrapper))
+    assertEquals(IntWrapper(84), intWrapper.virtualFunction(intWrapper))
+    assertEquals(IntWrapper(84), topLevelFunction(intWrapper))
+    assertEquals(StringWrapper("4242"), topLevelFunction(stringWrapper))
+    val h = HasWrappers(intWrapper)
+    assertEquals(intWrapper, h.i)
+    Interop.main()
+    return "OK"
+}
+
+// FILE: interop/Interop.java
+package interop;
+
+import example.*;
+
+public class Interop {
+    public static void main() {
+        IntWrapper intWrapper = new IntWrapper(42L);
+        ExampleKt.assertEquals(new IntWrapper(84), intWrapper.plainFunction(intWrapper));
+        StringWrapper stringWrapper = new StringWrapper(42);
+        ExampleKt.assertEquals(new StringWrapper("4242"), stringWrapper.virtualFunction(stringWrapper));
+        ExampleKt.assertEquals(new IntWrapper(84), intWrapper.virtualFunction(intWrapper));
+        ExampleKt.assertEquals(new IntWrapper(84L), ExampleKt.topLevelFunction(intWrapper));
+        ExampleKt.assertEquals(new StringWrapper("4242"), ExampleKt.topLevelFunction(stringWrapper));
+        HasWrappers h = new HasWrappers(intWrapper, stringWrapper);
+        ExampleKt.assertEquals(intWrapper, h.getI());
+        ExampleKt.assertEquals(stringWrapper, h.getJ());
+    }
+}
diff --git a/compiler/testData/codegen/box/valueClasses/exposedInlineClass.txt b/compiler/testData/codegen/box/valueClasses/exposedInlineClass.txt
new file mode 100644
index 0000000..afacfe9
--- /dev/null
+++ b/compiler/testData/codegen/box/valueClasses/exposedInlineClass.txt
@@ -0,0 +1,115 @@
+@kotlin.annotation.Target(allowedTargets=[CONSTRUCTOR])
+@java.lang.annotation.Retention(value=RUNTIME)
+@java.lang.annotation.Target(value=[CONSTRUCTOR])
+@kotlin.Metadata
+public annotation class example/A {
+    // source: 'Example.kt'
+}
+
+@kotlin.Metadata
+public final class example/ExampleKt {
+    // source: 'Example.kt'
+    public final static method assertEquals(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Object): void
+    public final static @org.jetbrains.annotations.NotNull method box(): java.lang.String
+    public final static @org.jetbrains.annotations.Nullable method suspendFunction(@org.jetbrains.annotations.NotNull p0: example.StringWrapper, @org.jetbrains.annotations.NotNull p1: kotlin.coroutines.Continuation): java.lang.Object
+    public final static @org.jetbrains.annotations.Nullable method suspendFunction-gComZGY(@org.jetbrains.annotations.NotNull p0: java.lang.String, @org.jetbrains.annotations.NotNull p1: kotlin.coroutines.Continuation): java.lang.Object
+    public final static @org.jetbrains.annotations.NotNull method topLevelFunction(@org.jetbrains.annotations.NotNull p0: example.IntWrapper): example.IntWrapper
+    public final static @org.jetbrains.annotations.NotNull method topLevelFunction(@org.jetbrains.annotations.NotNull p0: example.IntWrapper, @org.jetbrains.annotations.NotNull p1: example.IntWrapper): example.IntWrapper
+    public final static @org.jetbrains.annotations.NotNull method topLevelFunction(@org.jetbrains.annotations.NotNull p0: example.StringWrapper): example.StringWrapper
+    public final static method topLevelFunction-JIAth_k(p0: int, p1: int): int
+    public final static method topLevelFunction-OeTM1uU(p0: int): int
+    public final static @org.jetbrains.annotations.NotNull method topLevelFunction-bzFBYxs(@org.jetbrains.annotations.NotNull p0: java.lang.String): java.lang.String
+}
+
+@kotlin.Metadata
+public final class example/HasWrappers {
+    // source: 'Example.kt'
+    private final field i: int
+    private final @org.jetbrains.annotations.NotNull field j: java.lang.String
+    public method <init>(@org.jetbrains.annotations.NotNull p0: example.IntWrapper, @org.jetbrains.annotations.NotNull p1: example.StringWrapper): void
+    private method <init>(p0: int, p1: java.lang.String): void
+    public synthetic method <init>(p0: int, p1: java.lang.String, p2: int, p3: kotlin.jvm.internal.DefaultConstructorMarker): void
+    public synthetic method <init>(p0: int, p1: java.lang.String, p2: kotlin.jvm.internal.DefaultConstructorMarker): void
+    public final @org.jetbrains.annotations.NotNull method component1(): example.IntWrapper
+    public final method component1-P7HTv5Y(): int
+    public final @org.jetbrains.annotations.NotNull method component2(): example.StringWrapper
+    public final @org.jetbrains.annotations.NotNull method component2-yiaqiYc(): java.lang.String
+    public final @org.jetbrains.annotations.NotNull method copy(@org.jetbrains.annotations.NotNull p0: example.IntWrapper, @org.jetbrains.annotations.NotNull p1: example.StringWrapper): example.HasWrappers
+    public synthetic static method copy-026PlWA$default(p0: example.HasWrappers, p1: int, p2: java.lang.String, p3: int, p4: java.lang.Object): example.HasWrappers
+    public final @org.jetbrains.annotations.NotNull method copy-026PlWA(p0: int, @org.jetbrains.annotations.NotNull p1: java.lang.String): example.HasWrappers
+    public method equals(@org.jetbrains.annotations.Nullable p0: java.lang.Object): boolean
+    public final @org.jetbrains.annotations.NotNull method getI(): example.IntWrapper
+    public final method getI-P7HTv5Y(): int
+    public final @org.jetbrains.annotations.NotNull method getJ(): example.StringWrapper
+    public final @org.jetbrains.annotations.NotNull method getJ-yiaqiYc(): java.lang.String
+    public method hashCode(): int
+    public @org.jetbrains.annotations.NotNull method toString(): java.lang.String
+}
+
+@kotlin.Metadata
+public interface example/I {
+    // source: 'Example.kt'
+    public abstract method virtualFunction(p0: java.lang.Object): java.lang.Object
+}
+
+@kotlin.jvm.JvmInline
+@kotlin.Metadata
+@kotlin.jvm.JvmExposeBoxed
+public final class example/IntWrapper {
+    // source: 'Example.kt'
+    private final field i: int
+    public method <init>(p0: int): void
+    private synthetic method <init>(p0: int, p1: java.lang.Void): void
+    public method <init>(p0: long): void
+    public synthetic final static method box-impl(p0: int): example.IntWrapper
+    public static method constructor-impl(p0: int): int
+    public static method constructor-impl(p0: long): int
+    public method equals(p0: java.lang.Object): boolean
+    public static method equals-impl(p0: int, p1: java.lang.Object): boolean
+    public final static method equals-impl0(p0: int, p1: int): boolean
+    public final @org.jetbrains.annotations.NotNull method extensionFunction(@org.jetbrains.annotations.NotNull p0: example.IntWrapper, @org.jetbrains.annotations.NotNull p1: example.IntWrapper): example.IntWrapper
+    public final @org.jetbrains.annotations.NotNull method extensionFunction(@org.jetbrains.annotations.NotNull p0: example.StringWrapper, @org.jetbrains.annotations.NotNull p1: example.IntWrapper): example.IntWrapper
+    public final static method extensionFunction-7sm2r4Y(p0: int, @org.jetbrains.annotations.NotNull p1: java.lang.String, p2: int): int
+    public final static method extensionFunction-ke8FMIE(p0: int, p1: int, p2: int): int
+    public final method getI(): int
+    public method hashCode(): int
+    public static method hashCode-impl(p0: int): int
+    public final @org.jetbrains.annotations.NotNull method plainFunction(@org.jetbrains.annotations.NotNull p0: example.IntWrapper): example.IntWrapper
+    public final static method plainFunction-IkJdR0s(p0: int, p1: int): int
+    public method toString(): java.lang.String
+    public static method toString-impl(p0: int): java.lang.String
+    public synthetic final method unbox-impl(): int
+    public @org.jetbrains.annotations.NotNull method virtualFunction(@org.jetbrains.annotations.NotNull p0: example.IntWrapper): example.IntWrapper
+    public synthetic bridge method virtualFunction(p0: java.lang.Object): java.lang.Object
+    public method virtualFunction-IkJdR0s(p0: int): int
+    public static method virtualFunction-IkJdR0s(p0: int, p1: int): int
+}
+
+@kotlin.jvm.JvmInline
+@kotlin.Metadata
+@kotlin.jvm.JvmExposeBoxed
+public final class example/StringWrapper {
+    // source: 'Example.kt'
+    private final @org.jetbrains.annotations.NotNull field s: java.lang.String
+    public method <init>(@org.jetbrains.annotations.NotNull p0: java.lang.String): void
+    public @example.A method <init>(p0: int): void
+    private synthetic method <init>(p0: java.lang.String, p1: java.lang.Void): void
+    public synthetic final static method box-impl(p0: java.lang.String): example.StringWrapper
+    public static @org.jetbrains.annotations.NotNull method constructor-impl(@org.jetbrains.annotations.NotNull p0: java.lang.String): java.lang.String
+    public static @example.A @org.jetbrains.annotations.NotNull method constructor-impl(p0: int): java.lang.String
+    public method equals(p0: java.lang.Object): boolean
+    public static method equals-impl(p0: java.lang.String, p1: java.lang.Object): boolean
+    public final static method equals-impl0(p0: java.lang.String, p1: java.lang.String): boolean
+    public final @org.jetbrains.annotations.NotNull method getS(): java.lang.String
+    public method hashCode(): int
+    public static method hashCode-impl(p0: java.lang.String): int
+    public final method plainFunction(): void
+    public final static method plainFunction-impl(p0: java.lang.String): void
+    public method toString(): java.lang.String
+    public static method toString-impl(p0: java.lang.String): java.lang.String
+    public synthetic final method unbox-impl(): java.lang.String
+    public @org.jetbrains.annotations.NotNull method virtualFunction(@org.jetbrains.annotations.NotNull p0: example.StringWrapper): example.StringWrapper
+    public synthetic bridge method virtualFunction(p0: java.lang.Object): java.lang.Object
+    public @org.jetbrains.annotations.NotNull method virtualFunction-cnKg8a0(@org.jetbrains.annotations.NotNull p0: java.lang.String): java.lang.String
+    public static @org.jetbrains.annotations.NotNull method virtualFunction-cnKg8a0(p0: java.lang.String, @org.jetbrains.annotations.NotNull p1: java.lang.String): java.lang.String
+}
diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java
index 292915f..d6a81e0 100644
--- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java
+++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java
@@ -53188,6 +53188,12 @@
         }
 
         @Test
+        @TestMetadata("exposedInlineClass.kt")
+        public void testExposedInlineClass() throws Exception {
+            runTest("compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt");
+        }
+
+        @Test
         @TestMetadata("fakeOverrideCall.kt")
         public void testFakeOverrideCall() throws Exception {
             runTest("compiler/testData/codegen/box/valueClasses/fakeOverrideCall.kt");
diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenWithIrInlinerTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenWithIrInlinerTestGenerated.java
index bd3fd4d..bc01bbe 100644
--- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenWithIrInlinerTestGenerated.java
+++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenWithIrInlinerTestGenerated.java
@@ -53188,6 +53188,12 @@
         }
 
         @Test
+        @TestMetadata("exposedInlineClass.kt")
+        public void testExposedInlineClass() throws Exception {
+            runTest("compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt");
+        }
+
+        @Test
         @TestMetadata("fakeOverrideCall.kt")
         public void testFakeOverrideCall() throws Exception {
             runTest("compiler/testData/codegen/box/valueClasses/fakeOverrideCall.kt");
diff --git a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java
index a8278e9..8c094b4 100644
--- a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java
+++ b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java
@@ -42977,6 +42977,11 @@
             runTest("compiler/testData/codegen/box/valueClasses/equality.kt");
         }
 
+        @TestMetadata("exposedInlineClass.kt")
+        public void testExposedInlineClass() throws Exception {
+            runTest("compiler/testData/codegen/box/valueClasses/exposedInlineClass.kt");
+        }
+
         @TestMetadata("fakeOverrideCall.kt")
         public void testFakeOverrideCall() throws Exception {
             runTest("compiler/testData/codegen/box/valueClasses/fakeOverrideCall.kt");
diff --git a/core/compiler.common.jvm/src/org/jetbrains/kotlin/name/JvmNames.kt b/core/compiler.common.jvm/src/org/jetbrains/kotlin/name/JvmNames.kt
index f9309b1..1e22d5a 100644
--- a/core/compiler.common.jvm/src/org/jetbrains/kotlin/name/JvmNames.kt
+++ b/core/compiler.common.jvm/src/org/jetbrains/kotlin/name/JvmNames.kt
@@ -14,6 +14,15 @@
 
     val JVM_NAME_SHORT: String = JVM_NAME.shortName().asString()
 
+    @JvmField
+    val JVM_EXPOSE_BOXED: FqName = FqName("kotlin.jvm.JvmExposeBoxed")
+
+    @JvmField
+    val JVM_EXPOSE_BOXED_CLASS_ID = ClassId.topLevel(JVM_EXPOSE_BOXED)
+
+    @JvmField
+    val JVM_EXPOSE_BOXED_SHORT: String = JVM_EXPOSE_BOXED.shortName().asString()
+
     val JVM_MULTIFILE_CLASS: FqName = FqName("kotlin.jvm.JvmMultifileClass")
     val JVM_MULTIFILE_CLASS_ID: ClassId = ClassId(FqName("kotlin.jvm"), Name.identifier("JvmMultifileClass"))
     val JVM_MULTIFILE_CLASS_SHORT = JVM_MULTIFILE_CLASS.shortName().asString()
diff --git a/core/descriptors/src/org/jetbrains/kotlin/resolve/inlineClassesUtils.kt b/core/descriptors/src/org/jetbrains/kotlin/resolve/inlineClassesUtils.kt
index ef0baa2..062d2ed 100644
--- a/core/descriptors/src/org/jetbrains/kotlin/resolve/inlineClassesUtils.kt
+++ b/core/descriptors/src/org/jetbrains/kotlin/resolve/inlineClassesUtils.kt
@@ -19,6 +19,9 @@
 val JVM_INLINE_ANNOTATION_FQ_NAME = FqName("kotlin.jvm.JvmInline")
 val JVM_INLINE_ANNOTATION_CLASS_ID = ClassId.topLevel(JVM_INLINE_ANNOTATION_FQ_NAME)
 
+val JVM_EXPOSE_BOXED_ANNOTATION_FQ_NAME = FqName("kotlin.jvm.JvmExposeBoxed")
+val JVM_EXPOSE_BOXED_ANNOTATION_CLASS_ID = ClassId.topLevel(JVM_EXPOSE_BOXED_ANNOTATION_FQ_NAME)
+
 // FIXME: DeserializedClassDescriptor in reflection do not have @JvmInline annotation, that we
 // FIXME: would like to check as well.
 fun DeclarationDescriptor.isInlineClass(): Boolean = this is ClassDescriptor && this.valueClassRepresentation is InlineClassRepresentation
diff --git a/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt b/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt
index 6536780..279ccdd 100644
--- a/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt
+++ b/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt
@@ -216,3 +216,24 @@
 @SinceKotlin("1.8")
 @OptionalExpectation
 public expect annotation class JvmSerializableLambda()
+
+/**
+ * Instructs the compiler
+ * to expose the API related to the annotated [kotlin.jvm.JvmInline] value classes as its boxed variant for effective usage from Java.
+ *
+ * Particularly, it performs the following transformations:
+ *
+ * - For functions and constructors that take or return inline classes,
+ * a wrapper declaration for Java is created where inline classes are boxed.
+ * - Functions declared in the marked class can be called normally from Java.
+ * - Constructor available from Java is added.
+ *
+ * These additions preserve backwards compatibility (both binary and source), so existing inline classes can be marked safely.
+ */
+@Target(CLASS)
+@Retention(AnnotationRetention.BINARY)
+@MustBeDocumented
+@SinceKotlin("1.9")
+@OptionalExpectation
+@ExperimentalStdlibApi
+public expect annotation class JvmExposeBoxed()
diff --git a/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt b/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt
index 93ce1a2..6e95eae 100644
--- a/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt
+++ b/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt
@@ -177,3 +177,23 @@
 @MustBeDocumented
 @SinceKotlin("1.5")
 public actual annotation class JvmRecord
+
+/**
+ * Instructs the compiler
+ * to expose the API related to the annotated [kotlin.jvm.JvmInline] value classes as its boxed variant for effective usage from Java.
+ *
+ * Particularly, it performs the following transformations:
+ *
+ * - For functions and constructors that take or return inline classes,
+ * a wrapper declaration for Java is created where inline classes are boxed.
+ * - Functions declared in the marked class can be called normally from Java.
+ * - Constructor available from Java is added.
+ *
+ * These additions preserve backwards compatibility (both binary and source), so existing inline classes can be marked safely.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+@MustBeDocumented
+@SinceKotlin("1.9")
+@ExperimentalStdlibApi
+public actual annotation class JvmExposeBoxed
diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
index abd1d00..914b200 100644
--- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
+++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
@@ -3418,6 +3418,9 @@
 public abstract interface annotation class kotlin/jvm/JvmDefaultWithoutCompatibility : java/lang/annotation/Annotation {
 }
 
+public abstract interface annotation class kotlin/jvm/JvmExposeBoxed : java/lang/annotation/Annotation {
+}
+
 public abstract interface annotation class kotlin/jvm/JvmField : java/lang/annotation/Annotation {
 }