Mandatory method mangling when compiling to JS

@ilya.gorbunov Thanks for following up.

I understand the annotation proposition.

However, doesn’t the Jetbrains team feel like we shouldn’t have to add an annotation on every method that we intend to consume from JavaScript? Why not allow methods to not be mangled?

1 Like

The reason for mangling is the ability in Kotlin to overload function by parameter types. E.g. it’s possible to declare foo(x: Int) and foo(x: String) in the same scope, and these are different functions in terms of Kotlin. On a call site of foo compiler checks types and determines which of the functions called (or reports that none of the functions can be called with given parameters). It’s possible in JVM, since in JVM bytecode most of the calls to are encoded with invokevirtual or invokestatic opcodes, which require to specify both name and signature. However, there’s no such thing to JavaScript, so compiler has to invent the way to pass signature. The common technique is to calculate some unique suffix for each set of parameters.

Note that it’s impossible to declare large unmangled function that analyses parameter on run time and calls particular implementation, since run time loses some important information available to compiler. For example:

fun main(args: Array<String>) {
    foo("bar")
    foo("baz" as Any)
}

fun foo(x: String) {
    println("string");
}

fun foo(x: Any) {
    println("any");
}

is impossible to express via runtime resolution. Also, runtime resolution would be very slow, especially in some cases with generics.

Kotlin is not the only language which uses mangling. C++ generates mangled symbols in object files, so it’s impossible to call these functions from pure C. Moreoever, there’s no single standard mangling scheme for C++, so each compiler uses its own mangling algorithm. There’s extern "C" directive in C++ syntax that has effect more or less similar to JsName annotation in Kotlin.

1 Like

Hi @Alexey.Andreev
Thanks for the detailed answer.

I fully understand why this is needed in JavaScript when facing multiple methods with only the parameters differing between the two.

However, unless I’ve misconfigured something, it appears that the Kotlin to JavaScript compiler always mangles the function names, whether there is a function name conflict or not.

My suggestion would be to only use mangled names when necessary. When facing such a conflict, using @JsName would make more sense than the current situation where you must always provide the annotation if you want to avoid mangled names, whether there’s a conflict or not.

Let me know what you think!

1 Like

Ok, you create a library foo.1.0.0 which provides fun foo(x: Int) without mangling. User takes this library and calls foo(23) in his application. The generated JS would be straightforward: foo(23). Then you update you library to v1.1.0, which adds fun foo(x: String). Now the compiler has to mangle both functions to resolve the conflict. When the user updates library to the new version, he has to recompile his application to get mangled name on existing call site. You can say: oh, let he recompiles the library and get proper call site. However, that’s usually an issue with transitive dependencies. You may invent 9000 ways to solve problems and I will name 9000 reasons why it won’t work. IMO, the only viable solution here is to invent another annotation that can be applied to some larger scope (i.e. class or file) and that turns off mangling in this entire scope.

You may want to get semless interoperation between Kotlin and JS similar to the experience on JVM between Java and Kotlin (or experience similar to TypeScript and JS). However, looks like it’s impossible. Kotlin was designed primarily as a JVM language and borows many features of JVM languages (like method overloading, lazy class initialization and so forth). TypeScript was designed as JavaScript with type system and borrows JavaScript features (in most cases translation of TS to modern ES is just removal of type annotations). It’s possible to do some relatively good interop between Kotlin and JavaScript or between TypeScript and JVM (if someone manages to implement TypeScript → JVM bytecode translator), but it would not be 100% seamless.

4 Likes

@Alexey.Andreev those are all great points.

How use case right now is to build a validation library that would be used by

  • Our front end JavaScript (compiled from TypeScript) that runs in the browser
  • Our backend JavaScript services (compiled from TypeScript) that runs on Node.js
  • Our backend JVM services (compiled from Java and Kotlin)

We have a few simple interfaces and a bunch of objects we’d like to share.
Your proposed annotation that would apply to a whole class (perhaps a whole package or module?) would work for us.

In the meantime, we created a custom @Name annotation that maps to @JsName in our Kotlin to JavaScript project. We will use this annotation on every public method of our interface.

Hopefully this is useful feedback for your team. I realize that it is a complex issue! From my perspective, a solution that solves some limited use cases would still be better than the current solution of having mangling unless you add @JsName.

Thanks for your time!

