Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
michael@0 | 2 | * vim: set ts=8 sts=4 et sw=4 tw=99: |
michael@0 | 3 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include "jit/LIR.h" |
michael@0 | 8 | |
michael@0 | 9 | #include <ctype.h> |
michael@0 | 10 | |
michael@0 | 11 | #include "jsprf.h" |
michael@0 | 12 | |
michael@0 | 13 | #include "jit/IonSpewer.h" |
michael@0 | 14 | #include "jit/MIR.h" |
michael@0 | 15 | #include "jit/MIRGenerator.h" |
michael@0 | 16 | |
michael@0 | 17 | using namespace js; |
michael@0 | 18 | using namespace js::jit; |
michael@0 | 19 | |
michael@0 | 20 | LIRGraph::LIRGraph(MIRGraph *mir) |
michael@0 | 21 | : blocks_(mir->alloc()), |
michael@0 | 22 | constantPool_(mir->alloc()), |
michael@0 | 23 | constantPoolMap_(mir->alloc()), |
michael@0 | 24 | safepoints_(mir->alloc()), |
michael@0 | 25 | nonCallSafepoints_(mir->alloc()), |
michael@0 | 26 | numVirtualRegisters_(0), |
michael@0 | 27 | numInstructions_(1), // First id is 1. |
michael@0 | 28 | localSlotCount_(0), |
michael@0 | 29 | argumentSlotCount_(0), |
michael@0 | 30 | entrySnapshot_(nullptr), |
michael@0 | 31 | osrBlock_(nullptr), |
michael@0 | 32 | mir_(*mir) |
michael@0 | 33 | { |
michael@0 | 34 | } |
michael@0 | 35 | |
michael@0 | 36 | bool |
michael@0 | 37 | LIRGraph::addConstantToPool(const Value &v, uint32_t *index) |
michael@0 | 38 | { |
michael@0 | 39 | JS_ASSERT(constantPoolMap_.initialized()); |
michael@0 | 40 | |
michael@0 | 41 | ConstantPoolMap::AddPtr p = constantPoolMap_.lookupForAdd(v); |
michael@0 | 42 | if (p) { |
michael@0 | 43 | *index = p->value(); |
michael@0 | 44 | return true; |
michael@0 | 45 | } |
michael@0 | 46 | *index = constantPool_.length(); |
michael@0 | 47 | return constantPool_.append(v) && constantPoolMap_.add(p, v, *index); |
michael@0 | 48 | } |
michael@0 | 49 | |
michael@0 | 50 | bool |
michael@0 | 51 | LIRGraph::noteNeedsSafepoint(LInstruction *ins) |
michael@0 | 52 | { |
michael@0 | 53 | // Instructions with safepoints must be in linear order. |
michael@0 | 54 | JS_ASSERT_IF(!safepoints_.empty(), safepoints_.back()->id() < ins->id()); |
michael@0 | 55 | if (!ins->isCall() && !nonCallSafepoints_.append(ins)) |
michael@0 | 56 | return false; |
michael@0 | 57 | return safepoints_.append(ins); |
michael@0 | 58 | } |
michael@0 | 59 | |
michael@0 | 60 | void |
michael@0 | 61 | LIRGraph::removeBlock(size_t i) |
michael@0 | 62 | { |
michael@0 | 63 | blocks_.erase(blocks_.begin() + i); |
michael@0 | 64 | } |
michael@0 | 65 | |
michael@0 | 66 | uint32_t |
michael@0 | 67 | LBlock::firstId() |
michael@0 | 68 | { |
michael@0 | 69 | if (phis_.length()) { |
michael@0 | 70 | return phis_[0]->id(); |
michael@0 | 71 | } else { |
michael@0 | 72 | for (LInstructionIterator i(instructions_.begin()); i != instructions_.end(); i++) { |
michael@0 | 73 | if (i->id()) |
michael@0 | 74 | return i->id(); |
michael@0 | 75 | } |
michael@0 | 76 | } |
michael@0 | 77 | return 0; |
michael@0 | 78 | } |
michael@0 | 79 | uint32_t |
michael@0 | 80 | LBlock::lastId() |
michael@0 | 81 | { |
michael@0 | 82 | LInstruction *last = *instructions_.rbegin(); |
michael@0 | 83 | JS_ASSERT(last->id()); |
michael@0 | 84 | // The last instruction is a control flow instruction which does not have |
michael@0 | 85 | // any output. |
michael@0 | 86 | JS_ASSERT(last->numDefs() == 0); |
michael@0 | 87 | return last->id(); |
michael@0 | 88 | } |
michael@0 | 89 | |
michael@0 | 90 | LMoveGroup * |
michael@0 | 91 | LBlock::getEntryMoveGroup(TempAllocator &alloc) |
michael@0 | 92 | { |
michael@0 | 93 | if (entryMoveGroup_) |
michael@0 | 94 | return entryMoveGroup_; |
michael@0 | 95 | entryMoveGroup_ = LMoveGroup::New(alloc); |
michael@0 | 96 | if (begin()->isLabel()) |
michael@0 | 97 | insertAfter(*begin(), entryMoveGroup_); |
michael@0 | 98 | else |
michael@0 | 99 | insertBefore(*begin(), entryMoveGroup_); |
michael@0 | 100 | return entryMoveGroup_; |
michael@0 | 101 | } |
michael@0 | 102 | |
michael@0 | 103 | LMoveGroup * |
michael@0 | 104 | LBlock::getExitMoveGroup(TempAllocator &alloc) |
michael@0 | 105 | { |
michael@0 | 106 | if (exitMoveGroup_) |
michael@0 | 107 | return exitMoveGroup_; |
michael@0 | 108 | exitMoveGroup_ = LMoveGroup::New(alloc); |
michael@0 | 109 | insertBefore(*rbegin(), exitMoveGroup_); |
michael@0 | 110 | return exitMoveGroup_; |
michael@0 | 111 | } |
michael@0 | 112 | |
michael@0 | 113 | static size_t |
michael@0 | 114 | TotalOperandCount(MResumePoint *mir) |
michael@0 | 115 | { |
michael@0 | 116 | size_t accum = mir->numOperands(); |
michael@0 | 117 | while ((mir = mir->caller())) |
michael@0 | 118 | accum += mir->numOperands(); |
michael@0 | 119 | return accum; |
michael@0 | 120 | } |
michael@0 | 121 | |
michael@0 | 122 | LRecoverInfo::LRecoverInfo(TempAllocator &alloc) |
michael@0 | 123 | : instructions_(alloc), |
michael@0 | 124 | recoverOffset_(INVALID_RECOVER_OFFSET) |
michael@0 | 125 | { } |
michael@0 | 126 | |
michael@0 | 127 | LRecoverInfo * |
michael@0 | 128 | LRecoverInfo::New(MIRGenerator *gen, MResumePoint *mir) |
michael@0 | 129 | { |
michael@0 | 130 | LRecoverInfo *recoverInfo = new(gen->alloc()) LRecoverInfo(gen->alloc()); |
michael@0 | 131 | if (!recoverInfo || !recoverInfo->init(mir)) |
michael@0 | 132 | return nullptr; |
michael@0 | 133 | |
michael@0 | 134 | IonSpew(IonSpew_Snapshots, "Generating LIR recover info %p from MIR (%p)", |
michael@0 | 135 | (void *)recoverInfo, (void *)mir); |
michael@0 | 136 | |
michael@0 | 137 | return recoverInfo; |
michael@0 | 138 | } |
michael@0 | 139 | |
michael@0 | 140 | bool |
michael@0 | 141 | LRecoverInfo::init(MResumePoint *rp) |
michael@0 | 142 | { |
michael@0 | 143 | MResumePoint *it = rp; |
michael@0 | 144 | |
michael@0 | 145 | // Sort operations in the order in which we need to restore the stack. This |
michael@0 | 146 | // implies that outer frames, as well as operations needed to recover the |
michael@0 | 147 | // current frame, are located before the current frame. The inner-most |
michael@0 | 148 | // resume point should be the last element in the list. |
michael@0 | 149 | do { |
michael@0 | 150 | if (!instructions_.append(it)) |
michael@0 | 151 | return false; |
michael@0 | 152 | it = it->caller(); |
michael@0 | 153 | } while (it); |
michael@0 | 154 | |
michael@0 | 155 | Reverse(instructions_.begin(), instructions_.end()); |
michael@0 | 156 | MOZ_ASSERT(mir() == rp); |
michael@0 | 157 | return true; |
michael@0 | 158 | } |
michael@0 | 159 | |
michael@0 | 160 | LSnapshot::LSnapshot(LRecoverInfo *recoverInfo, BailoutKind kind) |
michael@0 | 161 | : numSlots_(TotalOperandCount(recoverInfo->mir()) * BOX_PIECES), |
michael@0 | 162 | slots_(nullptr), |
michael@0 | 163 | recoverInfo_(recoverInfo), |
michael@0 | 164 | snapshotOffset_(INVALID_SNAPSHOT_OFFSET), |
michael@0 | 165 | bailoutId_(INVALID_BAILOUT_ID), |
michael@0 | 166 | bailoutKind_(kind) |
michael@0 | 167 | { } |
michael@0 | 168 | |
michael@0 | 169 | bool |
michael@0 | 170 | LSnapshot::init(MIRGenerator *gen) |
michael@0 | 171 | { |
michael@0 | 172 | slots_ = gen->allocate<LAllocation>(numSlots_); |
michael@0 | 173 | return !!slots_; |
michael@0 | 174 | } |
michael@0 | 175 | |
michael@0 | 176 | LSnapshot * |
michael@0 | 177 | LSnapshot::New(MIRGenerator *gen, LRecoverInfo *recover, BailoutKind kind) |
michael@0 | 178 | { |
michael@0 | 179 | LSnapshot *snapshot = new(gen->alloc()) LSnapshot(recover, kind); |
michael@0 | 180 | if (!snapshot || !snapshot->init(gen)) |
michael@0 | 181 | return nullptr; |
michael@0 | 182 | |
michael@0 | 183 | IonSpew(IonSpew_Snapshots, "Generating LIR snapshot %p from recover (%p)", |
michael@0 | 184 | (void *)snapshot, (void *)recover); |
michael@0 | 185 | |
michael@0 | 186 | return snapshot; |
michael@0 | 187 | } |
michael@0 | 188 | |
michael@0 | 189 | void |
michael@0 | 190 | LSnapshot::rewriteRecoveredInput(LUse input) |
michael@0 | 191 | { |
michael@0 | 192 | // Mark any operands to this snapshot with the same value as input as being |
michael@0 | 193 | // equal to the instruction's result. |
michael@0 | 194 | for (size_t i = 0; i < numEntries(); i++) { |
michael@0 | 195 | if (getEntry(i)->isUse() && getEntry(i)->toUse()->virtualRegister() == input.virtualRegister()) |
michael@0 | 196 | setEntry(i, LUse(input.virtualRegister(), LUse::RECOVERED_INPUT)); |
michael@0 | 197 | } |
michael@0 | 198 | } |
michael@0 | 199 | |
michael@0 | 200 | LPhi * |
michael@0 | 201 | LPhi::New(MIRGenerator *gen, MPhi *ins) |
michael@0 | 202 | { |
michael@0 | 203 | LPhi *phi = new (gen->alloc()) LPhi(); |
michael@0 | 204 | LAllocation *inputs = gen->allocate<LAllocation>(ins->numOperands()); |
michael@0 | 205 | if (!inputs) |
michael@0 | 206 | return nullptr; |
michael@0 | 207 | |
michael@0 | 208 | phi->inputs_ = inputs; |
michael@0 | 209 | phi->setMir(ins); |
michael@0 | 210 | return phi; |
michael@0 | 211 | } |
michael@0 | 212 | |
michael@0 | 213 | void |
michael@0 | 214 | LInstruction::printName(FILE *fp, Opcode op) |
michael@0 | 215 | { |
michael@0 | 216 | static const char * const names[] = |
michael@0 | 217 | { |
michael@0 | 218 | #define LIROP(x) #x, |
michael@0 | 219 | LIR_OPCODE_LIST(LIROP) |
michael@0 | 220 | #undef LIROP |
michael@0 | 221 | }; |
michael@0 | 222 | const char *name = names[op]; |
michael@0 | 223 | size_t len = strlen(name); |
michael@0 | 224 | for (size_t i = 0; i < len; i++) |
michael@0 | 225 | fprintf(fp, "%c", tolower(name[i])); |
michael@0 | 226 | } |
michael@0 | 227 | |
michael@0 | 228 | void |
michael@0 | 229 | LInstruction::printName(FILE *fp) |
michael@0 | 230 | { |
michael@0 | 231 | printName(fp, op()); |
michael@0 | 232 | } |
michael@0 | 233 | |
michael@0 | 234 | static const char * const TypeChars[] = |
michael@0 | 235 | { |
michael@0 | 236 | "g", // GENERAL |
michael@0 | 237 | "i", // INT32 |
michael@0 | 238 | "o", // OBJECT |
michael@0 | 239 | "s", // SLOTS |
michael@0 | 240 | "f", // FLOAT32 |
michael@0 | 241 | "d", // DOUBLE |
michael@0 | 242 | #ifdef JS_NUNBOX32 |
michael@0 | 243 | "t", // TYPE |
michael@0 | 244 | "p" // PAYLOAD |
michael@0 | 245 | #elif JS_PUNBOX64 |
michael@0 | 246 | "x" // BOX |
michael@0 | 247 | #endif |
michael@0 | 248 | }; |
michael@0 | 249 | |
michael@0 | 250 | static void |
michael@0 | 251 | PrintDefinition(FILE *fp, const LDefinition &def) |
michael@0 | 252 | { |
michael@0 | 253 | fprintf(fp, "[%s", TypeChars[def.type()]); |
michael@0 | 254 | if (def.virtualRegister()) |
michael@0 | 255 | fprintf(fp, ":%d", def.virtualRegister()); |
michael@0 | 256 | if (def.policy() == LDefinition::PRESET) { |
michael@0 | 257 | fprintf(fp, " (%s)", def.output()->toString()); |
michael@0 | 258 | } else if (def.policy() == LDefinition::MUST_REUSE_INPUT) { |
michael@0 | 259 | fprintf(fp, " (!)"); |
michael@0 | 260 | } else if (def.policy() == LDefinition::PASSTHROUGH) { |
michael@0 | 261 | fprintf(fp, " (-)"); |
michael@0 | 262 | } |
michael@0 | 263 | fprintf(fp, "]"); |
michael@0 | 264 | } |
michael@0 | 265 | |
michael@0 | 266 | #ifdef DEBUG |
michael@0 | 267 | static void |
michael@0 | 268 | PrintUse(char *buf, size_t size, const LUse *use) |
michael@0 | 269 | { |
michael@0 | 270 | switch (use->policy()) { |
michael@0 | 271 | case LUse::REGISTER: |
michael@0 | 272 | JS_snprintf(buf, size, "v%d:r", use->virtualRegister()); |
michael@0 | 273 | break; |
michael@0 | 274 | case LUse::FIXED: |
michael@0 | 275 | // Unfortunately, we don't know here whether the virtual register is a |
michael@0 | 276 | // float or a double. Should we steal a bit in LUse for help? For now, |
michael@0 | 277 | // nothing defines any fixed xmm registers. |
michael@0 | 278 | JS_snprintf(buf, size, "v%d:%s", use->virtualRegister(), |
michael@0 | 279 | Registers::GetName(Registers::Code(use->registerCode()))); |
michael@0 | 280 | break; |
michael@0 | 281 | case LUse::ANY: |
michael@0 | 282 | JS_snprintf(buf, size, "v%d:r?", use->virtualRegister()); |
michael@0 | 283 | break; |
michael@0 | 284 | case LUse::KEEPALIVE: |
michael@0 | 285 | JS_snprintf(buf, size, "v%d:*", use->virtualRegister()); |
michael@0 | 286 | break; |
michael@0 | 287 | case LUse::RECOVERED_INPUT: |
michael@0 | 288 | JS_snprintf(buf, size, "v%d:**", use->virtualRegister()); |
michael@0 | 289 | break; |
michael@0 | 290 | default: |
michael@0 | 291 | MOZ_ASSUME_UNREACHABLE("invalid use policy"); |
michael@0 | 292 | } |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | const char * |
michael@0 | 296 | LAllocation::toString() const |
michael@0 | 297 | { |
michael@0 | 298 | // Not reentrant! |
michael@0 | 299 | static char buf[40]; |
michael@0 | 300 | |
michael@0 | 301 | switch (kind()) { |
michael@0 | 302 | case LAllocation::CONSTANT_VALUE: |
michael@0 | 303 | case LAllocation::CONSTANT_INDEX: |
michael@0 | 304 | return "c"; |
michael@0 | 305 | case LAllocation::GPR: |
michael@0 | 306 | JS_snprintf(buf, sizeof(buf), "=%s", toGeneralReg()->reg().name()); |
michael@0 | 307 | return buf; |
michael@0 | 308 | case LAllocation::FPU: |
michael@0 | 309 | JS_snprintf(buf, sizeof(buf), "=%s", toFloatReg()->reg().name()); |
michael@0 | 310 | return buf; |
michael@0 | 311 | case LAllocation::STACK_SLOT: |
michael@0 | 312 | JS_snprintf(buf, sizeof(buf), "stack:%d", toStackSlot()->slot()); |
michael@0 | 313 | return buf; |
michael@0 | 314 | case LAllocation::ARGUMENT_SLOT: |
michael@0 | 315 | JS_snprintf(buf, sizeof(buf), "arg:%d", toArgument()->index()); |
michael@0 | 316 | return buf; |
michael@0 | 317 | case LAllocation::USE: |
michael@0 | 318 | PrintUse(buf, sizeof(buf), toUse()); |
michael@0 | 319 | return buf; |
michael@0 | 320 | default: |
michael@0 | 321 | MOZ_ASSUME_UNREACHABLE("what?"); |
michael@0 | 322 | } |
michael@0 | 323 | } |
michael@0 | 324 | #endif // DEBUG |
michael@0 | 325 | |
michael@0 | 326 | void |
michael@0 | 327 | LAllocation::dump() const |
michael@0 | 328 | { |
michael@0 | 329 | fprintf(stderr, "%s\n", toString()); |
michael@0 | 330 | } |
michael@0 | 331 | |
michael@0 | 332 | void |
michael@0 | 333 | LInstruction::printOperands(FILE *fp) |
michael@0 | 334 | { |
michael@0 | 335 | for (size_t i = 0, e = numOperands(); i < e; i++) { |
michael@0 | 336 | fprintf(fp, " (%s)", getOperand(i)->toString()); |
michael@0 | 337 | if (i != numOperands() - 1) |
michael@0 | 338 | fprintf(fp, ","); |
michael@0 | 339 | } |
michael@0 | 340 | } |
michael@0 | 341 | |
michael@0 | 342 | void |
michael@0 | 343 | LInstruction::assignSnapshot(LSnapshot *snapshot) |
michael@0 | 344 | { |
michael@0 | 345 | JS_ASSERT(!snapshot_); |
michael@0 | 346 | snapshot_ = snapshot; |
michael@0 | 347 | |
michael@0 | 348 | #ifdef DEBUG |
michael@0 | 349 | if (IonSpewEnabled(IonSpew_Snapshots)) { |
michael@0 | 350 | IonSpewHeader(IonSpew_Snapshots); |
michael@0 | 351 | fprintf(IonSpewFile, "Assigning snapshot %p to instruction %p (", |
michael@0 | 352 | (void *)snapshot, (void *)this); |
michael@0 | 353 | printName(IonSpewFile); |
michael@0 | 354 | fprintf(IonSpewFile, ")\n"); |
michael@0 | 355 | } |
michael@0 | 356 | #endif |
michael@0 | 357 | } |
michael@0 | 358 | |
michael@0 | 359 | void |
michael@0 | 360 | LInstruction::dump(FILE *fp) |
michael@0 | 361 | { |
michael@0 | 362 | fprintf(fp, "{"); |
michael@0 | 363 | for (size_t i = 0; i < numDefs(); i++) { |
michael@0 | 364 | PrintDefinition(fp, *getDef(i)); |
michael@0 | 365 | if (i != numDefs() - 1) |
michael@0 | 366 | fprintf(fp, ", "); |
michael@0 | 367 | } |
michael@0 | 368 | fprintf(fp, "} <- "); |
michael@0 | 369 | |
michael@0 | 370 | printName(fp); |
michael@0 | 371 | |
michael@0 | 372 | |
michael@0 | 373 | printInfo(fp); |
michael@0 | 374 | |
michael@0 | 375 | if (numTemps()) { |
michael@0 | 376 | fprintf(fp, " t=("); |
michael@0 | 377 | for (size_t i = 0; i < numTemps(); i++) { |
michael@0 | 378 | PrintDefinition(fp, *getTemp(i)); |
michael@0 | 379 | if (i != numTemps() - 1) |
michael@0 | 380 | fprintf(fp, ", "); |
michael@0 | 381 | } |
michael@0 | 382 | fprintf(fp, ")"); |
michael@0 | 383 | } |
michael@0 | 384 | fprintf(fp, "\n"); |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | void |
michael@0 | 388 | LInstruction::dump() |
michael@0 | 389 | { |
michael@0 | 390 | return dump(stderr); |
michael@0 | 391 | } |
michael@0 | 392 | |
michael@0 | 393 | void |
michael@0 | 394 | LInstruction::initSafepoint(TempAllocator &alloc) |
michael@0 | 395 | { |
michael@0 | 396 | JS_ASSERT(!safepoint_); |
michael@0 | 397 | safepoint_ = new(alloc) LSafepoint(alloc); |
michael@0 | 398 | JS_ASSERT(safepoint_); |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | bool |
michael@0 | 402 | LMoveGroup::add(LAllocation *from, LAllocation *to, LDefinition::Type type) |
michael@0 | 403 | { |
michael@0 | 404 | #ifdef DEBUG |
michael@0 | 405 | JS_ASSERT(*from != *to); |
michael@0 | 406 | for (size_t i = 0; i < moves_.length(); i++) |
michael@0 | 407 | JS_ASSERT(*to != *moves_[i].to()); |
michael@0 | 408 | #endif |
michael@0 | 409 | return moves_.append(LMove(from, to, type)); |
michael@0 | 410 | } |
michael@0 | 411 | |
michael@0 | 412 | bool |
michael@0 | 413 | LMoveGroup::addAfter(LAllocation *from, LAllocation *to, LDefinition::Type type) |
michael@0 | 414 | { |
michael@0 | 415 | // Transform the operands to this move so that performing the result |
michael@0 | 416 | // simultaneously with existing moves in the group will have the same |
michael@0 | 417 | // effect as if the original move took place after the existing moves. |
michael@0 | 418 | |
michael@0 | 419 | for (size_t i = 0; i < moves_.length(); i++) { |
michael@0 | 420 | if (*moves_[i].to() == *from) { |
michael@0 | 421 | from = moves_[i].from(); |
michael@0 | 422 | break; |
michael@0 | 423 | } |
michael@0 | 424 | } |
michael@0 | 425 | |
michael@0 | 426 | if (*from == *to) |
michael@0 | 427 | return true; |
michael@0 | 428 | |
michael@0 | 429 | for (size_t i = 0; i < moves_.length(); i++) { |
michael@0 | 430 | if (*to == *moves_[i].to()) { |
michael@0 | 431 | moves_[i] = LMove(from, to, type); |
michael@0 | 432 | return true; |
michael@0 | 433 | } |
michael@0 | 434 | } |
michael@0 | 435 | |
michael@0 | 436 | return add(from, to, type); |
michael@0 | 437 | } |
michael@0 | 438 | |
michael@0 | 439 | void |
michael@0 | 440 | LMoveGroup::printOperands(FILE *fp) |
michael@0 | 441 | { |
michael@0 | 442 | for (size_t i = 0; i < numMoves(); i++) { |
michael@0 | 443 | const LMove &move = getMove(i); |
michael@0 | 444 | // Use two printfs, as LAllocation::toString is not reentrant. |
michael@0 | 445 | fprintf(fp, "[%s", move.from()->toString()); |
michael@0 | 446 | fprintf(fp, " -> %s]", move.to()->toString()); |
michael@0 | 447 | if (i != numMoves() - 1) |
michael@0 | 448 | fprintf(fp, ", "); |
michael@0 | 449 | } |
michael@0 | 450 | } |