Caleb B
01/02/2026, 1:30 AMthis being copied from LV 0 to LV 6? Is it supposed to do that?Youssef Shoaib [MOD]
01/02/2026, 2:10 AMinline functions work. All of these "inefficiencies" are easily optimized by ProGuard and/or the JITCaleb B
01/02/2026, 5:14 PMThis makes debugging throughAhhhh, that makes sense. Glad to know it's not as bad as I thought.functions work.inline
Caleb B
01/02/2026, 5:16 PMYoussef Shoaib [MOD]
01/02/2026, 5:19 PMCaleb B
01/02/2026, 5:19 PMCaleb B
01/02/2026, 5:23 PMINVOKEVIRTUAL a safe opcode to use for instance methods in general, even ones that aren't actually virtual? I just want to double-checkYoussef Shoaib [MOD]
01/02/2026, 5:35 PMCaleb B
01/02/2026, 5:36 PMCaleb B
01/02/2026, 5:36 PMCaleb B
01/02/2026, 5:36 PMCaleb B
01/02/2026, 5:37 PMCaleb B
01/02/2026, 5:37 PMCaleb B
01/02/2026, 5:37 PMCaleb B
01/02/2026, 5:37 PMYoussef Shoaib [MOD]
01/02/2026, 5:38 PMCaleb B
01/02/2026, 5:38 PMCaleb B
01/02/2026, 5:38 PMCaleb B
01/02/2026, 5:38 PMCaleb B
01/02/2026, 5:39 PMJava is going with typeclasses (through witnesses)They're doing product types? I never heard about that
Caleb B
01/02/2026, 5:39 PMYoussef Shoaib [MOD]
01/02/2026, 5:40 PMPair to me), but they're basically doing something like Haskell's typeclass lookup system and Rust's trait impl lookup system.Caleb B
01/02/2026, 5:41 PMCaleb B
01/02/2026, 5:42 PMYoussef Shoaib [MOD]
01/02/2026, 5:47 PMconst. Such a limitation doesn't need to exist in Kotlin, though, but it can be nice to limit the behaviour of the lookup system and make it predictable (e.g. no side effects executed by the lookup).Youssef Shoaib [MOD]
01/02/2026, 5:51 PMCaleb B
01/02/2026, 5:51 PMobj.into<T>() on an object and it would be substituted at compile-time with ThatTraitImpl(obj).Caleb B
01/02/2026, 5:51 PMCaleb B
01/02/2026, 5:52 PMCaleb B
01/02/2026, 5:56 PMrunValueGettingAlgorithm(&OutputWithBehaviorDefiningWhichValueToGet) where you have the same procedure run to get some arbitrary value no matter which one it is, and then a different output type depending on which value(s) you actually want to extract from it bundled with HOW you get those values, all resolved at compile-timeYoussef Shoaib [MOD]
01/02/2026, 5:57 PMinterface Field<T> {
operator fun plus(other: T): T
operator fun times(other: T); T
operator fun unaryMinus(): T
fun multInverse(): T
}
But where's the 0 and 1? The context style is nicer and clearer IMO:
interface Field<T> {
operator fun T.plus(other: T): T
operator fun T.times(other: T); T
operator fun T.unaryMinus(): T
fun T.multInverse(): T
val zero: T
val one: T
}
In some sense, this is like C++ conceptsCaleb B
01/02/2026, 5:59 PMCaleb B
01/02/2026, 6:00 PMCaleb B
01/02/2026, 6:00 PMYoussef Shoaib [MOD]
01/02/2026, 6:01 PMCaleb B
01/02/2026, 6:02 PMobj..Caleb B
01/02/2026, 6:03 PMCaleb B
01/02/2026, 6:04 PMobj..
The alternative being that you only see those methods when you say "this object is being viewed as x"Caleb B
01/02/2026, 6:06 PMYoussef Shoaib [MOD]
01/02/2026, 6:06 PMcontext(_: Foo) fun foo()
// somewhere without Foo in scope
fo// intellisense completes foo, but then complains because it isn't in scope
I think that's technically a feature, not a bug, with the intention being API discovery.
I agree though that, depending on your API, you don't want all these methods to show up. I think it should be easily fixable on the IDE side though. Simply introduce some @GetOutOfMyWayAutoComplete annotation that you can annotate foo with so that it only shows up if all its contexts are in scopeYoussef Shoaib [MOD]
01/02/2026, 6:10 PMrestricted type ComposableAnnotation = FirAnnotation
fun ComposableAnnotation.foo() {}
val FirAnnotation.isComposable get() {
contract { returns(true) implies this@isComposable is ComposableAnnotation }
return ...
}
// you can only call `foo` if you check `isComposable`
if (ann.isComposable) { ann.foo() } // smart castedCaleb B
01/02/2026, 6:10 PMCaleb B
01/02/2026, 6:11 PMCaleb B
01/02/2026, 6:13 PMCaleb B
01/02/2026, 6:13 PMCaleb B
01/02/2026, 6:16 PMCaleb B
01/02/2026, 6:18 PMCaleb B
01/02/2026, 6:19 PMCaleb B
01/02/2026, 6:20 PMCaleb B
01/02/2026, 6:26 PMYoussef Shoaib [MOD]
01/02/2026, 6:26 PMinterface OutputsTrait<T> {
fun T.zeroOut()
operator fun T.plusAssign(pos: RelativePosition)
}
// assume I have the bridge methods here somewhere
context(toGet: DataToGetWhenRecursing, _: OutputsTrait<O>)
fun <Out> getOffsetsRelativeToCommonParent(pos1: RelativePosition, pos2: RelativePosition, out1: Out, out2: Out): ErrorCode {
out1.zeroOut()
out2.zeroOut()
val err = if (pos1.nestIndex == pos2.nestIndex) {
if (pos1.parent == pos2.parent) {
out1 += pos1
out2 += pos2
OK
} else getOffsetsRelativeToCommonParentBoth(pos1, pos2, out1, out2)
} else if (Pos1.NestIndex > Pos2.NestIndex)
getOffsetsRelativeToCommonParentHigher(pos2, pos1, out2, out1)
else getOffsetsRelativeToCommonParentHigher(pos1, pos2, out1, out2)
if (err == OK)
return OK;
// on error
out1.zeroOut()
out2.zeroOut()
return err;
}
// impl
object RecursingHandleOutputsTrait: OutputsTrait<Pair<FVector, FQuat>> {
override fun Pair<FVector, FQuat>.zeroOut() {
first.set(FVector.zero)
second.set(FQuat.identity)
}
override fun Pair<FVector, FQuat>.plusAssign(pos: RelativePosition) {
first += pos.relativePos
second *= pos.orientation
}
}
As you can see, OutputTrait works really nicely as a contextCaleb B
01/02/2026, 6:27 PMCaleb B
01/02/2026, 6:27 PMCaleb B
01/02/2026, 6:28 PMYoussef Shoaib [MOD]
01/02/2026, 6:28 PMDataToGetWhenRecursing, since it's only used, well, when recursing, but it'd work nicely as well, and is passed implicitly to the auxillary methodsCaleb B
01/02/2026, 6:29 PMDataToGetWhenRecursing is just an enum so the right OutputTrait specialization is selectedCaleb B
01/02/2026, 6:30 PMOutputTrait.Both, OutputTrait.Pos, OutputTrait.Orient), that would do the same thingYoussef Shoaib [MOD]
01/02/2026, 6:31 PMcontext(toGet: OutputsTrait<O>)
fun <Out> getOffsetsRelativeToCommonParent(pos1: RelativePosition, pos2: RelativePosition, out1: Out, out2: Out): ErrorCode {
...
}
object DataToGetWhenRecursing {
object Both: OutputsTrait<Pair<FVector, FQuat>> { ... }
object Pos: OutputsTrait<FVector> { ... }
object Orient: OutputsTrait<FQuat> { ... }
}Caleb B
01/02/2026, 6:32 PMCaleb B
01/02/2026, 6:33 PMCaleb B
01/02/2026, 6:33 PMCaleb B
01/02/2026, 6:34 PMOutputsHandler instanceYoussef Shoaib [MOD]
01/02/2026, 6:34 PMgetOffsetsRelativeToCommonParent(pos1, pos2, someQuat, someOtherQuat)
and it resolving automagically.
> I mean it's still finding the behavior at runtime instead of compile-time, and still requires multiple runtime objects instead of compile-time objects, but yeah it works syntactically
I wouldn't worry too much about that, with how smart the JIT usually is. There's some discussion about getting some constexpr magic that would ensure that, though.
In fact, I'd be very surprisied if marking getOffsetsRelativeToCommonParent as inline didn't magically get rid of any inefficiency. R8 is a dang good optimizer, too, so it might be able to inline this itself, and once inlined, it surely can get rid of the interface entirely, and make it so the methods are statically knownCaleb B
01/02/2026, 6:36 PMYoussef Shoaib [MOD]
01/02/2026, 6:41 PMinline lambdas:
typealias ZeroOut<O> = O.() -> Unit
typealias Add<O> = O.(O) -> Unit
inline fun <Out> getOffsetsRelativeToCommonParent(pos1: RelativePosition, pos2: RelativePosition, out1: Out, out2: Out, zeroOut: ZeroOut<O>, add: Add<O>): ErrorCode {
// any mentions of foo += bar should now use foo.add(bar) instead
...
}
object DataToGetWhenRecursing {
object Both {
fun zeroOut(p: Pair<...>) = ...
fun add(p: Pair<...>, other: Pair<...>) = ...
}
object Pos { ... }
object Orient { ... }
}
// usage
getOffsetsRelativeToCommonParent(pos1, pos2, out1, out2, Both::zeroOut, Both::add)
It's guaranteed to inline at compile time, and statically refer to Both.zeroOut. Any optimizer worth its salt can then get rid of the object Both completelyCaleb B
01/02/2026, 6:45 PMtemplate<class OutputType = Pair<FVector, FQuat>>
struct OutputsTrait {
static void ZeroOut(OutputType& it)
{
it = TPair<FVector, FQuat>(FVector::Zero(), FQuat::Identity);
}
}
template<> struct OutputsTrait<FVector> {
static void ZeroOut(OutputType& it)
{
it = FVector::Zero();
}
}
template<> struct OutputsTrait<FQuat> {
static void ZeroOut(OutputType& it)
{
it = FQuat::Identity;
}
}
template<class _OutType, class _OutputsTrait = OutputsTrait<_OutType>>
void getOffsetsEtc(..., _OutType& Out1, _OutType& Out2) {
_OutputsTrait::ZeroOut(Out1);
}
And then it could be called just like this:
getOffsetsEtc<FVector>(..., FVector(), FVector())
Kotlin would let the known generic be elided, so you'd get the "automagic" getOffsetsEtc(FVector(), FVector())Youssef Shoaib [MOD]
01/02/2026, 6:48 PMCaleb B
01/02/2026, 6:48 PMIf you must ensure no allocations, though, then simply useThe issue with making the whole function inline is that it's a massive function being called in a lot of places. The code bloat would be enormous.lambdas:inline
Caleb B
01/02/2026, 6:49 PMYoussef Shoaib [MOD]
01/02/2026, 6:50 PMThe issue with making the whole function inline is that it's a massive function being called in a lot of places. The code bloat would be enormous.Isn't that what `template`s do? Or are they smart enough to only produce one copy per unique arguments?
Caleb B
01/02/2026, 6:51 PMCaleb B
01/02/2026, 6:51 PMYoussef Shoaib [MOD]
01/02/2026, 6:54 PMinline
> being able to switch on a reified type parameter
this kinda works, but it's not fully at compile time (although sometimes the compiler does optimize this IIRC):
inline fun <reified T> foo() {
if (5 is T) { ... }
else if ("" is T) { ... }
else if (FQuat.zero is T) { ... }
}Caleb B
01/02/2026, 6:55 PMCaleb B
01/02/2026, 6:57 PM// ASM opcodes
interface Opcodes { ... }
template<int OPCODE>
inline fun Int.invoke()
template<OPCODE == ALOAD>
inline fun OPCODE.invoke(variableIndex: Int) {
+VarInsnNode(ALOAD, variableIndex)
}Youssef Shoaib [MOD]
01/02/2026, 6:57 PMwhen {
someAnnotationCall is T -> annotationCallCheckers
someAnnotation is T -> annotationCheckers
...
}
I remember decompiling, and seeing a lot of these branches disappear since the compiler knew they must be false. Even if the compiler doesn't do it, they're statically-false conditions, so optimizer/JIT will get rid of all the branches but the one useful oneCaleb B
01/02/2026, 6:59 PMfun makeMethodInsns() = InsnList().apply {
+NEW("my/class/MyClass")
+DUP
+ALOAD(1)
+INVOKESPECIAL("my/class/MyClass", "<init>", "()V")
}Youssef Shoaib [MOD]
01/02/2026, 7:00 PMinvoke in the enum class.
I agree, the ASM API is disgusting lolCaleb B
01/02/2026, 7:01 PMYou'd need some example values of each, then you can do:I was switching on a reified generic type param, which means only the compiler knows it- oh I see what you're doing. That's a hacky as hell workaround, that's so weird
Caleb B
01/02/2026, 7:02 PMwhen (T) {
FirAnnotation -> ...
FirFunction -> ...
}Caleb B
01/02/2026, 7:02 PMCaleb B
01/02/2026, 7:02 PMCaleb B
01/02/2026, 7:03 PMIntrinsicConstEvaluation system? I could do so much with that.Youssef Shoaib [MOD]
01/02/2026, 7:08 PMThat's a hacky as hell workaround, that's so weirdit gets weirder, you can detect whether a reified generic is nullable or not:
if (null is T) // do something that requires a nullable T
I wish I could just do this:Contexts work nicely too, but you just need a lookup mechanism so that you don't suffer:
interface AddChecker<T: FirExpression> { fun addChecker(checker: FirExpressionChecker<T>) }
object AnnotationCallAddChecker: AddChecker<FirAnnotationCall> { fun addChecker(...) = ... }
...
addChecker(someAnnotationCallChecker) // should magically bring in AnnotationCallAddChecker as a context
In fact, if you know there's some set of contexts you want, you can bring them all in one fell swoop:
inline fun withAddCheckerContexts(block: context(AnnotationCallAddChecker, AnnotationAddChecker, ...) () -> Unit) = block(AnnotationCallAddChecker, AnnotationAddChecker, ...)
// usage
withAddCheckerContexts {
addChecker(someAnnotationCallChecker)
addChecker(someAnnotationChecker)
}
contexts are smart enough (I think, at least) to choose the right context based on the typeYoussef Shoaib [MOD]
01/02/2026, 7:09 PMis there a way to hijack K2'sI'm not sure, but it depends on what you want to do with it. If it's just about the efficiency of the produced code, you can always use an IR transformer and replace any code you want. FIR has more limited optionssystem? I could do so much with that.IntrinsicConstEvaluation
Caleb B
01/02/2026, 7:15 PMtemplate<> fun foo(intParam: Int) {
if constexpr (intParam == 4) {
println("Param is 4")
} else {
println("Param is something else")
}
}
foo(3)
foo(4)
// compiles to:
fun foo-4(intParam: Int) {
println("Param is 4")
}
fun foo(intParam: Int) {
println("Param is something else")
}
foo(3)
foo-4(4)
It becomes insanely powerful when you combine it with type-traits (assertions about a TYPE, like "is integral", "has a method named foo", etc.). Since it's just a source code replacer, you can do anything from structural typing to hotswapping entire APIs without any runtime penalty.Youssef Shoaib [MOD]
01/02/2026, 7:16 PMfoo(someInt) when the value is not known at compile time?Caleb B
01/02/2026, 7:17 PMCaleb B
01/02/2026, 7:18 PMCaleb B
01/02/2026, 7:18 PMCaleb B
01/02/2026, 7:18 PMCaleb B
01/02/2026, 7:19 PMCaleb B
01/02/2026, 7:20 PMfn makesArray<const SIZE: i32>() {
let arr = foo[SIZE];
}Youssef Shoaib [MOD]
01/02/2026, 7:21 PMCaleb B
01/02/2026, 7:23 PMequals method is sometimes this:
operator fun <T> equals(other: T) {
if constexpr(T != MyType) {
return false;
} else {
// actually perform the checks
}
}
And then you end up with one equals function that always returns false and another that actually has a body. Then the one that always returns false would likely get inlined by the compilerCaleb B
01/02/2026, 7:24 PMCaleb B
01/02/2026, 7:24 PMYoussef Shoaib [MOD]
01/02/2026, 7:29 PMAny.equals, then you can get close using contexts:
interface Eq<T> { infix fun T.eq(other: T): Boolean }
context(_: Eq<T>)
inline fun <reified T, reified R> T.eq(other: R): Boolean = this is R && other is T && this eq other
although that does runtime stuff if R is some supertype of T.
Or instead, purely at compile-time:
interface Eq<T> { infix fun T.eq(other: T): Boolean }
context(_: Eq<T>)
fun <T> T.eq(other: Any?): Boolean = false
context(eq: Eq<T>)
fun <T> T.eq(other: T): Boolean = with(eq) { this@eq eq other } // bridge methodCaleb B
01/02/2026, 7:46 PMYoussef Shoaib [MOD]
01/02/2026, 7:51 PMcontext(). There's no real "conversion" going on.
For trait objects, my understanding, again, is that they pack the trait implementation together with the object that it needs to operate on. I had an example of that in my post (which is probably 20 or so messages ago lol)
In fact, Rust traits seemingly don't need to take a self parameter for each method, so they're even closer to contexts than they are to conversions on a typeCaleb B
01/02/2026, 7:55 PMIn fact, Rust traits seemingly don't need to take aIt's for Rust's version of type-traits (properties of a type), in lieu of a runtime reflection system.parameter for each methodself
Caleb B
01/02/2026, 7:56 PMCaleb B
01/02/2026, 7:57 PMYoussef Shoaib [MOD]
01/02/2026, 7:57 PMCaleb B
01/02/2026, 8:10 PM// Contains the aliases for types that mean "T&"
template<typename T>
struct TReferenceType {
using Ref = T&
using ConstRef = const T&
}
// Specialization for types that are already a reference
template<typename U>
struct TReferenceType<U&> {
using Ref = T // U&
using ConstRef = const T // const U&
}
// Specialization for types that are already a CONST reference
template<typename U>
struct TReferenceType<const U&> {
using Ref = TRemoveConst<T>::Value
using ConstRef = T
}
// usage:
template<typename T>
fun acceptsReferenceOnly(param: TReferenceType<T>::Ref)
// Assertion of the properties of a type
template<typename T>
struct TIsIntegral {
enum {
Value = false;
}
}
// Specialization for `int` saying that the "int type" is "integral"
template<>
struct TIsIntegral<int> {
enum {
Value = true;
}
}
// Usage
template<typename T>
fun differentActionIfParamIsIntegral(mightBeInt: T) {
if constexpr (TIsIntegral<T>::Value == true) {
// act on int
} else {
// act on other type
}
}Caleb B
01/02/2026, 8:12 PMYoussef Shoaib [MOD]
01/02/2026, 8:13 PMCaleb B
01/02/2026, 8:15 PMCaleb B
01/02/2026, 8:17 PMCaleb B
01/02/2026, 8:20 PMCaleb B
01/02/2026, 8:21 PMRPOutputsTrait.PAIR = object : OutputsTrait<Pair<Vec, Quat>> vs ThirdPartyOutputsTrait.PAIR = object : OutputsTrait<Pair<Vec, Quat>>Caleb B
01/02/2026, 8:22 PMstd::hash<T> that defines the hashing method for a type T. Since it's just looked up as "find the implementation of `std::hashThatType`"Caleb B
01/02/2026, 8:23 PMCaleb B
01/02/2026, 8:25 PMYoussef Shoaib [MOD]
01/02/2026, 9:01 PMfun <T: Field> foo() {}
// Desugars to
context(_: Field<T>) fun <T> foo() {}
In fact, Scala has this!Caleb B
01/02/2026, 9:49 PM