Skip to content

Commit

Permalink
WIP - Simplify parameter logic
Browse files Browse the repository at this point in the history
  • Loading branch information
BenWoodworth committed Nov 13, 2023
1 parent 51ba855 commit a552b67
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 159 deletions.
131 changes: 35 additions & 96 deletions src/commonMain/kotlin/ParameterState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@ package com.benwoodworth.parameterize

import kotlin.reflect.KProperty

/*
* Conceptually, a parameter is a reusable source of arguments, and is in one of
* three different states internally.
/** TODO Revise
* Conceptually, a parameter is a reusable source of arguments, managing
* iterators so the arguments seamlessly loop back to the start again. This can
* be in one of
*
* Undeclared:
* Completely null state, waiting to be set up with property and arguments.
* Completely null state, waiting to be declared with a property and arguments.
*
* Parameters can be reset to this state, enabling instances to be reused in
* the future.
* the future with different properties.
*
* Declared:
* Set up with a property and arguments, but has not been used yet.
* Set up with a property and arguments, with an argument loaded in from the
* iterator and available to use.
*
* The argument and iterator are loaded lazily in case the parameter is not
* Arguments are loaded in one at a time in case the parameter is not
* actually used, saving any potentially unnecessary computation.
*
* Initialized:
* The parameter has been used, and has an argument set from the iterator.
* Stays initialized until reset.
*
* The stored argument iterator will always have a next argument. When there
* is no next argument, it will be set to null, and then lazily set to a new
* iterator again when the next argument is needed.
Expand All @@ -31,28 +29,19 @@ import kotlin.reflect.KProperty
* arguments will be assumed to be the same and ignored in favor of continuing
* through the current iterator.
*/

