Changeset 30
- Timestamp:
- 12/31/06 11:53:18 (2 years ago)
- Files:
-
- lex/trunk (modified) (1 prop)
- lex/trunk/src/com/qwirx/lex/HebrewConverter.java (added)
- lex/trunk/src/com/qwirx/lex/parser/Chart.java (added)
- lex/trunk/src/com/qwirx/lex/parser/Edge.java (modified) (1 diff)
- lex/trunk/src/com/qwirx/lex/parser/EdgeBase.java (modified) (2 diffs)
- lex/trunk/src/com/qwirx/lex/parser/Parser.java (modified) (4 diffs)
- lex/trunk/src/com/qwirx/lex/parser/Rule.java (modified) (11 diffs)
- lex/trunk/src/com/qwirx/lex/parser/RuleEdge.java (modified) (12 diffs)
- lex/trunk/src/com/qwirx/lex/parser/WordEdge.java (modified) (4 diffs)
- lex/trunk/test/com/qwirx/lex/ParserTest.java (modified) (31 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
lex/trunk
- Property svn:ignore set to
work
- Property svn:ignore set to
lex/trunk/src/com/qwirx/lex/parser/Edge.java
r26 r30 29 29 Edge getBoundCopy(); 30 30 Edge getUnboundCopy(); 31 32 boolean isAt(int pos); 31 33 } lex/trunk/src/com/qwirx/lex/parser/EdgeBase.java
r26 r30 71 71 * @param b The second Instance (actual) 72 72 */ 73 /* 73 74 public boolean equals(Object object) 74 75 { … … 142 143 return true; 143 144 } 145 */ 144 146 } lex/trunk/src/com/qwirx/lex/parser/Parser.java
r26 r30 9 9 import java.sql.ResultSet; 10 10 import java.sql.SQLException; 11 import java.util.ArrayList; 11 12 import java.util.Iterator; 12 13 import java.util.List; 13 import java.util.Stack;14 14 import java.util.Vector; 15 15 16 16 import com.qwirx.lex.DatabaseException; 17 import com.qwirx.lex.parser.Rule.Match;18 17 import com.qwirx.lex.sql.SqlDatabase; 19 18 … … 24 23 * Window - Preferences - Java - Code Style - Code Templates 25 24 */ 26 public class Parser { 25 public class Parser 26 { 27 27 private Rule [] rules; 28 28 29 public Parser(SqlDatabase db) throws DatabaseException, SQLException { 29 public Parser(SqlDatabase db) throws DatabaseException, SQLException 30 { 30 31 Vector ruleV = new Vector(); 31 32 … … 57 58 } 58 59 59 public Edge[][]parse(String input)60 public Chart parse(String input) 60 61 { 61 62 String[] words = input.split(" "); 62 63 Vector stacks = new Vector(); 64 stacks.add(new Stack()); 63 Chart chart = new Chart(words.length); 65 64 66 65 for (int i = 0; i < words.length; i++) 67 66 { 68 for (int s = 0; s < stacks.size(); s++) 67 chart.add(new WordEdge(words[i], i)); 68 69 for (int r = 0; r < rules.length; r++) 69 70 { 70 Stack stack = (Stack)( stacks.get(s) ); 71 stack.push(new WordEdge(words[i])); 72 } 71 /* 72 if (r == 4 && s == 5) 73 { 74 System.out.println("boo"); 75 } 76 */ 73 77 74 for (int s = 0; s < stacks.size(); s++) 75 { 76 Stack oldStack = (Stack)( stacks.get(s) ); 77 78 for (int r = 0; r < rules.length; r++) 79 { 80 /* 81 if (r == 4 && s == 5) 82 { 83 System.out.println("boo"); 84 } 85 */ 86 87 Rule rule = rules[r]; 88 List matches = rule.applyTo(oldStack); 89 90 for (Iterator m = matches.iterator(); m.hasNext(); ) 91 { 92 Match match = (Match)( m.next() ); 93 // System.out.println(stacks.size() + ": " + match.stack); 94 stacks.add(match.stack); 95 } 96 } 78 Rule rule = rules[r]; 79 List newEdges = rule.applyTo(chart, i); 80 chart.add(newEdges); 97 81 } 98 82 } 99 83 100 84 System.out.println("Parse finished."); 101 Edge[][] results = new Edge[stacks.size()][]; 102 103 for (int i = 0; i < stacks.size(); i++) 104 { 105 Stack stack = (Stack)( stacks.get(i) ); 106 // System.out.println("Stack "+(i+1)+" contains:"); 107 108 Edge[] thisResult = new Edge[stack.size()]; 109 110 for (int j = 0; j < stack.size(); j++) 111 { 112 // System.out.println(j+": "+ 113 // stack.get(j).toString()); 114 thisResult[j] = (Edge)( stack.get(j) ); 115 } 116 117 results[i] = thisResult; 118 } 119 120 return results; 85 86 return chart; 121 87 } 122 88 123 public Edge[][]parseFor(String input, String goal)89 public List parseFor(String input, String goal) 124 90 { 125 91 return parseFor(input, goal, false); 126 92 } 127 93 94 /* 128 95 private String getStringFromArray(Object [] array) 129 96 { … … 141 108 return sb.toString(); 142 109 } 110 */ 143 111 144 public Edge[][]parseFor(String input, String goal, boolean verbose)112 public List parseFor(String input, String goal, boolean verbose) 145 113 { 146 Edge[][] parseResults= parse(input);114 Chart chart = parse(input); 147 115 148 Vector results = new Vector(); 149 for (int i = 0; i < parseResults.length; i++) 116 List edges = chart.getEdges(); 117 List goals = new ArrayList(); 118 119 for (Iterator i = edges.iterator(); i.hasNext(); ) 150 120 { 151 Edge[] result = parseResults[i];152 if (result.length > 1) 153 {121 Edge edge = (Edge)( i.next() ); 122 if (! edge.symbol().equals(goal)) 123 { 154 124 if (verbose) 155 125 { 156 System.out.println("Rejected stack " + (i+1) + 157 ": too long: " + getStringFromArray(result)); 126 System.out.println("Rejected non-goal edge: " + edge); 158 127 } 159 continue; 160 } 161 162 String symbol = result[0].symbol(); 163 if (! symbol.equals(goal)) 164 { 165 if (verbose) 166 { 167 System.out.println("Rejected stack " + (i+1) + 168 ": not " + goal + ": " + getStringFromArray(result)); 169 } 128 170 129 continue; 171 130 } 172 131 173 results.add(result);132 boolean hasHoles = false; 174 133 175 if (!verbose)134 for (int j = 0; j < chart.getWidth(); j++) 176 135 { 136 if (! edge.isAt(j)) 137 { 138 hasHoles = true; 139 break; 140 } 141 } 142 143 if (hasHoles) 144 { 145 if (verbose) 146 { 147 System.out.println("Rejected edge with holes: " + edge); 148 } 177 149 continue; 178 150 } 179 151 180 System.out.println("Accepted stack "+(i+1)+":");181 182 for (int j = 0; j < result.length; j++)152 goals.add(edge); 153 154 if (verbose) 183 155 { 184 if (result[j] instanceof RuleEdge) 185 { 186 RuleEdge instance = (RuleEdge)( result[j] ); 187 StringBuffer buf = new StringBuffer(); 188 instance.appendStringPrettyPrint(buf); 189 System.out.println(buf.toString()); 190 } 191 else 192 { 193 System.out.print(result[j]); 194 System.out.print(" "); 195 } 196 } 197 198 System.out.println(""); 156 System.out.println("Accepted edge: " + edge); 157 } 199 158 } 200 159 201 Edge[][] resultArray = new Edge[results.size()][]; 202 resultArray = (Edge[][])( results.toArray(resultArray) ); 203 return resultArray; 160 return goals; 204 161 } 205 162 } lex/trunk/src/com/qwirx/lex/parser/Rule.java
r26 r30 7 7 package com.qwirx.lex.parser; 8 8 9 import java.util.ArrayList; 9 10 import java.util.Hashtable; 10 11 import java.util.Iterator; … … 24 25 private final boolean m_isPermutable; 25 26 private final boolean m_isSearching; 27 private final List [] m_PartBindingLists; 28 private final boolean [] m_PartsFinished; 29 private boolean [] m_PositionsUsed; 26 30 27 31 public Rule … … 37 41 this.fixedAttrs = fixedAttrs; 38 42 this.copyAttrs = copyAttrs; 39 this.m_isPermutable = isPermutable; 40 this.m_isSearching = isSearching; 43 44 m_isPermutable = isPermutable; 45 m_isSearching = isSearching; 46 m_PartBindingLists = new List [right.length]; 47 m_PartsFinished = new boolean [right.length]; 48 49 for (int i = 0; i < right.length; i++) 50 { 51 m_PartBindingLists[i] = new ArrayList(); 52 m_PartsFinished[i] = false; 53 } 41 54 } 42 55 … … 207 220 208 221 private int m_NumPartsRemaining; 209 private boolean [] m_PartsAlreadyUsed;210 222 211 223 private void hidePart(int index) 212 224 { 213 if (m_Parts AlreadyUsed[index])225 if (m_PartsFinished[index]) 214 226 { 215 227 throw new AssertionError("the current rule part should " + 216 228 "not be marked to be skipped"); 217 229 } 218 m_Parts AlreadyUsed[index] = true;230 m_PartsFinished[index] = true; 219 231 220 232 if (m_NumPartsRemaining <= 0) … … 228 240 private void unhidePart(int index) 229 241 { 230 if (!m_Parts AlreadyUsed[index])242 if (!m_PartsFinished[index]) 231 243 { 232 244 throw new AssertionError("the current rule part should " + 233 245 "be marked to be skipped"); 234 246 } 235 m_Parts AlreadyUsed[index] = false;247 m_PartsFinished[index] = false; 236 248 237 249 if (m_NumPartsRemaining >= right.length) … … 243 255 } 244 256 257 private Chart m_CurrentChart; 258 259 /** 260 * Public entry point for parsing. 261 * @param chart A chart containing existing edges to apply to 262 * @param endPos The right-hand side of the chart, where this rule is 263 * required to match this time (it may match elsewhere as well) 264 * @return A list of new Edges to be added to the Chart 265 */ 266 public List applyTo(Chart chart, int endPos) 267 { 268 m_CurrentChart = chart; 269 270 // debugging code 271 for (int i = 0; i < right.length; i++) 272 { 273 if (m_PartBindingLists[i].size() > 0) 274 { 275 throw new AssertionError 276 ( 277 "the part binding list should be empty at the start of " + 278 "the parse" 279 ); 280 } 281 282 if (m_PartsFinished[i]) 283 { 284 throw new AssertionError 285 ( 286 "no parts should be already used at the start of the parse" 287 ); 288 289 } 290 } 291 292 m_PositionsUsed = new boolean[endPos + 1]; 293 294 // we must always match an edge at the specified end position, 295 // which will be different every time we are called, 296 // otherwise we will generate duplicate edges. 297 298 List oldEdges = chart.getEdgesAt(endPos); 299 List newEdges = new ArrayList(); 300 301 if (m_isSearching || m_isPermutable) 302 { 303 // any part can match one or more edges 304 305 } 306 else 307 { 308 // the last part must match one or more edges, 309 // unless it's optional, in which case it might be skipped 310 // and the last-but-one might match, etc. 311 312 int firstPartSkipped = right.length; 313 314 for (int partIndex = right.length - 1; partIndex >= 0; partIndex--) 315 { 316 RulePart part = right[partIndex]; 317 318 for (Iterator i = oldEdges.iterator(); i.hasNext(); ) 319 { 320 Edge edge = (Edge)( i.next() ); 321 if (part.matches(edge)) 322 { 323 bindAndContinueParse(partIndex, edge, newEdges); 324 } 325 } 326 327 if (!part.canSkip()) 328 { 329 break; 330 } 331 332 m_PartsFinished[partIndex] = true; 333 firstPartSkipped = partIndex; 334 } 335 336 for (int i = firstPartSkipped; i < right.length; i++) 337 { 338 if (!m_PartsFinished[i]) throw new AssertionError(); 339 m_PartsFinished[i] = false; 340 } 341 } 342 343 return newEdges; 344 } 345 346 private void bindAndContinueParse(int partIndex, Edge edge, List newEdges) 347 { 348 for (int i = 0; i < right.length; i++) 349 { 350 if (m_PartBindingLists[i].contains(edge)) 351 { 352 throw new AssertionError 353 ( 354 "No part binding list should already contain this edge" 355 ); 356 } 357 } 358 359 m_PartBindingLists[partIndex].add(0, edge); 360 361 if (m_PartsFinished[partIndex]) 362 { 363 throw new AssertionError 364 ( 365 "Not allowed to bind a rule part that is already finished" 366 ); 367 } 368 369 for (int i = 0; i < m_PositionsUsed.length; i++) 370 { 371 if (!edge.isAt(i)) 372 { 373 continue; 374 } 375 376 if (m_PositionsUsed[i]) 377 { 378 throw new AssertionError 379 ( 380 "Not allowed to bind an edge that overlaps an existing " + 381 "bound edge" 382 ); 383 } 384 385 m_PositionsUsed[i] = true; 386 } 387 388 if (right[partIndex].canRepeat()) 389 { 390 continueParseWithThisPart(partIndex, true, newEdges); 391 } 392 else 393 { 394 m_PartsFinished[partIndex] = true; 395 continueParseWithNextPart(newEdges); 396 m_PartsFinished[partIndex] = false; 397 } 398 399 for (int i = 0; i < m_PositionsUsed.length; i++) 400 { 401 if (!edge.isAt(i)) 402 { 403 continue; 404 } 405 406 if (!m_PositionsUsed[i]) 407 { 408 throw new AssertionError 409 ( 410 "Positions used by this edge should be marked as used" 411 ); 412 } 413 414 m_PositionsUsed[i] = false; 415 } 416 417 for (int i = 0; i < right.length; i++) 418 { 419 if (i == partIndex) 420 { 421 if (!m_PartBindingLists[i].contains(edge)) 422 { 423 throw new AssertionError 424 ( 425 "The part binding list for this part should " + 426 "contain this edge" 427 ); 428 } 429 } 430 else 431 { 432 if (m_PartBindingLists[i].contains(edge)) 433 { 434 throw new AssertionError 435 ( 436 "No other part binding list should contain this edge" 437 ); 438 } 439 } 440 } 441 442 m_PartBindingLists[partIndex].remove(edge); 443 } 444 445 private void continueParseWithNextPart(List newEdges) 446 { 447 boolean foundUnfinishedPart = false; 448 449 for (int i = right.length - 1; i >= 0; i--) 450 { 451 if (m_PartsFinished[i]) 452 { 453 continue; 454 } 455 456 foundUnfinishedPart = true; 457 458 continueParseWithThisPart(i, false, newEdges); 459 460 // reordering of elements in permutable rules happens here, 461 // by allowing the loop to proceed with the next unfinished part. 462 if (!m_isPermutable) 463 { 464 return; 465 } 466 } 467 468 if (foundUnfinishedPart) 469 { 470 return; 471 } 472 473 // base case: we end up here when all parts have been used, 474 // and we should be ready to create a new edge. 475 476 // don't create new edges which don't contain any other edges: 477 // we could keep doing that forever. 478 479 int numEdges = 0; 480 481 for (int i = 0; i < right.length; i++) 482 { 483 numEdges += m_PartBindingLists[i].size(); 484 } 485 486 if (numEdges == 0) 487 { 488 return; 489 } 490 491 int position = 0; 492 Edge[] contained = new Edge[numEdges]; 493 494 for (int i = 0; i < right.length; i++) 495 { 496 for (Iterator j = m_PartBindingLists[i].iterator(); j.hasNext(); ) 497 { 498 Edge edge = (Edge)( j.next() ); 499 Edge unbound = edge.getUnboundCopy(); 500 try 501 { 502 unbound.bindTo(null, right[i]); 503 } 504 catch (AlreadyBoundException e) 505 { 506 throw new AssertionError(e); 507 } 508 contained[position++] = unbound; 509 } 510 } 511 512 if (position != numEdges) 513 { 514 throw new AssertionError(); 515 } 516 517 Edge newEdge = new RuleEdge(this, contained); 518 newEdges.add(newEdge); 519 } 520 521 private void continueParseWithThisPart(int partIndex, boolean hasMatched, 522 List newEdges) 523 { 524 if (m_PartsFinished[partIndex]) 525 { 526 throw new AssertionError 527 ( 528 "Not allowed to bind a rule part that is already finished" 529 ); 530 } 531 532 // find any other edges that we can use 533 534 int rightmostEmptyPos = -1; 535 for (int i = m_PositionsUsed.length - 1; i >= 0; i--) 536 { 537 if (!m_PositionsUsed[i]) 538 { 539 rightmostEmptyPos = i; 540 break; 541 } 542 } 543 544 List edges = m_CurrentChart.getEdgesAt(rightmostEmptyPos); 545 RulePart part = right[partIndex]; 546 547 for (Iterator i = edges.iterator(); i.hasNext(); ) 548 { 549 Edge edge = (Edge)( i.next() ); 550 if (!part.matches(edge)) 551 { 552 continue; 553 } 554 555 // check that the edge doesn't overlap any previously chosen ones 556 boolean overlaps = false; 557 for (int j = rightmostEmptyPos + 1; 558 j < m_CurrentChart.getWidth(); j++) 559 { 560 if (edge.isAt(j)) 561 { 562 overlaps = true; 563 break; 564 } 565 } 566 567 if (overlaps == true) 568 { 569 continue; 570 } 571 572 bindAndContinueParse(partIndex, edge, newEdges); 573 } 574 575 // Optional elements may only be skipped (match 0 times) 576 // in permutable rules if no matches have yet been made. 577 // This enforces the condition that they may not match 578 // nothing at different places in the same input, which 579 // would produce duplicate trees in the output. 580 581 boolean canSkip = part.canSkip(); 582 583 if (canSkip && m_isPermutable) 584 { 585 for (int i = 0; i < m_PositionsUsed.length; i++) 586 { 587 if (m_PositionsUsed[i]) 588 { 589 canSkip = false; 590 break; 591 } 592 } 593 } 594 595 // stop now if this part is not optional and has not matched yet 596 if (!canSkip && !hasMatched) 597 { 598 return; 599 } 600 601 // mark it as finished, so we can move onto the next part 602 // (in the case where it is optional and being skipped) 603 604 m_PartsFinished[partIndex] = true; 605 606 continueParseWithNextPart(newEdges); 607 608 if (!m_PartsFinished[partIndex]) 609 { 610 throw new AssertionError("this part should still be marked" + 611 "as fininshed"); 612 } 613 614 m_PartsFinished[partIndex] = false; 615 } 616 245 617 private void apply(List stack, Match match, List out) 246 618 { … … 251 623 for (int i = 0; i < right.length; i++) 252 624 { 253 if (m_Parts AlreadyUsed[i])625 if (m_PartsFinished[i]) 254 626 { 255 627 realNumPartsRemaining--; … … 321 693 for (int i = right.length - 1; i >= 0; i--) 322 694 { 323 if (!m_Parts AlreadyUsed[i])695 if (!m_PartsFinished[i]) 324 696 { 325 697 firstRulePartIndex = i; … … 339 711 for (int i = firstRulePartIndex; i < right.length; i++) 340 712 { 341 if (m_Parts AlreadyUsed[i])713 if (m_PartsFinished[i]) 342 714 { 343 715 // the caller has processed this part, and asked us to skip it … … 528 900 for (int i = right.length - 1; i >= 0; i--) 529 901 { 530 if (!m_Parts AlreadyUsed[i])902 if (!m_PartsFinished[i]) 531 903 { 532 904 nextPartIndex = i; … … 566 938 Match match = new Match(right.length); 567 939 List out = new Vector(); 568 m_PartsAlreadyUsed = new boolean [right.length]; 940 for (int i = 0; i < right.length; i++) 941 { 942 m_PartsFinished[i] = false; 943 } 569 944 m_NumPartsRemaining = right.length; 570 945 apply(stack, match, out); lex/trunk/src/com/qwirx/lex/parser/RuleEdge.java
r26 r30 16 16 { 17 17 private final Edge [] parts; 18 private final Rule rule;18 private final Rule rule; 19 19 private Map attribs = new Hashtable(); 20 20 … … 54 54 super(); 55 55 this.rule = rule; 56 this.parts = new Edge [parts.length]; 56 57 this.parts = new Edge [parts.length]; 57 58 58 59 for (int i = 0; i < this.parts.length; i++) … … 104 105 } 105 106 106 public String toString() { 107 public String toString() 108 { 107 109 StringBuffer buf = new StringBuffer(); 108 110 appendString(buf); 109 111 return buf.toString(); 110 112 } 111 public void appendString(StringBuffer buf) { 113 114 public void appendString(StringBuffer buf) 115 { 112 116 buf.append("{"); 113 117 buf.append(rule().symbol()); … … 115 119 116 120 Map attribs = attributes(); 117 for (Iterator i = attribs.entrySet().iterator(); i.hasNext(); ) { 121 for (Iterator i = attribs.entrySet().iterator(); i.hasNext(); ) 122 { 118 123 Entry e = (Entry)( i.next() ); 119 124 buf.append(e.getKey()); … … 142 147 buf.append("}"); 143 148 } 144 public void appendStringPrettyPrint(StringBuffer buf) { 149 150 public void appendStringPrettyPrint(StringBuffer buf) 151 { 145 152 appendStringPrettyPrint(buf, ""); 146 153 } 147 private void appendStringPrettyPrint(StringBuffer buf, String indent) { 154 155 private void appendStringPrettyPrint(StringBuffer buf, String indent) 156 { 148 157 buf.append(indent); 149 158 buf.append("{"); … … 152 161 153 162 Map attribs = attributes(); 154 for (Iterator i = attribs.entrySet().iterator(); i.hasNext(); ) { 163 for (Iterator i = attribs.entrySet().iterator(); i.hasNext(); ) 164 { 155 165 Entry e = (Entry)( i.next() ); 156 166 buf.append(e.getKey()); … … 190 200 buf.append("}"); 191 201 } 192 public String[] words() { 202 203 public String[] words() 204 { 193 205 Vector v = new Vector(); 194 206 appendWords(v); 195 207 return (String[])( v.toArray(new String[v.size()]) ); 196 208 } 197 public void appendWords(Vector words) { 198 for (int i = 0; i < parts.length; i++) { 209 210 public void appendWords(Vector words) 211 { 212 for (int i = 0; i < parts.length; i++) 213 { 199 214 parts[i].appendWords(words); 200 215 } 201 216 } 202 public Edge[] parts() { 217 218 public Edge[] parts() 219 { 203 220 Edge [] result = new Edge[parts.length]; 204 221 System.arraycopy(parts, 0, result, 0, parts.length); … … 219 236 220 237 public Map attributes() { return attribs; } 221 private void findAttributes() { 238 private void findAttributes() 239 { 222 240 attribs = new Hashtable(); 223 241 … … 240 258 241 259 Edge partInstance = parts[p]; 260 242 261 Object value = partInstance.attributes().get(name); 243 if (value == null) { 262 263 if (value == null) 264 { 244 265 throw new IllegalStateException( 245 266 partInstance.toString()+ … … 247 268 name+" to copy into "+this); 248 269 } 270 249 271 Object oldValue = attribs.get(name); 250 if (oldValue != null && ! oldValue.equals(value)) { 272 273 if (oldValue != null && ! oldValue.equals(value)) 274 { 251 275 throw new IllegalStateException( 252 276 "Unable to unify "+oldValue+" and "+value+ 253 277 " for attribute "+name+" of "+this); 254 278 } 255 if (oldValue == null) { 279 280 if (oldValue == null) 281 { 256 282 attribs.put(name, value); 257 283 } … … 281 307 } 282 308 } 283 public int getDepthScore() { 309 310 public int getDepthScore() 311 { 284 312 return getDepthScore(1); 285 313 } 286 private int getDepthScore(int initialDepth) { 314 315 private int getDepthScore(int initialDepth) 316 { 287 317 int d = initialDepth; 288 for (int i = 0; i < parts.length; i++) { 289 if (parts[i] instanceof RuleEdge) { 318 for (int i = 0; i < parts.length; i++) 319 { 320 if (parts[i] instanceof RuleEdge) 321 { 290 322 d += ((RuleEdge)(parts[i])).getDepthScore(initialDepth+1); 291 323 } … … 293 325 return d; 294 326 } 327 295 328 public Edge getUnboundCopy() 296 329 { 297 330 return new RuleEdge(rule, parts); 298 331 } 332 333 public boolean isAt(int position) 334 { 335 for (int i = 0; i < parts.length; i++) 336 { 337 if (parts[i].isAt(position)) 338 { 339 return true; 340 } 341 } 342 343 return false; 344 } 299 345 } lex/trunk/src/com/qwirx/lex/parser/WordEdge.java
r26 r30 19 19 private static final Edge[] parts = new Edge[0]; 20 20 private static final ImmutableMap attribs = new
