Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding support for context receivers in PropertySpec #1244

Closed
seriouslyhypersonic opened this issue Apr 28, 2022 · 14 comments
Closed
Labels

Comments

@seriouslyhypersonic
Copy link
Contributor

I opened this regarding support for context receivers yesterday and received the reply that they got supported with #1232. I checked out 1.12.0-SNAPSHOT but it seems there is currently no support for context receivers on PropertySpec, which is exactly the use case I have for the library I am writing 😅. Please consider adding support for this use case.

@ZacSweers
Copy link
Collaborator

The spec says

Property getters and setters

Which are implemented as FunSpecs on PropertySpec.

@seriouslyhypersonic
Copy link
Contributor Author

Makes sense of course! I was just too naïve and thought they would be implemented as another property of the property spec builder. Silly me.

@seriouslyhypersonic
Copy link
Contributor Author

Sorry to reopen, but I tried adding context receivers via getters/setters and it doesn't seem to work properly. The context is declared on the actual getter/setter and not on the property. Example:

PropertySpec
    .builder(name = "loudGreeting", String::class.asTypeName())
    .getter(
        FunSpec
            .getterBuilder()
            .contextReceivers(String::class.asTypeName())
            .addStatement("return \"Hello, \${uppercase()}\"")
            .build()
    )
    .build()

Yields:

public val loudGreeting: String
      context(String) // Error
      get() = "Hello, ${uppercase()}"

While I believe it should yield this instead:

context(String)
public val loudGreeting: String
      get() = "Hello, ${uppercase()}"

This is a very simple example which could surely have been simply solved as an extension property on String. My real use case is more complex because the property I am declaring is already an extension, but that extension must only be available in a specific context (with more than one receiver).

In fact, I believe the context declaration (if present) must be the first statement on the property declaration (including annotations). For example, lets assume we have:

annotation class MyFancyAnnotation

This:

PropertySpec
    .builder(name = "loudGreeting", String::class.asTypeName())
    .addAnnotation(MyFancyAnnotation::class.asTypeName())
    .getter(/*...*/)
    .build

Should yield:

context(String)
@MyFancyAnnotation
public val loudGreeting: String
    get() = "Hello, ${uppercase()}"

@ZacSweers
Copy link
Collaborator

Can you explain what you mean by "doesn't work properly"? I understand you expect it to be on the property itself and not the functions, but is that just a syntax/style thing or is it functionally incorrect to place them on the getter/setter methods? Please be specific and link specs as I could not find anything in the corresponding KEEP that indicated otherwise

@seriouslyhypersonic
Copy link
Contributor Author

The generated code is invalid. The context declaration must come first (as per the examples given). Also, if a property is annotated, declaring the context after the annotation is also invalid.

When you mentioned setting context receivers using the function spec for getters and setters, I though KotlinPoet would check all the declared type names and compute the actual context for the property as the union of all those types names and add it as the first statement for the property declaration. So if you did something like:

PropertySpec
    .builder(name = "someProperty", ReturnType::class.asTypeName())
    .receiver(TypeA::class.asTypeName())
    .getter(
        FunSpec
            .getterBuilder()
            .contextReceivers(TypeB::class.asTypeName())
            .addStatement(\*...*\)
            .build()
    )
    .setter(
          FunSpec
              .setterBuilder()
              .contextReceivers(TypeC::class.asTypeName())
              .addStatement(\*...*\)
              .build()
     )
     .build()

KotlinPoet would generate something like:

context(TypeB, TypeC)
var TypeA.someProperty: ReturnType
   get {\*...*\}
   set {\*...*\}

Anyways, from a language point of view, I don't think it would make much sense to be able to set the context for setter and getter separately. That would mean that the same symbol would have different access levels depending on the context. Also, it seems reasonable that the language would enforce the context declaration as the first statement (including annotations) since that defines the scope in which the symbol definition below is available. Unfortunately I don't have any relevant links to the corresponding KEEP, but you can check that the generated code won't compile.

@ZacSweers
Copy link
Collaborator

Can you paste an example snippet with a compiler failure stacktrace?

@seriouslyhypersonic
Copy link
Contributor Author

Sure, no prob.

This

PropertySpec
    .builder(name = "greeting", String::class.asTypeName())
    .getter(
        FunSpec.getterBuilder()
            .contextReceivers(String::class.asTypeName())
            .addStatement("return \"Hello, \${uppercase()}!\"")
            .build()
    )
    .build()

generates this:

public val greeting: String
    context(String)
    get() = "Hello, ${uppercase()}!"

and the compiler is sad because:

Property must be initialized (on val line)
Expecting a top level declaration (on getter line)

I filtered out the stack trace because right now the error messages regarding context receivers are still a bit cryptic.

@ZacSweers
Copy link
Collaborator

