Skip to content

binding.kt

src/jvmMain/kotlin/afield/document/properties/binding.kt · 264 lines

@file:afield.runtime.completion.CompletionCategory("Internal")

package afield.document.properties

Binds a property or one-argument function to a source of values with the < operator: ::speed < { mouseX } or ::speed < sequenceOf(1.0, 2.0, 3.0).

The callable reference must be bound to a receiver implementing BoundContext; the binding is stored there under the callable's name, replacing any earlier one. Each BoundContext.bindNow pulls the next value from every source, type-checks it against the property or parameter type, and applies it only when it differs from the last value applied. A sequence that runs out removes its binding; clear() on the reference removes it by hand.

Nothing in src/ implements BoundContext or calls bindNow yet, so this is the mechanism without a host.

import afield.data.collections.get
import afield.data.collections.times
import afield.document.graph.minusAssign
import afield.document.graph.plusAssign
import afield.document.workspace.file
import afield.graphics.render.rangeTo
import afield.math.autodiff.div
import afield.math.autodiff.minus
import afield.math.autodiff.plus
import afield.math.autodiff.times
import afield.math.linalg.remAssign
import afield.math.linalg.times
import afield.math.units.div
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times

import afield.audio.synth.*
import afield.data.collections.invoke
import afield.data.collections.m
import afield.graphics.render.label
import afield.graphics.shader.callable
import afield.graphics.vulkan.buffers.add
import afield.math.linalg.invoke
import afield.math.linalg.set
import kotlin.reflect.KClass
import kotlin.reflect.KFunction1
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubtypeOf

fun boundReceiverForCallable(k: Any): Any?

fun boundReceiverForCallable(k: Any): Any? {
    val m = kotlin.jvm.internal.CallableReference::class.java.getDeclaredMethod("getBoundReceiver")
    m.trySetAccessible()
    return m.invoke(k)
}

interface BoundContext

val bindings: MutableMap<String, BoundBinding>

val previousValues: MutableMap<String, Any?>

fun bindNow()

    fun bindNow() {
        if (bindings.isEmpty()) return

        val toRemove = mutableListOf<String>()
        val entries = bindings.toList()
        entries.forEach { (k, v) ->
            when (val next = v.source.next()) {
                BoundNext.Done -> {
                    toRemove.add(k)
                    previousValues.remove(k)
                }
                is BoundNext.Value -> {
                    val a = next.value
                    val hasPrev = previousValues.containsKey(k)
                    val b = previousValues[k]
                    if (!hasPrev || a != b) {
                        val checked = v.check(a)
                        v.apply(checked)
                        previousValues[k] = a
                    }
                }
            }
        }
        toRemove.forEach { bindings.remove(it) }
    }

fun clearBinding(name: String): Boolean

    fun clearBinding(name: String): Boolean {
        previousValues.remove(name)
        return bindings.remove(name) != null
    }
}

sealed interface BoundNext

data class Value(val value: Any?) : BoundNext

object Done : BoundNext

sealed interface BoundSource

fun next(): BoundNext

class FunctionSource(private val fn: () -> Any?) : BoundSource {

override fun next(): BoundNext = BoundNext.Value(fn())

class SequenceSource(private val iterator: Iterator<Any?>) : BoundSource

override fun next(): BoundNext

    override fun next(): BoundNext = if (iterator.hasNext()) {
        BoundNext.Value(iterator.next())
    } else {
        BoundNext.Done
    }
}

data class BoundBinding

body — 86 lines, from line 114
data class BoundBinding(
    val source: BoundSource,
    val check: (Any?) -> Any?,
    val apply: (Any?) -> Unit
)

private fun boundContextFor(callable: Any): BoundContext {
    return boundReceiverForCallable(callable) as? BoundContext
        ?: error("Callable must be bound to a BoundContext receiver")
}

private fun BoundContext.registerBinding(
    name: String,
    source: BoundSource,
    check: (Any?) -> Any?,
    apply: (Any?) -> Unit
): Int {
    val replaced = bindings.put(name, BoundBinding(source, check, apply)) != null
    previousValues.remove(name)
    return if (replaced) 1 else 0
}

private fun typeMismatch(label: String, expected: KType, value: Any?): Nothing {
    val actualType = value?.let { it::class.qualifiedName } ?: "null"
    val actualValue = safeToString(value)
    throw IllegalArgumentException(
        "Binding $label expected ${expected} but got $actualType = $actualValue"
    )
}

