Repository: fancy-regex/fancy-regex (language: rust)

Issue:
Support for Keep out \K
There is a method for this, without using lookbehind?


Discussion / hints:
That's not currently supported, no. Here's a good reference for what it does and how it works: https://www.regular-expressions.info/keep.html

I've added the labels for feature request and help wanted, and I'd be happy if someone wanted to take a stab at implementing this. It doesn't sound too hard to implement, and I can outline what would need to be done if anyone shows interest in picking this up.
I'm interested to pick this up, though I can't commit much time to it so could be a while before I would be able to raise a PR.
I guess it would just "redefine" where capture group 0 starts. I'm not sure how it would work if `\K` is used inside a subexpression/repetition though - it may not be possible to do it when parsing the regular expression pattern, and have to be done at runtime? Just off the top of my head thoughts without refreshing my memory of the code first ;)
Cool :)! Yep, I'm pretty sure it has to be done at runtime. I think it would involve roughly:

1. Parsing `\K` and creating an `Expr` for it and making sure the corresponding expression is marked as "hard" so that we won't try to delegate it to the regex crate. We can probably still delegate things before and after it to regex, but I haven't put much thought into that part yet.
2. In the VM, whenever we encounter a `\K` instruction, store the position as the match (group 0) start position (might have to keep track of the original as well so we can properly resume matching, not sure off the top of my head). That should just work for subexpressions and repetitions.

There's probably some special cases (e.g. can't be used within lookarounds) that we need to handle too, but yeah.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7f4332..3467017 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
 with the exception that 0.x versions can break between minor versions.
 
+## [Unreleased]
+### Added
+- Added support for `\K` keep out
+
 ## [0.8.0] - 2022-02-22
 ### Added
 - Allow users to disable any of the `unicode` and `perf-*` features of
diff --git a/src/analyze.rs b/src/analyze.rs
index cb85a94..7b5130a 100644
--- a/src/analyze.rs
+++ b/src/analyze.rs
@@ -185,6 +185,10 @@ impl<'a> Analyzer<'a> {
                 hard = true; // TODO: possibly could weaken
                 children.push(child_info);
             }
+            Expr::KeepOut => {
+                hard = true;
+                const_size = true;
+            }
         };
 
         Ok(Info {
diff --git a/src/compile.rs b/src/compile.rs
index e550889..19dd37f 100644
--- a/src/compile.rs
+++ b/src/compile.rs
@@ -164,6 +164,9 @@ impl Compiler {
             Expr::NamedBackref(_) => {
                 unreachable!("named backrefs should have been eliminated");
             }
+            Expr::KeepOut => {
+                self.b.add(Insn::Save(0));
+            }
         }
         Ok(())
     }
diff --git a/src/lib.rs b/src/lib.rs
index f571b5d..7cda184 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1186,6 +1186,8 @@ pub enum Expr {
     /// Atomic non-capturing group, e.g. `(?>ab|a)` in text that contains `ab` will match `ab` and
     /// never backtrack and try `a`, even if matching fails after the atomic group.
     AtomicGroup(Box<Expr>),
+    /// Keep matched text so far out of overall match
+    KeepOut,
 }
 
 /// Type of look-around assertion as used for a look-around expression.
diff --git a/src/parse.rs b/src/parse.rs
index 40fcbdd..7e4d472 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -385,6 +385,8 @@ impl<'a> Parser<'a> {
                     end += codepoint_len(b);
                 }
             }
