Optimize work with names and tokens

This commit is contained in:
Alexander Nozik 2022-08-22 14:16:16 +03:00
parent 70bd92f019
commit 233639f0b6
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
13 changed files with 72 additions and 47 deletions

View File

@ -6,7 +6,7 @@ plugins {
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.6.0-dev-14" version = "0.6.0-dev-15"
} }
subprojects { subprojects {

View File

@ -3,9 +3,8 @@ package space.kscience.dataforge.context
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.Name.Companion.parse
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.native.concurrent.ThreadLocal import kotlin.native.concurrent.ThreadLocal
@ -22,4 +21,4 @@ private object GlobalContext : Context("GLOBAL".asName(), null, emptySet(), Meta
public val Global: Context get() = GlobalContext public val Global: Context get() = GlobalContext
public fun Context(name: String? = null, block: ContextBuilder.() -> Unit = {}): Context = public fun Context(name: String? = null, block: ContextBuilder.() -> Unit = {}): Context =
Global.buildContext(name?.let(Name::parse), block) Global.buildContext(name?.parseAsName(), block)

View File

@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.misc.Named import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.provider.Provider import space.kscience.dataforge.provider.Provider
/** /**
@ -30,7 +31,7 @@ public interface Plugin : Named, ContextAware, Provider, MetaRepr {
/** /**
* The name of this plugin ignoring version and group * The name of this plugin ignoring version and group
*/ */
override val name: Name get() = Name.parse(tag.name) override val name: Name get() = tag.name.parseAsName()
/** /**
* Plugin dependencies which are required to attach this plugin. Plugin * Plugin dependencies which are required to attach this plugin. Plugin

View File

@ -3,7 +3,7 @@ package space.kscience.dataforge.properties
import space.kscience.dataforge.meta.Scheme import space.kscience.dataforge.meta.Scheme
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.startsWith import space.kscience.dataforge.names.startsWith
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
@ -18,7 +18,7 @@ public fun <S : Scheme, T : Any> S.property(property: KMutableProperty1<S, T?>):
override fun onChange(owner: Any?, callback: (T?) -> Unit) { override fun onChange(owner: Any?, callback: (T?) -> Unit) {
this@property.meta.onChange(this) { name -> this@property.meta.onChange(this) { name ->
if (name.startsWith(Name.parse(property.name))) { if (name.startsWith(property.name.parseAsName(true))) {
callback(property.get(this@property)) callback(property.get(this@property))
} }
} }

View File

@ -16,6 +16,7 @@
package space.kscience.dataforge.provider package space.kscience.dataforge.provider
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
/** /**
@ -60,9 +61,10 @@ public data class PathToken(val name: Name, val target: String? = null) {
public companion object { public companion object {
public const val TARGET_SEPARATOR: String = "::" public const val TARGET_SEPARATOR: String = "::"
public fun parse(token: String): PathToken {
public fun parse(token: String, cache: Boolean = false): PathToken {
val target = token.substringBefore(TARGET_SEPARATOR, "") val target = token.substringBefore(TARGET_SEPARATOR, "")
val name = Name.parse(token.substringAfter(TARGET_SEPARATOR)) val name = token.substringAfter(TARGET_SEPARATOR).parseAsName(cache)
if (target.contains("[")) TODO("target separators in queries are not supported") if (target.contains("[")) TODO("target separators in queries are not supported")
return PathToken(name, target) return PathToken(name, target)
} }

View File

@ -7,6 +7,7 @@ import space.kscience.dataforge.misc.DFBuilder
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.DFInternal import space.kscience.dataforge.misc.DFInternal
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -96,7 +97,7 @@ internal class ReduceAction<T : Any, R : Any>(
val groupMeta = group.meta val groupMeta = group.meta
val env = ActionEnv(Name.parse(groupName), groupMeta, meta) val env = ActionEnv(groupName.parseAsName(), groupMeta, meta)
@OptIn(DFInternal::class) val res: Data<R> = dataFlow.reduceToData( @OptIn(DFInternal::class) val res: Data<R> = dataFlow.reduceToData(
group.outputType, group.outputType,
meta = groupMeta meta = groupMeta

View File

@ -7,6 +7,7 @@ import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.toMutableMeta import space.kscience.dataforge.meta.toMutableMeta
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import kotlin.collections.set import kotlin.collections.set
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -35,7 +36,7 @@ public class SplitBuilder<T : Any, R : Any>(public val name: Name, public val me
* @param rule the rule to transform fragment name and meta using * @param rule the rule to transform fragment name and meta using
*/ */
public fun fragment(name: String, rule: FragmentRule<T, R>.() -> Unit) { public fun fragment(name: String, rule: FragmentRule<T, R>.() -> Unit) {
fragments[Name.parse(name)] = rule fragments[name.parseAsName()] = rule
} }
} }

View File

@ -6,10 +6,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.*
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.names.removeHeadOrNull
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.reflect.KType import kotlin.reflect.KType
@ -101,7 +98,7 @@ public fun <T : Any> DataSet<T>.branch(branchName: Name): DataSet<T> = if (branc
override val updates: Flow<Name> get() = this@branch.updates.mapNotNull { it.removeHeadOrNull(branchName) } override val updates: Flow<Name> get() = this@branch.updates.mapNotNull { it.removeHeadOrNull(branchName) }
} }
public fun <T : Any> DataSet<T>.branch(branchName: String): DataSet<T> = this@branch.branch(Name.parse(branchName)) public fun <T : Any> DataSet<T>.branch(branchName: String): DataSet<T> = this@branch.branch(branchName.parseAsName())
@DFExperimental @DFExperimental
public suspend fun <T : Any> DataSet<T>.rootData(): Data<T>? = get(Name.EMPTY) public suspend fun <T : Any> DataSet<T>.rootData(): Data<T>? = get(Name.EMPTY)