private fun typeChecker(label: String, expected: KType): (Any?) -> Any? {
    val nullable = expected.isMarkedNullable
    val expectedClass = expected.classifier as? KClass<*>
    if (expectedClass == Any::class && nullable) {
        return { it }
    }
    if (expectedClass != null) {
        return { value ->
            if (value == null) {
                if (!nullable) typeMismatch(label, expected, value)
                null
            } else {
                if (!expectedClass.isInstance(value)) {
                    typeMismatch(label, expected, value)
                }
                value
            }
        }
    }
    return { value ->
        if (value == null) {
            if (!nullable) typeMismatch(label, expected, value)
            null
        } else {
            val actualType = value::class.createType()
            if (!actualType.isSubtypeOf(expected)) {
                typeMismatch(label, expected, value)
            }
            value
        }
    }
}

private fun KFunction1<*, *>.valueParameterType(): KType {
    val param = this.parameters.firstOrNull { it.kind == KParameter.Kind.VALUE }
        ?: error("Function ${this.name} has no value parameter")
    return param.type
}

private fun safeToString(v: Any?): String = when (v) {
    null -> "null"
    is String -> "\"$v\""
    is Char -> "'$v'"
    is Boolean, is Number -> v.toString()
    is Array<*> -> v.contentDeepToString()
    is ByteArray -> v.contentToString()
    is ShortArray -> v.contentToString()
    is IntArray -> v.contentToString()
    is LongArray -> v.contentToString()
    is FloatArray -> v.contentToString()
    is DoubleArray -> v.contentToString()
    is CharArray -> v.contentToString()
    is BooleanArray -> v.contentToString()
    else -> runCatching { v.toString() }
        .getOrElse { "<toString failed: ${it::class.java.simpleName}: ${it.message}>" }
}

operator fun <A, B> KFunction1<A, B>.compareTo(other: () -> A): Int {

operator fun <A, B> KFunction1<A, B>.compareTo(other: () -> A): Int {
    val b = boundContextFor(this)
    val paramType = valueParameterType()
    val check = typeChecker("function '${this.name}'", paramType)
    return b.registerBinding(
        this.name,
        FunctionSource(other),
        check
    ) { value ->
        @Suppress("UNCHECKED_CAST")
        this.invoke(value as A)
    }
}

operator fun <A, B> KFunction1<A, B>.compareTo(other: Sequence<A>): Int

operator fun <A, B> KFunction1<A, B>.compareTo(other: Sequence<A>): Int {
    val b = boundContextFor(this)
    val paramType = valueParameterType()
    val check = typeChecker("function '${this.name}'", paramType)
    return b.registerBinding(
        this.name,
        SequenceSource(other.iterator()),
        check
    ) { value ->
        @Suppress("UNCHECKED_CAST")
        this.invoke(value as A)
    }
}

operator fun <T> KMutableProperty0<T>.compareTo(other: () -> T): Int {

operator fun <T> KMutableProperty0<T>.compareTo(other: () -> T): Int {
    val b = boundContextFor(this)
    val check = typeChecker("property '${this.name}'", this.returnType)
    return b.registerBinding(
        this.name,
        FunctionSource(other),
        check
    ) { value ->
        @Suppress("UNCHECKED_CAST")
        this.set(value as T)
    }
}

operator fun <T> KMutableProperty0<T>.compareTo(other: Sequence<T>): Int

operator fun <T> KMutableProperty0<T>.compareTo(other: Sequence<T>): Int {
    val b = boundContextFor(this)
    val check = typeChecker("property '${this.name}'", this.returnType)
    return b.registerBinding(
        this.name,
        SequenceSource(other.iterator()),
        check
    ) { value ->
        @Suppress("UNCHECKED_CAST")
        this.set(value as T)
    }
}

fun KFunction1<*, *>.clear(): Boolean

fun KFunction1<*, *>.clear(): Boolean {
    val b = boundContextFor(this)
    return b.clearBinding(this.name)
}

fun KMutableProperty0<*>.clear(): Boolean

fun KMutableProperty0<*>.clear(): Boolean {
    val b = boundContextFor(this)
    return b.clearBinding(this.name)
}