JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.91k stars 1.16k forks source link

Duplicate JS functions generated with the same name overwriting each other #3421

Open JakeWharton opened 1 year ago

JakeWharton commented 1 year ago

Experiencing this problem while upgrading to Kotlin 1.9.0 and Compose compiler 1.5.0.

Here is a minimally-reproducing sample: compose-js-duplicate-signatures.zip. Running ./gradlew build will exhibit the failure.

First, in the build I see this warning:

> Task :redwood-widget-compose:compileTestDevelopmentExecutableKotlinJs
w: <redwood:redwood-widget-compose> @ /Volumes/dev/cashapp/compose-js-duplicate-signatures/redwood-widget-compose/src/commonMain/kotlin/app/cash/redwood/widget/compose/ComposeWidgetChildren.kt:22:1: Abstract function 'insert' is not implemented in non-abstract class 'ComposeWidgetChildren'

Compilation of this type and subsequent tests for this type work on every other target (JVM and a bunch of native). Only JS exhibits this problem. (Note: I have disabled all targets except JS in the sample above, but they exist in the real project)

The insert function is defined in the Widget.Children interface in the 'redwood-widget' module.

public interface Widget<W : Any> {
  public val value: W

  public interface Children<W : Any> {
    public fun insert(index: Int, widget: Widget<W>)
  }
}

The ComposeWidgetChildren implementation in the 'redwood-widget-compose' module does override it with a generic type parameter that is a composable lambda.

public class ComposeWidgetChildren : Widget.Children<@Composable () -> Unit> {
  private val _widgets = mutableStateListOf<Widget<@Composable () -> Unit>>()

  @Composable
  public fun render() {
    for (index in _widgets.indices) {
      _widgets[index].value()
    }
  }

  override fun insert(index: Int, widget: Widget<@Composable () -> Unit>) {
    _widgets.add(index, widget)
  }
}

The JS tests for the 'redwood-widget-compose' module will fail with

IrLinkageError: Abstract function 'insert' is not implemented in non-abstract class 'ComposeWidgetChildren'
    at <global>.throwLinkageError(/Volumes/dev/cashapp/redwood-widget-compose/build/compileSync/js/test/testDevelopmentExecutable/kotlin/runtime/unlinked.kt:11)
    at protoOf.insert_n09y8r(/Volumes/dev/cashapp/compose-js-duplicate-signatures/redwood-widget-compose/src/commonMain/kotlin/app/cash/redwood/widget/compose/ComposeWidgetChildren.kt:20)
    at protoOf.insert_4qfcy3(redwood-redwood-widget-compose-test.2063309350.js:65529)
    at <global>.insert(/Volumes/dev/cashapp/compose-js-duplicate-signatures/redwood-widget-testing/src/commonMain/kotlin/app/cash/redwood/widget/testing/AbstractWidgetChildrenTest.kt:35)
    at protoOf.insertAppend_4conq6(/Volumes/dev/cashapp/compose-js-duplicate-signatures/redwood-widget-testing/src/commonMain/kotlin/app/cash/redwood/widget/testing/AbstractWidgetChildrenTest.kt:28)
    at <global>.test_fun$ComposeWidgetChildrenTest_test_fun$insertAppend_test_fun_i9ukp5(redwood-redwood-widget-compose-test.2063309350.js:65402)
    at Context.<anonymous>(/Volumes/dev/cashapp/compose-js-duplicate-signatures/build/js/packages/src/KotlinTestTeamCityConsoleAdapter.ts:70)

Peeking at the redwood-redwood-widget-compose.js file, we see this:

  protoOf(ComposeWidgetChildren).insert$composable_k4eb7o_k$ = function (index, widget) {
    this._widgets_1.add_exwzt0_k$(index, widget);
  };
  protoOf(ComposeWidgetChildren).insert_4qfcy3_k$ = function (index, widget) {
    return this.insert$composable_k4eb7o_k$(index, widget);
  };
  protoOf(ComposeWidgetChildren).insert_n09y8r_k$ = function (index, widget) {
    throwLinkageError("Abstract function 'insert' is not implemented in non-abstract class 'ComposeWidgetChildren'");
  };
  protoOf(ComposeWidgetChildren).insert_4qfcy3_k$ = function (index, widget) {
    return this.insert_n09y8r_k$(index, widget);
  };

This is the real implementation whose signature has been transformed with a $composable name part (despite not actually being a composable function), then presumably the insert_4q function is the override. But then we get this insert_n0 function which is some kind of abstract placeholder, and then another insert_4q which invokes that placeholder (and thus overrides the first insert_4q override).

If you disable the Compose compiler in this module the latter two functions disappear. The test obviously still fails at runtime, but with an IllegalStateException about the Compose compiler not being run after the insert function was successfully invoked.

When using Kotlin 1.8.22 and JB Compose compiler 1.4.8 only the first two JS functions are generated and the compilation and test both succeed.

dima-avdeev-jb commented 1 year ago

Thanks!

eymar commented 1 year ago

Hi @JakeWharton! I started to look into this issue and I'm a bit surprised with what I see now.

First I wanted it to fail at compile time instead of at runtime, so I added:

allprojects {

  afterEvaluate {

    tasks.withType(KotlinJsCompile.class) {
      kotlinOptions.freeCompilerArgs += [
              "-Xpartial-linkage=disable"
      ]
    }
  }
}

But it didn't fail at compile time, and it made it work at runtime too. I'm wondering if adding this flag would make all of your tests pass with k/js. In the meantime I'll ask kotlin team if it's expected - it doesn't seem to be right.

JakeWharton commented 1 year ago

Oh, interesting. I will try later today/tonight on the real project.

edit: It worked!

okushnikov commented 2 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.