• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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