Showing the changes to src/exec/debugger.cpp (1 of 3 files). Show all
Unified · Split
| @@ -73,8 +73,26 @@ namespace · 72 unmodified lines | ||
| 73 | 73 | << "\n"; |
| 74 | 74 | } |
| 75 | 75 | |
| 76 | + // Debug command context | |
| 77 | + struct dbg_cmd_cxt_t | |
| 78 | + { | |
| 79 | + dbg_cmd_cxt_t(const vm_t &v, const vm_registers_t &r) | |
| 80 | + : initial_register{ r } | |
| 81 | + , exit_break{ false } | |
| 82 | + , vm{ v } | |
| 83 | + { | |
| 84 | + current_register = &initial_register; | |
| 85 | + } | |
| 86 | + | |
| 87 | + bool exit_break; | |
| 88 | + | |
| 89 | + const vm_t &vm; | |
| 90 | + const vm_registers_t *current_register; | |
| 91 | + const vm_registers_t &initial_register; | |
| 92 | + }; | |
| 93 | + | |
| 76 | 94 | // Debug command exec operation |
| 77 | − using dbg_cmd_exec_t = bool(*)(const std::vector<std::string> &, const vm_t &, const vm_registers_t &); | |
| 95 | + using dbg_cmd_exec_t = void(*)(const std::vector<std::string> &, dbg_cmd_cxt_t &cxt); | |
| 78 | 96 | |
| 79 | 97 | // Debug command |
| 80 | 98 | struct dbg_cmd_t |
| @@ -118,9 +136,49 @@ namespace · 37 unmodified lines | ||
| 118 | 136 | |
| 119 | 137 | std::ostream& operator<<(std::ostream &ss, const vm_string_t *s) |
| 120 | 138 | { |
| 139 | + auto escaped_string = std::stringstream{}; | |
| 140 | + auto str = s->str(); | |
| 141 | + if (str != nullptr) | |
| 142 | + { | |
| 143 | + while (*str != '\0') | |
| 144 | + { | |
| 145 | + switch (*str) | |
| 146 | + { | |
| 147 | + case '\\': | |
| 148 | + escaped_string << "\\\\"; | |
| 149 | + break; | |
| 150 | + case '\a': | |
| 151 | + escaped_string << "\\a"; | |
| 152 | + break; | |
| 153 | + case '\b': | |
| 154 | + escaped_string << "\\b"; | |
| 155 | + break; | |
| 156 | + case '\f': | |
| 157 | + escaped_string << "\\f"; | |
| 158 | + break; | |
| 159 | + case '\n': | |
| 160 | + escaped_string << "\\n"; | |
| 161 | + break; | |
| 162 | + case '\r': | |
| 163 | + escaped_string << "\\r"; | |
| 164 | + break; | |
| 165 | + case '\t': | |
| 166 | + escaped_string << "\\t"; | |
| 167 | + break; | |
| 168 | + case '\v': | |
| 169 | + escaped_string << "\\v"; | |
| 170 | + break; | |
| 171 | + default: | |
| 172 | + escaped_string << *str; | |
| 173 | + } | |
| 174 | + | |
| 175 | + str++; | |
| 176 | + } | |
| 177 | + } | |
| 178 | + | |
| 121 | 179 | ss << "string\n" |
| 122 | 180 | << "\tlength: " << s->get_length() << "\n" |
| 123 | − << "\tvalue: " << s->str() << "\n"; | |
| 181 | + << "\tvalue: \"" << escaped_string.str() << "\"\n"; | |
| 124 | 182 | |
| 125 | 183 | return ss; |
| 126 | 184 | } |
| @@ -149,10 +207,33 @@ namespace · 22 unmodified lines | ||
| 149 | 207 | return ss; |
| 150 | 208 | } |
| 151 | 209 | |
| 152 | − std::ostream& operator<<(std::ostream &ss, const vm_alloc_t *a) | |
| 210 | + std::ostream& operator<<(std::ostream &ss, const vm_alloc_t *alloc) | |
| 153 | 211 | { |
| 154 | − ss << "alloc\n" | |
| 155 | − << "\tsize: " << a->alloc_type->size_in_bytes << "\n"; | |
| 212 | + if (alloc == nullptr) | |
| 213 | + { | |
| 214 | + ss << "<nil>"; | |
| 215 | + } | |
| 216 | + else if (alloc->alloc_type == intrinsic_type_desc::type<vm_string_t>()) | |
| 217 | + { | |
| 218 | + ss << vm_alloc_t::from_allocation<vm_string_t>(alloc->get_allocation()); | |
| 219 | + } | |
| 220 | + else if (alloc->alloc_type == intrinsic_type_desc::type<vm_array_t>()) | |
| 221 | + { | |
| 222 | + ss << vm_alloc_t::from_allocation<vm_array_t>(alloc->get_allocation()); | |
| 223 | + } | |
| 224 | + else if (alloc->alloc_type == intrinsic_type_desc::type<vm_list_t>()) | |
| 225 | + { | |
| 226 | + ss << vm_alloc_t::from_allocation<vm_list_t>(alloc->get_allocation()); | |
| 227 | + } | |
| 228 | + else if (alloc->alloc_type == intrinsic_type_desc::type<vm_channel_t>()) | |
| 229 | + { | |
| 230 | + ss << vm_alloc_t::from_allocation<vm_channel_t>(alloc->get_allocation()); | |
| 231 | + } | |
| 232 | + else | |
| 233 | + { | |
| 234 | + ss << "alloc\n" | |
| 235 | + << "\tsize: " << alloc->alloc_type->size_in_bytes << "\n"; | |
| 236 | + } | |
| 156 | 237 | |
| 157 | 238 | return ss; |
| 158 | 239 | } |
| @@ -210,72 +291,54 @@ namespace · 51 unmodified lines | ||
| 210 | 291 | auto frame_string = std::stringstream{}; |
| 211 | 292 | enum_pointer_fields(*frame->frame_type, frame->base(), [&frame_string](pointer_t p) |
| 212 | 293 | { |
| 213 | − frame_string << std::showbase << std::hex << reinterpret_cast<uintptr_t>(p) << " : " << std::dec; | |
| 214 | − | |
| 215 | − auto alloc = vm_alloc_t::from_allocation(p); | |
| 216 | − if (alloc->alloc_type == intrinsic_type_desc::type<vm_string_t>()) | |
| 217 | − frame_string << vm_alloc_t::from_allocation<vm_string_t>(p); | |
| 218 | − else if (alloc->alloc_type == intrinsic_type_desc::type<vm_array_t>()) | |
| 219 | − frame_string << vm_alloc_t::from_allocation<vm_array_t>(p); | |
| 220 | − else if (alloc->alloc_type == intrinsic_type_desc::type<vm_list_t>()) | |
| 221 | − frame_string << vm_alloc_t::from_allocation<vm_list_t>(p); | |
| 222 | − else if (alloc->alloc_type == intrinsic_type_desc::type<vm_channel_t>()) | |
| 223 | − frame_string << vm_alloc_t::from_allocation<vm_channel_t>(p); | |
| 224 | − else | |
| 225 | − frame_string << alloc; | |
| 294 | + frame_string << std::showbase << std::hex << reinterpret_cast<uintptr_t>(p) << " : " << std::dec << vm_alloc_t::from_allocation(p); | |
| 226 | 295 | }); |
| 227 | 296 | |
| 228 | 297 | return frame_string.str(); |
| 229 | 298 | } |
| 230 | 299 | |
| 231 | − bool cmd_continue(const std::vector<std::string> &, const vm_t &, const vm_registers_t &) | |
| 300 | + void cmd_continue(const std::vector<std::string> &, dbg_cmd_cxt_t &cxt) | |
| 232 | 301 | { |
| 233 | − return false; | |
| 302 | + cxt.exit_break = true; | |
| 234 | 303 | } |
| 235 | 304 | |
| 236 | − bool cmd_print_registers(const std::vector<std::string> &, const vm_t &, const vm_registers_t &r) | |
| 305 | + void cmd_print_registers(const std::vector<std::string> &, dbg_cmd_cxt_t &cxt) | |
| 237 | 306 | { |
| 238 | − auto s = registers_to_string(r); | |
| 307 | + auto s = registers_to_string(*cxt.current_register); | |
| 239 | 308 | debug_print_info(s); |
| 240 | − | |
| 241 | − return true; | |
| 242 | 309 | } |
| 243 | 310 | |
| 244 | − bool cmd_print_threads(const std::vector<std::string> &, const vm_t &vm, const vm_registers_t &) | |
| 311 | + void cmd_print_threads(const std::vector<std::string> &, dbg_cmd_cxt_t &cxt) | |
| 245 | 312 | { |
| 246 | 313 | auto msg = std::stringstream{}; |
| 247 | 314 | msg << "Threads:\n"; |
| 248 | − for (auto t : vm.get_scheduler_control().get_all_threads()) | |
| 315 | + for (auto t : cxt.vm.get_scheduler_control().get_all_threads()) | |
| 249 | 316 | msg << " " << std::setw(5) << t->get_thread_id() << " - " << t->get_state() << "\n"; |
| 250 | 317 | |
| 251 | 318 | debug_print_info(msg.str()); |
| 252 | − | |
| 253 | − return true; | |
| 254 | 319 | } |
| 255 | 320 | |
| 256 | − bool cmd_print_modules(const std::vector<std::string> &, const vm_t &vm, const vm_registers_t &) | |
| 321 | + void cmd_print_modules(const std::vector<std::string> &, dbg_cmd_cxt_t &cxt) | |
| 257 | 322 | { |
| 258 | 323 | auto msg = std::stringstream{}; |
| 259 | 324 | msg << "Modules:\n"; |
| 260 | − for (auto m : vm.get_loaded_modules()) | |
| 325 | + for (auto m : cxt.vm.get_loaded_modules()) | |
| 261 | 326 | msg << " " << m->module_name->str() << "\n"; |
| 262 | 327 | |
| 263 | 328 | debug_print_info(msg.str()); |
| 264 | − | |
| 265 | − return true; | |
| 266 | 329 | } |
| 267 | 330 | |
| 268 | − bool cmd_stacktrace(const std::vector<std::string> &a, const vm_t &vm, const vm_registers_t &r) | |
| 331 | + void cmd_stacktrace(const std::vector<std::string> &a, dbg_cmd_cxt_t &cxt) | |
| 269 | 332 | { |
| 270 | 333 | auto thread_registers = std::map<uint32_t, const vm_registers_t*>{}; |
| 271 | 334 | if (a.size() == 1) |
| 272 | 335 | { |
| 273 | − thread_registers[r.thread.get_thread_id()] = &r; | |
| 336 | + thread_registers[cxt.current_register->thread.get_thread_id()] = cxt.current_register; | |
| 274 | 337 | } |
| 275 | 338 | else |
| 276 | 339 | { |
| 277 | 340 | uint32_t thread_id; |
| 278 | − auto all_threads = vm.get_scheduler_control().get_all_threads(); | |
| 341 | + auto all_threads = cxt.vm.get_scheduler_control().get_all_threads(); | |
| 279 | 342 | if (a[1].compare("*") == 0) |
| 280 | 343 | { |
| 281 | 344 | thread_id = 0; |
| @@ -289,7 +352,7 @@ namespace · 7 unmodified lines | ||
| 289 | 352 | catch (...) |
| 290 | 353 | { |
| 291 | 354 | debug_print_error("Invalid thread ID format"); |
| 292 | − return true; | |
| 355 | + return; | |
| 293 | 356 | } |
| 294 | 357 | } |
| 295 | 358 | |
| @@ -311,7 +374,7 @@ namespace · 15 unmodified lines | ||
| 311 | 374 | if (thread_registers.empty()) |
| 312 | 375 | { |
| 313 | 376 | debug_print_error("Unknown thread ID"); |
| 314 | − return true; | |
| 377 | + return; | |
| 315 | 378 | } |
| 316 | 379 | |
| 317 | 380 | auto msg = std::stringstream{}; |
| @@ -323,19 +386,14 @@ namespace · 5 unmodified lines | ||
| 323 | 386 | } |
| 324 | 387 | |
| 325 | 388 | debug_print_info(msg.str()); |
| 326 | − | |
| 327 | − return true; | |
| 328 | 389 | } |
| 329 | 390 | |
| 330 | − // Forward declaration | |
| 331 | − void prompt(const vm_t &, const vm_registers_t &); | |
| 332 | − | |
| 333 | − bool cmd_switchthread(const std::vector<std::string> &a, const vm_t &vm, const vm_registers_t &r) | |
| 391 | + void cmd_switchthread(const std::vector<std::string> &a, dbg_cmd_cxt_t &cxt) | |
| 334 | 392 | { |
| 335 | 393 | if (a.size() == 1) |
| 336 | 394 | { |
| 337 | 395 | debug_print_error("Must supply a target thread ID"); |
| 338 | − return true; | |
| 396 | + return; | |
| 339 | 397 | } |
| 340 | 398 | |
| 341 | 399 | uint32_t thread_id; |
| @@ -346,10 +404,10 @@ namespace · 4 unmodified lines | ||
| 346 | 404 | catch (...) |
| 347 | 405 | { |
| 348 | 406 | debug_print_error("Invalid thread ID format"); |
| 349 | − return true; | |
| 407 | + return; | |
| 350 | 408 | } |
| 351 | 409 | |
| 352 | − auto all_threads = vm.get_scheduler_control().get_all_threads(); | |
| 410 | + auto all_threads = cxt.vm.get_scheduler_control().get_all_threads(); | |
| 353 | 411 | |
| 354 | 412 | // Find the thread |
| 355 | 413 | for (auto t : all_threads) |
| @@ -357,18 +415,15 @@ namespace · 1 unmodified line | ||
| 357 | 415 | const auto tid = t->get_thread_id(); |
| 358 | 416 | if (tid == thread_id) |
| 359 | 417 | { |
| 360 | − prompt(vm, t->get_registers()); | |
| 361 | − | |
| 362 | − // If we are returning from a thread switch we are not continuing, so return false to indicate as such. | |
| 363 | − return false; | |
| 418 | + cxt.current_register = &t->get_registers(); | |
| 419 | + return; | |
| 364 | 420 | } |
| 365 | 421 | } |
| 366 | 422 | |
| 367 | 423 | debug_print_error("Unknown thread ID"); |
| 368 | − return true; | |
| 369 | 424 | } |
| 370 | 425 | |
| 371 | − bool cmd_disassemble(const std::vector<std::string> &a, const vm_t &, const vm_registers_t &r) | |
| 426 | + void cmd_disassemble(const std::vector<std::string> &a, dbg_cmd_cxt_t &cxt) | |
| 372 | 427 | { |
| 373 | 428 | auto disassemble_length = 1; |
| 374 | 429 | if (a.size() > 1) |
| @@ -380,10 +435,11 @@ namespace · 5 unmodified lines | ||
| 380 | 435 | catch (...) |
| 381 | 436 | { |
| 382 | 437 | debug_print_error("Invalid disassemble count format"); |
| 383 | − return true; | |
| 438 | + return; | |
| 384 | 439 | } |
| 385 | 440 | } |
| 386 | 441 | |
| 442 | + const auto &r = *cxt.current_register; | |
| 387 | 443 | const auto &code_section = r.module_ref->code_section; |
| 388 | 444 | |
| 389 | 445 | auto begin_index = static_cast<std::size_t>(r.pc); |
| @@ -404,21 +460,122 @@ namespace · 14 unmodified lines | ||
| 404 | 460 | msg << " " << std::setw(5) << i << ": " << code_section[i].op << "\n"; |
| 405 | 461 | |
| 406 | 462 | debug_print_info(msg.str()); |
| 463 | + } | |
| 464 | + | |
| 465 | + void cmd_examine(const std::vector<std::string> &a, dbg_cmd_cxt_t &cxt) | |
| 466 | + { | |
| 467 | + if (a.size() < 3) | |
| 468 | + { | |
| 469 | + debug_print_error("Invalid number of arguments"); | |
| 470 | + return; | |
| 471 | + } | |
| 407 | 472 | |
| 408 | − return true; | |
| 473 | + std::shared_ptr<const type_descriptor_t> base_type; | |
| 474 | + word_t *base_pointer; | |
| 475 | + auto &base_ptr_id = a[1]; | |
| 476 | + if (base_ptr_id.compare("mp") == 0) | |
| 477 | + { | |
| 478 | + base_type = cxt.current_register->mp_base->alloc_type; | |
| 479 | + base_pointer = reinterpret_cast<word_t *>(cxt.current_register->mp_base->get_allocation()); | |
| 480 | + } | |
| 481 | + else if (base_ptr_id.compare("fp") == 0) | |
| 482 | + { | |
| 483 | + auto frame = cxt.current_register->stack.peek_frame(); | |
| 484 | + if (frame == nullptr) | |
| 485 | + { | |
| 486 | + debug_print_error("Invalid frame pointer "); | |
| 487 | + return; | |
| 488 | + } | |
| 489 | + | |
| 490 | + base_type = frame->frame_type; | |
| 491 | + base_pointer = reinterpret_cast<word_t *>(frame->base()); | |
| 492 | + } | |
| 493 | + else | |
| 494 | + { | |
| 495 | + debug_print_error("Invalid memory base pointer"); | |
| 496 | + return; | |
| 497 | + } | |
| 498 | + | |
| 499 | + assert(base_type != nullptr && base_pointer != nullptr); | |
| 500 | + | |
| 501 | + const auto pointer_size = static_cast<int>(sizeof(pointer_t)); | |
| 502 | + auto byte_offset1 = int{ 0 }; | |
| 503 | + auto byte_offset2 = int{ -1 }; | |
| 504 | + try | |
| 505 | + { | |
| 506 | + byte_offset1 = std::stoi(a[2]); | |
| 507 | + | |
| 508 | + // The 4th argument is optional | |
| 509 | + if (a.size() >= 4) | |
| 510 | + byte_offset2 = std::stoi(a[3]); | |
| 511 | + } | |
| 512 | + catch (...) | |
| 513 | + { | |
| 514 | + debug_print_error("Invalid memory offset format"); | |
| 515 | + return; | |
| 516 | + } | |
| 517 | + | |
| 518 | + if (0 > byte_offset1 || byte_offset1 >= base_type->size_in_bytes) | |
| 519 | + { | |
| 520 | + debug_print_error("First offset out of range"); | |
| 521 | + return; | |
| 522 | + } | |
| 523 | + | |
| 524 | + auto msg = std::stringstream{}; | |
| 525 | + if (byte_offset2 != -1) | |
| 526 | + msg << " " << byte_offset2 << "(" << byte_offset1 << "(" << a[1] << ")):\n"; | |
| 527 | + else | |
| 528 | + msg << " " << byte_offset1 << "(" << a[1] << "):\n"; | |
| 529 | + | |
| 530 | + auto value_is_pointer = bool{ false }; | |
| 531 | + const auto pointer_offset1 = byte_offset1 / pointer_size; | |
| 532 | + auto value_to_examine = reinterpret_cast<pointer_t>(base_pointer[pointer_offset1]); | |
| 533 | + if (runtime::is_offset_pointer(*base_type, pointer_offset1)) | |
| 534 | + { | |
| 535 | + value_is_pointer = true; | |
| 536 | + auto alloc_to_examine = vm_alloc_t::from_allocation(value_to_examine); | |
| 537 | + if (byte_offset2 != -1) | |
| 538 | + { | |
| 539 | + if (alloc_to_examine == nullptr) | |
| 540 | + { | |
| 541 | + debug_print_error("First offset is nil"); | |
| 542 | + return; | |
| 543 | + } | |
| 544 | + | |
| 545 | + if (0 > byte_offset2 || byte_offset2 >= alloc_to_examine->alloc_type->size_in_bytes) | |
| 546 | + { | |
| 547 | + debug_print_error("Second offset out of range"); | |
| 548 | + return; | |
| 549 | + } | |
| 550 | + | |
| 551 | + // Update data pointer for 2nd indirection | |
| 552 | + base_pointer = reinterpret_cast<word_t *>(alloc_to_examine->get_allocation()); | |
| 553 | + base_type = alloc_to_examine->alloc_type; | |
| 554 | + | |
| 555 | + const auto pointer_offset2 = byte_offset2 / pointer_size; | |
| 556 | + value_to_examine = reinterpret_cast<pointer_t>(base_pointer[pointer_offset2]); | |
| 557 | + value_is_pointer = runtime::is_offset_pointer(*base_type, pointer_offset2); | |
| 558 | + } | |
| 559 | + } | |
| 560 | + | |
| 561 | + if (value_is_pointer) | |
| 562 | + msg << " " << vm_alloc_t::from_allocation(value_to_examine); | |
| 563 | + else | |
| 564 | + msg << " " << std::setw(8) << std::showbase << std::hex << value_to_examine << std::dec; | |
| 565 | + | |
| 566 | + debug_print_info(msg.str()); | |
| 409 | 567 | } |
| 410 | 568 | |
| 411 | 569 | // Forward declaration |
| 412 | 570 | void print_help(); |
| 413 | 571 | |
| 414 | − bool cmd_help(const std::vector<std::string> &, const vm_t &, const vm_registers_t &) | |
| 572 | + void cmd_help(const std::vector<std::string> &, dbg_cmd_cxt_t &) | |
| 415 | 573 | { |
| 416 | 574 | print_help(); |
| 417 | − return true; | |
| 418 | 575 | } |
| 419 | 576 | |
| 420 | − // Easter egg | |
| 421 | − bool cmd_tlk_tilde_star(const std::vector<std::string> &, const vm_t &, const vm_registers_t &) | |
| 577 | + // ee | |
| 578 | + void cmd_tlk_tilde_star(const std::vector<std::string> &, dbg_cmd_cxt_t &) | |
| 422 | 579 | { |
| 423 | 580 | std::cout |
| 424 | 581 | << console_modifiers::black_font |
| @@ -460,8 +617,6 @@ namespace · 35 unmodified lines | ||
| 460 | 617 | << console_modifiers::bold |
| 461 | 618 | << "!\n" |
| 462 | 619 | << console_modifiers::reset_all; |
| 463 | − | |
| 464 | − return true; | |
| 465 | 620 | } |
| 466 | 621 | |
| 467 | 622 | const dbg_cmd_t cmds[] = |
| @@ -473,7 +628,10 @@ namespace · 5 unmodified lines | ||
| 473 | 628 | { "sw", "Stack walk/back trace", "bt ([0-9]+|\\*)?", "bt, bt 34, bt *", cmd_stacktrace }, |
| 474 | 629 | { "st", "Switch to thread", "st [0-9]+", "st 42", cmd_switchthread }, |
| 475 | 630 | { "d", "Disassemble the next/previous N instructions", "d (-?[0-9]+)?", "d, d -4, d 5", cmd_disassemble }, |
| 631 | + { "x", "Examine memory", "x [mp|fp] [0-9]+ ([0-9]+)?", "x mp 3, x fp 20 6", cmd_examine }, | |
| 476 | 632 | { "?", "Print help", nullptr, nullptr, cmd_help }, |
| 633 | + | |
| 634 | + // Undocumented commands | |
| 477 | 635 | { "tlk~*", nullptr, nullptr, nullptr, cmd_tlk_tilde_star } |
| 478 | 636 | }; |
| 479 | 637 | |
| @@ -548,12 +706,14 @@ namespace · 68 unmodified lines | ||
| 548 | 706 | |
| 549 | 707 | std::locale loc; |
| 550 | 708 | std::string cmd; |
| 709 | + | |
| 710 | + dbg_cmd_cxt_t cxt{ vm, r }; | |
| 551 | 711 | for (;;) |
| 552 | 712 | { |
| 553 | 713 | std::cout |
| 554 | 714 | << console_modifiers::green_font |
| 555 | 715 | << console_modifiers::bold |
| 556 | − << r.thread.get_thread_id() | |
| 716 | + << cxt.current_register->thread.get_thread_id() | |
| 557 | 717 | << " >>> " |
| 558 | 718 | << console_modifiers::reset_all; |
| 559 | 719 | |
| @@ -566,16 +726,15 @@ namespace · 6 unmodified lines | ||
| 566 | 726 | continue; |
| 567 | 727 | |
| 568 | 728 | auto cmd_exec = find_cmd(cmd_tokens[0]); |
| 569 | − if (cmd_exec != nullptr) | |
| 570 | − { | |
| 571 | − if (!cmd_exec(cmd_tokens, vm, r)) | |
| 572 | − break; | |
| 573 | − } | |
| 574 | − else | |
| 729 | + if (cmd_exec == nullptr) | |
| 575 | 730 | { |
| 576 | − debug_print_error("Unknown command"); | |
| 577 | − print_help(); | |
| 731 | + debug_print_error("Unknown command. '?' for help."); | |
| 732 | + continue; | |
| 578 | 733 | } |
| 734 | + | |
| 735 | + cmd_exec(cmd_tokens, cxt); | |
| 736 | + if (cxt.exit_break) | |
| 737 | + break; | |
| 579 | 738 | } |
| 580 | 739 | } |
| 581 | 740 | } |