From 5b95bd61034989cf3c9e0e76556946bd2a13d630 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 22:34:24 -0300 Subject: [PATCH 01/10] Removed redundant code. --- .../evomaster/core/search/gene/regex/DisjunctionListRxGene.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 5307762eb0..2828c7485e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -267,7 +267,6 @@ class DisjunctionListRxGene( if (bestCount > 0) { if (bestIndex != activeDisjunction) { - activeDisjunction = bestIndex tryToActivateGene(disjunctions[bestIndex]) } return force(disjunctions[bestIndex], value) @@ -322,7 +321,6 @@ class DisjunctionListRxGene( val target = order.first { disjunctions[it].canBeZeroWidth } disjunctions[target].forceZeroWidth() if (target != activeDisjunction) { - activeDisjunction = target tryToActivateGene(disjunctions[target]) } } From a4564fc2f47a34152d25c7969dc780e301357faa Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 23:19:49 -0300 Subject: [PATCH 02/10] Introduced WalkOutcome as a walk result instead of Int. --- .../search/gene/regex/DisjunctionRxGene.kt | 10 ++--- .../search/gene/regex/QuantifierRxGene.kt | 8 ++-- .../search/gene/utils/AssertionRepairWalk.kt | 37 +++++++++++++------ 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 567b90d0a8..34f6ae048e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -196,7 +196,7 @@ class DisjunctionRxGene( * @see [AssertionRepairWalk.absorbableCount] */ override fun absorbableCount(value: String): Int = - AssertionRepairWalk.absorbableCount(terms, value) + AssertionRepairWalk.absorbableCount(terms, value).consumed /** * Delegates to a backward walk over [terms]. Mirrors [absorbableCount], walking @@ -204,7 +204,7 @@ class DisjunctionRxGene( * @see [RxAbsorbable.absorbableSuffixCount] */ override fun absorbableSuffixCount(value: String): Int = - AssertionRepairWalk.absorbableSuffixCount(terms, value) + AssertionRepairWalk.absorbableSuffixCount(terms, value).consumed /** * True only if every term can independently render "", as this disjunction's own value is @@ -221,7 +221,7 @@ class DisjunctionRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForce(terms, value) + return AssertionRepairWalk.tryForce(terms, value).consumed } /** @@ -231,7 +231,7 @@ class DisjunctionRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForceSuffix(terms, value) + return AssertionRepairWalk.tryForceSuffix(terms, value).consumed } /** @@ -285,7 +285,7 @@ class DisjunctionRxGene( for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() || countFunction(target, candidate) == candidate.length) { + if (candidate.isEmpty() || countFunction(target, candidate).consumed == candidate.length) { if (candidate.isNotEmpty()) { forceFunction(target, candidate) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index ebb010daa2..fd7ca0eef7 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -248,7 +248,7 @@ class QuantifierRxGene( * @see [AssertionRepairWalk.absorbableCount] */ override fun absorbableCount(value: String): Int = - AssertionRepairWalk.absorbableCount(atoms, value) + AssertionRepairWalk.absorbableCount(atoms, value).consumed /** * Delegates to a backward walk over [atoms], mirroring [absorbableCount] in the @@ -256,7 +256,7 @@ class QuantifierRxGene( * @see [RxAbsorbable.absorbableSuffixCount] */ override fun absorbableSuffixCount(value: String): Int = - AssertionRepairWalk.absorbableSuffixCount(atoms, value) + AssertionRepairWalk.absorbableSuffixCount(atoms, value).consumed /** * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". @@ -272,7 +272,7 @@ class QuantifierRxGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForce(atoms, value) + return AssertionRepairWalk.tryForce(atoms, value).consumed } /** @@ -281,7 +281,7 @@ class QuantifierRxGene( */ override fun tryForceSuffix(value: String): Int { require(value.isNotEmpty()) - return AssertionRepairWalk.tryForceSuffix(atoms, value) + return AssertionRepairWalk.tryForceSuffix(atoms, value).consumed } /** diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 5a1dbb3322..2bf6c1a82e 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -3,6 +3,16 @@ package org.evomaster.core.search.gene.utils import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.regex.RxAbsorbable +/** + * Outcome of one greedy walk over a gene list: [consumed] is how much of the candidate string + * can be placed. If less than the full string was consumed, [hardMismatch] says why. `true` means + * a gene genuinely refused to match ([consumed] resets to 0 in that case) and the candidate is + * simply wrong here; `false` means the gene list just ran out of room with no mismatch, + * so the leftover is still worth trying to escape outwards. + */ + +data class WalkOutcome(val consumed: Int, val hardMismatch: Boolean) + /** * Greedy-walk utilities for assertion repair. * @@ -16,6 +26,7 @@ object AssertionRepairWalk { * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] * - [reversed]: whether to walk [genes] right-to-left for lookbehind or left-to-right for lookahead + * @see WalkOutcome */ private fun walk( genes: List, @@ -23,9 +34,9 @@ object AssertionRepairWalk { absorb: (RxAbsorbable, String) -> Int, onZeroWidth: (RxAbsorbable) -> Unit, reversed: Boolean - ): Int { + ): WalkOutcome { if (value.isEmpty()) { - return 0 + return WalkOutcome(0, hardMismatch = false) } var consumed = 0 val walkTarget = if (reversed) { @@ -41,7 +52,7 @@ object AssertionRepairWalk { val remaining = if (reversed) { value.dropLast(consumed) } else { - value.substring(consumed) + value.drop(consumed) } val amount = absorb(absorbable, remaining) if (amount == 0) { @@ -49,18 +60,19 @@ object AssertionRepairWalk { onZeroWidth(absorbable) continue } - return 0 + return WalkOutcome(0, hardMismatch = true) } consumed += amount } - return consumed + return WalkOutcome(consumed, hardMismatch = false) } /** * Maximum leading characters of [value] that can be absorbed across [genes] * left-to-right, without mutating anything. + * @see WalkOutcome */ - fun absorbableCount(genes: List, value: String): Int = + fun absorbableCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.absorbableCount(value) }, onZeroWidth = {} @@ -68,9 +80,10 @@ object AssertionRepairWalk { /** * Forces as much of [value] as possible into [genes] left-to-right, mutating each - * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. + * gene in place using each gene's [RxAbsorbable.tryForce]. + * @see WalkOutcome */ - fun tryForce(genes: List, value: String): Int = + fun tryForce(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = false, absorb = { gene, value -> gene.tryForce(value) }, onZeroWidth = { it.forceZeroWidth() } @@ -81,8 +94,9 @@ object AssertionRepairWalk { * trailing characters of [value] that can be absorbed across [genes] right-to-left * (walking [genes] in reverse, since the gene closest to the assertion's position is the * last one in [genes]), without mutating anything. + * @see WalkOutcome */ - fun absorbableSuffixCount(genes: List, value: String): Int = + fun absorbableSuffixCount(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.absorbableSuffixCount(value) }, onZeroWidth = {} @@ -91,9 +105,10 @@ object AssertionRepairWalk { /** * Suffix-anchored counterpart of [tryForce], used by lookbehind repair: forces as much * of [value] as possible into [genes] right-to-left (mirroring [tryForce]), mutating each - * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. + * gene in place using [RxAbsorbable.tryForceSuffix]. + * @see WalkOutcome */ - fun tryForceSuffix(genes: List, value: String): Int = + fun tryForceSuffix(genes: List, value: String): WalkOutcome = walk(genes, value, reversed = true, absorb = { gene, value -> gene.tryForceSuffix(value) }, onZeroWidth = { it.forceZeroWidth() } From 09fecf3ecaff2543451e72029f3752e68ed9967a Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 2 Aug 2026 23:36:58 -0300 Subject: [PATCH 03/10] Added AssertionRepairResult as new return type for attemptAssertionRepair instead of Unit. --- .../gene/regex/DisjunctionListRxGene.kt | 11 ++++--- .../search/gene/regex/DisjunctionRxGene.kt | 10 +++--- .../gene/utils/AssertionRepairResult.kt | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 2828c7485e..257e19c8a5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -5,6 +5,7 @@ import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.interfaces.PhenotypeDormantGene +import org.evomaster.core.search.gene.utils.AssertionRepairResult import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionListRxGeneImpact import org.evomaster.core.search.service.AdaptiveParameterControl @@ -326,11 +327,11 @@ class DisjunctionListRxGene( } /** - * Delegates assertion repair to whichever branch is currently active, as only that - * branch's rendered value is ever observed, so only it needs repairing. See - * [DisjunctionRxGene.attemptAssertionRepair] for the actual repair logic. + * Delegates to whichever branch is currently active, as only that branch's rendered + * value is ever observed, so only it needs repairing. See [DisjunctionRxGene.attemptAssertionRepair] for + * the actual repair logic and what its return value means. */ - fun attemptAssertionRepair(randomness: Randomness) { - disjunctions.getOrNull(activeDisjunction)?.attemptAssertionRepair(randomness) + fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { + return disjunctions[activeDisjunction].attemptAssertionRepair(randomness) } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 34f6ae048e..a72731f978 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -4,6 +4,7 @@ import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.output.OutputFormat import org.evomaster.core.search.gene.root.CompositeFixedGene import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.utils.AssertionRepairResult import org.evomaster.core.search.gene.utils.AssertionRepairWalk import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.impact.impactinfocollection.regex.DisjunctionRxGeneImpact @@ -250,9 +251,9 @@ class DisjunctionRxGene( * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. */ - fun attemptAssertionRepair(randomness: Randomness) { + fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { if (terms.none { it is AssertionRxGene }) { - return + return AssertionRepairResult.SUCCESS } for (idx in terms.indices) { @@ -271,7 +272,7 @@ class DisjunctionRxGene( innerGene.forceZeroWidth() continue } - return + return AssertionRepairResult.FAILURE } val (countFunction, forceFunction) = @@ -294,8 +295,9 @@ class DisjunctionRxGene( } } if (!satisfied) { - return + return AssertionRepairResult.FAILURE } } + return AssertionRepairResult.SUCCESS } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt new file mode 100644 index 0000000000..70a1d3fe19 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -0,0 +1,32 @@ +package org.evomaster.core.search.gene.utils + +import org.evomaster.core.search.gene.regex.DisjunctionListRxGene +import org.evomaster.core.search.gene.regex.DisjunctionRxGene + +/** + * Result of [DisjunctionRxGene.attemptAssertionRepair] / [DisjunctionListRxGene.attemptAssertionRepair]. + * + * Assertion repair for one scope (a [DisjunctionRxGene]'s own direct terms) can be successful, + * fail or be conditionally successful, depending on either an external prefix/postfix (or both). + * + * [neededPrefix] is a requirement on whatever precedes this scope (from a lookbehind-direction); + * [neededPostfix] is a requirement on whatever follows it (lookahead-direction). Each is + * either `null` (no requirement in that direction), `""` (everything further out on that side must + * also collapse to zero width), or a non-empty literal that must be absorbed there. + */ +data class AssertionRepairResult( + val success: Boolean, + val neededPrefix: String? = null, + val neededPostfix: String? = null +) { + init { + require(success || (neededPrefix == null && neededPostfix == null)) { + "A failed AssertionRepairResult cannot carry an outward requirement" + } + } + + companion object { + val SUCCESS = AssertionRepairResult(success = true) + val FAILURE = AssertionRepairResult(success = false) + } +} \ No newline at end of file From e0ee3d98ebe0db50dee03d6545dcc8ec469882a6 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 12:56:47 -0300 Subject: [PATCH 04/10] Refactored DisjunctionRxGene.attemptAssertionRepair. --- .../search/gene/regex/DisjunctionRxGene.kt | 110 +++++++++++++----- .../gene/utils/AssertionRepairResult.kt | 28 +++++ 2 files changed, 107 insertions(+), 31 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index a72731f978..8e67de63d3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -256,48 +256,96 @@ class DisjunctionRxGene( return AssertionRepairResult.SUCCESS } + return repairDirectAssertions(randomness) + } + + /** + * Repairs every direct-term assertion in [terms]. + */ + private fun repairDirectAssertions(randomness: Randomness): AssertionRepairResult { + var pending = AssertionRepairResult.SUCCESS for (idx in terms.indices) { val assertion = terms[idx] as? AssertionRxGene ?: continue - val innerGene = assertion.innerGene ?: continue + val backward = assertion.assertionType == AssertionType.LOOKBEHIND + val target = if (backward) genesBefore(idx) else genesAfter(idx) - val target = if (assertion.assertionType == AssertionType.LOOKBEHIND) { - terms.subList(0, idx).filter { it !is AssertionRxGene } + val resolution = if (target.isEmpty()) { + repairAssertionWithNoTarget(assertion, backward, randomness) } else { - terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } + repairAssertionAgainstTarget(assertion, target, backward, randomness) } - - // we may not be able to force genes as target is empty but if the lookaround can be zero-width it is fine. - if (target.isEmpty()) { - if (innerGene.canBeZeroWidth) { - innerGene.forceZeroWidth() - continue - } + pending = pending.mergedWith(resolution) + if (!pending.success) { return AssertionRepairResult.FAILURE } + } + return pending + } - val (countFunction, forceFunction) = - if (assertion.assertionType == AssertionType.LOOKBEHIND) { - AssertionRepairWalk::absorbableSuffixCount to AssertionRepairWalk::tryForceSuffix - } else { - AssertionRepairWalk::absorbableCount to AssertionRepairWalk::tryForce - } + /** + * Handles an assertion with nothing local to force onto: escapes zero-width if the assertion + * itself allows it. + */ + private fun repairAssertionWithNoTarget(assertion: AssertionRxGene, backward: Boolean, randomness: Randomness): AssertionRepairResult { + val innerGene = assertion.innerGene!! + if (innerGene.canBeZeroWidth) { + innerGene.forceZeroWidth() + return AssertionRepairResult.SUCCESS + } + return AssertionRepairResult.FAILURE + } - var satisfied = false - for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { - assertion.randomize(randomness, false) - val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() || countFunction(target, candidate).consumed == candidate.length) { - if (candidate.isNotEmpty()) { - forceFunction(target, candidate) - } - satisfied = true - break - } + /** + * Resamples [assertion] up to [MAX_LOCAL_ASSERTION_ATTEMPTS] times looking for a candidate + * [target] fully absorbs. + */ + private fun repairAssertionAgainstTarget(assertion: AssertionRxGene, target: List, backward: Boolean, randomness: Randomness): AssertionRepairResult { + val countFunction = countWalkFunction(backward) + val forceFunction = forceWalkFunction(backward) + + for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { + assertion.randomize(randomness, false) + val candidate = assertion.sampledInnerValue()!! + if (candidate.isEmpty()) { + return AssertionRepairResult.SUCCESS } - if (!satisfied) { - return AssertionRepairResult.FAILURE + if (countFunction(target, candidate).consumed == candidate.length) { + forceFunction(target, candidate) + return AssertionRepairResult.SUCCESS } } - return AssertionRepairResult.SUCCESS + return AssertionRepairResult.FAILURE + } + + /** + * The genes in [terms] lying before index [idx], excluding other assertions. This is the forcing + * target for a [AssertionType.LOOKBEHIND] assertion sitting at [idx]. + */ + private fun genesBefore(idx: Int): List = + terms.subList(0, idx).filter { it !is AssertionRxGene } + + /** + * The genes in [terms] lying after index [idx], excluding other assertions. This is the forcing + * target for a [AssertionType.LOOKAHEAD] assertion sitting at [idx]. + */ + private fun genesAfter(idx: Int): List = + terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } + + /** + * The read-only walk function for [backward]. + */ + private fun countWalkFunction(backward: Boolean) = if (backward) { + AssertionRepairWalk::absorbableSuffixCount + } else { + AssertionRepairWalk::absorbableCount + } + + /** + * The mutating counterpart of [countWalkFunction], for the same [backward] direction. + */ + private fun forceWalkFunction(backward: Boolean) = if (backward) { + AssertionRepairWalk::tryForceSuffix + } else { + AssertionRepairWalk::tryForce } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt index 70a1d3fe19..e79f68b26f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -25,8 +25,36 @@ data class AssertionRepairResult( } } + /** + * Combines this result with [next], the outcome of resolving one more requirement in the + * same scope. Failure on either side wins outright, otherwise [next]'s own + * [neededPrefix]/[neededPostfix] each take priority over this result's, falling back to + * this result's own value when [next] did not set one of them. + */ + fun mergedWith(next: AssertionRepairResult): AssertionRepairResult { + if (!success || !next.success) { + return FAILURE + } + return AssertionRepairResult( + success = true, + neededPrefix = next.neededPrefix ?: neededPrefix, + neededPostfix = next.neededPostfix ?: neededPostfix + ) + } + companion object { val SUCCESS = AssertionRepairResult(success = true) val FAILURE = AssertionRepairResult(success = false) + + /** + * A successful result whose only outcome is [remainder] still being needed by whatever + * lies further out, depending on [backward]. + */ + fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = + if (backward) { + AssertionRepairResult(success = true, neededPrefix = remainder) + } else { + AssertionRepairResult(success = true, neededPostfix = remainder) + } } } \ No newline at end of file From cf70fed0d7b1efd1410f1b5be6d19fc0b274b3c8 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:33:51 -0300 Subject: [PATCH 05/10] Added multiple passes to DisjunctionRxGene.attemptAssertionRepair to handle nested groups' outward requirements. --- .../search/gene/regex/DisjunctionRxGene.kt | 157 ++++++++++++++++-- 1 file changed, 146 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 8e67de63d3..d8c92e6184 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -22,6 +22,13 @@ import org.slf4j.LoggerFactory */ const val MAX_LOCAL_ASSERTION_ATTEMPTS = 20 +/** + * One nested group's own unresolved requirement, as settled by [DisjunctionRxGene.settleNestedGroups] + * and resolved by [DisjunctionRxGene.resolveNestedGroupRequirements]. [termIndex] is that group's + * own index in [DisjunctionRxGene.terms]. + */ +private data class NestedGroupRequirement(val termIndex: Int, val result: AssertionRepairResult) + class DisjunctionRxGene( name: String, val terms: List, @@ -249,18 +256,94 @@ class DisjunctionRxGene( * [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner * value onto the genes on the appropriate side of it within [terms]: * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] - * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. + * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND] + * + * Also, recurses into any direct term that is itself a nested group repairing + * it first, and resolving whatever it couldn't satisfy locally against this scope's own + * neighboring terms. Runs as three passes, in order: [settleNestedGroups], + * [resolveNestedGroupRequirements], [repairDirectAssertions]. + * + * @return whether repair succeeded, with possible outside requirements. */ fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { - if (terms.none { it is AssertionRxGene }) { + if (terms.none { it is AssertionRxGene || it is DisjunctionListRxGene }) { return AssertionRepairResult.SUCCESS } - return repairDirectAssertions(randomness) + val nestedGroupRequirements = settleNestedGroups(randomness) + ?: return AssertionRepairResult.FAILURE + + val nestedResult = resolveNestedGroupRequirements(nestedGroupRequirements) + if (!nestedResult.success) { + return AssertionRepairResult.FAILURE + } + + val directResult = repairDirectAssertions(randomness) + if (!directResult.success) { + return AssertionRepairResult.FAILURE + } + + return AssertionRepairResult( + success = true, + neededPrefix = directResult.neededPrefix ?: nestedResult.neededPrefix, + neededPostfix = directResult.neededPostfix ?: nestedResult.neededPostfix + ) + } + + /** + * Pass 1 of [attemptAssertionRepair]: settles every nested group's own internal repair, + * left to right, before anything in this scope uses it as a forcing target. + * + * @return the outward requirements for each nested group's own term index, in ascending index order + * (left to right, matching [terms] itself). `null` if any nested group's own repair failed outright. + */ + private fun settleNestedGroups(randomness: Randomness): List? { + val nestedGroupRequirements = mutableListOf() + for (idx in terms.indices) { + val term = terms[idx] as? DisjunctionListRxGene ?: continue + val result = term.attemptAssertionRepair(randomness) + if (!result.success) { + return null + } + if (result.neededPrefix != null || result.neededPostfix != null) { + nestedGroupRequirements.add(NestedGroupRequirement(idx, result)) + } + } + return nestedGroupRequirements + } + + /** + * Pass 2 of [attemptAssertionRepair]: resolves each nested group's own outward requirement + * (as settled by [settleNestedGroups]) against this scope's own neighboring terms. + * + * @return [AssertionRepairResult.FAILURE] if resolving any of them failed outright; otherwise + * a successful result carrying whatever this scope itself must still propagate outward. + */ + private fun resolveNestedGroupRequirements(nestedGroupRequirements: List): AssertionRepairResult { + var pending = AssertionRepairResult.SUCCESS + for ((idx, requirement) in nestedGroupRequirements) { + requirement.neededPrefix?.let { requirement -> + pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesBefore(idx), backward = true)) + } + if (!pending.success) { + return AssertionRepairResult.FAILURE + } + requirement.neededPostfix?.let { requirement -> + pending = pending.mergedWith(resolveOutwardRequirement(requirement, genesAfter(idx), backward = false)) + } + if (!pending.success) { + return AssertionRepairResult.FAILURE + } + } + return pending } /** - * Repairs every direct-term assertion in [terms]. + * Pass 3 of [attemptAssertionRepair]: repairs every direct-term assertion in [terms]. + * + * @return [AssertionRepairResult.FAILURE] if repairing any of them failed outright; otherwise + * a successful result carrying whatever this scope's own assertions still need propagated + * outward. */ private fun repairDirectAssertions(randomness: Randomness): AssertionRepairResult { var pending = AssertionRepairResult.SUCCESS @@ -284,7 +367,7 @@ class DisjunctionRxGene( /** * Handles an assertion with nothing local to force onto: escapes zero-width if the assertion - * itself allows it. + * itself allows it, otherwise samples once and escapes the whole candidate outward. */ private fun repairAssertionWithNoTarget(assertion: AssertionRxGene, backward: Boolean, randomness: Randomness): AssertionRepairResult { val innerGene = assertion.innerGene!! @@ -292,17 +375,21 @@ class DisjunctionRxGene( innerGene.forceZeroWidth() return AssertionRepairResult.SUCCESS } - return AssertionRepairResult.FAILURE + assertion.randomize(randomness, false) + val candidate = assertion.sampledInnerValue()!! + return AssertionRepairResult.stillNeeded(candidate, backward) } /** * Resamples [assertion] up to [MAX_LOCAL_ASSERTION_ATTEMPTS] times looking for a candidate - * [target] fully absorbs. + * [target] fully absorbs; if none does, escapes the last candidate tried outwards. This mirrors + * the same outcome [resolveOutwardRequirement] produces. */ private fun repairAssertionAgainstTarget(assertion: AssertionRxGene, target: List, backward: Boolean, randomness: Randomness): AssertionRepairResult { val countFunction = countWalkFunction(backward) val forceFunction = forceWalkFunction(backward) + var lastCandidate: String? = null for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue()!! @@ -313,20 +400,24 @@ class DisjunctionRxGene( forceFunction(target, candidate) return AssertionRepairResult.SUCCESS } + // Read-only for now, try to force full match before escaping partial match. + lastCandidate = candidate } - return AssertionRepairResult.FAILURE + + val candidate = lastCandidate ?: return AssertionRepairResult.FAILURE + return resolveOutwardRequirement(candidate, target, backward) } /** * The genes in [terms] lying before index [idx], excluding other assertions. This is the forcing - * target for a [AssertionType.LOOKBEHIND] assertion sitting at [idx]. + * target for a [AssertionType.LOOKBEHIND] assertion (or an outward requirement) sitting at [idx]. */ private fun genesBefore(idx: Int): List = terms.subList(0, idx).filter { it !is AssertionRxGene } /** * The genes in [terms] lying after index [idx], excluding other assertions. This is the forcing - * target for a [AssertionType.LOOKAHEAD] assertion sitting at [idx]. + * target for a [AssertionType.LOOKAHEAD] assertion (or an outward requirement) sitting at [idx]. */ private fun genesAfter(idx: Int): List = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } @@ -348,4 +439,48 @@ class DisjunctionRxGene( } else { AssertionRepairWalk::tryForce } -} + + /** + * Resolves a "" (empty) requirement: every gene in [target] must collapse to zero width. + */ + private fun resolveEmptyRequirement(target: List): AssertionRepairResult { + if (target.any { !(it as RxAbsorbable).canBeZeroWidth }) { + return AssertionRepairResult.FAILURE + } + target.forEach { (it as RxAbsorbable).forceZeroWidth() } + return AssertionRepairResult.SUCCESS + } + + /** + * Resolves an outward [requirement] against [target], a list of this scope's own genes lying to one + * side of wherever the requirement originated. [backward] selects which direction [target] is walked. + */ + private fun resolveOutwardRequirement(requirement: String, target: List, backward: Boolean): AssertionRepairResult { + if (requirement.isEmpty()) { + return resolveEmptyRequirement(target) + } + if (target.isEmpty()) { + return AssertionRepairResult.stillNeeded(requirement, backward) + } + + val countFunction = countWalkFunction(backward) + val outcome = countFunction(target, requirement) + if (outcome.hardMismatch) { + return AssertionRepairResult.FAILURE + } + + val forceFunction = forceWalkFunction(backward) + forceFunction(target, requirement) + + val consumed = outcome.consumed + return if (consumed == requirement.length) { + AssertionRepairResult.SUCCESS + } else { + val reminder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) + AssertionRepairResult.stillNeeded( + reminder, + backward + ) + } + } +} \ No newline at end of file From 52c00679cdd6b4b81888b98de33d7d6cd398b156 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:38:04 -0300 Subject: [PATCH 06/10] Allowed assertions nested inside groups in java visitor. --- .../org/evomaster/core/parser/GeneRegexJavaVisitor.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 32617e49e2..52f7d756cc 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -97,10 +97,17 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege private fun isAssertionNested(ctx: RegexJavaParser.AssertionContext): Boolean { var current = ctx.parent while (current != null && current !is RegexJavaParser.PatternContext) { - if (current is RegexJavaParser.AssertionContext - || (current is RegexJavaParser.AtomContext && current.disjunction() != null)) { + if (current is RegexJavaParser.AssertionContext) { + // assertion within assertion return true } + if (current is RegexJavaParser.AtomContext && current.disjunction() != null) { + val enclosingTerm = current.parent as? RegexJavaParser.TermContext + if (enclosingTerm?.quantifier() != null) { + // assertion within quantified group + return true + } + } current = current.parent } return false From 684d24a4944355537eed2b23d1398d3dec8f6517 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 13:40:15 -0300 Subject: [PATCH 07/10] Added some tests. --- .../core/parser/GeneRegexJavaVisitorTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index 19d7ba5216..14ad457648 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -508,4 +508,20 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?<=X)a") } assertThrows { checkSameAsJava("a(?<=[a&&b])a") } } + + @Test + fun testNestedAssertionInGroupLocallySatisfied() { + checkSameAsJava("^(a(?=bc)bc)d$") + checkSameAsJava("^(a(?<=a)b)c$") + checkSameAsJava("^((?x)(?=y)y)z$") + checkSameAsJava("^(a(?=ok)(o|k|a|y)*)$") + } + + @Test + fun testNestedAssertionOutwardEscape() { + checkSameAsJava("""^a((?=b\d)b)\d$""") + checkSameAsJava("""^\d(x(?<=\dx))y$""") + assertThrows { checkSameAsJava("""^a((?=b\d)b)y$""") } + assertThrows { checkSameAsJava("^(a(?=bc)d)e$") } + } } \ No newline at end of file From e62c4f5027d54f0e7ac471feb309813047f59f1c Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 18:07:59 -0300 Subject: [PATCH 08/10] Fixed typo. --- .../org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 4 ++-- .../evomaster/core/search/gene/utils/AssertionRepairWalk.kt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index d8c92e6184..c288df5856 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -476,9 +476,9 @@ class DisjunctionRxGene( return if (consumed == requirement.length) { AssertionRepairResult.SUCCESS } else { - val reminder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) + val remainder = if (backward) requirement.dropLast(consumed) else requirement.drop(consumed) AssertionRepairResult.stillNeeded( - reminder, + remainder, backward ) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 2bf6c1a82e..2429e61496 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -10,7 +10,6 @@ import org.evomaster.core.search.gene.regex.RxAbsorbable * simply wrong here; `false` means the gene list just ran out of room with no mismatch, * so the leftover is still worth trying to escape outwards. */ - data class WalkOutcome(val consumed: Int, val hardMismatch: Boolean) /** From 060a02dc8a9a3d004c8caf5d7d25cc518cb2d45f Mon Sep 17 00:00:00 2001 From: lmasroca Date: Mon, 3 Aug 2026 18:53:01 -0300 Subject: [PATCH 09/10] Added a note. --- .../org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index c288df5856..abd009f0dc 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -263,6 +263,9 @@ class DisjunctionRxGene( * neighboring terms. Runs as three passes, in order: [settleNestedGroups], * [resolveNestedGroupRequirements], [repairDirectAssertions]. * + * Note: forcing here is sequential and uncoordinated, so a later force can overwrite an + * earlier one. Still sound as the top-level pattern check catches any resulting mismatch. + * * @return whether repair succeeded, with possible outside requirements. */ fun attemptAssertionRepair(randomness: Randomness): AssertionRepairResult { From 0c535b5c701e150c0c91e16f0c44c93f324139e4 Mon Sep 17 00:00:00 2001 From: lmasroca Date: Sun, 9 Aug 2026 17:36:44 -0300 Subject: [PATCH 10/10] Moved AssertionRepairResult companion object to the top of the class file. --- .../gene/utils/AssertionRepairResult.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt index e79f68b26f..279ca34f4c 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairResult.kt @@ -19,6 +19,22 @@ data class AssertionRepairResult( val neededPrefix: String? = null, val neededPostfix: String? = null ) { + companion object { + val SUCCESS = AssertionRepairResult(success = true) + val FAILURE = AssertionRepairResult(success = false) + + /** + * A successful result whose only outcome is [remainder] still being needed by whatever + * lies further out, depending on [backward]. + */ + fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = + if (backward) { + AssertionRepairResult(success = true, neededPrefix = remainder) + } else { + AssertionRepairResult(success = true, neededPostfix = remainder) + } + } + init { require(success || (neededPrefix == null && neededPostfix == null)) { "A failed AssertionRepairResult cannot carry an outward requirement" @@ -41,20 +57,4 @@ data class AssertionRepairResult( neededPostfix = next.neededPostfix ?: neededPostfix ) } - - companion object { - val SUCCESS = AssertionRepairResult(success = true) - val FAILURE = AssertionRepairResult(success = false) - - /** - * A successful result whose only outcome is [remainder] still being needed by whatever - * lies further out, depending on [backward]. - */ - fun stillNeeded(remainder: String, backward: Boolean): AssertionRepairResult = - if (backward) { - AssertionRepairResult(success = true, neededPrefix = remainder) - } else { - AssertionRepairResult(success = true, neededPostfix = remainder) - } - } } \ No newline at end of file