Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ CharacterClassEscape
;

EQUAL : '=';
LESS_THAN : '<';
CARET : '^';
DOLLAR : '$';
SLASH : '\\';
Expand All @@ -97,7 +98,7 @@ COLON : ':';

BaseChar
// practically all chars but the ones used for control and digits
: ~[0-9:,^$\\.*+?()[\]{}|=-]
: ~[0-9:,^$\\.<*+?()[\]{}|=-]
;

fragment OctalEscapeSequence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')'
;

Expand Down Expand Up @@ -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.
;

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
Expand All @@ -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 }) {
Expand All @@ -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
Expand Down
Loading
Loading