Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(261)

Side by Side Diff: src/asmjs/asm-parser.cc

Issue 2782613002: Revert of [wasm][asm.js] Fix and enable several asm.js tests with the new parser. (Closed)
Patch Set: Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « src/asmjs/asm-parser.h ('k') | src/asmjs/asm-scanner.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2017 the V8 project authors. All rights reserved. 1 // Copyright 2017 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "src/asmjs/asm-parser.h" 5 #include "src/asmjs/asm-parser.h"
6 6
7 // Required to get M_E etc. for MSVC. 7 // Required to get M_E etc. for MSVC.
8 // References from STDLIB_MATH_VALUE_LIST in asm-names.h 8 // References from STDLIB_MATH_VALUE_LIST in asm-names.h
9 #if defined(_WIN32) 9 #if defined(_WIN32)
10 #define _USE_MATH_DEFINES 10 #define _USE_MATH_DEFINES
(...skipping 220 matching lines...) Expand 10 before | Expand all | Expand 10 after
231 if (info->import != nullptr) { 231 if (info->import != nullptr) {
232 return info->index; 232 return info->index;
233 } else { 233 } else {
234 return info->index + static_cast<uint32_t>(global_imports_.size()); 234 return info->index + static_cast<uint32_t>(global_imports_.size());
235 } 235 }
236 } 236 }
237 237
238 void AsmJsParser::AddGlobalImport(std::string name, AsmType* type, 238 void AsmJsParser::AddGlobalImport(std::string name, AsmType* type,
239 ValueType vtype, bool mutable_variable, 239 ValueType vtype, bool mutable_variable,
240 VarInfo* info) { 240 VarInfo* info) {
241 // TODO(bradnelson): Refactor memory management here.
242 // AsmModuleBuilder should really own import names.
243 char* name_data = zone()->NewArray<char>(name.size());
244 memcpy(name_data, name.data(), name.size());
245 if (mutable_variable) { 241 if (mutable_variable) {
246 // Allocate a separate variable for the import. 242 // Allocate a separate variable for the import.
247 DeclareGlobal(info, true, type, vtype); 243 DeclareGlobal(info, true, type, vtype);
248 // Record the need to initialize the global from the import. 244 // Record the need to initialize the global from the import.
249 global_imports_.push_back({name_data, name.size(), 0, info->index, true}); 245 global_imports_.push_back({name, 0, info->index, true});
250 } else { 246 } else {
251 // Just use the import directly. 247 // Just use the import directly.
252 global_imports_.push_back({name_data, name.size(), 0, info->index, false}); 248 global_imports_.push_back({name, 0, info->index, false});
253 } 249 }
254 GlobalImport& gi = global_imports_.back(); 250 GlobalImport& gi = global_imports_.back();
255 // TODO(bradnelson): Reuse parse buffer memory / make wasm-module-builder 251 // TODO(bradnelson): Reuse parse buffer memory / make wasm-module-builder
256 // managed the memory for the import name (currently have to keep our 252 // managed the memory for the import name (currently have to keep our
257 // own memory for it). 253 // own memory for it).
258 gi.import_index = module_builder_->AddGlobalImport( 254 gi.import_index = module_builder_->AddGlobalImport(
259 name_data, static_cast<int>(name.size()), vtype); 255 name.data(), static_cast<int>(name.size()), vtype);
260 if (!mutable_variable) { 256 if (!mutable_variable) {
261 info->DeclareGlobalImport(type, gi.import_index); 257 info->DeclareGlobalImport(type, gi.import_index);
262 } 258 }
263 } 259 }
264 260
265 void AsmJsParser::VarInfo::DeclareGlobalImport(AsmType* type, uint32_t index) { 261 void AsmJsParser::VarInfo::DeclareGlobalImport(AsmType* type, uint32_t index) {
266 kind = VarKind::kGlobal; 262 kind = VarKind::kGlobal;
267 this->type = type; 263 this->type = type;
268 this->index = index; 264 this->index = index;
269 mutable_variable = false; 265 mutable_variable = false;
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after
359 SkipSemicolon(); 355 SkipSemicolon();
360 RECURSE(ValidateModuleVars()); 356 RECURSE(ValidateModuleVars());
361 while (Peek(TOK(function))) { 357 while (Peek(TOK(function))) {
362 RECURSE(ValidateFunction()); 358 RECURSE(ValidateFunction());
363 } 359 }
364 while (Peek(TOK(var))) { 360 while (Peek(TOK(var))) {
365 RECURSE(ValidateFunctionTable()); 361 RECURSE(ValidateFunctionTable());
366 } 362 }
367 RECURSE(ValidateExport()); 363 RECURSE(ValidateExport());
368 364
369 // Check that all functions were eventually defined.
370 for (auto info : global_var_info_) {
371 if (info.kind != VarKind::kFunction) {
372 continue;
373 }
374 if (!info.function_defined) {
375 FAIL("Undefined function");
376 }
377 }
378
379 // Add start function to init things. 365 // Add start function to init things.
380 WasmFunctionBuilder* start = module_builder_->AddFunction(); 366 WasmFunctionBuilder* start = module_builder_->AddFunction();
381 module_builder_->MarkStartFunction(start); 367 module_builder_->MarkStartFunction(start);
382 for (auto global_import : global_imports_) { 368 for (auto global_import : global_imports_) {
383 if (global_import.needs_init) { 369 if (global_import.needs_init) {
384 start->EmitWithVarInt(kExprGetGlobal, global_import.import_index); 370 start->EmitWithVarInt(kExprGetGlobal, global_import.import_index);
385 start->EmitWithVarInt(kExprSetGlobal, 371 start->EmitWithVarInt(kExprSetGlobal,
386 static_cast<uint32_t>(global_import.global_index + 372 static_cast<uint32_t>(global_import.global_index +
387 global_imports_.size())); 373 global_imports_.size()));
388 } 374 }
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
454 EXPECT_TOKEN('='); 440 EXPECT_TOKEN('=');
455 double dvalue = 0.0; 441 double dvalue = 0.0;
456 uint64_t uvalue = 0; 442 uint64_t uvalue = 0;
457 if (CheckForDouble(&dvalue)) { 443 if (CheckForDouble(&dvalue)) {
458 DeclareGlobal(info, mutable_variable, AsmType::Double(), kWasmF64, 444 DeclareGlobal(info, mutable_variable, AsmType::Double(), kWasmF64,
459 WasmInitExpr(dvalue)); 445 WasmInitExpr(dvalue));
460 } else if (CheckForUnsigned(&uvalue)) { 446 } else if (CheckForUnsigned(&uvalue)) {
461 if (uvalue > 0x7fffffff) { 447 if (uvalue > 0x7fffffff) {
462 FAIL("Numeric literal out of range"); 448 FAIL("Numeric literal out of range");
463 } 449 }
464 DeclareGlobal(info, mutable_variable, 450 DeclareGlobal(info, mutable_variable, AsmType::Int(), kWasmI32,
465 mutable_variable ? AsmType::Int() : AsmType::Signed(), 451 WasmInitExpr(static_cast<int32_t>(uvalue)));
466 kWasmI32, WasmInitExpr(static_cast<int32_t>(uvalue)));
467 } else if (Check('-')) { 452 } else if (Check('-')) {
468 if (CheckForDouble(&dvalue)) { 453 if (CheckForDouble(&dvalue)) {
469 DeclareGlobal(info, mutable_variable, AsmType::Double(), kWasmF64, 454 DeclareGlobal(info, mutable_variable, AsmType::Double(), kWasmF64,
470 WasmInitExpr(-dvalue)); 455 WasmInitExpr(-dvalue));
471 } else if (CheckForUnsigned(&uvalue)) { 456 } else if (CheckForUnsigned(&uvalue)) {
472 if (uvalue > 0x7fffffff) { 457 if (uvalue > 0x7fffffff) {
473 FAIL("Numeric literal out of range"); 458 FAIL("Numeric literal out of range");
474 } 459 }
475 DeclareGlobal(info, mutable_variable, 460 DeclareGlobal(info, mutable_variable, AsmType::Int(), kWasmI32,
476 mutable_variable ? AsmType::Int() : AsmType::Signed(), 461 WasmInitExpr(-static_cast<int32_t>(uvalue)));
477 kWasmI32, WasmInitExpr(-static_cast<int32_t>(uvalue)));
478 } else { 462 } else {
479 FAIL("Expected numeric literal"); 463 FAIL("Expected numeric literal");
480 } 464 }
481 } else if (Check(TOK(new))) { 465 } else if (Check(TOK(new))) {
482 RECURSE(ValidateModuleVarNewStdlib(info)); 466 RECURSE(ValidateModuleVarNewStdlib(info));
483 } else if (Check(stdlib_name_)) { 467 } else if (Check(stdlib_name_)) {
484 EXPECT_TOKEN('.'); 468 EXPECT_TOKEN('.');
485 RECURSE(ValidateModuleVarStdlib(info)); 469 RECURSE(ValidateModuleVarStdlib(info));
486 } else if (ValidateModuleVarImport(info, mutable_variable)) { 470 } else if (ValidateModuleVarImport(info, mutable_variable)) {
487 // Handled inside. 471 // Handled inside.
488 } else if (scanner_.IsGlobal()) { 472 } else if (scanner_.IsGlobal()) {
489 RECURSE(ValidateModuleVarFromGlobal(info, mutable_variable)); 473 RECURSE(ValidateModuleVarFloat(info, mutable_variable));
490 } else { 474 } else {
491 FAIL("Bad variable declaration"); 475 FAIL("Bad variable declaration");
492 } 476 }
493 } 477 }
494 478
495 // 6.1 ValidateModule - global float declaration 479 // 6.1 ValidateModule - global float declaration
496 void AsmJsParser::ValidateModuleVarFromGlobal(VarInfo* info, 480 void AsmJsParser::ValidateModuleVarFloat(VarInfo* info, bool mutable_variable) {
497 bool mutable_variable) { 481 if (!GetVarInfo(Consume())->type->IsA(stdlib_fround_)) {
498 VarInfo* src_info = GetVarInfo(Consume()); 482 FAIL("Expected fround");
499 if (!src_info->type->IsA(stdlib_fround_)) {
500 if (src_info->mutable_variable) {
501 FAIL("Can only use immutable variables in global definition");
502 }
503 if (mutable_variable) {
504 FAIL("Can only define immutable variables with other immutables");
505 }
506 if (!src_info->type->IsA(AsmType::Int()) &&
507 !src_info->type->IsA(AsmType::Float()) &&
508 !src_info->type->IsA(AsmType::Double())) {
509 FAIL("Expected int, float, double, or fround for global definition");
510 }
511 info->kind = VarKind::kGlobal;
512 info->type = src_info->type;
513 info->index = src_info->index;
514 info->mutable_variable = false;
515 return;
516 } 483 }
517 EXPECT_TOKEN('('); 484 EXPECT_TOKEN('(');
518 bool negate = false; 485 bool negate = false;
519 if (Check('-')) { 486 if (Check('-')) {
520 negate = true; 487 negate = true;
521 } 488 }
522 double dvalue = 0.0; 489 double dvalue = 0.0;
523 uint64_t uvalue = 0; 490 uint64_t uvalue = 0;
524 if (CheckForDouble(&dvalue)) { 491 if (CheckForDouble(&dvalue)) {
525 if (negate) { 492 if (negate) {
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
558 if (!CheckForZero()) { 525 if (!CheckForZero()) {
559 FAILf("Expected |0 type annotation for foreign integer import"); 526 FAILf("Expected |0 type annotation for foreign integer import");
560 } 527 }
561 AddGlobalImport(import_name, AsmType::Int(), kWasmI32, mutable_variable, 528 AddGlobalImport(import_name, AsmType::Int(), kWasmI32, mutable_variable,
562 info); 529 info);
563 return true; 530 return true;
564 } 531 }
565 info->kind = VarKind::kImportedFunction; 532 info->kind = VarKind::kImportedFunction;
566 function_import_info_.resize(function_import_info_.size() + 1); 533 function_import_info_.resize(function_import_info_.size() + 1);
567 info->import = &function_import_info_.back(); 534 info->import = &function_import_info_.back();
568 // TODO(bradnelson): Refactor memory management here. 535 info->import->name = import_name;
569 // AsmModuleBuilder should really own import names.
570 info->import->function_name = zone()->NewArray<char>(import_name.size());
571 memcpy(info->import->function_name, import_name.data(), import_name.size());
572 info->import->function_name_size = import_name.size();
573 return true; 536 return true;
574 } 537 }
575 return false; 538 return false;
576 } 539 }
577 540
578 // 6.1 ValidateModule - one variable 541 // 6.1 ValidateModule - one variable
579 // 9 - Standard Library - heap types 542 // 9 - Standard Library - heap types
580 void AsmJsParser::ValidateModuleVarNewStdlib(VarInfo* info) { 543 void AsmJsParser::ValidateModuleVarNewStdlib(VarInfo* info) {
581 EXPECT_TOKEN(stdlib_name_); 544 EXPECT_TOKEN(stdlib_name_);
582 EXPECT_TOKEN('.'); 545 EXPECT_TOKEN('.');
(...skipping 16 matching lines...) Expand all
599 // 6.1 ValidateModule - one variable 562 // 6.1 ValidateModule - one variable
600 // 9 - Standard Library 563 // 9 - Standard Library
601 void AsmJsParser::ValidateModuleVarStdlib(VarInfo* info) { 564 void AsmJsParser::ValidateModuleVarStdlib(VarInfo* info) {
602 if (Check(TOK(Math))) { 565 if (Check(TOK(Math))) {
603 EXPECT_TOKEN('.'); 566 EXPECT_TOKEN('.');
604 switch (Consume()) { 567 switch (Consume()) {
605 #define V(name) \ 568 #define V(name) \
606 case TOK(name): \ 569 case TOK(name): \
607 DeclareGlobal(info, false, AsmType::Double(), kWasmF64, \ 570 DeclareGlobal(info, false, AsmType::Double(), kWasmF64, \
608 WasmInitExpr(M_##name)); \ 571 WasmInitExpr(M_##name)); \
609 stdlib_uses_.insert(AsmTyper::kMath##name); \
610 break; 572 break;
611 STDLIB_MATH_VALUE_LIST(V) 573 STDLIB_MATH_VALUE_LIST(V)
612 #undef V 574 #undef V
613 #define V(name, Name, op, sig) \ 575 #define V(name, Name, op, sig) \
614 case TOK(name): \ 576 case TOK(name): \
615 info->DeclareStdlibFunc(VarKind::kMath##Name, stdlib_##sig##_); \ 577 info->DeclareStdlibFunc(VarKind::kMath##Name, stdlib_##sig##_); \
616 stdlib_uses_.insert(AsmTyper::kMath##Name); \ 578 stdlib_uses_.insert(AsmTyper::kMath##Name); \
617 break; 579 break;
618 STDLIB_MATH_FUNCTION_LIST(V) 580 STDLIB_MATH_FUNCTION_LIST(V)
619 #undef V 581 #undef V
620 default: 582 default:
621 FAIL("Invalid member of stdlib.Math"); 583 FAIL("Invalid member of stdlib.Math");
622 } 584 }
623 } else if (Check(TOK(Infinity))) { 585 } else if (Check(TOK(Infinity))) {
624 DeclareGlobal(info, false, AsmType::Double(), kWasmF64, 586 DeclareGlobal(info, false, AsmType::Double(), kWasmF64,
625 WasmInitExpr(std::numeric_limits<double>::infinity())); 587 WasmInitExpr(std::numeric_limits<double>::infinity()));
626 stdlib_uses_.insert(AsmTyper::kInfinity);
627 } else if (Check(TOK(NaN))) { 588 } else if (Check(TOK(NaN))) {
628 DeclareGlobal(info, false, AsmType::Double(), kWasmF64, 589 DeclareGlobal(info, false, AsmType::Double(), kWasmF64,
629 WasmInitExpr(std::numeric_limits<double>::quiet_NaN())); 590 WasmInitExpr(std::numeric_limits<double>::quiet_NaN()));
630 stdlib_uses_.insert(AsmTyper::kNaN);
631 } else { 591 } else {
632 FAIL("Invalid member of stdlib"); 592 FAIL("Invalid member of stdlib");
633 } 593 }
634 } 594 }
635 595
636 // 6.2 ValidateExport 596 // 6.2 ValidateExport
637 void AsmJsParser::ValidateExport() { 597 void AsmJsParser::ValidateExport() {
638 // clang-format off 598 // clang-format off
639 EXPECT_TOKEN(TOK(return)); 599 EXPECT_TOKEN(TOK(return));
640 // clang format on 600 // clang format on
(...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after
728 if (!scanner_.IsGlobal()) { 688 if (!scanner_.IsGlobal()) {
729 FAIL("Expected function name"); 689 FAIL("Expected function name");
730 } 690 }
731 691
732 std::string function_name_raw = scanner_.GetIdentifierString(); 692 std::string function_name_raw = scanner_.GetIdentifierString();
733 AsmJsScanner::token_t function_name = Consume(); 693 AsmJsScanner::token_t function_name = Consume();
734 VarInfo* function_info = GetVarInfo(function_name); 694 VarInfo* function_info = GetVarInfo(function_name);
735 if (function_info->kind == VarKind::kUnused) { 695 if (function_info->kind == VarKind::kUnused) {
736 function_info->kind = VarKind::kFunction; 696 function_info->kind = VarKind::kFunction;
737 function_info->function_builder = module_builder_->AddFunction(); 697 function_info->function_builder = module_builder_->AddFunction();
738 // TODO(bradnelson): Cleanup memory management here.
739 // WasmModuleBuilder should own these.
740 char* function_name = zone()->NewArray<char>(function_name_raw.size());
741 memcpy(function_name, function_name_raw.data(), function_name_raw.size());
742 function_info->function_builder->SetName( 698 function_info->function_builder->SetName(
743 {function_name, static_cast<int>(function_name_raw.size())}); 699 {function_name_raw.c_str(),
700 static_cast<int>(function_name_raw.size())});
744 function_info->index = function_info->function_builder->func_index(); 701 function_info->index = function_info->function_builder->func_index();
745 function_info->function_defined = true; 702 function_info->function_defined = true;
746 } else if (function_info->function_defined) { 703 } else if (function_info->function_defined) {
747 FAIL("Function redefined"); 704 FAIL("Function redefined");
748 } else {
749 function_info->function_defined = true;
750 } 705 }
751 current_function_builder_ = function_info->function_builder; 706 current_function_builder_ = function_info->function_builder;
752 return_type_ = nullptr; 707 return_type_ = nullptr;
753 708
754 // Record start of the function, used as position for the stack check. 709 // Record start of the function, used as position for the stack check.
755 current_function_builder_->SetAsmFunctionStartPosition(start_position); 710 current_function_builder_->SetAsmFunctionStartPosition(start_position);
756 711
757 std::vector<AsmType*> params; 712 std::vector<AsmType*> params;
758 ValidateFunctionParams(&params); 713 ValidateFunctionParams(&params);
759 std::vector<ValueType> locals; 714 std::vector<ValueType> locals;
(...skipping 133 matching lines...) Expand 10 before | Expand all | Expand 10 after
893 // Store types. 848 // Store types.
894 EXPECT_TOKEN('='); 849 EXPECT_TOKEN('=');
895 double dvalue = 0.0; 850 double dvalue = 0.0;
896 uint64_t uvalue = 0; 851 uint64_t uvalue = 0;
897 if (Check('-')) { 852 if (Check('-')) {
898 if (CheckForDouble(&dvalue)) { 853 if (CheckForDouble(&dvalue)) {
899 info->kind = VarKind::kLocal; 854 info->kind = VarKind::kLocal;
900 info->type = AsmType::Double(); 855 info->type = AsmType::Double();
901 info->index = static_cast<uint32_t>(param_count + locals->size()); 856 info->index = static_cast<uint32_t>(param_count + locals->size());
902 locals->push_back(kWasmF64); 857 locals->push_back(kWasmF64);
903 byte code[] = {WASM_F64(-dvalue)}; 858 byte code[] = {WASM_F64(dvalue)};
904 current_function_builder_->EmitCode(code, sizeof(code)); 859 current_function_builder_->EmitCode(code, sizeof(code));
905 current_function_builder_->EmitSetLocal(info->index); 860 current_function_builder_->EmitSetLocal(info->index);
906 } else if (CheckForUnsigned(&uvalue)) { 861 } else if (CheckForUnsigned(&uvalue)) {
907 if (uvalue > 0x7fffffff) { 862 if (uvalue > 0x7fffffff) {
908 FAIL("Numeric literal out of range"); 863 FAIL("Numeric literal out of range");
909 } 864 }
910 info->kind = VarKind::kLocal; 865 info->kind = VarKind::kLocal;
911 info->type = AsmType::Int(); 866 info->type = AsmType::Int();
912 info->index = static_cast<uint32_t>(param_count + locals->size()); 867 info->index = static_cast<uint32_t>(param_count + locals->size());
913 locals->push_back(kWasmI32); 868 locals->push_back(kWasmI32);
(...skipping 1187 matching lines...) Expand 10 before | Expand all | Expand 10 after
2101 } 2056 }
2102 if (return_type->IsA(AsmType::Float())) { 2057 if (return_type->IsA(AsmType::Float())) {
2103 FAILn("Imported function can't be called as float"); 2058 FAILn("Imported function can't be called as float");
2104 } 2059 }
2105 DCHECK(function_info->import != nullptr); 2060 DCHECK(function_info->import != nullptr);
2106 // TODO(bradnelson): Factor out. 2061 // TODO(bradnelson): Factor out.
2107 uint32_t cache_index = function_info->import->cache.FindOrInsert(sig); 2062 uint32_t cache_index = function_info->import->cache.FindOrInsert(sig);
2108 uint32_t index; 2063 uint32_t index;
2109 if (cache_index >= function_info->import->cache_index.size()) { 2064 if (cache_index >= function_info->import->cache_index.size()) {
2110 index = module_builder_->AddImport( 2065 index = module_builder_->AddImport(
2111 function_info->import->function_name, 2066 function_info->import->name.data(),
2112 static_cast<uint32_t>(function_info->import->function_name_size), 2067 static_cast<uint32_t>(function_info->import->name.size()), sig);
2113 sig);
2114 function_info->import->cache_index.push_back(index); 2068 function_info->import->cache_index.push_back(index);
2115 } else { 2069 } else {
2116 index = function_info->import->cache_index[cache_index]; 2070 index = function_info->import->cache_index[cache_index];
2117 } 2071 }
2118 current_function_builder_->Emit(kExprCallFunction); 2072 current_function_builder_->Emit(kExprCallFunction);
2119 current_function_builder_->EmitVarUint(index); 2073 current_function_builder_->EmitVarUint(index);
2120 } else if (function_info->type->IsA(AsmType::None())) { 2074 } else if (function_info->type->IsA(AsmType::None())) {
2121 function_info->type = function_type; 2075 function_info->type = function_type;
2122 if (function_info->kind == VarKind::kTable) { 2076 if (function_info->kind == VarKind::kTable) {
2123 current_function_builder_->EmitGetLocal(tmp); 2077 current_function_builder_->EmitGetLocal(tmp);
(...skipping 288 matching lines...) Expand 10 before | Expand all | Expand 10 after
2412 break; 2366 break;
2413 } 2367 }
2414 scanner_.Next(); 2368 scanner_.Next();
2415 } 2369 }
2416 scanner_.Seek(start); 2370 scanner_.Seek(start);
2417 } 2371 }
2418 2372
2419 } // namespace wasm 2373 } // namespace wasm
2420 } // namespace internal 2374 } // namespace internal
2421 } // namespace v8 2375 } // namespace v8
OLDNEW
« no previous file with comments | « src/asmjs/asm-parser.h ('k') | src/asmjs/asm-scanner.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698