1 /* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.nashorn.internal.objects; 27 28 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError; 29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.List; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 import jdk.nashorn.internal.objects.annotations.Attribute; 37 import jdk.nashorn.internal.objects.annotations.Constructor; 38 import jdk.nashorn.internal.objects.annotations.Function; 39 import jdk.nashorn.internal.objects.annotations.Getter; 40 import jdk.nashorn.internal.objects.annotations.Property; 41 import jdk.nashorn.internal.objects.annotations.ScriptClass; 42 import jdk.nashorn.internal.objects.annotations.SpecializedConstructor; 43 import jdk.nashorn.internal.parser.RegExp; 44 import jdk.nashorn.internal.runtime.BitVector; 45 import jdk.nashorn.internal.runtime.JSType; 46 import jdk.nashorn.internal.runtime.ParserException; 47 import jdk.nashorn.internal.runtime.RegExpMatch; 48 import jdk.nashorn.internal.runtime.ScriptFunction; 49 import jdk.nashorn.internal.runtime.ScriptObject; 50 import jdk.nashorn.internal.runtime.ScriptRuntime; 51 52 /** 53 * ECMA 15.10 RegExp Objects. 54 */ 55 @ScriptClass("RegExp") 56 public final class NativeRegExp extends ScriptObject { 57 /** ECMA 15.10.7.5 lastIndex property */ 58 @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE) 59 public Object lastIndex; 60 61 /** Pattern string. */ 62 private String input; 63 64 /** Global search flag for this regexp. */ 65 private boolean global; 66 67 /** Case insensitive flag for this regexp */ 68 private boolean ignoreCase; 69 70 /** Multi-line flag for this regexp */ 71 private boolean multiline; 72 73 /** Java regex pattern to use for match. We compile to one of these */ 74 private Pattern pattern; 75 76 private BitVector groupsInNegativeLookahead; 77 78 /* 79 public NativeRegExp() { 80 init(); 81 }*/ 82 83 NativeRegExp(final String input, final String flagString) { 84 RegExp regExp = null; 85 try { 86 regExp = new RegExp(input, flagString); 87 } catch (final ParserException e) { 88 // translate it as SyntaxError object and throw it 89 e.throwAsEcmaException(); 90 throw new AssertionError(); //guard against null warnings below 91 } 92 93 this.setLastIndex(0); 94 this.input = regExp.getInput(); 95 this.global = regExp.isGlobal(); 96 this.ignoreCase = regExp.isIgnoreCase(); 97 this.multiline = regExp.isMultiline(); 98 this.pattern = regExp.getPattern(); 99 this.groupsInNegativeLookahead = regExp.getGroupsInNegativeLookahead(); 100 101 init(); 102 } 103 104 NativeRegExp(final String string) { 105 this(string, ""); 106 } 107 108 NativeRegExp(final NativeRegExp regExp) { 109 this.input = regExp.getInput(); 110 this.global = regExp.getGlobal(); 111 this.multiline = regExp.getMultiline(); 112 this.ignoreCase = regExp.getIgnoreCase(); 113 this.lastIndex = regExp.getLastIndexObject(); 114 this.pattern = regExp.getPattern(); 115 this.groupsInNegativeLookahead = regExp.getGroupsInNegativeLookahead(); 116 117 init(); 118 } 119 120 NativeRegExp(final Pattern pattern) { 121 this.input = pattern.pattern(); 122 this.multiline = (pattern.flags() & Pattern.MULTILINE) != 0; 123 this.ignoreCase = (pattern.flags() & Pattern.CASE_INSENSITIVE) != 0; 124 this.lastIndex = 0; 125 this.pattern = pattern; 126 127 init(); 128 } 129 130 @Override 131 public String getClassName() { 132 return "RegExp"; 133 } 134 135 /** 136 * ECMA 15.10.4 137 * 138 * Constructor 139 * 140 * @param isNew is the new operator used for instantiating this regexp 141 * @param self self reference 142 * @param args arguments (optional: pattern and flags) 143 * @return new NativeRegExp 144 */ 145 @Constructor(arity = 2) 146 public static Object constructor(final boolean isNew, final Object self, final Object... args) { 147 if (args.length > 1) { 148 return newRegExp(args[0], args[1]); 149 } else if (args.length > 0) { 150 return newRegExp(args[0], UNDEFINED); 151 } 152 153 return newRegExp(UNDEFINED, UNDEFINED); 154 } 155 156 /** 157 * ECMA 15.10.4 158 * 159 * Constructor - specialized version, no args, empty regexp 160 * 161 * @param isNew is the new operator used for instantiating this regexp 162 * @param self self reference 163 * @return new NativeRegExp 164 */ 165 @SpecializedConstructor 166 public static Object constructor(final boolean isNew, final Object self) { 167 return new NativeRegExp("", ""); 168 } 169 170 /** 171 * ECMA 15.10.4 172 * 173 * Constructor - specialized version, pattern, no flags 174 * 175 * @param isNew is the new operator used for instantiating this regexp 176 * @param self self reference 177 * @param pattern pattern 178 * @return new NativeRegExp 179 */ 180 @SpecializedConstructor 181 public static Object constructor(final boolean isNew, final Object self, final Object pattern) { 182 return newRegExp(pattern, UNDEFINED); 183 } 184 185 /** 186 * ECMA 15.10.4 187 * 188 * Constructor - specialized version, pattern and flags 189 * 190 * @param isNew is the new operator used for instantiating this regexp 191 * @param self self reference 192 * @param pattern pattern 193 * @param flags flags 194 * @return new NativeRegExp 195 */ 196 @SpecializedConstructor 197 public static Object constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) { 198 return newRegExp(pattern, flags); 199 } 200 201 /** 202 * External constructor used in generated code created by {@link jdk.nashorn.internal.codegen.CodeGenerator}, which 203 * explain the {@code public} access. 204 * 205 * @param regexp regexp 206 * @param flags flags 207 * @return new NativeRegExp 208 */ 209 public static NativeRegExp newRegExp(final Object regexp, final Object flags) { 210 String patternString = ""; 211 String flagString = ""; 212 boolean flagsDefined = false; 213 214 if (flags != UNDEFINED) { 215 flagsDefined = true; 216 flagString = JSType.toString(flags); 217 } 218 219 if (regexp != UNDEFINED) { 220 if (regexp instanceof NativeRegExp) { 221 if (!flagsDefined) { 222 return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as 223 } 224 typeError("regex.cant.supply.flags"); 225 } 226 patternString = JSType.toString(regexp); 227 } 228 229 return new NativeRegExp(patternString, flagString); 230 } 231 232 private String getFlagString() { 233 final StringBuilder sb = new StringBuilder(); 234 235 if (global) { 236 sb.append('g'); 237 } 238 if (ignoreCase) { 239 sb.append('i'); 240 } 241 if (multiline) { 242 sb.append('m'); 243 } 244 245 return sb.toString(); 246 } 247 248 @Override 249 public String safeToString() { 250 return "[RegExp " + toString() + "]"; 251 } 252 253 @Override 254 public String toString() { 255 return "/" + input + "/" + getFlagString(); 256 } 257 258 /** 259 * Nashorn extension: RegExp.prototype.compile - everybody implements this! 260 * 261 * @param self self reference 262 * @param pattern pattern 263 * @param flags flags 264 * @return new NativeRegExp 265 */ 266 @Function(attributes = Attribute.NOT_ENUMERABLE) 267 public static Object compile(final Object self, final Object pattern, final Object flags) { 268 final NativeRegExp regExp = checkRegExp(self); 269 final NativeRegExp compiled = newRegExp(pattern, flags); 270 // copy over fields to 'self' 271 regExp.setInput(compiled.getInput()); 272 regExp.setGlobal(compiled.getGlobal()); 273 regExp.setIgnoreCase(compiled.getIgnoreCase()); 274 regExp.setMultiline(compiled.getMultiline()); 275 regExp.setPattern(compiled.getPattern()); 276 regExp.setGroupsInNegativeLookahead(compiled.getGroupsInNegativeLookahead()); 277 278 // Some implementations return undefined. Some return 'self'. Since return 279 // value is most likely be ignored, we can play safe and return 'self'. 280 return regExp; 281 } 282 283 /** 284 * ECMA 15.10.6.2 RegExp.prototype.exec(string) 285 * 286 * @param self self reference 287 * @param string string to match against regexp 288 * @return array containing the matches or {@code null} if no match 289 */ 290 @Function(attributes = Attribute.NOT_ENUMERABLE) 291 public static Object exec(final Object self, final Object string) { 292 return checkRegExp(self).exec(JSType.toString(string)); 293 } 294 295 /** 296 * ECMA 15.10.6.3 RegExp.prototype.test(string) 297 * 298 * @param self self reference 299 * @param string string to test for matches against regexp 300 * @return true if matches found, false otherwise 301 */ 302 @Function(attributes = Attribute.NOT_ENUMERABLE) 303 public static Object test(final Object self, final Object string) { 304 return checkRegExp(self).test(JSType.toString(string)); 305 } 306 307 /** 308 * ECMA 15.10.6.4 RegExp.prototype.toString() 309 * 310 * @param self self reference 311 * @return string version of regexp 312 */ 313 @Function(attributes = Attribute.NOT_ENUMERABLE) 314 public static Object toString(final Object self) { 315 return checkRegExp(self).toString(); 316 } 317 318 /** 319 * ECMA 15.10.7.1 source 320 * 321 * @param self self reference 322 * @return the input string for the regexp 323 */ 324 @Getter(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE | Attribute.NOT_WRITABLE) 325 public static Object source(final Object self) { 326 return checkRegExp(self).input; 327 } 328 329 /** 330 * ECMA 15.10.7.2 global 331 * 332 * @param self self reference 333 * @return true if this regexp is flagged global, false otherwise 334 */ 335 @Getter(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE | Attribute.NOT_WRITABLE) 336 public static Object global(final Object self) { 337 return checkRegExp(self).global; 338 } 339 340 /** 341 * ECMA 15.10.7.3 ignoreCase 342 * 343 * @param self self reference 344 * @return true if this regexp if flagged to ignore case, false otherwise 345 */ 346 @Getter(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE | Attribute.NOT_WRITABLE) 347 public static Object ignoreCase(final Object self) { 348 return checkRegExp(self).ignoreCase; 349 } 350 351 /** 352 * ECMA 15.10.7.4 multiline 353 * 354 * @param self self reference 355 * @return true if this regexp is flagged to be multiline, false otherwise 356 */ 357 @Getter(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE | Attribute.NOT_WRITABLE) 358 public static Object multiline(final Object self) { 359 return checkRegExp(self).multiline; 360 } 361 362 private RegExpMatch execInner(final String string) { 363 if (this.pattern == null) { 364 return null; // never matches or similar, e.g. a[] 365 } 366 367 final Matcher matcher = pattern.matcher(string); 368 final int start = this.global ? getLastIndex() : 0; 369 370 if (start < 0 || start > string.length()) { 371 setLastIndex(0); 372 return null; 373 } 374 375 if (!matcher.find(start)) { 376 setLastIndex(0); 377 return null; 378 } 379 380 if (global) { 381 setLastIndex(matcher.end()); 382 } 383 384 return new RegExpMatch(string, matcher.start(), groups(matcher)); 385 } 386 387 /** 388 * Convert java.util.regex.Matcher groups to JavaScript groups. 389 * That is, replace null and groups that didn't match with undefined. 390 */ 391 private Object[] groups(final Matcher matcher) { 392 final int groupCount = matcher.groupCount(); 393 final Object[] groups = new Object[groupCount + 1]; 394 for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) { 395 final int groupStart = matcher.start(i); 396 if (lastGroupStart > groupStart 397 || (groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i))) { 398 // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated. 399 // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere 400 // in the pattern always return undefined because the negative lookahead must fail. 401 groups[i] = UNDEFINED; 402 continue; 403 } 404 final String group = matcher.group(i); 405 groups[i] = group == null ? UNDEFINED : group; 406 lastGroupStart = groupStart; 407 } 408 return groups; 409 } 410 411 /** 412 * Executes a search for a match within a string based on a regular 413 * expression. It returns an array of information or null if no match is 414 * found. 415 * 416 * @param string String to match. 417 * @return NativeArray of matches, string or null. 418 */ 419 public Object exec(final String string) { 420 final RegExpMatch m = execInner(string); 421 // the input string 422 if (m == null) { 423 return null; 424 } 425 426 return new NativeRegExpExecResult(m); 427 } 428 429 /** 430 * Executes a search for a match within a string based on a regular 431 * expression. 432 * 433 * @param string String to match. 434 * @return True if a match is found. 435 */ 436 public Object test(final String string) { 437 return exec(string) != null; 438 } 439 440 /** 441 * Searches and replaces the regular expression portion (match) with the 442 * replaced text instead. For the "replacement text" parameter, you can use 443 * the keywords $1 to $2 to replace the original text with values from 444 * sub-patterns defined within the main pattern. 445 * 446 * @param string String to match. 447 * @param replacement Replacement string. 448 * @return String with substitutions. 449 */ 450 Object replace(final String string, final String replacement, final ScriptFunction function) { 451 final Matcher matcher = pattern.matcher(string); 452 /* 453 * $$ -> $ 454 * $& -> the matched substring 455 * $` -> the portion of string that preceeds matched substring 456 * $' -> the portion of string that follows the matched substring 457 * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit 458 * $nn -> the nnth capture, where nn is a two digit decimal number [01-99]. 459 */ 460 String replace = replacement; 461 462 if (!global) { 463 if (!matcher.find()) { 464 return string; 465 } 466 467 final StringBuilder sb = new StringBuilder(); 468 if (function != null) { 469 replace = callReplaceValue(function, matcher, string); 470 } 471 appendReplacement(matcher, string, replace, sb, 0); 472 sb.append(string, matcher.end(), string.length()); 473 return sb.toString(); 474 } 475 476 int end = 0; // a.k.a. lastAppendPosition 477 setLastIndex(0); 478 479 boolean found; 480 try { 481 found = matcher.find(end); 482 } catch (final IndexOutOfBoundsException e) { 483 found = false; 484 } 485 486 if (!found) { 487 return string; 488 } 489 490 int previousLastIndex = 0; 491 final StringBuilder sb = new StringBuilder(); 492 do { 493 if (function != null) { 494 replace = callReplaceValue(function, matcher, string); 495 } 496 appendReplacement(matcher, string, replace, sb, end); 497 end = matcher.end(); 498 499 // ECMA 15.5.4.10 String.prototype.match(regexp) 500 final int thisIndex = end; 501 if (thisIndex == previousLastIndex) { 502 setLastIndex(thisIndex + 1); 503 previousLastIndex = thisIndex + 1; 504 } else { 505 previousLastIndex = thisIndex; 506 } 507 } while (matcher.find()); 508 509 sb.append(string, end, string.length()); 510 511 return sb.toString(); 512 } 513 514 private void appendReplacement(final Matcher matcher, final String text, final String replacement, final StringBuilder sb, final int lastAppendPosition) { 515 // Process substitution string to replace group references with groups 516 int cursor = 0; 517 final StringBuilder result = new StringBuilder(); 518 Object[] groups = null; 519 520 while (cursor < replacement.length()) { 521 char nextChar = replacement.charAt(cursor); 522 if (nextChar == '$') { 523 // Skip past $ 524 cursor++; 525 nextChar = replacement.charAt(cursor); 526 final int firstDigit = nextChar - '0'; 527 528 if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) { 529 // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit. 530 int refNum = firstDigit; 531 cursor++; 532 if (cursor < replacement.length() && firstDigit < matcher.groupCount()) { 533 final int secondDigit = replacement.charAt(cursor) - '0'; 534 if ((secondDigit >= 0) && (secondDigit <= 9)) { 535 final int newRefNum = (firstDigit * 10) + secondDigit; 536 if (newRefNum <= matcher.groupCount() && newRefNum > 0) { 537 // $nn ($01-$99) 538 refNum = newRefNum; 539 cursor++; 540 } 541 } 542 } 543 if (refNum > 0) { 544 if (groups == null) { 545 groups = groups(matcher); 546 } 547 // Append group if matched. 548 if (groups[refNum] != UNDEFINED) { 549 result.append((String) groups[refNum]); 550 } 551 } else { // $0. ignore. 552 assert refNum == 0; 553 result.append("$0"); 554 } 555 } else if (nextChar == '$') { 556 result.append('$'); 557 cursor++; 558 } else if (nextChar == '&') { 559 result.append(matcher.group()); 560 cursor++; 561 } else if (nextChar == '`') { 562 result.append(text.substring(0, matcher.start())); 563 cursor++; 564 } else if (nextChar == '\'') { 565 result.append(text.substring(matcher.end())); 566 cursor++; 567 } else { 568 // unknown substitution or $n with n>m. skip. 569 result.append('$'); 570 } 571 } else { 572 result.append(nextChar); 573 cursor++; 574 } 575 } 576 // Append the intervening text 577 sb.append(text, lastAppendPosition, matcher.start()); 578 // Append the match substitution 579 sb.append(result); 580 } 581 582 private String callReplaceValue(final ScriptFunction function, final Matcher matcher, final String string) { 583 final Object[] groups = groups(matcher); 584 final Object[] args = Arrays.copyOf(groups, groups.length + 2); 585 586 args[groups.length] = matcher.start(); 587 args[groups.length + 1] = string; 588 589 final Object self = function.isStrict() ? UNDEFINED : Global.instance(); 590 591 return JSType.toString(ScriptRuntime.apply(function, self, args)); 592 } 593 594 /** 595 * Breaks up a string into an array of substrings based on a regular 596 * expression or fixed string. 597 * 598 * @param string String to match. 599 * @param limit Split limit. 600 * @return Array of substrings. 601 */ 602 Object split(final String string, final long limit) { 603 return split(this, string, limit); 604 } 605 606 private static Object split(final NativeRegExp regexp0, final String input, final long limit) { 607 final List<Object> matches = new ArrayList<>(); 608 609 final NativeRegExp regexp = new NativeRegExp(regexp0); 610 regexp.setGlobal(true); 611 612 if (limit == 0L) { 613 return new NativeArray(); 614 } 615 616 RegExpMatch match; 617 final int inputLength = input.length(); 618 int lastLength = -1; 619 int lastLastIndex = 0; 620 621 while ((match = regexp.execInner(input)) != null) { 622 final int lastIndex = match.getIndex() + match.length(); 623 624 if (lastIndex > lastLastIndex) { 625 matches.add(input.substring(lastLastIndex, match.getIndex())); 626 if (match.getGroups().length > 1 && match.getIndex() < inputLength) { 627 matches.addAll(Arrays.asList(match.getGroups()).subList(1, match.getGroups().length)); 628 } 629 630 lastLength = match.length(); 631 lastLastIndex = lastIndex; 632 633 if (matches.size() >= limit) { 634 break; 635 } 636 } 637 638 // bump the index to avoid infinite loop 639 if (regexp.getLastIndex() == match.getIndex()) { 640 regexp.setLastIndex(match.getIndex() + 1); 641 } 642 } 643 644 if (matches.size() < limit) { 645 // check special case if we need to append an empty string at the 646 // end of the match 647 // if the lastIndex was the entire string 648 if (lastLastIndex == input.length()) { 649 if (lastLength > 0 || regexp.test("") == Boolean.FALSE) { 650 matches.add(""); 651 } 652 } else { 653 matches.add(input.substring(lastLastIndex, inputLength)); 654 } 655 } 656 657 return new NativeArray(matches.toArray()); 658 } 659 660 /** 661 * Tests for a match in a string. It returns the index of the match, or -1 662 * if not found. 663 * 664 * @param string String to match. 665 * @return Index of match. 666 */ 667 Object search(final String string) { 668 final Matcher matcher = pattern.matcher(string); 669 670 int start = 0; 671 if (global) { 672 start = getLastIndex(); 673 } 674 675 start = matcher.find(start) ? matcher.start() : -1; 676 677 if (global) { 678 setLastIndex(matcher.end()); 679 } 680 681 return start; 682 } 683 684 /** 685 * Fast lastIndex getter 686 * @return last index property as int 687 */ 688 public int getLastIndex() { 689 return JSType.toInt32(lastIndex); 690 } 691 692 /** 693 * Fast lastIndex getter 694 * @return last index property as boxed integer 695 */ 696 public Object getLastIndexObject() { 697 return lastIndex; 698 } 699 700 /** 701 * Fast lastIndex setter 702 * @param lastIndex lastIndex 703 */ 704 public void setLastIndex(final int lastIndex) { 705 this.lastIndex = JSType.toObject(lastIndex); 706 } 707 708 private void init() { 709 this.setProto(Global.instance().getRegExpPrototype()); 710 } 711 712 private static NativeRegExp checkRegExp(final Object self) { 713 Global.checkObjectCoercible(self); 714 if (self instanceof NativeRegExp) { 715 return (NativeRegExp)self; 716 } else if (self != null && self == Global.instance().getRegExpPrototype()) { 717 return Global.instance().DEFAULT_REGEXP; 718 } else { 719 typeError("not.a.regexp", ScriptRuntime.safeToString(self)); 720 return null; 721 } 722 } 723 724 private String getInput() { 725 return input; 726 } 727 728 private void setInput(final String input) { 729 this.input = input; 730 } 731 732 boolean getGlobal() { 733 return global; 734 } 735 736 private void setGlobal(final boolean global) { 737 this.global = global; 738 } 739 740 private boolean getIgnoreCase() { 741 return ignoreCase; 742 } 743 744 private void setIgnoreCase(final boolean ignoreCase) { 745 this.ignoreCase = ignoreCase; 746 } 747 748 private boolean getMultiline() { 749 return multiline; 750 } 751 752 private void setMultiline(final boolean multiline) { 753 this.multiline = multiline; 754 } 755 756 private Pattern getPattern() { 757 return pattern; 758 } 759 760 private void setPattern(final Pattern pattern) { 761 this.pattern = pattern; 762 } 763 764 private BitVector getGroupsInNegativeLookahead() { 765 return groupsInNegativeLookahead; 766 } 767 768 private void setGroupsInNegativeLookahead(final BitVector groupsInNegativeLookahead) { 769 this.groupsInNegativeLookahead = groupsInNegativeLookahead; 770 } 771 772 }