From c0c09aa78a508962b6a244e0d715653c2c1ff78e Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:27:54 -0300 Subject: [PATCH 01/56] Added new gene: AssertionRxGene. --- .../core/search/gene/regex/AssertionRxGene.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt new file mode 100644 index 0000000000..0c984fc52b --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -0,0 +1,105 @@ +package org.evomaster.core.search.gene.regex + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.root.SimpleGene +import org.evomaster.core.search.gene.utils.GeneUtils +import org.evomaster.core.search.service.AdaptiveParameterControl +import org.evomaster.core.search.service.Randomness +import org.evomaster.core.search.service.mutator.MutationWeightControl +import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo +import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy + +/** + * Specifies what kind of zero-width assertion an [AssertionRxGene] represents. + */ +enum class AssertionType { + /** (?=...) */ + LOOKAHEAD, + // TODO implement more assertion types +} + +/** + * Represents a zero-width assertion in the regex gene tree. + * + * This gene is placed in the tree at the position where the assertion appeared in the + * source regex. It produces no characters ([getValueAsPrintableString] always returns + * ""). + * + * Repair is triggered from [RegexGene.randomize] after the main tree is sampled and + * validation against the source pattern fails. See [RegexGene.attemptAssertionRepair]. + */ +class AssertionRxGene( + val assertionType: AssertionType = AssertionType.LOOKAHEAD, + val innerGene: DisjunctionListRxGene? +) : RxTerm, SimpleGene("assertion") { + + override fun checkForLocallyValidIgnoringChildren(): Boolean = true + + override fun isUnsatisfiable(): Boolean { + return when (assertionType) { + AssertionType.LOOKAHEAD -> innerGene == null + } + } + + override fun isMutable(): Boolean = innerGene?.isMutable() ?: false + + override fun copyContent(): Gene { + val copy = AssertionRxGene(assertionType, innerGene?.copy() as? DisjunctionListRxGene) + copy.name = this.name + return copy + } + + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { + innerGene?.randomize(randomness, tryToForceNewValue) + } + + override fun setValueWithRawString(value: String) { + innerGene?.setFromStringValue(value) + } + + override fun shallowMutate( + randomness: Randomness, + apc: AdaptiveParameterControl, + mwc: MutationWeightControl, + selectionStrategy: SubsetGeneMutationSelectionStrategy, + enableAdaptiveGeneMutation: Boolean, + additionalGeneMutationInfo: AdditionalGeneMutationInfo? + ): Boolean { + return false + } + + /** + * Zero-width: never contributes characters to the generated string. + */ + override fun getValueAsPrintableString( + previousGenes: List, + mode: GeneUtils.EscapeMode?, + targetFormat: OutputFormat?, + extraCheck: Boolean + ): String = "" + + /** + * Returns the value currently sampled from [innerGene], or null if there is no + * inner gene to sample from. + */ + fun sampledInnerValue(): String? { + if (innerGene == null) return null + return innerGene.getValueAsPrintableString(targetFormat = null) + } + + override fun containsSameValueAs(other: Gene): Boolean { + if (other !is AssertionRxGene) return false + if (assertionType != other.assertionType) return false + return sampledInnerValue() == other.sampledInnerValue() + } + + override fun unsafeCopyValueFrom(other: Gene): Boolean { + if (other !is AssertionRxGene || other.assertionType != assertionType) return false + return if (innerGene != null && other.innerGene != null) { + innerGene.unsafeCopyValueFrom(other.innerGene) + } else { + innerGene == null && other.innerGene == null + } + } +} \ No newline at end of file From bc2f711c1e38cf1f197a1f1e238ae527ce5eeb67 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:28:33 -0300 Subject: [PATCH 02/56] Updated gene count to reflect new gene added. --- .../org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt index c5c7e608ff..7c3a1823fc 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneNumberOfGenesTest.kt @@ -13,7 +13,7 @@ class GeneNumberOfGenesTest : AbstractGeneTest() { This number should not change, unless you explicitly add/remove any gene. if so, update this number accordingly */ - assertEquals(94, geneClasses.size) + assertEquals(95, geneClasses.size) } } From d46545c3bf7f16d827470ed4c905aa2bb44bd641 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:32:03 -0300 Subject: [PATCH 03/56] Added sampler for new gene. --- .../evomaster/core/search/gene/GeneSamplerForTests.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 97fe808654..65a5e273a9 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -143,6 +143,7 @@ object GeneSamplerForTests { QuantifierRxGene::class -> sampleQuantifierRxGene(rand) as T RegexGene::class -> sampleRegexGene(rand) as T BackReferenceRxGene::class -> sampleBackReferenceRxGene(rand) as T + AssertionRxGene::class -> sampleAssertionRxGene(rand) as T ObjectWithAttributesGene::class -> sampleObjectGeneWithAttributes(rand) as T //SQL genes @@ -428,6 +429,12 @@ object GeneSamplerForTests { ) } + fun sampleAssertionRxGene(rand: Randomness): AssertionRxGene { + val innerGene = sampleDisjunctionListRxGene(rand) + innerGene.doInitialize(rand) + return AssertionRxGene(innerGene=innerGene) + } + fun sampleRegexGene(rand: Randomness): RegexGene { return RegexGene( name = "rand RegexGene", @@ -473,7 +480,7 @@ object GeneSamplerForTests { //let's avoid huge trees... .filter { (it.java != DisjunctionListRxGene::class.java && it.java != DisjunctionRxGene::class.java - && it.java != BackReferenceRxGene::class.java) // as this also contains a DisjunctionListRxGene within + && it.java != BackReferenceRxGene::class.java && it.java != AssertionRxGene::class.java) // as this also contains a DisjunctionListRxGene within || rand.nextBoolean() } From 06bf520636466f539276c2243f0d6deb3930db27 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:37:24 -0300 Subject: [PATCH 04/56] Lowered chance for large trees. --- .../org/evomaster/core/search/gene/GeneSamplerForTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 65a5e273a9..bce6691c31 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -481,7 +481,7 @@ object GeneSamplerForTests { .filter { (it.java != DisjunctionListRxGene::class.java && it.java != DisjunctionRxGene::class.java && it.java != BackReferenceRxGene::class.java && it.java != AssertionRxGene::class.java) // as this also contains a DisjunctionListRxGene within - || rand.nextBoolean() + || rand.nextBoolean(0.2) } val numberOfTerms = rand.nextInt(1, 3) From c8397fd85b643c1c09fc41000222111574e99b00 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:47:19 -0300 Subject: [PATCH 05/56] Added new interface for assertion repair usage. --- .../core/search/gene/regex/RxAbsorbable.kt | 40 +++++++++++++++++++ .../core/search/gene/regex/RxTerm.kt | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt new file mode 100644 index 0000000000..e68282d443 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -0,0 +1,40 @@ +package org.evomaster.core.search.gene.regex + +/** + * Defines the interface for regex genes that can "absorb" part of a candidate + * string during assertion repair. + * + * When a sampled string fails to satisfy an assertion, the repair loop samples a + * candidate string from [AssertionRxGene.innerGene] and greedily walks the genes + * around the assertion in its parent [DisjunctionRxGene]'s terms list, + * asking each gene how much of the remaining candidate it can absorb and committing + * that amount before moving to the next gene. + * + * Regex genes that do not override the defaults are treated as non-absorbable (return 0, + * throw on force attempt). + */ +interface RxAbsorbable { + + /** + * Maximum number of leading characters of [value] this gene can be forced to + * produce, or already produces without needing to change. Default 0 = cannot help. + */ + fun absorbableCount(value: String): Int = 0 + + /** + * Places as many of [value]'s leading characters as possible, making the same + * decision [absorbableCount] would for this same [value]. This mutates internal state in + * place, and returns how many characters were actually placed. + */ + fun tryForce(value: String): Int { + throw IllegalStateException( + "${this::class.simpleName} does not support forcing but tryForce was called" + ) + } + + /** + * Read-only: could tryForce("") succeed on this gene right now, without actually + * calling it? Default false. + */ + fun canBeZeroWidth(): Boolean = false +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxTerm.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxTerm.kt index d6ccdf3574..dd34fac2bd 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxTerm.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxTerm.kt @@ -4,7 +4,7 @@ import org.evomaster.core.search.StructuralElement import org.evomaster.core.search.gene.Gene -interface RxTerm { +interface RxTerm: RxAbsorbable { /** * Returns true if this gene can never produce a valid value, * for example an empty character class intersection like [a&&b]. From 44f421d73cdac80e335e4307bcaa373b83b186f8 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 02:16:09 -0300 Subject: [PATCH 06/56] Added external flags to RegexGene. --- .../org/evomaster/core/search/gene/regex/RegexGene.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index 4787152f1c..aebc166830 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -11,6 +11,7 @@ import org.evomaster.core.search.service.Randomness import org.evomaster.core.search.service.mutator.MutationWeightControl import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy +import org.evomaster.core.utils.RegexFlags import java.util.regex.Pattern /** @@ -29,12 +30,13 @@ class RegexGene( * so, this is a reasonable workaround */ var fixedValue: String? = null, - var usingFixedValue: Boolean = false + var usingFixedValue: Boolean = false, + val externalRegexFlags: RegexFlags = RegexFlags() ) : CompositeFixedGene(name, disjunctions) { override fun copyContent(): Gene { - return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue) + return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags) } override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { From 655c9153e236f874ef4fea8a455c25bc5d17dead Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 02:18:32 -0300 Subject: [PATCH 07/56] Passed external flags to RegexGene. --- .../kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index c1a064d405..0b02a6f0a8 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -11,7 +11,7 @@ private const val EOF_TOKEN = "" /** * Created by arcuri82 on 11-Sep-19. */ -class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaBaseVisitor(){ +class GeneRegexJavaVisitor(val externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaBaseVisitor(){ private val hexEscapePrefixes = setOf('x', 'u') @@ -108,7 +108,8 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege "regex", disjList, text.substring(0, text.length - EOF_TOKEN.length), - RegexType.JVM + RegexType.JVM, + externalRegexFlags = externalRegexFlags ) return VisitResult(gene) From 0540b2df32f1a221c854ceb7a195ee33873d0b29 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 02:21:04 -0300 Subject: [PATCH 08/56] Added MultiCharacterRange.contains(Char) method. --- .../evomaster/core/utils/MultiCharacterRange.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt b/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt index 77c8b5f680..67e23994a7 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt @@ -204,6 +204,23 @@ class MultiCharacterRange internal constructor(val ranges: List) return intersect(this, other) } + /** + * Check if [c] is contained in this [MultiCharacterRange]. + */ + fun contains(c: Char): Boolean { + if (isEmpty) return false + // binary search as MultiCharacterRange's ranges are ordered and non-overlapping + return ranges.binarySearch { (start, end) -> + if(c in start..end) { + 0 + } else if (c < start) { + -1 + } else { + 1 + } + } >= 0 + } + val isEmpty: Boolean get() = ranges.isEmpty() val isNotEmpty: Boolean get() = ranges.isNotEmpty() val size: Int get() = ranges.size From 7e45828d3a62cf64cbcdeea3bf9120d84e6035e2 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 02:42:14 -0300 Subject: [PATCH 09/56] Added overrides for new interface RxAbsorbable methods for some regex genes. --- .../search/gene/regex/AnyCharacterRxGene.kt | 13 +++++ .../core/search/gene/regex/AssertionRxGene.kt | 7 +++ .../gene/regex/CharacterClassEscapeRxGene.kt | 26 ++++++++++ .../search/gene/regex/CharacterRangeRxGene.kt | 27 +++++++++++ .../gene/regex/PatternCharacterBlockGene.kt | 27 +++++++++++ .../search/gene/regex/QuantifierRxGene.kt | 47 ++++++++++++++++++- 6 files changed, 146 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt index 847937f9cb..b7be5a6fa8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt @@ -131,4 +131,17 @@ class AnyCharacterRxGene( return true } + override fun absorbableCount(value: String): Int { + if (value.isEmpty()) return 0 + return if (validRanges.contains(value[0])) 1 else 0 + } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableCount(value) + if (n == 1) { + this.value = value[0] + } + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 0c984fc52b..9f7e39ad00 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -102,4 +102,11 @@ class AssertionRxGene( innerGene == null && other.innerGene == null } } + + override fun canBeZeroWidth(): Boolean = true + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + return 0 + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt index a127e16055..54dea84aea 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt @@ -115,6 +115,9 @@ class CharacterClassEscapeRxGene( companion object{ private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java) + private fun Char.swapCase(): Char = + if (this.isUpperCase()) this.lowercaseChar() else this.uppercaseChar() + private val digitSet = listOf(CharacterRange('0', '9')) private val asciiLetterSet = listOf(CharacterRange(FIRST_LOWER_CASE_CHAR, LAST_LOWER_CASE_CHAR), CharacterRange(FIRST_UPPER_CASE_CHAR, LAST_UPPER_CASE_CHAR)) @@ -352,4 +355,27 @@ class CharacterClassEscapeRxGene( return false } + override fun absorbableCount(value: String): Int { + if (value.isEmpty()) return 0 + val c = value[0] + if (multiCharRange.contains(c)) return 1 + if (flags.isCaseable(c) && multiCharRange.contains(c.swapCase())) return 1 + return 0 + } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableCount(value) + require(n == 0 || n == 1) + if (n == 1) { + val c = value[0] + this.value = (if (multiCharRange.contains(c)) { + c + } else { + c.swapCase() + }).toString() + this.useUpperCase = c.isUpperCase() + } + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt index 139bda468b..597e35b731 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt @@ -28,6 +28,9 @@ class CharacterRangeRxGene( companion object{ private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java) + + private fun Char.swapCase(): Char = + if (this.isUpperCase()) this.lowercaseChar() else this.uppercaseChar() } // '\u0000' is a placeholder for the unsatisfiable case (empty MCR with no valid ranges). @@ -180,4 +183,28 @@ class CharacterRangeRxGene( return false } + + override fun absorbableCount(value: String): Int { + if (value.isEmpty()) return 0 + val c = value[0] + if (validRanges.contains(c)) return 1 + if (flags.isCaseable(c) && validRanges.contains(c.swapCase())) return 1 + return 0 + } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableCount(value) + require(n == 0 || n == 1) + if (n == 1) { + val c = value[0] + this.value = if (validRanges.contains(c)) { + c + } else { + c.swapCase() + } + this.useUpperCase = c.isUpperCase() + } + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index cb1fd3cd54..1415dec7ca 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -115,4 +115,31 @@ class PatternCharacterBlockGene( return containsSameValueAs(other) } + override fun absorbableCount(value: String): Int { + var i = 0 + while (i < value.length && i < stringBlock.length) { + val c = stringBlock[i] + val matches = if (flags.isCaseable(c)) + value[i].equals(c, ignoreCase = true) + else + value[i] == c + if (!matches) return 0 + i++ + } + return i + } + + override fun canBeZeroWidth(): Boolean = stringBlock.isEmpty() + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableCount(value) + require(n == stringBlock.length || n == value.length || n==0) + for (i in 0 until n) { + if (flags.isCaseable(stringBlock[i])) { + caseChoices[i] = value[i].isUpperCase() + } + } + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index 1ed5edf059..6338455611 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -1,6 +1,5 @@ package org.evomaster.core.search.gene.regex -import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeGene import org.evomaster.core.search.gene.Gene @@ -240,4 +239,50 @@ class QuantifierRxGene( true } } + + override fun absorbableCount(value: String): Int { + if (value.isEmpty() || atoms.isEmpty()) return 0 + val absTemplate = template as? RxAbsorbable ?: return 0 + var consumed = 0 + var slotsUsed = 0 + while (consumed < value.length && slotsUsed < atoms.size) { + val perSlot = absTemplate.absorbableCount(value.substring(consumed)) + if (perSlot == 0) break + consumed += perSlot + slotsUsed++ + } + return consumed + } + + override fun canBeZeroWidth(): Boolean = + min == 0 || (template as? RxAbsorbable)?.canBeZeroWidth() == true + + override fun tryForce(value: String): Int { + val absTemplate = template as RxAbsorbable + var remaining = value + // we first determine pieces for each atom before mutating, storing them on a list. + val plannedAtomValues = mutableListOf() + while (remaining.isNotEmpty() && plannedAtomValues.size < atoms.size) { + val canTake = absTemplate.absorbableCount(remaining) + if (canTake == 0) break + plannedAtomValues.add(remaining.take(canTake)) + remaining = remaining.substring(canTake) + } + if (plannedAtomValues.isNotEmpty()) { + for (i in plannedAtomValues.indices) { + val atom = atoms[i] as RxAbsorbable + atom.tryForce(plannedAtomValues[i]) + } + return value.length - remaining.length + } + if (canBeZeroWidth()) { + if (min == 0) { + killAllChildren() + } else { + atoms.forEach { (it as RxAbsorbable).tryForce("") } + } + } + return 0 + } + } From b0f694bd84ff4b5c33bec4c089440d904131bb0c Mon Sep 17 00:00:00 2001 From: lmasroca Date: Wed, 15 Jul 2026 00:46:56 -0300 Subject: [PATCH 10/56] Updated RxAbsorbable, added some methods and overrides. --- .../core/search/gene/regex/AssertionRxGene.kt | 4 ++++ .../gene/regex/PatternCharacterBlockGene.kt | 5 ++++ .../core/search/gene/regex/RxAbsorbable.kt | 23 ++++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 9f7e39ad00..9e105028b6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -109,4 +109,8 @@ class AssertionRxGene( require(value.isNotEmpty()) return 0 } + + override fun forceZeroWidth() { + // already always zero-width, nothing to do + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index 1415dec7ca..e512804f24 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -142,4 +142,9 @@ class PatternCharacterBlockGene( } return n } + + override fun forceZeroWidth() { + require(canBeZeroWidth()) + // stringBlock is empty, so there is nothing to place - already zero-width. + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt index e68282d443..7440cff8cd 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -9,9 +9,6 @@ package org.evomaster.core.search.gene.regex * around the assertion in its parent [DisjunctionRxGene]'s terms list, * asking each gene how much of the remaining candidate it can absorb and committing * that amount before moving to the next gene. - * - * Regex genes that do not override the defaults are treated as non-absorbable (return 0, - * throw on force attempt). */ interface RxAbsorbable { @@ -22,9 +19,9 @@ interface RxAbsorbable { fun absorbableCount(value: String): Int = 0 /** - * Places as many of [value]'s leading characters as possible, making the same - * decision [absorbableCount] would for this same [value]. This mutates internal state in - * place, and returns how many characters were actually placed. + * Places as many of [value]'s leading characters as possible and returns how many characters were actually placed. + * + * Precondition: [value] is not empty. */ fun tryForce(value: String): Int { throw IllegalStateException( @@ -33,8 +30,18 @@ interface RxAbsorbable { } /** - * Read-only: could tryForce("") succeed on this gene right now, without actually - * calling it? Default false. + * Read-only: could [forceZeroWidth] succeed on this gene right now? Default false. */ fun canBeZeroWidth(): Boolean = false + + /** + * Forces this gene to render as "". Unlike [tryForce], there is nothing to plan or report. + * + * Precondition: [canBeZeroWidth] + */ + fun forceZeroWidth() { + throw IllegalStateException( + "${this::class.simpleName} cannot be zero-width but forceZeroWidth was called" + ) + } } \ No newline at end of file From ac93549d38dfc8ef2295a64718c773c8250a4c61 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Wed, 15 Jul 2026 20:47:38 -0300 Subject: [PATCH 11/56] Added lookaheads to java regex grammar. --- .../org/evomaster/core/parser/RegexJava.g4 | 9 +++--- .../core/parser/GeneRegexJavaVisitor.kt | 28 ++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJava.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJava.g4 index 0f9d358f03..220817f748 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJava.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJava.g4 @@ -49,7 +49,7 @@ assertion //TODO //// | '\\' 'b' //// | '\\' 'B' -//// | '(' '?' '=' disjunction ')' + | PAREN_open QUESTION EQUAL disjunction PAREN_close //// | '(' '?' '!' disjunction ')' ; @@ -179,7 +179,7 @@ patternCharacter // These are also allowed as literals when no matching pair exists | BRACE_close | BRACKET_close - | COLON + | COLON | EQUAL | DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on. ; @@ -231,7 +231,7 @@ classAtomNoDash | DecimalDigit | COMMA | CARET | DOLLAR | DOT | STAR | PLUS | QUESTION | PAREN_open | PAREN_close | BRACKET_open | BRACE_open | BRACE_close | OR | E | Q - | COLON + | COLON | EQUAL // should be interpreted literally: // As they are lexer tokens, these character sequences are captured as such. In particular these require some extra // steps to interpret them correctly given the context. @@ -274,6 +274,7 @@ CharacterClassEscape : SLASH [dDsSwWvVhH] ; +EQUAL : '='; CARET : '^'; DOLLAR : '$'; SLASH : '\\'; @@ -298,7 +299,7 @@ E: 'E'; BaseChar // practically all chars but the ones used for control and digits - : ~[0-9:,^$\\.*+?()[\]{}|-] + : ~[0-9:,^$\\.*+?()[\]{}|=-] ; fragment OctalEscapeSequence diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 0b02a6f0a8..9e5c7f45ff 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -87,6 +87,24 @@ class GeneRegexJavaVisitor(val externalRegexFlags: RegexFlags = RegexFlags()) : return disjList } + /** + * Walks up [ctx]'s ancestry towards the top-level pattern, throwing as soon as it + * finds one of the currently-unsupported ways an assertion's ancestry can appear (nested). + * Returns normally if it reaches the top-level pattern without hitting either. + */ + private fun rejectIfNestedAssertion(ctx: RegexJavaParser.AssertionContext) { + var current = ctx.parent + while (current != null && current !is RegexJavaParser.PatternContext) { + if (current is RegexJavaParser.AssertionContext) { + throw IllegalStateException("Nested assertions not supported") + } + if (current is RegexJavaParser.AtomContext && current.disjunction() != null) { + throw IllegalStateException("Assertions inside groups are not currently supported") + } + current = current.parent + } + } + override fun visitPattern(ctx: RegexJavaParser.PatternContext): VisitResult { val res = ctx.disjunction().accept(this) @@ -232,7 +250,15 @@ class GeneRegexJavaVisitor(val externalRegexFlags: RegexFlags = RegexFlags()) : val res = VisitResult() if(ctx.assertion() != null){ - res.data = ctx.assertion().text + val assertionCtx = ctx.assertion() + if (assertionCtx.CARET() != null || assertionCtx.DOLLAR() != null) { + res.data = ctx.assertion().text + } else { + rejectIfNestedAssertion(ctx.assertion()) + val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) + val assertionGene = AssertionRxGene(innerDisjList) + res.genes.add(assertionGene) + } return res } From a58d4e2cec4be902ceef5c132a3ea0495f481a49 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 18:48:08 -0300 Subject: [PATCH 12/56] Added AssertionRepairWalk util and missing RxAbsorbable overrides. --- .../search/gene/regex/BackReferenceRxGene.kt | 5 ++ .../gene/regex/DisjunctionListRxGene.kt | 61 +++++++++++++++++++ .../search/gene/regex/DisjunctionRxGene.kt | 53 ++++++++++++++++ .../search/gene/regex/QuantifierRxGene.kt | 51 ++++------------ .../search/gene/utils/AssertionRepairWalk.kt | 59 ++++++++++++++++++ 5 files changed, 191 insertions(+), 38 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt index 8a85458d28..7be7853a9e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt @@ -81,4 +81,9 @@ class BackReferenceRxGene( // nothing to copy, as the value comes from the capture group return containsSameValueAs(other) } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + return 0 + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 43676bcfb4..4058cf983f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -206,4 +206,65 @@ class DisjunctionListRxGene( return true } + + /** + * Ranks all branches by how much of [value] they can absorb, without mutating + * anything. + */ + private fun rankBranches(value: String): Pair? { + if (value.isEmpty() || disjunctions.isEmpty()) return null + var bestCount = disjunctions[activeDisjunction].absorbableCount(value) + var bestIndex = activeDisjunction + for (i in disjunctions.indices) { + if (i == activeDisjunction) continue + if (bestCount == value.length) break + val can = disjunctions[i].absorbableCount(value) + if (can > bestCount) { + bestCount = can + bestIndex = i + } + } + return bestCount to bestIndex + } + + override fun absorbableCount(value: String): Int = + rankBranches(value)?.first ?: 0 + + override fun canBeZeroWidth(): Boolean = disjunctions.any { it.canBeZeroWidth() } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val (bestCount, bestIndex) = rankBranches(value) ?: (0 to activeDisjunction) + + if (bestCount > 0) { + if (bestIndex != activeDisjunction) { + activeDisjunction = bestIndex + tryToActivateGene(disjunctions[bestIndex]) + } + return disjunctions[bestIndex].tryForce(value) + } + + return 0 + } + + override fun forceZeroWidth() { + require(canBeZeroWidth()) + // try the active branch first to avoid an unnecessary switch + val order = listOf(activeDisjunction) + disjunctions.indices.filter { it != activeDisjunction } + val target = order.first { disjunctions[it].canBeZeroWidth() } + disjunctions[target].forceZeroWidth() + if (target != activeDisjunction) { + activeDisjunction = target + tryToActivateGene(disjunctions[target]) + } + } + + /** + * Delegates assertion repair to whichever branch is currently active, as only that + * branch's rendered value is ever observed, so only it needs repairing. See + * [DisjunctionRxGene.attemptAssertionRepair] for the actual repair logic. + */ + fun attemptAssertionRepair(randomness: Randomness) { + disjunctions.getOrNull(activeDisjunction)?.attemptAssertionRepair(randomness) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 4582495b4e..4960f573e2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -4,6 +4,7 @@ import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.utils.AssertionRepairWalk import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionRxGeneImpact import org.evomaster.core.search.service.AdaptiveParameterControl @@ -14,6 +15,11 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation import org.slf4j.Logger import org.slf4j.LoggerFactory +/** + * How many times a single [DisjunctionRxGene] tries to fix one of its own direct-term + * assertions, see [DisjunctionRxGene.attemptAssertionRepair]. + */ +const val MAX_LOCAL_ASSERTION_ATTEMPTS = 20 class DisjunctionRxGene( name: String, @@ -184,4 +190,51 @@ class DisjunctionRxGene( return ok } + override fun absorbableCount(value: String): Int = + AssertionRepairWalk.absorbableCount(terms, value) + + override fun canBeZeroWidth(): Boolean = + terms.all { (it as RxAbsorbable).canBeZeroWidth() } + + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + val placed = AssertionRepairWalk.tryForce(terms, value) + if (placed == 0 && canBeZeroWidth()) { + forceZeroWidth() + } + return placed + } + + override fun forceZeroWidth() { + require(canBeZeroWidth()) + terms.forEach { (it as RxAbsorbable).forceZeroWidth() } + } + + /** + * Attempts to repair this disjunction's own value so that each of its direct-term + * [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner + * value onto the genes that follow it within [terms]. + */ + fun attemptAssertionRepair(randomness: Randomness) { + if (terms.none { it is AssertionRxGene }) return + + for (idx in terms.indices) { + val assertion = terms[idx] as? AssertionRxGene ?: continue + if (assertion.innerGene == null) continue + + val genesAfter = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } + if (genesAfter.isEmpty()) return + + var satisfied = false + for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { + assertion.randomize(randomness, false) + val candidate = assertion.sampledInnerValue() ?: break + if (candidate.isEmpty() || AssertionRepairWalk.tryForce(genesAfter, candidate) == candidate.length) { + satisfied = true + break + } + } + if (!satisfied) return + } + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index 6338455611..d8eae322a2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -3,6 +3,7 @@ package org.evomaster.core.search.gene.regex import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeGene import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.utils.AssertionRepairWalk import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.AdaptiveParameterControl import org.evomaster.core.search.service.Randomness @@ -240,49 +241,23 @@ class QuantifierRxGene( } } - override fun absorbableCount(value: String): Int { - if (value.isEmpty() || atoms.isEmpty()) return 0 - val absTemplate = template as? RxAbsorbable ?: return 0 - var consumed = 0 - var slotsUsed = 0 - while (consumed < value.length && slotsUsed < atoms.size) { - val perSlot = absTemplate.absorbableCount(value.substring(consumed)) - if (perSlot == 0) break - consumed += perSlot - slotsUsed++ - } - return consumed - } + override fun absorbableCount(value: String): Int = + AssertionRepairWalk.absorbableCount(atoms, value) override fun canBeZeroWidth(): Boolean = min == 0 || (template as? RxAbsorbable)?.canBeZeroWidth() == true override fun tryForce(value: String): Int { - val absTemplate = template as RxAbsorbable - var remaining = value - // we first determine pieces for each atom before mutating, storing them on a list. - val plannedAtomValues = mutableListOf() - while (remaining.isNotEmpty() && plannedAtomValues.size < atoms.size) { - val canTake = absTemplate.absorbableCount(remaining) - if (canTake == 0) break - plannedAtomValues.add(remaining.take(canTake)) - remaining = remaining.substring(canTake) - } - if (plannedAtomValues.isNotEmpty()) { - for (i in plannedAtomValues.indices) { - val atom = atoms[i] as RxAbsorbable - atom.tryForce(plannedAtomValues[i]) - } - return value.length - remaining.length - } - if (canBeZeroWidth()) { - if (min == 0) { - killAllChildren() - } else { - atoms.forEach { (it as RxAbsorbable).tryForce("") } - } - } - return 0 + require(value.isNotEmpty()) + return AssertionRepairWalk.tryForce(atoms, value) } + override fun forceZeroWidth() { + require(canBeZeroWidth()) + if (min == 0) { + killAllChildren() + } else { + atoms.forEach { (it as RxAbsorbable).forceZeroWidth() } + } + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt new file mode 100644 index 0000000000..d1c0958678 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -0,0 +1,59 @@ +package org.evomaster.core.search.gene.utils + +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.regex.RxAbsorbable + +/** + * Greedy-walk utilities for assertion repair. + * + * Given a candidate string sampled from [org.evomaster.core.search.gene.regex.AssertionRxGene.innerGene], attempts to place + * as much of it as possible into a sequence of genes by asking each gene (via [org.evomaster.core.search.gene.regex.RxAbsorbable]) + * how much it can take and committing that amount. + */ +object AssertionRepairWalk { + /** + * Maximum leading characters of [value] that can be absorbed across [genes] + * left-to-right, without mutating anything. + */ + fun absorbableCount(genes: List, value: String): Int { + if (value.isEmpty()) return 0 + var consumed = 0 + for (gene in genes) { + if (consumed >= value.length) break + val absorbable = gene as RxAbsorbable + val canTake = absorbable.absorbableCount(value.substring(consumed)) + if (canTake > 0) { + consumed += canTake + continue + } + if (absorbable.canBeZeroWidth()) continue + return 0 + } + return consumed + } + + /** + * Forces as much of [value] as possible into [genes] left-to-right, mutating each + * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. + */ + fun tryForce(genes: List, value: String): Int { + if (value.isEmpty()) return 0 + var consumed = 0 + for (gene in genes) { + if (consumed >= value.length) break + val absorbable = gene as RxAbsorbable + val remaining = value.substring(consumed) + val placed = absorbable.tryForce(remaining) + if (placed == 0) { + if (absorbable.canBeZeroWidth()) { + absorbable.forceZeroWidth() + continue + } else { + return 0 + } + } + consumed += placed + } + return consumed + } +} From 919485dbd78d806f4b4bb6fa5d360677e22b64dc Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 18:51:10 -0300 Subject: [PATCH 13/56] Removed unused enum. --- .../core/search/gene/regex/AssertionRxGene.kt | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 9e105028b6..834f353aa8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -10,15 +10,6 @@ import org.evomaster.core.search.service.mutator.MutationWeightControl import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy -/** - * Specifies what kind of zero-width assertion an [AssertionRxGene] represents. - */ -enum class AssertionType { - /** (?=...) */ - LOOKAHEAD, - // TODO implement more assertion types -} - /** * Represents a zero-width assertion in the regex gene tree. * @@ -26,26 +17,22 @@ enum class AssertionType { * source regex. It produces no characters ([getValueAsPrintableString] always returns * ""). * - * Repair is triggered from [RegexGene.randomize] after the main tree is sampled and - * validation against the source pattern fails. See [RegexGene.attemptAssertionRepair]. + * Repair is triggered from [DisjunctionRxGene.attemptAssertionRepair], invoked by + * [RegexGene.randomize] after the disjunction's own sampled value is checked against + * the source pattern and found not to match. */ class AssertionRxGene( - val assertionType: AssertionType = AssertionType.LOOKAHEAD, val innerGene: DisjunctionListRxGene? ) : RxTerm, SimpleGene("assertion") { override fun checkForLocallyValidIgnoringChildren(): Boolean = true - override fun isUnsatisfiable(): Boolean { - return when (assertionType) { - AssertionType.LOOKAHEAD -> innerGene == null - } - } + override fun isUnsatisfiable(): Boolean = innerGene == null override fun isMutable(): Boolean = innerGene?.isMutable() ?: false override fun copyContent(): Gene { - val copy = AssertionRxGene(assertionType, innerGene?.copy() as? DisjunctionListRxGene) + val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene) copy.name = this.name return copy } @@ -90,12 +77,11 @@ class AssertionRxGene( override fun containsSameValueAs(other: Gene): Boolean { if (other !is AssertionRxGene) return false - if (assertionType != other.assertionType) return false return sampledInnerValue() == other.sampledInnerValue() } override fun unsafeCopyValueFrom(other: Gene): Boolean { - if (other !is AssertionRxGene || other.assertionType != assertionType) return false + if (other !is AssertionRxGene) return false return if (innerGene != null && other.innerGene != null) { innerGene.unsafeCopyValueFrom(other.innerGene) } else { From d1ed3a35e5ea5b5a02de22fc931603b703fb9a32 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 18:59:11 -0300 Subject: [PATCH 14/56] Replaced Pair usage with new data class. --- .../core/search/gene/regex/DisjunctionListRxGene.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 4058cf983f..b383d35aeb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -15,6 +15,7 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation import org.slf4j.Logger import org.slf4j.LoggerFactory +private data class BranchRanking(val absorbableCount: Int, val branchIndex: Int) class DisjunctionListRxGene( val disjunctions: List @@ -211,7 +212,7 @@ class DisjunctionListRxGene( * Ranks all branches by how much of [value] they can absorb, without mutating * anything. */ - private fun rankBranches(value: String): Pair? { + private fun rankBranches(value: String): BranchRanking? { if (value.isEmpty() || disjunctions.isEmpty()) return null var bestCount = disjunctions[activeDisjunction].absorbableCount(value) var bestIndex = activeDisjunction @@ -224,17 +225,17 @@ class DisjunctionListRxGene( bestIndex = i } } - return bestCount to bestIndex + return BranchRanking(bestCount, bestIndex) } override fun absorbableCount(value: String): Int = - rankBranches(value)?.first ?: 0 + rankBranches(value)?.absorbableCount ?: 0 override fun canBeZeroWidth(): Boolean = disjunctions.any { it.canBeZeroWidth() } override fun tryForce(value: String): Int { require(value.isNotEmpty()) - val (bestCount, bestIndex) = rankBranches(value) ?: (0 to activeDisjunction) + val (bestCount, bestIndex) = rankBranches(value) ?: BranchRanking(0, activeDisjunction) if (bestCount > 0) { if (bestIndex != activeDisjunction) { From 9d6df0d982c6369e06b1e410a1d60baaa143e3db Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 19:15:58 -0300 Subject: [PATCH 15/56] Added check and repair logic to RegexGene for JVM regexes. --- .../core/search/gene/regex/RegexGene.kt | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index aebc166830..4fd67aa942 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -12,8 +12,14 @@ import org.evomaster.core.search.service.mutator.MutationWeightControl import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy import org.evomaster.core.utils.RegexFlags +import org.evomaster.core.utils.RegexWithExternalFlags import java.util.regex.Pattern +/** + * How many times we try to resample the whole [RegexGene] and attempt repairing before we give up. + */ +const val MAX_TREE_REPAIR_ATTEMPTS = 10 + /** * A gene representing a regular expression (regex). */ @@ -39,13 +45,43 @@ class RegexGene( return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags) } + companion object { + private val patternCache = java.util.concurrent.ConcurrentHashMap() + + private fun compiledPattern(sourceRegex: String, flags: RegexFlags): Pattern { + return patternCache.computeIfAbsent(RegexWithExternalFlags(sourceRegex, flags)) { + Pattern.compile(sourceRegex, flags.toJavaFlagBitmask()) + } + } + } + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { usingFixedValue = if(fixedValue == null){ false } else { randomness.nextBoolean() } - disjunctions.randomize(randomness, tryToForceNewValue) + + if (regexType != RegexType.JVM) { + disjunctions.randomize(randomness, tryToForceNewValue) + return + } + + // this sourceRegex may have preprocessing steps applied... should do this with the original string... + val pattern = compiledPattern(sourceRegex, externalRegexFlags) + + repeat(MAX_TREE_REPAIR_ATTEMPTS) { _ -> + disjunctions.randomize(randomness, tryToForceNewValue) + if (pattern.matcher(disjunctions.getValueAsPrintableString()).find()) { + return + } + disjunctions.attemptAssertionRepair(randomness) + if (pattern.matcher(disjunctions.getValueAsPrintableString()).find()) { + return + } + } + + throw IllegalStateException("Could not repair regex value") } @Deprecated("Do not call directly outside this package. Call setFromStringValue") @@ -68,7 +104,7 @@ class RegexGene( if(regexType == RegexType.JVM){ val matcher = try{ - Pattern.compile(sourceRegex).matcher(fixedValue!!) + compiledPattern(sourceRegex, externalRegexFlags).matcher(fixedValue!!) }catch(e: Exception){ return false } From ef26857015f1370e7e49c4639185815e69e41f8d Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 19:28:32 -0300 Subject: [PATCH 16/56] Passed original java regex string with no preprocessing to RegexGene for repairs. --- .../kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt | 4 ++-- .../src/main/kotlin/org/evomaster/core/parser/RegexHandler.kt | 2 +- .../kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 9e5c7f45ff..057b81a5a7 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -11,7 +11,7 @@ private const val EOF_TOKEN = "" /** * Created by arcuri82 on 11-Sep-19. */ -class GeneRegexJavaVisitor(val externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaBaseVisitor(){ +class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: RegexFlags = RegexFlags()) : RegexJavaBaseVisitor(){ private val hexEscapePrefixes = setOf('x', 'u') @@ -125,7 +125,7 @@ class GeneRegexJavaVisitor(val externalRegexFlags: RegexFlags = RegexFlags()) : val gene = RegexGene( "regex", disjList, - text.substring(0, text.length - EOF_TOKEN.length), + sourceRegex, RegexType.JVM, externalRegexFlags = externalRegexFlags ) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/RegexHandler.kt b/core/src/main/kotlin/org/evomaster/core/parser/RegexHandler.kt index 0835531389..a1a522ec0a 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/RegexHandler.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/RegexHandler.kt @@ -44,7 +44,7 @@ object RegexHandler { val pattern = parser.pattern() - val res = GeneRegexJavaVisitor(externalRegexFlags).visit(pattern) + val res = GeneRegexJavaVisitor(regex, externalRegexFlags).visit(pattern) val gene= res.genes.first() as RegexGene cacheJVM[key] = gene.copy() as RegexGene diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index 4fd67aa942..50e2704694 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -67,7 +67,6 @@ class RegexGene( return } - // this sourceRegex may have preprocessing steps applied... should do this with the original string... val pattern = compiledPattern(sourceRegex, externalRegexFlags) repeat(MAX_TREE_REPAIR_ATTEMPTS) { _ -> From 7a3aab6a50f671320013acd0bf9b5e3c300f6d0c Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 19:28:45 -0300 Subject: [PATCH 17/56] Added a comment. --- .../org/evomaster/core/search/gene/GeneSamplerForTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index bce6691c31..a5ab247d75 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -481,7 +481,7 @@ object GeneSamplerForTests { .filter { (it.java != DisjunctionListRxGene::class.java && it.java != DisjunctionRxGene::class.java && it.java != BackReferenceRxGene::class.java && it.java != AssertionRxGene::class.java) // as this also contains a DisjunctionListRxGene within - || rand.nextBoolean(0.2) + || rand.nextBoolean(0.2) // reduced chance for larger trees } val numberOfTerms = rand.nextInt(1, 3) From 564235b8360b0ec67f08a2c4a110da964ac8240d Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 16 Jul 2026 19:30:29 -0300 Subject: [PATCH 18/56] Added tests for simple lookaheads. --- .../core/parser/GeneRegexJavaVisitorTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index 9feefa394e..d1441d6bcc 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -445,4 +445,29 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { fun testUnicodeCharClassFlagImpliesUnicodeCase(){ checkCanSample("(?iU)Å", "å", 100) } + + @Test + fun testSimpleLookaheads() { + checkSameAsJava("(?=aaa5)aaa\\d") + checkSameAsJava("(?=.*\\d).{4,8}") + checkSameAsJava("(?=.*[A-Z])[a-zA-Z]{4,8}") + checkSameAsJava("(?=.*[a-z])[a-zA-Z]{4,8}") + checkSameAsJava("foo(?=.*\\d)[a-z\\d]{3,6}") + checkSameAsJava("(?=.*\\d)\\w{6,12}") + checkSameAsJava("(?=.*[^A-Za-z0-9])\\w{6,12}$") + checkSameAsJava("^(?=.*\\d)[a-zA-Z\\d]{8,16}$") + checkSameAsJava("(?=\\d)\\d{1,5}") + checkSameAsJava("(?=.*\\d)([a-z]+|\\d+){2,4}") + checkSameAsJava("(?=(!.*[a-z]+))\\1") + checkSameAsJava("(?=(\\d|\\s|d))(\\d|\\s|d)*") + checkSameAsJava("(?=a*)\\w*") + checkSameAsJava("(?=xbcde)x(bcdX|bc)de") + checkSameAsJava("(?=.*[A-Z])(?=.*[a-z])[a-zA-Z]{4,8}") + checkSameAsJava("(?=ababc)(ab|abc)+") + assertThrows { checkSameAsJava("(?=.*\\d)(?=.*[A-Z])[a-zA-Z]{4,8}") } + assertThrows { checkSameAsJava("(?=.*\\d)(?=.*[A-Z])") } + assertThrows { checkSameAsJava("(?=.*\\d)[a-z]+") } + assertThrows { checkSameAsJava("(?=bbbX)aaa[a-z]") } + assertThrows { checkSameAsJava("(?=abcde)a(bcef|de)de") } + } } \ No newline at end of file From fcfc949f4b108f2378b8faaf10cad36e57a72728 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 17 Jul 2026 19:46:57 -0300 Subject: [PATCH 19/56] Merge clean-up: re-added lookaheads to the java regex grammar. --- .../main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 | 3 ++- .../antlr4/org/evomaster/core/parser/RegexJavaParser.g4 | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 index d1f6d1d397..5753d6a4fa 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 @@ -76,6 +76,7 @@ CharacterClassEscape : SLASH [dDsSwWvVhH] ; +EQUAL : '='; CARET : '^'; DOLLAR : '$'; SLASH : '\\'; @@ -96,7 +97,7 @@ COLON : ':'; BaseChar // practically all chars but the ones used for control and digits - : ~[0-9:,^$\\.*+?()[\]{}|-] + : ~[0-9:,^$\\.*+?()[\]{}|=-] ; fragment OctalEscapeSequence diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 index 6a0a9425ed..32a933b7c9 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 @@ -52,7 +52,7 @@ assertion //TODO //// | '\\' 'b' //// | '\\' 'B' -//// | '(' '?' '=' disjunction ')' + | PAREN_open QUESTION EQUAL disjunction PAREN_close //// | '(' '?' '!' disjunction ')' ; @@ -116,7 +116,7 @@ patternCharacter // These are also allowed as literals when no matching pair exists | BRACE_close | BRACKET_close - | COLON + | COLON | EQUAL | DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on. ; @@ -168,7 +168,7 @@ classAtomNoDash | DecimalDigit | COMMA | CARET | DOLLAR | DOT | STAR | PLUS | QUESTION | PAREN_open | PAREN_close | BRACKET_open | BRACE_open | BRACE_close | OR - | COLON + | COLON | EQUAL // should be interpreted literally: // As they are lexer tokens, these character sequences are captured as such. In particular these require some extra // steps to interpret them correctly given the context. From 714fb6b6381142f40d2feb512b981dad286d5e86 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 17 Jul 2026 21:37:40 -0300 Subject: [PATCH 20/56] Moved java Pattern compilation to RegexGene initialization. --- .../org/evomaster/core/search/gene/regex/RegexGene.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index 50e2704694..3aad0b157d 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -55,6 +55,9 @@ class RegexGene( } } + private val pattern: Pattern? = + if (regexType == RegexType.JVM) compiledPattern(sourceRegex, externalRegexFlags) else null + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { usingFixedValue = if(fixedValue == null){ false @@ -67,11 +70,9 @@ class RegexGene( return } - val pattern = compiledPattern(sourceRegex, externalRegexFlags) - repeat(MAX_TREE_REPAIR_ATTEMPTS) { _ -> disjunctions.randomize(randomness, tryToForceNewValue) - if (pattern.matcher(disjunctions.getValueAsPrintableString()).find()) { + if (pattern!!.matcher(disjunctions.getValueAsPrintableString()).find()) { return } disjunctions.attemptAssertionRepair(randomness) @@ -103,7 +104,7 @@ class RegexGene( if(regexType == RegexType.JVM){ val matcher = try{ - compiledPattern(sourceRegex, externalRegexFlags).matcher(fixedValue!!) + pattern!!.matcher(fixedValue!!) }catch(e: Exception){ return false } From 7874c1d93576c7f8e33f969fce520c277a201187 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 17 Jul 2026 21:48:58 -0300 Subject: [PATCH 21/56] Re-structured lookahead tests. --- .../org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index d4c9091b4e..9da13414fe 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -470,8 +470,12 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { checkSameAsJava("(?=(\\d|\\s|d))(\\d|\\s|d)*") checkSameAsJava("(?=a*)\\w*") checkSameAsJava("(?=xbcde)x(bcdX|bc)de") - checkSameAsJava("(?=.*[A-Z])(?=.*[a-z])[a-zA-Z]{4,8}") + checkSameAsJava("^(?=(\\S+))(\\d+h)?(\\d+m)?(\\d+s)?$") checkSameAsJava("(?=ababc)(ab|abc)+") + } + + @Test + fun testUnsatisfiableLookaheads() { assertThrows { checkSameAsJava("(?=.*\\d)(?=.*[A-Z])[a-zA-Z]{4,8}") } assertThrows { checkSameAsJava("(?=.*\\d)(?=.*[A-Z])") } assertThrows { checkSameAsJava("(?=.*\\d)[a-z]+") } From f3b0fedb3c1c3a06566d92ee84a516375a3f5c9b Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 20 Jul 2026 13:58:17 -0300 Subject: [PATCH 22/56] Made assertion repairs only run when regex contains assertions. --- .../core/parser/GeneRegexJavaVisitor.kt | 6 +++++- .../core/search/gene/regex/RegexGene.kt | 11 +++++++---- .../core/utils/MultiCharacterRange.kt | 19 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 09baabe8c7..c17bfa8013 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -60,6 +60,8 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege */ private var currentFlags = externalRegexFlags + private var hasAssertions = false + /** * Builds DisjunctionListRxGenes from a disjunction context, returns null if disjunction is unsatisfiable. */ @@ -127,7 +129,8 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege disjList, sourceRegex, RegexType.JVM, - externalRegexFlags = externalRegexFlags + externalRegexFlags = externalRegexFlags, + hasAssertions = hasAssertions ) return VisitResult(gene) @@ -257,6 +260,7 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege rejectIfNestedAssertion(ctx.assertion()) val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) val assertionGene = AssertionRxGene(innerDisjList) + hasAssertions = true res.genes.add(assertionGene) } return res diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index 3aad0b157d..c269be9c12 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -37,7 +37,8 @@ class RegexGene( */ var fixedValue: String? = null, var usingFixedValue: Boolean = false, - val externalRegexFlags: RegexFlags = RegexFlags() + val externalRegexFlags: RegexFlags = RegexFlags(), + val hasAssertions: Boolean = false ) : CompositeFixedGene(name, disjunctions) { @@ -75,9 +76,11 @@ class RegexGene( if (pattern!!.matcher(disjunctions.getValueAsPrintableString()).find()) { return } - disjunctions.attemptAssertionRepair(randomness) - if (pattern.matcher(disjunctions.getValueAsPrintableString()).find()) { - return + if (hasAssertions) { + disjunctions.attemptAssertionRepair(randomness) + if (pattern.matcher(disjunctions.getValueAsPrintableString()).find()) { + return + } } } diff --git a/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt b/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt index 67e23994a7..477149a524 100644 --- a/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt +++ b/core/src/main/kotlin/org/evomaster/core/utils/MultiCharacterRange.kt @@ -208,17 +208,18 @@ class MultiCharacterRange internal constructor(val ranges: List) * Check if [c] is contained in this [MultiCharacterRange]. */ fun contains(c: Char): Boolean { - if (isEmpty) return false + if (isEmpty) { + return false + } // binary search as MultiCharacterRange's ranges are ordered and non-overlapping - return ranges.binarySearch { (start, end) -> - if(c in start..end) { - 0 - } else if (c < start) { - -1 - } else { - 1 + val index = ranges.binarySearch { (start, end) -> + when { + c < start -> 1 // Target is before this range, search left + c > end -> -1 // Target is after this range, search right + else -> 0 // Target is inside this range, match } - } >= 0 + } + return index >= 0 } val isEmpty: Boolean get() = ranges.isEmpty() From 2c22705b1d06bad68d78db212a99c60b738db9eb Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 20 Jul 2026 14:30:05 -0300 Subject: [PATCH 23/56] Added braces. --- .../search/gene/regex/AnyCharacterRxGene.kt | 10 ++++++++-- .../core/search/gene/regex/AssertionRxGene.kt | 12 ++++++++--- .../gene/regex/CharacterClassEscapeRxGene.kt | 18 +++++++++++++---- .../search/gene/regex/CharacterRangeRxGene.kt | 18 +++++++++++++---- .../gene/regex/DisjunctionListRxGene.kt | 12 ++++++++--- .../search/gene/regex/DisjunctionRxGene.kt | 16 +++++++++++---- .../gene/regex/PatternCharacterBlockGene.kt | 9 ++++++--- .../core/search/gene/regex/RegexGene.kt | 6 +++++- .../search/gene/utils/AssertionRepairWalk.kt | 20 ++++++++++++++----- 9 files changed, 92 insertions(+), 29 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt index b7be5a6fa8..9596a89e02 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt @@ -132,8 +132,14 @@ class AnyCharacterRxGene( } override fun absorbableCount(value: String): Int { - if (value.isEmpty()) return 0 - return if (validRanges.contains(value[0])) 1 else 0 + if (value.isEmpty()) { + return 0 + } + return if (validRanges.contains(value[0])) { + 1 + } else { + 0 + } } override fun tryForce(value: String): Int { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 834f353aa8..c8b363cef1 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -71,17 +71,23 @@ class AssertionRxGene( * inner gene to sample from. */ fun sampledInnerValue(): String? { - if (innerGene == null) return null + if (innerGene == null) { + return null + } return innerGene.getValueAsPrintableString(targetFormat = null) } override fun containsSameValueAs(other: Gene): Boolean { - if (other !is AssertionRxGene) return false + if (other !is AssertionRxGene) { + return false + } return sampledInnerValue() == other.sampledInnerValue() } override fun unsafeCopyValueFrom(other: Gene): Boolean { - if (other !is AssertionRxGene) return false + if (other !is AssertionRxGene) { + return false + } return if (innerGene != null && other.innerGene != null) { innerGene.unsafeCopyValueFrom(other.innerGene) } else { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt index 54dea84aea..674bf1613e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt @@ -116,7 +116,11 @@ class CharacterClassEscapeRxGene( private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java) private fun Char.swapCase(): Char = - if (this.isUpperCase()) this.lowercaseChar() else this.uppercaseChar() + if (this.isUpperCase()) { + this.lowercaseChar() + } else { + this.uppercaseChar() + } private val digitSet = listOf(CharacterRange('0', '9')) private val asciiLetterSet = listOf(CharacterRange(FIRST_LOWER_CASE_CHAR, LAST_LOWER_CASE_CHAR), @@ -356,10 +360,16 @@ class CharacterClassEscapeRxGene( } override fun absorbableCount(value: String): Int { - if (value.isEmpty()) return 0 + if (value.isEmpty()) { + return 0 + } val c = value[0] - if (multiCharRange.contains(c)) return 1 - if (flags.isCaseable(c) && multiCharRange.contains(c.swapCase())) return 1 + if (multiCharRange.contains(c)) { + return 1 + } + if (flags.isCaseable(c) && multiCharRange.contains(c.swapCase())) { + return 1 + } return 0 } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt index 597e35b731..cc59940bba 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt @@ -30,7 +30,11 @@ class CharacterRangeRxGene( private val log = LoggerFactory.getLogger(CharacterRangeRxGene::class.java) private fun Char.swapCase(): Char = - if (this.isUpperCase()) this.lowercaseChar() else this.uppercaseChar() + if (this.isUpperCase()) { + this.lowercaseChar() + } else { + this.uppercaseChar() + } } // '\u0000' is a placeholder for the unsatisfiable case (empty MCR with no valid ranges). @@ -185,10 +189,16 @@ class CharacterRangeRxGene( } override fun absorbableCount(value: String): Int { - if (value.isEmpty()) return 0 + if (value.isEmpty()) { + return 0 + } val c = value[0] - if (validRanges.contains(c)) return 1 - if (flags.isCaseable(c) && validRanges.contains(c.swapCase())) return 1 + if (validRanges.contains(c)) { + return 1 + } + if (flags.isCaseable(c) && validRanges.contains(c.swapCase())) { + return 1 + } return 0 } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index b383d35aeb..c429abbdcb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -213,12 +213,18 @@ class DisjunctionListRxGene( * anything. */ private fun rankBranches(value: String): BranchRanking? { - if (value.isEmpty() || disjunctions.isEmpty()) return null + if (value.isEmpty() || disjunctions.isEmpty()) { + return null + } var bestCount = disjunctions[activeDisjunction].absorbableCount(value) var bestIndex = activeDisjunction for (i in disjunctions.indices) { - if (i == activeDisjunction) continue - if (bestCount == value.length) break + if (i == activeDisjunction) { + continue + } + if (bestCount == value.length) { + break + } val can = disjunctions[i].absorbableCount(value) if (can > bestCount) { bestCount = can diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 4960f573e2..bc06413ca3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -216,14 +216,20 @@ class DisjunctionRxGene( * value onto the genes that follow it within [terms]. */ fun attemptAssertionRepair(randomness: Randomness) { - if (terms.none { it is AssertionRxGene }) return + if (terms.none { it is AssertionRxGene }) { + return + } for (idx in terms.indices) { val assertion = terms[idx] as? AssertionRxGene ?: continue - if (assertion.innerGene == null) continue + if (assertion.innerGene == null) { + continue + } val genesAfter = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } - if (genesAfter.isEmpty()) return + if (genesAfter.isEmpty()) { + return + } var satisfied = false for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { @@ -234,7 +240,9 @@ class DisjunctionRxGene( break } } - if (!satisfied) return + if (!satisfied) { + return + } } } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index e512804f24..3041e840d1 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -119,11 +119,14 @@ class PatternCharacterBlockGene( var i = 0 while (i < value.length && i < stringBlock.length) { val c = stringBlock[i] - val matches = if (flags.isCaseable(c)) + val matches = if (flags.isCaseable(c)) { value[i].equals(c, ignoreCase = true) - else + } else { value[i] == c - if (!matches) return 0 + } + if (!matches) { + return 0 + } i++ } return i diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index c269be9c12..daa9c0491f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -57,7 +57,11 @@ class RegexGene( } private val pattern: Pattern? = - if (regexType == RegexType.JVM) compiledPattern(sourceRegex, externalRegexFlags) else null + if (regexType == RegexType.JVM) { + compiledPattern(sourceRegex, externalRegexFlags) + } else { + null + } override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { usingFixedValue = if(fixedValue == null){ diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index d1c0958678..668f685544 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -16,17 +16,23 @@ object AssertionRepairWalk { * left-to-right, without mutating anything. */ fun absorbableCount(genes: List, value: String): Int { - if (value.isEmpty()) return 0 + if (value.isEmpty()) { + return 0 + } var consumed = 0 for (gene in genes) { - if (consumed >= value.length) break + if (consumed >= value.length) { + break + } val absorbable = gene as RxAbsorbable val canTake = absorbable.absorbableCount(value.substring(consumed)) if (canTake > 0) { consumed += canTake continue } - if (absorbable.canBeZeroWidth()) continue + if (absorbable.canBeZeroWidth()) { + continue + } return 0 } return consumed @@ -37,10 +43,14 @@ object AssertionRepairWalk { * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. */ fun tryForce(genes: List, value: String): Int { - if (value.isEmpty()) return 0 + if (value.isEmpty()) { + return 0 + } var consumed = 0 for (gene in genes) { - if (consumed >= value.length) break + if (consumed >= value.length) { + break + } val absorbable = gene as RxAbsorbable val remaining = value.substring(consumed) val placed = absorbable.tryForce(remaining) From 736e1f1ea513be1e3301ced8552dd18b9ff38532 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 20 Jul 2026 15:19:53 -0300 Subject: [PATCH 24/56] Made canBeZeroWidth a "val" instead of "fun". --- .../core/search/gene/regex/AnyCharacterRxGene.kt | 2 ++ .../evomaster/core/search/gene/regex/AssertionRxGene.kt | 2 +- .../core/search/gene/regex/BackReferenceRxGene.kt | 2 ++ .../core/search/gene/regex/CharacterClassEscapeRxGene.kt | 2 ++ .../core/search/gene/regex/CharacterRangeRxGene.kt | 2 ++ .../core/search/gene/regex/DisjunctionListRxGene.kt | 6 +++--- .../evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 8 ++++---- .../core/search/gene/regex/PatternCharacterBlockGene.kt | 4 ++-- .../evomaster/core/search/gene/regex/QuantifierRxGene.kt | 6 +++--- .../org/evomaster/core/search/gene/regex/RxAbsorbable.kt | 4 ++-- .../core/search/gene/utils/AssertionRepairWalk.kt | 4 ++-- 11 files changed, 25 insertions(+), 17 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt index 9596a89e02..04a13cedce 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt @@ -142,6 +142,8 @@ class AnyCharacterRxGene( } } + override val canBeZeroWidth: Boolean = false + override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index c8b363cef1..4e6c08c6c5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -95,7 +95,7 @@ class AssertionRxGene( } } - override fun canBeZeroWidth(): Boolean = true + override val canBeZeroWidth: Boolean = true override fun tryForce(value: String): Int { require(value.isNotEmpty()) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt index 7be7853a9e..5c2a26d3c5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt @@ -82,6 +82,8 @@ class BackReferenceRxGene( return containsSameValueAs(other) } + override val canBeZeroWidth: Boolean = false + override fun tryForce(value: String): Int { require(value.isNotEmpty()) return 0 diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt index 674bf1613e..8708ff35ef 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt @@ -373,6 +373,8 @@ class CharacterClassEscapeRxGene( return 0 } + override val canBeZeroWidth: Boolean = false + override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt index cc59940bba..c885d26272 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt @@ -202,6 +202,8 @@ class CharacterRangeRxGene( return 0 } + override val canBeZeroWidth: Boolean = false + override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index c429abbdcb..73b018dbd6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -237,7 +237,7 @@ class DisjunctionListRxGene( override fun absorbableCount(value: String): Int = rankBranches(value)?.absorbableCount ?: 0 - override fun canBeZeroWidth(): Boolean = disjunctions.any { it.canBeZeroWidth() } + override val canBeZeroWidth: Boolean = disjunctions.any { it.canBeZeroWidth } override fun tryForce(value: String): Int { require(value.isNotEmpty()) @@ -255,10 +255,10 @@ class DisjunctionListRxGene( } override fun forceZeroWidth() { - require(canBeZeroWidth()) + require(canBeZeroWidth) // try the active branch first to avoid an unnecessary switch val order = listOf(activeDisjunction) + disjunctions.indices.filter { it != activeDisjunction } - val target = order.first { disjunctions[it].canBeZeroWidth() } + val target = order.first { disjunctions[it].canBeZeroWidth } disjunctions[target].forceZeroWidth() if (target != activeDisjunction) { activeDisjunction = target diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index bc06413ca3..5b6d0bbf6f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -193,20 +193,20 @@ class DisjunctionRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(terms, value) - override fun canBeZeroWidth(): Boolean = - terms.all { (it as RxAbsorbable).canBeZeroWidth() } + override val canBeZeroWidth: Boolean = + terms.all { (it as RxAbsorbable).canBeZeroWidth } override fun tryForce(value: String): Int { require(value.isNotEmpty()) val placed = AssertionRepairWalk.tryForce(terms, value) - if (placed == 0 && canBeZeroWidth()) { + if (placed == 0 && canBeZeroWidth) { forceZeroWidth() } return placed } override fun forceZeroWidth() { - require(canBeZeroWidth()) + require(canBeZeroWidth) terms.forEach { (it as RxAbsorbable).forceZeroWidth() } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index 3041e840d1..71264312f5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -132,7 +132,7 @@ class PatternCharacterBlockGene( return i } - override fun canBeZeroWidth(): Boolean = stringBlock.isEmpty() + override val canBeZeroWidth: Boolean = stringBlock.isEmpty() override fun tryForce(value: String): Int { require(value.isNotEmpty()) @@ -147,7 +147,7 @@ class PatternCharacterBlockGene( } override fun forceZeroWidth() { - require(canBeZeroWidth()) + require(canBeZeroWidth) // stringBlock is empty, so there is nothing to place - already zero-width. } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index d8eae322a2..f20e81d3d3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -244,8 +244,8 @@ class QuantifierRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(atoms, value) - override fun canBeZeroWidth(): Boolean = - min == 0 || (template as? RxAbsorbable)?.canBeZeroWidth() == true + override val canBeZeroWidth: Boolean = + min == 0 || (template as? RxAbsorbable)?.canBeZeroWidth == true override fun tryForce(value: String): Int { require(value.isNotEmpty()) @@ -253,7 +253,7 @@ class QuantifierRxGene( } override fun forceZeroWidth() { - require(canBeZeroWidth()) + require(canBeZeroWidth) if (min == 0) { killAllChildren() } else { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt index 7440cff8cd..b6fe74a921 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -30,9 +30,9 @@ interface RxAbsorbable { } /** - * Read-only: could [forceZeroWidth] succeed on this gene right now? Default false. + * Read-only: could [forceZeroWidth] succeed on this gene right now? */ - fun canBeZeroWidth(): Boolean = false + val canBeZeroWidth: Boolean /** * Forces this gene to render as "". Unlike [tryForce], there is nothing to plan or report. diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 668f685544..24e7ffd425 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -30,7 +30,7 @@ object AssertionRepairWalk { consumed += canTake continue } - if (absorbable.canBeZeroWidth()) { + if (absorbable.canBeZeroWidth) { continue } return 0 @@ -55,7 +55,7 @@ object AssertionRepairWalk { val remaining = value.substring(consumed) val placed = absorbable.tryForce(remaining) if (placed == 0) { - if (absorbable.canBeZeroWidth()) { + if (absorbable.canBeZeroWidth) { absorbable.forceZeroWidth() continue } else { From 7f39dd18c0fea6e088a9faa076dd130b0ec9ca00 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 20 Jul 2026 15:20:49 -0300 Subject: [PATCH 25/56] Enabled disabled test. --- .../test/kotlin/org/evomaster/core/parser/RegexHandlerTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/RegexHandlerTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/RegexHandlerTest.kt index 1382ba3741..82f7480509 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/RegexHandlerTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/RegexHandlerTest.kt @@ -14,7 +14,6 @@ import java.util.regex.Pattern internal class RegexHandlerTest{ - @Disabled("Needs to hande lookahead in regex") @Test fun testLanguageTool(){ val s = "^((?iu)@.+)$" From 5eaaf788bdb47addc0f5bcb0eb0d604a46035f7c Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 20 Jul 2026 15:29:15 -0300 Subject: [PATCH 26/56] Cleared up some comments. --- .../org/evomaster/core/search/gene/regex/RxAbsorbable.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt index b6fe74a921..e0cb41c491 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -13,7 +13,7 @@ package org.evomaster.core.search.gene.regex interface RxAbsorbable { /** - * Maximum number of leading characters of [value] this gene can be forced to + * Read-only: maximum number of leading characters of [value] this gene can be forced to * produce, or already produces without needing to change. Default 0 = cannot help. */ fun absorbableCount(value: String): Int = 0 @@ -30,7 +30,7 @@ interface RxAbsorbable { } /** - * Read-only: could [forceZeroWidth] succeed on this gene right now? + * Read-only: could [forceZeroWidth] succeed on this gene (i.e.: "" can match this expression)? */ val canBeZeroWidth: Boolean From b28e279c4d203a4897dbeda910dffdd8ed4c8a0a Mon Sep 17 00:00:00 2001 From: lmasroca Date: Wed, 22 Jul 2026 16:19:40 -0300 Subject: [PATCH 27/56] Requested changes. --- .../core/parser/GeneRegexJavaVisitor.kt | 21 ++++++++++--------- .../core/search/gene/regex/AssertionRxGene.kt | 4 ++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index c17bfa8013..49c6f80040 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -90,21 +90,20 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege } /** - * Walks up [ctx]'s ancestry towards the top-level pattern, throwing as soon as it - * finds one of the currently-unsupported ways an assertion's ancestry can appear (nested). - * Returns normally if it reaches the top-level pattern without hitting either. + * Walks up [ctx]'s ancestry towards the top-level pattern, searching for one of + * the currently-unsupported ways an assertion's ancestry can appear (nested). + * Returns true if it reaches the top-level pattern without hitting either, false otherwise. */ - private fun rejectIfNestedAssertion(ctx: RegexJavaParser.AssertionContext) { + private fun isAssertionNested(ctx: RegexJavaParser.AssertionContext): Boolean { var current = ctx.parent while (current != null && current !is RegexJavaParser.PatternContext) { - if (current is RegexJavaParser.AssertionContext) { - throw IllegalStateException("Nested assertions not supported") - } - if (current is RegexJavaParser.AtomContext && current.disjunction() != null) { - throw IllegalStateException("Assertions inside groups are not currently supported") + if (current is RegexJavaParser.AssertionContext + || (current is RegexJavaParser.AtomContext && current.disjunction() != null)) { + return true } current = current.parent } + return false } override fun visitPattern(ctx: RegexJavaParser.PatternContext): VisitResult { @@ -257,7 +256,9 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege if (assertionCtx.CARET() != null || assertionCtx.DOLLAR() != null) { res.data = ctx.assertion().text } else { - rejectIfNestedAssertion(ctx.assertion()) + require(!isAssertionNested(ctx.assertion())){ + "Nested assertions are not currently supported." + } val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) val assertionGene = AssertionRxGene(innerDisjList) hasAssertions = true diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 4e6c08c6c5..6e7991a2ad 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -22,6 +22,10 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation * the source pattern and found not to match. */ class AssertionRxGene( + /** + * The assertion's inner disjunction gene, can be null as the disjunction can be unsatisfiable, + * in that case [innerGene] is null. + */ val innerGene: DisjunctionListRxGene? ) : RxTerm, SimpleGene("assertion") { From 0986eaa2d8d81fb1588e6b17ab246af04b514d45 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Wed, 22 Jul 2026 19:11:35 -0300 Subject: [PATCH 28/56] Requested changes 2: more javadocs. --- .../search/gene/regex/AnyCharacterRxGene.kt | 13 ++++++++++++ .../core/search/gene/regex/AssertionRxGene.kt | 14 +++++++++++++ .../search/gene/regex/BackReferenceRxGene.kt | 9 ++++++++ .../gene/regex/CharacterClassEscapeRxGene.kt | 14 +++++++++++++ .../search/gene/regex/CharacterRangeRxGene.kt | 14 +++++++++++++ .../gene/regex/DisjunctionListRxGene.kt | 21 +++++++++++++++++++ .../search/gene/regex/DisjunctionRxGene.kt | 19 +++++++++++++++++ .../gene/regex/PatternCharacterBlockGene.kt | 20 +++++++++++++++++- .../search/gene/regex/QuantifierRxGene.kt | 20 ++++++++++++++++++ 9 files changed, 143 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt index 04a13cedce..3bbf1e0bcb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AnyCharacterRxGene.kt @@ -131,6 +131,11 @@ class AnyCharacterRxGene( return true } + /** + * 1 if [value]'s first character is within [validRanges] (i.e. one `.` itself could + * render, given the current flags), else 0. + * @see [RxAbsorbable.absorbableCount] + */ override fun absorbableCount(value: String): Int { if (value.isEmpty()) { return 0 @@ -142,8 +147,16 @@ class AnyCharacterRxGene( } } + /** Always false: `.` always renders exactly one character. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = false + /** + * Forces [value]'s first character onto this gene if `.` could render it; mirrors + * [absorbableCount], so it never mutates when [absorbableCount] would return 0. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 6e7991a2ad..a180054bc8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -99,13 +99,27 @@ class AssertionRxGene( } } + /** + * Always true: an assertion never renders characters ([getValueAsPrintableString] is + * always ""), so it can always collapse to zero width. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = true + /** + * Always 0: an assertion never absorbs candidate text into itself, it only supplies a + * candidate for its siblings to absorb, via [sampledInnerValue]. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) return 0 } + /** + * No-op: already always zero-width, so there's nothing to force. + * @see [RxAbsorbable.forceZeroWidth] + */ override fun forceZeroWidth() { // already always zero-width, nothing to do } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt index 5c2a26d3c5..63006020e2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt @@ -82,8 +82,17 @@ class BackReferenceRxGene( return containsSameValueAs(other) } + /** + * Returns false as we do not want backreferences to mutate a previous group. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = false + /** + * Always 0: a backreference's value is derived entirely from a previous [captureGroup], + * so unlike an ordinary leaf it can not be forced to absorb arbitrary candidate text. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) return 0 diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt index 8708ff35ef..32081c17ac 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterClassEscapeRxGene.kt @@ -359,6 +359,11 @@ class CharacterClassEscapeRxGene( return false } + /** + * 1 if [value]'s first character (or its case-swapped counterpart, when caseable) is + * within [multiCharRange], else 0. + * @see [RxAbsorbable.absorbableCount] + */ override fun absorbableCount(value: String): Int { if (value.isEmpty()) { return 0 @@ -373,8 +378,17 @@ class CharacterClassEscapeRxGene( return 0 } + /** + * Always false: a character-class escape always renders exactly one character. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = false + /** + * Forces [value]'s first character onto this gene, swapping case if that's what + * matched; mirrors [absorbableCount]. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt index c885d26272..8a61ef7bd3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/CharacterRangeRxGene.kt @@ -188,6 +188,11 @@ class CharacterRangeRxGene( return false } + /** + * 1 if [value]'s first character (or its case-swapped counterpart, when caseable) is + * within [validRanges], else 0. + * @see [RxAbsorbable.absorbableCount] + */ override fun absorbableCount(value: String): Int { if (value.isEmpty()) { return 0 @@ -202,8 +207,17 @@ class CharacterRangeRxGene( return 0 } + /** + * Always false: a character range always renders exactly one character. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = false + /** + * Forces [value]'s first character onto this gene, swapping case if that's what + * matched; mirrors [absorbableCount]. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 73b018dbd6..5fe86980a2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -234,11 +234,27 @@ class DisjunctionListRxGene( return BranchRanking(bestCount, bestIndex) } + /** + * Ranks every branch by how much of [value] it could absorb, without mutating, and + * reports the best [RxAbsorbable.absorbableCount]. + * @see [RxAbsorbable.absorbableCount] + * @see [rankBranches] + */ override fun absorbableCount(value: String): Int = rankBranches(value)?.absorbableCount ?: 0 + /** + * True if at least one branch can render "", as we can select that branch and force it. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = disjunctions.any { it.canBeZeroWidth } + /** + * Activates whichever branch can best absorb [value] (switching [activeDisjunction] if + * needed) and forces it there. + * @see [RxAbsorbable.tryForce] + * @see [rankBranches] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val (bestCount, bestIndex) = rankBranches(value) ?: BranchRanking(0, activeDisjunction) @@ -254,6 +270,11 @@ class DisjunctionListRxGene( return 0 } + /** + * Forces the active branch to zero width if it can; otherwise switches to the first + * branch that can and forces that one instead. + * @see [RxAbsorbable.forceZeroWidth] + */ override fun forceZeroWidth() { require(canBeZeroWidth) // try the active branch first to avoid an unnecessary switch diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 5b6d0bbf6f..5625739c90 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -190,12 +190,27 @@ class DisjunctionRxGene( return ok } + /** + * Delegates to a forward walk over [terms]. + * @see [RxAbsorbable.absorbableCount] + * @see [AssertionRepairWalk.absorbableCount] + */ override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(terms, value) + /** + * True only if every term can independently render "", as this disjunction's own value is + * the concatenation of all of them. + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = terms.all { (it as RxAbsorbable).canBeZeroWidth } + /** + * Delegates to a forward walk over [terms], mirroring [absorbableCount]. + * @see [RxAbsorbable.tryForce] + * @see [AssertionRepairWalk.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val placed = AssertionRepairWalk.tryForce(terms, value) @@ -205,6 +220,10 @@ class DisjunctionRxGene( return placed } + /** + * Forces every term to zero width individually. + * @see [RxAbsorbable.forceZeroWidth] + */ override fun forceZeroWidth() { require(canBeZeroWidth) terms.forEach { (it as RxAbsorbable).forceZeroWidth() } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index 71264312f5..8eea221606 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -115,6 +115,12 @@ class PatternCharacterBlockGene( return containsSameValueAs(other) } + /** + * How many of [stringBlock]'s leading characters match [value]'s leading characters, + * case-insensitively wherever [flags] allows it. 0 when a character does not match, + * one of the strings must be consumed completely. + * @see [RxAbsorbable.absorbableCount] + */ override fun absorbableCount(value: String): Int { var i = 0 while (i < value.length && i < stringBlock.length) { @@ -132,8 +138,17 @@ class PatternCharacterBlockGene( return i } + /** + * True only when [stringBlock] is empty, as a non-empty literal can never render "". + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = stringBlock.isEmpty() + /** + * Commits the matching leading characters' case to match [value]; mirrors + * [absorbableCount] exactly. + * @see [RxAbsorbable.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) val n = absorbableCount(value) @@ -146,8 +161,11 @@ class PatternCharacterBlockGene( return n } + /** + * No-op: only reachable when [stringBlock] is empty, so there's nothing to place. + * @see [RxAbsorbable.forceZeroWidth] + */ override fun forceZeroWidth() { require(canBeZeroWidth) - // stringBlock is empty, so there is nothing to place - already zero-width. } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index f20e81d3d3..61eb5dc8d7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -241,17 +241,37 @@ class QuantifierRxGene( } } + /** + * Delegates to a forward walk over [atoms], this gene has no absorption logic beyond + * what its repeated atoms can each individually take. + * @see [RxAbsorbable.absorbableCount] + * @see [AssertionRepairWalk.absorbableCount] + */ override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(atoms, value) + /** + * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". + * @see [RxAbsorbable.canBeZeroWidth] + */ override val canBeZeroWidth: Boolean = min == 0 || (template as? RxAbsorbable)?.canBeZeroWidth == true + /** + * Delegates to a forward walk over [atoms], mirroring [absorbableCount]. + * @see [RxAbsorbable.tryForce] + * @see [AssertionRepairWalk.tryForce] + */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) return AssertionRepairWalk.tryForce(atoms, value) } + /** + * Collapses to zero repetitions if [min] == 0 (removing every atom), otherwise forces + * each existing atom to zero width individually. + * @see [RxAbsorbable.forceZeroWidth] + */ override fun forceZeroWidth() { require(canBeZeroWidth) if (min == 0) { From 49ea960ad007cbfcef69d10fec8545c55ee029da Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 03:07:00 -0300 Subject: [PATCH 29/56] Removed unnecessary code. --- .../evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 5625739c90..2fcbebbb1d 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -213,11 +213,7 @@ class DisjunctionRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - val placed = AssertionRepairWalk.tryForce(terms, value) - if (placed == 0 && canBeZeroWidth) { - forceZeroWidth() - } - return placed + return AssertionRepairWalk.tryForce(terms, value) } /** From 972800bc8af20d044c68178e387eea5bb85b9626 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 03:11:38 -0300 Subject: [PATCH 30/56] Made repair loop check full absorbability before forcing begins, avoiding cross-attempt mutation contamination. --- .../evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 2fcbebbb1d..2afec87505 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -250,7 +250,11 @@ class DisjunctionRxGene( for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() || AssertionRepairWalk.tryForce(genesAfter, candidate) == candidate.length) { + if (candidate.isEmpty() + || AssertionRepairWalk.absorbableCount(genesAfter, candidate) == candidate.length) { + if (candidate.isNotEmpty()) { + AssertionRepairWalk.tryForce(genesAfter, candidate) + } satisfied = true break } From faab11191183bdb7f25a240f554c7f5bd7069c5a Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 18:51:32 -0300 Subject: [PATCH 31/56] Made AssertionRxGene extend CompositeFixedGene, added unsatisfiable lookahead tests. --- .../core/search/gene/regex/AssertionRxGene.kt | 18 ++++++++++++++---- .../core/parser/GeneRegexJavaVisitorTest.kt | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index a180054bc8..29b74793cb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -2,7 +2,7 @@ package org.evomaster.core.search.gene.regex import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.Gene -import org.evomaster.core.search.gene.root.SimpleGene +import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.AdaptiveParameterControl import org.evomaster.core.search.service.Randomness @@ -27,7 +27,12 @@ class AssertionRxGene( * in that case [innerGene] is null. */ val innerGene: DisjunctionListRxGene? -) : RxTerm, SimpleGene("assertion") { +) : RxTerm, CompositeFixedGene("assertion", listOfNotNull(innerGene)) { + + /** + * To handle null [innerGene], in which case the assertion is unsatisfiable. + */ + override fun canBeChildless() = true override fun checkForLocallyValidIgnoringChildren(): Boolean = true @@ -45,8 +50,13 @@ class AssertionRxGene( innerGene?.randomize(randomness, tryToForceNewValue) } - override fun setValueWithRawString(value: String) { - innerGene?.setFromStringValue(value) + override fun customShouldApplyShallowMutation( + randomness: Randomness, + selectionStrategy: SubsetGeneMutationSelectionStrategy, + enableAdaptiveGeneMutation: Boolean, + additionalGeneMutationInfo: AdditionalGeneMutationInfo? + ): Boolean { + return false } override fun shallowMutate( diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index 9da13414fe..a8220c3f22 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -481,5 +481,7 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?=.*\\d)[a-z]+") } assertThrows { checkSameAsJava("(?=bbbX)aaa[a-z]") } assertThrows { checkSameAsJava("(?=abcde)a(bcef|de)de") } + assertThrows { checkSameAsJava("(?=[a&&b])a(bcef|de)de") } + checkSameAsJava("abc|(?=[a&&b])def") } } \ No newline at end of file From 6e1e1e046c71ad42a3d1eb2d754acc872933cf0d Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 18:52:16 -0300 Subject: [PATCH 32/56] Made BackReferenceRxGene extend CompositeFixedGenes. --- .../search/gene/regex/BackReferenceRxGene.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt index 63006020e2..223a88daa7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt @@ -2,7 +2,7 @@ package org.evomaster.core.search.gene.regex import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.Gene -import org.evomaster.core.search.gene.root.SimpleGene +import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.AdaptiveParameterControl import org.evomaster.core.search.service.Randomness @@ -20,7 +20,12 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation class BackReferenceRxGene( val groupIndex: Int, val captureGroup: DisjunctionListRxGene? -) : RxAtom, SimpleGene("\\$groupIndex") { +) : RxAtom, CompositeFixedGene("\\$groupIndex", listOfNotNull(captureGroup)) { + + /** + * To handle null [captureGroup], in which case the back reference is unsatisfiable. + */ + override fun canBeChildless() = true override fun isUnsatisfiable(): Boolean { return captureGroup == null || captureGroup.isUnsatisfiable() @@ -39,16 +44,19 @@ class BackReferenceRxGene( return copy } - override fun setValueWithRawString(value: String) { - throw IllegalStateException( - "Cannot set value directly on a BackReferenceRxGene, set the capture group instead." - ) - } - override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { throw IllegalStateException("Cannot randomize a BackReferenceRxGene, randomize the capture group instead.") } + override fun customShouldApplyShallowMutation( + randomness: Randomness, + selectionStrategy: SubsetGeneMutationSelectionStrategy, + enableAdaptiveGeneMutation: Boolean, + additionalGeneMutationInfo: AdditionalGeneMutationInfo? + ): Boolean { + return false + } + override fun shallowMutate( randomness: Randomness, apc: AdaptiveParameterControl, From cac44c99ebbbfcaa4075a81182033ce1211d6eec Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 21:19:52 -0300 Subject: [PATCH 33/56] Revert "Made BackReferenceRxGene extend CompositeFixedGenes." This reverts commit 6e1e1e046c71ad42a3d1eb2d754acc872933cf0d. --- .../search/gene/regex/BackReferenceRxGene.kt | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt index 223a88daa7..63006020e2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/BackReferenceRxGene.kt @@ -2,7 +2,7 @@ package org.evomaster.core.search.gene.regex import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.Gene -import org.evomaster.core.search.gene.root.CompositeFixedGene +import org.evomaster.core.search.gene.root.SimpleGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.AdaptiveParameterControl import org.evomaster.core.search.service.Randomness @@ -20,12 +20,7 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation class BackReferenceRxGene( val groupIndex: Int, val captureGroup: DisjunctionListRxGene? -) : RxAtom, CompositeFixedGene("\\$groupIndex", listOfNotNull(captureGroup)) { - - /** - * To handle null [captureGroup], in which case the back reference is unsatisfiable. - */ - override fun canBeChildless() = true +) : RxAtom, SimpleGene("\\$groupIndex") { override fun isUnsatisfiable(): Boolean { return captureGroup == null || captureGroup.isUnsatisfiable() @@ -44,17 +39,14 @@ class BackReferenceRxGene( return copy } - override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { - throw IllegalStateException("Cannot randomize a BackReferenceRxGene, randomize the capture group instead.") + override fun setValueWithRawString(value: String) { + throw IllegalStateException( + "Cannot set value directly on a BackReferenceRxGene, set the capture group instead." + ) } - override fun customShouldApplyShallowMutation( - randomness: Randomness, - selectionStrategy: SubsetGeneMutationSelectionStrategy, - enableAdaptiveGeneMutation: Boolean, - additionalGeneMutationInfo: AdditionalGeneMutationInfo? - ): Boolean { - return false + override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) { + throw IllegalStateException("Cannot randomize a BackReferenceRxGene, randomize the capture group instead.") } override fun shallowMutate( From 53340908201c2cff875ea0e4e085f295a1c2daad Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 23:07:15 -0300 Subject: [PATCH 34/56] Added shared walk method for AssertionRepairWalk reducing repeated code. --- .../search/gene/utils/AssertionRepairWalk.kt | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 24e7ffd425..9dea06f1a7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -12,10 +12,16 @@ import org.evomaster.core.search.gene.regex.RxAbsorbable */ object AssertionRepairWalk { /** - * Maximum leading characters of [value] that can be absorbed across [genes] - * left-to-right, without mutating anything. + * Shared algorithm behind all public functions below, these differ only in: + * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) + * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] */ - fun absorbableCount(genes: List, value: String): Int { + private fun walk( + genes: List, + value: String, + absorb: (RxAbsorbable, String) -> Int, + onZeroWidth: (RxAbsorbable) -> Unit + ): Int { if (value.isEmpty()) { return 0 } @@ -25,45 +31,37 @@ object AssertionRepairWalk { break } val absorbable = gene as RxAbsorbable - val canTake = absorbable.absorbableCount(value.substring(consumed)) - if (canTake > 0) { - consumed += canTake - continue - } - if (absorbable.canBeZeroWidth) { - continue + val remaining = value.substring(consumed) + val amount = absorb(absorbable, remaining) + if (amount == 0) { + if (absorbable.canBeZeroWidth) { + onZeroWidth(absorbable) + continue + } + return 0 } - return 0 + consumed += amount } return consumed } + /** + * Maximum leading characters of [value] that can be absorbed across [genes] + * left-to-right, without mutating anything. + */ + fun absorbableCount(genes: List, value: String): Int = + walk(genes, value, + absorb = { gene, value -> gene.absorbableCount(value) }, + onZeroWidth = {} + ) + /** * Forces as much of [value] as possible into [genes] left-to-right, mutating each * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. */ - fun tryForce(genes: List, value: String): Int { - if (value.isEmpty()) { - return 0 - } - var consumed = 0 - for (gene in genes) { - if (consumed >= value.length) { - break - } - val absorbable = gene as RxAbsorbable - val remaining = value.substring(consumed) - val placed = absorbable.tryForce(remaining) - if (placed == 0) { - if (absorbable.canBeZeroWidth) { - absorbable.forceZeroWidth() - continue - } else { - return 0 - } - } - consumed += placed - } - return consumed - } -} + fun tryForce(genes: List, value: String): Int = + walk(genes, value, + absorb = { gene, value -> gene.tryForce(value) }, + onZeroWidth = { it.forceZeroWidth() } + ) +} \ No newline at end of file From 4dc16e75ee0ed7278f9d40565c426c646d947443 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 23:31:32 -0300 Subject: [PATCH 35/56] Allowed lookbehinds in java regex grammar. --- .../antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 | 3 ++- .../antlr4/org/evomaster/core/parser/RegexJavaParser.g4 | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 index 5753d6a4fa..e175228b26 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 @@ -77,6 +77,7 @@ CharacterClassEscape ; EQUAL : '='; +LESS_THAN : '<'; CARET : '^'; DOLLAR : '$'; SLASH : '\\'; @@ -97,7 +98,7 @@ COLON : ':'; BaseChar // practically all chars but the ones used for control and digits - : ~[0-9:,^$\\.*+?()[\]{}|=-] + : ~[0-9:,^$\\.<*+?()[\]{}|=-] ; fragment OctalEscapeSequence diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 index 32a933b7c9..27f14c38c0 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 @@ -52,7 +52,8 @@ assertion //TODO //// | '\\' 'b' //// | '\\' 'B' - | PAREN_open QUESTION EQUAL disjunction PAREN_close + | PAREN_open QUESTION EQUAL disjunction PAREN_close // lookahead (?=...) + | PAREN_open QUESTION LESS_THAN EQUAL disjunction PAREN_close // lookbehind (?<=...) //// | '(' '?' '!' disjunction ')' ; @@ -116,7 +117,7 @@ patternCharacter // These are also allowed as literals when no matching pair exists | BRACE_close | BRACKET_close - | COLON | EQUAL + | COLON | EQUAL | LESS_THAN | DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on. ; @@ -168,7 +169,7 @@ classAtomNoDash | DecimalDigit | COMMA | CARET | DOLLAR | DOT | STAR | PLUS | QUESTION | PAREN_open | PAREN_close | BRACKET_open | BRACE_open | BRACE_close | OR - | COLON | EQUAL + | COLON | EQUAL | LESS_THAN // should be interpreted literally: // As they are lexer tokens, these character sequences are captured as such. In particular these require some extra // steps to interpret them correctly given the context. From 648a509fdc28af4c370082e205e68399a0b014a2 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 23:33:06 -0300 Subject: [PATCH 36/56] Added enum for AssertionType, for use in AssertionRxGene. --- .../core/parser/GeneRegexJavaVisitor.kt | 2 +- .../core/search/gene/regex/AssertionRxGene.kt | 21 ++++++++++++++----- .../core/search/gene/GeneSamplerForTests.kt | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 49c6f80040..8fa2528e51 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -260,7 +260,7 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege "Nested assertions are not currently supported." } val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) - val assertionGene = AssertionRxGene(innerDisjList) + val assertionGene = AssertionRxGene(innerDisjList, AssertionType.LOOKAHEAD) hasAssertions = true res.genes.add(assertionGene) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 29b74793cb..3082b511b5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -10,6 +10,11 @@ import org.evomaster.core.search.service.mutator.MutationWeightControl import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy +/** + * Distinguishes which direction an [AssertionRxGene] forces a candidate during repair. + */ +enum class AssertionType { LOOKAHEAD, LOOKBEHIND } + /** * Represents a zero-width assertion in the regex gene tree. * @@ -19,15 +24,18 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation * * Repair is triggered from [DisjunctionRxGene.attemptAssertionRepair], invoked by * [RegexGene.randomize] after the disjunction's own sampled value is checked against - * the source pattern and found not to match. + * the source pattern and found not to match. [assertionType] determines which side of + * the enclosing disjunction's terms the candidate gets forced onto. This gene's own + * methods stay direction-agnostic. */ class AssertionRxGene( /** * The assertion's inner disjunction gene, can be null as the disjunction can be unsatisfiable, * in that case [innerGene] is null. */ - val innerGene: DisjunctionListRxGene? -) : RxTerm, CompositeFixedGene("assertion", listOfNotNull(innerGene)) { + val innerGene: DisjunctionListRxGene?, + val assertionType: AssertionType +) : RxTerm, CompositeFixedGene("assertion:${assertionType.name}", listOfNotNull(innerGene)) { /** * To handle null [innerGene], in which case the assertion is unsatisfiable. @@ -41,7 +49,7 @@ class AssertionRxGene( override fun isMutable(): Boolean = innerGene?.isMutable() ?: false override fun copyContent(): Gene { - val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene) + val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene, assertionType) copy.name = this.name return copy } @@ -95,11 +103,14 @@ class AssertionRxGene( if (other !is AssertionRxGene) { return false } + if (assertionType != other.assertionType) { + return false + } return sampledInnerValue() == other.sampledInnerValue() } override fun unsafeCopyValueFrom(other: Gene): Boolean { - if (other !is AssertionRxGene) { + if (other !is AssertionRxGene || assertionType != other.assertionType) { return false } return if (innerGene != null && other.innerGene != null) { diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index a5ab247d75..7c42a6b9d7 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -432,7 +432,7 @@ object GeneSamplerForTests { fun sampleAssertionRxGene(rand: Randomness): AssertionRxGene { val innerGene = sampleDisjunctionListRxGene(rand) innerGene.doInitialize(rand) - return AssertionRxGene(innerGene=innerGene) + return AssertionRxGene(innerGene=innerGene, AssertionType.LOOKAHEAD) } fun sampleRegexGene(rand: Randomness): RegexGene { From c3e06a45e880c3d566b5557d4a02dc656a004d7a Mon Sep 17 00:00:00 2001 From: lmasroca Date: Thu, 23 Jul 2026 23:36:21 -0300 Subject: [PATCH 37/56] Allowed lookbehinds in java visitor. --- .../org/evomaster/core/parser/GeneRegexJavaVisitor.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 8fa2528e51..32617e49e2 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -260,7 +260,12 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege "Nested assertions are not currently supported." } val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) - val assertionGene = AssertionRxGene(innerDisjList, AssertionType.LOOKAHEAD) + val assertionType = if (assertionCtx.LESS_THAN() != null) { + AssertionType.LOOKBEHIND + } else { + AssertionType.LOOKAHEAD + } + val assertionGene = AssertionRxGene(innerDisjList, assertionType) hasAssertions = true res.genes.add(assertionGene) } From 60c641f2ec7612ddd2844b8fee2b834b3eb5bd02 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 00:45:23 -0300 Subject: [PATCH 38/56] Added new suffix versions of RxAbsorbable's methods for lookbehind repairs. --- .../core/search/gene/regex/RxAbsorbable.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt index e0cb41c491..9056ff6581 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -44,4 +44,29 @@ interface RxAbsorbable { "${this::class.simpleName} cannot be zero-width but forceZeroWidth was called" ) } + + /** + * Suffix-anchored counterpart of [absorbableCount], used by lookbehind repair: how many of + * [value]'s trailing characters could this gene be forced to produce. + * + * Default delegates to [absorbableCount] fed reversed [value]. This default is + * used by genes that produce exactly one character, like [AnyCharacterRxGene]. + */ + fun absorbableSuffixCount(value: String): Int = + if (value.isEmpty()) 0 else absorbableCount(value.reversed()) + + /** + * Suffix-anchored counterpart of [tryForce]: places as many of [value]'s trailing characters + * as possible and returns how many were actually placed. + * + * Default delegates to [tryForce] fed reversed [value], in a similar way to [absorbableSuffixCount]. + * + * Precondition: [value] is not empty. + */ + fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableSuffixCount(value) + if (n > 0) tryForce(value.reversed()) + return n + } } \ No newline at end of file From e1b084c33b29fe0a9c57bb97c12581e45d3dc78d Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 00:46:17 -0300 Subject: [PATCH 39/56] Added new suffix versions of AssertionRepairWalk's methods for lookbehind repairs. --- .../search/gene/utils/AssertionRepairWalk.kt | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 9dea06f1a7..a9eb50643b 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -15,23 +15,34 @@ object AssertionRepairWalk { * Shared algorithm behind all public functions below, these differ only in: * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] + * - [forward]: whether to walk [genes] left-to-right for lookahead or right-to-left for lookbehind */ private fun walk( genes: List, value: String, absorb: (RxAbsorbable, String) -> Int, - onZeroWidth: (RxAbsorbable) -> Unit + onZeroWidth: (RxAbsorbable) -> Unit, + forward: Boolean ): Int { if (value.isEmpty()) { return 0 } var consumed = 0 - for (gene in genes) { + val walkTarget = if (forward) { + genes + } else { + genes.asReversed() + } + for (gene in walkTarget) { if (consumed >= value.length) { break } val absorbable = gene as RxAbsorbable - val remaining = value.substring(consumed) + val remaining = if (forward) { + value.substring(consumed) + } else { + value.dropLast(consumed) + } val amount = absorb(absorbable, remaining) if (amount == 0) { if (absorbable.canBeZeroWidth) { @@ -50,7 +61,7 @@ object AssertionRepairWalk { * left-to-right, without mutating anything. */ fun absorbableCount(genes: List, value: String): Int = - walk(genes, value, + walk(genes, value, forward = true, absorb = { gene, value -> gene.absorbableCount(value) }, onZeroWidth = {} ) @@ -60,8 +71,31 @@ object AssertionRepairWalk { * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. */ fun tryForce(genes: List, value: String): Int = - walk(genes, value, + walk(genes, value, forward = true, absorb = { gene, value -> gene.tryForce(value) }, onZeroWidth = { it.forceZeroWidth() } ) + + /** + * Suffix-anchored counterpart of [absorbableCount], used by lookbehind repair: maximum + * trailing characters of [value] that can be absorbed across [genes] right-to-left + * (walking [genes] in reverse, since the gene closest to the assertion's position is the + * last one in [genes]), without mutating anything. + */ + fun absorbableSuffixCount(genes: List, value: String): Int = + walk(genes, value, forward = false, + absorb = { gene, value -> gene.absorbableSuffixCount(value) }, + onZeroWidth = {} + ) + + /** + * Suffix-anchored counterpart of [tryForce], used by lookbehind repair: forces as much + * of [value] as possible into [genes] right-to-left (mirroring [tryForce]), mutating each + * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. + */ + fun tryForceSuffix(genes: List, value: String): Int = + walk(genes, value, forward = false, + absorb = { gene, value -> gene.tryForceSuffix(value) }, + onZeroWidth = { it.forceZeroWidth() } + ) } \ No newline at end of file From 1bc6c85a60a75497b37d702247fe26df8e6b4280 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 03:48:42 -0300 Subject: [PATCH 40/56] Added overrides for new suffix RxAbsorbable methods. --- .../gene/regex/DisjunctionListRxGene.kt | 46 +++++++++-- .../search/gene/regex/DisjunctionRxGene.kt | 18 +++++ .../gene/regex/PatternCharacterBlockGene.kt | 81 +++++++++++++++---- .../search/gene/regex/QuantifierRxGene.kt | 17 ++++ 4 files changed, 141 insertions(+), 21 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 5fe86980a2..e35e561a34 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -210,13 +210,17 @@ class DisjunctionListRxGene( /** * Ranks all branches by how much of [value] they can absorb, without mutating - * anything. + * anything. Shared by both directions: [absorb] is [DisjunctionRxGene.absorbableCount] + * for lookahead's ranking, [DisjunctionRxGene.absorbableSuffixCount] for lookbehind's. */ - private fun rankBranches(value: String): BranchRanking? { + private fun rankBranches( + value: String, + absorb: (DisjunctionRxGene, String) -> Int + ): BranchRanking? { if (value.isEmpty() || disjunctions.isEmpty()) { return null } - var bestCount = disjunctions[activeDisjunction].absorbableCount(value) + var bestCount = absorb(disjunctions[activeDisjunction], value) var bestIndex = activeDisjunction for (i in disjunctions.indices) { if (i == activeDisjunction) { @@ -225,7 +229,7 @@ class DisjunctionListRxGene( if (bestCount == value.length) { break } - val can = disjunctions[i].absorbableCount(value) + val can = absorb(disjunctions[i], value) if (can > bestCount) { bestCount = can bestIndex = i @@ -241,7 +245,7 @@ class DisjunctionListRxGene( * @see [rankBranches] */ override fun absorbableCount(value: String): Int = - rankBranches(value)?.absorbableCount ?: 0 + rankBranches(value){ disjunction, value -> disjunction.absorbableCount(value) }?.absorbableCount ?: 0 /** * True if at least one branch can render "", as we can select that branch and force it. @@ -257,7 +261,8 @@ class DisjunctionListRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - val (bestCount, bestIndex) = rankBranches(value) ?: BranchRanking(0, activeDisjunction) + val (bestCount, bestIndex) = rankBranches(value){ disjunction, value -> disjunction.absorbableCount(value) } + ?: BranchRanking(0, activeDisjunction) if (bestCount > 0) { if (bestIndex != activeDisjunction) { @@ -270,6 +275,35 @@ class DisjunctionListRxGene( return 0 } + /** + * Suffix counterpart of [absorbableCount]: ranks every branch by how much of + * [value]'s trailing characters it could absorb, without mutating. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + rankBranches(value) { d, v -> d.absorbableSuffixCount(v) }?.absorbableCount ?: 0 + + /** + * Suffix counterpart of [tryForce]: activates whichever branch can best absorb + * [value]'s trailing characters and forces it there, walking right-to-left. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + val (bestCount, bestIndex) = rankBranches(value, { d, v -> d.absorbableSuffixCount(v) }) + ?: BranchRanking(0, activeDisjunction) + + if (bestCount > 0) { + if (bestIndex != activeDisjunction) { + activeDisjunction = bestIndex + tryToActivateGene(disjunctions[bestIndex]) + } + return disjunctions[bestIndex].tryForceSuffix(value) + } + + return 0 + } + /** * Forces the active branch to zero width if it can; otherwise switches to the first * branch that can and forces that one instead. diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 2afec87505..d3294db4be 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -198,6 +198,14 @@ class DisjunctionRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(terms, value) + /** + * Delegates to a backward walk over [terms]. Mirrors [absorbableCount], walking + * right-to-left since lookbehind's target sits before the assertion. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + AssertionRepairWalk.absorbableSuffixCount(terms, value) + /** * True only if every term can independently render "", as this disjunction's own value is * the concatenation of all of them. @@ -216,6 +224,16 @@ class DisjunctionRxGene( return AssertionRepairWalk.tryForce(terms, value) } + /** + * Delegates to a backward walk over [terms], mirroring [tryForce] in the opposite + * direction. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + return AssertionRepairWalk.tryForceSuffix(terms, value) + } + /** * Forces every term to zero width individually. * @see [RxAbsorbable.forceZeroWidth] diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index 8eea221606..a2d78499b4 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -116,19 +116,30 @@ class PatternCharacterBlockGene( } /** - * How many of [stringBlock]'s leading characters match [value]'s leading characters, - * case-insensitively wherever [flags] allows it. 0 when a character does not match, - * one of the strings must be consumed completely. - * @see [RxAbsorbable.absorbableCount] + * Maps a forward walk-position [index] to the real index into [value], depending on [reversed]. */ - override fun absorbableCount(value: String): Int { + private fun realIndex(reversed: Boolean, value: String, index: Int): Int = + if (reversed) { + value.lastIndex - index + } else { + index + } + + /** + * Shared body behind [absorbableCount]/[absorbableSuffixCount], they differ only in which + * end of [stringBlock] and [value] the walk anchors to, via [reversed]. No partial matches, + * returns 0 or the shortest string's length (which was matched). + */ + private fun matchCount(value: String, reversed: Boolean): Int { var i = 0 while (i < value.length && i < stringBlock.length) { - val c = stringBlock[i] + val blockIdx = realIndex(reversed, stringBlock, i) + val valueIdx = realIndex(reversed, value, i) + val c = stringBlock[blockIdx] val matches = if (flags.isCaseable(c)) { - value[i].equals(c, ignoreCase = true) + value[valueIdx].equals(c, ignoreCase = true) } else { - value[i] == c + value[valueIdx] == c } if (!matches) { return 0 @@ -138,6 +149,29 @@ class PatternCharacterBlockGene( return i } + /** + * Shared body behind [tryForce]/[tryForceSuffix]: commits the case (upper/lower) of the + * first (or last if [reversed]) [matchedLength] characters of [stringBlock] to match + * [value]'s corresponding characters. + */ + private fun applyForce(value: String, matchedLength: Int, reversed: Boolean) { + for (i in 0 until matchedLength) { + val blockIdx = realIndex(reversed, stringBlock, i) + val valueIdx = realIndex(reversed, value, i) + if (flags.isCaseable(stringBlock[blockIdx])) { + caseChoices[blockIdx] = value[valueIdx].isUpperCase() + } + } + } + + /** + * How many of [stringBlock]'s leading characters match [value]'s leading characters, + * case-insensitively wherever [flags] allows it. 0 when a character does not match, + * one of the strings must be consumed completely. + * @see [RxAbsorbable.absorbableCount] + */ + override fun absorbableCount(value: String): Int = matchCount(value, reversed = false) + /** * True only when [stringBlock] is empty, as a non-empty literal can never render "". * @see [RxAbsorbable.canBeZeroWidth] @@ -151,13 +185,9 @@ class PatternCharacterBlockGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - val n = absorbableCount(value) - require(n == stringBlock.length || n == value.length || n==0) - for (i in 0 until n) { - if (flags.isCaseable(stringBlock[i])) { - caseChoices[i] = value[i].isUpperCase() - } - } + val n = matchCount(value, reversed = false) + require(n == stringBlock.length || n == value.length || n == 0) + applyForce(value, matchedLength = n, reversed = false) return n } @@ -168,4 +198,25 @@ class PatternCharacterBlockGene( override fun forceZeroWidth() { require(canBeZeroWidth) } + + /** + * Suffix-anchored mirror of [absorbableCount]: how many of [stringBlock]'s trailing characters + * match [value]'s trailing characters, case-insensitively wherever [flags] allows it. + * 0 when a character does not match, one of the strings must be consumed completely. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = matchCount(value, reversed = true) + + /** + * Suffix-anchored mirror of [tryForce]: Commits the matching trailing characters' case to + * match [value]; mirrors [absorbableSuffixCount] exactly. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + val n = matchCount(value, reversed = true) + require(n == stringBlock.length || n == value.length || n == 0) + applyForce(value, matchedLength = n, reversed = true) + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index 61eb5dc8d7..ebb010daa2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -250,6 +250,14 @@ class QuantifierRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(atoms, value) + /** + * Delegates to a backward walk over [atoms], mirroring [absorbableCount] in the + * opposite direction. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + AssertionRepairWalk.absorbableSuffixCount(atoms, value) + /** * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". * @see [RxAbsorbable.canBeZeroWidth] @@ -267,6 +275,15 @@ class QuantifierRxGene( return AssertionRepairWalk.tryForce(atoms, value) } + /** + * Delegates to a backward walk over [atoms], mirroring [tryForce]. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + return AssertionRepairWalk.tryForceSuffix(atoms, value) + } + /** * Collapses to zero repetitions if [min] == 0 (removing every atom), otherwise forces * each existing atom to zero width individually. From 63d2bdaa58afd75f1154be38d3b38fa05eb3ccba Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 03:51:59 -0300 Subject: [PATCH 41/56] Added support for right-to-left repairs in DisjunctionRxGene.attemptAssertionRepair, allowing lookbehind repairs. --- .../search/gene/regex/DisjunctionRxGene.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index d3294db4be..567b90d0a8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -246,7 +246,9 @@ class DisjunctionRxGene( /** * Attempts to repair this disjunction's own value so that each of its direct-term * [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner - * value onto the genes that follow it within [terms]. + * value onto the genes on the appropriate side of it within [terms]: + * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] + * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. */ fun attemptAssertionRepair(randomness: Randomness) { if (terms.none { it is AssertionRxGene }) { @@ -255,23 +257,37 @@ class DisjunctionRxGene( for (idx in terms.indices) { val assertion = terms[idx] as? AssertionRxGene ?: continue - if (assertion.innerGene == null) { - continue + val innerGene = assertion.innerGene ?: continue + + val target = if (assertion.assertionType == AssertionType.LOOKBEHIND) { + terms.subList(0, idx).filter { it !is AssertionRxGene } + } else { + terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } } - val genesAfter = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } - if (genesAfter.isEmpty()) { + // we may not be able to force genes as target is empty but if the lookaround can be zero-width it is fine. + if (target.isEmpty()) { + if (innerGene.canBeZeroWidth) { + innerGene.forceZeroWidth() + continue + } return } + val (countFunction, forceFunction) = + if (assertion.assertionType == AssertionType.LOOKBEHIND) { + AssertionRepairWalk::absorbableSuffixCount to AssertionRepairWalk::tryForceSuffix + } else { + AssertionRepairWalk::absorbableCount to AssertionRepairWalk::tryForce + } + var satisfied = false for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() - || AssertionRepairWalk.absorbableCount(genesAfter, candidate) == candidate.length) { + if (candidate.isEmpty() || countFunction(target, candidate) == candidate.length) { if (candidate.isNotEmpty()) { - AssertionRepairWalk.tryForce(genesAfter, candidate) + forceFunction(target, candidate) } satisfied = true break From 3489f49fc0f5fdbe1a386046aea98a54fd81afe6 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 03:52:55 -0300 Subject: [PATCH 42/56] Added lookbehind tests. --- .../core/parser/GeneRegexJavaVisitorTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index a8220c3f22..19d7ba5216 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -484,4 +484,28 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?=[a&&b])a(bcef|de)de") } checkSameAsJava("abc|(?=[a&&b])def") } + + @Test + fun testSimpleLookbehinds() { + checkSameAsJava("foo(?<=oo)\\d+") + checkSameAsJava("\\d(?<=[13579])") + checkSameAsJava("a(?<=a)b") + checkSameAsJava("\\w*(?<=z)c") + checkSameAsJava("[a-z]+(?<=aa|bb)cc") + checkSameAsJava("a(?<=a)b") + checkSameAsJava("(abc|ab|a)(?<=abc)") + checkSameAsJava("(?a)\\k") + } + + @Test + fun testLookbehindRepairAcrossDirections() { + checkSameAsJava("\\w+(?<=X*)m(?=z)\\w") + checkSameAsJava("^(?<=X*)m(?=z)(a|z)") + } + + @Test + fun testUnsatisfiableLookbehinds() { + assertThrows { checkSameAsJava("(?<=X)a") } + assertThrows { checkSameAsJava("a(?<=[a&&b])a") } + } } \ No newline at end of file From d7c97db168cd8a110c3714f52216dcc89a8467cb Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 13:37:28 -0300 Subject: [PATCH 43/56] Passed hasAssertions to RegexGene constructor in RegexGene.copyContent. --- .../kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index daa9c0491f..f09b7b67b6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -43,7 +43,7 @@ class RegexGene( override fun copyContent(): Gene { - return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags) + return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags, hasAssertions) } companion object { From d23e2fd813378074ff59a12235b0f3c7f4e25a5b Mon Sep 17 00:00:00 2001 From: lmasroca Date: Fri, 24 Jul 2026 13:37:28 -0300 Subject: [PATCH 44/56] Passed hasAssertions to RegexGene constructor in RegexGene.copyContent. --- .../kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index daa9c0491f..f09b7b67b6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -43,7 +43,7 @@ class RegexGene( override fun copyContent(): Gene { - return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags) + return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags, hasAssertions) } companion object { From cf9abae99de34445718e73ff211827d093d8d273 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sat, 25 Jul 2026 17:53:14 -0300 Subject: [PATCH 45/56] Refactor --- .../gene/regex/DisjunctionListRxGene.kt | 50 +++++++++++-------- .../search/gene/utils/AssertionRepairWalk.kt | 24 ++++----- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index e35e561a34..5307762eb0 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -254,27 +254,41 @@ class DisjunctionListRxGene( override val canBeZeroWidth: Boolean = disjunctions.any { it.canBeZeroWidth } /** - * Activates whichever branch can best absorb [value] (switching [activeDisjunction] if - * needed) and forces it there. - * @see [RxAbsorbable.tryForce] - * @see [rankBranches] + * Shared rank-then-force logic behind [tryForce]/[tryForceSuffix]: ranks all branches via + * [absorb], switches [activeDisjunction] to the winner if needed, then delegates to [force] + * on that branch. */ - override fun tryForce(value: String): Int { - require(value.isNotEmpty()) - val (bestCount, bestIndex) = rankBranches(value){ disjunction, value -> disjunction.absorbableCount(value) } - ?: BranchRanking(0, activeDisjunction) + private fun forceBestBranch( + value: String, + absorb: (DisjunctionRxGene, String) -> Int, + force: (DisjunctionRxGene, String) -> Int + ): Int { + val (bestCount, bestIndex) = rankBranches(value, absorb) ?: BranchRanking(0, activeDisjunction) if (bestCount > 0) { if (bestIndex != activeDisjunction) { activeDisjunction = bestIndex tryToActivateGene(disjunctions[bestIndex]) } - return disjunctions[bestIndex].tryForce(value) + return force(disjunctions[bestIndex], value) } - return 0 } + /** + * Activates whichever branch can best absorb [value] (switching [activeDisjunction] if + * needed) and forces it there. + * @see [RxAbsorbable.tryForce] + * @see [rankBranches] + */ + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + return forceBestBranch(value, + absorb = { d, v -> d.absorbableCount(v) }, + force = { d, v -> d.tryForce(v) } + ) + } + /** * Suffix counterpart of [absorbableCount]: ranks every branch by how much of * [value]'s trailing characters it could absorb, without mutating. @@ -290,18 +304,10 @@ class DisjunctionListRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - val (bestCount, bestIndex) = rankBranches(value, { d, v -> d.absorbableSuffixCount(v) }) - ?: BranchRanking(0, activeDisjunction) - - if (bestCount > 0) { - if (bestIndex != activeDisjunction) { - activeDisjunction = bestIndex - tryToActivateGene(disjunctions[bestIndex]) - } - return disjunctions[bestIndex].tryForceSuffix(value) - } - - return 0 + return forceBestBranch(value, + absorb = { d, v -> d.absorbableSuffixCount(v) }, + force = { d, v -> d.tryForceSuffix(v) } + ) } /** diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index a9eb50643b..5a1dbb3322 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -15,33 +15,33 @@ object AssertionRepairWalk { * Shared algorithm behind all public functions below, these differ only in: * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] - * - [forward]: whether to walk [genes] left-to-right for lookahead or right-to-left for lookbehind + * - [reversed]: whether to walk [genes] right-to-left for lookbehind or left-to-right for lookahead */ private fun walk( genes: List, value: String, absorb: (RxAbsorbable, String) -> Int, onZeroWidth: (RxAbsorbable) -> Unit, - forward: Boolean + reversed: Boolean ): Int { if (value.isEmpty()) { return 0 } var consumed = 0 - val walkTarget = if (forward) { - genes - } else { + val walkTarget = if (reversed) { genes.asReversed() + } else { + genes } for (gene in walkTarget) { if (consumed >= value.length) { break } val absorbable = gene as RxAbsorbable - val remaining = if (forward) { - value.substring(consumed) - } else { + val remaining = if (reversed) { value.dropLast(consumed) + } else { + value.substring(consumed) } val amount = absorb(absorbable, remaining) if (amount == 0) { @@ -61,7 +61,7 @@ object AssertionRepairWalk { * left-to-right, without mutating anything. */ fun absorbableCount(genes: List, value: String): Int = - walk(genes, value, forward = true, + walk(genes, value, reversed = false, absorb = { gene, value -> gene.absorbableCount(value) }, onZeroWidth = {} ) @@ -71,7 +71,7 @@ object AssertionRepairWalk { * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. */ fun tryForce(genes: List, value: String): Int = - walk(genes, value, forward = true, + walk(genes, value, reversed = false, absorb = { gene, value -> gene.tryForce(value) }, onZeroWidth = { it.forceZeroWidth() } ) @@ -83,7 +83,7 @@ object AssertionRepairWalk { * last one in [genes]), without mutating anything. */ fun absorbableSuffixCount(genes: List, value: String): Int = - walk(genes, value, forward = false, + walk(genes, value, reversed = true, absorb = { gene, value -> gene.absorbableSuffixCount(value) }, onZeroWidth = {} ) @@ -94,7 +94,7 @@ object AssertionRepairWalk { * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. */ fun tryForceSuffix(genes: List, value: String): Int = - walk(genes, value, forward = false, + walk(genes, value, reversed = true, absorb = { gene, value -> gene.tryForceSuffix(value) }, onZeroWidth = { it.forceZeroWidth() } ) From 5b95bd61034989cf3c9e0e76556946bd2a13d630 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 22:34:24 -0300 Subject: [PATCH 46/56] Removed redundant code. --- .../evomaster/core/search/gene/regex/DisjunctionListRxGene.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 5307762eb0..2828c7485e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -267,7 +267,6 @@ class DisjunctionListRxGene( if (bestCount > 0) { if (bestIndex != activeDisjunction) { - activeDisjunction = bestIndex tryToActivateGene(disjunctions[bestIndex]) } return force(disjunctions[bestIndex], value) @@ -322,7 +321,6 @@ class DisjunctionListRxGene( val target = order.first { disjunctions[it].canBeZeroWidth } disjunctions[target].forceZeroWidth() if (target != activeDisjunction) { - activeDisjunction = target tryToActivateGene(disjunctions[target]) } } From a4564fc2f47a34152d25c7969dc780e301357faa Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 23:19:49 -0300 Subject: [PATCH 47/56] Introduced WalkOutcome as a walk result instead of Int. --- .../search/gene/regex/DisjunctionRxGene.kt | 10 ++--- .../search/gene/regex/QuantifierRxGene.kt | 8 ++-- .../search/gene/utils/AssertionRepairWalk.kt | 37 +++++++++++++------ 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 567b90d0a8..34f6ae048e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -196,7 +196,7 @@ class DisjunctionRxGene( * @see [AssertionRepairWalk.absorbableCount] */ override fun absorbableCount(value: String): Int = - AssertionRepairWalk.absorbableCount(terms, value) + AssertionRepairWalk.absorbableCount(terms, value).consumed /** * Delegates to a backward walk over [terms]. Mirrors [absorbableCount], walking @@ -204,7 +204,7 @@ class DisjunctionRxGene( * @see [RxAbsorbable.absorbableSuffixCount] */ override fun absorbableSuffixCount(value: String): Int = - AssertionRepairWalk.absorbableSuffixCount(terms, value) + AssertionRepairWalk.absorbableSuffixCount(terms, value).consumed /** * True only if every term can independently render "", as this disjunction's own value is @@ -221,7 +221,7 @@ class DisjunctionRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForce(terms, value) + return AssertionRepairWalk.tryForce(terms, value).consumed } /** @@ -231,7 +231,7 @@ class DisjunctionRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForceSuffix(terms, value) + return AssertionRepairWalk.tryForceSuffix(terms, value).consumed } /** @@ -285,7 +285,7 @@ class DisjunctionRxGene( for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() || countFunction(target, candidate) == candidate.length) { + if (candidate.isEmpty() || countFunction(target, candidate).consumed == candidate.length) { if (candidate.isNotEmpty()) { forceFunction(target, candidate) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index ebb010daa2..fd7ca0eef7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -248,7 +248,7 @@ class QuantifierRxGene( * @see [AssertionRepairWalk.absorbableCount] */ override fun absorbableCount(value: String): Int = - AssertionRepairWalk.absorbableCount(atoms, value) + AssertionRepairWalk.absorbableCount(atoms, value).consumed /** * Delegates to a backward walk over [atoms], mirroring [absorbableCount] in the @@ -256,7 +256,7 @@ class QuantifierRxGene( * @see [RxAbsorbable.absorbableSuffixCount] */ override fun absorbableSuffixCount(value: String): Int = - AssertionRepairWalk.absorbableSuffixCount(atoms, value) + AssertionRepairWalk.absorbableSuffixCount(atoms, value).consumed /** * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". @@ -272,7 +272,7 @@ class QuantifierRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForce(atoms, value) + return AssertionRepairWalk.tryForce(atoms, value).consumed } /** @@ -281,7 +281,7 @@ class QuantifierRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForceSuffix(atoms, value) + return AssertionRepairWalk.tryForceSuffix(atoms, value).consumed } /** diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 5a1dbb3322..2bf6c1a82e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -3,6 +3,16 @@ package org.evomaster.core.search.gene.utils import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.regex.RxAbsorbable +/** + * Outcome of one greedy walk over a gene list: [consumed] is how much of the candidate string + * can be placed. If less than the full string was consumed, [hardMismatch] says why. `true` means + * a gene genuinely refused to match ([consumed] resets to 0 in that case) and the candidate is + * simply wrong here; `false` means the gene list just ran out of room with no mismatch, + * so the leftover is still worth trying to escape outwards. + */ + +data class WalkOutcome(val consumed: Int, val hardMismatch: Boolean) + /** * Greedy-walk utilities for assertion repair. * @@ -16,6 +26,7 @@ object AssertionRepairWalk { * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] * - [reversed]: whether to walk [genes] right-to-left for lookbehind or left-to-right for lookahead + * @see WalkOutcome */ private fun walk( genes: List, @@ -23,9 +34,9 @@ object AssertionRepairWalk { absorb: (RxAbsorbable, String) -> Int, onZeroWidth: (RxAbsorbable) -> Unit, reversed: Boolean - ): Int { + ): WalkOutcome { if (value.isEmpty()) { - return 0 + return WalkOutcome(0, hardMismatch = false) } var consumed = 0 val walkTarget = if (reversed) { @@ -41,7 +52,7 @@ object AssertionRepairWalk { val remaining = if (reversed) { value.dropLast(consumed) } else { - value.substring(consumed) + value.drop(consumed) } val amount = absorb(absorbable, remaining) if (amount == 0) { @@ -49,18 +60,19 @@ object AssertionRepairWalk { onZeroWidth(absorbable) continue } - return 0 + return WalkOutcome(0, hardMismatch = true) } consumed += amount } - return consumed + return WalkOutcome(consumed, hardMismatch = false) } /** * Maximum leading characters of [value] that can be absorbed across [genes] * left-to-right, without mutating anything. + * @see WalkOutcome */ - fun absorbableCount(genes: List, value: String): Int = + fun absorbableCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.absorbableCount(value) }, onZeroWidth = {} @@ -68,9 +80,10 @@ object AssertionRepairWalk { /** * Forces as much of [value] as possible into [genes] left-to-right, mutating each - * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. + * gene in place using each gene's [RxAbsorbable.tryForce]. + * @see WalkOutcome */ - fun tryForce(genes: List, value: String): Int = + fun tryForce(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.tryForce(value) }, onZeroWidth = { it.forceZeroWidth() } @@ -81,8 +94,9 @@ object AssertionRepairWalk { * trailing characters of [value] that can be absorbed across [genes] right-to-left * (walking [genes] in reverse, since the gene closest to the assertion's position is the * last one in [genes]), without mutating anything. + * @see WalkOutcome */ - fun absorbableSuffixCount(genes: List, value: String): Int = + fun absorbableSuffixCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.absorbableSuffixCount(value) }, onZeroWidth = {} @@ -91,9 +105,10 @@ object AssertionRepairWalk { /** * Suffix-anchored counterpart of [tryForce], used by lookbehind repair: forces as much * of [value] as possible into [genes] right-to-left (mirroring [tryForce]), mutating each - * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. + * gene in place using [RxAbsorbable.tryForceSuffix]. + * @see WalkOutcome */ - fun tryForceSuffix(genes: List, value: String): Int = + fun tryForceSuffix(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.tryForceSuffix(value) }, onZeroWidth = { it.forceZeroWidth() } From 09fecf3ecaff2543451e72029f3752e68ed9967a Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 23:36:58 -0300 Subject: [PATCH 48/56] Added AssertionRepairResult as new return type for attemptAssertionRepair instead of Unit. --- .../gene/regex/DisjunctionListRxGene.kt | 11 ++++--- .../search/gene/regex/DisjunctionRxGene.kt | 10 +++--- .../gene/utils/AssertionRepairResult.kt | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 2828c7485e..257e19c8a5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -5,6 +5,7 @@ import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.interfaces.PhenotypeDormantGene +import org.evomaster.core.search.gene.utils.AssertionRepairResult import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionListRxGeneImpact import org.evomaster.core.search.service.AdaptiveParameterControl @@ -326,11 +327,11 @@ class DisjunctionListRxGene( } /** - * Delegates assertion repair to whichever branch is currently active, as only that - * branch's rendered value is ever observed, so only it needs repairing. See - * [DisjunctionRxGene.attemptAssertionRepair] for the actual repair logic. + * Delegates to whichever branch is currently active, as only that branch's rendered + * value is ever observed, so only it needs repairing. See [DisjunctionRxGene.attemptAssertionRepair] for + * the actual repair logic and what its return value means. */ - fun attemptAssertionRepair(randomness: Randomness) { - disjunctions.getOrNull(activeDisjunction)?.attemptAssertionRepair(randomness) + fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { + return disjunctions[activeDisjunction].attemptAssertionRepair(randomness) } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 34f6ae048e..a72731f978 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -4,6 +4,7 @@ import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.utils.AssertionRepairResult import org.evomaster.core.search.gene.utils.AssertionRepairWalk import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionRxGeneImpact @@ -250,9 +251,9 @@ class DisjunctionRxGene( * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. */ - fun attemptAssertionRepair(randomness: Randomness) { + fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { if (terms.none { it is AssertionRxGene }) { - return + return AssertionRepairResult.SUCCESS } for (idx in terms.indices) { @@ -271,7 +272,7 @@ class DisjunctionRxGene( innerGene.forceZeroWidth() continue } - return + return AssertionRepairResult.FAILURE } val (countFunction, forceFunction) = @@ -294,8 +295,9 @@ class DisjunctionRxGene( } } if (!satisfied) { - return + return AssertionRepairResult.FAILURE } } + return AssertionRepairResult.SUCCESS } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt new file mode 100644 index 0000000000..70a1d3fe19 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -0,0 +1,32 @@ +package org.evomaster.core.search.gene.utils + +import org.evomaster.core.search.gene.regex.DisjunctionListRxGene +import org.evomaster.core.search.gene.regex.DisjunctionRxGene + +/** + * Result of [DisjunctionRxGene.attemptAssertionRepair] / [DisjunctionListRxGene.attemptAssertionRepair]. + * + * Assertion repair for one scope (a [DisjunctionRxGene]'s own direct terms) can be successful, + * fail or be conditionally successful, depending on either an external prefix/postfix (or both). + * + * [neededPrefix] is a requirement on whatever precedes this scope (from a lookbehind-direction); + * [neededPostfix] is a requirement on whatever follows it (lookahead-direction). Each is + * either `null` (no requirement in that direction), `""` (everything further out on that side must + * also collapse to zero width), or a non-empty literal that must be absorbed there. + */ +data class AssertionRepairResult( + val success: Boolean, + val neededPrefix: String? = null, + val neededPostfix: String? = null +) { + init { + require(success || (neededPrefix == null && neededPostfix == null)) { + "A failed AssertionRepairResult cannot carry an outward requirement" + } + } + + companion object { + val SUCCESS = AssertionRepairResult(success = true) + val FAILURE = AssertionRepairResult(success = false) + } +} \ No newline at end of file From e0ee3d98ebe0db50dee03d6545dcc8ec469882a6 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 12:56:47 -0300 Subject: [PATCH 49/56] Refactored DisjunctionRxGene.attemptAssertionRepair. --- .../search/gene/regex/DisjunctionRxGene.kt | 110 +++++++++++++----- .../gene/utils/AssertionRepairResult.kt | 28 +++++ 2 files changed, 107 insertions(+), 31 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index a72731f978..8e67de63d3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -256,48 +256,96 @@ class DisjunctionRxGene( return AssertionRepairResult.SUCCESS } + return repairDirectAssertions(randomness) + } + + /** + * Repairs every direct-term assertion in [terms]. + */ + private fun repairDirectAssertions(randomness: Randomness): AssertionRepairResult { + var pending = AssertionRepairResult.SUCCESS for (idx in terms.indices) { val assertion = terms[idx] as? AssertionRxGene ?: continue - val innerGene = assertion.innerGene ?: continue + val backward = assertion.assertionType == AssertionType.LOOKBEHIND + val target = if (backward) genesBefore(idx) else genesAfter(idx) - val target = if (assertion.assertionType == AssertionType.LOOKBEHIND) { - terms.subList(0, idx).filter { it !is AssertionRxGene } + val resolution = if (target.isEmpty()) { + repairAssertionWithNoTarget(assertion, backward, randomness) } else { - terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } + repairAssertionAgainstTarget(assertion, target, backward, randomness) } - - // we may not be able to force genes as target is empty but if the lookaround can be zero-width it is fine. - if (target.isEmpty()) { - if (innerGene.canBeZeroWidth) { - innerGene.forceZeroWidth() - continue - } + pending = pending.mergedWith(resolution) + if (!pending.success) { return AssertionRepairResult.FAILURE } + } + return pending + } - val (countFunction, forceFunction) = - if (assertion.assertionType == AssertionType.LOOKBEHIND) { - AssertionRepairWalk::absorbableSuffixCount to AssertionRepairWalk::tryForceSuffix - } else { - AssertionRepairWalk::absorbableCount to AssertionRepairWalk::tryForce - } + /** + * Handles an assertion with nothing local to force onto: escapes zero-width if the assertion + * itself allows it. + */ + private fun repairAssertionWithNoTarget(assertion: AssertionRxGene, backward: Boolean, randomness: Randomness): AssertionRepairResult { + val innerGene = assertion.innerGene!! + if (innerGene.canBeZeroWidth) { + innerGene.forceZeroWidth() + return AssertionRepairResult.SUCCESS + } + return AssertionRepairResult.FAILURE + } - var satisfied = false - for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { - assertion.randomize(randomness, false) - val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() || countFunction(target, candidate).consumed == candidate.length) { - if (candidate.isNotEmpty()) { - forceFunction(target, candidate) - } - satisfied = true - break - } + /** + * Resamples [assertion] up to [MAX_LOCAL_ASSERTION_ATTEMPTS] times looking for a candidate + * [target] fully absorbs. + */ + private fun repairAssertionAgainstTarget(assertion: AssertionRxGene, target: List, backward: Boolean, randomness: Randomness): AssertionRepairResult { + val countFunction = countWalkFunction(backward) + val forceFunction = forceWalkFunction(backward) + + for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { + assertion.randomize(randomness, false) + val candidate = assertion.sampledInnerValue()!! + if (candidate.isEmpty()) { + return AssertionRepairResult.SUCCESS } - if (!satisfied) { - return AssertionRepairResult.FAILURE + if (countFunction(target, candidate).consumed == candidate.length) { + forceFunction(target, candidate) + return AssertionRepairResult.SUCCESS } } - return AssertionRepairResult.SUCCESS + return AssertionRepairResult.FAILURE + } + + /** + * The genes in [terms] lying before index [idx], excluding other assertions. This is the forcing + * target for a [AssertionType.LOOKBEHIND] assertion sitting at [idx]. + */ + private fun genesBefore(idx: Int): List = + terms.subList(0, idx).filter { it !is AssertionRxGene } + + /** + * The genes in [terms] lying after index [idx], excluding other assertions. This is the forcing + * target for a [AssertionType.LOOKAHEAD] assertion sitting at [idx]. + */ + private fun genesAfter(idx: Int): List = + terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } + + /** + * The read-only walk function for [backward]. + */ + private fun countWalkFunction(backward: Boolean) = if (backward) { + AssertionRepairWalk::absorbableSuffixCount + } else { + AssertionRepairWalk::absorbableCount + } + + /** + * The mutating counterpart of [countWalkFunction], for the same [backward] direction. + */ + private fun forceWalkFunction(backward: Boolean) = if (backward) { + AssertionRepairWalk::tryForceSuffix + } else { + AssertionRepairWalk::tryForce } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt index 70a1d3fe19..e79f68b26f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -25,8 +25,36 @@ data class AssertionRepairResult( } } + /** + * Combines this result with [next], the outcome of resolving one more requirement in the + * same scope. Failure on either side wins outright, otherwise [next]'s own + * [neededPrefix]/[neededPostfix] each take priority over this result's, falling back to + * this result's own value when [next] did not set one of them. + */ + fun mergedWith(next: AssertionRepairResult): AssertionRepairResult { + if (!success || !next.success) { + return FAILURE + } + return AssertionRepairResult( + success = true, + neededPrefix = next.neededPrefix ?: neededPrefix, + neededPostfix = next.neededPostfix ?: neededPostfix + ) + } + companion object { val SUCCESS = AssertionRepairResult(success = true) val FAILURE = AssertionRepairResult(success = false) + + /** + * A successful result whose only outcome is [remainder] still being needed by whatever + * lies further out, depending on [backward]. + */ + fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = + if (backward) { + AssertionRepairResult(success = true, neededPrefix = remainder) + } else { + AssertionRepairResult(success = true, neededPostfix = remainder) + } } } \ No newline at end of file From cf70fed0d7b1efd1410f1b5be6d19fc0b274b3c8 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:33:51 -0300 Subject: [PATCH 50/56] Added multiple passes to DisjunctionRxGene.attemptAssertionRepair to handle nested groups' outward requirements. --- .../search/gene/regex/DisjunctionRxGene.kt | 157 ++++++++++++++++-- 1 file changed, 146 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 8e67de63d3..d8c92e6184 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -22,6 +22,13 @@ import org.slf4j.LoggerFactory */ const val MAX_LOCAL_ASSERTION_ATTEMPTS = 20 +/** + * One nested group's own unresolved requirement, as settled by [DisjunctionRxGene.settleNestedGroups] + * and resolved by [DisjunctionRxGene.resolveNestedGroupRequirements]. [termIndex] is that group's + * own index in [DisjunctionRxGene.terms]. + */ +private data class NestedGroupRequirement(val termIndex: Int, val result: AssertionRepairResult) + class DisjunctionRxGene( name: String, val terms: List, @@ -249,18 +256,94 @@ class DisjunctionRxGene( * [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner * value onto the genes on the appropriate side of it within [terms]: * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] - * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. + * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND] + * + * Also, recurses into any direct term that is itself a nested group repairing + * it first, and resolving whatever it couldn't satisfy locally against this scope's own + * neighboring terms. Runs as three passes, in order: [settleNestedGroups], + * [resolveNestedGroupRequirements], [repairDirectAssertions]. + * + * @return whether repair succeeded, with possible outside requirements. */ fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { - if (terms.none { it is AssertionRxGene }) { + if (terms.none { it is AssertionRxGene || it is DisjunctionListRxGene }) { return AssertionRepairResult.SUCCESS } - return repairDirectAssertions(randomness) + val nestedGroupRequirements = settleNestedGroups(randomness) + ?: return AssertionRepairResult.FAILURE + + val nestedResult = resolveNestedGroupRequirements(nestedGroupRequirements) + if (!nestedResult.success) { + return AssertionRepairResult.FAILURE + } + + val directResult = repairDirectAssertions(randomness) + if (!directResult.success) { + return AssertionRepairResult.FAILURE + } + + return AssertionRepairResult( + success = true, + neededPrefix = directResult.neededPrefix ?: nestedResult.neededPrefix, + neededPostfix = directResult.neededPostfix ?: nestedResult.neededPostfix + ) + } + + /** + * Pass 1 of [attemptAssertionRepair]: settles every nested group's own internal repair, + * left to right, before anything in this scope uses it as a forcing target. + * + * @return the outward requirements for each nested group's own term index, in ascending index order + * (left to right, matching [terms] itself). `null` if any nested group's own repair failed outright. + */ + private fun settleNestedGroups(randomness: Randomness): List? { + val nestedGroupRequirements = mutableListOf() + for (idx in terms.indices) { + val term = terms[idx] as? DisjunctionListRxGene ?: continue + val result = term.attemptAssertionRepair(randomness) + if (!result.success) { + return null + } + if (result.neededPrefix != null || result.neededPostfix != null) { + nestedGroupRequirements.add(NestedGroupRequirement(idx, result)) + } + } + return nestedGroupRequirements + } + + /** + * Pass 2 of [attemptAssertionRepair]: resolves each nested group's own outward requirement + * (as settled by [settleNestedGroups]) against this scope's own neighboring terms. + * + * @return [AssertionRepairResult.FAILURE] if resolving any of them failed outright; otherwise + * a successful result carrying whatever this scope itself must still propagate outward. + */ + private fun resolveNestedGroupRequirements(nestedGroupRequirements: List): AssertionRepairResult { + var pending = AssertionRepairResult.SUCCESS + for ((idx, requirement) in nestedGroupRequirements) { + requirement.neededPrefix?.let { requirement -> + pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesBefore(idx), backward = true)) + } + if (!pending.success) { + return AssertionRepairResult.FAILURE + } + requirement.neededPostfix?.let { requirement -> + pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesAfter(idx), backward = false)) + } + if (!pending.success) { + return AssertionRepairResult.FAILURE + } + } + return pending } /** - * Repairs every direct-term assertion in [terms]. + * Pass 3 of [attemptAssertionRepair]: repairs every direct-term assertion in [terms]. + * + * @return [AssertionRepairResult.FAILURE] if repairing any of them failed outright; otherwise + * a successful result carrying whatever this scope's own assertions still need propagated + * outward. */ private fun repairDirectAssertions(randomness: Randomness): AssertionRepairResult { var pending = AssertionRepairResult.SUCCESS @@ -284,7 +367,7 @@ class DisjunctionRxGene( /** * Handles an assertion with nothing local to force onto: escapes zero-width if the assertion - * itself allows it. + * itself allows it, otherwise samples once and escapes the whole candidate outward. */ private fun repairAssertionWithNoTarget(assertion: AssertionRxGene, backward: Boolean, randomness: Randomness): AssertionRepairResult { val innerGene = assertion.innerGene!! @@ -292,17 +375,21 @@ class DisjunctionRxGene( innerGene.forceZeroWidth() return AssertionRepairResult.SUCCESS } - return AssertionRepairResult.FAILURE + assertion.randomize(randomness, false) + val candidate = assertion.sampledInnerValue()!! + return AssertionRepairResult.stillNeeded(candidate, backward) } /** * Resamples [assertion] up to [MAX_LOCAL_ASSERTION_ATTEMPTS] times looking for a candidate - * [target] fully absorbs. + * [target] fully absorbs; if none does, escapes the last candidate tried outwards. This mirrors + * the same outcome [resolveOutwardRequirement] produces. */ private fun repairAssertionAgainstTarget(assertion: AssertionRxGene, target: List, backward: Boolean, randomness: Randomness): AssertionRepairResult { val countFunction = countWalkFunction(backward) val forceFunction = forceWalkFunction(backward) + var lastCandidate: String? = null for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue()!! @@ -313,20 +400,24 @@ class DisjunctionRxGene( forceFunction(target, candidate) return AssertionRepairResult.SUCCESS } + // Read-only for now, try to force full match before escaping partial match. + lastCandidate = candidate } - return AssertionRepairResult.FAILURE + + val candidate = lastCandidate ?: return AssertionRepairResult.FAILURE + return resolveOutwardRequirement(candidate, target, backward) } /** * The genes in [terms] lying before index [idx], excluding other assertions. This is the forcing - * target for a [AssertionType.LOOKBEHIND] assertion sitting at [idx]. + * target for a [AssertionType.LOOKBEHIND] assertion (or an outward requirement) sitting at [idx]. */ private fun genesBefore(idx: Int): List = terms.subList(0, idx).filter { it !is AssertionRxGene } /** * The genes in [terms] lying after index [idx], excluding other assertions. This is the forcing - * target for a [AssertionType.LOOKAHEAD] assertion sitting at [idx]. + * target for a [AssertionType.LOOKAHEAD] assertion (or an outward requirement) sitting at [idx]. */ private fun genesAfter(idx: Int): List = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } @@ -348,4 +439,48 @@ class DisjunctionRxGene( } else { AssertionRepairWalk::tryForce } -} + + /** + * Resolves a "" (empty) requirement: every gene in [target] must collapse to zero width. + */ + private fun resolveEmptyRequirement(target: List): AssertionRepairResult { + if (target.any { !(it as RxAbsorbable).canBeZeroWidth }) { + return AssertionRepairResult.FAILURE + } + target.forEach { (it as RxAbsorbable).forceZeroWidth() } + return AssertionRepairResult.SUCCESS + } + + /** + * Resolves an outward [requirement] against [target], a list of this scope's own genes lying to one + * side of wherever the requirement originated. [backward] selects which direction [target] is walked. + */ + private fun resolveOutwardRequirement(requirement: String, target: List, backward: Boolean): AssertionRepairResult { + if (requirement.isEmpty()) { + return resolveEmptyRequirement(target) + } + if (target.isEmpty()) { + return AssertionRepairResult.stillNeeded(requirement, backward) + } + + val countFunction = countWalkFunction(backward) + val outcome = countFunction(target, requirement) + if (outcome.hardMismatch) { + return AssertionRepairResult.FAILURE + } + + val forceFunction = forceWalkFunction(backward) + forceFunction(target, requirement) + + val consumed = outcome.consumed + return if (consumed == requirement.length) { + AssertionRepairResult.SUCCESS + } else { + val reminder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) + AssertionRepairResult.stillNeeded( + reminder, + backward + ) + } + } +} \ No newline at end of file From 52c00679cdd6b4b81888b98de33d7d6cd398b156 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:38:04 -0300 Subject: [PATCH 51/56] Allowed assertions nested inside groups in java visitor. --- .../org/evomaster/core/parser/GeneRegexJavaVisitor.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 32617e49e2..52f7d756cc 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -97,10 +97,17 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege private fun isAssertionNested(ctx: RegexJavaParser.AssertionContext): Boolean { var current = ctx.parent while (current != null && current !is RegexJavaParser.PatternContext) { - if (current is RegexJavaParser.AssertionContext - || (current is RegexJavaParser.AtomContext && current.disjunction() != null)) { + if (current is RegexJavaParser.AssertionContext) { + // assertion within assertion return true } + if (current is RegexJavaParser.AtomContext && current.disjunction() != null) { + val enclosingTerm = current.parent as? RegexJavaParser.TermContext + if (enclosingTerm?.quantifier() != null) { + // assertion within quantified group + return true + } + } current = current.parent } return false From 684d24a4944355537eed2b23d1398d3dec8f6517 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:40:15 -0300 Subject: [PATCH 52/56] Added some tests. --- .../core/parser/GeneRegexJavaVisitorTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index 19d7ba5216..14ad457648 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -508,4 +508,20 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?<=X)a") } assertThrows { checkSameAsJava("a(?<=[a&&b])a") } } + + @Test + fun testNestedAssertionInGroupLocallySatisfied() { + checkSameAsJava("^(a(?=bc)bc)d$") + checkSameAsJava("^(a(?<=a)b)c$") + checkSameAsJava("^((?x)(?=y)y)z$") + checkSameAsJava("^(a(?=ok)(o|k|a|y)*)$") + } + + @Test + fun testNestedAssertionOutwardEscape() { + checkSameAsJava("""^a((?=b\d)b)\d$""") + checkSameAsJava("""^\d(x(?<=\dx))y$""") + assertThrows { checkSameAsJava("""^a((?=b\d)b)y$""") } + assertThrows { checkSameAsJava("^(a(?=bc)d)e$") } + } } \ No newline at end of file From d67266d05e4ece4f3d5e74b81ce6c476986feea0 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 22:34:24 -0300 Subject: [PATCH 53/56] Removed redundant code. --- .../evomaster/core/search/gene/regex/DisjunctionListRxGene.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 5307762eb0..2828c7485e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -267,7 +267,6 @@ class DisjunctionListRxGene( if (bestCount > 0) { if (bestIndex != activeDisjunction) { - activeDisjunction = bestIndex tryToActivateGene(disjunctions[bestIndex]) } return force(disjunctions[bestIndex], value) @@ -322,7 +321,6 @@ class DisjunctionListRxGene( val target = order.first { disjunctions[it].canBeZeroWidth } disjunctions[target].forceZeroWidth() if (target != activeDisjunction) { - activeDisjunction = target tryToActivateGene(disjunctions[target]) } } From e62c4f5027d54f0e7ac471feb309813047f59f1c Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 18:07:59 -0300 Subject: [PATCH 54/56] Fixed typo. --- .../org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 4 ++-- .../evomaster/core/search/gene/utils/AssertionRepairWalk.kt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index d8c92e6184..c288df5856 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -476,9 +476,9 @@ class DisjunctionRxGene( return if (consumed == requirement.length) { AssertionRepairResult.SUCCESS } else { - val reminder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) + val remainder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) AssertionRepairResult.stillNeeded( - reminder, + remainder, backward ) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 2bf6c1a82e..2429e61496 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -10,7 +10,6 @@ import org.evomaster.core.search.gene.regex.RxAbsorbable * simply wrong here; `false` means the gene list just ran out of room with no mismatch, * so the leftover is still worth trying to escape outwards. */ - data class WalkOutcome(val consumed: Int, val hardMismatch: Boolean) /** From 060a02dc8a9a3d004c8caf5d7d25cc518cb2d45f Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 18:53:01 -0300 Subject: [PATCH 55/56] Added a note. --- .../org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index c288df5856..abd009f0dc 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -263,6 +263,9 @@ class DisjunctionRxGene( * neighboring terms. Runs as three passes, in order: [settleNestedGroups], * [resolveNestedGroupRequirements], [repairDirectAssertions]. * + * Note: forcing here is sequential and uncoordinated, so a later force can overwrite an + * earlier one. Still sound as the top-level pattern check catches any resulting mismatch. + * * @return whether repair succeeded, with possible outside requirements. */ fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { From 0c535b5c701e150c0c91e16f0c44c93f324139e4 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 9 Aug 2026 17:36:44 -0300 Subject: [PATCH 56/56] Moved AssertionRepairResult companion object to the top of the class file. --- .../gene/utils/AssertionRepairResult.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt index e79f68b26f..279ca34f4c 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -19,6 +19,22 @@ data class AssertionRepairResult( val neededPrefix: String? = null, val neededPostfix: String? = null ) { + companion object { + val SUCCESS = AssertionRepairResult(success = true) + val FAILURE = AssertionRepairResult(success = false) + + /** + * A successful result whose only outcome is [remainder] still being needed by whatever + * lies further out, depending on [backward]. + */ + fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = + if (backward) { + AssertionRepairResult(success = true, neededPrefix = remainder) + } else { + AssertionRepairResult(success = true, neededPostfix = remainder) + } + } + init { require(success || (neededPrefix == null && neededPostfix == null)) { "A failed AssertionRepairResult cannot carry an outward requirement" @@ -41,20 +57,4 @@ data class AssertionRepairResult( neededPostfix = next.neededPostfix ?: neededPostfix ) } - - companion object { - val SUCCESS = AssertionRepairResult(success = true) - val FAILURE = AssertionRepairResult(success = false) - - /** - * A successful result whose only outcome is [remainder] still being needed by whatever - * lies further out, depending on [backward]. - */ - fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = - if (backward) { - AssertionRepairResult(success = true, neededPrefix = remainder) - } else { - AssertionRepairResult(success = true, neededPostfix = remainder) - } - } } \ No newline at end of file