1 /* 2 * Copyright (C) 2017 The Libphonenumber Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.i18n.phonenumbers.metadata.finitestatematcher.compiler; 18 19 import static com.google.common.collect.ImmutableList.toImmutableList; 20 import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap; 21 import static com.google.i18n.phonenumbers.metadata.RangeSpecification.ALL_DIGITS_MASK; 22 import static java.lang.Integer.numberOfTrailingZeros; 23 import static java.util.stream.Collectors.joining; 24 25 import com.google.common.base.Preconditions; 26 import com.google.common.collect.ImmutableList; 27 import com.google.common.collect.ImmutableMap; 28 import com.google.common.collect.ImmutableSet; 29 import com.google.common.collect.ImmutableSetMultimap; 30 import com.google.common.collect.Iterables; 31 import com.google.common.io.ByteArrayDataOutput; 32 import com.google.i18n.phonenumbers.metadata.RangeTree.DfaNode; 33 import com.google.i18n.phonenumbers.metadata.finitestatematcher.OpCode; 34 import com.google.i18n.phonenumbers.metadata.finitestatematcher.compiler.Statistics.Type; 35 import java.util.ArrayList; 36 import java.util.Collection; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Map.Entry; 40 41 /** 42 * A specific instance of a number matching operation derived from a DFA. Operations are created by 43 * analyzing a sequence in a DFA and knowing how to write the corresponding instruction(s) as bytes 44 * (to be processed by DigitSequenceMatcher or similar). 45 */ 46 abstract class Operation { 47 /** Represents the digits which can be accepted during matching operations. */ 48 private enum Digit { 49 // Order of enums must match the digit value itself (this is checked for in the constructor). 50 ZERO(0), ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8), NINE(9); 51 52 private static final Digit[] VALUES = values(); 53 54 // Iteration order is order of enum declaration (and thus also the value order). 55 public static final ImmutableSet<Digit> ALL = ImmutableSet.copyOf(VALUES); 56 Digit(int value)57 Digit(int value) { 58 // No need to store the digit value if we know it matches our ordinal value. 59 Preconditions.checkArgument(value == ordinal()); 60 } 61 62 /** Returns the digit corresponding to the integral value in the range {@code 0...9}. */ of(int n)63 public static Digit of(int n) { 64 return VALUES[n]; 65 } 66 67 /** 68 * Returns the set of digits corresponding to a bit-mask in which bits 0 to 9 represent the 69 * corresponding digits. 70 */ fromMask(int mask)71 public static ImmutableSet<Digit> fromMask(int mask) { 72 Preconditions.checkArgument(mask >= 1 && mask <= ALL_DIGITS_MASK); 73 if (mask == ALL_DIGITS_MASK) { 74 return ALL; 75 } 76 ImmutableSet.Builder<Digit> digits = ImmutableSet.builder(); 77 for (int n = 0; n <= 9; n++) { 78 if ((mask & (1 << n)) != 0) { 79 digits.add(VALUES[n]); 80 } 81 } 82 return digits.build(); 83 } 84 85 /** Returns the integer value of this digit instance. */ value()86 public int value() { 87 return ordinal(); 88 } 89 } 90 91 /** 92 * An invalid jump offset indicating that instead of jumping to a new instruction, the state 93 * machine can just terminate (used to avoid jumping directly to the termination instruction). 94 */ 95 static final int TERMINATION_OFFSET = -1; 96 97 /** The number of bytes required by a "long" branch instruction. */ 98 private static final int LONG_BRANCH_SIZE = 2; 99 100 private final boolean isTerminating; 101 private final boolean isBranching; 102 Operation(boolean isTerminating, boolean isBranching)103 private Operation(boolean isTerminating, boolean isBranching) { 104 this.isTerminating = isTerminating; 105 this.isBranching = isBranching; 106 } 107 108 /** Returns whether this operation can terminate the state machine when it has been reached. */ isTerminating()109 boolean isTerminating() { 110 return isTerminating; 111 } 112 113 /** 114 * Returns whether this operation is branching. A branching operation has more than one output 115 * node it can reach. 116 */ isBranching()117 boolean isBranching() { 118 return isBranching; 119 } 120 121 /** 122 * Returns the output nodes of this operation. For branching operations the order of multiple 123 * output nodes is defined by the operation itself (most operations are not branching and have 124 * only one output state anyway). 125 */ getOuts()126 abstract ImmutableList<DfaNode> getOuts(); 127 128 /** Returns the op-code for this operation, used when writing out instruction bytes. */ getOpCode()129 abstract OpCode getOpCode(); 130 131 /** Writes this operation out as a series of instruction bytes. */ writeImpl( ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats)132 abstract void writeImpl( 133 ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats); 134 writeTo(ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats)135 void writeTo(ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats) { 136 if (isTerminating()) { 137 stats.record(Type.TERMINATING); 138 } 139 writeImpl(out, offsetMap, stats); 140 } 141 142 /** 143 * Merges two adjacent operations (a poor man's compiler optimization). Useful for collapsing 144 * sequences of "ANY" operations. If this instruction cannot be merged with the given "next" 145 * instruction then it should return {@code null}, which is the default behavior. 146 * 147 * @param next the operation following this operation which we will try and merge with. 148 */ mergeWith(Operation next)149 Operation mergeWith(Operation next) { 150 return null; 151 } 152 153 /** Writes a branch instructions into the output byte sequence. */ writeBranch(ByteArrayDataOutput out, int jump, Statistics stats)154 static void writeBranch(ByteArrayDataOutput out, int jump, Statistics stats) { 155 Preconditions.checkArgument(jump >= 0 && jump < 0x1000, "invalid jump: " + jump); 156 if (jump == 0) { 157 stats.record(Type.CONTINUATION); 158 } else if (jump < 16) { 159 stats.record(Type.SHORT_BRANCH); 160 out.writeByte((OpCode.BRANCH.ordinal() << 5) | jump); 161 } else { 162 stats.record(jump < 0x100 ? Type.MEDIUM_BRANCH : Type.LONG_BRANCH); 163 out.writeShort((OpCode.BRANCH.ordinal() << 13) | (1 << 12) | jump); 164 } 165 } 166 167 /** Writes a termination byte into the output byte sequence. */ 168 static void writeTerminator(ByteArrayDataOutput out, Statistics stats) { 169 stats.record(Type.FINAL); 170 out.writeByte(0); 171 } 172 173 /** 174 * Creates a new operation to represent the output state transition given by {@code outMasks}. 175 * Note that where multiple nodes exist in {@code outMasks}, their ordering must be consistent 176 * with the {@code Mapping} operation (whereby nodes are ordered by the lowest bit set in the 177 * corresponding mask. 178 */ 179 static Operation from(boolean isTerminating, ImmutableMap<DfaNode, Integer> outMasks) { 180 if (outMasks.isEmpty()) { 181 // No out nodes; then it's a "Terminal" operation. 182 Preconditions.checkState(isTerminating); 183 return new Operation.Terminal(); 184 } 185 ImmutableList<DfaNode> outStates = outMasks.keySet().asList(); 186 if (outStates.size() == 1) { 187 DfaNode outState = Iterables.getOnlyElement(outStates); 188 int digitMask = outMasks.get(outState); 189 if (Integer.bitCount(digitMask) == 1) { 190 // One output state reached by a single input; then it's a "Single" operation. 191 return new Operation.Single(isTerminating, numberOfTrailingZeros(digitMask), outStates); 192 } 193 if (digitMask == ALL_DIGITS_MASK) { 194 // One output state reached by any input; then it's an "Any" operation. 195 return new Operation.Any(isTerminating, 1, outStates); 196 } 197 // One output state reached other general input; then it's a "Range" operation. 198 return new Operation.Range(isTerminating, digitMask, outStates); 199 } 200 if (outStates.size() == 2) { 201 // Test if the 2 disjoint masks cover all inputs. If so, use a shorter branch operation. 202 List<Integer> masks = outMasks.values().asList(); 203 if ((masks.get(0) | masks.get(1)) == ALL_DIGITS_MASK) { 204 // One of two output nodes reached by any input; then it's a branching "Range" operation. 205 return new Operation.Range(isTerminating, masks.get(0), outStates); 206 } 207 } 208 // Any other combination of nodes or inputs; then it's a "Mapping" operation. This code relies 209 // on the ordering of entries in the output map to correspond to edge order. 210 return new Operation.Mapping(isTerminating, outMasks); 211 } 212 213 /** Respresents a state with no legal outputs, which must be a terminal state in the matcher. */ 214 private static final class Terminal extends Operation { 215 Terminal() { 216 super(true, true); 217 } 218 219 @Override 220 OpCode getOpCode() { 221 return OpCode.BRANCH; 222 } 223 224 @Override 225 ImmutableList<DfaNode> getOuts() { 226 return ImmutableList.of(); 227 } 228 229 @Override 230 void writeImpl(ByteArrayDataOutput out, Map<DfaNode, Integer> unused, Statistics stats) { 231 writeTerminator(out, stats); 232 } 233 234 @Override 235 public String toString() { 236 return "TERMINAL"; 237 } 238 } 239 240 /** 241 * Respresents a state which can be transitioned from to a single output state via a single input 242 * (eg, "0" or "9"). 243 */ 244 private static final class Single extends Operation { 245 private final Digit digit; 246 private final ImmutableList<DfaNode> outs; 247 248 Single(boolean isTerminating, int digit, ImmutableList<DfaNode> outs) { 249 super(isTerminating, false); 250 Preconditions.checkArgument(outs.size() == 1); 251 this.digit = Digit.of(digit); 252 this.outs = outs; 253 } 254 255 @Override 256 OpCode getOpCode() { 257 return OpCode.SINGLE; 258 } 259 260 @Override ImmutableList<DfaNode> getOuts() { 261 return outs; 262 } 263 264 @Override 265 void writeImpl(ByteArrayDataOutput out, Map<DfaNode, Integer> unused, Statistics stats) { 266 // <--------- 1 byte ---------> 267 // [ OPCODE | TRM | VALUE ] 268 out.writeByte((getOpCode().ordinal() << 5) 269 | (isTerminating() ? (1 << 4) : 0) 270 | digit.value()); 271 } 272 273 @Override 274 public String toString() { 275 return format(digit.value()); 276 } 277 } 278 279 /** 280 * Respresents a state which can be transitioned from to a single output state via any input 281 * (ie, "\d"). Successive "Any" oeprations can be merged to represent a repeated sequence 282 * (eg, "\d{5}"). 283 */ 284 private static final class Any extends Operation { 285 private final int count; 286 private final ImmutableList<DfaNode> outs; 287 288 Any(boolean isTerminating, int count, ImmutableList<DfaNode> outs) { 289 super(isTerminating, false); 290 Preconditions.checkArgument(outs.size() == 1); 291 Preconditions.checkArgument(count > 0); 292 this.count = count; 293 this.outs = outs; 294 } 295 296 @Override 297 OpCode getOpCode() { 298 return OpCode.ANY; 299 } 300 301 @Override ImmutableList<DfaNode> getOuts() { 302 return outs; 303 } 304 305 @Override 306 void writeImpl(ByteArrayDataOutput out, Map<DfaNode, Integer> unused, Statistics stats) { 307 int remainingCount = count; 308 // <--------- 1 byte ---------> 309 // [ OPCODE | TRM | COUNT-1 ] 310 int anyN = (getOpCode().ordinal() << 5) | (isTerminating() ? (1 << 4) : 0); 311 while (remainingCount > 16) { 312 out.writeByte(anyN | 15); 313 remainingCount -= 16; 314 } 315 out.writeByte(anyN | remainingCount - 1); 316 } 317 318 @Override 319 public Operation mergeWith(Operation next) { 320 if (next.getOpCode() == OpCode.ANY && isTerminating() == next.isTerminating()) { 321 return new Any(isTerminating(), this.count + ((Any) next).count, ((Any) next).outs); 322 } 323 return null; 324 } 325 326 @Override 327 public String toString() { 328 return format(count); 329 } 330 } 331 332 /** 333 * Represents a state which can be transitioned from via an arbitrary set of inputs to either 334 * one or two output nodes (eg, "[23-69]" or "[0-4]X|[5-9]Y"). In the case where there are two 335 * output nodes, any input must reach one of the two possible nodes (ie, there is no invalid 336 * input). 337 */ 338 private static final class Range extends Operation { 339 private final ImmutableSet<Digit> digits; 340 private final ImmutableList<DfaNode> outs; 341 342 Range(boolean isTerminating, int digitMask, ImmutableList<DfaNode> outs) { 343 super(isTerminating, outs.size() == 2); 344 Preconditions.checkArgument(outs.size() <= 2); 345 this.digits = Digit.fromMask(digitMask); 346 this.outs = outs; 347 } 348 349 @Override 350 OpCode getOpCode() { 351 return OpCode.RANGE; 352 } 353 354 /** 355 * For branching Range operations (with 2 output nodes), the order is that the state matched 356 * by {@code digits} is the first state and the state reached by any other input is second. 357 */ 358 @Override ImmutableList<DfaNode> getOuts() { 359 return outs; 360 } 361 362 @Override 363 void writeImpl(ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats) { 364 // <-------------- 2 bytes --------------> <-------- 2 bytes ---------> 365 // [ OPCODE | TRM | 0 | BIT SET ] 366 // [ OPCODE | TRM | 1 | BIT SET | JUMP_IN | JUMP_OUT ] 367 out.writeShort((getOpCode().ordinal() << 13) 368 | (isTerminating() ? (1 << 12) : 0) 369 | (isBranching() ? (1 << 11) : 0) 370 | asBitMask(digits)); 371 if (isBranching()) { 372 writeJumpTable(out, ImmutableList.of( 373 offsetMap.get(outs.get(0)), offsetMap.get(outs.get(1))), stats); 374 } 375 } 376 377 @Override 378 public String toString() { 379 return format(asRangeString(digits)); 380 } 381 } 382 383 /** 384 * Represents a state in the matcher which can be transitioned from via an arbitrary set of 385 * inputs, to an arbitrary set of nodes. This is the most general form of operation and (apart 386 * from branches) provides the only truly necessary instruction in the matcher; everything else 387 * is just some specialization of this operation. 388 */ 389 private static final class Mapping extends Operation { 390 private final ImmutableSetMultimap<DfaNode, Digit> nodeMap; 391 392 Mapping(boolean isTerminating, ImmutableMap<DfaNode, Integer> outMasks) { 393 super(isTerminating, true); 394 this.nodeMap = outMasks.entrySet().stream() 395 .collect(flatteningToImmutableSetMultimap( 396 Entry::getKey, e -> Digit.fromMask(e.getValue()).stream())); 397 } 398 399 @Override 400 OpCode getOpCode() { 401 return isTerminating() ? OpCode.TMAP : OpCode.MAP; 402 } 403 404 /** 405 * For Mapping operations, output node order is defined by the lowest digit by which that 406 * node can be reached. For example, if a map operation can reach three nodes {@code A}, 407 * {@code B} and {@code C} via inputs in the ranges {@code [1-38]}, {@code [4-6]} and 408 * {@code [09]} respectively, then they will be ordered {@code (C, A, B)}. 409 */ 410 @Override ImmutableList<DfaNode> getOuts() { 411 return nodeMap.keySet().asList(); 412 } 413 414 @Override 415 void writeImpl(ByteArrayDataOutput out, Map<DfaNode, Integer> offsetMap, Statistics stats) { 416 // <------------ 4 bytes ------------> <-- 1 byte per offset ---> 417 // [ OPCODE | CODED MAP | JUMP_1 | ... | JUMP_N ] 418 out.writeInt((getOpCode().ordinal() << 29) | asCodedMap(nodeMap)); 419 ImmutableList<Integer> offsets = 420 getOuts().stream().map(offsetMap::get).collect(toImmutableList()); 421 writeJumpTable(out, offsets, stats); 422 } 423 424 @Override 425 public String toString() { 426 return format(nodeMap.asMap().values().stream() 427 .map(Operation::asRangeString).collect(joining(", "))); 428 } 429 } 430 431 String format(Object extra) { 432 return String.format("%s%s : %s", getOpCode(), isTerminating() ? "*" : "", extra); 433 } 434 435 /** 436 * Returns an integer with the lowest 10 bits set in accordance with the digits in the given set. 437 */ 438 private static int asBitMask(ImmutableSet<Digit> digits) { 439 int bitMask = 0; 440 for (Digit digit : digits) { 441 bitMask |= (1 << digit.value()); 442 } 443 return bitMask; 444 } 445 446 /** 447 * Returns a integer with the lowest 29 bits set to encode an arbitrary mapping from input digit 448 * to an output index. The 29 bits are partitioned such that lower inputs require fewer bits to 449 * encode (output indices are assigned as they are encountered, starting at the first input). 450 * Each digit can then be quickly mapped to either its 1-indexed output node, or 0 if the input 451 * was invalid. 452 */ 453 private static int asCodedMap(ImmutableSetMultimap<DfaNode, Digit> nodeMap) { 454 int codedMap = 0; 455 List<DfaNode> outs = nodeMap.keySet().asList(); 456 for (int n = 0; n < outs.size(); n++) { 457 for (Digit digit : nodeMap.get(outs.get(n))) { 458 // Coded indices are 1-to-10 (0 is the "invalid" node). 459 codedMap |= ((n + 1) << OpCode.getMapShift(digit.value())); 460 } 461 } 462 return codedMap; 463 } 464 465 /** 466 * Writes a sequence of offsets representing a unsigned byte-based jump table after either a 467 * Mapping or Range instruction. This accounts correctly for the need to introduce a new 468 * "trampoline" branch instruction after the jump table (when the desired offset is too large 469 * to fit in a single unsigned byte). 470 * <p> 471 * Offsets are either: 472 * <ul> 473 * <li>The number of bytes to jump from the end of the current {@code Sequence} bytes to the 474 * start of the destination {@code Sequence} bytes. 475 * <li>{@code -1} to indicate that a terminal node has been reached. 476 * </ul> 477 * <p> 478 * Note that the offset written into the jump table itself must be relative to the beginning of 479 * the jump table and so must be adjusted by the number of bytes in the jump table and any other 480 * branch instructions that follow it. This it probably the most awkward logic in the entire 481 * compiler. 482 */ 483 static void writeJumpTable(ByteArrayDataOutput out, List<Integer> offsets, 484 Statistics stats) { 485 int jumpTableSize = offsets.size(); 486 boolean needsExtraBranches = false; 487 for (int n = 0; n < jumpTableSize && !needsExtraBranches; n++) { 488 // Check whether the adjusted offset (ie, the one we would write) will fit in a byte. 489 // It's no issue to have offsets of -1 as it can never trigger "needsExtraBranches". 490 needsExtraBranches = (offsets.get(n) + jumpTableSize >= 0x100); 491 } 492 if (needsExtraBranches) { 493 // We only get here if at least one offset (after adjustment by the original jump table size) 494 // would not fit into a byte. Now we must calculate exactly how many extra branches we are 495 // going to need. For this we must assume the worst case adjustment of "3 x jumpTableSize" 496 // which is 1 byte for the jump table offset and 2 bytes for the extra branch for every entry. 497 // This is pessimistic because there will now be cases where we write a trampoline jump for 498 // an offset that could have fitted had we not assumed that we might need the extra space for 499 // the branch. However these cases are rare enough that we choose to ignore them. 500 int maxOffsetAdjust = ((1 + LONG_BRANCH_SIZE) * jumpTableSize); 501 int extraBranchCount = 0; 502 for (int n = 0; n < jumpTableSize; n++) { 503 if (offsets.get(n) + maxOffsetAdjust >= 0x100) { 504 extraBranchCount += 1; 505 } 506 } 507 // Now we know a reasonable upper bound for how many extra branches are needed, use this to 508 // adjust the actual offsets and write them. When a "trampoline" branch instruction is needed 509 // we split the offset so the jump table jumps to the branch instruction and that jumps the 510 // rest. Branch instructions are positioned, in order, immediately after the jump table. 511 List<Integer> extraBranchOffsets = new ArrayList<>(); 512 int totalOffsetAdjust = jumpTableSize + (LONG_BRANCH_SIZE * extraBranchCount); 513 for (int n = 0; n < jumpTableSize; n++) { 514 int offset = offsets.get(n); 515 if (offset >= 0) { 516 int worstCaseOffset = offset + maxOffsetAdjust; 517 // Get the actual total offset we want to jump by. 518 offset += totalOffsetAdjust; 519 // Use the worst case offset here so we repeat exactly the same decision as the loop 520 // above (otherwise we might add fewer branches which would screw up our offsets). 521 if (worstCaseOffset >= 0x100) { 522 // Split the original offset, recording the jump to the trampoline branch as well as 523 // the branch offset itself. Note that the offset adjustment changes as more trampoline 524 // branches are encountered (but the overall offset jumped remains the same). 525 int extraBranchIndex = extraBranchOffsets.size(); 526 // This offset will always be small (max jump table is 10 entries, so offset to the 527 // last possible branch will be at most 28 bytes). 528 int branchInstructionOffset = jumpTableSize + (LONG_BRANCH_SIZE * extraBranchIndex); 529 // Subtract one additional branch instruction here because when we trampoline jump, we 530 // jump to the start of the branch instruction, but jump away from the end of it. 531 extraBranchOffsets.add((offset - branchInstructionOffset) - LONG_BRANCH_SIZE); 532 offset = branchInstructionOffset; 533 } 534 // Write the total offset (offset must be < 0x100 here as worstCaseOffset was < 0x100). 535 Preconditions.checkState(offset < 0x100, "jump too long: %s", offset); 536 out.writeByte(offset); 537 } else { 538 // If the destination of this jump would just be a termination instruction, just write 539 // the termination byte here directly (no point jumping to the termination byte). 540 Preconditions.checkArgument(offset == TERMINATION_OFFSET, "bad offset: %s", offset); 541 writeTerminator(out, stats); 542 } 543 } 544 // Write out the trampoline jumps in the order they were found. 545 for (int offset : extraBranchOffsets) { 546 stats.record(Type.DOUBLE_JUMP); 547 Operation.writeBranch(out, offset, stats); 548 } 549 } else { 550 // In the simple case, there are no extra branches, so we just write the offsets we have. 551 // This has the same effect as running the code above with (extraBranchCount == 0) but can be 552 // reached more optimistically because we don't need to account for the worst case offset 553 // adjustment when deciding if it's safe to just use the offsets we were given. It's a form 554 // of hysteresis between the no-branch and extra-branch cases. 555 for (int n = 0; n < jumpTableSize; n++) { 556 int offset = offsets.get(n); 557 if (offset >= 0) { 558 offset += jumpTableSize; 559 Preconditions.checkState(offset < 0x100, "jump too long: " + offset); 560 out.writeByte(offset); 561 } else { 562 writeTerminator(out, stats); 563 } 564 } 565 } 566 } 567 568 // Helper function for asRanges() to print a single range (eg, "[014-7]"). 569 private static String asRangeString(Collection<Digit> digits) { 570 StringBuilder out = new StringBuilder(); 571 out.append("["); 572 Digit lhs = null; 573 Digit rhs = null; 574 for (Digit digit : digits) { 575 if (lhs != null) { 576 if (digit.value() == rhs.value() + 1) { 577 rhs = digit; 578 continue; 579 } 580 if (rhs != lhs) { 581 if (rhs.value() > lhs.value() + 1) { 582 out.append("-"); 583 } 584 out.append(rhs.value()); 585 } 586 } 587 lhs = digit; 588 rhs = digit; 589 out.append(lhs.value()); 590 } 591 if (rhs != lhs) { 592 if (rhs.value() > lhs.value() + 1) { 593 out.append("-"); 594 } 595 out.append(rhs.value()); 596 } 597 out.append("]"); 598 return out.toString(); 599 } 600 } 601