From 8b18aff905bb092428e22f5c36b1f2f0b77219ba Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:23:18 +0400 Subject: [PATCH 01/54] Add JMH benchmarks for datetime format creation and performance evaluation - Implement SerialFormatBenchmark to test repeated datetime format sequences. - Implement PythonDateTimeFormatBenchmark to evaluate Python-compatible datetime formats. - Implement ParallelFormatBenchmark to test creation of formats using nested and alternative parsing logic. --- .../src/jmh/kotlin/ParallelFormatBenchmark.kt | 88 +++++++++++++++++++ .../kotlin/PythonDateTimeFormatBenchmark.kt | 47 ++++++++++ .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 ++++++++++ 3 files changed, 180 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt new file mode 100644 index 000000000..07bc7eed7 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.alternativeParsing +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class ParallelFormatBenchmark { + + @Param("2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12") + var n = 0 + + @Benchmark + fun formatCreationWithAlternativeParsing(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + alternativeParsing( + { monthNumber() }, + { day() }, + primaryFormat = { hour() } + ) + char('@') + minute() + char('#') + second() + } + } + blackhole.consume(format) + } + + @Benchmark + fun formatCreationWithNestedAlternativeParsing(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { index -> + alternativeParsing( + { monthNumber(); char('-'); day() }, + { day(); char('/'); monthNumber() }, + primaryFormat = { year(); char('-'); monthNumber(); char('-'); day() } + ) + + if (index and 1 == 0) { + alternativeParsing( + { + alternativeParsing( + { hour(); char(':'); minute() }, + { minute(); char(':'); second() }, + primaryFormat = { hour(); char(':'); minute(); char(':'); second() } + ) + }, + primaryFormat = { + year(); char('-'); monthNumber(); char('-'); day() + char('T') + hour(); char(':'); minute(); char(':'); second() + } + ) + } + + char('|') + if (index % 3 == 0) { + char('|') + } + + if (index and 2 == 0) { + alternativeParsing( + { char('Z') }, + { char('+'); hour(); char(':'); minute() }, + primaryFormat = { char('-'); hour(); char(':'); minute() } + ) + } + } + } + blackhole.consume(format) + } +} diff --git a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt new file mode 100644 index 000000000..24e3bbfcb --- /dev/null +++ b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import kotlinx.datetime.format.optional +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.* + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class PythonDateTimeFormatBenchmark { + + @Benchmark + fun buildPythonDateTimeFormat(blackhole: Blackhole) { + val v = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + day() + char(' ') + hour() + char(':') + minute() + optional { + char(':') + second() + optional { + char('.') + secondFraction() + } + } + } + blackhole.consume(v) + } +} diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt new file mode 100644 index 000000000..fb63f577e --- /dev/null +++ b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class SerialFormatBenchmark { + + @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") + var n = 0 + + @Benchmark + fun largeSerialFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + char('^') + monthNumber() + char('&') + day() + char('!') + hour() + char('$') + minute() + char('#') + second() + char('@') + } + } + blackhole.consume(format) + } +} From d4a2bdda0026818e5d88c01d61cc248b307309ee Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:23:35 +0400 Subject: [PATCH 02/54] Introduce `ConcatenatedListView` for efficient list concatenation in internal datetime parsing logic. --- .../format/parser/ConcatenatedListView.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 core/common/src/internal/format/parser/ConcatenatedListView.kt diff --git a/core/common/src/internal/format/parser/ConcatenatedListView.kt b/core/common/src/internal/format/parser/ConcatenatedListView.kt new file mode 100644 index 000000000..3f11c3826 --- /dev/null +++ b/core/common/src/internal/format/parser/ConcatenatedListView.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.internal.format.parser + +internal class ConcatenatedListView(val list1: List, val list2: List) : AbstractList() { + override val size: Int + get() = list1.size + list2.size + + override fun get(index: Int): T = if (index < list1.size) list1[index] else list2[index - list1.size] + + override fun iterator(): Iterator = ConcatenatedListViewIterator() + + private inner class ConcatenatedListViewIterator : Iterator { + private val iterators: List> = buildList { + collectIterators(list1) + collectIterators(list2) + } + private var index = 0 + + private fun MutableList>.collectIterators(list: List) { + if (list is ConcatenatedListView) { + collectIterators(list.list1) + collectIterators(list.list2) + } else { + add(list.iterator()) + } + } + + override fun hasNext(): Boolean { + while (index < iterators.size && !iterators[index].hasNext()) { + index++ + } + return index < iterators.size + } + + override fun next(): T = iterators[index].next() + } +} From e03b0a9feaaca4a0403fe724c77b75e2b1a7237b Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:32:34 +0400 Subject: [PATCH 03/54] Remove redundant type parameter `` in `ParserStructure.simplify` method definition --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 9958e3fb9..f7b93c4ba 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,7 +49,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { + fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModificationsForTails = unconditionalModifications.toMutableList() From 0eadd925c29b927d0f9c8f34c3eddd0b97d4c7bf Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:25:49 +0400 Subject: [PATCH 04/54] Add unconditionalModifications after each step of simplification --- .../src/internal/format/parser/Parser.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f7b93c4ba..30e75960d 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,10 +49,10 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { + fun ParserStructure.simplify(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null - val unconditionalModificationsForTails = unconditionalModifications.toMutableList() + val unconditionalModificationsForTails = mutableListOf>() // joining together the number consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { @@ -73,7 +73,7 @@ internal fun List>.concat(): ParserStructure { } } val mergedTails = followedBy.flatMap { - val simplified = it.simplify(unconditionalModificationsForTails) + val simplified = it.simplify() // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, // unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d). // As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty @@ -82,25 +82,24 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - }.ifEmpty { - // preserving the invariant that `mergedTails` contains all unconditional modifications - listOf(ParserStructure(unconditionalModificationsForTails, emptyList())) } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt + newOperations.addAll(unconditionalModificationsForTails) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + newOperations.addAll(unconditionalModificationsForTails) ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { is NumberSpanParserOperation -> { ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + it.operations.drop( + listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModificationsForTails + it.operations.drop( 1 ), it.followedBy @@ -108,12 +107,12 @@ internal fun List>.concat(): ParserStructure { } null -> ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan)), + unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)), it.followedBy ) else -> ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, it.followedBy ) } @@ -122,7 +121,7 @@ internal fun List>.concat(): ParserStructure { } } val naiveParser = foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.append(acc) } - return naiveParser.simplify(emptyList()) + return naiveParser.simplify() } internal interface Copyable { From 7cb14e0f8e59a24423bc14bc023e5b8b230e2391 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:37:26 +0400 Subject: [PATCH 05/54] Rename `unconditionalModificationsForTails` to `unconditionalModifications` for consistency and clarity in `simplify` method logic --- core/common/src/internal/format/parser/Parser.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 30e75960d..4f0d1138a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -52,7 +52,7 @@ internal fun List>.concat(): ParserStructure { fun ParserStructure.simplify(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null - val unconditionalModificationsForTails = mutableListOf>() + val unconditionalModifications = mutableListOf>() // joining together the number consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { @@ -63,7 +63,7 @@ internal fun List>.concat(): ParserStructure { currentNumberSpan = op.consumers.toMutableList() } } else if (op is UnconditionalModification) { - unconditionalModificationsForTails.add(op) + unconditionalModifications.add(op) } else { if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) @@ -85,21 +85,21 @@ internal fun List>.concat(): ParserStructure { } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt - newOperations.addAll(unconditionalModificationsForTails) + newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) - newOperations.addAll(unconditionalModificationsForTails) + newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { is NumberSpanParserOperation -> { ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModificationsForTails + it.operations.drop( + listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModifications + it.operations.drop( 1 ), it.followedBy @@ -107,12 +107,12 @@ internal fun List>.concat(): ParserStructure { } null -> ParserStructure( - unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)), + unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)), it.followedBy ) else -> ParserStructure( - unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, it.followedBy ) } From 823e65ac9a11fb7eb490bb1737c8ce20184755a9 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:50:19 +0400 Subject: [PATCH 06/54] Refactor `ParserStructure` logic to use `buildList` for streamlined list construction and improved readability. --- .../src/internal/format/parser/Parser.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4f0d1138a..f83643059 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -97,22 +97,26 @@ internal fun List>.concat(): ParserStructure { } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> { - ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModifications + it.operations.drop( - 1 - ), - it.followedBy - ) - } + is NumberSpanParserOperation -> ParserStructure(buildList(unconditionalModifications.size + it.operations.size) { + add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + addAll(unconditionalModifications) + addAll(it.operations.drop(1)) + }, it.followedBy) null -> ParserStructure( - unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)), + buildList(unconditionalModifications.size + 1) { + addAll(unconditionalModifications) + add(NumberSpanParserOperation(currentNumberSpan)) + }, it.followedBy ) else -> ParserStructure( - unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + buildList(unconditionalModifications.size + 1 + it.operations.size) { + addAll(unconditionalModifications) + add(NumberSpanParserOperation(currentNumberSpan)) + addAll(it.operations) + }, it.followedBy ) } From e3d88b2b0bd1bcc235cac629817b4eee4254f59b Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:55:29 +0400 Subject: [PATCH 07/54] Refactor `ParserStructure` instantiation to improve formatting and readability. --- core/common/src/internal/format/parser/Parser.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f83643059..e1fb7ab4e 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -97,11 +97,14 @@ internal fun List>.concat(): ParserStructure { } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> ParserStructure(buildList(unconditionalModifications.size + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - addAll(unconditionalModifications) - addAll(it.operations.drop(1)) - }, it.followedBy) + is NumberSpanParserOperation -> ParserStructure( + buildList(unconditionalModifications.size + it.operations.size) { + add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + addAll(unconditionalModifications) + addAll(it.operations.drop(1)) + }, + it.followedBy + ) null -> ParserStructure( buildList(unconditionalModifications.size + 1) { From f07556fdc910e223c5a14a51fd9611091b139d2c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 19:22:37 +0400 Subject: [PATCH 08/54] Reorder `unconditionalModifications` placement in `ParserStructure`. --- core/common/src/internal/format/parser/Parser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e1fb7ab4e..66be1f065 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -108,16 +108,16 @@ internal fun List>.concat(): ParserStructure { null -> ParserStructure( buildList(unconditionalModifications.size + 1) { - addAll(unconditionalModifications) add(NumberSpanParserOperation(currentNumberSpan)) + addAll(unconditionalModifications) }, it.followedBy ) else -> ParserStructure( buildList(unconditionalModifications.size + 1 + it.operations.size) { - addAll(unconditionalModifications) add(NumberSpanParserOperation(currentNumberSpan)) + addAll(unconditionalModifications) addAll(it.operations) }, it.followedBy From 3a3ba5495f714cf3e380c3019a752171eea4cc6c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 19:51:24 +0400 Subject: [PATCH 09/54] Passes all tests and it has benchmarc score increase on Python datetime format! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units PythonDateTimeFormatBenchmark.buildPythonDateTimeFormat avgt 5 4142.002 ± 374.247 ns/op --- .../src/internal/format/parser/Parser.kt | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 66be1f065..dbaa565ca 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,7 +49,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(): ParserStructure { + fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() @@ -72,8 +72,37 @@ internal fun List>.concat(): ParserStructure { newOperations.add(op) } } + + if (followedBy.isEmpty()) { + if (other.operations.isNotEmpty()) { + if (currentNumberSpan == null) { + val firstOperation = other.operations.first() + if (firstOperation is NumberSpanParserOperation) { + newOperations.add(other.operations.first()) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations.drop(1)) + } else { + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations) + } + } else { + val firstOperation = other.operations.first() + if (firstOperation is NumberSpanParserOperation) { + newOperations.add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations.drop(1)) + } else { + newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations) + } + } + return ParserStructure(newOperations, other.followedBy) + } + } + val mergedTails = followedBy.flatMap { - val simplified = it.simplify() + val simplified = it.simplifyAndAppend(other) // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, // unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d). // As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty @@ -82,7 +111,7 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - } + }.ifEmpty { other.followedBy } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -127,8 +156,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, newTails) } } - val naiveParser = foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.append(acc) } - return naiveParser.simplify() + return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } } internal interface Copyable { From 6b753bd9bf9627418d049e44ffe11e5b8b690438 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:02:18 +0400 Subject: [PATCH 10/54] Refactor `ParserStructure` logic to extract `mergeOperations` for cleaner handling of operation merging and reduce duplication. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units PythonDateTimeFormatBenchmark.buildPythonDateTimeFormat avgt 5 3708.643 ± 29.908 ns/op --- .../src/internal/format/parser/Parser.kt | 100 ++++++++---------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index dbaa565ca..f65df6001 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,6 +49,43 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } + fun mergeOperations( + baseOperations: List>, + numberSpan: List>?, + unconditionalModifications: List>, + operationsToMerge: List>, + followedBy: List> + ): ParserStructure { + val operations = buildList { + addAll(baseOperations) + when (val firstOperation = operationsToMerge.firstOrNull()) { + is NumberSpanParserOperation -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + } else { + add(firstOperation) + } + addAll(unconditionalModifications) + addAll(operationsToMerge.drop(1)) + } + null -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan)) + } + addAll(unconditionalModifications) + } + else -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan)) + } + addAll(unconditionalModifications) + addAll(operationsToMerge) + } + } + } + return ParserStructure(operations, followedBy) + } + fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null @@ -73,34 +110,6 @@ internal fun List>.concat(): ParserStructure { } } - if (followedBy.isEmpty()) { - if (other.operations.isNotEmpty()) { - if (currentNumberSpan == null) { - val firstOperation = other.operations.first() - if (firstOperation is NumberSpanParserOperation) { - newOperations.add(other.operations.first()) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations.drop(1)) - } else { - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations) - } - } else { - val firstOperation = other.operations.first() - if (firstOperation is NumberSpanParserOperation) { - newOperations.add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations.drop(1)) - } else { - newOperations.add(NumberSpanParserOperation(currentNumberSpan)) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations) - } - } - return ParserStructure(newOperations, other.followedBy) - } - } - val mergedTails = followedBy.flatMap { val simplified = it.simplifyAndAppend(other) // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, @@ -111,7 +120,12 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - }.ifEmpty { other.followedBy } + }.ifEmpty { + if (other.operations.isNotEmpty()) { + return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other.operations, other.followedBy) + } + other.followedBy + } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -125,33 +139,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { - when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> ParserStructure( - buildList(unconditionalModifications.size + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - addAll(unconditionalModifications) - addAll(it.operations.drop(1)) - }, - it.followedBy - ) - - null -> ParserStructure( - buildList(unconditionalModifications.size + 1) { - add(NumberSpanParserOperation(currentNumberSpan)) - addAll(unconditionalModifications) - }, - it.followedBy - ) - - else -> ParserStructure( - buildList(unconditionalModifications.size + 1 + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan)) - addAll(unconditionalModifications) - addAll(it.operations) - }, - it.followedBy - ) - } + mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it.operations, it.followedBy) } ParserStructure(newOperations, newTails) } From 67630d99ca35337fd75576ee3cc83d4e6de77850 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:09:56 +0400 Subject: [PATCH 11/54] Refactor `ParserStructure.concat` logic to simplify `mergeOperations` usage by passing `ParserStructure` directly instead of separating operations and followedBy. --- .../src/internal/format/parser/Parser.kt | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f65df6001..c75b92ab3 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -41,24 +41,16 @@ internal class ParserStructure( "${operations.joinToString(", ")}(${followedBy.joinToString(";")})" } -// TODO: O(size of the resulting parser ^ 2), but can be O(size of the resulting parser) internal fun List>.concat(): ParserStructure { - fun ParserStructure.append(other: ParserStructure): ParserStructure = if (followedBy.isEmpty()) { - ParserStructure(operations + other.operations, other.followedBy) - } else { - ParserStructure(operations, followedBy.map { it.append(other) }) - } - fun mergeOperations( baseOperations: List>, numberSpan: List>?, unconditionalModifications: List>, - operationsToMerge: List>, - followedBy: List> + simplifiedParserStructure: ParserStructure, ): ParserStructure { val operations = buildList { addAll(baseOperations) - when (val firstOperation = operationsToMerge.firstOrNull()) { + when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { is NumberSpanParserOperation -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) @@ -66,7 +58,7 @@ internal fun List>.concat(): ParserStructure { add(firstOperation) } addAll(unconditionalModifications) - addAll(operationsToMerge.drop(1)) + addAll(simplifiedParserStructure.operations.drop(1)) } null -> { if (numberSpan != null) { @@ -79,11 +71,11 @@ internal fun List>.concat(): ParserStructure { add(NumberSpanParserOperation(numberSpan)) } addAll(unconditionalModifications) - addAll(operationsToMerge) + addAll(simplifiedParserStructure.operations) } } } - return ParserStructure(operations, followedBy) + return ParserStructure(operations, simplifiedParserStructure.followedBy) } fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { @@ -122,7 +114,7 @@ internal fun List>.concat(): ParserStructure { listOf(simplified) }.ifEmpty { if (other.operations.isNotEmpty()) { - return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other.operations, other.followedBy) + return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other) } other.followedBy } @@ -139,7 +131,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { - mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it.operations, it.followedBy) + mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it) } ParserStructure(newOperations, newTails) } From 459dd76cbd737b17c3bc5a6d136bafa41814ea5a Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:12:13 +0400 Subject: [PATCH 12/54] Add missing newline --- core/common/src/internal/format/parser/Parser.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index c75b92ab3..2360ac0d2 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -118,6 +118,7 @@ internal fun List>.concat(): ParserStructure { } other.followedBy } + return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -136,6 +137,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, newTails) } } + return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } } From ed9db86aaf594586db0fdea17beba2c2000ec158 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:14:54 +0400 Subject: [PATCH 13/54] Rename `operations` to `mergedOperations`. --- core/common/src/internal/format/parser/Parser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 2360ac0d2..4102c6f12 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -48,7 +48,7 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { - val operations = buildList { + val mergedOperations = buildList { addAll(baseOperations) when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { is NumberSpanParserOperation -> { @@ -75,7 +75,7 @@ internal fun List>.concat(): ParserStructure { } } } - return ParserStructure(operations, simplifiedParserStructure.followedBy) + return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { From 9a9a628afdc7551f8a43b2be3b7afd9a6b3951d0 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 12:52:25 +0400 Subject: [PATCH 14/54] Simplify the ` mergedTails ` condition. --- core/common/src/internal/format/parser/Parser.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4102c6f12..8d37c0dda 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -123,9 +123,7 @@ internal fun List>.concat(): ParserStructure { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) - } else if (mergedTails.none { - it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true - }) { + } else if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) newOperations.addAll(unconditionalModifications) From d923b96f2759eca62a2ba6b1d9a205a41fa15816 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:06:23 +0400 Subject: [PATCH 15/54] Reorder and simplify `unconditionalModifications` handling in `ParserStructure` logic. --- core/common/src/internal/format/parser/Parser.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 8d37c0dda..e4b62dbce 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -54,26 +54,24 @@ internal fun List>.concat(): ParserStructure { is NumberSpanParserOperation -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + addAll(simplifiedParserStructure.operations.drop(1)) } else { - add(firstOperation) + addAll(simplifiedParserStructure.operations) } - addAll(unconditionalModifications) - addAll(simplifiedParserStructure.operations.drop(1)) } null -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan)) } - addAll(unconditionalModifications) } else -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan)) } - addAll(unconditionalModifications) addAll(simplifiedParserStructure.operations) } } + addAll(unconditionalModifications) } return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } From f5db9de0e622326e1066eb148cb194785395a689 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:12:59 +0400 Subject: [PATCH 16/54] Refactor `ParserStructure` logic to streamline `mergedOperations` construction and simplify `NumberSpanParserOperation` handling. --- .../src/internal/format/parser/Parser.kt | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e4b62dbce..e0d709899 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -50,24 +50,17 @@ internal fun List>.concat(): ParserStructure { ): ParserStructure { val mergedOperations = buildList { addAll(baseOperations) - when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { - is NumberSpanParserOperation -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(simplifiedParserStructure.operations.drop(1)) - } else { - addAll(simplifiedParserStructure.operations) - } + val firstOperation = simplifiedParserStructure.operations.firstOrNull() + when { + numberSpan == null -> { + addAll(simplifiedParserStructure.operations) } - null -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan)) - } + firstOperation is NumberSpanParserOperation -> { + add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + addAll(simplifiedParserStructure.operations.drop(1)) } else -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan)) - } + add(NumberSpanParserOperation(numberSpan)) addAll(simplifiedParserStructure.operations) } } From 29078047e18fa16ef4a63bd94fda7225461227bc Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:37:22 +0400 Subject: [PATCH 17/54] =?UTF-8?q?Fix=20typo=20in=20comment:=20"number=20co?= =?UTF-8?q?nsumers"=20=E2=86=92=20"number=20of=20consumers".?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e0d709899..4f1d3be98 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -73,7 +73,7 @@ internal fun List>.concat(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() - // joining together the number consumers in this parser before the first alternative; + // joining together the number of consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { if (op is NumberSpanParserOperation) { From 6b1028869d992ff4f7e02e8a23e5d915c1806414 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:49:02 +0400 Subject: [PATCH 18/54] Refactor `mergedOperations` construction to reuse `operationsToMerge` and avoid duplication. --- core/common/src/internal/format/parser/Parser.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4f1d3be98..0eabba3d1 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -48,20 +48,21 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { + val operationsToMerge = simplifiedParserStructure.operations + val firstOperation = operationsToMerge.firstOrNull() val mergedOperations = buildList { addAll(baseOperations) - val firstOperation = simplifiedParserStructure.operations.firstOrNull() when { numberSpan == null -> { - addAll(simplifiedParserStructure.operations) + addAll(operationsToMerge) } firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(simplifiedParserStructure.operations.drop(1)) + addAll(operationsToMerge.drop(1)) } else -> { add(NumberSpanParserOperation(numberSpan)) - addAll(simplifiedParserStructure.operations) + addAll(operationsToMerge) } } addAll(unconditionalModifications) From d371115587caedba99a6d3d1a011309cd2c71f98 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:37:37 +0400 Subject: [PATCH 19/54] Refactor `PythonDateTimeFormatBenchmark` into `CommonFormats`, update benchmarking parameters, and add ISO DateTime format benchmark. --- ...imeFormatBenchmark.kt => CommonFormats.kt} | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) rename benchmarks/src/jmh/kotlin/{PythonDateTimeFormatBenchmark.kt => CommonFormats.kt} (52%) diff --git a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt similarity index 52% rename from benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt rename to benchmarks/src/jmh/kotlin/CommonFormats.kt index 24e3bbfcb..5229a6d2e 100644 --- a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,19 +7,20 @@ package kotlinx.datetime +import kotlinx.datetime.format.alternativeParsing import kotlinx.datetime.format.char import kotlinx.datetime.format.optional import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.* -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 20, time = 2) +@Measurement(iterations = 30, time = 2) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) -@Fork(1) -open class PythonDateTimeFormatBenchmark { +@Fork(2) +open class CommonFormats { @Benchmark fun buildPythonDateTimeFormat(blackhole: Blackhole) { @@ -44,4 +45,32 @@ open class PythonDateTimeFormatBenchmark { } blackhole.consume(v) } + + @Benchmark + fun buildIsoDateTimeFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + date(LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + day() + }) + alternativeParsing({ char('t') }) { char('T') } + time(LocalTime.Format { + hour() + char(':') + minute() + alternativeParsing({}) { + char(':') + second() + optional { + char('.') + secondFraction(1, 9) + } + } + }) + } + blackhole.consume(format) + } } From 1d2a7e230532992d55f5275afcb878ae7440f055 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:45:48 +0400 Subject: [PATCH 20/54] Add benchmark for building four-digit UTC offset format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index 5229a6d2e..d7728cfeb 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -73,4 +73,13 @@ open class CommonFormats { } blackhole.consume(format) } + + @Benchmark + fun buildFourDigitsUtcOffsetFormat(blackhole: Blackhole) { + val format = UtcOffset.Format { + offsetHours() + offsetMinutesOfHour() + } + blackhole.consume(format) + } } From c674a8381c7509129d275efcb835bac9f555e8ad Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:54:48 +0400 Subject: [PATCH 21/54] Add benchmark for building RFC 1123 DateTime format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index d7728cfeb..e76065e30 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,6 +7,10 @@ package kotlinx.datetime +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.Padding import kotlinx.datetime.format.alternativeParsing import kotlinx.datetime.format.char import kotlinx.datetime.format.optional @@ -82,4 +86,10 @@ open class CommonFormats { } blackhole.consume(format) } + + @Benchmark + fun buildRfc1123DateTimeFormat(blackhole: Blackhole) { + val format = DateTimeComponents.Formats.RFC_1123 + blackhole.consume(format) + } } From 502b0c3cf8f2cc1216ae9425628dc566d2804036 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:58:31 +0400 Subject: [PATCH 22/54] Add benchmark for building ISO DateTime with offset format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index e76065e30..12e831fb7 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -9,6 +9,7 @@ package kotlinx.datetime import kotlinx.datetime.format.DateTimeComponents import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.ISO_DATE import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.Padding import kotlinx.datetime.format.alternativeParsing @@ -92,4 +93,37 @@ open class CommonFormats { val format = DateTimeComponents.Formats.RFC_1123 blackhole.consume(format) } + + @Benchmark + fun buildIsoDateTimeOffsetFormat(blackhole: Blackhole) { + val format = DateTimeComponents.Format { + date(LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + day() + }) + alternativeParsing({ + char('t') + }) { + char('T') + } + hour() + char(':') + minute() + char(':') + second() + optional { + char('.') + secondFraction(1, 9) + } + alternativeParsing({ + offsetHours() + }) { + offset(UtcOffset.Formats.ISO) + } + } + blackhole.consume(format) + } } From ba37c51d3b63a0a10bc9a5675a8e88ea9a6f1a02 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:01:23 +0400 Subject: [PATCH 23/54] Refactor RFC 1123 and UTC offset format benchmarks to use inline format construction. --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 58 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index 12e831fb7..1c383c608 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,14 +7,7 @@ package kotlinx.datetime -import kotlinx.datetime.format.DateTimeComponents -import kotlinx.datetime.format.DayOfWeekNames -import kotlinx.datetime.format.ISO_DATE -import kotlinx.datetime.format.MonthNames -import kotlinx.datetime.format.Padding -import kotlinx.datetime.format.alternativeParsing -import kotlinx.datetime.format.char -import kotlinx.datetime.format.optional +import kotlinx.datetime.format.* import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.* @@ -90,7 +83,40 @@ open class CommonFormats { @Benchmark fun buildRfc1123DateTimeFormat(blackhole: Blackhole) { - val format = DateTimeComponents.Formats.RFC_1123 + val format = DateTimeComponents.Format { + alternativeParsing({ + // the day of week may be missing + }) { + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + chars(", ") + } + day(Padding.NONE) + char(' ') + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + year() + char(' ') + hour() + char(':') + minute() + optional { + char(':') + second() + } + chars(" ") + alternativeParsing({ + chars("UT") + }, { + chars("Z") + }) { + optional("GMT") { + offset(UtcOffset.Format { + offsetHours() + offsetMinutesOfHour() + }) + } + } + } blackhole.consume(format) } @@ -121,7 +147,19 @@ open class CommonFormats { alternativeParsing({ offsetHours() }) { - offset(UtcOffset.Formats.ISO) + offset(UtcOffset.Format { + alternativeParsing({ chars("z") }) { + optional("Z") { + offsetHours() + char(':') + offsetMinutesOfHour() + optional { + char(':') + offsetSecondsOfMinute() + } + } + } + }) } } blackhole.consume(format) From 41d338a2ae583b418482e8be60dc7e90da1ce204 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:46:34 +0400 Subject: [PATCH 24/54] Remove `ConcatenatedListView` as it is no longer in use. --- .../format/parser/ConcatenatedListView.kt | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 core/common/src/internal/format/parser/ConcatenatedListView.kt diff --git a/core/common/src/internal/format/parser/ConcatenatedListView.kt b/core/common/src/internal/format/parser/ConcatenatedListView.kt deleted file mode 100644 index 3f11c3826..000000000 --- a/core/common/src/internal/format/parser/ConcatenatedListView.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019-2025 JetBrains s.r.o. and contributors. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -package kotlinx.datetime.internal.format.parser - -internal class ConcatenatedListView(val list1: List, val list2: List) : AbstractList() { - override val size: Int - get() = list1.size + list2.size - - override fun get(index: Int): T = if (index < list1.size) list1[index] else list2[index - list1.size] - - override fun iterator(): Iterator = ConcatenatedListViewIterator() - - private inner class ConcatenatedListViewIterator : Iterator { - private val iterators: List> = buildList { - collectIterators(list1) - collectIterators(list2) - } - private var index = 0 - - private fun MutableList>.collectIterators(list: List) { - if (list is ConcatenatedListView) { - collectIterators(list.list1) - collectIterators(list.list2) - } else { - add(list.iterator()) - } - } - - override fun hasNext(): Boolean { - while (index < iterators.size && !iterators[index].hasNext()) { - index++ - } - return index < iterators.size - } - - override fun next(): T = iterators[index].next() - } -} From 5af9c09670bca1fcc27cae583eb552ed3b442641 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:48:10 +0400 Subject: [PATCH 25/54] Remove `SerialFormatBenchmark` as it is no longer in use. --- .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt deleted file mode 100644 index fb63f577e..000000000 --- a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019-2025 JetBrains s.r.o. and contributors. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -@file:Suppress("unused") - -package kotlinx.datetime - -import kotlinx.datetime.format.char -import org.openjdk.jmh.annotations.* -import org.openjdk.jmh.infra.Blackhole -import java.util.concurrent.TimeUnit - -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -@Fork(1) -open class SerialFormatBenchmark { - - @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") - var n = 0 - - @Benchmark - fun largeSerialFormat(blackhole: Blackhole) { - val format = LocalDateTime.Format { - repeat(n) { - char('^') - monthNumber() - char('&') - day() - char('!') - hour() - char('$') - minute() - char('#') - second() - char('@') - } - } - blackhole.consume(format) - } -} From 6470c77b7e16aeba2d6877fe9cbffd0fa5feeb28 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:49:16 +0400 Subject: [PATCH 26/54] Update `ParallelFormatBenchmark` warmup and measurement parameters. --- benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt index 07bc7eed7..c74dfaa02 100644 --- a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt @@ -13,8 +13,8 @@ import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.TimeUnit -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 10, time = 2) +@Measurement(iterations = 20, time = 2) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) From 292ea76cc15b5c9e67a97a089db155664a56c685 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:54:47 +0400 Subject: [PATCH 27/54] Update copyright year range in `Parser.kt` header. --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 0eabba3d1..86bd19ee6 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 JetBrains s.r.o. and contributors. + * Copyright 2023-2025 JetBrains s.r.o. and contributors. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ From de419ddf750ba8f4fdab66ceb91df1de524080da Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:39:52 +0400 Subject: [PATCH 28/54] Optimize concatenation of flat parsers. --- .../src/internal/format/parser/Parser.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 86bd19ee6..cebf3c76a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -128,7 +128,36 @@ internal fun List>.concat(): ParserStructure { } } - return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } + var result = ParserStructure(emptyList(), emptyList()) + val flatParsers = mutableListOf>>() + + for (parser in this.asReversed()) { + if (parser.followedBy.isEmpty()) { + flatParsers.add(parser.operations) + } else { + if (flatParsers.isNotEmpty()) { + val operations = buildList() { + for (i in flatParsers.lastIndex downTo 0) { + addAll(flatParsers[i]) + } + } + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + flatParsers.clear() + } + result = parser.simplifyAndAppend(result) + } + } + + if (flatParsers.isNotEmpty()) { + val operations = buildList { + for (i in flatParsers.lastIndex downTo 0) { + addAll(flatParsers[i]) + } + } + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + } + + return result } internal interface Copyable { From b9b0a4ba4699cdaf14bb32994293cb09c1c11bab Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:48:41 +0400 Subject: [PATCH 29/54] Refactor `Parser` to streamline handling of accumulated operations. --- .../src/internal/format/parser/Parser.kt | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index cebf3c76a..d27920290 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -129,34 +129,30 @@ internal fun List>.concat(): ParserStructure { } var result = ParserStructure(emptyList(), emptyList()) - val flatParsers = mutableListOf>>() + val accumulatedOperations = mutableListOf>>() - for (parser in this.asReversed()) { - if (parser.followedBy.isEmpty()) { - flatParsers.add(parser.operations) - } else { - if (flatParsers.isNotEmpty()) { - val operations = buildList() { - for (i in flatParsers.lastIndex downTo 0) { - addAll(flatParsers[i]) - } + fun flushAccumulatedOperations() { + if (accumulatedOperations.isNotEmpty()) { + val operations = buildList { + for (parserOperations in accumulatedOperations.asReversed()) { + addAll(parserOperations) } - result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) - flatParsers.clear() } - result = parser.simplifyAndAppend(result) + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + accumulatedOperations.clear() } } - if (flatParsers.isNotEmpty()) { - val operations = buildList { - for (i in flatParsers.lastIndex downTo 0) { - addAll(flatParsers[i]) - } + for (parser in this.asReversed()) { + if (parser.followedBy.isEmpty()) { + accumulatedOperations.add(parser.operations) + } else { + flushAccumulatedOperations() + result = parser.simplifyAndAppend(result) } - result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) } + flushAccumulatedOperations() return result } From 2ed069e94e6239f433bc7f452521e79a0130d74e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:56:57 +0400 Subject: [PATCH 30/54] Add `SerialFormatBenchmark` for evaluating large format serialization performance --- .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt new file mode 100644 index 000000000..d6eeaad03 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 10, time = 2) +@Measurement(iterations = 20, time = 2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class SerialFormatBenchmark { + + @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") + var n = 0 + + @Benchmark + fun largeSerialFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + char('^') + monthNumber() + char('&') + day() + char('!') + hour() + char('$') + minute() + char('#') + second() + char('@') + } + } + blackhole.consume(format) + } +} From 991a1fb359e303b2ec8f7fddffd59c02d79ed988 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Fri, 21 Nov 2025 11:28:55 +0400 Subject: [PATCH 31/54] Refactor `Parser` to replace `drop(1)` with an explicit loop for merging operations. --- core/common/src/internal/format/parser/Parser.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index d27920290..e5d3f7382 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -58,7 +58,9 @@ internal fun List>.concat(): ParserStructure { } firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(operationsToMerge.drop(1)) + for (i in 1..operationsToMerge.lastIndex) { + add(operationsToMerge[i]) + } } else -> { add(NumberSpanParserOperation(numberSpan)) From ed4720b02183cae446fcc5281a71b887d05ece12 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Fri, 21 Nov 2025 11:44:02 +0400 Subject: [PATCH 32/54] Refactor `Parser` to apply `unconditionalModifications` after processing `currentNumberSpan`. --- core/common/src/internal/format/parser/Parser.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e5d3f7382..22901f38a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -91,6 +91,8 @@ internal fun List>.concat(): ParserStructure { if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) currentNumberSpan = null + newOperations.addAll(unconditionalModifications) + unconditionalModifications.clear() } newOperations.add(op) } From 8e370662c3bbd3c01e47e6097c641f89bc5e8588 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 16:06:54 +0400 Subject: [PATCH 33/54] Fix typo in comment within `Parser.kt` --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 22901f38a..8d680e666 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -76,7 +76,7 @@ internal fun List>.concat(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() - // joining together the number of consumers in this parser before the first alternative; + // joining together the number consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { if (op is NumberSpanParserOperation) { From c7c13508cc347c6ae52d0d2c3c67a2c74e260d89 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 16:35:15 +0400 Subject: [PATCH 34/54] Document `mergeOperations` function with detailed explanation of parameters and behavior in `Parser.kt`. --- .../src/internal/format/parser/Parser.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 8d680e666..fad53110a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -42,6 +42,21 @@ internal class ParserStructure( } internal fun List>.concat(): ParserStructure { + /** + * Merges pending operations (base operations, number span, and unconditional modifications) + * with a simplified parser structure. + * + * Invariant: this function should only be called when `simplifiedParserStructure.operations` + * is non-empty. If the structure consists solely of alternatives (empty operations with + * non-empty followedBy), this function should NOT be called. + * + * @param baseOperations Operations to prepend (already processed operations from this parser) + * @param numberSpan Pending number consumers that need to be merged or added (`null` if none pending) + * @param unconditionalModifications Operations that must execute after all others + * @param simplifiedParserStructure The simplified parser structure to merge with (must have operations) + * + * @return A new parser structure with all operations merged and the alternatives from [simplifiedParserStructure] + */ fun mergeOperations( baseOperations: List>, numberSpan: List>?, @@ -53,20 +68,24 @@ internal fun List>.concat(): ParserStructure { val mergedOperations = buildList { addAll(baseOperations) when { + // No pending number span: just append all operations numberSpan == null -> { addAll(operationsToMerge) } + // Merge the pending number span with the first operation if it's also a number span firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) for (i in 1..operationsToMerge.lastIndex) { add(operationsToMerge[i]) } } + // Add the pending number span as a separate operation before the others else -> { add(NumberSpanParserOperation(numberSpan)) addAll(operationsToMerge) } } + // Unconditional modifications always go at the end of the branch addAll(unconditionalModifications) } return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) From 859bc1889c1875543fcda1178e2919065d9edcdc Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 16:47:58 +0400 Subject: [PATCH 35/54] Fix the comment regarding unconditional modifications in `Parser.kt`. --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index fad53110a..185f5be66 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -96,7 +96,7 @@ internal fun List>.concat(): ParserStructure { var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() // joining together the number consumers in this parser before the first alternative; - // collecting the unconditional modifications to push them to the end of all the parser's branches. + // collecting the unconditional modifications. for (op in operations) { if (op is NumberSpanParserOperation) { if (currentNumberSpan != null) { From 83e576384eb6ed94e5a625fe60b1d96e7dc5b621 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 17:31:39 +0400 Subject: [PATCH 36/54] Document `simplifyAndAppend` function with detailed explanation of behavior, parameters, and invariants in `Parser.kt`. --- .../src/internal/format/parser/Parser.kt | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 185f5be66..845a38258 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -91,12 +91,32 @@ internal fun List>.concat(): ParserStructure { return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } + /** + * Simplifies this parser structure and appends [other] to all execution paths. + * + * Simplification includes: + * - Merging consecutive number spans into single operations + * - Collecting unconditional modifications and applying them before regular operations or at branch ends + * - Flattening nested alternatives + * + * Number span handling at branch ends: + * - If no alternative starts with a number span, the pending number span is added as a separate operation + * - If any alternative starts with a number span, the pending number span is distributed to all alternatives + * via [mergeOperations] for proper merging + * + * Invariant: [mergeOperations] is only called when the target structure has non-empty + * operations, ensuring correct merging and unconditional modification placement. + * + * @param other The simplified parser structure to append + * @return A new parser structure representing the simplified concatenation + */ fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() - // joining together the number consumers in this parser before the first alternative; - // collecting the unconditional modifications. + + // Joining together the number consumers in this parser before the first alternative. + // Collecting the unconditional modifications. for (op in operations) { if (op is NumberSpanParserOperation) { if (currentNumberSpan != null) { @@ -107,6 +127,7 @@ internal fun List>.concat(): ParserStructure { } else if (op is UnconditionalModification) { unconditionalModifications.add(op) } else { + // Flush pending number span and unconditional modifications before regular operations if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) currentNumberSpan = null @@ -117,6 +138,7 @@ internal fun List>.concat(): ParserStructure { } } + // Recursively process alternatives, appending [other] and flattening nested structures val mergedTails = followedBy.flatMap { val simplified = it.simplifyAndAppend(other) // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, @@ -129,21 +151,26 @@ internal fun List>.concat(): ParserStructure { listOf(simplified) }.ifEmpty { if (other.operations.isNotEmpty()) { + // Safe to call mergeOperations: target has operations return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other) } + // [other] has no operations, just alternatives; use them as our tails other.followedBy } return if (currentNumberSpan == null) { - // the last operation was not a number span, or it was a number span that we are allowed to interrupt + // The last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { - // the last operation was a number span, but there are no alternatives that start with a number span. + // The last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else { + // The last operation was a number span, and some alternatives start with one: distribute for merging. + // [mergeOperations] is safe here because each alternative in [mergedTails] has operations + // (verified by the structure coming from the recursive [simplifyAndAppend] calls). val newTails = mergedTails.map { mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it) } From 97b820f39fa54ccdcacf70ba259da59f4c2c46c2 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 19:35:34 +0400 Subject: [PATCH 37/54] Improve comments in `Parser` for better clarity on reverse order processing and batching operations --- core/common/src/internal/format/parser/Parser.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 845a38258..1abb7e7e8 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -178,11 +178,13 @@ internal fun List>.concat(): ParserStructure { } } + // Combine parsers in reverse order, batching operations from parsers without followedBy. var result = ParserStructure(emptyList(), emptyList()) val accumulatedOperations = mutableListOf>>() fun flushAccumulatedOperations() { if (accumulatedOperations.isNotEmpty()) { + // Reverse to restore the original order (since parsers are processed in reverse). val operations = buildList { for (parserOperations in accumulatedOperations.asReversed()) { addAll(parserOperations) @@ -195,13 +197,16 @@ internal fun List>.concat(): ParserStructure { for (parser in this.asReversed()) { if (parser.followedBy.isEmpty()) { + // No followedBy: accumulate for batch processing. accumulatedOperations.add(parser.operations) } else { + // Has followedBy: flush accumulated operations, then process individually. flushAccumulatedOperations() result = parser.simplifyAndAppend(result) } } + // Flush remaining accumulated operations. flushAccumulatedOperations() return result } From 8eddac6b1f520afd77cbe34e5ecb2bc713f4944a Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 19:40:23 +0400 Subject: [PATCH 38/54] Document `concat` extension function in `Parser.kt` with details on behavior, reverse order processing, and structure simplification. --- core/common/src/internal/format/parser/Parser.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 1abb7e7e8..f2c0b643d 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -41,6 +41,14 @@ internal class ParserStructure( "${operations.joinToString(", ")}(${followedBy.joinToString(";")})" } +/** + * Concatenates a list of parser structures into a single structure. + * + * Processes parsers in reverse order, batching operations from parsers without alternatives + * and simplifying the resulting structure (merging number spans, handling unconditional modifications). + * + * @return A single parser structure representing the composition of all parsers in the list + */ internal fun List>.concat(): ParserStructure { /** * Merges pending operations (base operations, number span, and unconditional modifications) From 5c46bc3de67f69698ea2b3921f0fd753a99f5efc Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 19:47:23 +0400 Subject: [PATCH 39/54] Fix typos and clarify comments in `Parser.kt` regarding invariants and structure equivalence. --- core/common/src/internal/format/parser/Parser.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f2c0b643d..58598116c 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -54,8 +54,8 @@ internal fun List>.concat(): ParserStructure { * Merges pending operations (base operations, number span, and unconditional modifications) * with a simplified parser structure. * - * Invariant: this function should only be called when `simplifiedParserStructure.operations` - * is non-empty. If the structure consists solely of alternatives (empty operations with + * Invariant: this function should only be called when [simplifiedParserStructure.operations] + * is non-empty. If the structure consists only of alternatives (empty operations with * non-empty followedBy), this function should NOT be called. * * @param baseOperations Operations to prepend (already processed operations from this parser) @@ -149,7 +149,7 @@ internal fun List>.concat(): ParserStructure { // Recursively process alternatives, appending [other] and flattening nested structures val mergedTails = followedBy.flatMap { val simplified = it.simplifyAndAppend(other) - // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, + // Parser `ParserStructure(emptyList(), p)` is equivalent to `p`, // unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d). // As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty // string. For example, (|a|b) is not equivalent to (a|b). From 873a49e0cb39336d4527f1350a3004f50943d0cb Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 27 Nov 2025 19:49:14 +0400 Subject: [PATCH 40/54] Add validation check for non-empty operations in `mergeOperations` of `Parser.kt`. --- core/common/src/internal/format/parser/Parser.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 58598116c..0f95bd40e 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -71,6 +71,7 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { + require(simplifiedParserStructure.operations.isNotEmpty()) { "Cannot merge operations from empty structure" } val operationsToMerge = simplifiedParserStructure.operations val firstOperation = operationsToMerge.firstOrNull() val mergedOperations = buildList { From 097601601a06cb1d218f07c4034cb2cc1b089e8c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Fri, 28 Nov 2025 17:40:26 +0400 Subject: [PATCH 41/54] Refactor `concat` function in `Parser.kt`, simplifying comments --- .../src/internal/format/parser/Parser.kt | 71 +++---------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 0f95bd40e..eee805828 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -42,29 +42,11 @@ internal class ParserStructure( } /** - * Concatenates a list of parser structures into a single structure. - * - * Processes parsers in reverse order, batching operations from parsers without alternatives - * and simplifying the resulting structure (merging number spans, handling unconditional modifications). - * - * @return A single parser structure representing the composition of all parsers in the list + * Concatenates a list of parser structures into a single structure, processing them in reverse order. + * Simplifies the result by merging number spans and handling unconditional modifications. */ internal fun List>.concat(): ParserStructure { - /** - * Merges pending operations (base operations, number span, and unconditional modifications) - * with a simplified parser structure. - * - * Invariant: this function should only be called when [simplifiedParserStructure.operations] - * is non-empty. If the structure consists only of alternatives (empty operations with - * non-empty followedBy), this function should NOT be called. - * - * @param baseOperations Operations to prepend (already processed operations from this parser) - * @param numberSpan Pending number consumers that need to be merged or added (`null` if none pending) - * @param unconditionalModifications Operations that must execute after all others - * @param simplifiedParserStructure The simplified parser structure to merge with (must have operations) - * - * @return A new parser structure with all operations merged and the alternatives from [simplifiedParserStructure] - */ + // Invariant: only called when simplifiedParserStructure.operations is non-empty fun mergeOperations( baseOperations: List>, numberSpan: List>?, @@ -77,55 +59,32 @@ internal fun List>.concat(): ParserStructure { val mergedOperations = buildList { addAll(baseOperations) when { - // No pending number span: just append all operations numberSpan == null -> { addAll(operationsToMerge) } - // Merge the pending number span with the first operation if it's also a number span firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) for (i in 1..operationsToMerge.lastIndex) { add(operationsToMerge[i]) } } - // Add the pending number span as a separate operation before the others else -> { add(NumberSpanParserOperation(numberSpan)) addAll(operationsToMerge) } } - // Unconditional modifications always go at the end of the branch addAll(unconditionalModifications) } return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } - /** - * Simplifies this parser structure and appends [other] to all execution paths. - * - * Simplification includes: - * - Merging consecutive number spans into single operations - * - Collecting unconditional modifications and applying them before regular operations or at branch ends - * - Flattening nested alternatives - * - * Number span handling at branch ends: - * - If no alternative starts with a number span, the pending number span is added as a separate operation - * - If any alternative starts with a number span, the pending number span is distributed to all alternatives - * via [mergeOperations] for proper merging - * - * Invariant: [mergeOperations] is only called when the target structure has non-empty - * operations, ensuring correct merging and unconditional modification placement. - * - * @param other The simplified parser structure to append - * @return A new parser structure representing the simplified concatenation - */ + // Simplifies this parser and appends [other] to all execution paths. + // Merges number spans, collects unconditional modifications, and flattens alternatives. fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() - // Joining together the number consumers in this parser before the first alternative. - // Collecting the unconditional modifications. for (op in operations) { if (op is NumberSpanParserOperation) { if (currentNumberSpan != null) { @@ -136,7 +95,6 @@ internal fun List>.concat(): ParserStructure { } else if (op is UnconditionalModification) { unconditionalModifications.add(op) } else { - // Flush pending number span and unconditional modifications before regular operations if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) currentNumberSpan = null @@ -147,7 +105,6 @@ internal fun List>.concat(): ParserStructure { } } - // Recursively process alternatives, appending [other] and flattening nested structures val mergedTails = followedBy.flatMap { val simplified = it.simplifyAndAppend(other) // Parser `ParserStructure(emptyList(), p)` is equivalent to `p`, @@ -160,7 +117,7 @@ internal fun List>.concat(): ParserStructure { listOf(simplified) }.ifEmpty { if (other.operations.isNotEmpty()) { - // Safe to call mergeOperations: target has operations + // The invariant is preserved: other.operations is non-empty return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other) } // [other] has no operations, just alternatives; use them as our tails @@ -168,32 +125,27 @@ internal fun List>.concat(): ParserStructure { } return if (currentNumberSpan == null) { - // The last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { - // The last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else { - // The last operation was a number span, and some alternatives start with one: distribute for merging. - // [mergeOperations] is safe here because each alternative in [mergedTails] has operations - // (verified by the structure coming from the recursive [simplifyAndAppend] calls). - val newTails = mergedTails.map { - mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it) + // Distribute number span across alternatives that start with number spans + // The invariant is preserved: the structure coming from the recursive [simplifyAndAppend] calls + val newTails = mergedTails.map { structure -> + mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, structure) } ParserStructure(newOperations, newTails) } } - // Combine parsers in reverse order, batching operations from parsers without followedBy. var result = ParserStructure(emptyList(), emptyList()) val accumulatedOperations = mutableListOf>>() fun flushAccumulatedOperations() { if (accumulatedOperations.isNotEmpty()) { - // Reverse to restore the original order (since parsers are processed in reverse). val operations = buildList { for (parserOperations in accumulatedOperations.asReversed()) { addAll(parserOperations) @@ -206,16 +158,13 @@ internal fun List>.concat(): ParserStructure { for (parser in this.asReversed()) { if (parser.followedBy.isEmpty()) { - // No followedBy: accumulate for batch processing. accumulatedOperations.add(parser.operations) } else { - // Has followedBy: flush accumulated operations, then process individually. flushAccumulatedOperations() result = parser.simplifyAndAppend(result) } } - // Flush remaining accumulated operations. flushAccumulatedOperations() return result } From 313432cdd67d382728ca3e0969d33c6858f9a3e6 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 18:03:45 +0400 Subject: [PATCH 42/54] Add `ParserStructureConcatenationTest` to verify `concat` behavior and operation distribution --- .../ParserStructureConcatenationTest.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 core/common/test/format/ParserStructureConcatenationTest.kt diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt new file mode 100644 index 000000000..7c98a23f9 --- /dev/null +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.format + +import kotlinx.datetime.internal.format.parser.ConstantNumberConsumer +import kotlinx.datetime.internal.format.parser.NumberSpanParserOperation +import kotlinx.datetime.internal.format.parser.ParserStructure +import kotlinx.datetime.internal.format.parser.concat +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ParserStructureConcatenationTest { + + @Test + fun concatDistributesTopLevelNumberSpanParserOperationIntoBranches() { + val parser = ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("12"))) + ), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ), + ParserStructure( + operations = listOf(), + followedBy = listOf() + ) + ) + ) + + val actual = listOf(parser).concat() + + with(actual) { + assertEquals(0, operations.size) + assertEquals(2, followedBy.size) + with(followedBy[0]) { + assertEquals(0, followedBy.size) + with(operations) { + assertEquals(1, size) + assertTrue(operations[0] is NumberSpanParserOperation) + assertEquals(2, (operations[0] as NumberSpanParserOperation).consumers.size) + } + } + with(followedBy[1]) { + assertEquals(0, followedBy.size) + with(operations) { + assertEquals(1, size) + with(operations) { + assertEquals(1, size) + assertTrue(operations[0] is NumberSpanParserOperation) + assertEquals(1, (operations[0] as NumberSpanParserOperation).consumers.size) + } + } + } + } + } +} From f05b0bf7a28839ef854e77fbf7fa6c03cc4df8a6 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 18:04:20 +0400 Subject: [PATCH 43/54] Remove unnecessary validation check for non-empty operations in `mergeOperations` of `Parser.kt`. --- core/common/src/internal/format/parser/Parser.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index eee805828..948b06894 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -53,7 +53,6 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { - require(simplifiedParserStructure.operations.isNotEmpty()) { "Cannot merge operations from empty structure" } val operationsToMerge = simplifiedParserStructure.operations val firstOperation = operationsToMerge.firstOrNull() val mergedOperations = buildList { From b4b080d5944c97ee66cbcaeda93f9a2a8fcaaea6 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 18:49:40 +0400 Subject: [PATCH 44/54] Add test case for `concat` to verify operation flattening and nesting behavior. --- .../ParserStructureConcatenationTest.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index 7c98a23f9..37d89caf2 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -15,6 +15,53 @@ import kotlin.test.assertTrue class ParserStructureConcatenationTest { + @Test + fun concatFlattensOperations() { + val parser = ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + val actual = listOf(parser).concat() + + with(actual) { + assertTrue(operations.isEmpty()) + assertEquals(1, followedBy.size) + with(followedBy[0]) { + assertEquals(1, operations.size) + assertTrue(operations[0] is NumberSpanParserOperation) + assertTrue(followedBy.isEmpty()) + } + } + } + @Test fun concatDistributesTopLevelNumberSpanParserOperationIntoBranches() { val parser = ParserStructure( From 72e8b12204a889ecfd1f5554721d13e649c2abac Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 18:51:21 +0400 Subject: [PATCH 45/54] Refactor tests in `ParserStructureConcatenationTest` to use `assertTrue` for empty collection checks. --- core/common/test/format/ParserStructureConcatenationTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index 37d89caf2..055d88cd8 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -85,10 +85,10 @@ class ParserStructureConcatenationTest { val actual = listOf(parser).concat() with(actual) { - assertEquals(0, operations.size) + assertTrue(operations.isEmpty()) assertEquals(2, followedBy.size) with(followedBy[0]) { - assertEquals(0, followedBy.size) + assertTrue(followedBy.isEmpty()) with(operations) { assertEquals(1, size) assertTrue(operations[0] is NumberSpanParserOperation) @@ -96,7 +96,7 @@ class ParserStructureConcatenationTest { } } with(followedBy[1]) { - assertEquals(0, followedBy.size) + assertTrue(followedBy.isEmpty()) with(operations) { assertEquals(1, size) with(operations) { From ae9a10c681ef1ea7327a10251d270cceaf256912 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 19:05:59 +0400 Subject: [PATCH 46/54] Remove redundant comments regarding invariants in `concat` function of `Parser.kt`. --- core/common/src/internal/format/parser/Parser.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 948b06894..20a682d28 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -46,7 +46,6 @@ internal class ParserStructure( * Simplifies the result by merging number spans and handling unconditional modifications. */ internal fun List>.concat(): ParserStructure { - // Invariant: only called when simplifiedParserStructure.operations is non-empty fun mergeOperations( baseOperations: List>, numberSpan: List>?, @@ -116,7 +115,6 @@ internal fun List>.concat(): ParserStructure { listOf(simplified) }.ifEmpty { if (other.operations.isNotEmpty()) { - // The invariant is preserved: other.operations is non-empty return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other) } // [other] has no operations, just alternatives; use them as our tails @@ -132,7 +130,6 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, mergedTails) } else { // Distribute number span across alternatives that start with number spans - // The invariant is preserved: the structure coming from the recursive [simplifyAndAppend] calls val newTails = mergedTails.map { structure -> mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, structure) } From 4f3ac206822cf5fc41bc3d3b9bc0478fa26e8373 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 19:34:34 +0400 Subject: [PATCH 47/54] Add test for `concat` to verify distribution of `UnconditionalModification` after `NumberSpanParserOperation`. --- .../ParserStructureConcatenationTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index 055d88cd8..0e2477c4c 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -8,6 +8,7 @@ package kotlinx.datetime.test.format import kotlinx.datetime.internal.format.parser.ConstantNumberConsumer import kotlinx.datetime.internal.format.parser.NumberSpanParserOperation import kotlinx.datetime.internal.format.parser.ParserStructure +import kotlinx.datetime.internal.format.parser.UnconditionalModification import kotlinx.datetime.internal.format.parser.concat import kotlin.test.Test import kotlin.test.assertEquals @@ -15,6 +16,38 @@ import kotlin.test.assertTrue class ParserStructureConcatenationTest { + @Test + fun concatDistributesUnconditionalModificationAfterNumberSpanParserOperation() { + val parser = ParserStructure( + operations = listOf( + UnconditionalModification { } + ), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ) + ) + ) + + val actual = listOf(parser).concat() + + with(actual) { + assertTrue(operations.isEmpty()) + with(followedBy) { + assertEquals(1, size) + with(followedBy[0]) { + assertEquals(2, operations.size) + assertTrue(operations[0] is NumberSpanParserOperation) + assertTrue(operations[1] is UnconditionalModification) + assertTrue(followedBy.isEmpty()) + } + } + } + } + @Test fun concatFlattensOperations() { val parser = ParserStructure( From bbf5bc5320a303995aea511c82b683468730db11 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 19:35:26 +0400 Subject: [PATCH 48/54] Fix a bug --- core/common/src/internal/format/parser/Parser.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 20a682d28..7788c6bcb 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -121,11 +121,10 @@ internal fun List>.concat(): ParserStructure { other.followedBy } - return if (currentNumberSpan == null) { - newOperations.addAll(unconditionalModifications) - ParserStructure(newOperations, mergedTails) - } else if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { - newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + return if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { + if (currentNumberSpan != null) { + newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + } newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else { From 3c88d25431992c40897dd66787b0fb3d8542a4cb Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 19:45:25 +0400 Subject: [PATCH 49/54] Refactor `ParserStructureConcatenationTest` to inline parser creation in `concat` tests. --- .../ParserStructureConcatenationTest.kt | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index 0e2477c4c..ae7472e98 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -18,21 +18,21 @@ class ParserStructureConcatenationTest { @Test fun concatDistributesUnconditionalModificationAfterNumberSpanParserOperation() { - val parser = ParserStructure( - operations = listOf( - UnconditionalModification { } - ), - followedBy = listOf( - ParserStructure( - operations = listOf( - NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) - ), - followedBy = listOf() + val actual = listOf( + ParserStructure( + operations = listOf( + UnconditionalModification { } + ), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ) ) ) - ) - - val actual = listOf(parser).concat() + ).concat() with(actual) { assertTrue(operations.isEmpty()) @@ -50,26 +50,28 @@ class ParserStructureConcatenationTest { @Test fun concatFlattensOperations() { - val parser = ParserStructure( - operations = listOf(), - followedBy = listOf( - ParserStructure( - operations = listOf(), - followedBy = listOf( - ParserStructure( - operations = listOf(), - followedBy = listOf( - ParserStructure( - operations = listOf(), - followedBy = listOf( - ParserStructure( - operations = listOf(), - followedBy = listOf( - ParserStructure( - operations = listOf( - NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) - ), - followedBy = listOf() + val actual = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf(), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ) ) ) ) @@ -80,9 +82,7 @@ class ParserStructureConcatenationTest { ) ) ) - ) - - val actual = listOf(parser).concat() + ).concat() with(actual) { assertTrue(operations.isEmpty()) @@ -97,25 +97,25 @@ class ParserStructureConcatenationTest { @Test fun concatDistributesTopLevelNumberSpanParserOperationIntoBranches() { - val parser = ParserStructure( - operations = listOf( - NumberSpanParserOperation(listOf(ConstantNumberConsumer("12"))) - ), - followedBy = listOf( - ParserStructure( - operations = listOf( - NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) - ), - followedBy = listOf() + val actual = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("12"))) ), - ParserStructure( - operations = listOf(), - followedBy = listOf() + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ), + ParserStructure( + operations = listOf(), + followedBy = listOf() + ) ) ) - ) - - val actual = listOf(parser).concat() + ).concat() with(actual) { assertTrue(operations.isEmpty()) From 75e00b0715b011c13fdc13e5f0d1144d32b442de Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 20:06:38 +0400 Subject: [PATCH 50/54] Add test for `concat` to verify distribution of `NumberSpanParserOperation` before `UnconditionalModification`. --- .../ParserStructureConcatenationTest.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index ae7472e98..0f81167af 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -16,6 +16,51 @@ import kotlin.test.assertTrue class ParserStructureConcatenationTest { + /* + * ---- ------ + * / / + * ---- concat_with ---- ==> -- + * + * , - NumberSpanParserOperations + * - UnconditionalModification + */ + // Reproducer from https://github.com/Kotlin/kotlinx-datetime/pull/585 + @Test + fun concatDistributesNumberSpanParserOperation() { + val actual = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("12"))) + ), + followedBy = listOf() + ), + ParserStructure( + operations = listOf(UnconditionalModification { }), + followedBy = listOf( + ParserStructure( + operations = listOf( + NumberSpanParserOperation(listOf(ConstantNumberConsumer("34"))) + ), + followedBy = listOf() + ) + ) + ) + ).concat() + + with(actual) { + assertTrue(operations.isEmpty()) + with(followedBy) { + assertEquals(1, size) + with(this[0]) { + assertEquals(2, operations.size) + assertTrue(operations[0] is NumberSpanParserOperation) + assertEquals(2, (operations[0] as NumberSpanParserOperation).consumers.size) + assertTrue(operations[1] is UnconditionalModification) + } + } + } + } + @Test fun concatDistributesUnconditionalModificationAfterNumberSpanParserOperation() { val actual = listOf( From 87640fe61f7cd37a71e9a455132ee9ba9000f4ba Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 20:07:15 +0400 Subject: [PATCH 51/54] Refactor comment --- core/common/test/format/ParserStructureConcatenationTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index 0f81167af..c393a988b 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -17,6 +17,8 @@ import kotlin.test.assertTrue class ParserStructureConcatenationTest { /* + * Reproducer from https://github.com/Kotlin/kotlinx-datetime/pull/585 + * * ---- ------ * / / * ---- concat_with ---- ==> -- @@ -24,7 +26,7 @@ class ParserStructureConcatenationTest { * , - NumberSpanParserOperations * - UnconditionalModification */ - // Reproducer from https://github.com/Kotlin/kotlinx-datetime/pull/585 + // @Test fun concatDistributesNumberSpanParserOperation() { val actual = listOf( From aaf58eca87af30ba40dd6be660d4787d69633a13 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 3 Dec 2025 20:43:56 +0400 Subject: [PATCH 52/54] Refactor `Parser.kt` to replace `if-else` with `when` for operation handling. --- .../src/internal/format/parser/Parser.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 7788c6bcb..41ce7f562 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -84,22 +84,24 @@ internal fun List>.concat(): ParserStructure { val unconditionalModifications = mutableListOf>() for (op in operations) { - if (op is NumberSpanParserOperation) { - if (currentNumberSpan != null) { - currentNumberSpan.addAll(op.consumers) - } else { - currentNumberSpan = op.consumers.toMutableList() + when (op) { + is NumberSpanParserOperation -> { + if (currentNumberSpan != null) { + currentNumberSpan.addAll(op.consumers) + } else { + currentNumberSpan = op.consumers.toMutableList() + } } - } else if (op is UnconditionalModification) { - unconditionalModifications.add(op) - } else { - if (currentNumberSpan != null) { - newOperations.add(NumberSpanParserOperation(currentNumberSpan)) - currentNumberSpan = null - newOperations.addAll(unconditionalModifications) - unconditionalModifications.clear() + is UnconditionalModification -> unconditionalModifications.add(op) + else -> { + if (currentNumberSpan != null) { + newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + currentNumberSpan = null + newOperations.addAll(unconditionalModifications) + unconditionalModifications.clear() + } + newOperations.add(op) } - newOperations.add(op) } } From 56b5b4d47e9ed3e5d88d027dec08c66388b1930e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 4 Dec 2025 10:38:01 +0400 Subject: [PATCH 53/54] Add some optimization for unconditionalModifications promotion --- core/common/src/internal/format/parser/Parser.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 41ce7f562..280259750 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -123,7 +123,10 @@ internal fun List>.concat(): ParserStructure { other.followedBy } - return if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { + return if ( + currentNumberSpan == null && newOperations.isNotEmpty() || + mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation } + ) { if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) } From 0c14518fd08946eae121e67e11662b4a209e57b4 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 4 Dec 2025 10:40:00 +0400 Subject: [PATCH 54/54] Fix typo in comment of `ParserStructureConcatenationTest` test case. --- core/common/test/format/ParserStructureConcatenationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/test/format/ParserStructureConcatenationTest.kt b/core/common/test/format/ParserStructureConcatenationTest.kt index c393a988b..77b9f0cc1 100644 --- a/core/common/test/format/ParserStructureConcatenationTest.kt +++ b/core/common/test/format/ParserStructureConcatenationTest.kt @@ -19,7 +19,7 @@ class ParserStructureConcatenationTest { /* * Reproducer from https://github.com/Kotlin/kotlinx-datetime/pull/585 * - * ---- ------ + * ---- ------ * / / * ---- concat_with ---- ==> -- *