internal class ParameterState<@Suppress("unused") out T> internal constructor() {
internal class ParameterState {
/*
* The internal state does not use the generic type, so T is purely for
* syntax, helping Kotlin infer a property type from the parameter through
* provideDelegate. So because T is not used, it can be safely cast to a
* different generic type, despite it being an "unchecked cast".
* (e.g. the declared property's type when providing the delegate)
*
* Instead, methods have their own generic type, checked against a property
* The internal state does not use a generic type for the arguments.
* instead, methods have their own generic type, and check against a property
* that's passed in. Since the internal argument state is always of the same
* type as the currently declared property, checking that the property taken
* into the method is the same is enough to ensure that the argument
* returned is the correct type.
*/

// Declared
private var property: KProperty<*>? = null
private var arguments: Iterable<*>? = null

// Initialized
private var argument: Any? = Uninitialized // T | Uninitialized
private var argument: Any? = null // T
private var argumentIterator: Iterator<*>? = null

internal var hasBeenUsed: Boolean = false
Expand All @@ -61,23 +50,23 @@ internal class ParameterState<@Suppress("unused") out T> internal constructor()
internal fun reset() {
property = null
arguments = null
argument = null
argumentIterator = null
argument = Uninitialized
hasBeenUsed = false
}

/**
* @throws IllegalStateException if used before the argument has been initialized.
* @throws IllegalStateException if used before the argument has been declared.
*/
internal val isLastArgument: Boolean
get() {
check(argument !== Uninitialized) { "Argument has not been initialized" }
checkNotNull(property) { "Parameter has not been declared" }
return argumentIterator == null
}


/**
* Returns a string representation of the current argument, or a "not initialized" message.
* Returns a string representation of the current argument, or a "not declared" message. (TODO)
*
* Useful while debugging, e.g. inline hints that show property values:
* ```
Expand All @@ -98,40 +87,28 @@ internal class ParameterState<@Suppress("unused") out T> internal constructor()
* @throws ParameterizeContinue if [arguments] is empty.
*/
internal fun <T> declare(property: KProperty<T>, arguments: Iterable<T>) {
val declaredProperty = this.property

if (declaredProperty == null) {
initialize(arguments) // Before any state gets changed, in case arguments is empty
this.property = property
this.arguments = arguments
} else if (!property.equalsProperty(declaredProperty)) {
// Nothing to do if already declared (besides validating the property)
this.property?.let { declaredProperty ->
if (property.equalsProperty(declaredProperty)) return
throw ParameterizeException("Expected to be declaring `${declaredProperty.name}`, but got `${property.name}`")
}
}

/**
* Initialize and return the argument.
*
* @throws ParameterizeContinue if [arguments] is empty.
*/
private fun <T> initialize(arguments: Iterable<T>): T {
val iterator = arguments.iterator()
if (!iterator.hasNext()) {
throw ParameterizeContinue
}

val argument = iterator.next()
this.argument = argument

if (iterator.hasNext()) {
argumentIterator = iterator
throw ParameterizeContinue // Before changing any state
}

return argument
this.property = property
this.arguments = arguments
this.argument = iterator.next()
this.argumentIterator = iterator.takeIf { it.hasNext() }
}

/**
* Get the current argument, or initialize it from the arguments that were originally declared.
* Get the current argument, and set [hasBeenUsed] to true.
*
* @throws ParameterizeException if already declared for a different [property].
* @throws IllegalStateException if the argument has not been declared yet.
*/
internal fun <T> getArgument(property: KProperty<T>): T {
val declaredProperty = checkNotNull(this.property) {
Expand All @@ -142,49 +119,26 @@ internal class ParameterState<@Suppress("unused") out T> internal constructor()
throw ParameterizeException("Cannot use parameter delegate with `${property.name}`. Already declared for `${declaredProperty.name}`.")
}

var argument = argument
if (argument !== Uninitialized) {
@Suppress("UNCHECKED_CAST") // Argument is initialized with property's arguments, so must be T
argument as T
} else {
val arguments = checkNotNull(arguments) {
"Parameter is declared with ${property.name}, but ${::arguments.name} is null"
}

@Suppress("UNCHECKED_CAST") // The arguments are declared for property, so must be Iterable<T>
argument = initialize(arguments as Iterable<T>)
}

hasBeenUsed = true
return argument

@Suppress("UNCHECKED_CAST") // Argument is declared with property's arguments, so must be T
return argument as T
}

/**
* Iterates the parameter argument.
*
* @throws IllegalStateException if the argument has not been initialized yet.
* @throws IllegalStateException if the argument has not been declared yet.
*/
internal fun nextArgument() {
val arguments = checkNotNull(arguments) {
"Cannot iterate arguments before parameter has been declared"
}

check(argument != Uninitialized) {
"Cannot iterate arguments before parameter argument has been initialized"
}

var iterator = argumentIterator
if (iterator == null) {
iterator = arguments.iterator()

argumentIterator = iterator
}
val iterator = argumentIterator ?: arguments.iterator()

argument = iterator.next()

if (!iterator.hasNext()) {
argumentIterator = null
}
argumentIterator = iterator.takeIf { it.hasNext() }
}

/**
Expand All @@ -197,21 +151,6 @@ internal class ParameterState<@Suppress("unused") out T> internal constructor()
"Cannot get failure argument before parameter has been declared"
}

val argument = argument
check (argument !== Uninitialized) {
"Parameter delegate is declared with ${property.name}, but ${::argument.name} is uninitialized"
}

return ParameterizeFailure.Argument(property, argument)
}

/**
* A placeholder value for [argument] to signify that it is not initialized.
*
* Since the argument itself may be nullable, `null` can't be used instead,
* as that may actually be the argument's value.
*/
private object Uninitialized {
override fun toString(): String = "Parameter argument not initialized yet."
}
}
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/Parameterize.kt
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public class ParameterizeScope internal constructor(
/** @suppress */
public operator fun <T> ParameterDelegate<T>.getValue(thisRef: Any?, property: KProperty<*>): T =
@Suppress("UNCHECKED_CAST")
state.getParameterArgument(this, property as KProperty<T>)
parameterState.getArgument(property as KProperty<T>)


/** @suppress */
Expand All @@ -181,8 +181,8 @@ public class ParameterizeScope internal constructor(
)

/** @suppress */
public class ParameterDelegate<out T> internal constructor(
internal val parameterState: ParameterState<T>
public class ParameterDelegate<@Suppress("unused") out T> internal constructor(
internal val parameterState: ParameterState
) {
override fun toString(): String =
parameterState.toString()
Expand Down
32 changes: 3 additions & 29 deletions src/commonMain/kotlin/ParameterizeState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ internal class ParameterizeState {
* The true number of parameters in the current iteration is maintained in [parameterCount].
*/
private val parameters = ArrayList<ParameterDelegate<Nothing>>()
private val parametersUsed = ArrayList<ParameterDelegate<*>>()
private var parameterBeingUsed: KProperty<*>? = null

private var parameterCount = 0
private var parameterCountAfterAllUsed = 0

private var iterationCount = 0L
private var failureCount = 0L
Expand All @@ -41,8 +38,8 @@ internal class ParameterizeState {
}

fun <T> declareParameter(property: KProperty<T>, arguments: Iterable<T>): ParameterDelegate<Nothing> {
parameterBeingUsed.let {
if (it != null) throw ParameterizeException("Nesting parameters is not currently supported: `${property.name}` was declared within `${it.name}`'s arguments")
parameterBeingUsed?.let {
throw ParameterizeException("Nesting parameters is not currently supported: `${property.name}` was declared within `${it.name}`'s arguments")
}

val parameterIndex = parameterCount
Expand Down Expand Up @@ -73,23 +70,6 @@ internal class ParameterizeState {
}
}

fun <T> getParameterArgument(parameter: ParameterDelegate<*>, property: KProperty<T>): T {
val isFirstUse = !parameter.parameterState.hasBeenUsed

return parameter.parameterState.getArgument(property)
.also {
if (isFirstUse) trackUsedParameter(parameter)
}
}

private fun trackUsedParameter(parameter: ParameterDelegate<*>) {
parametersUsed += parameter

if (!parameter.parameterState.isLastArgument) {
parameterCountAfterAllUsed = parameterCount
}
}

/**
* Iterate the last parameter that has a next argument (in order of when their arguments were calculated), and reset
* all parameters that were first used after it (since they may depend on the now changed value, and may be computed
Expand All @@ -100,12 +80,7 @@ internal class ParameterizeState {
private fun nextArgumentPermutationOrFalse(): Boolean {
var iterated = false

val declaredParameterIterator = parameters
.listIterator(parameterCount)

while (declaredParameterIterator.hasPrevious()) {
val parameter = declaredParameterIterator.previous()

for (parameter in parameters.subList(0, parameterCount).asReversed()) {
if (!parameter.parameterState.isLastArgument) {
parameter.parameterState.nextArgument()
iterated = true
Expand All @@ -116,7 +91,6 @@ internal class ParameterizeState {
}

parameterCount = 0
parameterCountAfterAllUsed = 0

return iterated
}
Expand Down
8 changes: 4 additions & 4 deletions src/commonTest/kotlin/ParameterStateSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ class ParameterStateSpec {
private val property: String get() = error("${::property.name} is not meant to be used")
private val differentProperty: String get() = error("${::differentProperty.name} is not meant to be used")

private lateinit var parameter: ParameterState<String>
private lateinit var parameter: ParameterState

@BeforeTest
fun beforeTest() {
parameter = ParameterState()
}


private fun assertUndeclared(parameter: ParameterState<*>) {
private fun assertUndeclared(parameter: ParameterState) {
val failure = assertFailsWith<IllegalStateException> {
parameter.getArgument(::property)
}
Expand Down Expand Up @@ -252,7 +252,7 @@ class ParameterStateSpec {
val failure = assertFailsWith<IllegalStateException> {
parameter.isLastArgument
}
assertEquals("Argument has not been initialized", failure.message)
assertEquals("Parameter has not been declared", failure.message)
}

@Test
Expand All @@ -265,7 +265,7 @@ class ParameterStateSpec {
}

@Test
fun get_failure_argument_when_initialized_should_have_correct_property_and_argument() {
fun get_failure_argument_when_declared_should_have_correct_property_and_argument() {
val expectedArgument = "a"
parameter.declare(::property, listOf(expectedArgument))
parameter.getArgument(::property)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ class ParameterizeConfigurationOnCompleteSpec {
}
) {
val iteration by parameter(0..100)
useParameter(iteration)

expectedIterationCount++
}
Expand All @@ -102,7 +101,6 @@ class ParameterizeConfigurationOnCompleteSpec {
}
) {
val iteration by parameter(0..100)
useParameter(iteration)

expectedIterationCount++

Expand All @@ -122,7 +120,6 @@ class ParameterizeConfigurationOnCompleteSpec {
}
) {
val iteration by parameter(0..100)
useParameter(iteration)

if (iteration % 2 == 0 || iteration % 7 == 0) {
expectedFailureCount++
Expand All @@ -139,7 +136,6 @@ class ParameterizeConfigurationOnCompleteSpec {
}
) {
val iteration by parameter(0..100)
useParameter(iteration)
}
}

Expand Down
Loading

0 comments on commit a552b67

Please sign in to comment.