Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java_version: [11]
java_version: [21]
steps:
- name: Checkout
uses: actions/checkout@v3
Expand All @@ -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'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ build/
.gradle/
local.properties
.DS_Store
.claude/
.kotlin/
249 changes: 216 additions & 33 deletions compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the type is a kotlin non-nullable type

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For KotlinCodeGenerator, internally, the global property will be nullable but the return type will be non-null.

image image

}

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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<MethodSpec> {
private fun FactoryProviderMethod.specs(
isBaselineStrategy: Boolean,
perDependencyLockFields: PerDependencyLockFields?
): List<MethodSpec> {
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())
Expand All @@ -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()
Expand Down Expand Up @@ -308,10 +485,16 @@ object JavaCodeGenerator {
return CodeBlock.of("<li>{@link \$L#\$N(\$L)}</li>\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 {
Expand Down
Loading
Loading