What you can probably get away with (due to the fact that in Javascript a type is merely a convention) is to define your interface as an external interface that you then implement in your class. You are however in that case playing tricks with the typesystem. JsName doesn’t do that, but is more tedious.

So as @Alexey.Andreev stated, we will not get away without mangling on the long run (as soon as overloads come into play, it is not going to work anymore otherwise) but honestly, the mangling algorithm used makes the generated code hard to read/call properly if you plan to use the library from plain js.

So my proposal would be to provide a possibility for the user to easily hook into the mangling process and specify his own mangling algorithm function to come out with reproducible, consistent and (for him) easily usable function names.

Another option is to restrict what you can export to JS, e.g.only export classes which can make do without mangling. What do you think. Would that cover your use cases?

cc @Svyatoslav.Kuzmich

I also want to avoid JsName.

My suggestion: class annotation

// (1)
@DisableOverload
class A{
	fun a(a:Int){}    // without mangling, = JsName("a")
	fun a(a:String){} // compile error, conflict "a"
}

// (2)
@GenerateOverloadRuntimeDispatch
class A(){
	fun foo(a:Int) { println("foo(Int)") }
	fun foo(a:String) { println("foo(String)") }
	
	// <auto-generated>
	@JsName("foo")
	fun fooRuntimeDispatch(a:Any?) {
		when(a){
			is Int -> foo(a)
			is String -> foo(a)
			else -> throw IllegalArgumentException()
		}
	}
	// </auto-generated>
}

for your information: overload dispatch in Groovy
https://groovy-lang.org/differences.html#_multi_methods

Could be workable. I think the first needs to be only on classes, not interfaces, or you could still end up with Oreos overloads when you implement multiple interfaces. Not sure if generic type arguments cause problems. And I wonder about how extension methods appear in JS - haven’t checked.

The latter maybe works only if the overloads are all disjoint classes (given Alexey Andreev’s comment Mandatory method mangling when compiling to JS - #2 by Alexey.Andreev), not inheriting classes or interfaces - which is a bit of an odd restriction for Kotlin in general. But maybe this one could be implemented in a third-party annotation processor if it’s not general enough.

Thanks for your reply.
I will write my thoughts in a little more detail.

Regarding (1), It can also be added to the interface, and DisableOverload is expanded for each method.
An error occurs when conflicting with extends or implements.

@DisableOverload interface IA{ fun foo(a:Int) }
// (compiler)-> interface IA{ @DisableOverload fun foo(a:Int) }

interface IB{ fun foo(a:String) }
interface IC :
	IA,
	IB   // error: foo(Int) is DisableOverload, conflict foo(String)
{}

Regarding (2),
Other Kotlin code calls overload directly (foo with auto mangling).
In JS, call “foo”(fooRuntimeDispatch).

mangling is inherited

interface IA {
	@JsName("foo") fun foo(a:Int)
}
class CA : IA {
	override fun foo(a:Int){ println ("foo(Int)") }
	fun foo(a:Any){ println ("foo(Any)") }
}
// JavaScript
new output.test.CA().foo(""); // -> "foo(Int)"

Use the same priority as the compiler

fun foo(a:Iterable<Int>){ println ("foo(Iterable)") }
fun foo(a:List<Int>){ println ("foo(List)") }

//info https://docs.oracle.com/javase/specs/jls/se13/html/jls-15.html#jls-15.12.2
@JsName("fooCompileTime")
fun fooCompileTime(){
	val v:ArrayList<Int> = arrayListOf()
	foo(v) // -> "foo(List)"
}
@JsName("fooRuntime")
fun fooRuntime(){
	val v:Any = arrayListOf<Int>()
	when(v){
		is List<*> -> foo(v as List<Int>) // most specific
		is Iterable<*> -> foo(v as Iterable<Int>)
	}
}

I avoid the following because it is not compilable for the JVM.

class A{
	fun foo(a:List<String>){ }
	fun foo(a:List<Int>){ } // conflict JVM signature
}

If it is “resolution ambiguity” at compile time, it will be the same at runtime.
Or use the first adaptation.

fun foo(a:Appendable){ }
fun foo(a:CharSequence){ }
fun fooAmbiguity(){
	foo(StringBuilder()) // resolution ambiguity
}

While I am aware that dynamic dispatch overloading is common in JavaScript framework this is an approach that is actually quite brittle when possible values can actually overlap. Indeed, for jquery it even overloads based upon string values.