From 48a3dbc88b2c09987c8174b9174a2590e7054849 Mon Sep 17 00:00:00 2001 From: Rubin Yoo Date: Fri, 17 Jul 2026 23:40:55 -0700 Subject: [PATCH] Add configurable caching strategies for Motif scopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CachingStrategy enum (NONE, BASELINE, BASELINE_WITH_LOCK_SELECTABLE) - Add DoNotCache annotation and MotifRuntimeConfig for runtime config - Add MotifLock for per-dependency locking support - Generate caching code based on strategy in Java/Kotlin code generators - Fix CI: upgrade Java 11 → 17 (required by AGP 7.4.2) - Fix google-java-format 1.8 → 1.17.0 (required by Java 17) - Fix AndroidManifest package attribute conflicts with namespace - Fix dagger-comparison R import to match namespace Co-Authored-By: Claude --- .github/workflows/ci.yml | 4 +- .gitignore | 2 + .../motif/compiler/JavaCodeGenerator.kt | 249 ++++++++-- .../motif/compiler/KotlinCodeGenerator.kt | 282 +++++++++-- .../main/kotlin/motif/compiler/ScopeImpl.kt | 35 +- .../kotlin/motif/compiler/ScopeImplFactory.kt | 444 +++++++++++++++++- .../motif/compiler/BaselineCodegenTest.kt | 159 +++++++ .../JAVA/ScopeImpl.java.txt | 62 +++ .../KOTLIN/ScopeImpl.kt.txt | 56 +++ .../JAVA/ScopeImpl.java.txt | 73 +++ .../KOTLIN/ScopeImpl.kt.txt | 69 +++ .../JAVA/ScopeImpl.java.txt | 65 +++ .../KOTLIN/ScopeImpl.kt.txt | 57 +++ .../T017_spread/JAVA/ScopeImpl.java.txt | 67 +++ .../T017_spread/KOTLIN/ScopeImpl.kt.txt | 50 ++ .../JAVA/ScopeImpl.java.txt | 53 +++ .../KOTLIN/ScopeImpl.kt.txt | 43 ++ .../T024_child/JAVA/ChildImpl.java.txt | 53 +++ .../T024_child/JAVA/ScopeImpl.java.txt | 55 +++ .../T024_child/KOTLIN/ChildImpl.kt.txt | 44 ++ .../T024_child/KOTLIN/ScopeImpl.kt.txt | 44 ++ .../JAVA/ScopeImpl.java.txt | 49 ++ .../KOTLIN/ScopeImpl.kt.txt | 41 ++ compiler/src/test/resources/default.pro | 6 +- gradle.properties | 5 +- gradle/dependencies.gradle | 4 +- lib/src/main/java/motif/CachingStrategy.java | 78 +++ lib/src/main/java/motif/DoNotCache.java | 23 +- lib/src/main/java/motif/MotifLock.java | 36 ++ .../main/java/motif/MotifRuntimeConfig.java | 65 +++ lib/src/main/java/motif/Scope.java | 7 +- .../main/kotlin/motif/models/FactoryMethod.kt | 8 +- samples/dagger-comparison/build.gradle | 1 + .../src/main/AndroidManifest.xml | 5 +- .../daggercomparison/dagger/LoggedInView.java | 2 +- .../daggercomparison/dagger/RootView.java | 2 +- .../daggercomparison/motif/LoggedInView.java | 2 +- .../daggercomparison/motif/RootView.java | 2 +- samples/sample-kotlin-ksp/build.gradle | 1 + .../src/main/AndroidManifest.xml | 5 +- .../main/java/motif/sample/MainActivity.kt | 1 + samples/sample-kotlin/build.gradle | 1 + .../src/main/AndroidManifest.xml | 5 +- .../main/java/motif/sample/MainActivity.kt | 1 + .../src/main/java/motif/sample/MainScope.kt | 3 +- .../src/main/java/motif/sample/ViewRouter.kt | 19 + samples/sample-lib/build.gradle | 1 + .../sample-lib/src/main/AndroidManifest.xml | 4 +- .../lib/bottom_header/BottomHeaderScope.java | 3 +- samples/sample/build.gradle | 3 +- samples/sample/src/main/AndroidManifest.xml | 5 +- .../app/bottom_sheet/BottomSheetScope.java | 3 +- .../sample/app/photo_grid/PhotoGridScope.java | 3 +- .../PhotoGridItemController.java | 2 - .../photo_grid_item/PhotoGridItemView.java | 8 +- .../sample/app/photo_list/PhotoListScope.java | 3 +- .../photo_list_item/PhotoListItemView.java | 5 +- .../java/motif/sample/app/root/RootScope.java | 3 +- .../src/main/res/layout/bottomsheet.xml | 2 +- tests/build.gradle | 6 + .../GRAPH.txt | 0 .../Scope.kt | 4 +- .../Test.java | 2 +- .../Test.java | 71 --- .../FastDep.kt | 19 + .../GRAPH.txt | 44 ++ .../Latches.kt | 25 + .../Scope.kt | 52 ++ .../SlowDep.kt | 19 + .../Test.java | 65 +++ .../FastDep.kt | 19 + .../GRAPH.txt | 44 ++ .../Latches.kt | 25 + .../Scope.kt | 52 ++ .../SlowDep.kt | 19 + .../Test.java | 57 +++ .../GRAPH.txt | 0 .../Scope.kt | 39 ++ .../Test.java | 60 +++ .../GRAPH.txt | 9 +- .../Scope.kt | 18 +- .../KT012_runtime_selectable_kotlin/Test.java | 56 +++ .../T077_use_smart_cache_java/GRAPH.txt | 42 ++ .../Scope.java | 4 +- .../Test.java | 2 +- .../Test.java | 65 --- .../FastDep.java | 19 + .../GRAPH.txt | 44 ++ .../Latches.java | 25 + .../Scope.java | 52 ++ .../SlowDep.java | 19 + .../Test.java | 65 +++ .../FastDep.java | 19 + .../GRAPH.txt | 44 ++ .../Latches.java | 25 + .../Scope.java | 52 ++ .../SlowDep1.java | 19 + .../Test.java | 57 +++ .../GRAPH.txt | 80 ++++ .../Scope.java | 77 +++ .../Test.java | 67 +++ .../dependency/DeadCodeDep.java | 18 + .../dependency/DoNotCacheDep.java | 18 + .../dependency/ExposedDep.java | 18 + .../dependency/MultiUseDep.java | 18 + .../dependency/NotExposedDep.java | 18 + .../dependency/PublicAccessorDep.java | 18 + .../dependency/SingleUseDep.java | 18 + .../DoNotCacheWrapper.java | 35 ++ .../GRAPH.txt | 61 +++ .../Scope.java | 63 +++ .../Test.java | 79 ++++ .../dependency/DoNotCacheDep.java | 18 + .../dependency/ExposedDep.java | 18 + .../dependency/NotExposedDep.java | 18 + .../GRAPH.txt | 42 ++ .../Scope.java | 45 ++ .../Test.java | 60 +++ .../T083_runtime_selectable/GRAPH.txt | 37 ++ .../T083_runtime_selectable/Scope.java | 40 ++ .../T083_runtime_selectable/Test.java | 56 +++ .../GRAPH.txt | 6 +- .../Scope.java | 38 ++ .../Test.java | 48 ++ .../dependency/OnlySmartCacheDep.java | 18 + .../Child.java} | 19 +- .../T085_runtime_selectable_child/GRAPH.txt | 50 ++ .../T085_runtime_selectable_child/Scope.java | 39 ++ .../T085_runtime_selectable_child/Test.java | 54 +++ .../GRAPH.txt | 39 ++ .../Scope.java | 40 ++ .../Test.java | 83 ++++ 132 files changed, 4693 insertions(+), 304 deletions(-) create mode 100644 compiler/src/test/java/motif/compiler/BaselineCodegenTest.kt create mode 100644 compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T009_dependency_cache/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T009_dependency_cache/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T017_spread/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T017_spread/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T021_objects_interface/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T021_objects_interface/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ChildImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ChildImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ScopeImpl.kt.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T044_static_objects_method/JAVA/ScopeImpl.java.txt create mode 100644 compiler/src/test/resources/baseline_codegen/T044_static_objects_method/KOTLIN/ScopeImpl.kt.txt create mode 100644 lib/src/main/java/motif/CachingStrategy.java create mode 100644 lib/src/main/java/motif/MotifLock.java create mode 100644 lib/src/main/java/motif/MotifRuntimeConfig.java create mode 100644 samples/sample-kotlin/src/main/java/motif/sample/ViewRouter.kt rename tests/src/main/java/testcases/{KT008_use_null_field_init_kotlin => KT008_use_smart_cache}/GRAPH.txt (100%) rename tests/src/main/java/testcases/{KT008_use_null_field_init_kotlin => KT008_use_smart_cache}/Scope.kt (90%) rename tests/src/main/java/testcases/{KT008_use_null_field_init_kotlin => KT008_use_smart_cache}/Test.java (95%) delete mode 100644 tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.java create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/FastDep.kt create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/GRAPH.txt create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Latches.kt create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Scope.kt create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/SlowDep.kt create mode 100644 tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Test.java create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/FastDep.kt create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/GRAPH.txt create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Latches.kt create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Scope.kt create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/SlowDep.kt create mode 100644 tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Test.java rename tests/src/main/java/testcases/{T077_use_null_field_init_java => KT011_baseline_with_lock_selectable_kotlin}/GRAPH.txt (100%) create mode 100644 tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Scope.kt create mode 100644 tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Test.java rename tests/src/main/java/testcases/{T078_use_null_field_concurrency_java => KT012_runtime_selectable_kotlin}/GRAPH.txt (85%) rename tests/src/main/java/testcases/{KT009_use_null_field_concurrency_kotlin => KT012_runtime_selectable_kotlin}/Scope.kt (69%) create mode 100644 tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Test.java create mode 100644 tests/src/main/java/testcases/T077_use_smart_cache_java/GRAPH.txt rename tests/src/main/java/testcases/{T077_use_null_field_init_java => T077_use_smart_cache_java}/Scope.java (90%) rename tests/src/main/java/testcases/{T077_use_null_field_init_java => T077_use_smart_cache_java}/Test.java (95%) delete mode 100644 tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/FastDep.java create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Latches.java create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Scope.java create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/SlowDep.java create mode 100644 tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Test.java create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/FastDep.java create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Latches.java create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Scope.java create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/SlowDep1.java create mode 100644 tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Test.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/Scope.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/Test.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DeadCodeDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DoNotCacheDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/ExposedDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/MultiUseDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/NotExposedDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/PublicAccessorDep.java create mode 100644 tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/SingleUseDep.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/DoNotCacheWrapper.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Scope.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Test.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/DoNotCacheDep.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/ExposedDep.java create mode 100644 tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/NotExposedDep.java create mode 100644 tests/src/main/java/testcases/T082_baseline_with_lock_selectable/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Scope.java create mode 100644 tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Test.java create mode 100644 tests/src/main/java/testcases/T083_runtime_selectable/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T083_runtime_selectable/Scope.java create mode 100644 tests/src/main/java/testcases/T083_runtime_selectable/Test.java rename tests/src/main/java/testcases/{KT009_use_null_field_concurrency_kotlin => T084_do_not_cache_only_smart_cache_baseline}/GRAPH.txt (91%) create mode 100644 tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Scope.java create mode 100644 tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Test.java create mode 100644 tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/dependency/OnlySmartCacheDep.java rename tests/src/main/java/testcases/{T078_use_null_field_concurrency_java/Scope.java => T085_runtime_selectable_child/Child.java} (68%) create mode 100644 tests/src/main/java/testcases/T085_runtime_selectable_child/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T085_runtime_selectable_child/Scope.java create mode 100644 tests/src/main/java/testcases/T085_runtime_selectable_child/Test.java create mode 100644 tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/GRAPH.txt create mode 100644 tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Scope.java create mode 100644 tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Test.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8326214..a383c925 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java_version: [11] + java_version: [21] steps: - name: Checkout uses: actions/checkout@v3 @@ -31,4 +31,4 @@ jobs: run: ./gradlew check --stacktrace - name: Upload Snapshot run: ./gradlew publish --no-daemon --no-parallel -PmavenCentralUsername="${{ secrets.SonatypeUsername }}" -PmavenCentralPassword="${{ secrets.SonatypePassword }}" - if: success() && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && matrix.java_version == '11' + if: success() && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && matrix.java_version == '21' diff --git a/.gitignore b/.gitignore index 91bd3351..c914027a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build/ .gradle/ local.properties .DS_Store +.claude/ +.kotlin/ diff --git a/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt b/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt index 30c56e05..cf8a56e5 100644 --- a/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt +++ b/compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt @@ -38,27 +38,145 @@ object JavaCodeGenerator { return JavaFile.builder(scopeImpl.className.j.packageName(), typeSpec).build() } - private fun ScopeImpl.spec(): TypeSpec = - TypeSpec.classBuilder(className.j) + // [Caching strategy] Routes to wrapper, variant, or standard spec. + private fun ScopeImpl.spec(): TypeSpec { + if (isRuntimeSelectableWrapper) { + return wrapperSpec() + } + + val classNameToUse = + if (variantSuffix != null) { + com.squareup.javapoet.ClassName.get( + className.j.packageName(), className.j.simpleName() + variantSuffix) + } else { + className.j + } + + return spec(classNameToUse) + } + + // Builds the class TypeSpec for a ScopeImpl. + private fun ScopeImpl.spec(classNameToUse: com.squareup.javapoet.ClassName): TypeSpec = + TypeSpec.classBuilder(classNameToUse) .apply { addAnnotation(scopeImplAnnotation.spec()) addModifiers(Modifier.PUBLIC) addSuperinterface(superClassName.j) objectsField?.let { addField(it.spec()) } addField(dependenciesField.spec()) - cacheFields.forEach { addField(it.spec(useNullFieldInitialization)) } - addMethod(constructor.spec()) + cacheFields.forEach { addField(it.spec(isBaselineStrategy = isBaselineStrategy)) } + addPerDependencyLockFields(perDependencyLockFields) + addMethod(constructor.spec(perDependencyLockFields)) alternateConstructor?.let { addMethod(it.spec()) } accessMethodImpls.forEach { addMethod(it.spec()) } childMethodImpls.forEach { addMethod(it.spec()) } addMethod(scopeProviderMethod.spec()) - factoryProviderMethods.forEach { addMethods(it.specs(useNullFieldInitialization)) } + factoryProviderMethods.forEach { + addMethods( + it.specs( + isBaselineStrategy = isBaselineStrategy, + perDependencyLockFields = perDependencyLockFields)) + } dependencyProviderMethods.forEach { addMethod(it.spec()) } dependencies?.let { addType(it.spec()) } - objectsImpl?.let { addType(it.spec()) } + // RUNTIME_SELECTABLE generates sibling variant classes that share Objects, requiring + // wider visibility + objectsImpl?.let { addType(it.spec(widenObjectsVisibility = variantSuffix != null)) } } .build() + // [Caching strategy] Adds MotifLock fields when per-dependency locking is enabled. + private fun TypeSpec.Builder.addPerDependencyLockFields(lockFields: PerDependencyLockFields?) { + lockFields?.locks?.values?.forEach { lockFieldName -> + addField( + FieldSpec.builder( + com.squareup.javapoet.ClassName.get("motif", "MotifLock"), + lockFieldName, + Modifier.PRIVATE, + Modifier.FINAL) + .build()) + } + } + + /** + * Generates a runtime wrapper class for RUNTIME_SELECTABLE strategy. The wrapper delegates to + * variant implementations based on MotifRuntimeConfig.cachingStrategy. + */ + private fun ScopeImpl.wrapperSpec(): TypeSpec { + val delegateField = + FieldSpec.builder(superClassName.j, "delegate", Modifier.PRIVATE, Modifier.FINAL).build() + + // Creates a condition block to choose between SmartCache and BaselineSelectableLock + val wrapperConstructor = + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(dependenciesField.dependenciesClassName.j, "dependencies") + .addStatement("this.dependencies = dependencies") + .beginControlFlow( + "if (\$T.cachingStrategy == \$T.SMART_CACHE)", + com.squareup.javapoet.ClassName.get("motif", "MotifRuntimeConfig"), + com.squareup.javapoet.ClassName.get("motif", "CachingStrategy")) + .addStatement( + "this.delegate = new \$T(dependencies)", + com.squareup.javapoet.ClassName.get( + className.j.packageName(), className.j.simpleName() + "_SmartCache")) + .nextControlFlow("else") + .addStatement( + "this.delegate = new \$T(dependencies)", + com.squareup.javapoet.ClassName.get( + className.j.packageName(), + className.j.simpleName() + "_BaselineSelectableLock")) + .endControlFlow() + .build() + + return TypeSpec.classBuilder(className.j) + .apply { + addAnnotation(scopeImplAnnotation.spec()) + addModifiers(Modifier.PUBLIC) + addSuperinterface(superClassName.j) + + addField(delegateField) + addField(dependenciesField.spec()) + addMethod(wrapperConstructor) + alternateConstructor?.let { addMethod(it.spec()) } + + accessMethodImpls.forEach { accessMethod -> + addMethod( + MethodSpec.overriding( + accessMethod.overriddenMethod.element.toJavac(), + accessMethod.overriddenMethod.owner.toJavac() as DeclaredType, + accessMethod.env.toJavac().typeUtils, + ) + .addStatement("return delegate.\$N()", accessMethod.overriddenMethod.name) + .build()) + } + + // Delegate all child methods + childMethodImpls.forEach { childMethod -> + addMethod( + MethodSpec.methodBuilder(childMethod.childMethodName) + .addAnnotation(Override::class.java) + .addModifiers(Modifier.PUBLIC) + .returns(childMethod.childClassName.j) + .apply { + childMethod.parameters.forEach { param -> addParameter(param.spec()) } + } + .addStatement( + "return delegate.\$N(\$L)", + childMethod.childMethodName, + childMethod.parameters.joinToString(", ") { it.name }) + .build()) + } + + // Add Objects nested class if present (for variants to reference) + objectsImpl?.let { addType(it.spec(widenObjectsVisibility = true)) } + + // Add Dependencies interface so variant implementations can reference it + dependencies?.let { addType(it.spec()) } + } + .build() + } + private fun ScopeImplAnnotation.spec(): AnnotationSpec = AnnotationSpec.builder(motif.ScopeImpl::class.java) .apply { @@ -80,21 +198,41 @@ object JavaCodeGenerator { private fun DependenciesField.spec(): FieldSpec = FieldSpec.builder(dependenciesClassName.j, name, Modifier.PRIVATE, Modifier.FINAL).build() - private fun CacheField.spec(useNullFieldInitialization: Boolean): FieldSpec = - if (useNullFieldInitialization) { - FieldSpec.builder(Object::class.java, name, Modifier.PRIVATE, Modifier.VOLATILE).build() - } else { + private fun CacheField.spec(isBaselineStrategy: Boolean): FieldSpec = + if (isBaselineStrategy) { + // BASELINE: Use None.NONE sentinel FieldSpec.builder(Object::class.java, name, Modifier.PRIVATE, Modifier.VOLATILE) .initializer("\$T.NONE", None::class.java) .build() + } else { + // SMART_CACHE: Use null initialization + FieldSpec.builder(Object::class.java, name, Modifier.PRIVATE, Modifier.VOLATILE).build() } - private fun Constructor.spec(): MethodSpec = - MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addParameter(dependenciesClassName.j, dependenciesParameterName) - .addStatement("this.\$N = \$N", dependenciesFieldName, dependenciesParameterName) - .build() + private fun Constructor.spec(perDependencyLockFields: PerDependencyLockFields?): MethodSpec { + val builder = + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(dependenciesClassName.j, dependenciesParameterName) + .addStatement("this.\$N = \$N", dependenciesFieldName, dependenciesParameterName) + + // Use local variable to read runtime config once for consistent lock initialization + if (perDependencyLockFields != null && perDependencyLockFields.locks.isNotEmpty()) { + builder.addStatement( + "boolean usePerDependencyLocking = \$T.usePerDependencyLock", + com.squareup.javapoet.ClassName.get("motif", "MotifRuntimeConfig")) + } + + // Initialize lock fields conditionally based on usePerDependencyLocking + perDependencyLockFields?.locks?.values?.forEach { lockFieldName -> + builder.addStatement( + "this.\$N = usePerDependencyLocking ? new \$T() : null", + lockFieldName, + com.squareup.javapoet.ClassName.get("motif", "MotifLock")) + } + + return builder.build() + } private fun AlternateConstructor.spec(): MethodSpec = MethodSpec.constructorBuilder() @@ -123,7 +261,11 @@ object JavaCodeGenerator { .build() @OptIn(KotlinPoetJavaPoetPreview::class) - private fun ChildDependenciesImpl.spec(): TypeSpec { + // Inline anonymous class that wires a child scope's Dependencies to the parent graph. + private fun ChildDependenciesImpl.spec(): CodeBlock = CodeBlock.of("\$L", spec_anonymousClass()) + + @OptIn(KotlinPoetJavaPoetPreview::class) + private fun ChildDependenciesImpl.spec_anonymousClass(): TypeSpec { val isKotlinDepInterface = env.findTypeElement(childDependenciesClassName.j).isKotlinSource(env) return TypeSpec.anonymousClassBuilder("") .apply { @@ -168,32 +310,53 @@ object JavaCodeGenerator { private fun ScopeProviderMethod.spec(): MethodSpec = MethodSpec.methodBuilder(name).returns(scopeClassName.j).addStatement("return this").build() - private fun FactoryProviderMethod.specs(useNullFieldInitialization: Boolean): List { + private fun FactoryProviderMethod.specs( + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields? + ): List { val primarySpec = MethodSpec.methodBuilder(name) .returns(returnTypeName.j) - .addStatement(body.spec(useNullFieldInitialization)) + .addStatement(body.spec(isBaselineStrategy, perDependencyLockFields)) .build() val spreadSpecs = spreadProviderMethods.map { it.spec() } return listOf(primarySpec) + spreadSpecs } - private fun FactoryProviderMethodBody.spec(useNullFieldInitialization: Boolean): CodeBlock = + private fun FactoryProviderMethodBody.spec( + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields? + ): CodeBlock = when (this) { - is FactoryProviderMethodBody.Cached -> spec(useNullFieldInitialization) + is FactoryProviderMethodBody.Cached -> spec(isBaselineStrategy, perDependencyLockFields) is FactoryProviderMethodBody.Uncached -> spec() } private fun FactoryProviderMethodBody.Cached.spec( - useNullFieldInitialization: Boolean, + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields? ): CodeBlock { - if (useNullFieldInitialization) { + // SMART_CACHE strategy: Use null initialization + if (!isBaselineStrategy) { val localFieldName = "_$cacheFieldName" - return CodeBlock.builder() - // Using a local variable reduces atomic read overhead - .add("Object $localFieldName = \$N;\n", cacheFieldName) - .beginControlFlow("if (\$N == null)", localFieldName) - .beginControlFlow("synchronized (this)") + // Get the lock field name for this cache field (if per-dependency locks are enabled) + val lockFieldName = perDependencyLockFields?.locks?.get(cacheFieldName) + + val builder = + CodeBlock.builder() + // Using a local variable reduces atomic read overhead + .add("Object $localFieldName = \$N;\n", cacheFieldName) + .beginControlFlow("if (\$N == null)", localFieldName) + + // Add synchronized block using nullable lock pattern: lock_foo != null ? lock_foo : this + if (lockFieldName != null) { + builder.beginControlFlow( + "synchronized(\$N != null ? \$N : this)", lockFieldName, lockFieldName) + } else { + builder.beginControlFlow("synchronized (this)") + } + + return builder .add("\$N = \$N;\n", localFieldName, cacheFieldName) .beginControlFlow("if (\$N == null)", localFieldName) .add("\$N = \$L;\n", localFieldName, instantiation.spec()) @@ -211,9 +374,23 @@ object JavaCodeGenerator { .add("return (\$T) \$N", returnTypeName.j, localFieldName) .build() } - return CodeBlock.builder() - .beginControlFlow("if (\$N == \$T.NONE)", cacheFieldName, None::class.java) - .beginControlFlow("synchronized (this)") + // BASELINE strategy: Use None.NONE sentinel + // Get the lock field name for this cache field (if per-dependency locks are enabled) + val lockFieldName = perDependencyLockFields?.locks?.get(cacheFieldName) + + val builder = + CodeBlock.builder() + .beginControlFlow("if (\$N == \$T.NONE)", cacheFieldName, None::class.java) + + // Add synchronized block using nullable lock pattern: lock_foo != null ? lock_foo : this + if (lockFieldName != null) { + builder.beginControlFlow( + "synchronized(\$N != null ? \$N : this)", lockFieldName, lockFieldName) + } else { + builder.beginControlFlow("synchronized (this)") + } + + return builder .beginControlFlow("if (\$N == \$T.NONE)", cacheFieldName, None::class.java) .add("\$N = \$L;", cacheFieldName, instantiation.spec()) .endControlFlow() @@ -308,10 +485,16 @@ object JavaCodeGenerator { return CodeBlock.of("
  • {@link \$L#\$N(\$L)}
  • \n", owner, methodName, parameterTypeString) } - private fun ObjectsImpl.spec(): TypeSpec = + private fun ObjectsImpl.spec(widenObjectsVisibility: Boolean): TypeSpec = TypeSpec.classBuilder(className.j) .apply { - addModifiers(Modifier.PRIVATE, Modifier.STATIC) + if (widenObjectsVisibility) { + // Package-private (no access modifier) so RUNTIME_SELECTABLE variants, + // which are separate top-level classes, can access it. + addModifiers(Modifier.STATIC) + } else { + addModifiers(Modifier.PRIVATE, Modifier.STATIC) + } if (isInterface) { addSuperinterface(superClassName.j) } else { diff --git a/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt b/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt index 4bdc2654..397009aa 100644 --- a/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt +++ b/compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt @@ -40,8 +40,28 @@ object KotlinCodeGenerator { return FileSpec.get(scopeImpl.className.kt.packageName, typeSpec) } - private fun ScopeImpl.spec(): TypeSpec = - TypeSpec.classBuilder(className.kt) + // [Caching strategy] Routes to wrapper, variant, or standard spec. + private fun ScopeImpl.spec(): TypeSpec { + if (isRuntimeSelectableWrapper) { + return wrapperSpec() + } + + val classNameToUse = + if (variantSuffix != null) { + com.squareup.kotlinpoet.ClassName( + className.kt.packageName, + className.kt.simpleName + variantSuffix, + ) + } else { + className.kt + } + + return spec(classNameToUse) + } + + // Builds the class TypeSpec for a ScopeImpl. + private fun ScopeImpl.spec(classNameToUse: com.squareup.kotlinpoet.ClassName): TypeSpec = + TypeSpec.classBuilder(classNameToUse) .apply { addAnnotation(suppressAnnotationSpec("REDUNDANT_PROJECTION", "UNCHECKED_CAST")) addAnnotation(scopeImplAnnotation.spec()) @@ -49,7 +69,8 @@ object KotlinCodeGenerator { addSuperinterface(superClassName.kt) objectsField?.let { addProperty(it.spec()) } addProperty(dependenciesField.spec()) - cacheFields.forEach { addProperty(it.spec(useNullFieldInitialization)) } + cacheFields.forEach { addProperty(it.spec(isBaselineStrategy = isBaselineStrategy)) } + addPerDependencyLockFields(perDependencyLockFields) primaryConstructor(constructor.spec()) alternateConstructor?.let { addFunction(it.spec()) } accessMethodImpls @@ -60,13 +81,165 @@ object KotlinCodeGenerator { .forEach { addProperty(it.propSpec()) } childMethodImpls.forEach { addFunction(it.spec()) } addFunction(scopeProviderMethod.spec()) - factoryProviderMethods.forEach { addFunctions(it.specs(useNullFieldInitialization)) } + factoryProviderMethods.forEach { + addFunctions( + it.specs( + isBaselineStrategy = isBaselineStrategy, + perDependencyLockFields = perDependencyLockFields, + ), + ) + } dependencyProviderMethods.forEach { addFunction(it.spec()) } dependencies?.let { addType(it.spec()) } - objectsImpl?.let { addType(it.spec()) } + // RUNTIME_SELECTABLE generates sibling variant classes that share Objects, requiring + // wider visibility + objectsImpl?.let { addType(it.spec(widenObjectsVisibility = variantSuffix != null)) } } .build() + // [Caching strategy] Adds MotifLock fields and config cache when per-dependency locking is + // enabled. + private fun TypeSpec.Builder.addPerDependencyLockFields(lockFields: PerDependencyLockFields?) { + lockFields ?: return + if (lockFields.locks.isNotEmpty()) { + addProperty( + PropertySpec.builder("usePerDependencyLocking", Boolean::class) + .addModifiers(KModifier.PRIVATE) + .initializer( + "%T.usePerDependencyLock", + ClassName.bestGuess("motif.MotifRuntimeConfig"), + ) + .build(), + ) + } + lockFields.locks.values.forEach { lockFieldName -> + addProperty( + PropertySpec.builder( + lockFieldName, + ClassName.bestGuess("motif.MotifLock").copy(nullable = true), + ) + .addModifiers(KModifier.PRIVATE) + .mutable(false) + .initializer( + "if (usePerDependencyLocking) %T() else null", + ClassName.bestGuess("motif.MotifLock"), + ) + .build(), + ) + } + } + + /** + * Generates a runtime wrapper class for RUNTIME_SELECTABLE strategy. The wrapper delegates to + * variant implementations based on MotifRuntimeConfig.cachingStrategy. + */ + private fun ScopeImpl.wrapperSpec(): TypeSpec { + // Creates a condition block to choose between SmartCache and BaselineSelectableLock + val wrapperConstructor = + FunSpec.constructorBuilder() + .addParameter(dependenciesField.name, dependenciesField.dependenciesClassName.kt) + .build() + + return TypeSpec.classBuilder(className.kt) + .apply { + addAnnotation(suppressAnnotationSpec("REDUNDANT_PROJECTION", "UNCHECKED_CAST")) + addAnnotation(scopeImplAnnotation.spec()) + addModifiers(if (internalScope) KModifier.INTERNAL else KModifier.PUBLIC) + addSuperinterface(superClassName.kt) + + addProperty(dependenciesField.spec()) + + addProperty( + PropertySpec.builder("delegate", superClassName.kt, KModifier.PRIVATE) + .initializer( + CodeBlock.builder() + .add( + "when (%T.cachingStrategy) {\n", + ClassName.bestGuess("motif.MotifRuntimeConfig"), + ) + .indent() + .add( + "%T.SMART_CACHE -> %T_SmartCache(dependencies)\n", + ClassName.bestGuess("motif.CachingStrategy"), + className.kt, + ) + .add("else -> %T_BaselineSelectableLock(dependencies)\n", className.kt) + .unindent() + .add("}") + .build(), + ) + .build(), + ) + + primaryConstructor(wrapperConstructor) + alternateConstructor?.let { addFunction(it.spec()) } + + accessMethodImpls + .filter { !it.overriddenMethod.isSynthetic } + .forEach { + addFunction( + XFunSpec.overriding( + it.overriddenMethod.element, + it.overriddenMethod.owner, + it.env, + ) + .addStatement("return delegate.%N()", it.overriddenMethod.name) + .build(), + ) + } + + // Delegate synthetic properties + accessMethodImpls + .filter { it.overriddenMethod.isSynthetic } + .forEach { + val propName = + with(it.overriddenMethod.name) { + when { + startsWith("get") -> this.substring(3).decapitalize() + startsWith("is") -> this.substring(2).decapitalize() + else -> this + } + } + addProperty( + PropertySpec.builder( + propName, + ClassName.bestGuess(it.overriddenMethod.returnType.qualifiedName), + ) + .addModifiers(KModifier.OVERRIDE) + .initializer("delegate.%N", propName) + .build(), + ) + } + + // Delegate child methods + childMethodImpls.forEach { childMethod -> + addFunction( + FunSpec.builder(childMethod.childMethodName) + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(childMethod.childClassName.kt) + .apply { + childMethod.parameters.forEach { param -> + addParameter(param.name, param.typeName.kt) + } + } + .addStatement( + "return delegate.%N(%L)", + childMethod.childMethodName, + childMethod.parameters.joinToString { it.name }, + ) + .build(), + ) + } + + // Add Objects nested class if present (for variants to reference) + objectsImpl?.let { addType(it.spec(widenObjectsVisibility = true)) } + + // Add Dependencies interface so variant implementations can reference it + dependencies?.let { addType(it.spec()) } + } + .build() + } + private fun ScopeImplAnnotation.spec(): AnnotationSpec = AnnotationSpec.builder(motif.ScopeImpl::class) .apply { @@ -97,18 +270,20 @@ object KotlinCodeGenerator { .initializer(name) .build() - private fun CacheField.spec(useNullFieldInitialization: Boolean): PropertySpec = - if (useNullFieldInitialization) { - PropertySpec.builder(name, Any::class.asTypeName().copy(true), KModifier.PRIVATE) + private fun CacheField.spec(isBaselineStrategy: Boolean): PropertySpec = + if (isBaselineStrategy) { + // BASELINE: Use None.NONE sentinel + PropertySpec.builder(name, Any::class, KModifier.PRIVATE) .mutable(true) .addAnnotation(Volatile::class) - .initializer("null") + .initializer("%T.NONE", None::class) .build() } else { - PropertySpec.builder(name, Any::class, KModifier.PRIVATE) + // SMART_CACHE: Use null initialization + PropertySpec.builder(name, Any::class.asTypeName().copy(true), KModifier.PRIVATE) .mutable(true) .addAnnotation(Volatile::class) - .initializer("%T.NONE", None::class) + .initializer("null") .build() } @@ -158,6 +333,7 @@ object KotlinCodeGenerator { .build() } + // Inline anonymous class that wires a child scope's Dependencies to the parent graph. private fun ChildDependenciesImpl.spec(): TypeSpec = TypeSpec.anonymousClassBuilder() .apply { @@ -203,48 +379,88 @@ object KotlinCodeGenerator { .addStatement("return this") .build() - private fun FactoryProviderMethod.specs(useNullFieldInitialization: Boolean): List { + private fun FactoryProviderMethod.specs( + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields?, + ): List { val primarySpec = FunSpec.builder(name) .addModifiers(KModifier.INTERNAL) .returns(returnTypeName.reloadedForTypeArgs(env)) - .addCode(body.spec(useNullFieldInitialization)) + .addCode(body.spec(isBaselineStrategy, perDependencyLockFields)) .build() val spreadSpecs = spreadProviderMethods.map { it.spec() } return listOf(primarySpec) + spreadSpecs } - private fun FactoryProviderMethodBody.spec(useNullFieldInitialization: Boolean): CodeBlock = + private fun FactoryProviderMethodBody.spec( + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields?, + ): CodeBlock = when (this) { - is FactoryProviderMethodBody.Cached -> spec(useNullFieldInitialization) + is FactoryProviderMethodBody.Cached -> spec(isBaselineStrategy, perDependencyLockFields) is FactoryProviderMethodBody.Uncached -> spec() } private fun FactoryProviderMethodBody.Cached.spec( - useNullFieldInitialization: Boolean, + isBaselineStrategy: Boolean, + perDependencyLockFields: PerDependencyLockFields?, ): CodeBlock { - if (useNullFieldInitialization) { + // SMART_CACHE strategy: Use null initialization instead of None.NONE sentinel + // This path is taken when cachingStrategy == SMART_CACHE + if (!isBaselineStrategy) { val localFieldName = "_$cacheFieldName" + // Get the lock field name for this cache field (if per-dependency locks are enabled) + val lockFieldName = perDependencyLockFields?.locks?.get(cacheFieldName) + val codeBlockBuilder = CodeBlock.builder() // Using a local variable reduces atomic read overhead .addStatement("var $localFieldName = %N;\n", cacheFieldName) .beginControlFlow("if (%N == null)", localFieldName) - .beginControlFlow("synchronized (this)") - .addStatement("%N = %N", localFieldName, cacheFieldName) - .beginControlFlow("if (%N == null)", localFieldName) - .addStatement("%N = %L", localFieldName, instantiation.spec()) - .addStatement("%N = %N", cacheFieldName, localFieldName) - .endControlFlow() - .endControlFlow() - .endControlFlow() + + // Add synchronized block using nullable lock pattern: lock_foo ?: this + if (lockFieldName != null) { + codeBlockBuilder.beginControlFlow("synchronized(%N ?: this)", lockFieldName) + } else { + codeBlockBuilder.beginControlFlow("synchronized (this)") + } + + codeBlockBuilder + .addStatement("%N = %N", localFieldName, cacheFieldName) + .beginControlFlow("if (%N == null)", localFieldName) + .addStatement("%N = %L", localFieldName, instantiation.spec()) + .beginControlFlow("if (%N == null)", localFieldName) + .addStatement( + "throw %T(%S)", + NullPointerException::class, + "Factory method cannot return null", + ) + .endControlFlow() + .addStatement("%N = %N", cacheFieldName, localFieldName) + .endControlFlow() + .endControlFlow() + .endControlFlow() return codeBlockBuilder .add("return ( %N as %T )", localFieldName, returnTypeName.reloadedForTypeArgs(env)) .build() } - return CodeBlock.builder() - .beginControlFlow("if (%N == %T.NONE)", cacheFieldName, None::class) - .beginControlFlow("synchronized (this)") + + // BASELINE strategy: Use None.NONE sentinel with synchronized(this) - matches alpha09 exactly + // Get the lock field name for this cache field (if per-dependency locks are enabled) + val lockFieldName = perDependencyLockFields?.locks?.get(cacheFieldName) + + val codeBuilder = + CodeBlock.builder().beginControlFlow("if (%N == %T.NONE)", cacheFieldName, None::class) + + // Add synchronized block using nullable lock pattern: lock_foo ?: this + if (lockFieldName != null) { + codeBuilder.beginControlFlow("synchronized(%N ?: this)", lockFieldName) + } else { + codeBuilder.beginControlFlow("synchronized (this)") + } + + return codeBuilder .beginControlFlow("if (%N == %T.NONE)", cacheFieldName, None::class) .addStatement("%N=%L", cacheFieldName, instantiation.spec()) .endControlFlow() @@ -356,10 +572,16 @@ object KotlinCodeGenerator { private fun JavaDocMethodLink.spec(): CodeBlock = CodeBlock.of("* [%L.%N]\n", owner, methodName) - private fun ObjectsImpl.spec(): TypeSpec = + private fun ObjectsImpl.spec(widenObjectsVisibility: Boolean): TypeSpec = TypeSpec.classBuilder(className.kt) .apply { - addModifiers(KModifier.PRIVATE) + if (widenObjectsVisibility) { + // internal so RUNTIME_SELECTABLE variants, which are separate top-level + // classes, can access it. + addModifiers(KModifier.INTERNAL) + } else { + addModifiers(KModifier.PRIVATE) + } if (isInterface) { addSuperinterface(superClassName.kt) } else { diff --git a/compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt b/compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt index 499e9c0b..924f1f99 100644 --- a/compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt +++ b/compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt @@ -35,7 +35,7 @@ import motif.ast.compiler.CompilerMethod * implementations. */ class ScopeImpl( - val useNullFieldInitialization: Boolean, + val isBaselineStrategy: Boolean, val className: ClassName, val superClassName: ClassName, val internalScope: Boolean, @@ -43,6 +43,7 @@ class ScopeImpl( val objectsField: ObjectsField?, val dependenciesField: DependenciesField, val cacheFields: List, + val perDependencyLockFields: PerDependencyLockFields?, val constructor: Constructor, val alternateConstructor: AlternateConstructor?, val accessMethodImpls: List, @@ -52,6 +53,18 @@ class ScopeImpl( val dependencyProviderMethods: List, val objectsImpl: ObjectsImpl?, val dependencies: Dependencies?, + /** + * True if this ScopeImpl represents the runtime wrapper for RUNTIME_SELECTABLE strategy. The + * wrapper delegates to variant classes based on MotifRuntimeConfig.cachingStrategy. False for + * normal implementations and variant classes. + */ + val isRuntimeSelectableWrapper: Boolean = false, + /** + * Suffix appended to the class name for variant classes in RUNTIME_SELECTABLE strategy. + * Examples: "_BaselineSelectableLock", "_SmartCache" Null for normal implementations and + * wrapper classes. + */ + val variantSuffix: String? = null, ) /** @@ -94,6 +107,26 @@ class DependenciesField(val dependenciesClassName: ClassName, val name: String) */ class CacheField(val name: String) +/** + * Per-dependency lock fields for BASELINE_WITH_LOCK_SELECTABLE strategy. + * + * ``` + * private final MotifLock lock_foo; + * private final MotifLock lock_bar; + * ``` + * + * Maps cache field names to their corresponding lock field names. Lock fields are nullable and + * conditionally initialized based on MotifRuntimeConfig.usePerDependencyLock. + */ +class PerDependencyLockFields( + // Map + val locks: Map, +) { + init { + require(locks.isNotEmpty()) { "PerDependencyLockFields must have at least one lock entry" } + } +} + /** * ``` * public FooScopeImpl(FooScopeImpl.Dependencies dependencies) { diff --git a/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt b/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt index f8049db9..5615df9d 100644 --- a/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt +++ b/compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt @@ -54,7 +54,7 @@ private constructor( private fun create(): List = graph.scopes .filter { scope -> env.findTypeElement(scope.implClassName.j.toString()) == null } - .map { scope -> Factory(scope).create() } + .flatMap { scope -> Factory(scope).create() } private inner class Factory(private val scope: Scope) { @@ -65,29 +65,118 @@ private constructor( private val providerMethodNames = mutableMapOf() private val cacheFieldNames = mutableMapOf() - fun create(): ScopeImpl { + // Memoization for shouldCache() to avoid recomputation and handle recursion + private val shouldCacheCache = mutableMapOf() + private val shouldCacheComputing = mutableSetOf() + + // Memoization for dependent lookup: Type -> Set of FactoryMethods + // Also used for usage count via dependentsCache[type]?.size + private val dependentsCache by lazy { + val dependents = mutableMapOf>() + scope.factoryMethods.forEach { factoryMethod -> + factoryMethod.parameters.forEach { param -> + dependents.getOrPut(param.type) { mutableSetOf() }.add(factoryMethod) + } + // Also count spread method consumers: the spread source type is consumed by the + // factory method that produces it, so downstream spread consumers should count + // as additional usage of the spread source's return type + factoryMethod.spread?.methods?.forEach { spreadMethod -> + dependents.getOrPut(spreadMethod.sourceType) { mutableSetOf() }.add(factoryMethod) + } + } + dependents + } + + fun create(): List { + val isInternal = (scope.clazz as? CompilerClass)?.isInternal() ?: false + val scopeAnnotation = + scope.clazz.annotations.find { it.className == motif.Scope::class.java.name }!! + val cachingStrategy = resolveCachingStrategy(scopeAnnotation) + + // For RUNTIME_SELECTABLE, we generate 3 ScopeImpl instances: + // 1. Wrapper class (isRuntimeSelectableWrapper = true, variantSuffix = null) + // 2. BASELINE_WITH_LOCK_SELECTABLE variant (variantSuffix = "_BaselineSelectableLock") + // 3. SMART_CACHE variant (variantSuffix = "_SmartCache") + if (cachingStrategy == motif.CachingStrategy.RUNTIME_SELECTABLE) { + return listOf( + createWrapper(), + createVariant( + motif.CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE, + "_BaselineSelectableLock", + ), + createVariant(motif.CachingStrategy.SMART_CACHE, "_SmartCache"), + ) + } + + return listOf(createVariant(cachingStrategy, variantSuffix = null)) + } + + private fun createWrapper(): ScopeImpl { + val isInternal = (scope.clazz as? CompilerClass)?.isInternal() ?: false + // Wrapper generates the Dependencies nested class that variants will reference + val scopeAnnotation = + scope.clazz.annotations.find { it.className == motif.Scope::class.java.name }!! + val cachingStrategy = resolveCachingStrategy(scopeAnnotation) + + return ScopeImpl( + isBaselineStrategy = true, // Not used for wrapper + className = scope.implClassName, + superClassName = scope.typeName, + internalScope = isInternal, + scopeImplAnnotation = scopeImplAnnotation(), + objectsField = null, // Wrapper doesn't need objects field (it delegates) + dependenciesField = dependenciesField(), + cacheFields = emptyList(), // Wrapper doesn't have cache fields + perDependencyLockFields = null, // Wrapper doesn't have lock fields + constructor = constructor(), + alternateConstructor = alternateConstructor(), + accessMethodImpls = accessMethodImpls(), + childMethodImpls = childMethodImpls(cachingStrategy, variantSuffix = null), + scopeProviderMethod = scopeProviderMethod(), + factoryProviderMethods = emptyList(), // Wrapper delegates instead + dependencyProviderMethods = emptyList(), // Wrapper delegates instead + objectsImpl = + objectsImpl(), // Wrapper needs Objects nested class for variants to reference + dependencies = + dependencies( + forceGenerate = true, + ), // Wrapper needs Dependencies nested class for variants to reference + isRuntimeSelectableWrapper = true, + variantSuffix = null, + ) + } + + private fun createVariant( + cachingStrategy: motif.CachingStrategy, + variantSuffix: String?, + ): ScopeImpl { val isInternal = (scope.clazz as? CompilerClass)?.isInternal() ?: false + val isBaselineStrategy = + cachingStrategy == motif.CachingStrategy.BASELINE || + cachingStrategy == motif.CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE + val useSelectiveCaching = cachingStrategy == motif.CachingStrategy.SMART_CACHE + return ScopeImpl( - (scope.clazz.annotations - .find { it.className == motif.Scope::class.java.name }!! - .annotationValueMap[SCOPE_ANNOTATION_FIELD_USE_NULL] - as? Boolean) ?: false, + isBaselineStrategy, scope.implClassName, scope.typeName, isInternal, scopeImplAnnotation(), objectsField(), dependenciesField(), - cacheFields(), + cacheFields(useSelectiveCaching), + perDependencyLockFields(cachingStrategy, useSelectiveCaching), constructor(), alternateConstructor(), accessMethodImpls(), - childMethodImpls(), + childMethodImpls(cachingStrategy, variantSuffix), scopeProviderMethod(), - factoryProviderMethods(), + factoryProviderMethods(useSelectiveCaching), dependencyProviderMethods(), objectsImpl(), dependencies(), + isRuntimeSelectableWrapper = false, + variantSuffix = variantSuffix, ) } @@ -105,11 +194,48 @@ private constructor( private fun dependenciesField(): DependenciesField = DependenciesField(scope.dependenciesClassName, DEPENDENCIES_FIELD_NAME) - private fun cacheFields(): List = + private fun cacheFields(useSelectiveCaching: Boolean): List = scope.factoryMethods - .filter { it.isCached } + .filter { factoryMethod -> + if (useSelectiveCaching) { + !shouldSkipCaching(factoryMethod, useSelectiveCaching) && shouldCache(factoryMethod) + } else { + factoryMethod.isCached + } + } .map { factoryMethod -> CacheField(getCacheFieldName(factoryMethod.returnType.type)) } + private fun perDependencyLockFields( + cachingStrategy: motif.CachingStrategy, + useSelectiveCaching: Boolean, + ): PerDependencyLockFields? { + // Generate per-dependency lock fields for strategies that support runtime lock selection + // Both BASELINE_WITH_LOCK_SELECTABLE and SMART_CACHE support per-dependency locks + if (cachingStrategy != motif.CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE && + cachingStrategy != motif.CachingStrategy.SMART_CACHE) { + return null + } + + // Create a map from cache field names to lock field names + val locks = + scope.factoryMethods + .filter { factoryMethod -> + if (useSelectiveCaching) { + !shouldSkipCaching(factoryMethod, useSelectiveCaching) && + shouldCache(factoryMethod) + } else { + factoryMethod.isCached + } + } + .associate { factoryMethod -> + val cacheFieldName = getCacheFieldName(factoryMethod.returnType.type) + val lockFieldName = "lock_$cacheFieldName" + cacheFieldName to lockFieldName + } + + return if (locks.isEmpty()) null else PerDependencyLockFields(locks) + } + private fun constructor(): Constructor = Constructor(scope.dependenciesClassName, "dependencies", DEPENDENCIES_FIELD_NAME) @@ -130,16 +256,25 @@ private constructor( ) } - private fun childMethodImpls(): List = - graph.getChildEdges(scope).map(this::childMethodImpl) + private fun childMethodImpls( + cachingStrategy: motif.CachingStrategy, + variantSuffix: String?, + ): List = + graph.getChildEdges(scope).map { childEdge -> + childMethodImpl(childEdge, cachingStrategy, variantSuffix) + } - private fun childMethodImpl(childEdge: ScopeEdge): ChildMethodImpl = + private fun childMethodImpl( + childEdge: ScopeEdge, + cachingStrategy: motif.CachingStrategy, + variantSuffix: String?, + ): ChildMethodImpl = ChildMethodImpl( childEdge.child.typeName, childEdge.child.implClassName, childEdge.method.method.name, childEdge.method.parameters.map(this::childMethodImplParameter), - childDependenciesImpl(childEdge), + childDependenciesImpl(childEdge, cachingStrategy, variantSuffix), ) private fun childMethodImplParameter( @@ -150,14 +285,31 @@ private constructor( childMethodParameter.parameter.name, ) - private fun childDependenciesImpl(childEdge: ScopeEdge): ChildDependenciesImpl { + private fun childDependenciesImpl( + childEdge: ScopeEdge, + cachingStrategy: motif.CachingStrategy, + variantSuffix: String?, + ): ChildDependenciesImpl { + // For variants, use the variant class name (with suffix) for the parent scope class name + // This is needed because variants are separate top-level classes, not nested classes + val parentScopeClassName = + if (variantSuffix != null) { + ClassName.get( + scope.implClassName.j.packageName(), + scope.implClassName.j.simpleName() + variantSuffix, + ) + } else { + scope.implClassName + } + val parameters: Map = childEdge.method.parameters.associateBy { parameter -> parameter.type } val dependencyMethodImpls = getDependencyMethodData(childEdge.child).map { methodData -> - childDependencyMethodImpl(parameters, methodData) + childDependencyMethodImpl(parameters, methodData, parentScopeClassName) } val isAbstractClass = dependencyMethodImpls.any { it.isInternal } + return ChildDependenciesImpl( childEdge.child.dependenciesClassName, dependencyMethodImpls, @@ -169,12 +321,13 @@ private constructor( private fun childDependencyMethodImpl( parameters: Map, methodData: DependencyMethodData, + parentScopeClassName: ClassName, ): ChildDependencyMethodImpl { val parameter = parameters[methodData.returnType] val returnExpression = if (parameter == null) { ChildDependencyMethodImpl.ReturnExpression.Provider( - scope.implClassName, + parentScopeClassName, getProviderMethodName(methodData.returnType), ) } else { @@ -195,7 +348,7 @@ private constructor( return ScopeProviderMethod(name, scope.typeName, isInternal) } - private fun factoryProviderMethods(): List = + private fun factoryProviderMethods(useSelectiveCaching: Boolean): List = scope.factoryMethods.map { factoryMethod -> val returnType = factoryMethod.returnType.type val spreadProviderMethods = @@ -203,20 +356,32 @@ private constructor( FactoryProviderMethod( getProviderMethodName(returnType), returnType.type.typeName, - factoryProviderMethodBody(factoryMethod), + factoryProviderMethodBody(factoryMethod, useSelectiveCaching), spreadProviderMethods, env, ) } - private fun factoryProviderMethodBody(factoryMethod: FactoryMethod): FactoryProviderMethodBody { + private fun factoryProviderMethodBody( + factoryMethod: FactoryMethod, + useSelectiveCaching: Boolean, + ): FactoryProviderMethodBody { val instantiation = when (factoryMethod) { is BasicFactoryMethod -> basicInstantiation(factoryMethod) is ConstructorFactoryMethod -> constructorInstantiation(factoryMethod) is BindsFactoryMethod -> bindsInstantiation(factoryMethod) } - return if (factoryMethod.isCached) { + + val shouldBeCached = + !shouldSkipCaching(factoryMethod, useSelectiveCaching) && + if (useSelectiveCaching) { + shouldCache(factoryMethod) + } else { + factoryMethod.isCached + } + + return if (shouldBeCached) { FactoryProviderMethodBody.Cached( getCacheFieldName(factoryMethod.returnType.type), factoryMethod.returnType.type.type.typeName, @@ -298,8 +463,8 @@ private constructor( ) } - private fun dependencies(): Dependencies? { - if (scope.dependencies != null) { + private fun dependencies(forceGenerate: Boolean = false): Dependencies? { + if (!forceGenerate && scope.dependencies != null) { return null } val methods = @@ -367,6 +532,215 @@ private constructor( return type } + + // === Selective Caching Logic for SMART_CACHE === + + /** + * Determines whether a factory method should be cached based on usage patterns. Used in + * SMART_CACHE mode to optimize memory usage by skipping caching for single-use, internal-only + * dependencies. + */ + private fun shouldCache(factoryMethod: FactoryMethod): Boolean { + val returnType = factoryMethod.returnType.type + + // Check memoization cache first + shouldCacheCache[returnType]?.let { + return it + } + + // Detect cycles (shouldn't happen with valid DI graphs, but be defensive) + if (returnType in shouldCacheComputing) { + // Conservative: cache dependencies involved in cycles to break the cycle + // Unless explicitly marked with @DoNotCache for all modes (not just SmartCache) + return !factoryMethod.hasDoNotCache || factoryMethod.doNotCacheOnlyForSmartCache + } + + // Mark as computing to detect cycles + shouldCacheComputing.add(returnType) + + try { + val result = computeShouldCache(factoryMethod) + shouldCacheCache[returnType] = result + return result + } finally { + shouldCacheComputing.remove(returnType) + } + } + + /** Core caching decision logic applying multiple heuristics. */ + private fun computeShouldCache(factoryMethod: FactoryMethod): Boolean { + val returnType = factoryMethod.returnType.type + + // Rule 1: If method has @DoNotCache annotation, never cache + if (factoryMethod.hasDoNotCache) { + return false + } + + // Rule 2: Skip cache if return type has @DoNotCache annotation + if (hasDoNotCacheAnnotation(returnType)) { + return false + } + + // Rule 3: Skip cache for abstract passthrough methods + // These are abstract methods that just cast/forward a parameter to a different type + // with no construction cost + if (isPassthroughMethod(factoryMethod)) { + return false + } + + // Rule 4: Check if this dependency has public accessor method + val hasAccessor = scope.accessMethods.any { it.returnType == returnType } + if (hasAccessor) { + return true + } + + // Count how many times this dependency is used internally + val usageCount = countInternalUsage(returnType) + + // Rule 5: Dead code - not used at all, never cache + if (usageCount == 0) { + return false + } + + // Rule 6: Used multiple times internally + if (usageCount > 1) { + return true + } + + // Rule 7: Has @Expose annotation + if (factoryMethod.isExposed) { + return true + } + + // Rule 8: Usage count = 1 and the dependent is cached or used once. + return !isDependentCreatedOnce(returnType) + } + + /** + * Checks if a method is a simple passthrough (abstract single-parameter method that just + * casts/forwards without construction overhead). + */ + private fun isPassthroughMethod(factoryMethod: FactoryMethod): Boolean { + // Only check abstract methods - concrete methods are kept as-is + if (!factoryMethod.method.isAbstract()) { + return false + } + + // Must have exactly 1 parameter + if (factoryMethod.parameters.size != 1) { + return false + } + + val paramType = factoryMethod.parameters.single().type + val returnType = factoryMethod.returnType.type.type as? CompilerType ?: return false + val paramCompilerType = paramType.type as? CompilerType ?: return false + + // Check if return type is assignable from parameter type + val isAssignable = + try { + val returnXType = env.findType(returnType.qualifiedName) + val paramXType = env.findType(paramCompilerType.qualifiedName) + + if (returnXType != null && paramXType != null) { + returnXType.isAssignableFrom(paramXType) + } else { + false + } + } catch (e: Exception) { + false + } + + return isAssignable + } + + /** Checks if a type has @DoNotCache annotation (walks up the class hierarchy). */ + private fun hasDoNotCacheAnnotation(type: Type): Boolean { + val compilerType = type.type as? CompilerType ?: return false + + try { + val xType = env.findType(compilerType.qualifiedName) ?: return false + val typeElement = xType.typeElement ?: return false + + // Walk up the class hierarchy checking for @DoNotCache annotation + var currentElement = typeElement + val visited = mutableSetOf() + + while (true) { + val qname = currentElement.qualifiedName + + // Prevent infinite loops + if (qname in visited) break + visited.add(qname) + + // Check if current class has @DoNotCache annotation + if (currentElement.hasAnnotation(motif.DoNotCache::class)) { + return true + } + + // Move to superclass + val superType = currentElement.superClass + if (superType == null || superType.typeElement == null) { + break + } + + currentElement = superType.typeElement!! + + // Stop at Object/Any + val superQName = currentElement.qualifiedName + if (superQName == "java.lang.Object" || superQName == "kotlin.Any") { + break + } + } + + return false + } catch (e: Exception) { + return false + } + } + + /** Checks if the single dependent of a type is created only once. */ + private fun isDependentCreatedOnce(type: Type): Boolean { + // Since this is only called when usageCount == 1, there's exactly one dependent + val dependent = dependentsCache[type]?.singleOrNull() ?: return true + + // If the dependent has @DoNotCache, it will be recreated each time + // So we should cache this dependency to avoid recreating it + if (dependent.hasDoNotCache) { + return false // Dependent is NOT created once -> cache this dependency + } + + // If the dependent is cached, it's only created once + if (shouldCache(dependent)) { + return true + } + + // If the dependent is not cached, check how many times it's used + val dependentUsageCount = dependentsCache[dependent.returnType.type]?.size ?: 0 + + // Return true if the dependent is only used once + return dependentUsageCount <= 1 + } + + /** Determines if caching should be skipped based on @DoNotCache annotation settings. */ + private fun shouldSkipCaching( + factoryMethod: FactoryMethod, + useSelectiveCaching: Boolean, + ): Boolean { + if (!factoryMethod.hasDoNotCache) { + return false + } + + // If onlyForSmartCacheMode = true, only skip caching in SMART_CACHE mode + if (factoryMethod.doNotCacheOnlyForSmartCache) { + return useSelectiveCaching + } + + // Otherwise, skip in all modes + return true + } + + /** Returns the number of times a type is used internally within the scope. */ + private fun countInternalUsage(type: Type): Int = dependentsCache[type]?.size ?: 0 } private class DependencyMethodData( @@ -429,11 +803,31 @@ private constructor( scope.implClassName.nestedClass("Objects") } + /** + * Resolves the caching strategy from the @Scope annotation. Defaults to BASELINE for backwards + * compatibility and safety. + */ + private fun resolveCachingStrategy( + scopeAnnotation: motif.ast.IrAnnotation, + ): motif.CachingStrategy { + val strategyValue = + scopeAnnotation.annotationValueMap[SCOPE_ANNOTATION_FIELD_CACHING_STRATEGY] + ?: return motif.CachingStrategy.BASELINE // Default to BASELINE if not specified + + return when (strategyValue.toString()) { + "SMART_CACHE" -> motif.CachingStrategy.SMART_CACHE + "BASELINE_WITH_LOCK_SELECTABLE" -> motif.CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE + "RUNTIME_SELECTABLE" -> motif.CachingStrategy.RUNTIME_SELECTABLE + "BASELINE" -> motif.CachingStrategy.BASELINE + else -> motif.CachingStrategy.BASELINE // Unknown strategies default to BASELINE + } + } + companion object { private const val OBJECTS_FIELD_NAME = "objects" private const val DEPENDENCIES_FIELD_NAME = "dependencies" - private const val SCOPE_ANNOTATION_FIELD_USE_NULL = "useNullFieldInitialization" + private const val SCOPE_ANNOTATION_FIELD_CACHING_STRATEGY = "cachingStrategy" fun create(env: XProcessingEnv, graph: ResolvedGraph): List = ScopeImplFactory(env, graph).create() diff --git a/compiler/src/test/java/motif/compiler/BaselineCodegenTest.kt b/compiler/src/test/java/motif/compiler/BaselineCodegenTest.kt new file mode 100644 index 00000000..e9663ceb --- /dev/null +++ b/compiler/src/test/java/motif/compiler/BaselineCodegenTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package motif.compiler + +import androidx.room.compiler.processing.ExperimentalProcessingApi +import androidx.room.compiler.processing.util.Source +import androidx.room.compiler.processing.util.compiler.TestCompilationArguments +import androidx.room.compiler.processing.util.compiler.compile +import com.google.common.truth.Truth +import dagger.internal.codegen.ComponentProcessor +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files +import javax.tools.Diagnostic +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Verifies that BASELINE generates byte-for-byte identical code to pre-change output. BASELINE is + * the control arm of the caching-strategy A/B test. + * + * On mismatch the expected file is rewritten; verify the change is intended, then commit it. + */ +@RunWith(Parameterized::class) +@ExperimentalProcessingApi +class BaselineCodegenTest( + private val caseName: String, + private val mode: OutputMode, +) { + + companion object { + private val SOURCE_ROOT = File("../tests/src/main/java") + + // Expected files under baseline_codegen/ were captured once from the pre-change main branch. + private val EXPECTED_ROOT = File("src/test/resources/baseline_codegen") + + // Covers distinct BASELINE code shapes: no cache, cached dep, + // @DoNotCache, spread, child scopes, interface Objects, static Objects methods. + private val CASES = + listOf( + "T003_multiple_dependencies", + "T009_dependency_cache", + "T010_dependency_cache_donotcache", + "T017_spread", + "T024_child", + "T021_objects_interface", + "T044_static_objects_method", + ) + + @JvmStatic + @Parameterized.Parameters(name = "{0}_{1}") + fun data(): Collection> = + CASES.flatMap { case -> OutputMode.values().map { mode -> arrayOf(case, mode) } } + } + + @Test + fun baselineCodegenMatchesExpected() { + // given: a scope compiled with the default BASELINE strategy + val generated = generateScopeImpls(caseName, mode) + Truth.assertWithMessage("No ScopeImpl generated for $caseName ($mode)") + .that(generated) + .isNotEmpty() + + // when: comparing generated output against expected files + val expectedDir = File(EXPECTED_ROOT, "$caseName/${mode.name}") + val mismatches = mutableListOf() + generated.forEach { (fileName, actual) -> + val expectedFile = File(expectedDir, "$fileName.txt") + val expected = + if (expectedFile.exists()) { + com.google.common.io.Files.asCharSource(expectedFile, Charset.defaultCharset()).read() + } else { + "" + } + if (expected != actual) { + expectedFile.parentFile.mkdirs() + expectedFile.writeText(actual) + mismatches += "$caseName/${mode.name}/$fileName" + } + } + + // then: no drift from pre-change output + if (mismatches.isNotEmpty()) { + Truth.assertWithMessage( + """ + BASELINE generated code changed for: ${mismatches.joinToString()}. + BASELINE is the control arm and must match pre-change output. + 1. Verify the change is intended (this often signals accidental drift). + 2. Commit the updated expected file(s). + """ + .trimIndent(), + ) + .fail() + } + } + + private fun generateScopeImpls(caseName: String, mode: OutputMode): Map { + val caseDir = File(SOURCE_ROOT, "testcases/$caseName") + val sources = caseDir.asSources() + val workingDir = Files.createTempDirectory("baseline-codegen-$caseName-$mode").toFile() + val result = + compile( + workingDir = workingDir, + arguments = + TestCompilationArguments( + sources = sources, + classpath = emptyList(), + inheritClasspath = true, + kaptProcessors = listOf(Processor(), ComponentProcessor()), + kotlincArguments = listOf("-language-version", "1.9", "-api-version", "1.9"), + processorOptions = mapOf("motif.mode" to mode.name.lowercase()), + ), + ) + if (!result.success || result.diagnostics[Diagnostic.Kind.ERROR].orEmpty().isNotEmpty()) { + val messages = + result.diagnostics[Diagnostic.Kind.ERROR].orEmpty().joinToString("\n") { it.msg } + Truth.assertWithMessage("Compilation failed for $caseName ($mode):\n$messages").fail() + } + val ext = if (mode == OutputMode.JAVA) "java" else "kt" + return workingDir + .walkTopDown() + .filter { it.isFile && it.name.endsWith("Impl.$ext") } + .associate { it.name to it.readText() } + .toSortedMap() + } + + private fun File.asSources(): List = + walkTopDown() + .filter { + !it.isDirectory && + it.extension in setOf("kt", "java") && + it.name != "ScopeImpl.java" && + it.name != "Test.java" + } + .mapNotNull { file -> + val relPath = file.relativeTo(SOURCE_ROOT).toString() + val qName = relPath.substringBeforeLast(".").replace('/', '.') + when (file.extension) { + "java" -> Source.loadJavaSource(file, qName) + "kt" -> Source.loadKotlinSource(file, relPath) + else -> null + } + } + .toList() +} diff --git a/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..362fcca2 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/JAVA/ScopeImpl.java.txt @@ -0,0 +1,62 @@ +package testcases.T003_multiple_dependencies; + +import java.lang.Integer; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object string = None.NONE; + + private volatile Object integer = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String string() { + return string2(); + } + + Scope scope() { + return this; + } + + String string2() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = objects.string(integer());} + } + } + return (String) string; + } + + Integer integer() { + if (integer == None.NONE) { + synchronized (this) { + if (integer == None.NONE) { + integer = objects.integer();} + } + } + return (Integer) integer; + } + + private static class Objects extends Scope.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..6ef750e4 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T003_multiple_dependencies/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,56 @@ +package testcases.T003_multiple_dependencies + +import kotlin.Any +import kotlin.Int +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + @Volatile + private var integer: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun string(): String = string2() + + public fun scope(): Scope = this + + internal fun string2(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=objects.string(integer()) + } + } + } + return ( string as String ) + } + + internal fun integer(): Int { + if (integer == None.NONE) { + synchronized (this) { + if (integer == None.NONE) { + integer=objects.integer() + } + } + } + return ( integer as Int ) + } + + private class Objects : Scope.Objects() +} diff --git a/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..fec0d015 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/JAVA/ScopeImpl.java.txt @@ -0,0 +1,73 @@ +package testcases.T009_dependency_cache; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object string = None.NONE; + + private volatile Object aString = None.NONE; + + private volatile Object iString = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String string() { + return string2(); + } + + Scope scope() { + return this; + } + + String string2() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = objects.string(aString(), iString());} + } + } + return (String) string; + } + + String aString() { + if (aString == None.NONE) { + synchronized (this) { + if (aString == None.NONE) { + aString = objects.a(iString(), iString());} + } + } + return (String) aString; + } + + String iString() { + if (iString == None.NONE) { + synchronized (this) { + if (iString == None.NONE) { + iString = objects.i();} + } + } + return (String) iString; + } + + private static class Objects extends Scope.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..a30c86f5 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T009_dependency_cache/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,69 @@ +package testcases.T009_dependency_cache + +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + @Volatile + private var aString: Any = None.NONE + + @Volatile + private var iString: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun string(): String = string2() + + public fun scope(): Scope = this + + internal fun string2(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=objects.string(aString(), iString()) + } + } + } + return ( string as String ) + } + + internal fun aString(): String { + if (aString == None.NONE) { + synchronized (this) { + if (aString == None.NONE) { + aString=objects.a(iString(), iString()) + } + } + } + return ( aString as String ) + } + + internal fun iString(): String { + if (iString == None.NONE) { + synchronized (this) { + if (iString == None.NONE) { + iString=objects.i() + } + } + } + return ( iString as String ) + } + + private class Objects : Scope.Objects() +} diff --git a/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..73b9cef3 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/JAVA/ScopeImpl.java.txt @@ -0,0 +1,65 @@ +package testcases.T010_dependency_cache_donotcache; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object string = None.NONE; + + private volatile Object aString = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String string() { + return string2(); + } + + Scope scope() { + return this; + } + + String string2() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = objects.string(aString(), iString());} + } + } + return (String) string; + } + + String aString() { + if (aString == None.NONE) { + synchronized (this) { + if (aString == None.NONE) { + aString = objects.a(iString(), iString());} + } + } + return (String) aString; + } + + String iString() { + return objects.i(); + } + + private static class Objects extends Scope.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..375296f0 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T010_dependency_cache_donotcache/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,57 @@ +package testcases.T010_dependency_cache_donotcache + +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + @Volatile + private var aString: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun string(): String = string2() + + public fun scope(): Scope = this + + internal fun string2(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=objects.string(aString(), iString()) + } + } + } + return ( string as String ) + } + + internal fun aString(): String { + if (aString == None.NONE) { + synchronized (this) { + if (aString == None.NONE) { + aString=objects.a(iString(), iString()) + } + } + } + return ( aString as String ) + } + + internal fun iString(): String = objects.i() + + private class Objects : Scope.Objects() +} diff --git a/compiler/src/test/resources/baseline_codegen/T017_spread/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T017_spread/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..36043a65 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T017_spread/JAVA/ScopeImpl.java.txt @@ -0,0 +1,67 @@ +package testcases.T017_spread; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.UnsupportedOperationException; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object spreadable = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String a() { + return aString(); + } + + @Override + public String b() { + return bString(); + } + + Scope scope() { + return this; + } + + Spreadable spreadable() { + if (spreadable == None.NONE) { + synchronized (this) { + if (spreadable == None.NONE) { + spreadable = new Spreadable();} + } + } + return (Spreadable) spreadable; + } + + String aString() { + return spreadable().a(); + } + + String bString() { + return spreadable().b(); + } + + private static class Objects extends Scope.Objects { + @Override + Spreadable spreadable() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T017_spread/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T017_spread/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..c4067001 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T017_spread/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,50 @@ +package testcases.T017_spread + +import java.lang.UnsupportedOperationException +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var spreadable: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun a(): String = aString() + + public override fun b(): String = bString() + + public fun scope(): Scope = this + + internal fun spreadable(): Spreadable { + if (spreadable == None.NONE) { + synchronized (this) { + if (spreadable == None.NONE) { + spreadable=Spreadable() + } + } + } + return ( spreadable as Spreadable ) + } + + public fun aString(): String = spreadable().a() + + public fun bString(): String = spreadable().b() + + private class Objects : Scope.Objects() { + internal override fun spreadable(): Spreadable = throw UnsupportedOperationException() + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T021_objects_interface/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T021_objects_interface/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..a4f7d796 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T021_objects_interface/JAVA/ScopeImpl.java.txt @@ -0,0 +1,53 @@ +package testcases.T021_objects_interface; + +import java.lang.Object; +import java.lang.Override; +import java.lang.UnsupportedOperationException; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object dependency = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public Dependency dependency() { + return dependency2(); + } + + Scope scope() { + return this; + } + + Dependency dependency2() { + if (dependency == None.NONE) { + synchronized (this) { + if (dependency == None.NONE) { + dependency = new Dependency();} + } + } + return (Dependency) dependency; + } + + private static class Objects implements Scope.Objects { + @Override + public Dependency dependency() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T021_objects_interface/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T021_objects_interface/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..b1d0e282 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T021_objects_interface/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,43 @@ +package testcases.T021_objects_interface + +import java.lang.UnsupportedOperationException +import kotlin.Any +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var dependency: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun dependency(): Dependency = dependency2() + + public fun scope(): Scope = this + + internal fun dependency2(): Dependency { + if (dependency == None.NONE) { + synchronized (this) { + if (dependency == None.NONE) { + dependency=Dependency() + } + } + } + return ( dependency as Dependency ) + } + + private class Objects : Scope.Objects { + public override fun dependency(): Dependency = throw UnsupportedOperationException() + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ChildImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ChildImpl.java.txt new file mode 100644 index 00000000..8cd67d72 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ChildImpl.java.txt @@ -0,0 +1,53 @@ +package testcases.T024_child; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.ScopeImpl; +import motif.internal.None; + +@ScopeImpl( + children = {}, + scope = Child.class, + dependencies = ChildImpl.Dependencies.class +) +public class ChildImpl implements Child { + private final Child.Objects objects = new Objects(); + + private final Dependencies dependencies; + + private volatile Object string = None.NONE; + + public ChildImpl(Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ChildImpl() { + this(new Dependencies() {}); + } + + @Override + public String string() { + return string2(); + } + + Child child() { + return this; + } + + String string2() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = objects.string();} + } + } + return (String) string; + } + + public interface Dependencies { + } + + private static class Objects extends Child.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..3e19937d --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T024_child/JAVA/ScopeImpl.java.txt @@ -0,0 +1,55 @@ +package testcases.T024_child; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.internal.None; + +@motif.ScopeImpl( + children = Child.class, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object string = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String string() { + return string2(); + } + + @Override + public Child child() { + return new ChildImpl(new ChildImpl.Dependencies() { + }); + } + + Scope scope() { + return this; + } + + String string2() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = objects.string();} + } + } + return (String) string; + } + + private static class Objects extends Scope.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ChildImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ChildImpl.kt.txt new file mode 100644 index 00000000..47938da7 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ChildImpl.kt.txt @@ -0,0 +1,44 @@ +package testcases.T024_child + +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.ScopeImpl +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@ScopeImpl( + children = [], + scope = Child::class, + dependencies = ChildImpl.Dependencies::class, +) +public class ChildImpl( + private val dependencies: Dependencies, +) : Child { + private final val objects: Child.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + public constructor() : this(object : Dependencies {}) + + public override fun string(): String = string2() + + public fun child(): Child = this + + internal fun string2(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=objects.string() + } + } + } + return ( string as String ) + } + + public interface Dependencies + + private class Objects : Child.Objects() +} diff --git a/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..f3f10d58 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T024_child/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,44 @@ +package testcases.T024_child + +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [Child::class], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun string(): String = string2() + + public override fun child(): Child = ChildImpl(object : ChildImpl.Dependencies { + }) + + public fun scope(): Scope = this + + internal fun string2(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=objects.string() + } + } + } + return ( string as String ) + } + + private class Objects : Scope.Objects() +} diff --git a/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/JAVA/ScopeImpl.java.txt b/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/JAVA/ScopeImpl.java.txt new file mode 100644 index 00000000..db4e0a52 --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/JAVA/ScopeImpl.java.txt @@ -0,0 +1,49 @@ +package testcases.T044_static_objects_method; + +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import motif.internal.None; + +@motif.ScopeImpl( + children = {}, + scope = Scope.class, + dependencies = Scope.Dependencies.class +) +public class ScopeImpl implements Scope { + private final Scope.Objects objects = new Objects(); + + private final Scope.Dependencies dependencies; + + private volatile Object string = None.NONE; + + public ScopeImpl(Scope.Dependencies dependencies) { + this.dependencies = dependencies; + } + + public ScopeImpl() { + this(new Scope.Dependencies() {}); + } + + @Override + public String s() { + return string(); + } + + Scope scope() { + return this; + } + + String string() { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string = Scope.Objects.s();} + } + } + return (String) string; + } + + private static class Objects extends Scope.Objects { + } +} diff --git a/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/KOTLIN/ScopeImpl.kt.txt b/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/KOTLIN/ScopeImpl.kt.txt new file mode 100644 index 00000000..38b2bc3a --- /dev/null +++ b/compiler/src/test/resources/baseline_codegen/T044_static_objects_method/KOTLIN/ScopeImpl.kt.txt @@ -0,0 +1,41 @@ +package testcases.T044_static_objects_method + +import kotlin.Any +import kotlin.String +import kotlin.Suppress +import kotlin.jvm.Volatile +import motif.`internal`.None + +@Suppress("REDUNDANT_PROJECTION", "UNCHECKED_CAST") +@motif.ScopeImpl( + children = [], + scope = Scope::class, + dependencies = Scope.Dependencies::class, +) +public class ScopeImpl( + private val dependencies: Scope.Dependencies, +) : Scope { + private final val objects: Scope.Objects = Objects() + + @Volatile + private var string: Any = None.NONE + + public constructor() : this(object : Scope.Dependencies {}) + + public override fun s(): String = string() + + public fun scope(): Scope = this + + internal fun string(): String { + if (string == None.NONE) { + synchronized (this) { + if (string == None.NONE) { + string=Scope.Objects.s() + } + } + } + return ( string as String ) + } + + private class Objects : Scope.Objects() +} diff --git a/compiler/src/test/resources/default.pro b/compiler/src/test/resources/default.pro index 6df5beb3..77a8f04e 100644 --- a/compiler/src/test/resources/default.pro +++ b/compiler/src/test/resources/default.pro @@ -3,4 +3,8 @@ -dontshrink -keep class **Test { public static void run(); -} \ No newline at end of file +} + +# Keep debug information for better error messages +-keepattributes SourceFile,LineNumberTable +-dontobfuscate \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 305a24a8..fa3883b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # GROUP=com.uber.motif -VERSION_NAME=0.4.0-alpha11-SNAPHSOT +VERSION_NAME=0.4.0-alpha12-SNAPHSOT POM_DESCRIPTION=Simple DI API for Android / Java. POM_URL=https://github.com/uber/motif/ POM_SCM_URL=https://github.com/uber/motif/ @@ -36,3 +36,6 @@ android.enableJetifier=true # https://github.com/Kotlin/dokka/issues/1405 org.gradle.jvmargs=-XX:MaxMetaspaceSize=1g +android.nonTransitiveRClass=false +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 8f93028f..5f87386b 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -22,7 +22,7 @@ def versions = [ dagger: '2.47', butterknife: '10.1.0', glide: '4.9.0', - gjf: '1.8', + gjf: '1.17.0', kotlinpoet: '1.12.0', room: '2.1.0', roomCompilerProcessing: '2.7.0-alpha13', @@ -42,7 +42,7 @@ ext.deps = [ targetSdkVersion: 30, gradlePlugins: [ - android: 'com.android.tools.build:gradle:7.4.2', + android: 'com.android.tools.build:gradle:8.11.2', intellij: 'org.jetbrains.intellij:org.jetbrains.intellij.gradle.plugin:1.17.4', kotlin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", ksp: "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${versions.ksp}", diff --git a/lib/src/main/java/motif/CachingStrategy.java b/lib/src/main/java/motif/CachingStrategy.java new file mode 100644 index 00000000..d8b0abc9 --- /dev/null +++ b/lib/src/main/java/motif/CachingStrategy.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package motif; + +/** + * Defines the caching strategy used by Motif for dependency instantiation. + * + *

    The caching strategy determines how Motif generates code for cached dependencies, affecting + * both memory usage and thread synchronization patterns. + */ +public enum CachingStrategy { + /** + * BASELINE strategy (default): - Uses None.NONE sentinel pattern for uninitialized cache fields - + * Uses synchronized(this) for thread-safe lazy initialization - Matches the traditional Motif + * code generation behavior (alpha09 and earlier) - Cache fields are volatile Object initialized + * to None.NONE - Double-checked locking pattern with synchronized(this) + * + *

    This is the proven, stable strategy that maintains backward compatibility. + */ + BASELINE, + + /** + * BASELINE_WITH_LOCK_SELECTABLE strategy: - Like BASELINE but supports optional per-dependency + * locks - When MotifRuntimeConfig.usePerDependencyLock is true, uses MotifLock instances - When + * MotifRuntimeConfig.usePerDependencyLock is false, falls back to synchronized(this) - Lock + * fields are nullable and conditionally initialized at construction time - Enables runtime + * selection of locking granularity + * + *

    This strategy allows applications to choose between coarse-grained (synchronized(this)) and + * fine-grained (per-dependency locks) synchronization at runtime. + */ + BASELINE_WITH_LOCK_SELECTABLE, + + /** + * SMART_CACHE strategy: - Uses null for uninitialized cache fields (instead of None.NONE + * sentinel) - Cache fields are volatile nullable types initialized to null - Supports optional + * per-dependency locks via MotifRuntimeConfig.usePerDependencyLock - When per-dependency locks + * enabled, uses MotifLock instances - When per-dependency locks disabled, falls back to + * synchronized(this) - Double-checked locking with null check instead of None.NONE check - + * Reduces memory footprint by eliminating None.NONE sentinel objects + * + *

    This strategy is semantically equivalent to BASELINE but more memory-efficient. It uses the + * JVM's native null representation instead of sentinel objects. + */ + SMART_CACHE, + + /** + * RUNTIME_SELECTABLE strategy: - Generates both BASELINE_WITH_LOCK_SELECTABLE and SMART_CACHE + * implementations as variant classes - Creates a wrapper class that delegates to the selected + * implementation - Runtime selection via MotifRuntimeConfig.cachingStrategy - Allows switching + * between strategies without recompilation - At runtime, wrapper checks + * MotifRuntimeConfig.cachingStrategy and instantiates appropriate variant - Variant classes are + * named: ScopeImpl_BaselineSelectableLock and ScopeImpl_SmartCache - Wrapper class maintains + * original ScopeImpl name for compatibility + * + *

    This strategy provides maximum flexibility by generating both implementations and allowing + * the application to choose at runtime which caching strategy to use. The wrapper class has + * minimal overhead - just a single delegation call. + * + *

    Use this strategy when you want to: - Test different caching strategies without + * recompilation - Choose caching strategy based on device characteristics or runtime conditions - + * Provide configurable caching behavior to end users + */ + RUNTIME_SELECTABLE +} diff --git a/lib/src/main/java/motif/DoNotCache.java b/lib/src/main/java/motif/DoNotCache.java index 5b8ea8e7..cd2e79d7 100644 --- a/lib/src/main/java/motif/DoNotCache.java +++ b/lib/src/main/java/motif/DoNotCache.java @@ -15,4 +15,25 @@ */ package motif; -public @interface DoNotCache {} +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Prevents caching of a dependency in Motif scopes. + * + *

    Apply to a factory method or type to skip caching. Use onlyForSmartCacheMode = true to skip + * caching only in SMART_CACHE mode while still caching in BASELINE mode. + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface DoNotCache { + /** + * If true, only applies to SMART_CACHE mode. Dependencies will still be cached when using + * BASELINE mode. + * + *

    Default: false (applies to all caching strategies) + */ + boolean onlyForSmartCacheMode() default false; +} diff --git a/lib/src/main/java/motif/MotifLock.java b/lib/src/main/java/motif/MotifLock.java new file mode 100644 index 00000000..c8bd1084 --- /dev/null +++ b/lib/src/main/java/motif/MotifLock.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package motif; + +/** + * A lightweight lock object for per-dependency synchronization in Motif scopes. + * + *

    This class exists primarily to make lock objects identifiable in heap dumps and memory + * profilers, making it easier to analyze memory usage and contention. + * + *

    Unlike using {@code Object} or {@code Any} as lock objects, MotifLock instances have a + * distinct class name that appears in memory analysis tools, enabling developers to quickly + * identify and measure the memory impact of per-dependency locking. + * + *

    Memory Footprint: This class is intentionally minimal with no fields or methods, + * keeping its memory overhead as low as possible (just object header). + * + *

    Usage: Generated Motif code uses MotifLock instances when {@link + * MotifRuntimeConfig#usePerDependencyLock} is enabled. + */ +public final class MotifLock { + // Empty class - serves only as an identifiable lock object +} diff --git a/lib/src/main/java/motif/MotifRuntimeConfig.java b/lib/src/main/java/motif/MotifRuntimeConfig.java new file mode 100644 index 00000000..320674a5 --- /dev/null +++ b/lib/src/main/java/motif/MotifRuntimeConfig.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package motif; + +/** + * Runtime configuration for Motif code generation behavior. + * + *

    These settings control how generated Motif code behaves at runtime, particularly around + * caching strategies and synchronization patterns. + * + *

    Thread Safety: All fields are volatile to ensure visibility across threads. + * Applications should set these values once during initialization, before any Motif scopes are + * created. + */ +public final class MotifRuntimeConfig { + + /** + * The caching strategy to use for dependency instantiation. + * + *

    Default: {@link CachingStrategy#BASELINE} + * + *

    This setting only affects scopes generated with RUNTIME_SELECTABLE strategy. Scopes compiled + * with a specific strategy (BASELINE or BASELINE_WITH_LOCK_SELECTABLE) always use their compiled + * strategy regardless of this setting. + */ + public static volatile CachingStrategy cachingStrategy = CachingStrategy.BASELINE; + + /** + * Whether to use per-dependency locks instead of synchronized(this). + * + *

    Default: false (use synchronized(this)) + * + *

    When true, scopes compiled with BASELINE_WITH_LOCK_SELECTABLE will use individual MotifLock + * instances for each cached dependency, enabling finer-grained synchronization and better + * concurrency. + * + *

    When false, all scopes fall back to synchronized(this) for simplicity and lower memory + * overhead. + * + *

    Performance Considerations: + * + *

      + *
    • true = Better concurrency, higher memory usage (one lock per dependency) + *
    • false = Lower memory, coarser synchronization + *
    + */ + public static volatile boolean usePerDependencyLock = false; + + private MotifRuntimeConfig() { + // Static utility class + } +} diff --git a/lib/src/main/java/motif/Scope.java b/lib/src/main/java/motif/Scope.java index 489bf148..a3dd1ddf 100644 --- a/lib/src/main/java/motif/Scope.java +++ b/lib/src/main/java/motif/Scope.java @@ -17,8 +17,9 @@ public @interface Scope { /** - * @return on false, the field will be initialized with [None.NONE]. Otherwise, null & - * [Initialized.INITIALIZED] will be used to skip the field initialization. + * Defines the caching strategy for scoped dependencies. + * + * @return the caching strategy to use (default: BASELINE) */ - boolean useNullFieldInitialization() default false; + CachingStrategy cachingStrategy() default CachingStrategy.BASELINE; } diff --git a/models/src/main/kotlin/motif/models/FactoryMethod.kt b/models/src/main/kotlin/motif/models/FactoryMethod.kt index 4ef69ca4..dc544816 100644 --- a/models/src/main/kotlin/motif/models/FactoryMethod.kt +++ b/models/src/main/kotlin/motif/models/FactoryMethod.kt @@ -29,7 +29,13 @@ sealed class FactoryMethod(val method: IrMethod, val objects: Objects) { abstract val parameters: List val isStatic = method.isStatic() - val isCached = !method.hasAnnotation(DoNotCache::class) + val hasDoNotCache = method.hasAnnotation(DoNotCache::class) + val doNotCacheOnlyForSmartCache: Boolean = run { + val annotation = + method.annotations.find { it.matchesClass(DoNotCache::class) } ?: return@run false + annotation.annotationValueMap["onlyForSmartCacheMode"] as? Boolean ?: false + } + val isCached = !hasDoNotCache || doNotCacheOnlyForSmartCache val isExposed = method.hasAnnotation(Expose::class) val spread: Spread? = diff --git a/samples/dagger-comparison/build.gradle b/samples/dagger-comparison/build.gradle index f23c0be2..d89c4708 100644 --- a/samples/dagger-comparison/build.gradle +++ b/samples/dagger-comparison/build.gradle @@ -3,6 +3,7 @@ plugins { } android { + namespace 'motif.dagger_comparison' compileSdkVersion deps.build.compileSdkVersion buildToolsVersion deps.build.buildToolsVersion diff --git a/samples/dagger-comparison/src/main/AndroidManifest.xml b/samples/dagger-comparison/src/main/AndroidManifest.xml index 07062ae2..d0073a45 100644 --- a/samples/dagger-comparison/src/main/AndroidManifest.xml +++ b/samples/dagger-comparison/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - \ No newline at end of file + diff --git a/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/LoggedInView.java b/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/LoggedInView.java index 73ae6fe7..e467b6db 100644 --- a/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/LoggedInView.java +++ b/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/LoggedInView.java @@ -20,7 +20,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; -import motif.sample.R; +import motif.dagger_comparison.R; public class LoggedInView extends FrameLayout { diff --git a/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/RootView.java b/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/RootView.java index 5d38bc85..badc2a54 100644 --- a/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/RootView.java +++ b/samples/dagger-comparison/src/main/java/motif/daggercomparison/dagger/RootView.java @@ -20,7 +20,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; -import motif.sample.R; +import motif.dagger_comparison.R; public class RootView extends FrameLayout { diff --git a/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/LoggedInView.java b/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/LoggedInView.java index d850cb20..ed106617 100644 --- a/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/LoggedInView.java +++ b/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/LoggedInView.java @@ -20,7 +20,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; -import motif.sample.R; +import motif.dagger_comparison.R; public class LoggedInView extends FrameLayout { diff --git a/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/RootView.java b/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/RootView.java index e32695c2..ce8c2ae3 100644 --- a/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/RootView.java +++ b/samples/dagger-comparison/src/main/java/motif/daggercomparison/motif/RootView.java @@ -20,7 +20,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.FrameLayout; -import motif.sample.R; +import motif.dagger_comparison.R; public class RootView extends FrameLayout { diff --git a/samples/sample-kotlin-ksp/build.gradle b/samples/sample-kotlin-ksp/build.gradle index db4e8854..8ac09d54 100644 --- a/samples/sample-kotlin-ksp/build.gradle +++ b/samples/sample-kotlin-ksp/build.gradle @@ -9,6 +9,7 @@ kotlin { } android { + namespace 'motif.sample_kotlin_ksp' compileSdkVersion deps.build.compileSdkVersion buildToolsVersion deps.build.buildToolsVersion diff --git a/samples/sample-kotlin-ksp/src/main/AndroidManifest.xml b/samples/sample-kotlin-ksp/src/main/AndroidManifest.xml index f1f99975..ad6e936d 100644 --- a/samples/sample-kotlin-ksp/src/main/AndroidManifest.xml +++ b/samples/sample-kotlin-ksp/src/main/AndroidManifest.xml @@ -1,9 +1,8 @@ - + - + diff --git a/samples/sample-kotlin-ksp/src/main/java/motif/sample/MainActivity.kt b/samples/sample-kotlin-ksp/src/main/java/motif/sample/MainActivity.kt index 1dbeef6c..ec80b4fb 100644 --- a/samples/sample-kotlin-ksp/src/main/java/motif/sample/MainActivity.kt +++ b/samples/sample-kotlin-ksp/src/main/java/motif/sample/MainActivity.kt @@ -19,6 +19,7 @@ import android.app.Activity import android.os.Bundle import android.widget.TextView import motif.ScopeFactory +import motif.sample_kotlin_ksp.R class MainActivity : Activity() { diff --git a/samples/sample-kotlin/build.gradle b/samples/sample-kotlin/build.gradle index b41a8aa6..a838416c 100644 --- a/samples/sample-kotlin/build.gradle +++ b/samples/sample-kotlin/build.gradle @@ -9,6 +9,7 @@ kotlin { } android { + namespace 'motif.sample_kotlin' compileSdkVersion deps.build.compileSdkVersion buildToolsVersion deps.build.buildToolsVersion diff --git a/samples/sample-kotlin/src/main/AndroidManifest.xml b/samples/sample-kotlin/src/main/AndroidManifest.xml index f1f99975..ad6e936d 100644 --- a/samples/sample-kotlin/src/main/AndroidManifest.xml +++ b/samples/sample-kotlin/src/main/AndroidManifest.xml @@ -1,9 +1,8 @@ - + - + diff --git a/samples/sample-kotlin/src/main/java/motif/sample/MainActivity.kt b/samples/sample-kotlin/src/main/java/motif/sample/MainActivity.kt index f4632cfb..a5b770ff 100644 --- a/samples/sample-kotlin/src/main/java/motif/sample/MainActivity.kt +++ b/samples/sample-kotlin/src/main/java/motif/sample/MainActivity.kt @@ -19,6 +19,7 @@ import android.app.Activity import android.os.Bundle import android.widget.TextView import motif.ScopeFactory +import motif.sample_kotlin.R class MainActivity : Activity() { diff --git a/samples/sample-kotlin/src/main/java/motif/sample/MainScope.kt b/samples/sample-kotlin/src/main/java/motif/sample/MainScope.kt index 4e6b2ac2..73af5517 100644 --- a/samples/sample-kotlin/src/main/java/motif/sample/MainScope.kt +++ b/samples/sample-kotlin/src/main/java/motif/sample/MainScope.kt @@ -16,10 +16,11 @@ package motif.sample import javax.inject.Named +import motif.CachingStrategy import motif.Creatable import motif.Scope -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.SMART_CACHE) interface MainScope : Creatable { fun greeter(): Greeter diff --git a/samples/sample-kotlin/src/main/java/motif/sample/ViewRouter.kt b/samples/sample-kotlin/src/main/java/motif/sample/ViewRouter.kt new file mode 100644 index 00000000..166a7a4c --- /dev/null +++ b/samples/sample-kotlin/src/main/java/motif/sample/ViewRouter.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package motif.sample + +// Simple ViewRouter class for testing wildcard parameters +class ViewRouter diff --git a/samples/sample-lib/build.gradle b/samples/sample-lib/build.gradle index 926ca2cc..4306d54a 100644 --- a/samples/sample-lib/build.gradle +++ b/samples/sample-lib/build.gradle @@ -3,6 +3,7 @@ plugins { } android { + namespace 'motif.sample_lib' compileSdkVersion deps.build.compileSdkVersion buildToolsVersion deps.build.buildToolsVersion diff --git a/samples/sample-lib/src/main/AndroidManifest.xml b/samples/sample-lib/src/main/AndroidManifest.xml index 324fea90..10b169c7 100644 --- a/samples/sample-lib/src/main/AndroidManifest.xml +++ b/samples/sample-lib/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + - \ No newline at end of file + diff --git a/samples/sample-lib/src/main/java/motif/sample/lib/bottom_header/BottomHeaderScope.java b/samples/sample-lib/src/main/java/motif/sample/lib/bottom_header/BottomHeaderScope.java index b3b2d869..3db98c94 100644 --- a/samples/sample-lib/src/main/java/motif/sample/lib/bottom_header/BottomHeaderScope.java +++ b/samples/sample-lib/src/main/java/motif/sample/lib/bottom_header/BottomHeaderScope.java @@ -15,10 +15,11 @@ */ package motif.sample.lib.bottom_header; +import motif.CachingStrategy; import motif.Scope; import motif.sample.lib.controller.ControllerObjects; -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.SMART_CACHE) public interface BottomHeaderScope { BottomHeaderView view(); diff --git a/samples/sample/build.gradle b/samples/sample/build.gradle index 8ef8443d..e29c58ee 100644 --- a/samples/sample/build.gradle +++ b/samples/sample/build.gradle @@ -3,6 +3,7 @@ plugins { } android { + namespace 'motif.sample' compileSdkVersion deps.build.compileSdkVersion buildToolsVersion deps.build.buildToolsVersion @@ -35,7 +36,6 @@ android { } dependencies { - annotationProcessor deps.butterknifeCompiler annotationProcessor deps.glideCompiler annotationProcessor project(':compiler') implementation project(':samples-sample-lib') @@ -43,7 +43,6 @@ dependencies { implementation deps.glide implementation deps.stetho implementation deps.support.design - implementation deps.butterknife testImplementation deps.test.junit testImplementation deps.test.truth diff --git a/samples/sample/src/main/AndroidManifest.xml b/samples/sample/src/main/AndroidManifest.xml index d5ceeed6..c7609d4c 100644 --- a/samples/sample/src/main/AndroidManifest.xml +++ b/samples/sample/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - \ No newline at end of file + diff --git a/samples/sample/src/main/java/motif/sample/app/bottom_sheet/BottomSheetScope.java b/samples/sample/src/main/java/motif/sample/app/bottom_sheet/BottomSheetScope.java index 986c0699..9e82dec6 100644 --- a/samples/sample/src/main/java/motif/sample/app/bottom_sheet/BottomSheetScope.java +++ b/samples/sample/src/main/java/motif/sample/app/bottom_sheet/BottomSheetScope.java @@ -16,13 +16,14 @@ package motif.sample.app.bottom_sheet; import android.view.ViewGroup; +import motif.CachingStrategy; import motif.Scope; import motif.sample.app.photo_grid.PhotoGridScope; import motif.sample.app.photo_list.PhotoListScope; import motif.sample.lib.bottom_header.BottomHeaderScope; import motif.sample.lib.controller.ControllerObjects; -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.SMART_CACHE) public interface BottomSheetScope { BottomSheetView view(); diff --git a/samples/sample/src/main/java/motif/sample/app/photo_grid/PhotoGridScope.java b/samples/sample/src/main/java/motif/sample/app/photo_grid/PhotoGridScope.java index 88acbdbc..6b770dbe 100644 --- a/samples/sample/src/main/java/motif/sample/app/photo_grid/PhotoGridScope.java +++ b/samples/sample/src/main/java/motif/sample/app/photo_grid/PhotoGridScope.java @@ -15,13 +15,14 @@ */ package motif.sample.app.photo_grid; +import motif.CachingStrategy; import motif.Scope; import motif.sample.app.photo_grid_item.PhotoGridItemScope; import motif.sample.app.photo_grid_item.PhotoGridItemView; import motif.sample.lib.controller.ControllerObjects; import motif.sample.lib.db.Photo; -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.SMART_CACHE) public interface PhotoGridScope { PhotoGridView view(); diff --git a/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemController.java b/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemController.java index b47abca5..6c16f040 100644 --- a/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemController.java +++ b/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemController.java @@ -15,7 +15,6 @@ */ package motif.sample.app.photo_grid_item; -import butterknife.ButterKnife; import motif.sample.lib.controller.Controller; import motif.sample.lib.db.Photo; import motif.sample.lib.multiselect.MultiSelector; @@ -32,7 +31,6 @@ public class PhotoGridItemController extends Controller { this.scope = scope; this.multiSelector = multiSelector; this.photo = photo; - ButterKnife.bind(this, view); } @Override diff --git a/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemView.java b/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemView.java index cb01bc82..03f2293a 100644 --- a/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemView.java +++ b/samples/sample/src/main/java/motif/sample/app/photo_grid_item/PhotoGridItemView.java @@ -23,8 +23,6 @@ import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.Nullable; -import butterknife.BindView; -import butterknife.ButterKnife; import com.bumptech.glide.Glide; import com.jakewharton.rxbinding3.view.RxView; import io.reactivex.Observable; @@ -34,10 +32,7 @@ public class PhotoGridItemView extends FrameLayout { - @BindView(R.id.image) ImageView imageView; - - @BindView(R.id.touch) View touchView; @Nullable private View overlayView; @@ -57,7 +52,8 @@ public PhotoGridItemView(Context context, @Nullable AttributeSet attrs, int defS @Override protected void onFinishInflate() { super.onFinishInflate(); - ButterKnife.bind(this); + imageView = findViewById(R.id.image); + touchView = findViewById(R.id.touch); } Observable clicks() { diff --git a/samples/sample/src/main/java/motif/sample/app/photo_list/PhotoListScope.java b/samples/sample/src/main/java/motif/sample/app/photo_list/PhotoListScope.java index ce22f0e7..cb77e780 100644 --- a/samples/sample/src/main/java/motif/sample/app/photo_list/PhotoListScope.java +++ b/samples/sample/src/main/java/motif/sample/app/photo_list/PhotoListScope.java @@ -15,13 +15,14 @@ */ package motif.sample.app.photo_list; +import motif.CachingStrategy; import motif.Scope; import motif.sample.app.photo_list_item.PhotoListItemScope; import motif.sample.app.photo_list_item.PhotoListItemView; import motif.sample.lib.controller.ControllerObjects; import motif.sample.lib.db.Photo; -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.SMART_CACHE) public interface PhotoListScope { PhotoListView view(); diff --git a/samples/sample/src/main/java/motif/sample/app/photo_list_item/PhotoListItemView.java b/samples/sample/src/main/java/motif/sample/app/photo_list_item/PhotoListItemView.java index 8ee1af03..4e52920f 100644 --- a/samples/sample/src/main/java/motif/sample/app/photo_list_item/PhotoListItemView.java +++ b/samples/sample/src/main/java/motif/sample/app/photo_list_item/PhotoListItemView.java @@ -23,15 +23,12 @@ import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import butterknife.BindView; -import butterknife.ButterKnife; import com.bumptech.glide.Glide; import motif.sample.R; import motif.sample.lib.db.Photo; public class PhotoListItemView extends FrameLayout { - @BindView(R.id.image) ImageView imageView; public PhotoListItemView(@NonNull Context context) { @@ -54,7 +51,7 @@ public void setPhoto(Photo photo) { @Override protected void onFinishInflate() { super.onFinishInflate(); - ButterKnife.bind(this); + imageView = findViewById(R.id.image); } public static PhotoListItemView create(ViewGroup parent) { diff --git a/samples/sample/src/main/java/motif/sample/app/root/RootScope.java b/samples/sample/src/main/java/motif/sample/app/root/RootScope.java index d943ea69..f2b6d0ca 100644 --- a/samples/sample/src/main/java/motif/sample/app/root/RootScope.java +++ b/samples/sample/src/main/java/motif/sample/app/root/RootScope.java @@ -19,6 +19,7 @@ import android.content.Context; import android.view.ViewGroup; import androidx.appcompat.app.AppCompatActivity; +import motif.CachingStrategy; import motif.Expose; import motif.Scope; import motif.sample.app.bottom_sheet.BottomSheetScope; @@ -27,7 +28,7 @@ import motif.sample.lib.db.Database; import motif.sample.lib.multiselect.MultiSelector; -@Scope(useNullFieldInitialization = true) +@Scope(cachingStrategy = CachingStrategy.RUNTIME_SELECTABLE) public interface RootScope { RootView view(); diff --git a/samples/sample/src/main/res/layout/bottomsheet.xml b/samples/sample/src/main/res/layout/bottomsheet.xml index 5e96e65b..56abcc3d 100644 --- a/samples/sample/src/main/res/layout/bottomsheet.xml +++ b/samples/sample/src/main/res/layout/bottomsheet.xml @@ -2,7 +2,7 @@ { fun fooObject(): Any diff --git a/tests/src/main/java/testcases/KT008_use_null_field_init_kotlin/Test.java b/tests/src/main/java/testcases/KT008_use_smart_cache/Test.java similarity index 95% rename from tests/src/main/java/testcases/KT008_use_null_field_init_kotlin/Test.java rename to tests/src/main/java/testcases/KT008_use_smart_cache/Test.java index 1cb8bf80..2af05d30 100644 --- a/tests/src/main/java/testcases/KT008_use_null_field_init_kotlin/Test.java +++ b/tests/src/main/java/testcases/KT008_use_smart_cache/Test.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package testcases.KT008_use_null_field_init_kotlin; +package testcases.KT008_use_smart_cache; import static com.google.common.truth.Truth.assertThat; diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.java b/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.java deleted file mode 100644 index 30a1e9c3..00000000 --- a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Test.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2018-2019 Uber Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package testcases.KT009_use_null_field_concurrency_kotlin; - -import static com.google.common.truth.Truth.assertThat; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - - -public class Test { - - /** - * This tests if the ScopeImpl synchronized blocks check and assign correct values - */ - public static void run() { - Scope scope = new ScopeImpl(); - int nThreads = 2; - ExecutorService executorService = Executors.newFixedThreadPool(nThreads); - CountDownLatch getFooObjectLatch = new CountDownLatch(nThreads); - CountDownLatch end = new CountDownLatch(nThreads); - AtomicBoolean isFooObjectNull = new AtomicBoolean(false); - try { - synchronized (scope) { // blocks the synchronized in scope.fooObject - for (int i = 0; i < nThreads; i++) { - executorService.submit(() -> { - getFooObjectLatch.countDown(); - try { - Object fooObject = scope.fooObject(); - if(fooObject == null) { - isFooObjectNull.set(true); - } - } catch (Exception e) { - isFooObjectNull.set(true); - } - end.countDown(); - }); - } - getFooObjectLatch.await(1000, TimeUnit.MILLISECONDS); - } - // at this point, the two threads will compete to create the fooObject - - // Verify - if(end.await(1000, TimeUnit.MILLISECONDS)) { - assertThat(isFooObjectNull.get()).isFalse(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/FastDep.kt b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/FastDep.kt new file mode 100644 index 00000000..b21a6d9e --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/FastDep.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_smart_cache_single_lock_java + +// Dependency that initializes quickly, used to verify it is not blocked by slowDep. +class FastDep diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/GRAPH.txt b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/GRAPH.txt new file mode 100644 index 00000000..e4418c0f --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/GRAPH.txt @@ -0,0 +1,44 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- Latches ---- + [ Provided By ] + [ Consumed By ] + * Scope | Objects.slowDep(latches) + + ==== Provides ==== + + ---- FastDep | Objects.fastDep ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fastDep() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- SlowDep | Objects.slowDep ---- + [ Required ] + Latches + [ Provided By ] + [ Consumed By ] + * Scope | Scope.slowDep() + + diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Latches.kt b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Latches.kt new file mode 100644 index 00000000..edc94406 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Latches.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_smart_cache_single_lock_java + +import java.util.concurrent.CountDownLatch + +// Wraps two CountDownLatches into one type to avoid [DUPLICATED DEPENDENCIES METHOD] +// since Motif requires distinct types in Dependencies. +class Latches { + val started = CountDownLatch(1) + val release = CountDownLatch(1) +} diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Scope.kt b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Scope.kt new file mode 100644 index 00000000..66672d01 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Scope.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_smart_cache_single_lock_java + +import java.util.concurrent.TimeUnit +import motif.Creatable + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +interface Scope : Creatable { + + fun slowDep(): SlowDep + fun fastDep(): FastDep + + @motif.Objects + abstract class Objects { + + // Signals latches.started once inside the synchronized block, then blocks on + // latches.release. This lets the test deterministically wait until slowDep holds + // the lock before measuring fastDep. + fun slowDep(latches: Latches): SlowDep { + latches.started.countDown() + try { + latches.release.await(1, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + return SlowDep() + } + + fun fastDep(): FastDep { + return FastDep() + } + + } + + interface Dependencies { + fun latches(): Latches + } +} diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/SlowDep.kt b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/SlowDep.kt new file mode 100644 index 00000000..8c5d2551 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/SlowDep.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_smart_cache_single_lock_java + +// Dependency that holds a lock during initialization to test lock contention. +class SlowDep diff --git a/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Test.java b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Test.java new file mode 100644 index 00000000..9d83ca19 --- /dev/null +++ b/tests/src/main/java/testcases/KT009_use_smart_cache_single_lock_java/Test.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT009_use_smart_cache_single_lock_java; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import motif.MotifRuntimeConfig; + +public class Test { + + /** + * With usePerDependencyLock = false, all dependencies share synchronized(this), + * so a fastDep() call must block while slowDep() holds the lock. + */ + public static void run() throws Exception { + // given a scope where slowDep signals once it holds the lock, then blocks + MotifRuntimeConfig.usePerDependencyLock = false; + + Latches latches = new Latches(); + Scope scope = new ScopeImpl(() -> latches); + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + // when slowDep runs and is confirmed to hold the lock + Future slowFuture = executor.submit(scope::slowDep); + assertThat(latches.getStarted().await(1, TimeUnit.SECONDS)).isTrue(); + + // then fastDep blocks because it needs the same lock + Future fastFuture = executor.submit(scope::fastDep); + TimeoutException exception = null; + try { + fastFuture.get(100, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + exception = e; + } + assertThat(exception).isNotNull(); + + // and once the lock is released both calls complete + latches.getRelease().countDown(); + slowFuture.get(1, TimeUnit.SECONDS); + fastFuture.get(1, TimeUnit.SECONDS); + } finally { + executor.shutdown(); + } + } +} diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/FastDep.kt b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/FastDep.kt new file mode 100644 index 00000000..0dcb1ad2 --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/FastDep.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT010_use_smart_cache_multi_lock_java + +// Dependency that initializes quickly, used to verify it is not blocked by slowDep. +class FastDep diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/GRAPH.txt b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/GRAPH.txt new file mode 100644 index 00000000..e4418c0f --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/GRAPH.txt @@ -0,0 +1,44 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- Latches ---- + [ Provided By ] + [ Consumed By ] + * Scope | Objects.slowDep(latches) + + ==== Provides ==== + + ---- FastDep | Objects.fastDep ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fastDep() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- SlowDep | Objects.slowDep ---- + [ Required ] + Latches + [ Provided By ] + [ Consumed By ] + * Scope | Scope.slowDep() + + diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Latches.kt b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Latches.kt new file mode 100644 index 00000000..d534e3f4 --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Latches.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT010_use_smart_cache_multi_lock_java + +import java.util.concurrent.CountDownLatch + +// Wraps two CountDownLatches into one type to avoid [DUPLICATED DEPENDENCIES METHOD] +// since Motif requires distinct types in Dependencies. +class Latches { + val started = CountDownLatch(1) + val release = CountDownLatch(1) +} diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Scope.kt b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Scope.kt new file mode 100644 index 00000000..80850ac2 --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Scope.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT010_use_smart_cache_multi_lock_java + +import java.util.concurrent.TimeUnit +import motif.Creatable + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +interface Scope : Creatable { + + fun slowDep(): SlowDep + fun fastDep(): FastDep + + @motif.Objects + abstract class Objects { + + // Signals latches.started once holding its own lock, then blocks on + // latches.release. The test waits for started before asserting fastDep can + // proceed on a different lock. + fun slowDep(latches: Latches): SlowDep { + latches.started.countDown() + try { + latches.release.await(1, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + return SlowDep() + } + + fun fastDep(): FastDep { + return FastDep() + } + + } + + interface Dependencies { + fun latches(): Latches + } +} diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/SlowDep.kt b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/SlowDep.kt new file mode 100644 index 00000000..0ae07fa8 --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/SlowDep.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT010_use_smart_cache_multi_lock_java + +// Dependency that holds a lock during initialization to test lock contention. +class SlowDep diff --git a/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Test.java b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Test.java new file mode 100644 index 00000000..0a68a082 --- /dev/null +++ b/tests/src/main/java/testcases/KT010_use_smart_cache_multi_lock_java/Test.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT010_use_smart_cache_multi_lock_java; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import motif.MotifRuntimeConfig; + +public class Test { + + /** + * With usePerDependencyLock = true, each dependency uses its own lock, so fastDep() + * can initialize while slowDep() holds a different lock. + */ + public static void run() throws Exception { + // given a scope where slowDep signals once it holds its lock, then blocks + MotifRuntimeConfig.usePerDependencyLock = true; + + Latches latches = new Latches(); + Scope scope = new ScopeImpl(() -> latches); + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + // when slowDep runs and is confirmed to hold its lock + Future slowFuture = executor.submit(scope::slowDep); + assertThat(latches.getStarted().await(1, TimeUnit.SECONDS)).isTrue(); + + // then fastDep still completes because it uses a different lock + FastDep fastDep = executor.submit(scope::fastDep).get(1, TimeUnit.SECONDS); + assertThat(fastDep).isNotNull(); + + // cleanup: release slowDep + latches.getRelease().countDown(); + slowFuture.get(1, TimeUnit.SECONDS); + } finally { + executor.shutdown(); + } + } +} diff --git a/tests/src/main/java/testcases/T077_use_null_field_init_java/GRAPH.txt b/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/GRAPH.txt similarity index 100% rename from tests/src/main/java/testcases/T077_use_null_field_init_java/GRAPH.txt rename to tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/GRAPH.txt diff --git a/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Scope.kt b/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Scope.kt new file mode 100644 index 00000000..81914264 --- /dev/null +++ b/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Scope.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT011_baseline_with_lock_selectable_kotlin + +import motif.Creatable +import motif.CachingStrategy + +@motif.Scope(cachingStrategy = CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE) +interface Scope : Creatable { + + // Public accessor methods to test caching + fun fooString(): String + fun fooInt(): Int + fun fooObject(): Any + + @motif.Objects + abstract class Objects { + fun fooString(): String = String("foo".toCharArray()) + + fun fooInt(): Int = 42 + + fun fooObject(): Any = Any() + } + + interface Dependencies +} diff --git a/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Test.java b/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Test.java new file mode 100644 index 00000000..d76783ee --- /dev/null +++ b/tests/src/main/java/testcases/KT011_baseline_with_lock_selectable_kotlin/Test.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT011_baseline_with_lock_selectable_kotlin; + +import static com.google.common.truth.Truth.assertThat; + +import motif.MotifRuntimeConfig; + +public class Test { + + public static void run() { + // Test with per-dependency locks disabled (synchronized(this)) + MotifRuntimeConfig.usePerDependencyLock = false; + Scope scope1 = new ScopeImpl(); + + String string1a = scope1.fooString(); + String string1b = scope1.fooString(); + assertThat(string1a).isSameInstanceAs(string1b); + + Integer int1a = scope1.fooInt(); + Integer int1b = scope1.fooInt(); + assertThat(int1a).isSameInstanceAs(int1b); + + Object obj1a = scope1.fooObject(); + Object obj1b = scope1.fooObject(); + assertThat(obj1a).isSameInstanceAs(obj1b); + + // Test with per-dependency locks enabled + MotifRuntimeConfig.usePerDependencyLock = true; + Scope scope2 = new ScopeImpl(); + + String string2a = scope2.fooString(); + String string2b = scope2.fooString(); + assertThat(string2a).isSameInstanceAs(string2b); + + Integer int2a = scope2.fooInt(); + Integer int2b = scope2.fooInt(); + assertThat(int2a).isSameInstanceAs(int2b); + + Object obj2a = scope2.fooObject(); + Object obj2b = scope2.fooObject(); + assertThat(obj2a).isSameInstanceAs(obj2b); + + // Reset to default + MotifRuntimeConfig.usePerDependencyLock = false; + } +} diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/GRAPH.txt similarity index 85% rename from tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt rename to tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/GRAPH.txt index 325f6fba..5d18ddea 100644 --- a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/GRAPH.txt +++ b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/GRAPH.txt @@ -20,10 +20,15 @@ ==== Provides ==== - ---- Object | Objects.fooObject ---- + ---- int | Objects.fooInt ---- [ Required ] [ Consumed By ] - * Scope | Scope.fooObject() + * Scope | Scope.fooInt() + + ---- String | Objects.fooString ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooString() ---- Scope | implicit ---- [ Required ] diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Scope.kt similarity index 69% rename from tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt rename to tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Scope.kt index 9a0ef8f2..788ad614 100644 --- a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/Scope.kt +++ b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Scope.kt @@ -13,20 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package testcases.KT009_use_null_field_concurrency_kotlin +package testcases.KT012_runtime_selectable_kotlin import motif.Creatable +import motif.CachingStrategy -@motif.Scope(useNullFieldInitialization = true) +@motif.Scope(cachingStrategy = CachingStrategy.RUNTIME_SELECTABLE) interface Scope : Creatable { - fun fooObject(): Any + + // Public accessor methods to test caching + fun fooString(): String + fun fooInt(): Int @motif.Objects abstract class Objects { - fun fooObject(): Any { - return Any() - } + fun fooString(): String = String("foo".toCharArray()) + + fun fooInt(): Int = 42 } interface Dependencies -} \ No newline at end of file +} diff --git a/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Test.java b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Test.java new file mode 100644 index 00000000..02bcb1ee --- /dev/null +++ b/tests/src/main/java/testcases/KT012_runtime_selectable_kotlin/Test.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.KT012_runtime_selectable_kotlin; + +import static com.google.common.truth.Truth.assertThat; + +import motif.MotifRuntimeConfig; +import motif.CachingStrategy; + +public class Test { + + public static void run() { + // Save original strategy + CachingStrategy originalStrategy = MotifRuntimeConfig.cachingStrategy; + + // Test with BASELINE_WITH_LOCK_SELECTABLE strategy + MotifRuntimeConfig.cachingStrategy = CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE; + Scope scope1 = new ScopeImpl(); + + String string1a = scope1.fooString(); + String string1b = scope1.fooString(); + assertThat(string1a).isSameInstanceAs(string1b); + + Integer int1a = scope1.fooInt(); + Integer int1b = scope1.fooInt(); + assertThat(int1a).isSameInstanceAs(int1b); + + // Test with SMART_CACHE strategy + MotifRuntimeConfig.cachingStrategy = CachingStrategy.SMART_CACHE; + Scope scope2 = new ScopeImpl(); + + String string2a = scope2.fooString(); + String string2b = scope2.fooString(); + assertThat(string2a).isSameInstanceAs(string2b); + + Integer int2a = scope2.fooInt(); + Integer int2b = scope2.fooInt(); + assertThat(int2a).isSameInstanceAs(int2b); + + // Restore original strategy + MotifRuntimeConfig.cachingStrategy = originalStrategy; + } +} diff --git a/tests/src/main/java/testcases/T077_use_smart_cache_java/GRAPH.txt b/tests/src/main/java/testcases/T077_use_smart_cache_java/GRAPH.txt new file mode 100644 index 00000000..abf4850c --- /dev/null +++ b/tests/src/main/java/testcases/T077_use_smart_cache_java/GRAPH.txt @@ -0,0 +1,42 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- int | Objects.fooInt ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooInt() + + ---- Object | Objects.fooObject ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooObject() + + ---- String | Objects.fooString ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooString() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T077_use_null_field_init_java/Scope.java b/tests/src/main/java/testcases/T077_use_smart_cache_java/Scope.java similarity index 90% rename from tests/src/main/java/testcases/T077_use_null_field_init_java/Scope.java rename to tests/src/main/java/testcases/T077_use_smart_cache_java/Scope.java index 535ddd56..daa58487 100644 --- a/tests/src/main/java/testcases/T077_use_null_field_init_java/Scope.java +++ b/tests/src/main/java/testcases/T077_use_smart_cache_java/Scope.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package testcases.T077_use_null_field_init_java; +package testcases.T077_use_smart_cache_java; import motif.Creatable; -@motif.Scope(useNullFieldInitialization = true) +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) public interface Scope extends Creatable { Object fooObject(); diff --git a/tests/src/main/java/testcases/T077_use_null_field_init_java/Test.java b/tests/src/main/java/testcases/T077_use_smart_cache_java/Test.java similarity index 95% rename from tests/src/main/java/testcases/T077_use_null_field_init_java/Test.java rename to tests/src/main/java/testcases/T077_use_smart_cache_java/Test.java index 1601cd1b..76edd33f 100644 --- a/tests/src/main/java/testcases/T077_use_null_field_init_java/Test.java +++ b/tests/src/main/java/testcases/T077_use_smart_cache_java/Test.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package testcases.T077_use_null_field_init_java; +package testcases.T077_use_smart_cache_java; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java b/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java deleted file mode 100644 index 6085a7ab..00000000 --- a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Test.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2018-2019 Uber Technologies, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package testcases.T078_use_null_field_concurrency_java; - -import static com.google.common.truth.Truth.assertThat; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class Test { - - /** - * This tests if the ScopeImpl synchronized blocks check and assign correct values - */ - public static void run() { - Scope scope = new ScopeImpl(); - int nThreads = 2; - ExecutorService executorService = Executors.newFixedThreadPool(nThreads); - CountDownLatch getFooObjectLatch = new CountDownLatch(nThreads); - CountDownLatch end = new CountDownLatch(nThreads); - AtomicBoolean isFooObjectNull = new AtomicBoolean(false); - try { - synchronized (scope) { // blocks the synchronized in scope.fooObject - for (int i = 0; i < nThreads; i++) { - executorService.submit(() -> { - getFooObjectLatch.countDown(); - Object fooObject = scope.fooObject(); - if(fooObject == null) { - isFooObjectNull.set(true); - } - end.countDown(); - }); - } - getFooObjectLatch.await(1000, TimeUnit.MILLISECONDS); - } - // at this point, the two threads will compete to create the fooObject - - // Verify - if(end.await(1000, TimeUnit.MILLISECONDS)) { - assertThat(isFooObjectNull.get()).isFalse(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/FastDep.java b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/FastDep.java new file mode 100644 index 00000000..3f095cea --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/FastDep.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_smart_cache_single_lock_java; + +// Dependency that initializes quickly, used to verify it is not blocked by slowDep. +public class FastDep {} diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/GRAPH.txt b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/GRAPH.txt new file mode 100644 index 00000000..e4418c0f --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/GRAPH.txt @@ -0,0 +1,44 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- Latches ---- + [ Provided By ] + [ Consumed By ] + * Scope | Objects.slowDep(latches) + + ==== Provides ==== + + ---- FastDep | Objects.fastDep ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fastDep() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- SlowDep | Objects.slowDep ---- + [ Required ] + Latches + [ Provided By ] + [ Consumed By ] + * Scope | Scope.slowDep() + + diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Latches.java b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Latches.java new file mode 100644 index 00000000..8b18889b --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Latches.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_smart_cache_single_lock_java; + +import java.util.concurrent.CountDownLatch; + +// Wraps two CountDownLatches into one type to avoid [DUPLICATED DEPENDENCIES METHOD] +// since Motif requires distinct types in Dependencies. +public class Latches { + public final CountDownLatch started = new CountDownLatch(1); + public final CountDownLatch release = new CountDownLatch(1); +} diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Scope.java b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Scope.java new file mode 100644 index 00000000..f07b682d --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Scope.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_smart_cache_single_lock_java; + +import java.util.concurrent.TimeUnit; +import motif.Creatable; + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +public interface Scope extends Creatable { + + SlowDep slowDep(); + FastDep fastDep(); + + @motif.Objects + class Objects { + + // Signals latches.started once inside the synchronized block, then blocks on + // latches.release. This lets the test deterministically wait until slowDep holds + // the lock before measuring fastDep. + SlowDep slowDep(Latches latches) { + latches.started.countDown(); + try { + latches.release.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return new SlowDep(); + } + + FastDep fastDep() { + return new FastDep(); + } + + } + + interface Dependencies { + Latches latches(); + } +} diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/SlowDep.java b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/SlowDep.java new file mode 100644 index 00000000..98b8a610 --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/SlowDep.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_smart_cache_single_lock_java; + +// Dependency that holds a lock during initialization to test lock contention. +public class SlowDep {} diff --git a/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Test.java b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Test.java new file mode 100644 index 00000000..ca06a455 --- /dev/null +++ b/tests/src/main/java/testcases/T078_use_smart_cache_single_lock_java/Test.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T078_use_smart_cache_single_lock_java; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import motif.MotifRuntimeConfig; + +public class Test { + + /** + * With usePerDependencyLock = false, all dependencies share synchronized(this), + * so a fastDep() call must block while slowDep() holds the lock. + */ + public static void run() throws Exception { + // given a scope where slowDep signals once it holds the lock, then blocks + MotifRuntimeConfig.usePerDependencyLock = false; + + Latches latches = new Latches(); + Scope scope = new ScopeImpl(() -> latches); + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + // when slowDep runs and is confirmed to hold the lock + Future slowFuture = executor.submit(scope::slowDep); + assertThat(latches.started.await(1, TimeUnit.SECONDS)).isTrue(); + + // then fastDep blocks because it needs the same lock + Future fastFuture = executor.submit(scope::fastDep); + TimeoutException exception = null; + try { + fastFuture.get(100, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + exception = e; + } + assertThat(exception).isNotNull(); + + // and once the lock is released both calls complete + latches.release.countDown(); + slowFuture.get(1, TimeUnit.SECONDS); + fastFuture.get(1, TimeUnit.SECONDS); + } finally { + executor.shutdown(); + } + } +} diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/FastDep.java b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/FastDep.java new file mode 100644 index 00000000..5c68156a --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/FastDep.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T079_use_smart_cache_multi_lock_java; + +// Dependency that initializes quickly, used to verify it is not blocked by slowDep. +public class FastDep {} diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/GRAPH.txt b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/GRAPH.txt new file mode 100644 index 00000000..087fdbeb --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/GRAPH.txt @@ -0,0 +1,44 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- Latches ---- + [ Provided By ] + [ Consumed By ] + * Scope | Objects.slowDep1(latches) + + ==== Provides ==== + + ---- FastDep | Objects.fastDep ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fastDep() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- SlowDep1 | Objects.slowDep1 ---- + [ Required ] + Latches + [ Provided By ] + [ Consumed By ] + * Scope | Scope.slowDep1() + + diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Latches.java b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Latches.java new file mode 100644 index 00000000..bffef539 --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Latches.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T079_use_smart_cache_multi_lock_java; + +import java.util.concurrent.CountDownLatch; + +// Wraps two CountDownLatches into one type to avoid [DUPLICATED DEPENDENCIES METHOD] +// since Motif requires distinct types in Dependencies. +public class Latches { + public final CountDownLatch started = new CountDownLatch(1); + public final CountDownLatch release = new CountDownLatch(1); +} diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Scope.java b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Scope.java new file mode 100644 index 00000000..17854f93 --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Scope.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T079_use_smart_cache_multi_lock_java; + +import java.util.concurrent.TimeUnit; +import motif.Creatable; + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +public interface Scope extends Creatable { + + SlowDep1 slowDep1(); + FastDep fastDep(); + + @motif.Objects + class Objects { + + // Signals latches.started once holding its own lock, then blocks on + // latches.release. The test waits for started before asserting fastDep can + // proceed on a different lock. + SlowDep1 slowDep1(Latches latches) { + latches.started.countDown(); + try { + latches.release.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return new SlowDep1(); + } + + FastDep fastDep() { + return new FastDep(); + } + + } + + interface Dependencies { + Latches latches(); + } +} diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/SlowDep1.java b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/SlowDep1.java new file mode 100644 index 00000000..44acc02c --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/SlowDep1.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T079_use_smart_cache_multi_lock_java; + +// Dependency that holds a lock during initialization to test lock contention. +public class SlowDep1 {} diff --git a/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Test.java b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Test.java new file mode 100644 index 00000000..e1a56930 --- /dev/null +++ b/tests/src/main/java/testcases/T079_use_smart_cache_multi_lock_java/Test.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T079_use_smart_cache_multi_lock_java; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import motif.MotifRuntimeConfig; + +public class Test { + + /** + * With usePerDependencyLock = true, each dependency uses its own lock, so fastDep() + * can initialize while slowDep1() holds a different lock. + */ + public static void run() throws Exception { + // given a scope where slowDep1 signals once it holds its lock, then blocks + MotifRuntimeConfig.usePerDependencyLock = true; + + Latches latches = new Latches(); + Scope scope = new ScopeImpl(() -> latches); + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + // when slowDep1 runs and is confirmed to hold its lock + Future slowFuture = executor.submit(scope::slowDep1); + assertThat(latches.started.await(1, TimeUnit.SECONDS)).isTrue(); + + // then fastDep still completes because it uses a different lock + FastDep fastDep = executor.submit(scope::fastDep).get(1, TimeUnit.SECONDS); + assertThat(fastDep).isNotNull(); + + // cleanup: release slowDep1 + latches.release.countDown(); + slowFuture.get(1, TimeUnit.SECONDS); + } finally { + executor.shutdown(); + } + } +} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/GRAPH.txt b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/GRAPH.txt new file mode 100644 index 00000000..a2eaa66e --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/GRAPH.txt @@ -0,0 +1,80 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- Integer | Objects.intDep ---- + [ Required ] + MultiUseDep + [ Provided By ] + * Scope | Objects.multiUseDep + [ Consumed By ] + + ---- String | Objects.stringDep ---- + [ Required ] + MultiUseDep + [ Provided By ] + * Scope | Objects.multiUseDep + ExposedDep + [ Provided By ] + * Scope | Objects.exposedDep + SingleUseDep + [ Provided By ] + * Scope | Objects.singleUseDep + [ Consumed By ] + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- DeadCodeDep | Objects.deadCodeDep ---- + [ Required ] + [ Consumed By ] + + ---- DoNotCacheDep | Objects.doNotCacheDep ---- + [ Required ] + [ Consumed By ] + + ---- ExposedDep | Objects.exposedDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.stringDep(exposedDep) + + ---- MultiUseDep | Objects.multiUseDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.stringDep(multiUseDep) + * Scope | Objects.intDep(multiUseDep) + + ---- NotExposedDep | Objects.notExposedDep ---- + [ Required ] + [ Consumed By ] + + ---- PublicAccessorDep | Objects.publicAccessorDep ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.publicAccessorDep() + + ---- SingleUseDep | Objects.singleUseDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.stringDep(singleUseDep) + + diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Scope.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Scope.java new file mode 100644 index 00000000..7fa67ebb --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Scope.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache; + +import motif.Creatable; +import testcases.T080_smart_cache_selective_cache.dependency.DoNotCacheDep; +import testcases.T080_smart_cache_selective_cache.dependency.DeadCodeDep; +import testcases.T080_smart_cache_selective_cache.dependency.ExposedDep; +import testcases.T080_smart_cache_selective_cache.dependency.MultiUseDep; +import testcases.T080_smart_cache_selective_cache.dependency.NotExposedDep; +import testcases.T080_smart_cache_selective_cache.dependency.PublicAccessorDep; +import testcases.T080_smart_cache_selective_cache.dependency.SingleUseDep; + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +public interface Scope extends Creatable { + + PublicAccessorDep publicAccessorDep(); + + @motif.Objects + class Objects { + + // Single-use internal dependency (only used by consumer1) + SingleUseDep singleUseDep() { + return new SingleUseDep(); + } + + // Multiple-use dependency (used by consumer1 and consumer2) + MultiUseDep multiUseDep() { + return new MultiUseDep(); + } + + String stringDep(MultiUseDep multiUseDep, ExposedDep exposedDep, SingleUseDep singleUseDep) { + return "test"; + } + + Integer intDep(MultiUseDep multiUseDep) { + return 1; + } + + PublicAccessorDep publicAccessorDep() { + return new PublicAccessorDep(); + } + + @motif.DoNotCache(onlyForSmartCacheMode = true) + DoNotCacheDep doNotCacheDep() { + return new DoNotCacheDep(); + } + + @motif.Expose + DeadCodeDep deadCodeDep() { + return new DeadCodeDep(); + } + @motif.Expose + ExposedDep exposedDep() { + return new ExposedDep(); + } + + NotExposedDep notExposedDep() { + return new NotExposedDep(); + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Test.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Test.java new file mode 100644 index 00000000..4853b2ae --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/Test.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache; + +import static com.google.common.truth.Truth.assertThat; + + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class Test { + + public static void run() throws NoSuchFieldException { + Scope scope = new ScopeImpl(); + + // Case Dependency with public accessor 2: Cached + verifyDeclareFieldVolatile("publicAccessorDep"); + + // Case Dependency with @DoNotCache: Not Cached + verifyDeclareFieldDoesNotExist("doNotCacheDep"); + + // Case Dead Code: Not Cached + verifyDeclareFieldDoesNotExist("deadCodeDep"); + + // Case @Exposed: Cached + verifyDeclareFieldVolatile("exposedDep"); + + // Case Single Usage: Not Cached + verifyDeclareFieldDoesNotExist("singleUseDep"); + + // Case Multiple Use: Cached + verifyDeclareFieldVolatile("multiUseDep"); + } + + private static void verifyDeclareFieldVolatile(String fieldName) { + Field field = null; + try { + field = ScopeImpl.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + assertThat("").containsMatch("Field '" + fieldName + "' not found in ScopeImpl"); + } + boolean isVolatile = Modifier.isVolatile(field.getModifiers()); + assertThat(isVolatile).isTrue(); + } + + private static void verifyDeclareFieldDoesNotExist(String fieldName) { + try { + ScopeImpl.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return; + } + assertThat("").contains("Field '" + fieldName + "' should not be declared in ScopeImpl"); + } +} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DeadCodeDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DeadCodeDep.java new file mode 100644 index 00000000..0b54db4c --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DeadCodeDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class DeadCodeDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DoNotCacheDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DoNotCacheDep.java new file mode 100644 index 00000000..33644985 --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/DoNotCacheDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class DoNotCacheDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/ExposedDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/ExposedDep.java new file mode 100644 index 00000000..ac4c864a --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/ExposedDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class ExposedDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/MultiUseDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/MultiUseDep.java new file mode 100644 index 00000000..47d006c0 --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/MultiUseDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class MultiUseDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/NotExposedDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/NotExposedDep.java new file mode 100644 index 00000000..1559f74b --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/NotExposedDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class NotExposedDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/PublicAccessorDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/PublicAccessorDep.java new file mode 100644 index 00000000..dddd8da9 --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/PublicAccessorDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class PublicAccessorDep {} diff --git a/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/SingleUseDep.java b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/SingleUseDep.java new file mode 100644 index 00000000..04586741 --- /dev/null +++ b/tests/src/main/java/testcases/T080_smart_cache_selective_cache/dependency/SingleUseDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T080_smart_cache_selective_cache.dependency; + +public class SingleUseDep {} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/DoNotCacheWrapper.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/DoNotCacheWrapper.java new file mode 100644 index 00000000..2278ddbc --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/DoNotCacheWrapper.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper; + +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.DoNotCacheDep; +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.ExposedDep; +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.NotExposedDep; + +public class DoNotCacheWrapper { + public final ExposedDep exposedDep; + public final NotExposedDep notExposedDep; + public final DoNotCacheDep doNotCacheDep; + + public DoNotCacheWrapper( + ExposedDep exposedDep, + NotExposedDep notExposedDep, + DoNotCacheDep doNotCacheDep) { + this.exposedDep = exposedDep; + this.notExposedDep = notExposedDep; + this.doNotCacheDep = doNotCacheDep; + } +} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/GRAPH.txt b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/GRAPH.txt new file mode 100644 index 00000000..4088a884 --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/GRAPH.txt @@ -0,0 +1,61 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- CachingStrategy | Objects.getCachingStrategy ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.getCachingStrategy() + + ---- DoNotCacheWrapper | Objects.exposedDepWrapper ---- + [ Required ] + ExposedDep + [ Provided By ] + * Scope | Objects.exposedDep + NotExposedDep + [ Provided By ] + * Scope | Objects.notExposedDep + DoNotCacheDep + [ Provided By ] + * Scope | Objects.doNotCacheDep + [ Consumed By ] + * Scope | Scope.doNotCacheWrapper() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ---- DoNotCacheDep | Objects.doNotCacheDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.exposedDepWrapper(doNotCacheDep) + + ---- ExposedDep | Objects.exposedDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.exposedDepWrapper(exposedDep) + + ---- NotExposedDep | Objects.notExposedDep ---- + [ Required ] + [ Consumed By ] + * Scope | Objects.exposedDepWrapper(notExposedDep) + + diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Scope.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Scope.java new file mode 100644 index 00000000..d0be4a9d --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Scope.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper; + +import motif.Creatable; +import motif.DoNotCache; +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.DoNotCacheDep; +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.ExposedDep; +import testcases.T081_smart_cache_do_not_cache_wrapper.dependency.NotExposedDep; + +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +public interface Scope extends Creatable { + + // Helper to verify caching strategy + motif.CachingStrategy getCachingStrategy(); + + DoNotCacheWrapper doNotCacheWrapper(); + + @motif.Objects + class Objects { + + @DoNotCache + DoNotCacheWrapper exposedDepWrapper(ExposedDep exposedDep, NotExposedDep notExposedDep, DoNotCacheDep doNotCacheDep) { + return new DoNotCacheWrapper(exposedDep, notExposedDep, doNotCacheDep); + } + + // Dependency with @DoNotCache annotation + @motif.DoNotCache + DoNotCacheDep doNotCacheDep() { + return new DoNotCacheDep(); + } + + // Dependency with @Expose annotation + // Rule 7: @Expose annotation - should be cached + @motif.Expose + ExposedDep exposedDep() { + return new ExposedDep(); + } + + NotExposedDep notExposedDep() { + return new NotExposedDep(); + } + + motif.CachingStrategy getCachingStrategy() { + return motif.CachingStrategy.SMART_CACHE; + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Test.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Test.java new file mode 100644 index 00000000..795338d7 --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/Test.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper; + +import static com.google.common.truth.Truth.assertThat; + + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import motif.CachingStrategy; + +public class Test { + + public static void run() throws NoSuchFieldException { + Scope scope = new ScopeImpl(); + + // Verify caching strategy is SMART_CACHE + assertThat(scope.getCachingStrategy()).isEqualTo(CachingStrategy.SMART_CACHE); + + // Test Rule 7: Dependency with @Expose (should be cached) + DoNotCacheWrapper wrapper1 = scope.doNotCacheWrapper(); + DoNotCacheWrapper wrapper2 = scope.doNotCacheWrapper(); + // Case DoNotExpose: No cache + assertThat(wrapper1) + .isNotSameInstanceAs(wrapper2); + verifyDeclareFieldDoesNotExist("doNotCacheWrapper"); + + // Case Exposed dependency + assertThat(wrapper1.exposedDep) + .isSameInstanceAs(wrapper2.exposedDep); + verifyDeclareFieldVolatile("exposedDep"); + + // Case Not Exposed dependency + assertThat(wrapper1.notExposedDep) + .isSameInstanceAs(wrapper2.notExposedDep); + verifyDeclareFieldVolatile("notExposedDep"); + + + // Case @DoNotCache dependency + assertThat(wrapper1.doNotCacheDep) + .isNotSameInstanceAs(wrapper2.doNotCacheDep); + verifyDeclareFieldDoesNotExist("typeDoNotCacheDep"); + + } + + private static void verifyDeclareFieldVolatile(String fieldName) { + Field field = null; + try { + field = ScopeImpl.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + assertThat("").containsMatch("Field '" + fieldName + "' not found in ScopeImpl"); + } + boolean isVolatile = Modifier.isVolatile(field.getModifiers()); + assertThat(isVolatile).isTrue(); + } + + private static void verifyDeclareFieldDoesNotExist(String fieldName) { + try { + ScopeImpl.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + return; + } + assertThat("").contains("Field '" + fieldName + "' should not be declared in ScopeImpl"); + } +} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/DoNotCacheDep.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/DoNotCacheDep.java new file mode 100644 index 00000000..2e362227 --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/DoNotCacheDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper.dependency; + +public class DoNotCacheDep {} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/ExposedDep.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/ExposedDep.java new file mode 100644 index 00000000..49c39b9c --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/ExposedDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper.dependency; + +public class ExposedDep {} diff --git a/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/NotExposedDep.java b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/NotExposedDep.java new file mode 100644 index 00000000..6f3e04ff --- /dev/null +++ b/tests/src/main/java/testcases/T081_smart_cache_do_not_cache_wrapper/dependency/NotExposedDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T081_smart_cache_do_not_cache_wrapper.dependency; + +public class NotExposedDep {} diff --git a/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/GRAPH.txt b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/GRAPH.txt new file mode 100644 index 00000000..f7be7e40 --- /dev/null +++ b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/GRAPH.txt @@ -0,0 +1,42 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- Integer | Objects.fooInt ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooInt() + + ---- Object | Objects.fooObject ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooObject() + + ---- String | Objects.fooString ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooString() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Scope.java b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Scope.java new file mode 100644 index 00000000..c5305cd8 --- /dev/null +++ b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Scope.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T082_baseline_with_lock_selectable; + +import motif.Creatable; +import motif.CachingStrategy; + +@motif.Scope(cachingStrategy = CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE) +public interface Scope extends Creatable { + + // Public accessor methods to test caching + String fooString(); + Integer fooInt(); + Object fooObject(); + + @motif.Objects + class Objects { + String fooString() { + return new String("foo"); + } + + Integer fooInt() { + return new Integer(42); + } + + Object fooObject() { + return new Object(); + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Test.java b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Test.java new file mode 100644 index 00000000..bfe7551e --- /dev/null +++ b/tests/src/main/java/testcases/T082_baseline_with_lock_selectable/Test.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T082_baseline_with_lock_selectable; + +import static com.google.common.truth.Truth.assertThat; + +import motif.MotifRuntimeConfig; + +public class Test { + + public static void run() { + // Test with per-dependency locks disabled (synchronized(this)) + MotifRuntimeConfig.usePerDependencyLock = false; + Scope scope1 = new ScopeImpl(); + + String string1a = scope1.fooString(); + String string1b = scope1.fooString(); + assertThat(string1a).isSameInstanceAs(string1b); + + Integer int1a = scope1.fooInt(); + Integer int1b = scope1.fooInt(); + assertThat(int1a).isSameInstanceAs(int1b); + + Object obj1a = scope1.fooObject(); + Object obj1b = scope1.fooObject(); + assertThat(obj1a).isSameInstanceAs(obj1b); + + // Test with per-dependency locks enabled + MotifRuntimeConfig.usePerDependencyLock = true; + Scope scope2 = new ScopeImpl(); + + String string2a = scope2.fooString(); + String string2b = scope2.fooString(); + assertThat(string2a).isSameInstanceAs(string2b); + + Integer int2a = scope2.fooInt(); + Integer int2b = scope2.fooInt(); + assertThat(int2a).isSameInstanceAs(int2b); + + Object obj2a = scope2.fooObject(); + Object obj2b = scope2.fooObject(); + assertThat(obj2a).isSameInstanceAs(obj2b); + + // Reset to default + MotifRuntimeConfig.usePerDependencyLock = false; + } +} diff --git a/tests/src/main/java/testcases/T083_runtime_selectable/GRAPH.txt b/tests/src/main/java/testcases/T083_runtime_selectable/GRAPH.txt new file mode 100644 index 00000000..652248e7 --- /dev/null +++ b/tests/src/main/java/testcases/T083_runtime_selectable/GRAPH.txt @@ -0,0 +1,37 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- Integer | Objects.fooInt ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooInt() + + ---- String | Objects.fooString ---- + [ Required ] + [ Consumed By ] + * Scope | Scope.fooString() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T083_runtime_selectable/Scope.java b/tests/src/main/java/testcases/T083_runtime_selectable/Scope.java new file mode 100644 index 00000000..474961c4 --- /dev/null +++ b/tests/src/main/java/testcases/T083_runtime_selectable/Scope.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T083_runtime_selectable; + +import motif.Creatable; +import motif.CachingStrategy; + +@motif.Scope(cachingStrategy = CachingStrategy.RUNTIME_SELECTABLE) +public interface Scope extends Creatable { + + // Public accessor methods to test caching + String fooString(); + Integer fooInt(); + + @motif.Objects + class Objects { + String fooString() { + return new String("foo"); + } + + Integer fooInt() { + return new Integer(42); + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T083_runtime_selectable/Test.java b/tests/src/main/java/testcases/T083_runtime_selectable/Test.java new file mode 100644 index 00000000..460114b6 --- /dev/null +++ b/tests/src/main/java/testcases/T083_runtime_selectable/Test.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T083_runtime_selectable; + +import static com.google.common.truth.Truth.assertThat; + +import motif.MotifRuntimeConfig; +import motif.CachingStrategy; + +public class Test { + + public static void run() { + // Save original strategy + CachingStrategy originalStrategy = MotifRuntimeConfig.cachingStrategy; + + // Test with BASELINE_WITH_LOCK_SELECTABLE strategy + MotifRuntimeConfig.cachingStrategy = CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE; + Scope scope1 = new ScopeImpl(); + + String string1a = scope1.fooString(); + String string1b = scope1.fooString(); + assertThat(string1a).isSameInstanceAs(string1b); + + Integer int1a = scope1.fooInt(); + Integer int1b = scope1.fooInt(); + assertThat(int1a).isSameInstanceAs(int1b); + + // Test with SMART_CACHE strategy + MotifRuntimeConfig.cachingStrategy = CachingStrategy.SMART_CACHE; + Scope scope2 = new ScopeImpl(); + + String string2a = scope2.fooString(); + String string2b = scope2.fooString(); + assertThat(string2a).isSameInstanceAs(string2b); + + Integer int2a = scope2.fooInt(); + Integer int2b = scope2.fooInt(); + assertThat(int2a).isSameInstanceAs(int2b); + + // Restore original strategy + MotifRuntimeConfig.cachingStrategy = originalStrategy; + } +} diff --git a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/GRAPH.txt similarity index 91% rename from tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt rename to tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/GRAPH.txt index 325f6fba..03a7231d 100644 --- a/tests/src/main/java/testcases/KT009_use_null_field_concurrency_kotlin/GRAPH.txt +++ b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/GRAPH.txt @@ -20,13 +20,13 @@ ==== Provides ==== - ---- Object | Objects.fooObject ---- + ---- Scope | implicit ---- [ Required ] [ Consumed By ] - * Scope | Scope.fooObject() - ---- Scope | implicit ---- + ---- OnlySmartCacheDep | Objects.onlySmartCacheDep ---- [ Required ] [ Consumed By ] + * Scope | Scope.onlySmartCacheDep() diff --git a/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Scope.java b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Scope.java new file mode 100644 index 00000000..3d2a05ad --- /dev/null +++ b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Scope.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T084_do_not_cache_only_smart_cache_baseline; + +import motif.Creatable; +import testcases.T084_do_not_cache_only_smart_cache_baseline.dependency.OnlySmartCacheDep; + +// Verifies that @DoNotCache(onlyForSmartCacheMode = true) still caches under BASELINE. +// It only disables caching in SMART_CACHE mode. +@motif.Scope(cachingStrategy = motif.CachingStrategy.BASELINE) +public interface Scope extends Creatable { + + OnlySmartCacheDep onlySmartCacheDep(); + + @motif.Objects + class Objects { + + @motif.DoNotCache(onlyForSmartCacheMode = true) + OnlySmartCacheDep onlySmartCacheDep() { + return new OnlySmartCacheDep(); + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Test.java b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Test.java new file mode 100644 index 00000000..0d50f72c --- /dev/null +++ b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/Test.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T084_do_not_cache_only_smart_cache_baseline; + +import static com.google.common.truth.Truth.assertThat; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class Test { + + public static void run() throws NoSuchFieldException { + // given a BASELINE scope with @DoNotCache(onlyForSmartCacheMode = true) + Scope scope = new ScopeImpl(); + + // when the dependency is requested twice + Object first = scope.onlySmartCacheDep(); + Object second = scope.onlySmartCacheDep(); + + // then it is cached (onlyForSmartCacheMode only disables caching in SMART_CACHE) + assertThat(first).isSameInstanceAs(second); + verifyDeclareFieldVolatile("onlySmartCacheDep"); + } + + private static void verifyDeclareFieldVolatile(String fieldName) { + Field field = null; + try { + field = ScopeImpl.class.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + assertThat("").containsMatch("Field '" + fieldName + "' not found in ScopeImpl"); + } + boolean isVolatile = Modifier.isVolatile(field.getModifiers()); + assertThat(isVolatile).isTrue(); + } +} diff --git a/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/dependency/OnlySmartCacheDep.java b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/dependency/OnlySmartCacheDep.java new file mode 100644 index 00000000..c255dae4 --- /dev/null +++ b/tests/src/main/java/testcases/T084_do_not_cache_only_smart_cache_baseline/dependency/OnlySmartCacheDep.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T084_do_not_cache_only_smart_cache_baseline.dependency; + +public class OnlySmartCacheDep {} diff --git a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java b/tests/src/main/java/testcases/T085_runtime_selectable_child/Child.java similarity index 68% rename from tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java rename to tests/src/main/java/testcases/T085_runtime_selectable_child/Child.java index 9e3c109c..eb8f08e6 100644 --- a/tests/src/main/java/testcases/T078_use_null_field_concurrency_java/Scope.java +++ b/tests/src/main/java/testcases/T085_runtime_selectable_child/Child.java @@ -13,23 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package testcases.T078_use_null_field_concurrency_java; +package testcases.T085_runtime_selectable_child; -import motif.Creatable; +import motif.Scope; -@motif.Scope(useNullFieldInitialization = true) -public interface Scope extends Creatable { +@Scope +public interface Child { - Object fooObject(); + String string(); @motif.Objects class Objects { - Object fooObject() { - return new Object(); + String string() { + return "c"; } - } - - interface Dependencies {} -} \ No newline at end of file +} diff --git a/tests/src/main/java/testcases/T085_runtime_selectable_child/GRAPH.txt b/tests/src/main/java/testcases/T085_runtime_selectable_child/GRAPH.txt new file mode 100644 index 00000000..69e159c1 --- /dev/null +++ b/tests/src/main/java/testcases/T085_runtime_selectable_child/GRAPH.txt @@ -0,0 +1,50 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- String | Objects.string ---- + [ Required ] + [ Consumed By ] + * Child | Child.string() + * Scope | Scope.string() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + ------- + | Child | + ------- + + ==== Required ==== + + ==== Provides ==== + + ---- String | Objects.string ---- + [ Required ] + [ Consumed By ] + * Child | Child.string() + + ---- Child | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T085_runtime_selectable_child/Scope.java b/tests/src/main/java/testcases/T085_runtime_selectable_child/Scope.java new file mode 100644 index 00000000..cc270d1b --- /dev/null +++ b/tests/src/main/java/testcases/T085_runtime_selectable_child/Scope.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T085_runtime_selectable_child; + +import motif.Creatable; +import motif.CachingStrategy; + +// RUNTIME_SELECTABLE parent with a child scope. Exercises the wrapper's child-method +// delegation under both the control and treatment variants. +@motif.Scope(cachingStrategy = CachingStrategy.RUNTIME_SELECTABLE) +public interface Scope extends Creatable { + + String string(); + + Child child(); + + @motif.Objects + class Objects { + + String string() { + return "p"; + } + } + + interface Dependencies {} +} diff --git a/tests/src/main/java/testcases/T085_runtime_selectable_child/Test.java b/tests/src/main/java/testcases/T085_runtime_selectable_child/Test.java new file mode 100644 index 00000000..81504e38 --- /dev/null +++ b/tests/src/main/java/testcases/T085_runtime_selectable_child/Test.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T085_runtime_selectable_child; + +import static com.google.common.truth.Truth.assertThat; + +import motif.MotifRuntimeConfig; +import motif.CachingStrategy; + +public class Test { + + public static void run() { + CachingStrategy original = MotifRuntimeConfig.cachingStrategy; + try { + // given the control strategy, the wrapper delegates parent and child methods + MotifRuntimeConfig.cachingStrategy = CachingStrategy.BASELINE_WITH_LOCK_SELECTABLE; + verifyParentAndChild(); + + // given the treatment strategy, the same delegation holds + MotifRuntimeConfig.cachingStrategy = CachingStrategy.SMART_CACHE; + verifyParentAndChild(); + } finally { + MotifRuntimeConfig.cachingStrategy = original; + } + } + + private static void verifyParentAndChild() { + // when a fresh scope is created and a child is requested + Scope scope = new ScopeImpl(); + + // then parent factory methods resolve through the delegate + assertThat(scope.string()).isEqualTo("p"); + + // and the child scope is reachable and functional + Child child = scope.child(); + assertThat(child.string()).isEqualTo("c"); + + // and each child() call returns a new child scope + assertThat(scope.child()).isNotSameInstanceAs(child); + } +} diff --git a/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/GRAPH.txt b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/GRAPH.txt new file mode 100644 index 00000000..7d452f10 --- /dev/null +++ b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/GRAPH.txt @@ -0,0 +1,39 @@ +######################################################################## +# # +# This file is auto-generated by running the Motif compiler tests and # +# serves a as validation of graph correctness. IntelliJ plugin tests # +# also rely on this file to ensure that the plugin graph understanding # +# is equivalent to the compiler's. # +# # +# - Do not edit manually. # +# - Commit changes to source control. # +# - Since this file is autogenerated, code review changes carefully to # +# ensure correctness. # +# # +######################################################################## + + ------- +| Scope | + ------- + + ==== Required ==== + + ---- AtomicInteger ---- + [ Provided By ] + [ Consumed By ] + * Scope | Objects.cachedDep(creationCount) + + ==== Provides ==== + + ---- Object | Objects.cachedDep ---- + [ Required ] + AtomicInteger + [ Provided By ] + [ Consumed By ] + * Scope | Scope.cachedDep() + + ---- Scope | implicit ---- + [ Required ] + [ Consumed By ] + + diff --git a/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Scope.java b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Scope.java new file mode 100644 index 00000000..9d6d2538 --- /dev/null +++ b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Scope.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T086_smart_cache_concurrent_contention; + +import java.util.concurrent.atomic.AtomicInteger; +import motif.Creatable; + +// Concurrency test scope: the cached factory increments a counter so the test can +// verify it runs exactly once even when many threads race to create it. +@motif.Scope(cachingStrategy = motif.CachingStrategy.SMART_CACHE) +public interface Scope extends Creatable { + + Object cachedDep(); + + @motif.Objects + class Objects { + + Object cachedDep(AtomicInteger creationCount) { + creationCount.incrementAndGet(); + return new Object(); + } + } + + interface Dependencies { + AtomicInteger creationCount(); + } +} diff --git a/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Test.java b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Test.java new file mode 100644 index 00000000..10875f05 --- /dev/null +++ b/tests/src/main/java/testcases/T086_smart_cache_concurrent_contention/Test.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package testcases.T086_smart_cache_concurrent_contention; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import motif.MotifRuntimeConfig; + +public class Test { + + private static final int THREADS = 16; + + /** + * Many threads race to read the same cached dependency. Double-checked locking must + * ensure the factory runs exactly once and every caller sees the same non-null instance. + * Runs under both single-lock and per-dependency-lock modes. + */ + public static void run() throws Exception { + boolean original = MotifRuntimeConfig.usePerDependencyLock; + try { + verifyCachedOnce(false); + verifyCachedOnce(true); + } finally { + MotifRuntimeConfig.usePerDependencyLock = original; + } + } + + private static void verifyCachedOnce(boolean usePerDependencyLock) throws Exception { + // given a fresh scope and many threads released simultaneously + MotifRuntimeConfig.usePerDependencyLock = usePerDependencyLock; + AtomicInteger creationCount = new AtomicInteger(0); + Scope scope = new ScopeImpl(() -> creationCount); + + ExecutorService executor = Executors.newFixedThreadPool(THREADS); + CyclicBarrier barrier = new CyclicBarrier(THREADS); + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < THREADS; i++) { + futures.add(executor.submit((Callable) () -> { + barrier.await(); // maximize the race window + return scope.cachedDep(); + })); + } + + // when all threads have returned + Object first = futures.get(0).get(5, TimeUnit.SECONDS); + + // then every caller got the same non-null instance + assertThat(first).isNotNull(); + for (Future future : futures) { + assertThat(future.get(5, TimeUnit.SECONDS)).isSameInstanceAs(first); + } + + // and the factory ran exactly once despite the contention + assertThat(creationCount.get()).isEqualTo(1); + } finally { + executor.shutdown(); + } + } +}