View File

@ -101,14 +101,12 @@ public operator fun Meta.get(token: NameToken): Meta? = items[token]
* *
* If [name] is empty return current [Meta] * If [name] is empty return current [Meta]
*/ */
public operator fun Meta.get(name: Name): Meta? = this.getMeta(name) public operator fun Meta?.get(name: Name): Meta? = this?.getMeta(name)
//TODO allow nullable receivers after Kotlin 1.7
/** /**
* Parse [Name] from [key] using full name notation and pass it to [Meta.get] * Parse [Name] from [key] using full name notation and pass it to [Meta.get]
*/ */
public operator fun Meta.get(key: String): Meta? = this[Name.parse(key)] public operator fun Meta?.get(key: String): Meta? = this?.get(key.parseAsName(true))
/** /**
* Get all items matching given name. The index of the last element, if present is used as a [Regex], * Get all items matching given name. The index of the last element, if present is used as a [Regex],
@ -134,7 +132,7 @@ public fun Meta.getIndexed(name: Name): Map<String?, Meta> {
} }
} }
public fun Meta.getIndexed(name: String): Map<String?, Meta> = getIndexed(name.parseAsName()) public fun Meta.getIndexed(name: String): Map<String?, Meta> = getIndexed(name.parseAsName(true))
/** /**
* A meta node that ensures that all of its descendants has at least the same type. * A meta node that ensures that all of its descendants has at least the same type.
@ -171,16 +169,16 @@ public operator fun <M : TypedMeta<M>> TypedMeta<M>.get(token: NameToken): M? =
* *
* If [name] is empty return current [Meta] * If [name] is empty return current [Meta]
*/ */
public tailrec operator fun <M : TypedMeta<M>> TypedMeta<M>.get(name: Name): M? = if (name.isEmpty()) { public tailrec operator fun <M : TypedMeta<M>> TypedMeta<M>?.get(name: Name): M? = when {
self this == null -> null
} else { name.isEmpty() -> self
get(name.firstOrNull()!!)?.get(name.cutFirst()) else -> get(name.firstOrNull()!!)?.get(name.cutFirst())
} }
/** /**
* Parse [Name] from [key] using full name notation and pass it to [TypedMeta.get] * Parse [Name] from [key] using full name notation and pass it to [TypedMeta.get]
*/ */
public operator fun <M : TypedMeta<M>> TypedMeta<M>.get(key: String): M? = this[Name.parse(key)] public operator fun <M : TypedMeta<M>> TypedMeta<M>?.get(key: String): M? = this[key.parseAsName(true)]
/** /**
@ -223,7 +221,8 @@ public fun Meta.isEmpty(): Boolean = this === Meta.EMPTY
public fun <M : TypedMeta<M>> TypedMeta<M>.getIndexed(name: Name): Map<String?, M> = public fun <M : TypedMeta<M>> TypedMeta<M>.getIndexed(name: Name): Map<String?, M> =
(this as Meta).getIndexed(name) as Map<String?, M> (this as Meta).getIndexed(name) as Map<String?, M>
public fun <M : TypedMeta<M>> TypedMeta<M>.getIndexed(name: String): Map<String?, Meta> = getIndexed(Name.parse(name)) public fun <M : TypedMeta<M>> TypedMeta<M>.getIndexed(name: String): Map<String?, Meta> =
getIndexed(name.parseAsName(true))
public val Meta?.string: String? get() = this?.value?.string public val Meta?.string: String? get() = this?.value?.string

View File

@ -77,7 +77,7 @@ public operator fun MetaDescriptor.get(name: Name): MetaDescriptor? = when (name
else -> get(name.firstOrNull()!!.asName())?.get(name.cutFirst()) else -> get(name.firstOrNull()!!.asName())?.get(name.cutFirst())
} }
public operator fun MetaDescriptor.get(name: String): MetaDescriptor? = get(Name.parse(name)) public operator fun MetaDescriptor.get(name: String): MetaDescriptor? = get(name.parseAsName(true))
public fun MetaDescriptor.validate(value: Value?): Boolean = if (value == null) { public fun MetaDescriptor.validate(value: Value?): Boolean = if (value == null) {
valueRequirement != ValueRequirement.REQUIRED valueRequirement != ValueRequirement.REQUIRED

View File

@ -2,6 +2,7 @@ package space.kscience.dataforge.names
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import kotlin.native.concurrent.ThreadLocal
/** /**
@ -55,15 +56,15 @@ public class Name(public val tokens: List<NameToken>) {
/** /**
* Convert a [String] to name parsing it and extracting name tokens and index syntax. * Convert a [String] to name parsing it and extracting name tokens and index syntax.
* This operation is rather heavy so it should be used with care in high performance code. * This operation is rather heavy, so it should be used with care in high performance code.
*/ */
public fun parse(string: String): Name { public fun parse(string: String): Name {
if (string.isBlank()) return Name.EMPTY if (string.isBlank()) return EMPTY
val tokens = sequence { val tokens = sequence {
var bodyBuilder = StringBuilder() var bodyBuilder = StringBuilder()
var queryBuilder = StringBuilder() var queryBuilder = StringBuilder()
var bracketCount: Int = 0 var bracketCount = 0
var escape: Boolean = false var escape = false
fun queryOn() = bracketCount > 0 fun queryOn() = bracketCount > 0
for (it in string) { for (it in string) {
@ -76,9 +77,11 @@ public class Name(public val tokens: List<NameToken>) {
} }
escape = false escape = false
} }
it == '\\' -> { it == '\\' -> {
escape = true escape = true
} }
queryOn() -> { queryOn() -> {
when (it) { when (it) {
'[' -> bracketCount++ '[' -> bracketCount++
@ -86,6 +89,7 @@ public class Name(public val tokens: List<NameToken>) {
} }
if (queryOn()) queryBuilder.append(it) if (queryOn()) queryBuilder.append(it)
} }
else -> when (it) { else -> when (it) {
'.' -> { '.' -> {
val query = if (queryBuilder.isEmpty()) null else queryBuilder.toString() val query = if (queryBuilder.isEmpty()) null else queryBuilder.toString()
@ -93,6 +97,7 @@ public class Name(public val tokens: List<NameToken>) {
bodyBuilder = StringBuilder() bodyBuilder = StringBuilder()
queryBuilder = StringBuilder() queryBuilder = StringBuilder()
} }
'[' -> bracketCount++ '[' -> bracketCount++
']' -> error("Syntax error: closing bracket ] not have not matching open bracket") ']' -> error("Syntax error: closing bracket ] not have not matching open bracket")
else -> { else -> {
@ -203,4 +208,14 @@ public fun Name.removeHeadOrNull(head: Name): Name? = if (startsWith(head)) {
null null
} }
public fun String.parseAsName(): Name = Name.parse(this) @ThreadLocal
private val nameCache = HashMap<String, Name>()
/**
* Parse a string as name. If [cache], use global cache for parsed name to avoid multiple parsings.
*/
public fun String.parseAsName(cache: Boolean = false): Name = if (cache) {
nameCache.getOrPut(this) { Name.parse(this) }
} else {
Name.parse(this)
}

View File

@ -15,16 +15,22 @@ public data class NameToken(val body: String, val index: String? = null) {
if (body.isEmpty()) error("Syntax error: Name token body is empty") if (body.isEmpty()) error("Syntax error: Name token body is empty")
} }
private fun String.escape() = private val bodyEscaped by lazy {
replace("\\", "\\\\") val escaped = buildString {
.replace(".", "\\.") body.forEach {
.replace("[", "\\[") if (it in escapedChars) {
.replace("]", "\\]") append('\\')
}
append(it)
}
}
if (escaped == body) body else escaped
}
override fun toString(): String = if (hasIndex()) { override fun toString(): String = if (hasIndex()) {
"${body.escape()}[$index]" "${bodyEscaped}[$index]"
} else { } else {
body.escape() bodyEscaped
} }
/** /**
@ -39,6 +45,8 @@ public data class NameToken(val body: String, val index: String? = null) {
public companion object { public companion object {
private val escapedChars = listOf('\\', '.', '[', ']')
/** /**
* Parse name token from a string * Parse name token from a string
*/ */

View File

@ -28,10 +28,12 @@ public fun Name.matches(pattern: Name): Boolean = when {
length >= pattern.length length >= pattern.length
&& Name(tokens.subList(0, pattern.length - 1)).matches(pattern.cutLast()) && Name(tokens.subList(0, pattern.length - 1)).matches(pattern.cutLast())
} }
pattern.startsWith(Name.MATCH_ALL_TOKEN) -> { pattern.startsWith(Name.MATCH_ALL_TOKEN) -> {
length >= pattern.length length >= pattern.length
&& Name(tokens.subList(tokens.size - pattern.length + 1, tokens.size)).matches(pattern.cutFirst()) && Name(tokens.subList(tokens.size - pattern.length + 1, tokens.size)).matches(pattern.cutFirst())
} }
else -> { else -> {
tokens.indices.forEach { tokens.indices.forEach {
val thisToken = tokens.getOrNull(it) ?: return false val thisToken = tokens.getOrNull(it) ?: return false
@ -44,4 +46,4 @@ public fun Name.matches(pattern: Name): Boolean = when {
} }
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public fun Name.matches(pattern: String): Boolean = matches(Name.parse(pattern)) public fun Name.matches(pattern: String): Boolean = matches(pattern.parseAsName(true))