Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import "dart:typed_data"; | 5 import "dart:typed_data"; |
| 6 import "dart:collection" show HashMap, LinkedHashMap, Maps; | |
| 7 import "dart:_internal" show SubListIterable, makeListFixedLength; | |
| 6 | 8 |
| 7 // JSON conversion. | 9 // JSON conversion. |
| 8 | 10 |
| 9 patch _parseJson(String json, reviver(var key, var value)) { | 11 patch _parseJson(String json, reviver(var key, var value)) { |
| 10 _BuildJsonListener listener; | 12 _BuildJsonListener listener; |
| 11 if (reviver == null) { | 13 if (reviver == null) { |
| 12 listener = new _BuildJsonListener(); | 14 listener = new _BuildJsonListener(); |
| 13 } else { | 15 } else { |
| 14 listener = new _ReviverJsonListener(reviver); | 16 listener = new _ReviverJsonListener(reviver); |
| 15 } | 17 } |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 51 * started. If the container is a [Map], there is also a current [key] | 53 * started. If the container is a [Map], there is also a current [key] |
| 52 * which is also stored on the stack. | 54 * which is also stored on the stack. |
| 53 */ | 55 */ |
| 54 List stack = []; | 56 List stack = []; |
| 55 /** The current [Map] or [List] being built. */ | 57 /** The current [Map] or [List] being built. */ |
| 56 var currentContainer; | 58 var currentContainer; |
| 57 /** The most recently read property key. */ | 59 /** The most recently read property key. */ |
| 58 String key; | 60 String key; |
| 59 /** The most recently read value. */ | 61 /** The most recently read value. */ |
| 60 var value; | 62 var value; |
| 63 /** Cache for reusable hidden classes of objects. Start out in multi-mode. */ | |
| 64 static _JsonTransitionMap staticCache = | |
| 65 new _JsonMultiTransitionMap(const _JsonHiddenClass.empty(), | |
| 66 new HashMap()); | |
| 67 // Counts how many transitions have been added to the cache. | |
| 68 // Cache is cleared when reaching the max size. | |
| 69 static int staticCacheSize = 0; | |
| 70 static const int MAX_STATIC_CACHE_SIZE = 512; | |
| 61 | 71 |
| 62 /** Pushes the currently active container (and key, if a [Map]). */ | 72 /** Pushes the currently active container (and key, if a [Map]). */ |
| 63 void pushContainer() { | 73 void pushContainer() { |
| 64 if (currentContainer is Map) stack.add(key); | 74 if (key != null) |
| 75 if (currentContainer is _JsonObjectBuilder) stack.add(key); | |
| 65 stack.add(currentContainer); | 76 stack.add(currentContainer); |
| 66 } | 77 } |
| 67 | 78 |
| 68 /** Pops the top container from the [stack], including a key if applicable. */ | 79 /** Pops the top container from the [stack], including a key if applicable. */ |
| 69 void popContainer() { | 80 void popContainer() { |
| 70 value = currentContainer; | 81 value = currentContainer; |
| 71 currentContainer = stack.removeLast(); | 82 currentContainer = stack.removeLast(); |
| 72 if (currentContainer is Map) key = stack.removeLast(); | 83 if (currentContainer is _JsonObjectBuilder) key = stack.removeLast(); |
| 73 } | 84 } |
| 74 | 85 |
| 75 void handleString(String value) { this.value = value; } | 86 void handleString(String value) { this.value = value; } |
| 76 void handleNumber(num value) { this.value = value; } | 87 void handleNumber(num value) { this.value = value; } |
| 77 void handleBool(bool value) { this.value = value; } | 88 void handleBool(bool value) { this.value = value; } |
| 78 void handleNull() { this.value = null; } | 89 void handleNull() { this.value = null; } |
| 79 | 90 |
| 80 void beginObject() { | 91 void beginObject() { |
| 81 pushContainer(); | 92 pushContainer(); |
| 82 currentContainer = {}; | 93 currentContainer = new _JsonObjectBuilder(staticCache); |
| 83 } | 94 } |
| 84 | 95 |
| 85 void propertyName() { | 96 void propertyName() { |
| 86 key = value; | 97 key = value; |
| 87 value = null; | 98 value = null; |
| 88 } | 99 } |
| 89 | 100 |
| 90 void propertyValue() { | 101 void propertyValue() { |
| 91 Map map = currentContainer; | 102 _JsonObjectBuilder builder = currentContainer; |
| 92 map[key] = value; | 103 builder.add(key, value); |
| 93 key = value = null; | 104 key = value = null; |
| 94 } | 105 } |
| 95 | 106 |
| 96 void endObject() { | 107 void endObject() { |
| 108 _JsonObjectBuilder builder = currentContainer; | |
| 109 currentContainer = builder.toMap(); | |
| 110 staticCacheSize += builder.transitionsAdded; | |
| 97 popContainer(); | 111 popContainer(); |
| 98 } | 112 } |
| 99 | 113 |
| 100 void beginArray() { | 114 void beginArray() { |
| 101 pushContainer(); | 115 pushContainer(); |
| 102 currentContainer = []; | 116 currentContainer = []; |
| 103 } | 117 } |
| 104 | 118 |
| 105 void arrayElement() { | 119 void arrayElement() { |
| 106 List list = currentContainer; | 120 List list = currentContainer; |
| 107 currentContainer.add(value); | 121 currentContainer.add(value); |
| 108 value = null; | 122 value = null; |
| 109 } | 123 } |
| 110 | 124 |
| 111 void endArray() { | 125 void endArray() { |
| 112 popContainer(); | 126 popContainer(); |
| 113 } | 127 } |
| 114 | 128 |
| 115 /** Read out the final result of parsing a JSON string. */ | 129 /** Read out the final result of parsing a JSON string. */ |
| 116 get result { | 130 get result { |
| 117 assert(currentContainer == null); | 131 assert(currentContainer == null); |
| 132 if (staticCacheSize > MAX_STATIC_CACHE_SIZE) { | |
| 133 _JsonMultiTransitionMap cache = staticCache; | |
| 134 cache.mapping.clear(); | |
| 135 staticCacheSize = 0; | |
| 136 } | |
| 118 return value; | 137 return value; |
| 119 } | 138 } |
| 120 } | 139 } |
| 121 | 140 |
| 122 class _ReviverJsonListener extends _BuildJsonListener { | 141 class _ReviverJsonListener extends _BuildJsonListener { |
| 123 final _Reviver reviver; | 142 final _Reviver reviver; |
| 124 _ReviverJsonListener(reviver(key, value)) : this.reviver = reviver; | 143 _ReviverJsonListener(reviver(key, value)) : this.reviver = reviver; |
| 125 | 144 |
| 126 void arrayElement() { | 145 void arrayElement() { |
| 127 List list = currentContainer; | 146 List list = currentContainer; |
| 128 value = reviver(list.length, value); | 147 value = reviver(list.length, value); |
| 129 super.arrayElement(); | 148 super.arrayElement(); |
| 130 } | 149 } |
| 131 | 150 |
| 132 void propertyValue() { | 151 void propertyValue() { |
| 133 value = reviver(key, value); | 152 value = reviver(key, value); |
| 134 super.propertyValue(); | 153 super.propertyValue(); |
| 135 } | 154 } |
| 136 | 155 |
| 137 get result { | 156 get result { |
| 138 return reviver(null, value); | 157 return reviver(null, value); |
| 139 } | 158 } |
| 140 } | 159 } |
| 141 | 160 |
| 142 class _JsonParser { | 161 class _JsonParser { |
| 143 // A simple non-recursive state-based parser for JSON. | 162 // A simple non-recursive state-based parser for JSON. |
| 144 // | 163 // |
| 145 // Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON | 164 // Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON |
| 146 // and strings also in OBJECT_EMPTY, OBJECT_COMMA. | 165 // and strings also in OBJECT_EMPTY, OBJECT_COMMA. |
| 147 // VALUE STRING : , } ] Transitions to | 166 // VALUE STRING : , } ] f to |
| 148 // EMPTY X X -> END | 167 // EMPTY X X -> END |
| 149 // ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop | 168 // ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop |
| 150 // ARRAY_VALUE @ @ -> ARRAY_COMMA / pop | 169 // ARRAY_VALUE @ @ -> ARRAY_COMMA / pop |
| 151 // ARRAY_COMMA X X -> ARRAY_VALUE | 170 // ARRAY_COMMA X X -> ARRAY_VALUE |
| 152 // OBJECT_EMPTY X @ -> OBJECT_KEY / pop | 171 // OBJECT_EMPTY X @ -> OBJECT_KEY / pop |
| 153 // OBJECT_KEY @ -> OBJECT_COLON | 172 // OBJECT_KEY @ -> OBJECT_COLON |
| 154 // OBJECT_COLON X X -> OBJECT_VALUE | 173 // OBJECT_COLON X X -> OBJECT_VALUE |
| 155 // OBJECT_VALUE @ @ -> OBJECT_COMMA / pop | 174 // OBJECT_VALUE @ @ -> OBJECT_COMMA / pop |
| 156 // OBJECT_COMMA X -> OBJECT_KEY | 175 // OBJECT_COMMA X -> OBJECT_KEY |
| 157 // END | 176 // END |
| (...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 384 fail(position); | 403 fail(position); |
| 385 } | 404 } |
| 386 listener.handleNull(); | 405 listener.handleNull(); |
| 387 return position + 4; | 406 return position + 4; |
| 388 } | 407 } |
| 389 | 408 |
| 390 int parseString(int position) { | 409 int parseString(int position) { |
| 391 // Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"' | 410 // Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"' |
| 392 // Initial position is right after first '"'. | 411 // Initial position is right after first '"'. |
| 393 int start = position; | 412 int start = position; |
| 394 int char; | 413 while (position < source.length) { |
| 395 do { | 414 int char = source.codeUnitAt(position); |
| 396 if (position == source.length) { | 415 if (char <= BACKSLASH) { // BACKSLASH is larger than QUOTE. |
| 397 fail(start - 1, "Unterminated string"); | 416 if (char == BACKSLASH) { |
| 398 } | 417 return parseStringWithEscapes(start, position); |
| 399 char = source.codeUnitAt(position); | 418 } |
| 400 if (char == QUOTE) { | 419 if (char == QUOTE) { |
| 401 listener.handleString(source.substring(start, position)); | 420 listener.handleString(source.substring(start, position)); |
| 402 return position + 1; | 421 return position + 1; |
| 403 } | 422 } |
| 404 if (char < SPACE) { | 423 if (char < SPACE) { |
| 405 fail(position, "Control character in string"); | 424 fail(position, "Control character in string"); |
| 425 } | |
| 406 } | 426 } |
| 407 position++; | 427 position++; |
| 408 } while (char != BACKSLASH); | 428 } |
| 429 fail(start - 1, "Unterminated string"); | |
| 430 } | |
| 431 | |
| 432 int parseStringWithEscapes(string, position) { | |
| 409 // Backslash escape detected. Collect character codes for rest of string. | 433 // Backslash escape detected. Collect character codes for rest of string. |
| 410 int firstEscape = position - 1; | 434 int firstEscape = position - 1; |
| 411 List<int> chars = <int>[]; | 435 List<int> chars = <int>[]; |
| 412 while (true) { | 436 while (true) { |
| 413 if (position == source.length) { | 437 if (position == source.length) { |
| 414 fail(start - 1, "Unterminated string"); | 438 fail(start - 1, "Unterminated string"); |
| 415 } | 439 } |
| 416 char = source.codeUnitAt(position); | 440 char = source.codeUnitAt(position); |
| 417 switch (char) { | 441 switch (char) { |
| 418 case CHAR_b: char = BACKSPACE; break; | 442 case CHAR_b: char = BACKSPACE; break; |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 465 return position + 1; | 489 return position + 1; |
| 466 } | 490 } |
| 467 if (char < SPACE) { | 491 if (char < SPACE) { |
| 468 fail(position, "Control character in string"); | 492 fail(position, "Control character in string"); |
| 469 } | 493 } |
| 470 } while (char != BACKSLASH); | 494 } while (char != BACKSLASH); |
| 471 position++; | 495 position++; |
| 472 } | 496 } |
| 473 } | 497 } |
| 474 | 498 |
| 475 int _handleLiteral(start, position, isDouble) { | |
| 476 String literal = source.substring(start, position); | |
| 477 // This correctly creates -0 for doubles. | |
| 478 num value = (isDouble ? double.parse(literal) : int.parse(literal)); | |
| 479 listener.handleNumber(value); | |
| 480 return position; | |
| 481 } | |
| 482 | |
| 483 int parseNumber(int char, int position) { | 499 int parseNumber(int char, int position) { |
| 484 // Format: | 500 // Format: |
| 485 // '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)? | 501 // '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)? |
| 486 int start = position; | 502 int start = position; |
| 487 int length = source.length; | 503 int length = source.length; |
| 504 int intValue = 0; // Collect int value while parsing. | |
| 505 int intSign = 1; | |
| 488 bool isDouble = false; | 506 bool isDouble = false; |
| 489 if (char == MINUS) { | 507 // Break this block when the end of the number literal is reached. |
| 490 position++; | 508 // At that time, position points to the next character, and isDouble |
| 491 if (position == length) fail(position, "Missing expected digit"); | 509 // is set if the literal contains a decimal point or an exponential. |
| 492 char = source.codeUnitAt(position); | 510 parsing: { |
| 493 } | 511 if (char == MINUS) { |
| 494 if (char < CHAR_0 || char > CHAR_9) { | 512 intSign = -1; |
| 495 fail(position, "Missing expected digit"); | |
| 496 } | |
| 497 if (char == CHAR_0) { | |
| 498 position++; | |
| 499 if (position == length) return _handleLiteral(start, position, false); | |
| 500 char = source.codeUnitAt(position); | |
| 501 if (CHAR_0 <= char && char <= CHAR_9) { | |
| 502 fail(position); | |
| 503 } | |
| 504 } else { | |
| 505 do { | |
| 506 position++; | |
| 507 if (position == length) return _handleLiteral(start, position, false); | |
| 508 char = source.codeUnitAt(position); | |
| 509 } while (CHAR_0 <= char && char <= CHAR_9); | |
| 510 } | |
| 511 if (char == DECIMALPOINT) { | |
| 512 isDouble = true; | |
| 513 position++; | |
| 514 if (position == length) fail(position, "Missing expected digit"); | |
| 515 char = source.codeUnitAt(position); | |
| 516 if (char < CHAR_0 || char > CHAR_9) fail(position); | |
| 517 do { | |
| 518 position++; | |
| 519 if (position == length) return _handleLiteral(start, position, true); | |
| 520 char = source.codeUnitAt(position); | |
| 521 } while (CHAR_0 <= char && char <= CHAR_9); | |
| 522 } | |
| 523 if (char == CHAR_e || char == CHAR_E) { | |
| 524 isDouble = true; | |
| 525 position++; | |
| 526 if (position == length) fail(position, "Missing expected digit"); | |
| 527 char = source.codeUnitAt(position); | |
| 528 if (char == PLUS || char == MINUS) { | |
| 529 position++; | 513 position++; |
| 530 if (position == length) fail(position, "Missing expected digit"); | 514 if (position == length) fail(position, "Missing expected digit"); |
| 531 char = source.codeUnitAt(position); | 515 char = source.codeUnitAt(position); |
| 532 } | 516 } |
| 533 if (char < CHAR_0 || char > CHAR_9) { | 517 if (char < CHAR_0 || char > CHAR_9) { |
| 534 fail(position, "Missing expected digit"); | 518 fail(position, "Missing expected digit"); |
| 535 } | 519 } |
| 536 do { | 520 if (char == CHAR_0) { |
| 537 position++; | 521 position++; |
| 538 if (position == length) return _handleLiteral(start, position, true); | 522 if (position == length) break parsing; |
| 539 char = source.codeUnitAt(position); | 523 char = source.codeUnitAt(position); |
| 540 } while (CHAR_0 <= char && char <= CHAR_9); | 524 if (CHAR_0 <= char && char <= CHAR_9) { |
| 525 fail(position); | |
| 526 } | |
| 527 } else { | |
| 528 do { | |
| 529 intValue = intValue * 10 + (char - CHAR_0); | |
| 530 position++; | |
| 531 if (position == length) break parsing; | |
| 532 char = source.codeUnitAt(position); | |
| 533 } while (CHAR_0 <= char && char <= CHAR_9); | |
| 534 } | |
| 535 if (char == DECIMALPOINT) { | |
| 536 isDouble = true; | |
| 537 position++; | |
| 538 if (position == length) fail(position, "Missing expected digit"); | |
| 539 char = source.codeUnitAt(position); | |
| 540 if (char < CHAR_0 || char > CHAR_9) fail(position); | |
| 541 do { | |
| 542 position++; | |
| 543 if (position == length) break parsing; | |
| 544 char = source.codeUnitAt(position); | |
| 545 } while (CHAR_0 <= char && char <= CHAR_9); | |
| 546 } | |
| 547 if (char == CHAR_e || char == CHAR_E) { | |
| 548 isDouble = true; | |
| 549 position++; | |
| 550 if (position == length) fail(position, "Missing expected digit"); | |
| 551 char = source.codeUnitAt(position); | |
| 552 if (char == PLUS || char == MINUS) { | |
| 553 position++; | |
| 554 if (position == length) fail(position, "Missing expected digit"); | |
| 555 char = source.codeUnitAt(position); | |
| 556 } | |
| 557 if (char < CHAR_0 || char > CHAR_9) { | |
| 558 fail(position, "Missing expected digit"); | |
| 559 } | |
| 560 do { | |
| 561 position++; | |
| 562 if (position == length) break parsing; | |
| 563 char = source.codeUnitAt(position); | |
| 564 } while (CHAR_0 <= char && char <= CHAR_9); | |
| 565 } | |
| 541 } | 566 } |
| 542 return _handleLiteral(start, position, isDouble); | 567 if (!isDouble) { |
| 568 listener.handleNumber(intSign * intValue); | |
| 569 return position; | |
| 570 } | |
| 571 // Consider whether we can have an int/double.parse that works on part of | |
| 572 // a string, to avoid creating the substring. | |
| 573 String literal = source.substring(start, position); | |
| 574 // This correctly creates -0.0 for doubles. | |
| 575 listener.handleNumber(double.parse(literal)); | |
| 576 return position; | |
| 543 } | 577 } |
| 544 | 578 |
| 545 void fail(int position, [String message]) { | 579 void fail(int position, [String message]) { |
| 546 if (message == null) message = "Unexpected character"; | 580 if (message == null) message = "Unexpected character"; |
| 547 listener.fail(source, position, message); | 581 listener.fail(source, position, message); |
| 548 // If the listener didn't throw, do it here. | 582 // If the listener didn't throw, do it here. |
| 549 String slice; | 583 String slice; |
| 550 int sliceEnd = position + 20; | 584 int sliceEnd = position + 20; |
| 551 if (sliceEnd > source.length) { | 585 if (sliceEnd > source.length) { |
| 552 slice = "'${source.substring(position)}'"; | 586 slice = "'${source.substring(position)}'"; |
| 553 } else { | 587 } else { |
| 554 slice = "'${source.substring(position, sliceEnd)}...'"; | 588 slice = "'${source.substring(position, sliceEnd)}...'"; |
| 555 } | 589 } |
| 556 throw new FormatException("Unexpected character at $position: $slice"); | 590 throw new FormatException("Unexpected character at $position: $slice"); |
| 557 } | 591 } |
| 558 } | 592 } |
| 559 | 593 |
| 594 /* | |
| 595 * JSON Map | |
| 596 * | |
| 597 * A map with hidden class structure. | |
| 598 * | |
| 599 * When building maps, don't use a linked hashmap directly. | |
| 600 * Instead use a "hidden class" map that keeps the hash structure | |
| 601 * in a separate sharable structure representation, and only the | |
| 602 * data in the actual map. | |
| 603 * Basically, use a map of string->index, and a list of values, | |
| 604 * and share the map between all objects with the same structure. | |
| 605 * | |
| 606 * JSON maps are expected to preserve order, so the hidden classes | |
| 607 * maintain the order of the keys. | |
| 608 * | |
| 609 * The maps will be a delegating map that points to the hidden class | |
| 610 * (itself a "map") except that all modifying operations makes the | |
| 611 * hidden class replace itself with a linked hash map. | |
| 612 */ | |
| 613 | |
| 614 /** | |
| 615 * A transition cache that shows transitions from one hidden class | |
| 616 * to another. | |
| 617 */ | |
| 618 class _JsonTransitionMap { | |
| 619 _JsonHiddenClass get hiddenClass; | |
| 620 /** See if there is a transition from this class with [key] as key. */ | |
| 621 _JsonTransitionMap lookup(String key); | |
| 622 /** Add a new transition from this class to a new one. */ | |
| 623 _JsonTransitionMap addAlternative(String key, _JsonTransitionMap targetMap); | |
| 624 /** Update the transition map that is linked by a given key. */ | |
| 625 void update(String key, _JsonTransitionMap map); | |
| 626 } | |
| 627 | |
| 628 class _JsonLeafTransitionMap implements _JsonTransitionMap { | |
| 629 final _JsonHiddenClass hiddenClass; | |
| 630 _JsonLeafTransitionMap(this.hiddenClass); | |
| 631 _JsonTransitionMap lookup(String key) => null; | |
| 632 _JsonTransitionMap addAlternative(String key, _JsonTransitionMap targetMap) { | |
| 633 return new _JsonSingletonTransitionMap(hiddenClass, key, targetMap); | |
| 634 } | |
| 635 void update(String key, _JsonTransitionMap map) { | |
| 636 assert(false); // Must not be called. | |
| 637 } | |
| 638 } | |
| 639 | |
| 640 class _JsonSingletonTransitionMap implements _JsonTransitionMap { | |
| 641 final _JsonHiddenClass hiddenClass; | |
| 642 final String key; | |
| 643 _JsonTransitionMap next; | |
| 644 _JsonSingletonTransitionMap(this.hiddenClass, this.key, this.next); | |
| 645 | |
| 646 _JsonTransitionMap lookup(String key) { | |
| 647 if (this.key == key) return next; | |
| 648 return null; | |
| 649 } | |
| 650 | |
| 651 _JsonTransitionMap addAlternative(String key, _JsonTransitionMap targetMap) { | |
| 652 Map mapping = new HashMap(); | |
| 653 mapping[this.key] = next; | |
| 654 mapping[key] = targetMap; | |
| 655 return new _JsonMultiTransitionMap(hiddenClass, mapping); | |
| 656 } | |
| 657 | |
| 658 void update(String key, _JsonTransitionMap map) { | |
| 659 assert(this.key == key); | |
| 660 next = map; | |
| 661 } | |
| 662 } | |
| 663 | |
| 664 class _JsonMultiTransitionMap implements _JsonTransitionMap { | |
| 665 final _JsonHiddenClass hiddenClass; | |
| 666 final Map mapping; | |
| 667 _JsonMultiTransitionMap(this.hiddenClass, this.mapping); | |
| 668 _JsonTransitionMap lookup(String key) => mapping[key]; | |
| 669 _JsonTransitionMap addAlternative(String key, _JsonTransitionMap targetMap) { | |
| 670 assert(!mapping.containsKey(key)); | |
| 671 mapping[key] = targetMap; | |
| 672 return this; | |
| 673 } | |
| 674 void update(String key, _JsonTransitionMap map) { | |
| 675 assert(mapping.containsKey(key)); | |
| 676 mapping[key] = map; | |
| 677 } | |
| 678 } | |
| 679 | |
| 680 /** | |
| 681 * A JSON Object builder that keeps a hidden class for keys and a list of | |
| 682 * values. | |
| 683 * | |
| 684 * When the object is complete, it can be extracted as a `Map` using `toMap`. | |
| 685 * | |
| 686 */ | |
| 687 class _JsonObjectBuilder { | |
| 688 int transitionsAdded = 0; | |
| 689 _JsonTransitionMap parentMap; | |
| 690 String previousKey; | |
| 691 _JsonTransitionMap currentMap; | |
| 692 | |
| 693 final List values = []; | |
| 694 | |
| 695 _JsonObjectBuilder(this.currentMap); | |
| 696 | |
| 697 Object toMap() { | |
| 698 return currentMap.hiddenClass.asMap(values); | |
| 699 } | |
| 700 | |
| 701 /** | |
| 702 * Add a property to the object being built. | |
| 703 * | |
| 704 * If the key is already in the object, its value is just overwritten. | |
| 705 * Otherwise the hidden class is transitioned to one with the new key | |
| 706 * and the result is added at the end. | |
| 707 */ | |
| 708 void add(String key, var value) { | |
| 709 int index = currentMap.hiddenClass.lookup(key); | |
| 710 if (index >= 0) { | |
| 711 values[index] = value; | |
| 712 } else { | |
| 713 _JsonTransitionMap nextMap = currentMap.lookup(key); | |
| 714 if (nextMap == null) { | |
| 715 _JsonHiddenClass nextClass = currentMap.hiddenClass.addKey(key); | |
| 716 nextMap = new _JsonLeafTransitionMap(nextClass); | |
| 717 currentMap = currentMap.addAlternative(key, nextMap); | |
| 718 if (parentMap != null) { | |
| 719 parentMap.update(previousKey, currentMap); | |
| 720 } | |
| 721 transitionsAdded++; | |
| 722 } | |
| 723 parentMap = currentMap; | |
| 724 previousKey = key; | |
| 725 currentMap = nextMap; | |
| 726 | |
| 727 values.add(value); | |
| 728 } | |
| 729 } | |
| 730 } | |
| 731 | |
| 732 /** | |
| 733 * A "hidden class" is a mapping from string key to integer index. | |
| 734 * | |
| 735 * A map using a class will have a list of values for each index in the | |
| 736 * hidden class. | |
| 737 */ | |
| 738 abstract class _JsonHiddenClass { | |
| 739 const _JsonHiddenClass(); | |
| 740 const factory _JsonHiddenClass.empty() = _JsonEmptyHiddenClass; | |
| 741 int lookup(String key); | |
| 742 Map toMap(List values) { | |
| 743 Map map = new LinkedHashMap<String, dynamic>(); | |
| 744 forEach((k, v) { map[k] = v; }); | |
| 745 } | |
| 746 Iterable<String> get keyIterable; | |
| 747 void forEach(List values, void action(String key, var value)); | |
| 748 int get length; | |
| 749 | |
| 750 _JsonHiddenClass addKey(String key); | |
| 751 | |
| 752 Map<String, dynamic> asMap(List values) { | |
| 753 return new _JsonHiddenClassMap(this, values).wrapper; | |
| 754 //return new _JsonHiddenClassMap(this, makeListFixedLength(values)).wrapper; | |
|
sra1
2014/02/27 04:45:22
Delete comment.
Lasse Reichstein Nielsen
2014/02/27 09:10:38
Acl, yes. It was an attempt to save a little extra
| |
| 755 } | |
| 756 } | |
| 757 | |
| 758 class _JsonEmptyHiddenClass extends _JsonHiddenClass { | |
| 759 const _JsonEmptyHiddenClass(); | |
| 760 int lookup(String key) => -1; | |
| 761 Map toMap(List values) => new LinkedHashMap<String, dynamic>(); | |
| 762 Iterable<String> get keyIterable => new Iterable<String>.generate(0, null); | |
| 763 void forEach(List values, void action(String key, var value)) {} | |
| 764 int get length => 0; | |
| 765 _JsonHiddenClass addKey(String key) { | |
| 766 return new _JsonSmallHiddenClass(<String>[key], 1); | |
| 767 } | |
| 768 } | |
| 769 | |
| 770 /** | |
| 771 * A hidden class for a JSON object that maps keys to value indices. | |
| 772 * | |
| 773 * This is intended for small objects. Looking up a key is done using | |
| 774 * linear search. | |
| 775 */ | |
| 776 class _JsonSmallHiddenClass extends _JsonHiddenClass { | |
| 777 final List keys; | |
| 778 final int length; // `keys` may contain more elements than length. | |
| 779 _JsonSmallHiddenClass(this.keys, this.length); | |
| 780 int lookup(String key) { | |
| 781 for (int i = 0; i < length; i++) { | |
| 782 if (keys[i] == key) return i; | |
| 783 } | |
| 784 return -1; | |
| 785 } | |
| 786 | |
| 787 Iterable<String> get keyIterable => | |
| 788 new SubListIterable<String>(keys, 0, length); | |
| 789 | |
| 790 void forEach(List values, void action(String key, var value)) { | |
| 791 for (int i = 0; i < length; i++) { | |
| 792 action(keys[i], values[i]); | |
| 793 } | |
| 794 } | |
| 795 | |
| 796 _JsonHiddenClass addKey(String key) { | |
| 797 const int MAX_SMALL_CLASS = 4; | |
| 798 if (length == MAX_SMALL_CLASS) { | |
| 799 Map map = new LinkedHashMap<String,int>(); | |
| 800 for (int i = 0; i < length; i++) map[keys[i]] = i; | |
| 801 map[key] = length; | |
| 802 return new _JsonMediumHiddenClass(map, length + 1); | |
| 803 } | |
| 804 // TODO(lrn): Add an implementation for larger key lists that doesn't use | |
| 805 // linear search. Switch to using that implementation here if length is | |
| 806 // above a threshold. | |
| 807 var newKeys; | |
| 808 if (keys.length > length) { | |
| 809 newKeys = keys.sublist(0, length); | |
| 810 } else { | |
| 811 newKeys = keys; | |
| 812 } | |
| 813 newKeys.add(key); | |
| 814 return new _JsonSmallHiddenClass(newKeys, length + 1); | |
| 815 } | |
| 816 } | |
| 817 | |
| 818 /** | |
| 819 * A hidden class that uses a [LinkedHashMap] to store the key-to-index mapping. | |
| 820 * | |
| 821 * This introduces the same overhead as a normal map, so if the hidden class | |
| 822 * is only used once, it's just an overhead. | |
| 823 */ | |
| 824 class _JsonMediumHiddenClass extends _JsonHiddenClass { | |
| 825 final LinkedHashMap<String, int> keys; | |
| 826 final int length; // `keys` may contain more elements than length. | |
| 827 _JsonMediumHiddenClass(this.keys, this.length); | |
| 828 | |
| 829 int lookup(String key) { | |
| 830 int index = keys[key]; | |
| 831 if (index == null || index >= length) return -1; | |
| 832 return index; | |
| 833 } | |
| 834 | |
| 835 Iterable<String> get keyIterable => keys.keys.take(length); | |
| 836 | |
| 837 void forEach(List values, void action(String key, var value)) { | |
| 838 int i = 0; | |
| 839 assert(length != 0); | |
| 840 for (String key in keys.keys) { | |
| 841 action(key, values[i]); | |
| 842 i++; | |
| 843 if (i == length) break; | |
| 844 } | |
| 845 } | |
| 846 | |
| 847 _JsonHiddenClass addKey(String key) { | |
| 848 // TODO(lrn): Add an implementation for larger key lists that doesn't use | |
| 849 // linear search. Switch to using that implementation here if length is | |
| 850 // above a threshold. | |
| 851 var newKeys; | |
| 852 if (keys.length > length) { | |
| 853 newKeys = new HashMap<String,int>(); | |
| 854 keys.forEach((String key, int value) { | |
| 855 if (value < length) newKeys[key] = value; | |
| 856 }); | |
| 857 } else { | |
| 858 newKeys = keys; | |
| 859 } | |
| 860 newKeys[key] = length; | |
| 861 return new _JsonMediumHiddenClass(newKeys, length + 1); | |
| 862 } | |
| 863 } | |
| 864 | |
| 865 | |
| 866 /** | |
| 867 * A map based on a hidden class. | |
| 868 * | |
| 869 * The hidden class translates string keys to integer indices, and the | |
| 870 * values are stored at those indices in [values]. | |
| 871 * The idea is that the hidden class can be shared between multiple similar | |
| 872 * objects, reducing the memory overhead of the map created by decoding a | |
| 873 * JSON Object. This only works when there are more than one object with | |
| 874 * the same structure. | |
| 875 * | |
| 876 * | |
| 877 * This object is hidden behind the [_JsonMapWrapper]. | |
| 878 * | |
| 879 * Any attempt to write to the map will make it convert itself to a | |
| 880 * [LinkedHashMap] with the same values, and make the wrapper delegate to that | |
| 881 * map instead. | |
| 882 */ | |
| 883 class _JsonHiddenClassMap implements Map { | |
| 884 final _JsonHiddenClass hiddenClass; | |
| 885 final List mapValues; | |
| 886 _JsonMapWrapper wrapper; | |
| 887 | |
| 888 _JsonHiddenClassMap(this.hiddenClass, this.mapValues) { | |
| 889 wrapper = new _JsonMapWrapper(this); | |
| 890 } | |
| 891 | |
| 892 Map convertToMap() { | |
| 893 Map map = hiddenClass.toMap(mapValues); | |
| 894 wapper._delegate = map; | |
| 895 return map; | |
| 896 } | |
| 897 | |
| 898 bool containsValue(Object value) { | |
| 899 for (int i = 0; i < mapValues.length; i++) { | |
| 900 if (mapValues[i] == value) return true; | |
| 901 } | |
| 902 return false; | |
| 903 } | |
| 904 | |
| 905 bool containsKey(Object key) => hiddenClass.lookup(key) >= 0; | |
| 906 | |
| 907 operator [](Object key) { | |
| 908 int index = hiddenClass.lookup(key); | |
| 909 if (index < 0) return null; | |
| 910 return mapValues[index]; | |
| 911 } | |
| 912 | |
| 913 void operator []=(String key, var value) { | |
| 914 convertToMap()[key] = value; | |
| 915 } | |
| 916 | |
| 917 putIfAbsent(String key, ifAbsent()) { | |
| 918 return convertToMap().putIfAbsent(key, ifAbsent); | |
| 919 } | |
| 920 | |
| 921 void addAll(Map<String, dynamic> other) { | |
| 922 convertToMap().addAll(other); | |
| 923 } | |
| 924 | |
| 925 remove(Object key) { | |
| 926 convertToMap().remove(key); | |
| 927 } | |
| 928 | |
| 929 void clear() { wrapper._delegate = new LinkedHashMap<String, dynamic>(); } | |
| 930 | |
| 931 void forEach(void f(String key, var value)) { | |
| 932 hiddenClass.forEach(mapValues, f); | |
| 933 } | |
| 934 | |
| 935 Iterable<String> get keys => hiddenClass.keyIterable; | |
| 936 | |
| 937 Iterable get valueIterator => | |
| 938 new SubListIterable(mapValues, 0, values.length); | |
|
sra1
2014/02/27 04:45:22
If we modify the map while iterating the keys or v
Lasse Reichstein Nielsen
2014/02/27 09:10:38
Ack.
Concurrent modification. Why don't we just di
| |
| 939 | |
| 940 int get length => mapValues.length; | |
| 941 | |
| 942 bool get isEmpty => mapValues.length == 0; | |
| 943 | |
| 944 bool get isNotEmpty => mapValues.length != 0; | |
| 945 | |
| 946 String toString() => Maps.mapToString(this); | |
| 947 } | |
| 948 | |
| 949 /** | |
| 950 * Delegating map wrapper. | |
| 951 * | |
| 952 * Used to have a "copy on write" map implementation optimized for reading, | |
| 953 * which converts itself to a [LinkedHashMap] on any write operation by | |
| 954 * creating the hash map and writing it to [_delegate]. | |
| 955 * | |
| 956 * This is the only object that the JSON decoder's user sees. | |
|
sra1
2014/02/26 21:56:06
Interesting trick.
Can you think of a way to make
Lasse Reichstein Nielsen
2014/02/27 09:10:38
I am considering adding a public static setter fun
| |
| 957 */ | |
| 958 class _JsonMapWrapper implements Map<String, dynamic> { | |
|
sra1
2014/02/26 21:56:06
The original map was created with
currentContaine
Lasse Reichstein Nielsen
2014/02/27 09:10:38
It wasn't the intent, because I hadn't noticed tha
| |
| 959 Map _delegate; | |
| 960 | |
| 961 _JsonMapWrapper(this._delegate); | |
| 962 | |
| 963 bool containsValue(Object value) => _delegate.containsValue(value); | |
| 964 | |
| 965 bool containsKey(Object key) => _delegate.containsKey(key); | |
| 966 | |
| 967 operator [](Object key) => _delegate[key]; | |
| 968 | |
| 969 void operator []=(String key, var value) { _delegate[key] = value; } | |
| 970 | |
| 971 putIfAbsent(String key, ifAbsent()) => _delegate.putIfAbsent(key, ifAbsent); | |
| 972 | |
| 973 void addAll(Map<String, dynamic> other) => _delegate.addAll(other); | |
| 974 | |
| 975 remove(Object key) => _delegate.remove(key); | |
| 976 | |
| 977 void clear() { _delegate.clear(); } | |
|
sra1
2014/02/26 21:56:06
FYI you can use => for these too.
I guess in poorl
Lasse Reichstein Nielsen
2014/02/27 09:10:38
I prefre (strongly) to not use "=>" for void funct
| |
| 978 | |
| 979 void forEach(void f(String key, var value)) { _delegate.forEach(f); } | |
| 980 | |
| 981 Iterable<String> get keys => _delegate.keys; | |
| 982 | |
| 983 Iterable get values => _delegate.values; | |
| 984 | |
| 985 int get length => _delegate.length; | |
| 986 | |
| 987 bool get isEmpty => _delegate.isEmpty; | |
| 988 | |
| 989 bool get isNotEmpty => _delegate.isNotEmpty; | |
| 990 | |
| 991 String toString() => _delegate.toString(); | |
| 992 } | |
| 993 | |
| 560 // UTF-8 conversion. | 994 // UTF-8 conversion. |
| 561 | 995 |
| 562 patch class _Utf8Encoder { | 996 patch class _Utf8Encoder { |
| 563 /* patch */ static List<int> _createBuffer(int size) => new Uint8List(size); | 997 /* patch */ static List<int> _createBuffer(int size) => new Uint8List(size); |
| 564 } | 998 } |
| 999 | |
| OLD | NEW |