diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 index 5753d6a4fa..e175228b26 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaLexer.g4 @@ -77,6 +77,7 @@ CharacterClassEscape ; EQUAL : '='; +LESS_THAN : '<'; CARET : '^'; DOLLAR : '$'; SLASH : '\\'; @@ -97,7 +98,7 @@ COLON : ':'; BaseChar // practically all chars but the ones used for control and digits - : ~[0-9:,^$\\.*+?()[\]{}|=-] + : ~[0-9:,^$\\.<*+?()[\]{}|=-] ; fragment OctalEscapeSequence diff --git a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 index 32a933b7c9..27f14c38c0 100644 --- a/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 +++ b/core/src/main/antlr4/org/evomaster/core/parser/RegexJavaParser.g4 @@ -52,7 +52,8 @@ assertion //TODO //// | '\\' 'b' //// | '\\' 'B' - | PAREN_open QUESTION EQUAL disjunction PAREN_close + | PAREN_open QUESTION EQUAL disjunction PAREN_close // lookahead (?=...) + | PAREN_open QUESTION LESS_THAN EQUAL disjunction PAREN_close // lookbehind (?<=...) //// | '(' '?' '!' disjunction ')' ; @@ -116,7 +117,7 @@ patternCharacter // These are also allowed as literals when no matching pair exists | BRACE_close | BRACKET_close - | COLON | EQUAL + | COLON | EQUAL | LESS_THAN | DOUBLE_AMPERSAND // char class intersection not supported by default in JS, only supported if "v" flag is turned on. ; @@ -168,7 +169,7 @@ classAtomNoDash | DecimalDigit | COMMA | CARET | DOLLAR | DOT | STAR | PLUS | QUESTION | PAREN_open | PAREN_close | BRACKET_open | BRACE_open | BRACE_close | OR - | COLON | EQUAL + | COLON | EQUAL | LESS_THAN // should be interpreted literally: // As they are lexer tokens, these character sequences are captured as such. In particular these require some extra // steps to interpret them correctly given the context. diff --git a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt index 49c6f80040..32617e49e2 100644 --- a/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt +++ b/core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt @@ -260,7 +260,12 @@ class GeneRegexJavaVisitor(val sourceRegex: String, val externalRegexFlags: Rege "Nested assertions are not currently supported." } val innerDisjList = buildDisjunctionList(assertionCtx.disjunction()) - val assertionGene = AssertionRxGene(innerDisjList) + val assertionType = if (assertionCtx.LESS_THAN() != null) { + AssertionType.LOOKBEHIND + } else { + AssertionType.LOOKAHEAD + } + val assertionGene = AssertionRxGene(innerDisjList, assertionType) hasAssertions = true res.genes.add(assertionGene) } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt index 29b74793cb..3082b511b5 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/AssertionRxGene.kt @@ -10,6 +10,11 @@ import org.evomaster.core.search.service.mutator.MutationWeightControl import org.evomaster.core.search.service.mutator.genemutation.AdditionalGeneMutationInfo import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutationSelectionStrategy +/** + * Distinguishes which direction an [AssertionRxGene] forces a candidate during repair. + */ +enum class AssertionType { LOOKAHEAD, LOOKBEHIND } + /** * Represents a zero-width assertion in the regex gene tree. * @@ -19,15 +24,18 @@ import org.evomaster.core.search.service.mutator.genemutation.SubsetGeneMutation * * Repair is triggered from [DisjunctionRxGene.attemptAssertionRepair], invoked by * [RegexGene.randomize] after the disjunction's own sampled value is checked against - * the source pattern and found not to match. + * the source pattern and found not to match. [assertionType] determines which side of + * the enclosing disjunction's terms the candidate gets forced onto. This gene's own + * methods stay direction-agnostic. */ class AssertionRxGene( /** * The assertion's inner disjunction gene, can be null as the disjunction can be unsatisfiable, * in that case [innerGene] is null. */ - val innerGene: DisjunctionListRxGene? -) : RxTerm, CompositeFixedGene("assertion", listOfNotNull(innerGene)) { + val innerGene: DisjunctionListRxGene?, + val assertionType: AssertionType +) : RxTerm, CompositeFixedGene("assertion:${assertionType.name}", listOfNotNull(innerGene)) { /** * To handle null [innerGene], in which case the assertion is unsatisfiable. @@ -41,7 +49,7 @@ class AssertionRxGene( override fun isMutable(): Boolean = innerGene?.isMutable() ?: false override fun copyContent(): Gene { - val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene) + val copy = AssertionRxGene(innerGene?.copy() as? DisjunctionListRxGene, assertionType) copy.name = this.name return copy } @@ -95,11 +103,14 @@ class AssertionRxGene( if (other !is AssertionRxGene) { return false } + if (assertionType != other.assertionType) { + return false + } return sampledInnerValue() == other.sampledInnerValue() } override fun unsafeCopyValueFrom(other: Gene): Boolean { - if (other !is AssertionRxGene) { + if (other !is AssertionRxGene || assertionType != other.assertionType) { return false } return if (innerGene != null && other.innerGene != null) { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt index 5fe86980a2..5307762eb0 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionListRxGene.kt @@ -210,13 +210,17 @@ class DisjunctionListRxGene( /** * Ranks all branches by how much of [value] they can absorb, without mutating - * anything. + * anything. Shared by both directions: [absorb] is [DisjunctionRxGene.absorbableCount] + * for lookahead's ranking, [DisjunctionRxGene.absorbableSuffixCount] for lookbehind's. */ - private fun rankBranches(value: String): BranchRanking? { + private fun rankBranches( + value: String, + absorb: (DisjunctionRxGene, String) -> Int + ): BranchRanking? { if (value.isEmpty() || disjunctions.isEmpty()) { return null } - var bestCount = disjunctions[activeDisjunction].absorbableCount(value) + var bestCount = absorb(disjunctions[activeDisjunction], value) var bestIndex = activeDisjunction for (i in disjunctions.indices) { if (i == activeDisjunction) { @@ -225,7 +229,7 @@ class DisjunctionListRxGene( if (bestCount == value.length) { break } - val can = disjunctions[i].absorbableCount(value) + val can = absorb(disjunctions[i], value) if (can > bestCount) { bestCount = can bestIndex = i @@ -241,7 +245,7 @@ class DisjunctionListRxGene( * @see [rankBranches] */ override fun absorbableCount(value: String): Int = - rankBranches(value)?.absorbableCount ?: 0 + rankBranches(value){ disjunction, value -> disjunction.absorbableCount(value) }?.absorbableCount ?: 0 /** * True if at least one branch can render "", as we can select that branch and force it. @@ -250,26 +254,62 @@ class DisjunctionListRxGene( override val canBeZeroWidth: Boolean = disjunctions.any { it.canBeZeroWidth } /** - * Activates whichever branch can best absorb [value] (switching [activeDisjunction] if - * needed) and forces it there. - * @see [RxAbsorbable.tryForce] - * @see [rankBranches] + * Shared rank-then-force logic behind [tryForce]/[tryForceSuffix]: ranks all branches via + * [absorb], switches [activeDisjunction] to the winner if needed, then delegates to [force] + * on that branch. */ - override fun tryForce(value: String): Int { - require(value.isNotEmpty()) - val (bestCount, bestIndex) = rankBranches(value) ?: BranchRanking(0, activeDisjunction) + private fun forceBestBranch( + value: String, + absorb: (DisjunctionRxGene, String) -> Int, + force: (DisjunctionRxGene, String) -> Int + ): Int { + val (bestCount, bestIndex) = rankBranches(value, absorb) ?: BranchRanking(0, activeDisjunction) if (bestCount > 0) { if (bestIndex != activeDisjunction) { activeDisjunction = bestIndex tryToActivateGene(disjunctions[bestIndex]) } - return disjunctions[bestIndex].tryForce(value) + return force(disjunctions[bestIndex], value) } - return 0 } + /** + * Activates whichever branch can best absorb [value] (switching [activeDisjunction] if + * needed) and forces it there. + * @see [RxAbsorbable.tryForce] + * @see [rankBranches] + */ + override fun tryForce(value: String): Int { + require(value.isNotEmpty()) + return forceBestBranch(value, + absorb = { d, v -> d.absorbableCount(v) }, + force = { d, v -> d.tryForce(v) } + ) + } + + /** + * Suffix counterpart of [absorbableCount]: ranks every branch by how much of + * [value]'s trailing characters it could absorb, without mutating. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + rankBranches(value) { d, v -> d.absorbableSuffixCount(v) }?.absorbableCount ?: 0 + + /** + * Suffix counterpart of [tryForce]: activates whichever branch can best absorb + * [value]'s trailing characters and forces it there, walking right-to-left. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + return forceBestBranch(value, + absorb = { d, v -> d.absorbableSuffixCount(v) }, + force = { d, v -> d.tryForceSuffix(v) } + ) + } + /** * Forces the active branch to zero width if it can; otherwise switches to the first * branch that can and forces that one instead. diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt index 2afec87505..567b90d0a8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/DisjunctionRxGene.kt @@ -198,6 +198,14 @@ class DisjunctionRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(terms, value) + /** + * Delegates to a backward walk over [terms]. Mirrors [absorbableCount], walking + * right-to-left since lookbehind's target sits before the assertion. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + AssertionRepairWalk.absorbableSuffixCount(terms, value) + /** * True only if every term can independently render "", as this disjunction's own value is * the concatenation of all of them. @@ -216,6 +224,16 @@ class DisjunctionRxGene( return AssertionRepairWalk.tryForce(terms, value) } + /** + * Delegates to a backward walk over [terms], mirroring [tryForce] in the opposite + * direction. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + return AssertionRepairWalk.tryForceSuffix(terms, value) + } + /** * Forces every term to zero width individually. * @see [RxAbsorbable.forceZeroWidth] @@ -228,7 +246,9 @@ class DisjunctionRxGene( /** * Attempts to repair this disjunction's own value so that each of its direct-term * [AssertionRxGene]s is actually satisfied, by forcing the assertion's sampled inner - * value onto the genes that follow it within [terms]. + * value onto the genes on the appropriate side of it within [terms]: + * - Forward, onto [terms] after it, for [AssertionType.LOOKAHEAD] + * - Backward, onto [terms] before it, for [AssertionType.LOOKBEHIND]. */ fun attemptAssertionRepair(randomness: Randomness) { if (terms.none { it is AssertionRxGene }) { @@ -237,23 +257,37 @@ class DisjunctionRxGene( for (idx in terms.indices) { val assertion = terms[idx] as? AssertionRxGene ?: continue - if (assertion.innerGene == null) { - continue + val innerGene = assertion.innerGene ?: continue + + val target = if (assertion.assertionType == AssertionType.LOOKBEHIND) { + terms.subList(0, idx).filter { it !is AssertionRxGene } + } else { + terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } } - val genesAfter = terms.subList(idx + 1, terms.size).filter { it !is AssertionRxGene } - if (genesAfter.isEmpty()) { + // we may not be able to force genes as target is empty but if the lookaround can be zero-width it is fine. + if (target.isEmpty()) { + if (innerGene.canBeZeroWidth) { + innerGene.forceZeroWidth() + continue + } return } + val (countFunction, forceFunction) = + if (assertion.assertionType == AssertionType.LOOKBEHIND) { + AssertionRepairWalk::absorbableSuffixCount to AssertionRepairWalk::tryForceSuffix + } else { + AssertionRepairWalk::absorbableCount to AssertionRepairWalk::tryForce + } + var satisfied = false for (attempt in 0 until MAX_LOCAL_ASSERTION_ATTEMPTS) { assertion.randomize(randomness, false) val candidate = assertion.sampledInnerValue() ?: break - if (candidate.isEmpty() - || AssertionRepairWalk.absorbableCount(genesAfter, candidate) == candidate.length) { + if (candidate.isEmpty() || countFunction(target, candidate) == candidate.length) { if (candidate.isNotEmpty()) { - AssertionRepairWalk.tryForce(genesAfter, candidate) + forceFunction(target, candidate) } satisfied = true break diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt index 8eea221606..a2d78499b4 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/PatternCharacterBlockGene.kt @@ -116,19 +116,30 @@ class PatternCharacterBlockGene( } /** - * How many of [stringBlock]'s leading characters match [value]'s leading characters, - * case-insensitively wherever [flags] allows it. 0 when a character does not match, - * one of the strings must be consumed completely. - * @see [RxAbsorbable.absorbableCount] + * Maps a forward walk-position [index] to the real index into [value], depending on [reversed]. */ - override fun absorbableCount(value: String): Int { + private fun realIndex(reversed: Boolean, value: String, index: Int): Int = + if (reversed) { + value.lastIndex - index + } else { + index + } + + /** + * Shared body behind [absorbableCount]/[absorbableSuffixCount], they differ only in which + * end of [stringBlock] and [value] the walk anchors to, via [reversed]. No partial matches, + * returns 0 or the shortest string's length (which was matched). + */ + private fun matchCount(value: String, reversed: Boolean): Int { var i = 0 while (i < value.length && i < stringBlock.length) { - val c = stringBlock[i] + val blockIdx = realIndex(reversed, stringBlock, i) + val valueIdx = realIndex(reversed, value, i) + val c = stringBlock[blockIdx] val matches = if (flags.isCaseable(c)) { - value[i].equals(c, ignoreCase = true) + value[valueIdx].equals(c, ignoreCase = true) } else { - value[i] == c + value[valueIdx] == c } if (!matches) { return 0 @@ -138,6 +149,29 @@ class PatternCharacterBlockGene( return i } + /** + * Shared body behind [tryForce]/[tryForceSuffix]: commits the case (upper/lower) of the + * first (or last if [reversed]) [matchedLength] characters of [stringBlock] to match + * [value]'s corresponding characters. + */ + private fun applyForce(value: String, matchedLength: Int, reversed: Boolean) { + for (i in 0 until matchedLength) { + val blockIdx = realIndex(reversed, stringBlock, i) + val valueIdx = realIndex(reversed, value, i) + if (flags.isCaseable(stringBlock[blockIdx])) { + caseChoices[blockIdx] = value[valueIdx].isUpperCase() + } + } + } + + /** + * How many of [stringBlock]'s leading characters match [value]'s leading characters, + * case-insensitively wherever [flags] allows it. 0 when a character does not match, + * one of the strings must be consumed completely. + * @see [RxAbsorbable.absorbableCount] + */ + override fun absorbableCount(value: String): Int = matchCount(value, reversed = false) + /** * True only when [stringBlock] is empty, as a non-empty literal can never render "". * @see [RxAbsorbable.canBeZeroWidth] @@ -151,13 +185,9 @@ class PatternCharacterBlockGene( */ override fun tryForce(value: String): Int { require(value.isNotEmpty()) - val n = absorbableCount(value) - require(n == stringBlock.length || n == value.length || n==0) - for (i in 0 until n) { - if (flags.isCaseable(stringBlock[i])) { - caseChoices[i] = value[i].isUpperCase() - } - } + val n = matchCount(value, reversed = false) + require(n == stringBlock.length || n == value.length || n == 0) + applyForce(value, matchedLength = n, reversed = false) return n } @@ -168,4 +198,25 @@ class PatternCharacterBlockGene( override fun forceZeroWidth() { require(canBeZeroWidth) } + + /** + * Suffix-anchored mirror of [absorbableCount]: how many of [stringBlock]'s trailing characters + * match [value]'s trailing characters, case-insensitively wherever [flags] allows it. + * 0 when a character does not match, one of the strings must be consumed completely. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = matchCount(value, reversed = true) + + /** + * Suffix-anchored mirror of [tryForce]: Commits the matching trailing characters' case to + * match [value]; mirrors [absorbableSuffixCount] exactly. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + val n = matchCount(value, reversed = true) + require(n == stringBlock.length || n == value.length || n == 0) + applyForce(value, matchedLength = n, reversed = true) + return n + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt index 61eb5dc8d7..ebb010daa2 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/QuantifierRxGene.kt @@ -250,6 +250,14 @@ class QuantifierRxGene( override fun absorbableCount(value: String): Int = AssertionRepairWalk.absorbableCount(atoms, value) + /** + * Delegates to a backward walk over [atoms], mirroring [absorbableCount] in the + * opposite direction. + * @see [RxAbsorbable.absorbableSuffixCount] + */ + override fun absorbableSuffixCount(value: String): Int = + AssertionRepairWalk.absorbableSuffixCount(atoms, value) + /** * True if zero repetitions are allowed ([min] == 0), or if [template] can itself render "". * @see [RxAbsorbable.canBeZeroWidth] @@ -267,6 +275,15 @@ class QuantifierRxGene( return AssertionRepairWalk.tryForce(atoms, value) } + /** + * Delegates to a backward walk over [atoms], mirroring [tryForce]. + * @see [RxAbsorbable.tryForceSuffix] + */ + override fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + return AssertionRepairWalk.tryForceSuffix(atoms, value) + } + /** * Collapses to zero repetitions if [min] == 0 (removing every atom), otherwise forces * each existing atom to zero width individually. diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt index daa9c0491f..f09b7b67b6 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RegexGene.kt @@ -43,7 +43,7 @@ class RegexGene( override fun copyContent(): Gene { - return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags) + return RegexGene(name, disjunctions.copy() as DisjunctionListRxGene, sourceRegex, regexType, fixedValue, usingFixedValue, externalRegexFlags, hasAssertions) } companion object { diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt index e0cb41c491..9056ff6581 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/regex/RxAbsorbable.kt @@ -44,4 +44,29 @@ interface RxAbsorbable { "${this::class.simpleName} cannot be zero-width but forceZeroWidth was called" ) } + + /** + * Suffix-anchored counterpart of [absorbableCount], used by lookbehind repair: how many of + * [value]'s trailing characters could this gene be forced to produce. + * + * Default delegates to [absorbableCount] fed reversed [value]. This default is + * used by genes that produce exactly one character, like [AnyCharacterRxGene]. + */ + fun absorbableSuffixCount(value: String): Int = + if (value.isEmpty()) 0 else absorbableCount(value.reversed()) + + /** + * Suffix-anchored counterpart of [tryForce]: places as many of [value]'s trailing characters + * as possible and returns how many were actually placed. + * + * Default delegates to [tryForce] fed reversed [value], in a similar way to [absorbableSuffixCount]. + * + * Precondition: [value] is not empty. + */ + fun tryForceSuffix(value: String): Int { + require(value.isNotEmpty()) + val n = absorbableSuffixCount(value) + if (n > 0) tryForce(value.reversed()) + return n + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt index 24e7ffd425..5a1dbb3322 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/utils/AssertionRepairWalk.kt @@ -12,58 +12,90 @@ import org.evomaster.core.search.gene.regex.RxAbsorbable */ object AssertionRepairWalk { /** - * Maximum leading characters of [value] that can be absorbed across [genes] - * left-to-right, without mutating anything. + * Shared algorithm behind all public functions below, these differ only in: + * - [absorb]: which [RxAbsorbable] operation to call per gene (a read-only count, or a mutating force) + * - [onZeroWidth]: what to do when [absorb] returns 0 and the gene [RxAbsorbable.canBeZeroWidth] + * - [reversed]: whether to walk [genes] right-to-left for lookbehind or left-to-right for lookahead */ - fun absorbableCount(genes: List, value: String): Int { + private fun walk( + genes: List, + value: String, + absorb: (RxAbsorbable, String) -> Int, + onZeroWidth: (RxAbsorbable) -> Unit, + reversed: Boolean + ): Int { if (value.isEmpty()) { return 0 } var consumed = 0 - for (gene in genes) { + val walkTarget = if (reversed) { + genes.asReversed() + } else { + genes + } + for (gene in walkTarget) { if (consumed >= value.length) { break } val absorbable = gene as RxAbsorbable - val canTake = absorbable.absorbableCount(value.substring(consumed)) - if (canTake > 0) { - consumed += canTake - continue + val remaining = if (reversed) { + value.dropLast(consumed) + } else { + value.substring(consumed) } - if (absorbable.canBeZeroWidth) { - continue + val amount = absorb(absorbable, remaining) + if (amount == 0) { + if (absorbable.canBeZeroWidth) { + onZeroWidth(absorbable) + continue + } + return 0 } - return 0 + consumed += amount } return consumed } + /** + * Maximum leading characters of [value] that can be absorbed across [genes] + * left-to-right, without mutating anything. + */ + fun absorbableCount(genes: List, value: String): Int = + walk(genes, value, reversed = false, + absorb = { gene, value -> gene.absorbableCount(value) }, + onZeroWidth = {} + ) + /** * Forces as much of [value] as possible into [genes] left-to-right, mutating each * gene in place using each gene's [RxAbsorbable.tryForce]. Returns total characters placed. */ - fun tryForce(genes: List, value: String): Int { - if (value.isEmpty()) { - return 0 - } - var consumed = 0 - for (gene in genes) { - if (consumed >= value.length) { - break - } - val absorbable = gene as RxAbsorbable - val remaining = value.substring(consumed) - val placed = absorbable.tryForce(remaining) - if (placed == 0) { - if (absorbable.canBeZeroWidth) { - absorbable.forceZeroWidth() - continue - } else { - return 0 - } - } - consumed += placed - } - return consumed - } -} + fun tryForce(genes: List, value: String): Int = + walk(genes, value, reversed = false, + absorb = { gene, value -> gene.tryForce(value) }, + onZeroWidth = { it.forceZeroWidth() } + ) + + /** + * Suffix-anchored counterpart of [absorbableCount], used by lookbehind repair: maximum + * trailing characters of [value] that can be absorbed across [genes] right-to-left + * (walking [genes] in reverse, since the gene closest to the assertion's position is the + * last one in [genes]), without mutating anything. + */ + fun absorbableSuffixCount(genes: List, value: String): Int = + walk(genes, value, reversed = true, + absorb = { gene, value -> gene.absorbableSuffixCount(value) }, + onZeroWidth = {} + ) + + /** + * Suffix-anchored counterpart of [tryForce], used by lookbehind repair: forces as much + * of [value] as possible into [genes] right-to-left (mirroring [tryForce]), mutating each + * gene in place using [RxAbsorbable.tryForceSuffix]. Returns total characters placed. + */ + fun tryForceSuffix(genes: List, value: String): Int = + walk(genes, value, reversed = true, + absorb = { gene, value -> gene.tryForceSuffix(value) }, + onZeroWidth = { it.forceZeroWidth() } + ) +} \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt index a8220c3f22..19d7ba5216 100644 --- a/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitorTest.kt @@ -484,4 +484,28 @@ class GeneRegexJavaVisitorTest : GeneRegexEcma262VisitorTest() { assertThrows { checkSameAsJava("(?=[a&&b])a(bcef|de)de") } checkSameAsJava("abc|(?=[a&&b])def") } + + @Test + fun testSimpleLookbehinds() { + checkSameAsJava("foo(?<=oo)\\d+") + checkSameAsJava("\\d(?<=[13579])") + checkSameAsJava("a(?<=a)b") + checkSameAsJava("\\w*(?<=z)c") + checkSameAsJava("[a-z]+(?<=aa|bb)cc") + checkSameAsJava("a(?<=a)b") + checkSameAsJava("(abc|ab|a)(?<=abc)") + checkSameAsJava("(?a)\\k") + } + + @Test + fun testLookbehindRepairAcrossDirections() { + checkSameAsJava("\\w+(?<=X*)m(?=z)\\w") + checkSameAsJava("^(?<=X*)m(?=z)(a|z)") + } + + @Test + fun testUnsatisfiableLookbehinds() { + assertThrows { checkSameAsJava("(?<=X)a") } + assertThrows { checkSameAsJava("a(?<=[a&&b])a") } + } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index a5ab247d75..7c42a6b9d7 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -432,7 +432,7 @@ object GeneSamplerForTests { fun sampleAssertionRxGene(rand: Randomness): AssertionRxGene { val innerGene = sampleDisjunctionListRxGene(rand) innerGene.doInitialize(rand) - return AssertionRxGene(innerGene=innerGene) + return AssertionRxGene(innerGene=innerGene, AssertionType.LOOKAHEAD) } fun sampleRegexGene(rand: Randomness): RegexGene {