From a763b97d504ffc1ba4d6d7596a9bc88ada74bef3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 28 Mar 2019 19:44:34 -0400 Subject: [PATCH 1/4] ci: only test Rust benchmarks No need to build the benchmark suite 4 times. --- ci/script.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ci/script.sh b/ci/script.sh index ba66be192b..342744c8ee 100755 --- a/ci/script.sh +++ b/ci/script.sh @@ -46,9 +46,7 @@ ci/test-regex-capi # very long time. Also, check that we can build the regex-debug tool. if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then cargo build --verbose --manifest-path regex-debug/Cargo.toml - for x in rust rust-bytes pcre1 onig; do - (cd bench && ./run $x --no-run --verbose) - done + (cd bench && ./run rust --no-run --verbose) # Test minimal versions. cargo +nightly generate-lockfile -Z minimal-versions From 5734233ba4d35b83ad3d22c2e307031da1ef9358 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 27 Mar 2019 22:16:46 -0400 Subject: [PATCH 2/4] literal: upgrade to aho-corasick 0.7 This is a "dumb" update in that we retain exactly the same functionality as before. --- Cargo.toml | 2 +- src/literal/mod.rs | 20 +++++++++++++------- src/literal/teddy_avx2/imp.rs | 18 +++++++++++------- src/literal/teddy_ssse3/imp.rs | 18 +++++++++++------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 777d8d298c..711afb978a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ members = [ [dependencies] # For very fast prefix literal matching. -aho-corasick = "0.6.7" +aho-corasick = "0.7.1" # For skipping along search text quickly when a leading byte is known. memchr = "2.0.2" # For managing regex caches quickly across multiple threads. diff --git a/src/literal/mod.rs b/src/literal/mod.rs index de1bd5339b..81bb6f942f 100644 --- a/src/literal/mod.rs +++ b/src/literal/mod.rs @@ -11,7 +11,7 @@ use std::cmp; use std::mem; -use aho_corasick::{Automaton, AcAutomaton, FullAcAutomaton}; +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use memchr::{memchr, memchr2, memchr3}; use syntax::hir::literal::{Literal, Literals}; @@ -46,7 +46,7 @@ enum Matcher { /// A single substring, find using Boyer-Moore. BoyerMoore(BoyerMooreSearch), /// An Aho-Corasick automaton. - AC(FullAcAutomaton), + AC { ac: AhoCorasick, lits: Vec }, /// A simd accelerated multiple string matcher. Used only for a small /// number of small literals. TeddySSSE3(TeddySSSE3), @@ -102,7 +102,9 @@ impl LiteralSearcher { Bytes(ref sset) => sset.find(haystack).map(|i| (i, i + 1)), FreqyPacked(ref s) => s.find(haystack).map(|i| (i, i + s.len())), BoyerMoore(ref s) => s.find(haystack).map(|i| (i, i + s.len())), - AC(ref aut) => aut.find(haystack).next().map(|m| (m.start, m.end)), + AC { ref ac, .. } => { + ac.find(haystack).map(|m| (m.start(), m.end())) + } TeddySSSE3(ref t) => t.find(haystack).map(|m| (m.start, m.end)), TeddyAVX2(ref t) => t.find(haystack).map(|m| (m.start, m.end)), } @@ -141,7 +143,7 @@ impl LiteralSearcher { Matcher::Bytes(ref sset) => LiteralIter::Bytes(&sset.dense), Matcher::FreqyPacked(ref s) => LiteralIter::Single(&s.pat), Matcher::BoyerMoore(ref s) => LiteralIter::Single(&s.pattern), - Matcher::AC(ref ac) => LiteralIter::AC(ac.patterns()), + Matcher::AC { ref lits, .. } => LiteralIter::AC(lits), Matcher::TeddySSSE3(ref ted) => { LiteralIter::TeddySSSE3(ted.patterns()) } @@ -174,7 +176,7 @@ impl LiteralSearcher { Bytes(ref sset) => sset.dense.len(), FreqyPacked(_) => 1, BoyerMoore(_) => 1, - AC(ref aut) => aut.len(), + AC { ref ac, .. } => ac.pattern_count(), TeddySSSE3(ref ted) => ted.len(), TeddyAVX2(ref ted) => ted.len(), } @@ -188,7 +190,7 @@ impl LiteralSearcher { Bytes(ref sset) => sset.approximate_size(), FreqyPacked(ref single) => single.approximate_size(), BoyerMoore(ref single) => single.approximate_size(), - AC(ref aut) => aut.heap_bytes(), + AC { ref ac, .. } => ac.heap_bytes(), TeddySSSE3(ref ted) => ted.approximate_size(), TeddyAVX2(ref ted) => ted.approximate_size(), } @@ -258,7 +260,11 @@ impl Matcher { // Fallthrough to ol' reliable Aho-Corasick... } let pats = lits.literals().to_owned(); - Matcher::AC(AcAutomaton::new(pats).into_full()) + let ac = AhoCorasickBuilder::new() + .dfa(true) + .build_with_size::(&pats) + .unwrap(); + Matcher::AC { ac, lits: pats } } } diff --git a/src/literal/teddy_avx2/imp.rs b/src/literal/teddy_avx2/imp.rs index 62015d07d2..3be0b1500f 100644 --- a/src/literal/teddy_avx2/imp.rs +++ b/src/literal/teddy_avx2/imp.rs @@ -9,7 +9,7 @@ basically the same as the SSSE3 version, but using 256-bit vectors instead of use std::cmp; -use aho_corasick::{Automaton, AcAutomaton, FullAcAutomaton}; +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use syntax::hir::literal::Literals; use vector::avx2::{AVX2VectorBuilder, u8x32}; @@ -38,7 +38,7 @@ pub struct Teddy { pats: Vec>, /// An Aho-Corasick automaton of the patterns. We use this when we need to /// search pieces smaller than the Teddy block size. - ac: FullAcAutomaton>, + ac: AhoCorasick, /// A set of 8 buckets. Each bucket corresponds to a single member of a /// bitset. A bucket contains zero or more substrings. This is useful /// when the number of substrings exceeds 8, since our bitsets cannot have @@ -88,10 +88,14 @@ impl Teddy { buckets[bucket].push(pati); masks.add(bucket as u8, pat); } + let ac = AhoCorasickBuilder::new() + .dfa(true) + .prefilter(false) + .build(&pats); Some(Teddy { vb: vb, pats: pats.to_vec(), - ac: AcAutomaton::new(pats.to_vec()).into_full(), + ac: ac, buckets: buckets, masks: masks, }) @@ -341,11 +345,11 @@ impl Teddy { /// block based approach. #[inline(never)] fn slow(&self, haystack: &[u8], pos: usize) -> Option { - self.ac.find(&haystack[pos..]).next().map(|m| { + self.ac.find(&haystack[pos..]).map(|m| { Match { - pat: m.pati, - start: pos + m.start, - end: pos + m.end, + pat: m.pattern(), + start: pos + m.start(), + end: pos + m.end(), } }) } diff --git a/src/literal/teddy_ssse3/imp.rs b/src/literal/teddy_ssse3/imp.rs index 85422b6409..c3dc31e04b 100644 --- a/src/literal/teddy_ssse3/imp.rs +++ b/src/literal/teddy_ssse3/imp.rs @@ -320,7 +320,7 @@ References use std::cmp; -use aho_corasick::{Automaton, AcAutomaton, FullAcAutomaton}; +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use syntax::hir::literal::Literals; use vector::ssse3::{SSSE3VectorBuilder, u8x16}; @@ -349,7 +349,7 @@ pub struct Teddy { pats: Vec>, /// An Aho-Corasick automaton of the patterns. We use this when we need to /// search pieces smaller than the Teddy block size. - ac: FullAcAutomaton>, + ac: AhoCorasick, /// A set of 8 buckets. Each bucket corresponds to a single member of a /// bitset. A bucket contains zero or more substrings. This is useful /// when the number of substrings exceeds 8, since our bitsets cannot have @@ -399,10 +399,14 @@ impl Teddy { buckets[bucket].push(pati); masks.add(bucket as u8, pat); } + let ac = AhoCorasickBuilder::new() + .dfa(true) + .prefilter(false) + .build(&pats); Some(Teddy { vb: vb, pats: pats.to_vec(), - ac: AcAutomaton::new(pats.to_vec()).into_full(), + ac: ac, buckets: buckets, masks: masks, }) @@ -651,11 +655,11 @@ impl Teddy { /// block based approach. #[inline(never)] fn slow(&self, haystack: &[u8], pos: usize) -> Option { - self.ac.find(&haystack[pos..]).next().map(|m| { + self.ac.find(&haystack[pos..]).map(|m| { Match { - pat: m.pati, - start: pos + m.start, - end: pos + m.end, + pat: m.pattern(), + start: pos + m.start(), + end: pos + m.end(), } }) } From 461673dae13d7feb869667c99b01236029a9184e Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 28 Mar 2019 21:00:20 -0400 Subject: [PATCH 3/4] syntax: add is_literal and is_alternation_literal This adds a couple new methods on HIR expressions for determining whether they are literals or not. This is useful for determining whether to apply optimizations such as Aho-Corasick without re-analyzing the syntax. --- regex-syntax/src/hir/mod.rs | 55 +++++++++++++++++++++++++++++++ regex-syntax/src/hir/translate.rs | 45 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/regex-syntax/src/hir/mod.rs b/regex-syntax/src/hir/mod.rs index 9aa60375da..40b4fea345 100644 --- a/regex-syntax/src/hir/mod.rs +++ b/regex-syntax/src/hir/mod.rs @@ -227,6 +227,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(true); + info.set_literal(true); + info.set_alternation_literal(true); Hir { kind: HirKind::Empty, info: info, @@ -253,6 +255,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(false); + info.set_literal(true); + info.set_alternation_literal(true); Hir { kind: HirKind::Literal(lit), info: info, @@ -271,6 +275,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(false); + info.set_literal(false); + info.set_alternation_literal(false); Hir { kind: HirKind::Class(class), info: info, @@ -289,6 +295,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(true); + info.set_literal(false); + info.set_alternation_literal(false); if let Anchor::StartText = anchor { info.set_anchored_start(true); info.set_line_anchored_start(true); @@ -322,6 +330,8 @@ impl Hir { info.set_line_anchored_end(false); info.set_any_anchored_start(false); info.set_any_anchored_end(false); + info.set_literal(false); + info.set_alternation_literal(false); // A negated word boundary matches the empty string, but a normal // word boundary does not! info.set_match_empty(word_boundary.is_negated()); @@ -357,6 +367,8 @@ impl Hir { info.set_any_anchored_start(rep.hir.is_any_anchored_start()); info.set_any_anchored_end(rep.hir.is_any_anchored_end()); info.set_match_empty(rep.is_match_empty() || rep.hir.is_match_empty()); + info.set_literal(false); + info.set_alternation_literal(false); Hir { kind: HirKind::Repetition(rep), info: info, @@ -375,6 +387,8 @@ impl Hir { info.set_any_anchored_start(group.hir.is_any_anchored_start()); info.set_any_anchored_end(group.hir.is_any_anchored_end()); info.set_match_empty(group.hir.is_match_empty()); + info.set_literal(false); + info.set_alternation_literal(false); Hir { kind: HirKind::Group(group), info: info, @@ -395,6 +409,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(true); + info.set_literal(true); + info.set_alternation_literal(true); // Some attributes require analyzing all sub-expressions. for e in &exprs { @@ -416,6 +432,14 @@ impl Hir { let x = info.is_match_empty() && e.is_match_empty(); info.set_match_empty(x); + + let x = info.is_literal() && e.is_literal(); + info.set_literal(x); + + let x = + info.is_alternation_literal() + && e.is_alternation_literal(); + info.set_alternation_literal(x); } // Anchored attributes require something slightly more // sophisticated. Normally, WLOG, to determine whether an @@ -488,6 +512,8 @@ impl Hir { info.set_any_anchored_start(false); info.set_any_anchored_end(false); info.set_match_empty(false); + info.set_literal(false); + info.set_alternation_literal(true); // Some attributes require analyzing all sub-expressions. for e in &exprs { @@ -523,6 +549,11 @@ impl Hir { let x = info.is_match_empty() || e.is_match_empty(); info.set_match_empty(x); + + let x = + info.is_alternation_literal() + && e.is_literal(); + info.set_alternation_literal(x); } Hir { kind: HirKind::Alternation(exprs), @@ -655,6 +686,28 @@ impl Hir { pub fn is_match_empty(&self) -> bool { self.info.is_match_empty() } + + /// Return true if and only if this HIR is a simple literal. This is only + /// true when this HIR expression is either itself a `Literal` or a + /// concatenation of only `Literal`s. + /// + /// For example, `f` and `foo` are literals, but `f+`, `(foo)`, `foo()` + /// are not (even though that contain sub-expressions that are literals). + pub fn is_literal(&self) -> bool { + self.info.is_literal() + } + + /// Return true if and only if this HIR is either a simple literal or an + /// alternation of simple literals. This is only + /// true when this HIR expression is either itself a `Literal` or a + /// concatenation of only `Literal`s or an alternation of only `Literal`s. + /// + /// For example, `f`, `foo`, `a|b|c`, and `foo|bar|baz` are alternaiton + /// literals, but `f+`, `(foo)`, `foo()` + /// are not (even though that contain sub-expressions that are literals). + pub fn is_alternation_literal(&self) -> bool { + self.info.is_alternation_literal() + } } impl HirKind { @@ -1415,6 +1468,8 @@ impl HirInfo { define_bool!(6, is_any_anchored_start, set_any_anchored_start); define_bool!(7, is_any_anchored_end, set_any_anchored_end); define_bool!(8, is_match_empty, set_match_empty); + define_bool!(9, is_literal, set_literal); + define_bool!(10, is_alternation_literal, set_alternation_literal); } #[cfg(test)] diff --git a/regex-syntax/src/hir/translate.rs b/regex-syntax/src/hir/translate.rs index 4ad11d12f3..31a1ca4e9d 100644 --- a/regex-syntax/src/hir/translate.rs +++ b/regex-syntax/src/hir/translate.rs @@ -2589,4 +2589,49 @@ mod tests { assert!(!t(r"\b").is_match_empty()); assert!(!t(r"(?-u)\b").is_match_empty()); } + + #[test] + fn analysis_is_literal() { + // Positive examples. + assert!(t(r"").is_literal()); + assert!(t(r"a").is_literal()); + assert!(t(r"ab").is_literal()); + assert!(t(r"abc").is_literal()); + assert!(t(r"(?m)abc").is_literal()); + + // Negative examples. + assert!(!t(r"^").is_literal()); + assert!(!t(r"a|b").is_literal()); + assert!(!t(r"(a)").is_literal()); + assert!(!t(r"a+").is_literal()); + assert!(!t(r"foo(a)").is_literal()); + assert!(!t(r"(a)foo").is_literal()); + assert!(!t(r"[a]").is_literal()); + } + + #[test] + fn analysis_is_alternation_literal() { + // Positive examples. + assert!(t(r"").is_alternation_literal()); + assert!(t(r"a").is_alternation_literal()); + assert!(t(r"ab").is_alternation_literal()); + assert!(t(r"abc").is_alternation_literal()); + assert!(t(r"(?m)abc").is_alternation_literal()); + assert!(t(r"a|b").is_alternation_literal()); + assert!(t(r"a|b|c").is_alternation_literal()); + assert!(t(r"foo|bar").is_alternation_literal()); + assert!(t(r"foo|bar|baz").is_alternation_literal()); + + // Negative examples. + assert!(!t(r"^").is_alternation_literal()); + assert!(!t(r"(a)").is_alternation_literal()); + assert!(!t(r"a+").is_alternation_literal()); + assert!(!t(r"foo(a)").is_alternation_literal()); + assert!(!t(r"(a)foo").is_alternation_literal()); + assert!(!t(r"[a]").is_alternation_literal()); + assert!(!t(r"[a]|b").is_alternation_literal()); + assert!(!t(r"a|[b]").is_alternation_literal()); + assert!(!t(r"(a)|b").is_alternation_literal()); + assert!(!t(r"a|(b)").is_alternation_literal()); + } } From d7c01ccdf09a87ec450930984d2f8d14f3a6d99c Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 28 Mar 2019 21:03:43 -0400 Subject: [PATCH 4/4] exec: add Aho-Corasick optimization Finally, if a regex is just `foo|bar|baz|...|quux`, we will now use plain old Aho-Corasick. The reason why we weren't doing this before is because Aho-Corasick didn't support proper leftmost-first match semantics. But since aho-corasick 0.7, it does, so we can now use it as a drop-in replacement. This basically fixes a pretty bad performance bug in a really common case, but it is otherwise really hacked. First of all, this only happens when a regex is literally `foo|bar|...|baz`. Even something like `foo|b(a)r|...|baz` will prevent this optimization from happening, which is a little silly. Second of all, this optimization only kicks in after we've compiled the full pattern, which adds quite a bit of overhead. Fixing this isn't trivial, since we may need the compiled program to resolve capturing groups. The way to do this is probably to specialize compilation for certain types of expressions. Maybe. Anyway, we hack this in for now, and punt on further improvements until we can really re-think how this should all work. --- src/exec.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++ tests/regression.rs | 10 +++++ 2 files changed, 106 insertions(+) diff --git a/src/exec.rs b/src/exec.rs index dd30c81ec5..c1f836339c 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -13,6 +13,7 @@ use std::collections::HashMap; use std::cmp; use std::sync::Arc; +use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind}; use thread_local::CachedThreadLocal; use syntax::ParserBuilder; use syntax::hir::Hir; @@ -86,6 +87,16 @@ struct ExecReadOnly { /// Prefix literals are stored on the `Program`, since they are used inside /// the matching engines. suffixes: LiteralSearcher, + /// An Aho-Corasick automaton with leftmost-first match semantics. + /// + /// This is only set when the entire regex is a simple unanchored + /// alternation of literals. We could probably use it more circumstances, + /// but this is already hacky enough in this architecture. + /// + /// N.B. We use u32 as a state ID representation under the assumption that + /// if we were to exhaust the ID space, we probably would have long + /// surpassed the compilation size limit. + ac: Option>, /// match_type encodes as much upfront knowledge about how we're going to /// execute a search as possible. match_type: MatchType, @@ -287,6 +298,7 @@ impl ExecBuilder { dfa: Program::new(), dfa_reverse: Program::new(), suffixes: LiteralSearcher::empty(), + ac: None, match_type: MatchType::Nothing, }); return Ok(Exec { ro: ro, cache: CachedThreadLocal::new() }); @@ -319,12 +331,32 @@ impl ExecBuilder { dfa.dfa_size_limit = self.options.dfa_size_limit; dfa_reverse.dfa_size_limit = self.options.dfa_size_limit; + let mut ac = None; + if parsed.exprs.len() == 1 { + if let Some(lits) = alternation_literals(&parsed.exprs[0]) { + // If we have a small number of literals, then let Teddy + // handle things (see literal/mod.rs). + if lits.len() > 32 { + let fsm = AhoCorasickBuilder::new() + .match_kind(MatchKind::LeftmostFirst) + .auto_configure(&lits) + // We always want this to reduce size, regardless of + // what auto-configure does. + .byte_classes(true) + .build_with_size::(&lits) + .expect("AC automaton too big"); + ac = Some(fsm); + } + } + } + let mut ro = ExecReadOnly { res: self.options.pats, nfa: nfa, dfa: dfa, dfa_reverse: dfa_reverse, suffixes: LiteralSearcher::suffixes(suffixes), + ac: ac, match_type: MatchType::Nothing, }; ro.match_type = ro.choose_match_type(self.match_type); @@ -633,6 +665,11 @@ impl<'c> ExecNoSync<'c> { lits.find_end(&text[start..]) .map(|(s, e)| (start + s, start + e)) } + AhoCorasick => { + self.ro.ac.as_ref().unwrap() + .find(&text[start..]) + .map(|m| (start + m.start(), start + m.end())) + } } } @@ -1163,6 +1200,9 @@ impl ExecReadOnly { // aren't anchored. We would then only search for all of them when at // the beginning of the input and use the subset in all other cases. if self.res.len() == 1 { + if self.ac.is_some() { + return Literal(MatchLiteralType::AhoCorasick); + } if self.nfa.prefixes.complete() { return if self.nfa.is_anchored_start { Literal(MatchLiteralType::AnchoredStart) @@ -1254,6 +1294,9 @@ enum MatchLiteralType { AnchoredStart, /// Match literals only at the end of text. AnchoredEnd, + /// Use an Aho-Corasick automaton. This requires `ac` to be Some on + /// ExecReadOnly. + AhoCorasick, } #[derive(Clone, Copy, Debug)] @@ -1295,6 +1338,59 @@ impl ProgramCacheInner { } } +/// Alternation literals checks if the given HIR is a simple alternation of +/// literals, and if so, returns them. Otherwise, this returns None. +fn alternation_literals(expr: &Hir) -> Option>> { + use syntax::hir::{HirKind, Literal}; + + // This is pretty hacky, but basically, if `is_alternation_literal` is + // true, then we can make several assumptions about the structure of our + // HIR. This is what justifies the `unreachable!` statements below. + // + // This code should be refactored once we overhaul this crate's + // optimization pipeline, because this is a terribly inflexible way to go + // about things. + + if !expr.is_alternation_literal() { + return None; + } + let alts = match *expr.kind() { + HirKind::Alternation(ref alts) => alts, + _ => return None, // one literal isn't worth it + }; + + let extendlit = |lit: &Literal, dst: &mut Vec| { + match *lit { + Literal::Unicode(c) => { + let mut buf = [0; 4]; + dst.extend_from_slice(c.encode_utf8(&mut buf).as_bytes()); + } + Literal::Byte(b) => { + dst.push(b); + } + } + }; + + let mut lits = vec![]; + for alt in alts { + let mut lit = vec![]; + match *alt.kind() { + HirKind::Literal(ref x) => extendlit(x, &mut lit), + HirKind::Concat(ref exprs) => { + for e in exprs { + match *e.kind() { + HirKind::Literal(ref x) => extendlit(x, &mut lit), + _ => unreachable!("expected literal, got {:?}", e), + } + } + } + _ => unreachable!("expected literal or concat, got {:?}", alt), + } + lits.push(lit); + } + Some(lits) +} + #[cfg(test)] mod test { #[test] diff --git a/tests/regression.rs b/tests/regression.rs index 724e01bfa0..ad34b64c24 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -114,3 +114,13 @@ ismatch!( \u{10}\u{11}\u{12}\u{13}\u{14}\u{15}\u{16}\u{17}\ \u{18}\u{19}\u{1a}\u{1b}\u{1c}\u{1d}\u{1e}\u{1f}", true); + +// Tests that our Aho-Corasick optimization works correctly. It only +// kicks in when we have >32 literals. +mat!( + ahocorasick1, + "samwise|sam|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|\ + A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z", + "samwise", + Some((0, 7)) +);