[Analysis API] Prototype new rendering engine
diff --git a/analysis/analysis-api-fir/resources/META-INF/analysis-api/analysis-api-fir.xml b/analysis/analysis-api-fir/resources/META-INF/analysis-api/analysis-api-fir.xml
index 5836635..303499f 100644
--- a/analysis/analysis-api-fir/resources/META-INF/analysis-api/analysis-api-fir.xml
+++ b/analysis/analysis-api-fir/resources/META-INF/analysis-api/analysis-api-fir.xml
@@ -44,6 +44,11 @@
         serviceImplementation="org.jetbrains.kotlin.analysis.api.fir.projectStructure.KaFirDanglingFileResolutionModeProvider"
     />
 
+    <applicationService
+        serviceInterface="org.jetbrains.kotlin.analysis.api.internals.KaRendererProvider"
+        serviceImplementation="org.jetbrains.kotlin.analysis.api.fir.rendering.KaRendererProviderImpl"
+    />
+
     <projectService
         serviceInterface="org.jetbrains.kotlin.references.utils.KotlinKDocResolutionStrategyProviderService"
         serviceImplementation="org.jetbrains.kotlin.analysis.api.fir.references.KotlinFirKDocResolutionStrategyProviderService"
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaFirRendererProvider.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaFirRendererProvider.kt
new file mode 100644
index 0000000..ddfc8ed
--- /dev/null
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaFirRendererProvider.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.fir.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.base.KaConstantValue
+import org.jetbrains.kotlin.analysis.api.rendering.KaPiece
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderer
+import org.jetbrains.kotlin.analysis.api.rendering.KaRendererBuilder
+import org.jetbrains.kotlin.analysis.api.rendering.KaPieceRenderer
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOption
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOutput
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingContext
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+import org.jetbrains.kotlin.analysis.api.types.KaType
+
+@Suppress("UNCHECKED_CAST")
+internal class KaRendererImpl(val renderers: KaPieceRendererMap, val options: KaRenderingOptionMap) : KaRenderer {
+    context(session: KaSession)
+    override fun render(symbol: KaSymbol, output: KaRenderingOutput) {
+        render(symbol, KaPiece.Symbol, createContext(output), output)
+    }
+
+    context(session: KaSession)
+    override fun render(type: KaType, output: KaRenderingOutput) {
+        render(type, KaPiece.Type, createContext(output), output)
+    }
+
+    context(session: KaSession)
+    override fun render(constantValue: KaConstantValue, output: KaRenderingOutput) {
+        render(constantValue, KaPiece.ConstantValue, createContext(output), output)
+    }
+
+    private fun createContext(output: KaRenderingOutput): KaRenderingContext {
+        return object : KaRenderingContext {
+            context(session: KaSession)
+            override fun <T> render(value: T, piece: KaPiece<T>) {
+                render(value, piece, this, output)
+            }
+
+            override fun <T> valueFor(option: KaRenderingOption<T>): T {
+                return (options[option] as T?) ?: option.defaultValue
+            }
+        }
+    }
+
+    context(session: KaSession)
+    private fun <T> render(value: T, piece: KaPiece<T>, context: KaRenderingContext, output: KaRenderingOutput) {
+        context(output, context) {
+            @Suppress("UNCHECKED_CAST")
+            val rendererTower = renderers.getValue(piece) as List<KaPieceRenderer<T>>
+            render(value, rendererTower, rendererTower.lastIndex)
+        }
+    }
+
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    private fun <T> render(value: T, rendererTower: List<KaPieceRenderer<T>>, topIndex: Int) {
+        var index = topIndex
+
+        val next = object : () -> Unit {
+            var nextIndex = 0
+
+            override fun invoke() {
+                if (nextIndex < 0) {
+                    error("The built-in renderer must not delegate")
+                }
+                render(value, rendererTower, nextIndex)
+            }
+        }
+
+        while (index >= 0) {
+            next.nextIndex = index - 1
+            val renderer = rendererTower[index]
+            if (renderer.render(value, next)) {
+                return // Value is handled
+            }
+            index -= 1
+        }
+    }
+
+    override fun copy(block: KaRendererBuilder.() -> Unit) = buildRenderer(this, block)
+}
+
+internal typealias KaPieceRendererMap = Map<KaPiece<*>, List<KaPieceRenderer<*>>>
+internal typealias KaRenderingOptionMap = Map<KaRenderingOption<*>, *>
+
+internal fun buildRenderer(origin: KaRendererImpl?, block: KaRendererBuilder.() -> Unit): KaRendererImpl {
+    val renderers: MutableMap<KaPiece<*>, List<KaPieceRenderer<*>>> = origin?.renderers?.toMutableMap() ?: HashMap()
+    val options: MutableMap<KaRenderingOption<*>, Any?> = origin?.options?.toMutableMap() ?: HashMap()
+
+    val builder = object : KaRendererBuilder {
+        override fun <T> push(renderer: KaPieceRenderer<T>) {
+            renderers.compute(renderer.piece) { _, oldValue ->
+                if (oldValue == null) {
+                    listOf(renderer)
+                } else {
+                    oldValue + renderer
+                }
+            }
+        }
+
+        override fun <T> set(option: KaRenderingOption<T>, value: T) {
+            options[option] = value
+        }
+
+        override fun <T> unset(option: KaRenderingOption<T>) {
+            options.remove(option)
+        }
+    }
+
+    builder.block()
+
+    return KaRendererImpl(renderers, options)
+}
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaRendererProviderImpl.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaRendererProviderImpl.kt
new file mode 100644
index 0000000..6363bb5
--- /dev/null
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/KaRendererProviderImpl.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.fir.rendering
+
+import org.jetbrains.kotlin.analysis.api.internals.KaRendererProvider
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderer
+
+internal class KaRendererProviderImpl : KaRendererProvider {
+    override val defaultRenderer: KaRenderer
+        get() = DEFAULT_RENDERER
+}
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/defaultRenderers.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/defaultRenderers.kt
new file mode 100644
index 0000000..6bba8e0
--- /dev/null
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/rendering/defaultRenderers.kt
@@ -0,0 +1,1489 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.fir.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.KaNonPublicApi
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotated
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotation
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotationValue
+import org.jetbrains.kotlin.analysis.api.base.KaConstantValue
+import org.jetbrains.kotlin.analysis.api.rendering.KaClassTypeQualification
+import org.jetbrains.kotlin.analysis.api.rendering.KaPiece
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderer
+import org.jetbrains.kotlin.analysis.api.rendering.KaPieceRenderer
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOption
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOutput
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingContext
+import org.jetbrains.kotlin.analysis.api.rendering.KaTextAttribute
+import org.jetbrains.kotlin.analysis.api.rendering.KaTypeApproximation
+import org.jetbrains.kotlin.analysis.api.rendering.render
+import org.jetbrains.kotlin.analysis.api.scopes.declaredMemberScope
+import org.jetbrains.kotlin.analysis.api.symbols.KaAnonymousFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaAnonymousObjectSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaBackingFieldSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaContextParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaEnumEntrySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaJavaFieldSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaKotlinPropertySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaLocalVariableSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaNamedClassSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPackageSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertyAccessorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertyGetterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySetterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaReceiverParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSamConstructorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolModality
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolVisibility
+import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.containingDeclaration
+import org.jetbrains.kotlin.analysis.api.symbols.findPackage
+import org.jetbrains.kotlin.analysis.api.symbols.markers.KaNamedSymbol
+import org.jetbrains.kotlin.analysis.api.types.KaCapturedType
+import org.jetbrains.kotlin.analysis.api.types.KaClassErrorType
+import org.jetbrains.kotlin.analysis.api.types.KaClassType
+import org.jetbrains.kotlin.analysis.api.types.KaDefinitelyNotNullType
+import org.jetbrains.kotlin.analysis.api.types.KaDynamicType
+import org.jetbrains.kotlin.analysis.api.types.KaErrorType
+import org.jetbrains.kotlin.analysis.api.types.KaFlexibleType
+import org.jetbrains.kotlin.analysis.api.types.KaFunctionType
+import org.jetbrains.kotlin.analysis.api.types.KaIntersectionType
+import org.jetbrains.kotlin.analysis.api.types.KaResolvedClassTypeQualifier
+import org.jetbrains.kotlin.analysis.api.types.KaStarTypeProjection
+import org.jetbrains.kotlin.analysis.api.types.KaType
+import org.jetbrains.kotlin.analysis.api.types.KaTypeArgumentWithVariance
+import org.jetbrains.kotlin.analysis.api.types.KaTypeParameterType
+import org.jetbrains.kotlin.analysis.api.types.KaTypeProjection
+import org.jetbrains.kotlin.analysis.api.types.approximateToDenotableSubtypeOrSelf
+import org.jetbrains.kotlin.analysis.api.types.approximateToDenotableSupertypeOrSelf
+import org.jetbrains.kotlin.analysis.api.types.classId
+import org.jetbrains.kotlin.analysis.api.types.expandedSymbol
+import org.jetbrains.kotlin.analysis.api.types.isMarkedNullable
+import org.jetbrains.kotlin.analysis.api.types.isNullable
+import org.jetbrains.kotlin.name.CallableId
+import org.jetbrains.kotlin.name.ClassId
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.name.SpecialNames
+import org.jetbrains.kotlin.name.StandardClassIds
+import org.jetbrains.kotlin.name.render
+import org.jetbrains.kotlin.types.Variance
+
+internal val DEFAULT_RENDERER: KaRenderer = buildRenderer(null) {
+    // Top-level dispatchers.
+    push(SymbolRenderer)
+    push(TypeRenderer)
+
+    // Shared building blocks.
+    push(SymbolNameRenderer)
+    push(SymbolModifiersRenderer)
+    push(VisibilityRenderer)
+    push(ModalityRenderer)
+    push(AnnotationsRenderer)
+    push(AnnotationRenderer)
+    push(AnnotationValuesRenderer)
+    push(AnnotationValueRenderer)
+    push(ConstantValueRenderer)
+    push(TypeAnnotationsRenderer)
+    push(TypeNullabilityRenderer)
+
+    // Types.
+    push(ClassTypeRenderer)
+    push(FunctionTypeRenderer)
+    push(TypeParameterTypeRenderer)
+    push(CapturedTypeRenderer)
+    push(DefinitelyNotNullTypeRenderer)
+    push(FlexibleTypeRenderer)
+    push(IntersectionTypeRenderer)
+    push(DynamicTypeRenderer)
+    push(ErrorTypeRenderer)
+    push(TypeArgumentListRenderer)
+    push(TypeProjectionRenderer)
+
+    // Type parameters.
+    push(TypeParameterListRenderer)
+    push(CallableTypeParameterListRenderer)
+    push(ClassifierTypeParameterListRenderer)
+    push(TypeParameterRenderer)
+    push(WhereClauseRenderer)
+    push(CallableWhereClauseRenderer)
+    push(ClassifierWhereClauseRenderer)
+
+    // Context parameters.
+    push(ContextParameterListRenderer)
+    push(CallableContextParameterListRenderer)
+    push(ContextParameterRenderer)
+    push(ContextParameterAnnotationsRenderer)
+    push(ContextParameterTypeRenderer)
+
+    // Value parameters.
+    push(ValueParameterListRenderer)
+    push(ValueParameterRenderer)
+    push(ValueParameterAnnotationsRenderer)
+    push(ValueParameterTypeRenderer)
+    push(ValueParameterDefaultValueRenderer)
+
+    // Functions.
+    push(FunctionRenderer)
+    push(FunctionModifiersRenderer)
+    push(FunctionAnnotationsRenderer)
+    push(FunctionReceiverRenderer)
+    push(FunctionValueParameterListRenderer)
+    push(FunctionReturnTypeRenderer)
+    push(NamedFunctionReturnTypeRenderer)
+    push(NamedFunctionRenderer)
+    push(ConstructorRenderer)
+    push(PropertyAccessorRenderer)
+
+    // Properties and variables.
+    push(PropertyRenderer)
+    push(PropertyModifiersRenderer)
+    push(PropertyAnnotationsRenderer)
+    push(PropertyReturnTypeRenderer)
+    push(PropertyAccessorsRenderer)
+    push(LocalVariableRenderer)
+    push(JavaFieldRenderer)
+    push(BackingFieldRenderer)
+    push(EnumEntryRenderer)
+
+    // Classifiers.
+    push(NamedClassRenderer)
+    push(AnonymousObjectRenderer)
+    push(TypeAliasRenderer)
+    push(PrimaryConstructorRenderer)
+    push(ClassModifiersRenderer)
+    push(ClassAnnotationsRenderer)
+    push(SupertypeListRenderer)
+    push(SupertypeRenderer)
+    push(ClassBodyRenderer)
+
+    // Packages.
+    push(PackageRenderer)
+}
+
+// -------------------------------------------------------------------------------------------------
+// Shared helpers.
+// -------------------------------------------------------------------------------------------------
+
+context(output: KaRenderingOutput)
+private fun keyword(text: String) {
+    output.append(text, KaTextAttribute.Keyword).space()
+}
+
+/**
+ * The visibility of [symbol] as it is written in source code. A primary constructor of an enum class or an object is implicitly private,
+ * so its visibility is reported as public and is not rendered.
+ */
+context(session: KaSession)
+private fun effectiveVisibility(symbol: KaDeclarationSymbol): KaSymbolVisibility {
+    if (symbol is KaConstructorSymbol && symbol.isPrimary) {
+        val containingClass = symbol.containingDeclaration as? KaClassSymbol
+        if (containingClass != null && (containingClass.classKind == KaClassKind.ENUM_CLASS || containingClass.classKind.isObject)) {
+            return KaSymbolVisibility.PUBLIC
+        }
+    }
+    return symbol.visibility
+}
+
+context(output: KaRenderingOutput)
+private fun identifier(name: String, symbol: KaSymbol) {
+    output.append(name, setOf(KaTextAttribute.Identifier, KaTextAttribute.Symbol(symbol)))
+}
+
+context(output: KaRenderingOutput)
+private fun identifier(name: Name, symbol: KaSymbol) {
+    // `Name.render()` wraps keywords and names with special characters (e.g. `<set-?>`) in backticks.
+    identifier(name.render(), symbol)
+}
+
+/**
+ * Renders a package fully-qualified name segment by segment (e.g. `org.example`), linking each segment to the [KaPackageSymbol] of the
+ * package it forms, when that package can be resolved.
+ */
+context(session: KaSession, output: KaRenderingOutput)
+private fun renderPackageQualifier(packageFqName: FqName) {
+    var current = FqName.ROOT
+    packageFqName.pathSegments().forEachIndexed { index, segment ->
+        if (index > 0) output.append(".", KaTextAttribute.Punctuation)
+        current = current.child(segment)
+        val packageSymbol = findPackage(current)
+        if (packageSymbol != null) {
+            identifier(segment, packageSymbol)
+        } else {
+            output.append(segment.render(), KaTextAttribute.Identifier)
+        }
+    }
+}
+
+/**
+ * Renders a receiver type of extension callable or a function type, wrapping it in parentheses when required by the Kotlin grammar
+ * (function types, definitely non-nullable types, and nullable types).
+ */
+context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+private fun renderReceiverType(type: KaType) {
+    val parenthesized = type is KaFunctionType || type is KaDefinitelyNotNullType || type.isMarkedNullable
+    if (parenthesized) output.append("(", KaTextAttribute.GroupStart)
+    render(type, KaPiece.Type)
+    if (parenthesized) output.append(")", KaTextAttribute.GroupEnd)
+}
+
+// -------------------------------------------------------------------------------------------------
+// Symbols: top-level dispatcher.
+// -------------------------------------------------------------------------------------------------
+
+private object SymbolRenderer : KaPieceRenderer<KaSymbol>(KaPiece.Symbol) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaSymbol, next: () -> Unit): Boolean {
+        when (value) {
+            is KaNamedFunctionSymbol -> render(value, KaPiece.NamedFunction)
+            is KaConstructorSymbol -> render(value, KaPiece.Constructor)
+            is KaPropertyAccessorSymbol -> render(value, KaPiece.PropertyAccessor)
+            is KaSamConstructorSymbol -> render(value, KaPiece.Function)
+            is KaAnonymousFunctionSymbol -> render(value, KaPiece.Function)
+            is KaPropertySymbol -> render(value, KaPiece.Property)
+            is KaLocalVariableSymbol -> render(value, KaPiece.LocalVariable)
+            is KaJavaFieldSymbol -> render(value, KaPiece.JavaField)
+            is KaBackingFieldSymbol -> render(value, KaPiece.BackingField)
+            is KaEnumEntrySymbol -> render(value, KaPiece.EnumEntry)
+            is KaValueParameterSymbol -> render(value, KaPiece.ValueParameter)
+            is KaContextParameterSymbol -> render(value, KaPiece.ContextParameter)
+            is KaReceiverParameterSymbol -> render(value, KaPiece.FunctionReceiver)
+            is KaNamedClassSymbol -> render(value, KaPiece.NamedClass)
+            is KaAnonymousObjectSymbol -> render(value, KaPiece.AnonymousObject)
+            is KaTypeAliasSymbol -> render(value, KaPiece.TypeAlias)
+            is KaTypeParameterSymbol -> render(value, KaPiece.TypeParameter)
+            is KaPackageSymbol -> render(value, KaPiece.Package)
+            else -> render(value, KaPiece.SymbolName)
+        }
+        return true
+    }
+}
+
+private object SymbolNameRenderer : KaPieceRenderer<KaSymbol>(KaPiece.SymbolName) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaSymbol, next: () -> Unit): Boolean {
+        when (value) {
+            // A package is declared by its fully qualified name, e.g. `package org.example`.
+            is KaPackageSymbol -> identifier(value.fqName.render(), value)
+            is KaNamedSymbol -> identifier(value.name, value)
+            else -> output.append("<symbol>", KaTextAttribute.Identifier)
+        }
+        return true
+    }
+}
+
+private object SymbolModifiersRenderer : KaPieceRenderer<KaDeclarationSymbol>(KaPiece.SymbolModifiers) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaDeclarationSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Visibility)
+        if (value.isExpect) keyword("expect")
+        if (value.isActual) keyword("actual")
+        if (value.isExternal) keyword("external")
+        return true
+    }
+}
+
+private object VisibilityRenderer : KaPieceRenderer<KaDeclarationSymbol>(KaPiece.Visibility) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaDeclarationSymbol, next: () -> Unit): Boolean {
+        val text = when (effectiveVisibility(value)) {
+            KaSymbolVisibility.PRIVATE -> "private"
+            KaSymbolVisibility.PROTECTED -> "protected"
+            KaSymbolVisibility.INTERNAL -> "internal"
+            else -> return true
+        }
+        keyword(text)
+        return true
+    }
+}
+
+private object ModalityRenderer : KaPieceRenderer<KaDeclarationSymbol>(KaPiece.Modality) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaDeclarationSymbol, next: () -> Unit): Boolean {
+        if (!shouldRenderModality(value)) return true
+
+        val text = when (value.modality) {
+            KaSymbolModality.ABSTRACT -> "abstract"
+            KaSymbolModality.OPEN -> "open"
+            KaSymbolModality.SEALED -> "sealed"
+            KaSymbolModality.FINAL -> return true
+        }
+        keyword(text)
+        return true
+    }
+
+    /**
+     * Whether the modality of [symbol] is meaningful:
+     *  - only plain classes carry an explicit modality; interfaces, objects, enums, and annotation classes have an implicit one;
+     *  - interface members are implicitly `abstract`/`open`, so their modality is never rendered;
+     *  - `open` is redundant when the containing class is `final` (a final class or object cannot be inherited from).
+     */
+    context(session: KaSession)
+    private fun shouldRenderModality(symbol: KaDeclarationSymbol): Boolean {
+        if (symbol is KaClassSymbol) {
+            return symbol.classKind == KaClassKind.CLASS
+        }
+
+        if (symbol !is KaCallableSymbol) return true
+
+        val containingClass = symbol.containingDeclaration as? KaClassSymbol
+        if (containingClass?.classKind == KaClassKind.INTERFACE) return false
+        if (symbol.modality == KaSymbolModality.OPEN && containingClass?.modality == KaSymbolModality.FINAL) return false
+
+        return true
+    }
+}
+
+private object AnnotationsRenderer : KaPieceRenderer<KaAnnotated>(KaPiece.Annotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaAnnotated, next: () -> Unit): Boolean {
+        val annotations = value.annotations
+        if (annotations.isEmpty()) return true
+
+        val onNewLine = context.valueFor(KaRenderingOption.AnnotationsOnNewLine)(session, context, value)
+        for (annotation in annotations) {
+            render(annotation, KaPiece.Annotation)
+            if (onNewLine) output.newLine() else output.space()
+        }
+        return true
+    }
+}
+
+private object AnnotationRenderer : KaPieceRenderer<KaAnnotation>(KaPiece.Annotation) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaAnnotation, next: () -> Unit): Boolean {
+        output.append("@", KaTextAttribute.Punctuation)
+        renderAnnotationName(value.classId)
+        render(value, KaPiece.AnnotationValues)
+        return true
+    }
+
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    private fun renderAnnotationName(classId: ClassId?) {
+        if (classId == null) {
+            output.append("ERROR", KaTextAttribute.Identifier)
+            return
+        }
+
+        val qualification = context.valueFor(KaRenderingOption.ClassTypeQualification)
+
+        if (qualification == KaClassTypeQualification.FULLY_QUALIFIED) {
+            val packageFqName = classId.packageFqName
+            if (!packageFqName.isRoot && packageFqName != CallableId.PACKAGE_FQ_NAME_FOR_LOCAL) {
+                renderPackageQualifier(packageFqName)
+                output.append(".", KaTextAttribute.Punctuation)
+            }
+        }
+
+        // `SIMPLE` keeps only the innermost segment; the other modes render all outer classifiers.
+        val segments = when (qualification) {
+            KaClassTypeQualification.SIMPLE -> listOf(classId.shortClassName)
+            else -> classId.relativeClassName.pathSegments()
+        }
+
+        segments.forEachIndexed { index, segment ->
+            if (index > 0) output.append(".", KaTextAttribute.Punctuation)
+            output.append(segment.render(), KaTextAttribute.Identifier)
+        }
+    }
+}
+
+private object AnnotationValuesRenderer : KaPieceRenderer<KaAnnotation>(KaPiece.AnnotationValues) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaAnnotation, next: () -> Unit): Boolean {
+        val arguments = value.arguments
+        if (arguments.isEmpty()) return true
+
+        output.append("(", KaTextAttribute.GroupStart)
+        arguments.forEachIndexed { index, argument ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            output.append(argument.name.asString(), KaTextAttribute.Identifier)
+            output.append(" = ", KaTextAttribute.Punctuation)
+            render(argument.expression, KaPiece.AnnotationValue)
+        }
+        output.append(")", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object AnnotationValueRenderer : KaPieceRenderer<KaAnnotationValue>(KaPiece.AnnotationValue) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaAnnotationValue, next: () -> Unit): Boolean {
+        when (value) {
+            is KaAnnotationValue.ConstantValue -> render(value.value, KaPiece.ConstantValue)
+            is KaAnnotationValue.EnumEntryValue -> {
+                val callableId = value.callableId
+                if (callableId != null) {
+                    callableId.className?.let { className ->
+                        output.append(className.shortName().asString(), KaTextAttribute.Identifier)
+                        output.append(".", KaTextAttribute.Punctuation)
+                    }
+                    output.append(callableId.callableName.asString(), KaTextAttribute.Identifier)
+                } else {
+                    output.append("ERROR", KaTextAttribute.Identifier)
+                }
+            }
+            is KaAnnotationValue.ClassLiteralValue -> {
+                render(value.type, KaPiece.Type)
+                output.append("::", KaTextAttribute.Punctuation)
+                output.append("class", KaTextAttribute.Keyword)
+            }
+            is KaAnnotationValue.NestedAnnotationValue -> {
+                render(value.annotation, KaPiece.Annotation)
+            }
+            is KaAnnotationValue.ArrayValue -> {
+                output.append("[", KaTextAttribute.GroupStart)
+                value.values.forEachIndexed { index, element ->
+                    if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+                    render(element, KaPiece.AnnotationValue)
+                }
+                output.append("]", KaTextAttribute.GroupEnd)
+            }
+            is KaAnnotationValue.UnsupportedValue -> {
+                output.append("ERROR", KaTextAttribute.Identifier)
+            }
+        }
+        return true
+    }
+}
+
+private object ConstantValueRenderer : KaPieceRenderer<KaConstantValue>(KaPiece.ConstantValue) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaConstantValue, next: () -> Unit): Boolean {
+        when (value) {
+            is KaConstantValue.NullValue, is KaConstantValue.BooleanValue -> {
+                output.append(value.render(), KaTextAttribute.Keyword)
+            }
+            is KaConstantValue.StringValue, is KaConstantValue.CharValue -> {
+                output.append(value.render(), KaTextAttribute.StringLiteral)
+            }
+            is KaConstantValue.ErrorValue -> {
+                output.append("ERROR", KaTextAttribute.Identifier)
+            }
+            else -> {
+                output.append(value.render(), KaTextAttribute.NumberLiteral)
+            }
+        }
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Functions.
+// -------------------------------------------------------------------------------------------------
+
+private object NamedFunctionRenderer : KaPieceRenderer<KaNamedFunctionSymbol>(KaPiece.NamedFunction) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaNamedFunctionSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.FunctionAnnotations)
+        render(value, KaPiece.CallableContextParameterList)
+        render(value, KaPiece.FunctionModifiers)
+        keyword("fun")
+
+        if (value.typeParameters.isNotEmpty()) {
+            render(value, KaPiece.CallableTypeParameterList)
+            output.space()
+        }
+
+        value.receiverParameter?.let { render(it, KaPiece.FunctionReceiver) }
+
+        render(value, KaPiece.SymbolName)
+
+        render(value, KaPiece.FunctionValueParameterList)
+
+        render(value, KaPiece.NamedFunctionReturnType)
+
+        render(value, KaPiece.CallableWhereClause)
+
+        return true
+    }
+}
+
+/** Generic function renderer used for anonymous functions and SAM constructors. */
+private object FunctionRenderer : KaPieceRenderer<KaFunctionSymbol>(KaPiece.Function) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.FunctionModifiers)
+        keyword("fun")
+
+        value.receiverParameter?.let { render(it, KaPiece.FunctionReceiver) }
+
+        if (value is KaNamedSymbol) {
+            render(value, KaPiece.SymbolName)
+        }
+
+        render(value, KaPiece.FunctionValueParameterList)
+
+        render(value, KaPiece.FunctionReturnType)
+        return true
+    }
+}
+
+private object ConstructorRenderer : KaPieceRenderer<KaConstructorSymbol>(KaPiece.Constructor) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaConstructorSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        render(value, KaPiece.Visibility)
+        output.append("constructor", KaTextAttribute.Keyword)
+        render(value, KaPiece.FunctionValueParameterList)
+        return true
+    }
+}
+
+private object PrimaryConstructorRenderer : KaPieceRenderer<KaConstructorSymbol>(KaPiece.PrimaryConstructor) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaConstructorSymbol, next: () -> Unit): Boolean {
+        // The `constructor` keyword is rendered when the primary constructor carries its own annotations or an explicit,
+        // non-default (non-public) visibility.
+        if (value.annotations.isNotEmpty() || effectiveVisibility(value) != KaSymbolVisibility.PUBLIC) {
+            output.space()
+            render(value, KaPiece.Annotations)
+            render(value, KaPiece.Visibility)
+            output.append("constructor", KaTextAttribute.Keyword)
+        }
+
+        val parameters = value.valueParameters
+        if (parameters.isNotEmpty()) {
+            output.append("(", KaTextAttribute.GroupStart)
+            parameters.forEachIndexed { index, parameter ->
+                if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+                renderSignatureParameter(parameter)
+            }
+            output.append(")", KaTextAttribute.GroupEnd)
+        }
+        return true
+    }
+
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    private fun renderSignatureParameter(parameter: KaValueParameterSymbol) {
+        if (parameter.isVararg) keyword("vararg")
+        render(parameter, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(parameter, KaPiece.ValueParameterType)
+    }
+}
+
+private object PropertyAccessorRenderer : KaPieceRenderer<KaPropertyAccessorSymbol>(KaPiece.PropertyAccessor) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertyAccessorSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Visibility)
+        val isGetter = value is KaPropertyGetterSymbol
+        output.append(if (isGetter) "get" else "set", KaTextAttribute.Keyword)
+        output.append("(", KaTextAttribute.GroupStart)
+        if (value is KaPropertySetterSymbol) {
+            render(value.parameter, KaPiece.ValueParameter)
+        }
+        output.append(")", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object FunctionModifiersRenderer : KaPieceRenderer<KaFunctionSymbol>(KaPiece.FunctionModifiers) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.SymbolModifiers)
+        render(value, KaPiece.Modality)
+        if (value is KaNamedFunctionSymbol) {
+            if (value.isOverride) keyword("override")
+            if (value.isTailRec) keyword("tailrec")
+            if (value.isSuspend) keyword("suspend")
+            if (value.isInline) keyword("inline")
+            if (value.isInfix) keyword("infix")
+            if (value.isOperator) keyword("operator")
+        }
+        return true
+    }
+}
+
+private object FunctionAnnotationsRenderer : KaPieceRenderer<KaFunctionSymbol>(KaPiece.FunctionAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+private object FunctionReceiverRenderer : KaPieceRenderer<KaParameterSymbol>(KaPiece.FunctionReceiver) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaParameterSymbol, next: () -> Unit): Boolean {
+        renderReceiverType(value.returnType)
+        output.append(".", KaTextAttribute.Punctuation)
+        return true
+    }
+}
+
+
+private object FunctionValueParameterListRenderer : KaPieceRenderer<KaFunctionSymbol>(KaPiece.FunctionValueParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionSymbol, next: () -> Unit): Boolean {
+        render(value.valueParameters, KaPiece.ValueParameterList)
+        return true
+    }
+}
+
+private object FunctionReturnTypeRenderer : KaPieceRenderer<KaFunctionSymbol>(KaPiece.FunctionReturnType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionSymbol, next: () -> Unit): Boolean {
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object NamedFunctionReturnTypeRenderer : KaPieceRenderer<KaNamedFunctionSymbol>(KaPiece.NamedFunctionReturnType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaNamedFunctionSymbol, next: () -> Unit): Boolean {
+        // An implicit `Unit` return type is omitted in source code. A nullable or annotated `Unit` is not implicit, so it is rendered.
+        val returnType = value.returnType
+        val isImplicitUnit = returnType.classId == StandardClassIds.Unit &&
+                !returnType.isNullable &&
+                returnType.annotations.isEmpty()
+
+        if (!isImplicitUnit) {
+            render(value, KaPiece.FunctionReturnType)
+        }
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Value parameters.
+// -------------------------------------------------------------------------------------------------
+
+private object ValueParameterListRenderer : KaPieceRenderer<List<KaValueParameterSymbol>>(KaPiece.ValueParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: List<KaValueParameterSymbol>, next: () -> Unit): Boolean {
+        output.append("(", KaTextAttribute.GroupStart)
+        value.forEachIndexed { index, parameter ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            render(parameter, KaPiece.ValueParameter)
+        }
+        output.append(")", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object ValueParameterRenderer : KaPieceRenderer<KaValueParameterSymbol>(KaPiece.ValueParameter) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaValueParameterSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.ValueParameterAnnotations)
+
+        if (value.isVararg) keyword("vararg")
+        if (value.isCrossinline) keyword("crossinline")
+        if (value.isNoinline) keyword("noinline")
+
+        value.primaryConstructorProperty?.let { property ->
+            keyword(if (property.isVal) "val" else "var")
+        }
+
+        render(value, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value, KaPiece.ValueParameterType)
+        render(value, KaPiece.ValueParameterDefaultValue)
+        return true
+    }
+}
+
+private object ValueParameterAnnotationsRenderer : KaPieceRenderer<KaValueParameterSymbol>(KaPiece.ValueParameterAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaValueParameterSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+private object ValueParameterTypeRenderer : KaPieceRenderer<KaValueParameterSymbol>(KaPiece.ValueParameterType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaValueParameterSymbol, next: () -> Unit): Boolean {
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object ValueParameterDefaultValueRenderer : KaPieceRenderer<KaValueParameterSymbol>(KaPiece.ValueParameterDefaultValue) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaValueParameterSymbol, next: () -> Unit): Boolean {
+        if (!value.hasDefaultValue) return true
+
+        // The default value expression is not part of the symbol, so it is rendered as a placeholder.
+        output.append(" = ", KaTextAttribute.Punctuation)
+        output.append("...", KaTextAttribute.Punctuation)
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Context parameters.
+// -------------------------------------------------------------------------------------------------
+
+private object ContextParameterListRenderer : KaPieceRenderer<List<KaContextParameterSymbol>>(KaPiece.ContextParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: List<KaContextParameterSymbol>, next: () -> Unit): Boolean {
+        output.append("context", KaTextAttribute.Keyword)
+        output.append("(", KaTextAttribute.GroupStart)
+        value.forEachIndexed { index, parameter ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            render(parameter, KaPiece.ContextParameter)
+        }
+        output.append(")", KaTextAttribute.GroupEnd)
+        val onNewLine = context.valueFor(KaRenderingOption.ContextReceiversOnNewLine)(session, context, value)
+        if (onNewLine) output.newLine() else output.space()
+        return true
+    }
+}
+
+private object CallableContextParameterListRenderer : KaPieceRenderer<KaCallableSymbol>(KaPiece.CallableContextParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaCallableSymbol, next: () -> Unit): Boolean {
+        val contextParameters = value.contextParameters
+        if (contextParameters.isNotEmpty()) {
+            render(contextParameters, KaPiece.ContextParameterList)
+        }
+        return true
+    }
+}
+
+private object ContextParameterRenderer : KaPieceRenderer<KaContextParameterSymbol>(KaPiece.ContextParameter) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaContextParameterSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.ContextParameterAnnotations)
+        render(value, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value, KaPiece.ContextParameterType)
+        return true
+    }
+}
+
+private object ContextParameterAnnotationsRenderer : KaPieceRenderer<KaContextParameterSymbol>(KaPiece.ContextParameterAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaContextParameterSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+private object ContextParameterTypeRenderer : KaPieceRenderer<KaContextParameterSymbol>(KaPiece.ContextParameterType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaContextParameterSymbol, next: () -> Unit): Boolean {
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Properties and variables.
+// -------------------------------------------------------------------------------------------------
+
+private object PropertyRenderer : KaPieceRenderer<KaPropertySymbol>(KaPiece.Property) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertySymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.PropertyAnnotations)
+        render(value, KaPiece.CallableContextParameterList)
+        render(value, KaPiece.PropertyModifiers)
+        keyword(if (value.isVal) "val" else "var")
+
+        if (value.typeParameters.isNotEmpty()) {
+            render(value, KaPiece.CallableTypeParameterList)
+            output.space()
+        }
+
+        value.receiverParameter?.let { render(it, KaPiece.FunctionReceiver) }
+
+        render(value, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value, KaPiece.PropertyReturnType)
+
+        render(value, KaPiece.CallableWhereClause)
+
+        render(value, KaPiece.PropertyAccessors)
+        return true
+    }
+}
+
+private object PropertyAccessorsRenderer : KaPieceRenderer<KaPropertySymbol>(KaPiece.PropertyAccessors) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertySymbol, next: () -> Unit): Boolean {
+        val getter = value.getter?.takeIf { it.isNotDefault }
+        val setter = value.setter?.takeIf { it.isNotDefault }
+        if (getter == null && setter == null) return true
+
+        output.indent()
+        getter?.let { output.newLine(); render(it, KaPiece.PropertyAccessor) }
+        setter?.let { output.newLine(); render(it, KaPiece.PropertyAccessor) }
+        output.unindent()
+        return true
+    }
+}
+
+private object PropertyModifiersRenderer : KaPieceRenderer<KaPropertySymbol>(KaPiece.PropertyModifiers) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertySymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.SymbolModifiers)
+        render(value, KaPiece.Modality)
+        if (value.isOverride) keyword("override")
+        if (value is KaKotlinPropertySymbol) {
+            if (value.isConst) keyword("const")
+            if (value.isLateInit) keyword("lateinit")
+        }
+        return true
+    }
+}
+
+private object PropertyAnnotationsRenderer : KaPieceRenderer<KaPropertySymbol>(KaPiece.PropertyAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertySymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+private object PropertyReturnTypeRenderer : KaPieceRenderer<KaPropertySymbol>(KaPiece.PropertyReturnType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPropertySymbol, next: () -> Unit): Boolean {
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object LocalVariableRenderer : KaPieceRenderer<KaLocalVariableSymbol>(KaPiece.LocalVariable) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaLocalVariableSymbol, next: () -> Unit): Boolean {
+        if (value.isLateInit) keyword("lateinit")
+        keyword(if (value.isVal) "val" else "var")
+        render(value, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object JavaFieldRenderer : KaPieceRenderer<KaJavaFieldSymbol>(KaPiece.JavaField) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaJavaFieldSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Visibility)
+        if (value.isStatic) keyword("static")
+        keyword(if (value.isVal) "val" else "var")
+        render(value, KaPiece.SymbolName)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object BackingFieldRenderer : KaPieceRenderer<KaBackingFieldSymbol>(KaPiece.BackingField) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaBackingFieldSymbol, next: () -> Unit): Boolean {
+        output.append("field", KaTextAttribute.Keyword)
+        output.append(": ", KaTextAttribute.Punctuation)
+        render(value.returnType, KaPiece.Type)
+        return true
+    }
+}
+
+private object EnumEntryRenderer : KaPieceRenderer<KaEnumEntrySymbol>(KaPiece.EnumEntry) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaEnumEntrySymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        render(value, KaPiece.SymbolName)
+        // Render the anonymous object body of the entry, e.g. `ENTRY { override fun foo() }`.
+        value.initializer?.let { initializer ->
+            render(initializer, KaPiece.ClassBody)
+        }
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Classifiers.
+// -------------------------------------------------------------------------------------------------
+
+private object NamedClassRenderer : KaPieceRenderer<KaNamedClassSymbol>(KaPiece.NamedClass) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaNamedClassSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.ClassAnnotations)
+        render(value, KaPiece.ClassModifiers)
+
+        val classKeyword = when (value.classKind) {
+            KaClassKind.INTERFACE -> "interface"
+            KaClassKind.OBJECT, KaClassKind.COMPANION_OBJECT -> "object"
+            else -> "class"
+        }
+        output.append(classKeyword, KaTextAttribute.Keyword)
+
+        val isDefaultCompanion = value.classKind == KaClassKind.COMPANION_OBJECT &&
+                value.name == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT
+
+        if (!isDefaultCompanion) {
+            output.space()
+            render(value, KaPiece.SymbolName)
+        }
+
+        if (value.typeParameters.isNotEmpty()) {
+            render(value, KaPiece.ClassifierTypeParameterList)
+        }
+
+        if (context.valueFor(KaRenderingOption.PrimaryConstructorInClassHeader)) {
+            value.primaryConstructor?.let { render(it, KaPiece.PrimaryConstructor) }
+        }
+
+        render(value, KaPiece.ClassifierWhereClause)
+        render(value, KaPiece.SupertypeList)
+        render(value, KaPiece.ClassBody)
+        return true
+    }
+
+    context(session: KaSession)
+    private val KaClassSymbol.primaryConstructor: KaConstructorSymbol?
+        get() = declaredMemberScope.constructors.firstOrNull { it.isPrimary }
+}
+
+private object AnonymousObjectRenderer : KaPieceRenderer<KaAnonymousObjectSymbol>(KaPiece.AnonymousObject) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaAnonymousObjectSymbol, next: () -> Unit): Boolean {
+        output.append("object", KaTextAttribute.Keyword)
+        render(value, KaPiece.SupertypeList)
+        render(value, KaPiece.ClassBody)
+        return true
+    }
+}
+
+private object TypeAliasRenderer : KaPieceRenderer<KaTypeAliasSymbol>(KaPiece.TypeAlias) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaTypeAliasSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.SymbolModifiers)
+        keyword("typealias")
+        render(value, KaPiece.SymbolName)
+        if (value.typeParameters.isNotEmpty()) {
+            render(value, KaPiece.ClassifierTypeParameterList)
+        }
+        output.append(" = ", KaTextAttribute.Punctuation)
+        render(value.expandedType, KaPiece.Type)
+        return true
+    }
+}
+
+private object ClassModifiersRenderer : KaPieceRenderer<KaNamedClassSymbol>(KaPiece.ClassModifiers) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaNamedClassSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.SymbolModifiers)
+        render(value, KaPiece.Modality)
+
+        when (value.classKind) {
+            KaClassKind.COMPANION_OBJECT -> keyword("companion")
+            KaClassKind.ENUM_CLASS -> keyword("enum")
+            KaClassKind.ANNOTATION_CLASS -> keyword("annotation")
+            else -> {}
+        }
+
+        if (value.isInner) keyword("inner")
+        if (value.isData) keyword("data")
+        if (value.isInline) keyword("value")
+        if (value.isFun) keyword("fun")
+        return true
+    }
+}
+
+private object ClassAnnotationsRenderer : KaPieceRenderer<KaNamedClassSymbol>(KaPiece.ClassAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaNamedClassSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+
+private object SupertypeListRenderer : KaPieceRenderer<KaClassSymbol>(KaPiece.SupertypeList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaClassSymbol, next: () -> Unit): Boolean {
+        val superTypes = value.superTypes.filter { !it.isTrivialSuperType() }
+        if (superTypes.isNotEmpty()) {
+            output.append(" : ", KaTextAttribute.Punctuation)
+            superTypes.forEachIndexed { index, superType ->
+                if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+                render(superType, KaPiece.Supertype)
+            }
+        }
+        return true
+    }
+
+    /** `kotlin.Any` and the implicit `kotlin.Enum`/`kotlin.Annotation` supertypes are omitted from super type lists. */
+    context(session: KaSession)
+    private fun KaType.isTrivialSuperType(): Boolean {
+        return when (classId) {
+            StandardClassIds.Any, StandardClassIds.Enum, StandardClassIds.Annotation -> true
+            else -> false
+        }
+    }
+}
+
+private object SupertypeRenderer : KaPieceRenderer<KaType>(KaPiece.Supertype) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaType, next: () -> Unit): Boolean {
+        render(value, KaPiece.Type)
+
+        // A class supertype is rendered with constructor-call syntax, e.g. `Base()`; interfaces are not.
+        if (value.expandedSymbol?.classKind?.isClass == true) {
+            output.append("(", KaTextAttribute.GroupStart)
+            output.append(")", KaTextAttribute.GroupEnd)
+        }
+        return true
+    }
+}
+
+private object ClassBodyRenderer : KaPieceRenderer<KaClassSymbol>(KaPiece.ClassBody) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaClassSymbol, next: () -> Unit): Boolean {
+        val ordering = context.valueFor(KaRenderingOption.ClassMemberOrdering)
+
+        val members = context.valueFor(KaRenderingOption.ClassMembers)(session, context, value)
+            .sortedWith { first, second -> ordering(session, context, first, second) }
+
+        if (members.isEmpty()) {
+            return true
+        }
+
+        val extraLineBetweenMembers = context.valueFor(KaRenderingOption.ExtraLineBetweenMembers)
+
+        output.space().append("{", KaTextAttribute.GroupStart).indent()
+
+        val enumEntries = members.filterIsInstance<KaEnumEntrySymbol>()
+        val otherMembers = members.filter { it !is KaEnumEntrySymbol }
+
+        // Enum entries come first, separated by commas, and are terminated with a semicolon when other declarations follow.
+        enumEntries.forEachIndexed { index, entry ->
+            output.newLine()
+            render(entry, KaPiece.Symbol)
+            when {
+                index < enumEntries.lastIndex -> output.append(",", KaTextAttribute.Punctuation)
+                otherMembers.isNotEmpty() -> output.append(";", KaTextAttribute.Punctuation)
+            }
+        }
+
+        otherMembers.forEachIndexed { index, member ->
+            // Separate members (and the enum entry block from the following declarations) with a blank line when requested.
+            if (extraLineBetweenMembers && (enumEntries.isNotEmpty() || index > 0)) {
+                output.newLine()
+            }
+            output.newLine()
+            render(member, KaPiece.Symbol)
+        }
+
+        output.unindent().newLine().append("}", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object PackageRenderer : KaPieceRenderer<KaPackageSymbol>(KaPiece.Package) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaPackageSymbol, next: () -> Unit): Boolean {
+        keyword("package")
+        render(value, KaPiece.SymbolName)
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Type parameters.
+// -------------------------------------------------------------------------------------------------
+
+private object TypeParameterListRenderer : KaPieceRenderer<List<KaTypeParameterSymbol>>(KaPiece.TypeParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: List<KaTypeParameterSymbol>, next: () -> Unit): Boolean {
+        output.append("<", KaTextAttribute.GroupStart)
+        value.forEachIndexed { index, typeParameter ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            render(typeParameter, KaPiece.TypeParameter)
+        }
+        output.append(">", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object CallableTypeParameterListRenderer : KaPieceRenderer<KaCallableSymbol>(KaPiece.CallableTypeParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaCallableSymbol, next: () -> Unit): Boolean {
+        render(value.typeParameters, KaPiece.TypeParameterList)
+        return true
+    }
+}
+
+private object ClassifierTypeParameterListRenderer : KaPieceRenderer<KaClassLikeSymbol>(KaPiece.ClassifierTypeParameterList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaClassLikeSymbol, next: () -> Unit): Boolean {
+        render(value.typeParameters, KaPiece.TypeParameterList)
+        return true
+    }
+}
+
+private object TypeParameterRenderer : KaPieceRenderer<KaTypeParameterSymbol>(KaPiece.TypeParameter) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaTypeParameterSymbol, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        if (value.isReified) keyword("reified")
+        if (value.variance != Variance.INVARIANT) keyword(value.variance.label)
+
+        render(value, KaPiece.SymbolName)
+
+        // A single upper bound is rendered inline; multiple bounds are deferred to a `where` clause.
+        if (value.upperBounds.size == 1) {
+            output.append(" : ", KaTextAttribute.Punctuation)
+            render(value.upperBounds.single(), KaPiece.Type)
+        }
+        return true
+    }
+}
+
+private object WhereClauseRenderer : KaPieceRenderer<List<KaTypeParameterSymbol>>(KaPiece.WhereClause) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: List<KaTypeParameterSymbol>, next: () -> Unit): Boolean {
+        // Only type parameters with more than one upper bound contribute to the `where` clause; single bounds are rendered inline.
+        val constrainedTypeParameters = value.filter { it.upperBounds.size > 1 }
+        if (constrainedTypeParameters.isEmpty()) return true
+
+        output.space()
+        keyword("where")
+        var first = true
+        for (typeParameter in constrainedTypeParameters) {
+            for (bound in typeParameter.upperBounds) {
+                if (!first) output.append(", ", KaTextAttribute.Punctuation)
+                first = false
+                render(typeParameter, KaPiece.SymbolName)
+                output.append(" : ", KaTextAttribute.Punctuation)
+                render(bound, KaPiece.Type)
+            }
+        }
+
+        return true
+    }
+}
+
+private object CallableWhereClauseRenderer : KaPieceRenderer<KaCallableSymbol>(KaPiece.CallableWhereClause) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaCallableSymbol, next: () -> Unit): Boolean {
+        render(value.typeParameters, KaPiece.WhereClause)
+        return true
+    }
+}
+
+private object ClassifierWhereClauseRenderer : KaPieceRenderer<KaClassLikeSymbol>(KaPiece.ClassifierWhereClause) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaClassLikeSymbol, next: () -> Unit): Boolean {
+        render(value.typeParameters, KaPiece.WhereClause)
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Types: top-level dispatcher.
+// -------------------------------------------------------------------------------------------------
+
+private object TypeRenderer : KaPieceRenderer<KaType>(KaPiece.Type) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaType, next: () -> Unit): Boolean {
+        val type = effectiveType(value)
+
+        if (type.annotations.isNotEmpty()) {
+            render(type, KaPiece.TypeAnnotations)
+        }
+
+        // If the type is a type-alias application, render the alias (e.g. `WithGeneric<Double>`) rather than its expansion.
+        val abbreviation = type.abbreviation
+        if (abbreviation != null) {
+            render(abbreviation, KaPiece.ClassType)
+            return true
+        }
+
+        when (type) {
+            is KaFunctionType -> render(type, KaPiece.FunctionType)
+            is KaErrorType -> render(type, KaPiece.ErrorType)
+            is KaClassType -> render(type, KaPiece.ClassType)
+            is KaTypeParameterType -> render(type, KaPiece.TypeParameterType)
+            is KaCapturedType -> render(type, KaPiece.CapturedType)
+            is KaDefinitelyNotNullType -> render(type, KaPiece.DefinitelyNotNullType)
+            is KaFlexibleType -> render(type, KaPiece.FlexibleType)
+            is KaIntersectionType -> render(type, KaPiece.IntersectionType)
+            is KaDynamicType -> render(type, KaPiece.DynamicType)
+            else -> output.append("ERROR", KaTextAttribute.Identifier)
+        }
+        return true
+    }
+
+    /**
+     * Applies [KaRenderingOption.TypeTransformation] and [KaRenderingOption.TypeApproximation] to [type].
+     *
+     * Every rendered type passes through [KaPiece.Type], so applying the options here covers all of them, including nested ones such as
+     * type arguments. Approximation of an outer type already makes its arguments denotable, so the nested applications are no-ops.
+     */
+    @OptIn(KaExperimentalApi::class)
+    context(session: KaSession, context: KaRenderingContext)
+    private fun effectiveType(type: KaType): KaType {
+        val transformed = context.valueFor(KaRenderingOption.TypeTransformation)(session, context, type)
+
+        return when (context.valueFor(KaRenderingOption.TypeApproximation)) {
+            KaTypeApproximation.NONE -> transformed
+            KaTypeApproximation.TO_DENOTABLE_SUBTYPE -> transformed.approximateToDenotableSubtypeOrSelf()
+            KaTypeApproximation.TO_DENOTABLE_SUPERTYPE -> transformed.approximateToDenotableSupertypeOrSelf(allowLocalDenotableTypes = true)
+        }
+    }
+}
+
+private object TypeAnnotationsRenderer : KaPieceRenderer<KaType>(KaPiece.TypeAnnotations) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaType, next: () -> Unit): Boolean {
+        render(value, KaPiece.Annotations)
+        return true
+    }
+}
+
+private object TypeNullabilityRenderer : KaPieceRenderer<KaType>(KaPiece.TypeNullability) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaType, next: () -> Unit): Boolean {
+        if (value.isMarkedNullable) output.append("?", KaTextAttribute.Punctuation)
+        return true
+    }
+}
+
+private object ClassTypeRenderer : KaPieceRenderer<KaClassType>(KaPiece.ClassType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaClassType, next: () -> Unit): Boolean {
+        val qualification = context.valueFor(KaRenderingOption.ClassTypeQualification)
+        val qualifiers = value.qualifiers
+
+        if (qualifiers.isEmpty()) {
+            // Fallback for class types that expose no explicit qualifier segments.
+            identifier(value.classId.shortClassName, value.symbol)
+            if (value.typeArguments.isNotEmpty()) {
+                render(value.typeArguments, KaPiece.TypeArgumentList)
+            }
+        } else {
+            if (qualification == KaClassTypeQualification.FULLY_QUALIFIED) {
+                val packageFqName = value.classId.packageFqName
+                if (!packageFqName.isRoot && packageFqName != CallableId.PACKAGE_FQ_NAME_FOR_LOCAL) {
+                    renderPackageQualifier(packageFqName)
+                    output.append(".", KaTextAttribute.Punctuation)
+                }
+            }
+
+            // `SIMPLE` keeps only the innermost segment; the other modes render all outer classifiers.
+            val renderedQualifiers = when (qualification) {
+                KaClassTypeQualification.SIMPLE -> listOf(qualifiers.last())
+                else -> qualifiers
+            }
+
+            renderedQualifiers.forEachIndexed { index, qualifier ->
+                if (index > 0) output.append(".", KaTextAttribute.Punctuation)
+                identifier(qualifier.name, qualifier.symbol)
+                if (qualifier.typeArguments.isNotEmpty()) {
+                    render(qualifier.typeArguments, KaPiece.TypeArgumentList)
+                }
+            }
+        }
+
+        render(value, KaPiece.TypeNullability)
+        return true
+    }
+}
+
+private object FunctionTypeRenderer : KaPieceRenderer<KaFunctionType>(KaPiece.FunctionType) {
+    @OptIn(KaExperimentalApi::class)
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFunctionType, next: () -> Unit): Boolean {
+        val nullable = value.isMarkedNullable
+        if (nullable) output.append("(", KaTextAttribute.GroupStart)
+
+        if (value.isSuspend) keyword("suspend")
+
+        val contextReceivers = value.contextReceivers
+        if (contextReceivers.isNotEmpty()) {
+            output.append("context", KaTextAttribute.Keyword)
+            output.append("(", KaTextAttribute.GroupStart)
+            contextReceivers.forEachIndexed { index, contextReceiver ->
+                if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+                render(contextReceiver.type, KaPiece.Type)
+            }
+            output.append(")", KaTextAttribute.GroupEnd)
+            output.space()
+        }
+
+        value.receiverType?.let { receiverType ->
+            renderReceiverType(receiverType)
+            output.append(".", KaTextAttribute.Punctuation)
+        }
+
+        output.append("(", KaTextAttribute.GroupStart)
+        value.parameterTypes.forEachIndexed { index, parameterType ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            render(parameterType, KaPiece.Type)
+        }
+        output.append(")", KaTextAttribute.GroupEnd)
+
+        output.append(" -> ", KaTextAttribute.Punctuation)
+        render(value.returnType, KaPiece.Type)
+
+        if (nullable) output.append(")", KaTextAttribute.GroupEnd)
+        render(value, KaPiece.TypeNullability)
+        return true
+    }
+}
+
+private object TypeParameterTypeRenderer : KaPieceRenderer<KaTypeParameterType>(KaPiece.TypeParameterType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaTypeParameterType, next: () -> Unit): Boolean {
+        identifier(value.name, value.symbol)
+        render(value, KaPiece.TypeNullability)
+        return true
+    }
+}
+
+private object CapturedTypeRenderer : KaPieceRenderer<KaCapturedType>(KaPiece.CapturedType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaCapturedType, next: () -> Unit): Boolean {
+        output.append("Captured", KaTextAttribute.Identifier)
+        output.append("(", KaTextAttribute.GroupStart)
+        render(value.projection, KaPiece.TypeProjection)
+        output.append(")", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object DefinitelyNotNullTypeRenderer : KaPieceRenderer<KaDefinitelyNotNullType>(KaPiece.DefinitelyNotNullType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaDefinitelyNotNullType, next: () -> Unit): Boolean {
+        render(value.original, KaPiece.Type)
+        output.append(" & ", KaTextAttribute.Punctuation)
+        output.append("Any", KaTextAttribute.Identifier)
+        return true
+    }
+}
+
+private object FlexibleTypeRenderer : KaPieceRenderer<KaFlexibleType>(KaPiece.FlexibleType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaFlexibleType, next: () -> Unit): Boolean {
+        val lower = value.lowerBound
+        val upper = value.upperBound
+
+        // A flexible type whose bounds differ only in nullability (e.g. `String..String?`) is rendered compactly as `String!`.
+        if (isNullabilityFlexibleType(lower, upper)) {
+            render(lower, KaPiece.Type)
+            output.append("!", KaTextAttribute.Punctuation)
+            return true
+        }
+
+        render(lower, KaPiece.Type)
+        output.append("..", KaTextAttribute.Punctuation)
+        render(upper, KaPiece.Type)
+        return true
+    }
+
+    context(session: KaSession)
+    private fun isNullabilityFlexibleType(lower: KaType, upper: KaType): Boolean {
+        val isSameType = lower is KaClassType && upper is KaClassType && lower.classId == upper.classId ||
+                lower is KaTypeParameterType && upper is KaTypeParameterType && lower.symbol == upper.symbol
+        if (!isSameType || lower.isMarkedNullable || !upper.isMarkedNullable) return false
+
+        // For class types, the bounds must additionally share the same type arguments.
+        if (lower is KaClassType && upper is KaClassType) {
+            val lowerArguments = lower.typeArguments
+            val upperArguments = upper.typeArguments
+            return lowerArguments.size == upperArguments.size &&
+                    lowerArguments.indices.all { upperArguments[it].type == lowerArguments[it].type }
+        }
+
+        return true
+    }
+}
+
+private object IntersectionTypeRenderer : KaPieceRenderer<KaIntersectionType>(KaPiece.IntersectionType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaIntersectionType, next: () -> Unit): Boolean {
+        value.conjuncts.forEachIndexed { index, conjunct ->
+            if (index > 0) output.append(" & ", KaTextAttribute.Punctuation)
+            render(conjunct, KaPiece.Type)
+        }
+        return true
+    }
+}
+
+private object DynamicTypeRenderer : KaPieceRenderer<KaDynamicType>(KaPiece.DynamicType) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaDynamicType, next: () -> Unit): Boolean {
+        output.append("dynamic", KaTextAttribute.Keyword)
+        return true
+    }
+}
+
+private object ErrorTypeRenderer : KaPieceRenderer<KaErrorType>(KaPiece.ErrorType) {
+    @OptIn(KaNonPublicApi::class)
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaErrorType, next: () -> Unit): Boolean {
+        // For unresolved class types, render the written form (with type arguments), e.g. `C<String>`, instead of just an error marker.
+        val qualifiers = (value as? KaClassErrorType)?.qualifiers
+        if (!qualifiers.isNullOrEmpty()) {
+            qualifiers.forEachIndexed { index, qualifier ->
+                if (index > 0) output.append(".", KaTextAttribute.Punctuation)
+                val symbol = (qualifier as? KaResolvedClassTypeQualifier)?.symbol
+                if (symbol != null) {
+                    identifier(qualifier.name, symbol)
+                } else {
+                    output.append(qualifier.name.render(), KaTextAttribute.Identifier)
+                }
+                if (qualifier.typeArguments.isNotEmpty()) {
+                    render(qualifier.typeArguments, KaPiece.TypeArgumentList)
+                }
+            }
+            return true
+        }
+
+        output.append(value.presentableText ?: "ERROR", KaTextAttribute.Identifier)
+        return true
+    }
+}
+
+// -------------------------------------------------------------------------------------------------
+// Type projections.
+// -------------------------------------------------------------------------------------------------
+
+private object TypeArgumentListRenderer : KaPieceRenderer<List<KaTypeProjection>>(KaPiece.TypeArgumentList) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: List<KaTypeProjection>, next: () -> Unit): Boolean {
+        output.append("<", KaTextAttribute.GroupStart)
+        value.forEachIndexed { index, projection ->
+            if (index > 0) output.append(", ", KaTextAttribute.Punctuation)
+            render(projection, KaPiece.TypeProjection)
+        }
+        output.append(">", KaTextAttribute.GroupEnd)
+        return true
+    }
+}
+
+private object TypeProjectionRenderer : KaPieceRenderer<KaTypeProjection>(KaPiece.TypeProjection) {
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    override fun render(value: KaTypeProjection, next: () -> Unit): Boolean {
+        when (value) {
+            is KaStarTypeProjection -> output.append("*", KaTextAttribute.Punctuation)
+            is KaTypeArgumentWithVariance -> {
+                if (value.variance != Variance.INVARIANT) keyword(value.variance.label)
+                render(value.type, KaPiece.Type)
+            }
+        }
+        return true
+    }
+}
diff --git a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeDependentAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeDependentAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java
new file mode 100644
index 0000000..9141f4b
--- /dev/null
+++ b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeDependentAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2010-2024 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.analysis.api.fir.test.cases.generated.cases.components.symbolDeclarationRenderer;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.util.KtTestUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
+import org.jetbrains.kotlin.analysis.test.framework.services.TargetPlatformEnum;
+import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractLegacyRenderingTest;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlin.analysis.api.fir.test.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration")
+@TestDataPath("$PROJECT_ROOT")
+public class FirIdeDependentAnalysisSourceLikeModuleLegacyRenderingTestGenerated extends AbstractLegacyRenderingTest {
+  @NotNull
+  @Override
+  public AnalysisApiTestConfigurator getConfigurator() {
+    return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
+      new AnalysisApiTestConfiguratorFactoryData(
+        TestModuleKind.SourceLike,
+        AnalysisSessionMode.Dependent,
+        AnalysisApiMode.Ide,
+        TargetPlatformEnum.JVM
+      )
+    );
+  }
+
+  private void run(String fileName) {
+    runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/" + fileName);
+  }
+
+  @Test
+  @TestMetadata("actualInheritance.kt")
+  public void testActualInheritance() {
+    run("actualInheritance.kt");
+  }
+
+  @Test
+  public void testAllFilesPresentInRenderDeclaration() {
+    KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration"), Pattern.compile("^(.+)\\.(kt|kts)$"), null, true);
+  }
+
+  @Test
+  @TestMetadata("annotation.kt")
+  public void testAnnotation() {
+    run("annotation.kt");
+  }
+
+  @Test
+  @TestMetadata("complexTypes.kt")
+  public void testComplexTypes() {
+    run("complexTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorInObject.kt")
+  public void testConstructorInObject() {
+    run("constructorInObject.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorOfAnonymousObject.kt")
+  public void testConstructorOfAnonymousObject() {
+    run("constructorOfAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("contextParameter.kt")
+  public void testContextParameter() {
+    run("contextParameter.kt");
+  }
+
+  @Test
+  @TestMetadata("delegates.kt")
+  public void testDelegates() {
+    run("delegates.kt");
+  }
+
+  @Test
+  @TestMetadata("derivedClass.kt")
+  public void testDerivedClass() {
+    run("derivedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("emptyAnonymousObject.kt")
+  public void testEmptyAnonymousObject() {
+    run("emptyAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("enums.kt")
+  public void testEnums() {
+    run("enums.kt");
+  }
+
+  @Test
+  @TestMetadata("enums2.kt")
+  public void testEnums2() {
+    run("enums2.kt");
+  }
+
+  @Test
+  @TestMetadata("expectActual.kt")
+  public void testExpectActual() {
+    run("expectActual.kt");
+  }
+
+  @Test
+  @TestMetadata("F.kt")
+  public void testF() {
+    run("F.kt");
+  }
+
+  @Test
+  @TestMetadata("functionTypes.kt")
+  public void testFunctionTypes() {
+    run("functionTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("functionalTypeAliases.kt")
+  public void testFunctionalTypeAliases() {
+    run("functionalTypeAliases.kt");
+  }
+
+  @Test
+  @TestMetadata("genericFunctions.kt")
+  public void testGenericFunctions() {
+    run("genericFunctions.kt");
+  }
+
+  @Test
+  @TestMetadata("genericProperty.kt")
+  public void testGenericProperty() {
+    run("genericProperty.kt");
+  }
+
+  @Test
+  @TestMetadata("intersectionType.kt")
+  public void testIntersectionType() {
+    run("intersectionType.kt");
+  }
+
+  @Test
+  @TestMetadata("nestedClass.kt")
+  public void testNestedClass() {
+    run("nestedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedOfAliasedType.kt")
+  public void testNestedOfAliasedType() {
+    run("NestedOfAliasedType.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedSuperType.kt")
+  public void testNestedSuperType() {
+    run("NestedSuperType.kt");
+  }
+
+  @Test
+  @TestMetadata("noPrimaryConstructor.kt")
+  public void testNoPrimaryConstructor() {
+    run("noPrimaryConstructor.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleClass.kt")
+  public void testSimpleClass() {
+    run("simpleClass.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleFun.kt")
+  public void testSimpleFun() {
+    run("simpleFun.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleTypeAlias.kt")
+  public void testSimpleTypeAlias() {
+    run("simpleTypeAlias.kt");
+  }
+
+  @Test
+  @TestMetadata("typeAliasWithGeneric.kt")
+  public void testTypeAliasWithGeneric() {
+    run("typeAliasWithGeneric.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameterVsNested.kt")
+  public void testTypeParameterVsNested() {
+    run("typeParameterVsNested.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameters.kt")
+  public void testTypeParameters() {
+    run("typeParameters.kt");
+  }
+
+  @Test
+  @TestMetadata("vararg.kt")
+  public void testVararg() {
+    run("vararg.kt");
+  }
+
+  @Test
+  @TestMetadata("where.kt")
+  public void testWhere() {
+    run("where.kt");
+  }
+
+  @Nested
+  @TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types")
+  @TestDataPath("$PROJECT_ROOT")
+  public class Types {
+    private void run(String fileName) {
+      runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/" + fileName);
+    }
+
+    @Test
+    public void testAllFilesPresentInTypes() {
+      KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types"), Pattern.compile("^(.+)\\.(kt|kts)$"), null, true);
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypes.kt")
+    public void testAnnotationOnTypes() {
+      run("annotationOnTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypesWithComplexExpression.kt")
+    public void testAnnotationOnTypesWithComplexExpression() {
+      run("annotationOnTypesWithComplexExpression.kt");
+    }
+
+    @Test
+    @TestMetadata("approximatedCapturedType.kt")
+    public void testApproximatedCapturedType() {
+      run("approximatedCapturedType.kt");
+    }
+
+    @Test
+    @TestMetadata("contextFunctionTypes.kt")
+    public void testContextFunctionTypes() {
+      run("contextFunctionTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("extraTypeArguments.kt")
+    public void testExtraTypeArguments() {
+      run("extraTypeArguments.kt");
+    }
+  }
+}
diff --git a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeNormalAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeNormalAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java
new file mode 100644
index 0000000..8eb1d8a
--- /dev/null
+++ b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirIdeNormalAnalysisSourceLikeModuleLegacyRenderingTestGenerated.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2010-2024 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.analysis.api.fir.test.cases.generated.cases.components.symbolDeclarationRenderer;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.util.KtTestUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.kotlin.analysis.api.fir.test.configurators.AnalysisApiFirTestConfiguratorFactory;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
+import org.jetbrains.kotlin.analysis.test.framework.services.TargetPlatformEnum;
+import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractLegacyRenderingTest;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlin.analysis.api.fir.test.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration")
+@TestDataPath("$PROJECT_ROOT")
+public class FirIdeNormalAnalysisSourceLikeModuleLegacyRenderingTestGenerated extends AbstractLegacyRenderingTest {
+  @NotNull
+  @Override
+  public AnalysisApiTestConfigurator getConfigurator() {
+    return AnalysisApiFirTestConfiguratorFactory.INSTANCE.createConfigurator(
+      new AnalysisApiTestConfiguratorFactoryData(
+        TestModuleKind.SourceLike,
+        AnalysisSessionMode.Normal,
+        AnalysisApiMode.Ide,
+        TargetPlatformEnum.JVM
+      )
+    );
+  }
+
+  private void run(String fileName) {
+    runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/" + fileName);
+  }
+
+  @Test
+  @TestMetadata("actualInheritance.kt")
+  public void testActualInheritance() {
+    run("actualInheritance.kt");
+  }
+
+  @Test
+  public void testAllFilesPresentInRenderDeclaration() {
+    KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration"), Pattern.compile("^(.+)\\.(kt|kts)$"), null, true);
+  }
+
+  @Test
+  @TestMetadata("annotation.kt")
+  public void testAnnotation() {
+    run("annotation.kt");
+  }
+
+  @Test
+  @TestMetadata("complexTypes.kt")
+  public void testComplexTypes() {
+    run("complexTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorInObject.kt")
+  public void testConstructorInObject() {
+    run("constructorInObject.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorOfAnonymousObject.kt")
+  public void testConstructorOfAnonymousObject() {
+    run("constructorOfAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("contextParameter.kt")
+  public void testContextParameter() {
+    run("contextParameter.kt");
+  }
+
+  @Test
+  @TestMetadata("delegates.kt")
+  public void testDelegates() {
+    run("delegates.kt");
+  }
+
+  @Test
+  @TestMetadata("derivedClass.kt")
+  public void testDerivedClass() {
+    run("derivedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("emptyAnonymousObject.kt")
+  public void testEmptyAnonymousObject() {
+    run("emptyAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("enums.kt")
+  public void testEnums() {
+    run("enums.kt");
+  }
+
+  @Test
+  @TestMetadata("enums2.kt")
+  public void testEnums2() {
+    run("enums2.kt");
+  }
+
+  @Test
+  @TestMetadata("expectActual.kt")
+  public void testExpectActual() {
+    run("expectActual.kt");
+  }
+
+  @Test
+  @TestMetadata("F.kt")
+  public void testF() {
+    run("F.kt");
+  }
+
+  @Test
+  @TestMetadata("functionTypes.kt")
+  public void testFunctionTypes() {
+    run("functionTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("functionalTypeAliases.kt")
+  public void testFunctionalTypeAliases() {
+    run("functionalTypeAliases.kt");
+  }
+
+  @Test
+  @TestMetadata("genericFunctions.kt")
+  public void testGenericFunctions() {
+    run("genericFunctions.kt");
+  }
+
+  @Test
+  @TestMetadata("genericProperty.kt")
+  public void testGenericProperty() {
+    run("genericProperty.kt");
+  }
+
+  @Test
+  @TestMetadata("intersectionType.kt")
+  public void testIntersectionType() {
+    run("intersectionType.kt");
+  }
+
+  @Test
+  @TestMetadata("nestedClass.kt")
+  public void testNestedClass() {
+    run("nestedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedOfAliasedType.kt")
+  public void testNestedOfAliasedType() {
+    run("NestedOfAliasedType.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedSuperType.kt")
+  public void testNestedSuperType() {
+    run("NestedSuperType.kt");
+  }
+
+  @Test
+  @TestMetadata("noPrimaryConstructor.kt")
+  public void testNoPrimaryConstructor() {
+    run("noPrimaryConstructor.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleClass.kt")
+  public void testSimpleClass() {
+    run("simpleClass.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleFun.kt")
+  public void testSimpleFun() {
+    run("simpleFun.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleTypeAlias.kt")
+  public void testSimpleTypeAlias() {
+    run("simpleTypeAlias.kt");
+  }
+
+  @Test
+  @TestMetadata("typeAliasWithGeneric.kt")
+  public void testTypeAliasWithGeneric() {
+    run("typeAliasWithGeneric.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameterVsNested.kt")
+  public void testTypeParameterVsNested() {
+    run("typeParameterVsNested.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameters.kt")
+  public void testTypeParameters() {
+    run("typeParameters.kt");
+  }
+
+  @Test
+  @TestMetadata("vararg.kt")
+  public void testVararg() {
+    run("vararg.kt");
+  }
+
+  @Test
+  @TestMetadata("where.kt")
+  public void testWhere() {
+    run("where.kt");
+  }
+
+  @Nested
+  @TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types")
+  @TestDataPath("$PROJECT_ROOT")
+  public class Types {
+    private void run(String fileName) {
+      runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/" + fileName);
+    }
+
+    @Test
+    public void testAllFilesPresentInTypes() {
+      KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types"), Pattern.compile("^(.+)\\.(kt|kts)$"), null, true);
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypes.kt")
+    public void testAnnotationOnTypes() {
+      run("annotationOnTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypesWithComplexExpression.kt")
+    public void testAnnotationOnTypesWithComplexExpression() {
+      run("annotationOnTypesWithComplexExpression.kt");
+    }
+
+    @Test
+    @TestMetadata("approximatedCapturedType.kt")
+    public void testApproximatedCapturedType() {
+      run("approximatedCapturedType.kt");
+    }
+
+    @Test
+    @TestMetadata("contextFunctionTypes.kt")
+    public void testContextFunctionTypes() {
+      run("contextFunctionTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("extraTypeArguments.kt")
+    public void testExtraTypeArguments() {
+      run("extraTypeArguments.kt");
+    }
+  }
+}
diff --git a/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/TestGenerator.kt b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/TestGenerator.kt
index 5d44c42..97753bc 100644
--- a/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/TestGenerator.kt
+++ b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/TestGenerator.kt
@@ -46,6 +46,7 @@
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.substututorFactory.AbstractSubstitutorBuilderTest
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationOverridesProvider.AbstractIsSubclassOfTest
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationOverridesProvider.AbstractOverriddenDeclarationProviderTest
+import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractLegacyRenderingTest
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractRendererTest
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractSymbolRenderingByReferenceTest
 import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolInfoProvider.*
@@ -514,6 +515,10 @@
     }
 
     component("symbolDeclarationRenderer") {
+        test<AbstractLegacyRenderingTest> {
+            model(it, "renderDeclaration")
+        }
+
         test<AbstractRendererTest> {
             model(it, "renderDeclaration")
         }
diff --git a/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractLegacyRenderingTest.kt b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractLegacyRenderingTest.kt
new file mode 100644
index 0000000..be14e2c
--- /dev/null
+++ b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractLegacyRenderingTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer
+
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode
+import org.jetbrains.kotlin.analysis.api.renderer.declarations.bodies.KaRendererBodyMemberScopeSorter
+import org.jetbrains.kotlin.analysis.api.renderer.declarations.impl.KaDeclarationRendererForSource
+import org.jetbrains.kotlin.analysis.api.renderer.declarations.renderers.KaClassifierBodyRenderer
+import org.jetbrains.kotlin.analysis.api.renderer.render
+import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.markers.KaDeclarationContainerSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.symbol
+import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedTest
+import org.jetbrains.kotlin.analysis.test.framework.projectStructure.KtTestModule
+import org.jetbrains.kotlin.analysis.test.framework.utils.executeOnPooledThreadInReadAction
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.test.services.TestServices
+import org.jetbrains.kotlin.test.services.assertions
+
+abstract class AbstractLegacyRenderingTest : AbstractAnalysisApiBasedTest() {
+    override fun doTestByMainFile(mainFile: KtFile, mainModule: KtTestModule, testServices: TestServices) {
+        val renderer = KaDeclarationRendererForSource.WITH_SHORT_NAMES.with {
+            classifierBodyRenderer = KaClassifierBodyRenderer.BODY_WITH_MEMBERS
+            bodyMemberScopeSorter = object : KaRendererBodyMemberScopeSorter {
+                override fun sortMembers(
+                    analysisSession: KaSession,
+                    members: List<KaDeclarationSymbol>,
+                    container: KaDeclarationContainerSymbol,
+                ): List<KaDeclarationSymbol> {
+                    with(analysisSession) {
+                        return KaRendererBodyMemberScopeSorter.ENUM_ENTRIES_AT_BEGINING
+                            .sortMembers(analysisSession, members, container)
+                            .sortedBy { it.render() }
+                    }
+                }
+            }
+        }
+
+        val actual = executeOnPooledThreadInReadAction {
+            buildString {
+                // Since we want to render the whole file, we shouldn't use `IGNORE_SELF` as it's only designed for local use cases.
+                copyAwareAnalyzeForTest(
+                    mainFile,
+                    danglingFileResolutionMode = KaDanglingFileResolutionMode.PREFER_SELF,
+                ) { contextFile ->
+                    contextFile.declarations.forEach { declaration ->
+                        append(declaration.symbol.render(renderer))
+                        appendLine()
+                        appendLine()
+                    }
+                }
+            }
+        }
+
+        testServices.assertions.assertEqualsToTestOutputFile(actual, extension = ".rendered")
+    }
+}
diff --git a/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractRendererTest.kt b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractRendererTest.kt
index ddbf7ca..3676742 100644
--- a/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractRendererTest.kt
+++ b/analysis/analysis-api-impl-base/testFixtures/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/symbolDeclarationRenderer/AbstractRendererTest.kt
@@ -5,15 +5,14 @@
 
 package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer
 
-import org.jetbrains.kotlin.analysis.api.KaSession
 import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode
-import org.jetbrains.kotlin.analysis.api.renderer.declarations.bodies.KaRendererBodyMemberScopeSorter
-import org.jetbrains.kotlin.analysis.api.renderer.declarations.impl.KaDeclarationRendererForSource
-import org.jetbrains.kotlin.analysis.api.renderer.declarations.renderers.KaClassifierBodyRenderer
-import org.jetbrains.kotlin.analysis.api.renderer.render
-import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
-import org.jetbrains.kotlin.analysis.api.symbols.markers.KaDeclarationContainerSymbol
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderer
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOption
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderingOutput
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
 import org.jetbrains.kotlin.analysis.api.symbols.symbol
+import org.jetbrains.kotlin.analysis.test.data.manager.TestVariantChain
+import org.jetbrains.kotlin.analysis.test.data.manager.withAdditionalVariant
 import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedTest
 import org.jetbrains.kotlin.analysis.test.framework.projectStructure.KtTestModule
 import org.jetbrains.kotlin.analysis.test.framework.utils.executeOnPooledThreadInReadAction
@@ -22,24 +21,10 @@
 import org.jetbrains.kotlin.test.services.assertions
 
 abstract class AbstractRendererTest : AbstractAnalysisApiBasedTest() {
-    override fun doTestByMainFile(mainFile: KtFile, mainModule: KtTestModule, testServices: TestServices) {
-        val renderer = KaDeclarationRendererForSource.WITH_SHORT_NAMES.with {
-            classifierBodyRenderer = KaClassifierBodyRenderer.BODY_WITH_MEMBERS
-            bodyMemberScopeSorter = object : KaRendererBodyMemberScopeSorter {
-                override fun sortMembers(
-                    analysisSession: KaSession,
-                    members: List<KaDeclarationSymbol>,
-                    container: KaDeclarationContainerSymbol,
-                ): List<KaDeclarationSymbol> {
-                    with(analysisSession) {
-                        return KaRendererBodyMemberScopeSorter.ENUM_ENTRIES_AT_BEGINING
-                            .sortMembers(analysisSession, members, container)
-                            .sortedBy { it.render() }
-                    }
-                }
-            }
-        }
+    override val variantChain: TestVariantChain
+        get() = super.variantChain.withAdditionalVariant("new")
 
+    override fun doTestByMainFile(mainFile: KtFile, mainModule: KtTestModule, testServices: TestServices) {
         val actual = executeOnPooledThreadInReadAction {
             buildString {
                 // Since we want to render the whole file, we shouldn't use `IGNORE_SELF` as it's only designed for local use cases.
@@ -47,8 +32,20 @@
                     mainFile,
                     danglingFileResolutionMode = KaDanglingFileResolutionMode.PREFER_SELF,
                 ) { contextFile ->
+                    val renderer = KaRenderer.default.copy {
+                        // Reproduce the legacy member ordering (see `AbstractLegacyRenderingTest`).
+                        set(KaRenderingOption.ClassMemberOrdering) { first, second ->
+                            fun renderToString(symbol: KaSymbol): String =
+                                KaRenderingOutput.plainString().also { KaRenderer.default.render(symbol, it) }.toString()
+
+                            renderToString(first).compareTo(renderToString(second))
+                        }
+                    }
+
                     contextFile.declarations.forEach { declaration ->
-                        append(declaration.symbol.render(renderer))
+                        val output = KaRenderingOutput.plainString(indentationUnit = "  ")
+                        renderer.render(declaration.symbol, output)
+                        append(output.toString())
                         appendLine()
                         appendLine()
                     }
diff --git a/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirStandaloneNormalAnalysisSourceModuleLegacyRenderingTestGenerated.java b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirStandaloneNormalAnalysisSourceModuleLegacyRenderingTestGenerated.java
new file mode 100644
index 0000000..1eab65f
--- /dev/null
+++ b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/symbolDeclarationRenderer/FirStandaloneNormalAnalysisSourceModuleLegacyRenderingTestGenerated.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2010-2024 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.analysis.api.standalone.fir.test.cases.generated.cases.components.symbolDeclarationRenderer;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.util.KtTestUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.kotlin.analysis.api.standalone.fir.test.configurators.AnalysisApiFirStandaloneModeTestConfiguratorFactory;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfiguratorFactoryData;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.TestModuleKind;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisSessionMode;
+import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiMode;
+import org.jetbrains.kotlin.analysis.test.framework.services.TargetPlatformEnum;
+import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.components.symbolDeclarationRenderer.AbstractLegacyRenderingTest;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlin.analysis.api.standalone.fir.test.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration")
+@TestDataPath("$PROJECT_ROOT")
+public class FirStandaloneNormalAnalysisSourceModuleLegacyRenderingTestGenerated extends AbstractLegacyRenderingTest {
+  @NotNull
+  @Override
+  public AnalysisApiTestConfigurator getConfigurator() {
+    return AnalysisApiFirStandaloneModeTestConfiguratorFactory.INSTANCE.createConfigurator(
+      new AnalysisApiTestConfiguratorFactoryData(
+        TestModuleKind.Source,
+        AnalysisSessionMode.Normal,
+        AnalysisApiMode.Standalone,
+        TargetPlatformEnum.JVM
+      )
+    );
+  }
+
+  private void run(String fileName) {
+    runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/" + fileName);
+  }
+
+  @Test
+  @TestMetadata("actualInheritance.kt")
+  public void testActualInheritance() {
+    run("actualInheritance.kt");
+  }
+
+  @Test
+  public void testAllFilesPresentInRenderDeclaration() {
+    KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration"), Pattern.compile("^(.+)\\.(kt)$"), null, true);
+  }
+
+  @Test
+  @TestMetadata("annotation.kt")
+  public void testAnnotation() {
+    run("annotation.kt");
+  }
+
+  @Test
+  @TestMetadata("complexTypes.kt")
+  public void testComplexTypes() {
+    run("complexTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorInObject.kt")
+  public void testConstructorInObject() {
+    run("constructorInObject.kt");
+  }
+
+  @Test
+  @TestMetadata("constructorOfAnonymousObject.kt")
+  public void testConstructorOfAnonymousObject() {
+    run("constructorOfAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("contextParameter.kt")
+  public void testContextParameter() {
+    run("contextParameter.kt");
+  }
+
+  @Test
+  @TestMetadata("delegates.kt")
+  public void testDelegates() {
+    run("delegates.kt");
+  }
+
+  @Test
+  @TestMetadata("derivedClass.kt")
+  public void testDerivedClass() {
+    run("derivedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("emptyAnonymousObject.kt")
+  public void testEmptyAnonymousObject() {
+    run("emptyAnonymousObject.kt");
+  }
+
+  @Test
+  @TestMetadata("enums.kt")
+  public void testEnums() {
+    run("enums.kt");
+  }
+
+  @Test
+  @TestMetadata("enums2.kt")
+  public void testEnums2() {
+    run("enums2.kt");
+  }
+
+  @Test
+  @TestMetadata("expectActual.kt")
+  public void testExpectActual() {
+    run("expectActual.kt");
+  }
+
+  @Test
+  @TestMetadata("F.kt")
+  public void testF() {
+    run("F.kt");
+  }
+
+  @Test
+  @TestMetadata("functionTypes.kt")
+  public void testFunctionTypes() {
+    run("functionTypes.kt");
+  }
+
+  @Test
+  @TestMetadata("functionalTypeAliases.kt")
+  public void testFunctionalTypeAliases() {
+    run("functionalTypeAliases.kt");
+  }
+
+  @Test
+  @TestMetadata("genericFunctions.kt")
+  public void testGenericFunctions() {
+    run("genericFunctions.kt");
+  }
+
+  @Test
+  @TestMetadata("genericProperty.kt")
+  public void testGenericProperty() {
+    run("genericProperty.kt");
+  }
+
+  @Test
+  @TestMetadata("intersectionType.kt")
+  public void testIntersectionType() {
+    run("intersectionType.kt");
+  }
+
+  @Test
+  @TestMetadata("nestedClass.kt")
+  public void testNestedClass() {
+    run("nestedClass.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedOfAliasedType.kt")
+  public void testNestedOfAliasedType() {
+    run("NestedOfAliasedType.kt");
+  }
+
+  @Test
+  @TestMetadata("NestedSuperType.kt")
+  public void testNestedSuperType() {
+    run("NestedSuperType.kt");
+  }
+
+  @Test
+  @TestMetadata("noPrimaryConstructor.kt")
+  public void testNoPrimaryConstructor() {
+    run("noPrimaryConstructor.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleClass.kt")
+  public void testSimpleClass() {
+    run("simpleClass.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleFun.kt")
+  public void testSimpleFun() {
+    run("simpleFun.kt");
+  }
+
+  @Test
+  @TestMetadata("simpleTypeAlias.kt")
+  public void testSimpleTypeAlias() {
+    run("simpleTypeAlias.kt");
+  }
+
+  @Test
+  @TestMetadata("typeAliasWithGeneric.kt")
+  public void testTypeAliasWithGeneric() {
+    run("typeAliasWithGeneric.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameterVsNested.kt")
+  public void testTypeParameterVsNested() {
+    run("typeParameterVsNested.kt");
+  }
+
+  @Test
+  @TestMetadata("typeParameters.kt")
+  public void testTypeParameters() {
+    run("typeParameters.kt");
+  }
+
+  @Test
+  @TestMetadata("vararg.kt")
+  public void testVararg() {
+    run("vararg.kt");
+  }
+
+  @Test
+  @TestMetadata("where.kt")
+  public void testWhere() {
+    run("where.kt");
+  }
+
+  @Nested
+  @TestMetadata("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types")
+  @TestDataPath("$PROJECT_ROOT")
+  public class Types {
+    private void run(String fileName) {
+      runTest("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/" + fileName);
+    }
+
+    @Test
+    public void testAllFilesPresentInTypes() {
+      KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types"), Pattern.compile("^(.+)\\.(kt)$"), null, true);
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypes.kt")
+    public void testAnnotationOnTypes() {
+      run("annotationOnTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("annotationOnTypesWithComplexExpression.kt")
+    public void testAnnotationOnTypesWithComplexExpression() {
+      run("annotationOnTypesWithComplexExpression.kt");
+    }
+
+    @Test
+    @TestMetadata("approximatedCapturedType.kt")
+    public void testApproximatedCapturedType() {
+      run("approximatedCapturedType.kt");
+    }
+
+    @Test
+    @TestMetadata("contextFunctionTypes.kt")
+    public void testContextFunctionTypes() {
+      run("contextFunctionTypes.kt");
+    }
+
+    @Test
+    @TestMetadata("extraTypeArguments.kt")
+    public void testExtraTypeArguments() {
+      run("extraTypeArguments.kt");
+    }
+  }
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/internals/KaRendererProvider.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/internals/KaRendererProvider.kt
new file mode 100644
index 0000000..7a5f445
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/internals/KaRendererProvider.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.internals
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.rendering.KaRenderer
+
+@KaExperimentalApi
+public interface KaRendererProvider {
+    public val defaultRenderer: KaRenderer
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaPieceRenderer.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaPieceRenderer.kt
new file mode 100644
index 0000000..aa42a98
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaPieceRenderer.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotated
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotation
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotationValue
+import org.jetbrains.kotlin.analysis.api.base.KaConstantValue
+import org.jetbrains.kotlin.analysis.api.symbols.KaAnonymousObjectSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaBackingFieldSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaContextParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaEnumEntrySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaJavaFieldSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaLocalVariableSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaNamedClassSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPackageSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertyAccessorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
+import org.jetbrains.kotlin.analysis.api.types.KaCapturedType
+import org.jetbrains.kotlin.analysis.api.types.KaClassType
+import org.jetbrains.kotlin.analysis.api.types.KaDefinitelyNotNullType
+import org.jetbrains.kotlin.analysis.api.types.KaDynamicType
+import org.jetbrains.kotlin.analysis.api.types.KaErrorType
+import org.jetbrains.kotlin.analysis.api.types.KaFlexibleType
+import org.jetbrains.kotlin.analysis.api.types.KaFunctionType
+import org.jetbrains.kotlin.analysis.api.types.KaIntersectionType
+import org.jetbrains.kotlin.analysis.api.types.KaType
+import org.jetbrains.kotlin.analysis.api.types.KaTypeParameterType
+import org.jetbrains.kotlin.analysis.api.types.KaTypeProjection
+
+/**
+ * Renders a single [KaPiece] into the contextual [KaRenderingOutput].
+ *
+ * Renderers for a given [piece] form a stack within a [KaRenderer]; [KaRendererBuilder.push] adds one on top. A renderer may
+ * emit nested pieces via the top-level [render] function and fall back to the renderer beneath it by invoking its `next` callback.
+ *
+ * @param piece the piece this renderer is responsible for.
+ */
+@KaExperimentalApi
+public abstract class KaPieceRenderer<T>(public val piece: KaPiece<T>) {
+    /**
+     * Renders [value] into the contextual [KaRenderingOutput]. Returns `true` if the value was handled, or `false` to fall back to
+     * the renderer beneath this one. Invoke [next] to render [value] with the renderer immediately beneath this one.
+     */
+    context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+    public abstract fun render(value: T, next: () -> Unit): Boolean
+
+    @KaExperimentalApi
+    public companion object {
+        /**
+         * The no-op renderer for the given [piece].
+         */
+        public fun <T> empty(piece: KaPiece<T>): KaPieceRenderer<T> {
+            return object : KaPieceRenderer<T>(piece) {
+                context(session: KaSession, context: KaRenderingContext, output: KaRenderingOutput)
+                override fun render(value: T, next: () -> Unit): Boolean = true
+            }
+        }
+    }
+}
+
+/** The rendering state passed to every [KaPieceRenderer], used to render nested pieces and to read [KaRenderingOption] values. */
+@KaExperimentalApi
+public interface KaRenderingContext {
+    /** Renders [value] as the given [piece], dispatching to the corresponding renderer stack. */
+    context(session: KaSession)
+    public fun <T> render(value: T, piece: KaPiece<T>)
+
+    /** Returns the effective value of [option] for this renderer (the overridden value, or its [KaRenderingOption.defaultValue]). */
+    public fun <T> valueFor(option: KaRenderingOption<T>): T
+}
+
+/** Renders [value] as the given [piece], dispatching to the corresponding renderer stack. */
+@KaExperimentalApi
+context(session: KaSession, context: KaRenderingContext)
+public fun <T> render(value: T, piece: KaPiece<T>) {
+    context.render(value, piece)
+}
+
+/**
+ * A typed, enumerated key that identifies a specific piece of a symbol or a type to render.
+ *
+ * Each [KaPiece] is associated with a stack of [KaPieceRenderer]s in a [KaRenderer]. Renderers may render other pieces via
+ * [KaRenderingContext], which allows customizing individual parts of the output without reimplementing the whole rendering.
+ *
+ * The two top-level entry points are [Symbol] (renders any [KaSymbol]) and [Type] (renders any [KaType]).
+ */
+@KaExperimentalApi
+public class KaPiece<T> private constructor() {
+    @KaExperimentalApi
+    public companion object {
+        /** The annotations of an annotated element, including the whitespace which separates them from what follows. */
+        public val Annotations: KaPiece<KaAnnotated> = KaPiece()
+
+        /** A single annotation entry, e.g. `@Foo(bar)`. */
+        public val Annotation: KaPiece<KaAnnotation> = KaPiece()
+
+        /** The parenthesized argument list of an annotation, e.g. `(message = "Use bar")`. */
+        public val AnnotationValues: KaPiece<KaAnnotation> = KaPiece()
+
+        /** A single annotation argument value (a constant, enum entry, class literal, nested annotation, or array). */
+        public val AnnotationValue: KaPiece<KaAnnotationValue> = KaPiece()
+
+        /** A constant value, e.g. `42`, `"text"`, or `null`. */
+        public val ConstantValue: KaPiece<KaConstantValue> = KaPiece()
+
+        /** Any [KaType]. Dispatches to a more specific type piece depending on the type kind. */
+        public val Type: KaPiece<KaType> = KaPiece()
+
+        /** The annotations attached to a [KaType]. */
+        public val TypeAnnotations: KaPiece<KaType> = KaPiece()
+
+        /** The nullability marker of a type, e.g. `?` in `String?`. Nothing is rendered for a non-nullable type. */
+        public val TypeNullability: KaPiece<KaType> = KaPiece()
+
+        /** A single type projection (an explicit type argument or a star projection). */
+        public val TypeProjection: KaPiece<KaTypeProjection> = KaPiece()
+
+        /** A comma-separated list of type arguments enclosed in angle brackets, e.g. `<Int, String>`. */
+        public val TypeArgumentList: KaPiece<List<KaTypeProjection>> = KaPiece()
+
+        /** A class type, e.g. `List<String>`. */
+        public val ClassType: KaPiece<KaClassType> = KaPiece()
+
+        /** A function type, e.g. `(Int) -> String`. */
+        public val FunctionType: KaPiece<KaFunctionType> = KaPiece()
+
+        /** A reference to a type parameter, e.g. `T`. */
+        public val TypeParameterType: KaPiece<KaTypeParameterType> = KaPiece()
+
+        /** A captured type produced by capturing a projection during type inference. */
+        public val CapturedType: KaPiece<KaCapturedType> = KaPiece()
+
+        /** A definitely non-nullable type, e.g. `T & Any`. */
+        public val DefinitelyNotNullType: KaPiece<KaDefinitelyNotNullType> = KaPiece()
+
+        /** A flexible type spanning a lower and an upper bound, such as a platform type. */
+        public val FlexibleType: KaPiece<KaFlexibleType> = KaPiece()
+
+        /** An intersection of several types, e.g. `A & B`. */
+        public val IntersectionType: KaPiece<KaIntersectionType> = KaPiece()
+
+        /** The Kotlin/JS `dynamic` type. */
+        public val DynamicType: KaPiece<KaDynamicType> = KaPiece()
+
+        /** A type that could not be resolved. */
+        public val ErrorType: KaPiece<KaErrorType> = KaPiece()
+
+        /** Any [KaSymbol]. Dispatches to a more specific symbol piece depending on the symbol kind. */
+        public val Symbol: KaPiece<KaSymbol> = KaPiece()
+
+        /** The name of a declaration, as rendered in the declaration itself, e.g. `foo` in `fun foo()`. */
+        public val SymbolName: KaPiece<KaSymbol> = KaPiece()
+
+        /** Common modifiers shared by all declarations (visibility, `expect`/`actual`, `external`). */
+        public val SymbolModifiers: KaPiece<KaDeclarationSymbol> = KaPiece()
+
+        /** The visibility modifier of a declaration, e.g. `private`. Nothing is rendered for a public or implicit visibility. */
+        public val Visibility: KaPiece<KaDeclarationSymbol> = KaPiece()
+
+        /** The modality modifier of a declaration, e.g. `abstract`. Nothing is rendered for a final, implicit, or redundant modality. */
+        public val Modality: KaPiece<KaDeclarationSymbol> = KaPiece()
+
+        /** A comma-separated list of type parameters enclosed in angle brackets, e.g. `<T : Number>`. */
+        public val TypeParameterList: KaPiece<List<KaTypeParameterSymbol>> = KaPiece()
+
+        /** The type parameter list of a callable. */
+        public val CallableTypeParameterList: KaPiece<KaCallableSymbol> = KaPiece()
+
+        /** The type parameter list of a class or a type alias. */
+        public val ClassifierTypeParameterList: KaPiece<KaClassLikeSymbol> = KaPiece()
+
+        /** A single type parameter, including its variance, `reified` modifier, and upper bound. */
+        public val TypeParameter: KaPiece<KaTypeParameterSymbol> = KaPiece()
+
+        /** The `where` clause listing the bounds of type parameters that have more than one upper bound. */
+        public val WhereClause: KaPiece<List<KaTypeParameterSymbol>> = KaPiece()
+
+        /** The `where` clause of a callable. */
+        public val CallableWhereClause: KaPiece<KaCallableSymbol> = KaPiece()
+
+        /** The `where` clause of a class or a type alias. */
+        public val ClassifierWhereClause: KaPiece<KaClassLikeSymbol> = KaPiece()
+
+        /** A `context(...)` clause. */
+        public val ContextParameterList: KaPiece<List<KaContextParameterSymbol>> = KaPiece()
+
+        /** The `context(...)` clause of a callable. */
+        public val CallableContextParameterList: KaPiece<KaCallableSymbol> = KaPiece()
+
+        /** A single context parameter. */
+        public val ContextParameter: KaPiece<KaContextParameterSymbol> = KaPiece()
+
+        /** The annotations of a context parameter. */
+        public val ContextParameterAnnotations: KaPiece<KaContextParameterSymbol> = KaPiece()
+
+        /** The type of context parameter. */
+        public val ContextParameterType: KaPiece<KaContextParameterSymbol> = KaPiece()
+
+        /** The parenthesized value parameter list, e.g. `(a: Int, b: String)`. */
+        public val ValueParameterList: KaPiece<List<KaValueParameterSymbol>> = KaPiece()
+
+        /** A single value parameter, including its modifiers, name, type, and default value. */
+        public val ValueParameter: KaPiece<KaValueParameterSymbol> = KaPiece()
+
+        /** The annotations of a value parameter. */
+        public val ValueParameterAnnotations: KaPiece<KaValueParameterSymbol> = KaPiece()
+
+        /** The type of value parameter. */
+        public val ValueParameterType: KaPiece<KaValueParameterSymbol> = KaPiece()
+
+        /** The default value of a value parameter, including the leading `=`, e.g. `= ...`. */
+        public val ValueParameterDefaultValue: KaPiece<KaValueParameterSymbol> = KaPiece()
+
+        /** Any [KaFunctionSymbol]; used for anonymous functions and SAM constructors. */
+        public val Function: KaPiece<KaFunctionSymbol> = KaPiece()
+
+        /** The modifiers of a function, e.g. `suspend`, `operator`, `inline`. */
+        public val FunctionModifiers: KaPiece<KaFunctionSymbol> = KaPiece()
+
+        /** The annotations of a function. */
+        public val FunctionAnnotations: KaPiece<KaFunctionSymbol> = KaPiece()
+
+        /** The receiver of an extension callable, including the trailing dot, e.g. `String.` in `fun String.foo()`. */
+        public val FunctionReceiver: KaPiece<KaParameterSymbol> = KaPiece()
+
+        /** The value parameter list of a function. */
+        public val FunctionValueParameterList: KaPiece<KaFunctionSymbol> = KaPiece()
+
+        /** The return type of a function, including the leading colon, e.g. `: String`. The type is always rendered. */
+        public val FunctionReturnType: KaPiece<KaFunctionSymbol> = KaPiece()
+
+        /** The return type of named function, including the leading colon, e.g. `: String`. */
+        public val NamedFunctionReturnType: KaPiece<KaNamedFunctionSymbol> = KaPiece()
+
+        /** A named function declaration. */
+        public val NamedFunction: KaPiece<KaNamedFunctionSymbol> = KaPiece()
+
+        /** A constructor declaration. */
+        public val Constructor: KaPiece<KaConstructorSymbol> = KaPiece()
+
+        /** The primary constructor of a class, as rendered in the class header, e.g. `(x: Int)` in `class Foo(x: Int)`. */
+        public val PrimaryConstructor: KaPiece<KaConstructorSymbol> = KaPiece()
+
+        /** The non-default accessors of a property, each on its own indented line below the property. */
+        public val PropertyAccessors: KaPiece<KaPropertySymbol> = KaPiece()
+
+        /** A property getter or setter. */
+        public val PropertyAccessor: KaPiece<KaPropertyAccessorSymbol> = KaPiece()
+
+        /** A property declaration (`val` or `var`). */
+        public val Property: KaPiece<KaPropertySymbol> = KaPiece()
+
+        /** The modifiers of a property, e.g. `const`, `lateinit`, `override`. */
+        public val PropertyModifiers: KaPiece<KaPropertySymbol> = KaPiece()
+
+        /** The annotations of a property. */
+        public val PropertyAnnotations: KaPiece<KaPropertySymbol> = KaPiece()
+
+        /** The type of property. */
+        public val PropertyReturnType: KaPiece<KaPropertySymbol> = KaPiece()
+
+        /** A local variable declaration. */
+        public val LocalVariable: KaPiece<KaLocalVariableSymbol> = KaPiece()
+
+        /** A field declared in Java. */
+        public val JavaField: KaPiece<KaJavaFieldSymbol> = KaPiece()
+
+        /** The backing field of a property. */
+        public val BackingField: KaPiece<KaBackingFieldSymbol> = KaPiece()
+
+        /** A single enum entry. */
+        public val EnumEntry: KaPiece<KaEnumEntrySymbol> = KaPiece()
+
+        /** A named class, interface, or object declaration. */
+        public val NamedClass: KaPiece<KaNamedClassSymbol> = KaPiece()
+
+        /** An anonymous object, e.g. `object : Runnable { ... }`. */
+        public val AnonymousObject: KaPiece<KaAnonymousObjectSymbol> = KaPiece()
+
+        /** A type alias declaration. */
+        public val TypeAlias: KaPiece<KaTypeAliasSymbol> = KaPiece()
+
+        /** The modifiers of a class, e.g. `data`, `sealed`, `enum`, `inner`. */
+        public val ClassModifiers: KaPiece<KaNamedClassSymbol> = KaPiece()
+
+        /** The annotations of a class. */
+        public val ClassAnnotations: KaPiece<KaNamedClassSymbol> = KaPiece()
+
+        /** The supertype list of a class, e.g. `: Base, Runnable`. */
+        public val SupertypeList: KaPiece<KaClassSymbol> = KaPiece()
+
+        /** A single supertype, including the constructor call parentheses of a class supertype, e.g. `Base()`. */
+        public val Supertype: KaPiece<KaType> = KaPiece()
+
+        /** The body of a class, containing its member declarations. */
+        public val ClassBody: KaPiece<KaClassSymbol> = KaPiece()
+
+        /** A package, e.g. `package kotlin.collections`. */
+        public val Package: KaPiece<KaPackageSymbol> = KaPiece()
+    }
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderer.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderer.kt
new file mode 100644
index 0000000..7ac0d36
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderer.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.rendering
+
+import com.intellij.openapi.components.service
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.base.KaConstantValue
+import org.jetbrains.kotlin.analysis.api.internals.KaRendererProvider
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+import org.jetbrains.kotlin.analysis.api.types.KaType
+import org.jetbrains.kotlin.analysis.api.types.KaTypeProjection
+
+/**
+ * Renders [KaSymbol]s, [KaType]s, [KaTypeProjection]s, and [KaConstantValue]s into a [KaRenderingOutput] as human-readable, Kotlin-like
+ * text.
+ *
+ * A renderer is an immutable bundle of [KaPieceRenderer]s (a stack per [KaPiece]) together with [KaRenderingOption] values. Obtain the
+ * standard renderer via [default] and derive customized variants with [copy].
+ */
+@KaExperimentalApi
+public interface KaRenderer {
+    /** Renders the declaration of [symbol] — its signature and modifiers, and, for a class, its body — into [output]. */
+    context(session: KaSession)
+    public fun render(symbol: KaSymbol, output: KaRenderingOutput)
+
+    /** Renders [type] into [output]. */
+    context(session: KaSession)
+    public fun render(type: KaType, output: KaRenderingOutput)
+
+    /** Renders [constantValue] into [output]. */
+    context(session: KaSession)
+    public fun render(constantValue: KaConstantValue, output: KaRenderingOutput)
+
+    /**
+     * Returns a copy of this renderer with the customizations applied by [block] (overriding [KaRenderingOption]s or pushing
+     * additional [KaPieceRenderer]s). This renderer is left unchanged.
+     */
+    public fun copy(block: KaRendererBuilder.() -> Unit): KaRenderer
+
+    @KaExperimentalApi
+    public companion object {
+        /** The standard [KaRenderer], which renders declarations and types close to their Kotlin source form. */
+        public val default: KaRenderer
+            get() {
+                @OptIn(KaExperimentalApi::class)
+                return service<KaRendererProvider>().defaultRenderer
+            }
+    }
+}
+
+/** Renders the [symbol] into a string. */
+@KaExperimentalApi
+context(session: KaSession)
+public fun KaRenderer.renderToString(symbol: KaSymbol): String {
+    val output = KaRenderingOutput.plainString()
+    render(symbol, output)
+    return output.toString()
+}
+
+/** Renders the [type] into a string. */
+@KaExperimentalApi
+context(session: KaSession)
+public fun KaRenderer.renderToString(type: KaType): String {
+    val output = KaRenderingOutput.plainString()
+    render(type, output)
+    return output.toString()
+}
+
+/** Renders the [constantValue] into a string. */
+@KaExperimentalApi
+context(session: KaSession)
+public fun KaRenderer.renderToString(constantValue: KaConstantValue): String {
+    val output = KaRenderingOutput.plainString()
+    render(constantValue, output)
+    return output.toString()
+}
+
+/** A mutable builder for customizing a [KaRenderer], available within [KaRenderer.copy]. */
+@KaExperimentalApi
+public interface KaRendererBuilder {
+    /** Pushes [renderer] on top of the stack for its [KaPieceRenderer.piece], so it takes precedence over the renderers below it. */
+    public fun <T> push(renderer: KaPieceRenderer<T>)
+
+    /** Overrides the value of [option]. */
+    public fun <T> set(option: KaRenderingOption<T>, value: T)
+
+    /** Resets [option] back to its [KaRenderingOption.defaultValue]. */
+    public fun <T> unset(option: KaRenderingOption<T>)
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOption.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOption.kt
new file mode 100644
index 0000000..e9e1de0
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOption.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotated
+import org.jetbrains.kotlin.analysis.api.scopes.declaredMemberScope
+import org.jetbrains.kotlin.analysis.api.scopes.staticDeclaredMemberScope
+import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaEnumEntrySymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaParameterSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolOrigin
+import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol
+import org.jetbrains.kotlin.analysis.api.types.KaType
+import org.jetbrains.kotlin.analysis.api.types.augmentedByWarningLevelAnnotations
+
+/**
+ * A typed configuration flag that influences rendering. Its [defaultValue] is used unless overridden via [KaRendererBuilder.set].
+ *
+ * The available options are defined as properties of the companion object.
+ */
+@KaExperimentalApi
+public class KaRenderingOption<T> internal constructor(public val defaultValue: T) {
+    @KaExperimentalApi
+    public companion object {
+        /** How qualified class type names are rendered (with the package, with outer classifiers, or as a simple name). */
+        public val ClassTypeQualification: KaRenderingOption<KaClassTypeQualification> =
+            KaRenderingOption(KaClassTypeQualification.WITH_OUTER_CLASSIFIERS)
+
+        /**
+         * A transformation applied to every rendered [KaType] before it is printed.
+         *
+         * The transformation is applied to each type as it is rendered, so it also affects nested types, such as type arguments, upper
+         * bounds, and the components of flexible and intersection types. It runs before [TypeApproximation].
+         *
+         * By default, warning-level nullability annotations are treated as strict ones (see [augmentedByWarningLevelAnnotations]), so
+         * `@RecentlyNullable X!` is rendered as `X?`.
+         */
+        public val TypeTransformation: KaRenderingOption<context(KaSession, KaRenderingContext) (KaType) -> KaType> =
+            KaRenderingOption { type -> type.augmentedByWarningLevelAnnotations }
+
+        /**
+         * Whether every rendered [KaType] is approximated to a type that can be written in Kotlin source code, and in which direction.
+         *
+         * The approximation is applied to each type as it is rendered, so it also affects nested types. It runs after
+         * [TypeTransformation].
+         */
+        public val TypeApproximation: KaRenderingOption<KaTypeApproximation> =
+            KaRenderingOption(KaTypeApproximation.NONE)
+
+        /** Whether the primary constructor is rendered in the class header (e.g. `class Foo(x: Int)`) rather than as a body member. */
+        public val PrimaryConstructorInClassHeader: KaRenderingOption<Boolean> =
+            KaRenderingOption(true)
+
+        /** Whether the `context(...)` receivers of the given element are placed on their own line rather than inline. */
+        public val ContextReceiversOnNewLine: KaRenderingOption<context(KaSession, KaRenderingContext) (Any) -> Boolean> =
+            KaRenderingOption { _ -> true }
+
+        /** Whether the annotations of the given element are placed on their own line rather than inline before it. */
+        public val AnnotationsOnNewLine: KaRenderingOption<context(KaSession, KaRenderingContext) (KaAnnotated) -> Boolean> =
+            KaRenderingOption { value ->
+                when (value) {
+                    is KaType, is KaTypeParameterSymbol, is KaParameterSymbol, is KaEnumEntrySymbol -> false
+                    is KaConstructorSymbol -> !value.isPrimary
+                    else -> true
+                }
+            }
+
+        /** Whether a blank line separates consecutive members within a class body. */
+        public val ExtraLineBetweenMembers: KaRenderingOption<Boolean> =
+            KaRenderingOption(true)
+
+        /** The symbol origins whose members are excluded from a rendered class body (e.g. inherited, generated, or delegated members). */
+        public val ClassMemberOrigins: KaRenderingOption<Set<KaSymbolOrigin>> =
+            KaRenderingOption(
+                setOf(
+                    KaSymbolOrigin.DELEGATED,
+                    KaSymbolOrigin.SOURCE_MEMBER_GENERATED,
+                    KaSymbolOrigin.SUBSTITUTION_OVERRIDE,
+                    KaSymbolOrigin.INTERSECTION_OVERRIDE,
+                )
+            )
+
+        /** Computes the members to render for a class, in the order they are gathered. */
+        public val ClassMembers: KaRenderingOption<context(KaSession, KaRenderingContext) (KaClassSymbol) -> List<KaSymbol>> =
+            KaRenderingOption { classSymbol ->
+                val context = contextOf<KaRenderingContext>()
+                val excludePrimaryConstructor = context.valueFor(PrimaryConstructorInClassHeader)
+                val allowedOrigins = context.valueFor(ClassMemberOrigins)
+
+                buildList {
+                    val declaredScope = classSymbol.declaredMemberScope
+
+                    if (!classSymbol.classKind.isObject) {
+                        addAll(declaredScope.constructors.filter { !excludePrimaryConstructor || !it.isPrimary }.toList())
+                    }
+
+                    addAll(declaredScope.callables.toList())
+
+                    val staticDeclaredScope = classSymbol.staticDeclaredMemberScope
+                    addAll(staticDeclaredScope.callables.toList())
+                    addAll(staticDeclaredScope.classifiers.toList())
+                }.filterNot { it.origin in allowedOrigins }
+            }
+
+        /** A comparator establishing the order in which class members are rendered. The default (returning `0`) keeps the [ClassMembers] order. */
+        public val ClassMemberOrdering: KaRenderingOption<context(KaSession, KaRenderingContext) (KaSymbol, KaSymbol) -> Int> =
+            KaRenderingOption { _, _ -> 0 }
+    }
+}
+
+/** Controls how qualified the name of a class type is rendered. */
+@KaExperimentalApi
+public enum class KaClassTypeQualification {
+    /** The fully qualified name, including the package, e.g. `org.example.Foo.Nested`. */
+    FULLY_QUALIFIED,
+
+    /** The name prefixed with its outer classifiers, but without the package, e.g. `Foo.Nested`. */
+    WITH_OUTER_CLASSIFIERS,
+
+    /** The simple name only, e.g. `Nested`. */
+    SIMPLE,
+}
+
+/**
+ * Controls whether rendered types are approximated to *denotable* types (types which can be written in Kotlin source code), and in which
+ * direction.
+ *
+ * Approximation matters for types that cannot be written down by a user, such as captured types, intersection types, or types with
+ * flexible bounds. Such a type usually has both a denotable subtype and a denotable supertype, and the correct choice depends on the
+ * position in which the type is used: an output position (e.g. a return type) requires a supertype, while an input position (e.g. a value
+ * parameter type) requires a subtype.
+ *
+ * @see KaRenderingOption.TypeApproximation
+ */
+@KaExperimentalApi
+public enum class KaTypeApproximation {
+    /** Types are rendered as they are, even if the result cannot be written in Kotlin source code. */
+    NONE,
+
+    /** Every type is approximated to its closest denotable subtype, which is correct for input positions, such as parameter types. */
+    TO_DENOTABLE_SUBTYPE,
+
+    /**
+     * Every type is approximated to its closest denotable supertype, which is correct for output positions, such as return types.
+     * Locally declared types are approximated to local supertypes.
+     */
+    TO_DENOTABLE_SUPERTYPE,
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOutput.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOutput.kt
new file mode 100644
index 0000000..780ea30
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaRenderingOutput.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.lifetime.KaLifetimeOwner
+import org.jetbrains.kotlin.analysis.api.lifetime.KaLifetimeToken
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol
+
+/**
+ * A destination for rendered output. A [KaRenderer] writes text fragments, each tagged with [KaTextAttribute]s that describe
+ * its semantic role, so a output may add syntax highlighting, produce plain text, or ignore the attributes entirely.
+ *
+ * All methods return `this` to allow chaining.
+ */
+@KaExperimentalApi
+public interface KaRenderingOutput {
+    /** Appends [text] tagged with the given [attributes]. */
+    public fun append(text: String, attributes: Set<KaTextAttribute>): KaRenderingOutput
+
+    /** Appends [text] tagged with a single [attribute]. */
+    public fun append(text: String, attribute: KaTextAttribute): KaRenderingOutput {
+        return append(text, setOf(attribute))
+    }
+
+    /** Increases the indentation level applied at the start of subsequent lines. */
+    public fun indent(): KaRenderingOutput
+
+    /** Decreases the indentation level. Must be balanced with a preceding [indent]. */
+    public fun unindent(): KaRenderingOutput
+
+    /** Starts a new line at the current indentation level. */
+    public fun newLine(): KaRenderingOutput
+
+    /** Appends a single space. */
+    public fun space(): KaRenderingOutput
+
+    @KaExperimentalApi
+    public companion object {
+        /**
+         * A simple [KaRenderingOutput] that accumulates rendered text into a plain [String], ignoring all [KaTextAttribute]s.
+         * Uses four spaces for member indentation.
+         *
+         * The rendered text is available via [toString].
+         */
+        public fun plainString(): KaRenderingOutput {
+            return KaStringRenderingOutput(indentationUnit = "    ")
+        }
+
+        /**
+         * A simple [KaRenderingOutput] that accumulates rendered text into a plain [String], ignoring all [KaTextAttribute]s.
+         * Uses the [indentationUnit] for member indentation.
+         *
+         * The rendered text is available via [toString].
+         */
+        public fun plainString(indentationUnit: String): KaRenderingOutput {
+            return KaStringRenderingOutput(indentationUnit)
+        }
+    }
+}
+
+/** Describes the semantic role of a fragment of rendered text, e.g. for syntax highlighting. */
+@KaExperimentalApi
+public sealed interface KaTextAttribute {
+    /** A hard keyword or soft/modifier keyword, such as `fun`, `val`, or `suspend`. */
+    @KaExperimentalApi
+    public object Keyword : KaTextAttribute
+
+    /** Punctuation, such as `:`, `,`, or `?`. */
+    @KaExperimentalApi
+    public object Punctuation : KaTextAttribute
+
+    /** An opening bracket of a group, such as `(`, `<`, or `{`. */
+    @KaExperimentalApi
+    public object GroupStart : KaTextAttribute
+
+    /** A closing bracket of a group, such as `)`, `>`, or `}`. */
+    @KaExperimentalApi
+    public object GroupEnd : KaTextAttribute
+
+    /** An identifier, such as a declaration or type name. See [Symbol] when the referenced symbol is known. */
+    @KaExperimentalApi
+    public object Identifier : KaTextAttribute
+
+    /** Whitespace that is not a line break. */
+    @KaExperimentalApi
+    public object Whitespace : KaTextAttribute
+
+    /** A string or character literal, such as `"text"` or `'c'`. */
+    @KaExperimentalApi
+    public object StringLiteral : KaTextAttribute
+
+    /** A numeric literal, such as `42`, `2f`, or `3u`. */
+    @KaExperimentalApi
+    public object NumberLiteral : KaTextAttribute
+
+    /** An identifier that references a known [symbol]. */
+    @KaExperimentalApi
+    public class Symbol(public val symbol: KaSymbol) : KaTextAttribute, KaLifetimeOwner {
+        override val token: KaLifetimeToken
+            get() = symbol.token
+    }
+}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaStringRenderingOutput.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaStringRenderingOutput.kt
new file mode 100644
index 0000000..5ce8d0a
--- /dev/null
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/rendering/KaStringRenderingOutput.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2010-2026 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.analysis.api.rendering
+
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+
+@KaExperimentalApi
+internal class KaStringRenderingOutput(private val indentationUnit: String) : KaRenderingOutput {
+    private val builder: StringBuilder = StringBuilder()
+    private var indentLevel: Int = 0
+    private var atLineStart: Boolean = true
+
+    override fun append(text: String, attributes: Set<KaTextAttribute>): KaRenderingOutput {
+        if (text.isEmpty()) return this
+        indentIfNeeded()
+        builder.append(text)
+        return this
+    }
+
+    override fun indent(): KaRenderingOutput {
+        indentLevel++
+        return this
+    }
+
+    override fun unindent(): KaRenderingOutput {
+        require(indentLevel > 0) { "Unbalanced indent()/unindent() calls." }
+        indentLevel--
+        return this
+    }
+
+    override fun newLine(): KaRenderingOutput {
+        // Avoid trailing whitespace at the end of the line.
+        while (builder.isNotEmpty() && builder.last() == ' ') {
+            builder.deleteCharAt(builder.length - 1)
+        }
+        builder.append('\n')
+        atLineStart = true
+        return this
+    }
+
+    override fun space(): KaRenderingOutput {
+        indentIfNeeded()
+        builder.append(' ')
+        return this
+    }
+
+    private fun indentIfNeeded() {
+        if (atLineStart) {
+            repeat(indentLevel) { builder.append(indentationUnit) }
+            atLineStart = false
+        }
+    }
+
+    override fun toString(): String = builder.toString().trim()
+}
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/annotation.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/annotation.new.rendered
new file mode 100644
index 0000000..714c8a9
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/annotation.new.rendered
@@ -0,0 +1,26 @@
+@Target(allowedTargets = [AnnotationTarget.ANNOTATION_CLASS])
+annotation class base
+
+@base
+annotation class derived
+
+@base
+class correct(x: Int) {
+  @base
+  constructor()
+
+  @base
+  val x: Int
+}
+
+@base
+enum class My {
+  @base FIRST,
+  @base SECOND
+}
+
+@base
+fun foo(@base y: @base Int): Int
+
+@base
+val z: Int
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/contextParameter.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/contextParameter.new.rendered
new file mode 100644
index 0000000..6a6ddc6
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/contextParameter.new.rendered
@@ -0,0 +1,15 @@
+interface Context2<A> {
+  fun getContextElement(): A
+}
+
+class Context2Impl<A>(value: A) : Context2<A> {
+  override fun getContextElement(): A
+
+  val value: A
+}
+
+context(b: B, `fun`: A)
+fun foo()
+
+context(`<unused var>`: Context2<C<String>>)
+fun bar()
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/simpleClass.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/simpleClass.new.rendered
new file mode 100644
index 0000000..0a18b80
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/simpleClass.new.rendered
@@ -0,0 +1,19 @@
+interface SomeInterface {
+  fun foo(x: Int, y: String): String
+
+  val bar: Boolean
+}
+
+class SomeClass : SomeInterface {
+  lateinit var fau: Double
+
+  override fun foo(x: Int, y: String): String
+
+  override var bar: Boolean
+    get()
+    set(value: Boolean)
+
+  private val baz: Int
+}
+
+value class InlineClass
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/typeParameters.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/typeParameters.new.rendered
new file mode 100644
index 0000000..d2375df
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/typeParameters.new.rendered
@@ -0,0 +1,20 @@
+@Target(allowedTargets = [AnnotationTarget.TYPE_PARAMETER])
+annotation class A
+
+interface List<@A out T : Any> {
+  infix fun concat(other: List<T>): List<T>
+
+  operator fun get(index: Int): T
+}
+
+typealias StringList = List<out String>
+
+typealias AnyList = List<*>
+
+abstract class AbstractList<out T : Any> : List<T>
+
+class SomeList : AbstractList<Int>() {
+  override infix fun concat(other: List<Int>): List<Int>
+
+  override operator fun get(index: Int): Int
+}
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypes.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypes.new.rendered
new file mode 100644
index 0000000..770023c
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypes.new.rendered
@@ -0,0 +1,9 @@
+@Target(allowedTargets = [AnnotationTarget.TYPE])
+annotation class A1
+
+@Target(allowedTargets = [AnnotationTarget.TYPE])
+annotation class A2(value: String) {
+  val value: String
+}
+
+fun x(): @A1 @A2(value = "LIST") List<@A2(value = "INT") Int>
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypesWithComplexExpression.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypesWithComplexExpression.new.rendered
new file mode 100644
index 0000000..1433689
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/annotationOnTypesWithComplexExpression.new.rendered
@@ -0,0 +1,6 @@
+@Target(allowedTargets = [AnnotationTarget.TYPE])
+annotation class A(value: Int) {
+  val value: Int
+}
+
+fun x(): @A(value = 3) Int
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/approximatedCapturedType.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/approximatedCapturedType.new.rendered
new file mode 100644
index 0000000..a37de78
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/types/approximatedCapturedType.new.rendered
@@ -0,0 +1,5 @@
+class SimpleClass
+
+val classloader: ClassLoader!
+
+val obj: KClass<out Any!>?
diff --git a/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/vararg.new.rendered b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/vararg.new.rendered
new file mode 100644
index 0000000..963737f
--- /dev/null
+++ b/analysis/analysis-api/testData/components/symbolDeclarationRenderer/renderDeclaration/vararg.new.rendered
@@ -0,0 +1,6 @@
+@Target(allowedTargets = [AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE])
+annotation class base
+
+fun foo1(vararg ints: Int)
+
+fun foo2(@base vararg ints: Int)