Repository: yandex/yatagan (kotlin)

Issue:
Thread assertion called inconsistently between RT and compiled backends.
RT calls thread assertion in its single-thread caching provider _unconditionally_, while generated code would make the call _only upon actual construction_ - if value is not yet initialized.

Need to fix the inconsistency one way or the other.

Pull request:
RT behaved inconsistently with generated code,
 and there was one more inconsistency with the latter itself.

Cover the fix with a test.

Fixes #12

Patch:
diff --git a/codegen/impl/src/main/kotlin/ScopedProviderGenerator.kt b/codegen/impl/src/main/kotlin/ScopedProviderGenerator.kt
index 9f52756c..4f5d2683 100644
--- a/codegen/impl/src/main/kotlin/ScopedProviderGenerator.kt
+++ b/codegen/impl/src/main/kotlin/ScopedProviderGenerator.kt
@@ -55,9 +55,6 @@ internal class ScopedProviderGenerator(
                     modifiers(PUBLIC)
                     annotation<Override>()
                     returnType(ClassName.OBJECT)
-                    if (!useDoubleChecking) {
-                        +"%T.assertThreadAccess()".formatCode(Names.ThreadAssertions)
-                    }
                     +"%T local = mValue".formatCode(ClassName.OBJECT)
                     controlFlow("if (local == null)") {
                         if (useDoubleChecking) {
@@ -69,6 +66,7 @@ internal class ScopedProviderGenerator(
                                 }
                             }
                         } else {
+                            +"%T.assertThreadAccess()".formatCode(Names.ThreadAssertions)
                             +"local = mDelegate.%N(mIndex)".formatCode(SlotSwitchingGenerator.FactoryMethodName)
                             +"mValue = local"
                         }
diff --git a/rt/engine/src/main/kotlin/accessStrategies.kt b/rt/engine/src/main/kotlin/accessStrategies.kt
index ac336ed4..deb02856 100644
--- a/rt/engine/src/main/kotlin/accessStrategies.kt
+++ b/rt/engine/src/main/kotlin/accessStrategies.kt
@@ -55,8 +55,8 @@ internal class CachingAccessStrategy(
     private var value: Any? = null
 
     override fun get(): Any {
-        ThreadAssertions.assertThreadAccess()
         value?.let { local -> return local }
+        ThreadAssertions.assertThreadAccess()
         return binding.accept(creationVisitor).also { value = it }
     }
     override fun getLazy(): Lazy<*> = this


Test patch:
diff --git a/testing/tests/src/test/kotlin/RuntimeTest.kt b/testing/tests/src/test/kotlin/RuntimeTest.kt
index ccb8ad49..22ebb8df 100644
--- a/testing/tests/src/test/kotlin/RuntimeTest.kt
+++ b/testing/tests/src/test/kotlin/RuntimeTest.kt
@@ -184,16 +184,20 @@ class RuntimeTest(
     @Test
     fun `thread assertions`() {
         givenKotlinSource("test.TestCase", """
-            import com.yandex.yatagan.*
+            import com.yandex.yatagan.*import java.lang.AssertionError
             import java.util.concurrent.*
             import javax.inject.*
             
             @Singleton class MyClassA @Inject constructor()
             @Singleton class MyClassB @Inject constructor()
+            @Singleton class MyClassC @Inject constructor()
+            /*un-scoped*/ class MyClassD @Inject constructor()
             
             @Component @Singleton interface MySTComponent {
                 fun getA(): Lazy<MyClassA>
                 fun getB(): MyClassB
+                fun getC(): MyClassC
+                fun getD(): Lazy<MyClassD>
             }
 
             fun test() {
@@ -205,18 +209,31 @@ class RuntimeTest(
                     }
                 })
                 try {
+                    c.getC()  // Create ClassC ahead of time
+                    val d1 = c.getD()
+                    d1.get()  // Create ClassD inside d1 holder.
+                    val d2 = c.getD()
+
                     val executor = Executors.newFixedThreadPool(2)
                     val t1 = executor.submit { 
                         try {
                             c.getA().get()
                             throw IllegalStateException("Test failed")
                         } catch (_: AssertionError) {}
+                        c.getC()  // Shouldn't throw as ClassC is already 
+                        d1.get()  // Shouldn't throw as object in d1 is pre-created
+                        try {
+                           d2.get()
+                           throw IllegalStateException("Test failed")
+                        } catch (_: AssertionError) {}
                     }
                     val t2 = executor.submit {
                         try {
                             c.getB()
                             throw IllegalStateException("Test failed")
                         } catch (_: AssertionError) {}
+                        c.getC() // Shouldn't throw as ClassC is already created
+                        d1.get()  // Shouldn't throw as object in d1 is pre-created
                     }
                     t1.get()
                     t2.get()