Please paste the full stacktrace. The one you quoted doesn't make much sense

@seriouslyhypersonic
Copy link
Contributor Author

seriouslyhypersonic commented Apr 29, 2022

It actually makes total sense. The context keyword expects a symbol definition therefore:

  1. The compiler complains on the val line that the property must be initialized because the line below is a context declaration. At this point the compiler is expecting a symbol definition and knows for sure that the greeting property has already been fully declared. That's a problem because it was not properly initialized.
  2. On the line below the context statement, the compiler is expecting either an annotation (as part of the symbol definition) or a symbol definition but instead it gets a getter. Of course, it complains because there is no top level declaration it can associate that getter with.

I can post the full stack trace, but you won't get anything more useful out of it. All the relevant info is already there. AndroidStudio is actually quite smart and able to give you these error messages on the appropriate lines as you type.

Maybe the more straightforward approach would be to prevent setting context receivers altogether in setters and getters specs and simply providing them at PropertySpec level.

Anyways, here you have the not-so-useful task log:

:app:kaptGenerateStubsDebugKotlin FAILED	
e:[...]DemoApp.kt: (28, 1): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 4): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 5): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 7): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 9): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 10): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 17): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 19): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 28): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 29): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 30): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 31): Expecting a top level declaration	
e:[...]DemoApp.kt: (28, 32): Expecting a top level declaration	

The even less useful full stack trace:

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:kaptGenerateStubsDebugKotlin'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:147)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:282)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:145)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:133)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
Caused by: org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
        at org.gradle.workers.internal.DefaultWorkerExecutor$WorkItemExecution.waitForCompletion(DefaultWorkerExecutor.java:339)
        at org.gradle.internal.work.DefaultAsyncWorkTracker.lambda$waitForItemsAndGatherFailures$2(DefaultAsyncWorkTracker.java:131)
        at org.gradle.internal.Factories$1.create(Factories.java:31)
        at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:341)
        at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:326)
        at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:127)
        at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:93)
        at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForAll(DefaultAsyncWorkTracker.java:79)
        at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForCompletion(DefaultAsyncWorkTracker.java:67)
        at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:250)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:227)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:210)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:193)
        at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:171)
        at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:89)
        at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:40)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:53)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:50)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:50)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:40)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:68)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:38)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:48)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:36)
        at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:41)
        at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:74)
        at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
        at org.gradle.internal.execution.steps.CreateOutputsStep.execute(CreateOutputsStep.java:51)
        at org.gradle.internal.execution.steps.CreateOutputsStep.execute(CreateOutputsStep.java:29)
        at org.gradle.internal.execution.steps.CaptureStateAfterExecutionStep.execute(CaptureStateAfterExecutionStep.java:61)
        at org.gradle.internal.execution.steps.CaptureStateAfterExecutionStep.execute(CaptureStateAfterExecutionStep.java:42)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:60)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:27)
        at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:180)
        at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)
        at org.gradle.internal.Either$Right.fold(Either.java:175)
        at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:59)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:36)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:25)
        at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:36)
        at org.gradle.internal.execution.steps.RecordOutputsStep.execute(RecordOutputsStep.java:22)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:110)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:56)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:56)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:38)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:73)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:44)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
        at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:89)
        at org.gradle.internal.execution.steps.ResolveCachingStateStep.execute(ResolveCachingStateStep.java:50)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:114)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:57)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:76)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:50)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.executeWithNoEmptySources(SkipEmptyWorkStep.java:249)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.executeWithNoEmptySources(SkipEmptyWorkStep.java:204)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:83)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:54)
        at org.gradle.internal.execution.steps.RemoveUntrackedExecutionStateStep.execute(RemoveUntrackedExecutionStateStep.java:32)
        at org.gradle.internal.execution.steps.RemoveUntrackedExecutionStateStep.execute(RemoveUntrackedExecutionStateStep.java:21)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:43)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:31)
        at org.gradle.internal.execution.steps.AssignWorkspaceStep.lambda$execute$0(AssignWorkspaceStep.java:40)
        at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:287)
        at org.gradle.internal.execution.steps.AssignWorkspaceStep.execute(AssignWorkspaceStep.java:40)
        at org.gradle.internal.execution.steps.AssignWorkspaceStep.execute(AssignWorkspaceStep.java:30)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:37)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:27)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:44)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:33)
        at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:76)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:144)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:133)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:74)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:333)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:320)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:313)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:299)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:143)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:227)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:218)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:140)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
Caused by: org.gradle.api.GradleException: Compilation error. See log for more details
        at org.jetbrains.kotlin.gradle.tasks.TasksUtilsKt.throwGradleExceptionIfError(tasksUtils.kt:22)
        at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:135)
        at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:79)
        at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
        at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
        at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
        at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:97)
        at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
        at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
        at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
        at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
        at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$2(DefaultWorkerExecutor.java:205)
        at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:187)
        at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:120)
        at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:162)
        at org.gradle.internal.Factories$1.create(Factories.java:31)
        at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:270)
        at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:119)
        at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:124)
        at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:157)
        at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:126)
        ... 2 more