+        } else if b == b'K' {
+            return Ok((end, Expr::KeepOut));
         } else if b'a' <= (b | 32) && (b | 32) <= b'z' {
             return Err(Error::InvalidEscape(format!("\\{}", &self.re[ix + 1..end])));
         } else if 0x20 <= b && b <= 0x7f {
@@ -1344,6 +1346,14 @@ mod tests {
         assert_error("(a|b|?)", "Target of repeat operator is invalid");
     }
 
+    #[test]
+    fn keepout() {
+        assert_eq!(
+            p("a\\Kb"),
+            Expr::Concat(vec![make_literal("a"), Expr::KeepOut, make_literal("b"),])
+        );
+    }
+
     // found by cargo fuzz, then minimized
     #[test]
     fn fuzz_1() {
diff --git a/src/vm.rs b/src/vm.rs
index fbf65ec..73b5b1e 100644
--- a/src/vm.rs
+++ b/src/vm.rs
@@ -439,6 +439,10 @@ pub(crate) fn run(
                     if option_flags & OPTION_TRACE != 0 {
                         println!("saves: {:?}", state.saves);
                     }
+                    let slot1 = state.get(1);
+                    if state.get(0) > slot1 {
+                        state.save(0, slot1);
+                    }
                     return Ok(Some(state.saves));
                 }
                 Insn::Any => {


Test patch:
diff --git a/tests/captures.rs b/tests/captures.rs
index 2848aaf..3db8bbd 100644
--- a/tests/captures.rs
+++ b/tests/captures.rs
@@ -50,6 +50,44 @@ fn captures_after_lookbehind() {
     assert_match(captures.get(2), "(foo bar)", 9, 18);
 }
 
+#[test]
+fn captures_with_keepout_inside_at_end() {
+    let captures = captures(r"\s*(\w+\K)(?=\.)", "foo bar.");
+    assert_eq!(captures.len(), 2);
+    assert_match(captures.get(0), "", 7, 7);
+    assert_match(captures.get(1), "bar", 4, 7);
+    assert!(captures.get(2).is_none());
+}
+
+#[test]
+fn captures_with_keepout_inside_in_middle() {
+    let captures = captures(r"\s*(b\Kar)(?=\.)", "foo bar.");
+    assert_eq!(captures.len(), 2);
+    assert_match(captures.get(0), "ar", 5, 7);
+    assert_match(captures.get(1), "bar", 4, 7);
+    assert!(captures.get(2).is_none());
+}
+
+#[test]
+fn captures_with_keepout_between() {
+    let captures = captures(r"(\w+)\K\s*(\w+)(?=\.)", "foo bar.");
+    assert_eq!(captures.len(), 3);
+    assert_match(captures.get(0), " bar", 3, 7);
+    assert_match(captures.get(1), "foo", 0, 3);
+    assert_match(captures.get(2), "bar", 4, 7);
+    assert!(captures.get(3).is_none());
+}
+
+#[test]
+fn captures_with_nested_keepout() {
+    let captures = captures(r"(\w\K)+\s*(\w+)(?=\.)", "foo bar.");
+    assert_eq!(captures.len(), 3);
+    assert_match(captures.get(0), " bar", 3, 7);
+    assert_match(captures.get(1), "o", 2, 3);
+    assert_match(captures.get(2), "bar", 4, 7);
+    assert!(captures.get(3).is_none());
+}
+
 #[test]
 fn captures_iter() {
     let text = "11 21 33";
diff --git a/tests/finding.rs b/tests/finding.rs
index b92bf87..e2f6123 100644
--- a/tests/finding.rs
+++ b/tests/finding.rs
@@ -134,6 +134,22 @@ fn delegates_match_unicode_scalar_value() {
     assert_eq!(find(r".(?=\ba+)", "\u{1F60A}a"), Some((0, 4)));
 }
 
+#[test]
+fn keepout_matches_in_correct_place() {
+    assert_eq!(find(r"a\Kb", "aaab"), Some((3, 4)));
+    assert_eq!(find(r".+\Kb", "aaab"), Some((3, 4)));
+    assert_eq!(find(r"(?:aaa\K)b", "aaab"), Some((3, 4)));
+}
+
+#[test]
+fn keepout_in_lookarounds_match_in_correct_place() {
+    assert_eq!(find(r"(?<=a\Kb)c", "abc"), Some((1, 3)));
+    assert_eq!(find(r"(?<!a\Kb)c", "axc"), Some((2, 3)));
+    assert_eq!(find(r"a(?=b\Kc)", "abc"), Some((1, 1)));
+    assert_eq!(find(r"a(?=b\Kc)..", "abc"), Some((2, 3)));
+    assert_eq!(find(r"a(?!b\Kc)", "abx"), Some((0, 1)));
+}
+
 #[test]
 fn find_iter() {
     let text = "11 22 33";
diff --git a/tests/oniguruma/test_utf8_ignore.c b/tests/oniguruma/test_utf8_ignore.c
index 3e7d9ad..58f0a81 100644
--- a/tests/oniguruma/test_utf8_ignore.c
+++ b/tests/oniguruma/test_utf8_ignore.c
@@ -314,21 +314,6 @@
   // Compile failed: InvalidEscape("\\O")
   x2("(?-m:\\O)", "\n", 0, 1);
 
-  // Compile failed: InvalidEscape("\\K")
-  x2("\\K", "a", 0, 0);
-
-  // Compile failed: InvalidEscape("\\K")
-  x2("a\\K", "a", 1, 1);
-
-  // Compile failed: InvalidEscape("\\K")
-  x2("a\\Kb", "ab", 1, 2);
-
-  // Compile failed: InvalidEscape("\\K")
-  x2("(a\\Kb|ac\\Kd)", "acd", 2, 3);
-
-  // Compile failed: InvalidEscape("\\K")
-  x2("(a\\Kb|\\Kac\\K)*", "acababacab", 9, 10);
-
   // No match found
   x2("(?:()|())*\\1", "abc", 0, 0);