From c0c09aa78a508962b6a244e0d715653c2c1ff78e Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 13 Jul 2026 01:27:54 -0300 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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(