@Egorand
Copy link
Collaborator

Egorand commented Apr 29, 2022

Played a bit with it and it seems that you can't add a context receiver to a property without a custom accessor - is that right?

I think the API should be similar to how we model inline properties: if context receivers are added to accessors, PropertySpec should emit them on the property and not on the accessors.

Anyways, from a language point of view, I don't think it would make much sense to be able to set the context for setter and getter separately. That would mean that the same symbol would have different access levels depending on the context.

Sorry, I don't think I understand the last sentence - can you elaborate please and provide an example?

@seriouslyhypersonic
Copy link
Contributor Author

seriouslyhypersonic commented Apr 29, 2022

Played a bit with it and it seems that you can't add a context receiver to a property without a custom accessor - is that right?

Yes, that sounds about right. It's the exact same issue we have with extension properties (no backing field). Actually, a property with a single context receiver, I believe, is exactly the same thing as an extension property.

I think the API should be similar to how we model inline properties: if context receivers are added to accessors, PropertySpec should emit them on the property and not on the accessors.

It doesn't make sense to add context receivers to accessors and the language currently forbids it.

Anyways, from a language point of view, I don't think it would make much sense to be able to set the context for setter and getter separately. That would mean that the same symbol would have different access levels depending on the context.

Sorry, I don't think I understand the last sentence - can you elaborate please and provide an example?

To elaborate on why it doesn't make sense to add context receivers to accessors consider the following (invalid) code:

// Confusing.kt

interface Water

interface Oil

var meaningOfLife: Int
    context(Water)
    get() = 42
    context(Oil)
    set(value) { field = value }

What would this mean?

  1. You can only read meaningOfLife from the Water context? But meaningOfLife is declared top-level on the var line, so why would it make sense to only read it in the context of Water?
  2. You can only set meaningOfLife from the Oil context? But then you would be able to set a property from a context (Oil) which you cannot read it from! How confusing 😅

I think this is the intended usage:

interface DogHardware {
   
    val appearance 
        get() = "look like a dog"
}

interface CatSoftware {
    val behaviour 
        get() = "pounce like a cat"
}

context(DogHardware, CatSoftware)
val description 
    get() = "I $appearance and $behaviour!"


class SomeAnimal : DogHardware, CatSoftware {
    override fun toString() = "I am a Fox: $description"
}

My suggestion for the API would be:

PropertySpec
    .builder(name = "loudGreeting", String::class.asTypeName())
    .contextReceivers(String::class.asTypeName() // how context receivers should be set for properties
    .getter(
        FunSpec
            .getterBuilder()
            .contextReceivers(String::class.asTypeName()) // forbidden
            .addStatement("return \"Hello, \${uppercase()}\"")
            .build()
    )
    .build()

@Egorand
Copy link
Collaborator

Egorand commented May 3, 2022

It doesn't make sense to add context receivers to accessors and the language currently forbids it.

I'll rephrase, my suggestion was to add context receivers to FunSpecs representing the accessors and then having the codegen logic for PropertySpec pick them up and generate correct code.

That said, I'm leaning towards actually adding the method on the PropertySpec.Builder - thanks for the extra context & your arguments make sense to me. Additionally, being able to have all context receivers declared in one spot and correctly ordered by the library consumers is better than having to collect them across FunSpecs. Optionally, we can throw an exception if at the time when PropertySpec.Builder.build() is called there are context receivers but no accessors defined.

Wanna send us a PR?

@seriouslyhypersonic
Copy link
Contributor Author

It doesn't make sense to add context receivers to accessors and the language currently forbids it.

I'll rephrase, my suggestion was to add context receivers to FunSpecs representing the accessors and then having the codegen logic for PropertySpec pick them up and generate correct code.

Oh, I see. That would indeed be an option. However, as you mentioned, having them scattered across the accessor's FunSpecs requires adding the logic to collect all context receivers which brings additional complexity. Moreover, I think one could argue that extra work is probably not justifiable since the language doesn't allow setting context receivers on a per-accessor basis and the second approach for the API would be consistent with that. Throwing the exception on missing accessors also seems a good idea.

Wanna send us a PR?

I can't really commit myself beacuse I'm quite tight on time and I also literally started using KP last week, so I am still unfamiliar. However, I can have a look at the lib code and if I am able to come up with something soon I'll send a PR.

@Egorand
Copy link
Collaborator

Egorand commented May 9, 2022

Closed by #1247

@Egorand Egorand closed this as completed May 9, 2022
@Egorand Egorand added the feature label May 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants