/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "common/stream.h" #include "common/util.h" #include "./ast.h" #include "./codewritervisitor.h" #include "./handler.h" #include "./names.h" #include "./script.h" namespace LingoDec { /* Handler */ void Handler::readRecord(Common::SeekableReadStream &stream) { nameID = stream.readSint16BE(); vectorPos = stream.readUint16BE(); compiledLen = stream.readUint32BE(); compiledOffset = stream.readUint32BE(); argumentCount = stream.readUint16BE(); argumentOffset = stream.readUint32BE(); localsCount = stream.readUint16BE(); localsOffset = stream.readUint32BE(); globalsCount = stream.readUint16BE(); globalsOffset = stream.readUint32BE(); unknown1 = stream.readUint32BE(); unknown2 = stream.readUint16BE(); lineCount = stream.readUint16BE(); lineOffset = stream.readUint32BE(); // yet to implement if (script->version >= 850) stackHeight = stream.readUint32BE(); } void Handler::readData(Common::SeekableReadStream &stream) { stream.seek(compiledOffset); while (stream.pos() < compiledOffset + compiledLen) { uint32 pos = stream.pos() - compiledOffset; byte op = stream.readByte(); OpCode opcode = static_cast(op >= 0x40 ? 0x40 + op % 0x40 : op); // argument can be one, two or four bytes int32 obj = 0; if (op >= 0xc0) { // four bytes obj = stream.readSint32BE(); } else if (op >= 0x80) { // two bytes if (opcode == kOpPushInt16 || opcode == kOpPushInt8) { // treat pushint's arg as signed // pushint8 may be used to push a 16-bit int in older Lingo obj = stream.readSint16BE(); } else { obj = stream.readUint16BE(); } } else if (op >= 0x40) { // one byte if (opcode == kOpPushInt8) { // treat pushint's arg as signed obj = stream.readSByte(); } else { obj = stream.readByte(); } } Bytecode bytecode(op, obj, pos); bytecodeArray.push_back(bytecode); bytecodePosMap[pos] = bytecodeArray.size() - 1; } argumentNameIDs = readVarnamesTable(stream, argumentCount, argumentOffset); localNameIDs = readVarnamesTable(stream, localsCount, localsOffset); globalNameIDs = readVarnamesTable(stream, globalsCount, globalsOffset); } Common::Array Handler::readVarnamesTable(Common::SeekableReadStream &stream, uint16 count, uint32 offset) { stream.seek(offset); Common::Array nameIDs; nameIDs.resize(count); for (size_t i = 0; i < count; i++) { nameIDs[i] = stream.readUint16BE(); } return nameIDs; } void Handler::readNames() { if (!isGenericEvent) { name = getName(nameID); } for (size_t i = 0; i < argumentNameIDs.size(); i++) { if (i == 0 && script->isFactory()) continue; argumentNames.push_back(getName(argumentNameIDs[i])); } for (auto nameID_ : localNameIDs) { if (validName(nameID_)) { localNames.push_back(getName(nameID_)); } } for (auto nameID_ : globalNameIDs) { if (validName(nameID_)) { globalNames.push_back(getName(nameID_)); } } } bool Handler::validName(int id) const { return script->validName(id); } Common::String Handler::getName(int id) const { return script->getName(id); } Common::String Handler::getArgumentName(int id) const { if (-1 < id && (unsigned)id < argumentNameIDs.size()) return getName(argumentNameIDs[id]); return Common::String::format("UNKNOWN_ARG_%d", id); } Common::String Handler::getLocalName(int id) const { if (-1 < id && (unsigned)id < localNameIDs.size()) return getName(localNameIDs[id]); return Common::String::format("UNKNOWN_LOCAL_%d", id); } Common::SharedPtr Handler::pop() { if (stack.empty()) return Common::SharedPtr(new ErrorNode(0)); auto res = stack.back(); stack.pop_back(); return res; } int Handler::variableMultiplier() { if (script->version >= 850) return 1; if (script->version >= 500) return 8; return 6; } Common::SharedPtr Handler::readVar(int varType) { Common::SharedPtr castID; if (varType == 0x6 && script->version >= 500) // field cast ID castID = pop(); Common::SharedPtr id = pop(); switch (varType) { case 0x1: // global case 0x2: // global case 0x3: // property/instance return id; case 0x4: // arg { Common::String name_ = getArgumentName(id->getValue()->i / variableMultiplier()); auto ref = Common::SharedPtr(new Datum(kDatumVarRef, name_)); return Common::SharedPtr(new LiteralNode(id->_startOffset, Common::move(ref))); } case 0x5: // local { Common::String name_ = getLocalName(id->getValue()->i / variableMultiplier()); auto ref = Common::SharedPtr(new Datum(kDatumVarRef, name_)); return Common::SharedPtr(new LiteralNode(id->_startOffset, Common::move(ref))); } case 0x6: // field return Common::SharedPtr(new MemberExprNode(id->_startOffset, "field", Common::move(id), Common::move(castID))); default: warning("findVar: unhandled var type %d", varType); break; } return Common::SharedPtr(new ErrorNode(id->_startOffset)); } Common::String Handler::getVarNameFromSet(const Bytecode &bytecode) { Common::String varName; switch (bytecode.opcode) { case kOpSetGlobal: case kOpSetGlobal2: varName = getName(bytecode.obj); break; case kOpSetProp: varName = getName(bytecode.obj); break; case kOpSetParam: varName = getArgumentName(bytecode.obj / variableMultiplier()); break; case kOpSetLocal: varName = getLocalName(bytecode.obj / variableMultiplier()); break; default: varName = "ERROR"; break; } return varName; } Common::SharedPtr Handler::readV4Property(uint32 offset, int propertyType, int propertyID) { switch (propertyType) { case 0x00: { if (propertyID <= 0x0b) { // movie property Common::String propName(StandardNames::moviePropertyNames[propertyID]); return Common::SharedPtr(new TheExprNode(offset, propName)); } else { // last chunk auto string = pop(); auto chunkType = static_cast(propertyID - 0x0b); return Common::SharedPtr(new LastStringChunkExprNode(offset, chunkType, Common::move(string))); } } break; case 0x01: // number of chunks { auto string = pop(); return Common::SharedPtr(new StringChunkCountExprNode(offset, static_cast(propertyID), Common::move(string))); } break; case 0x02: // menu property { auto menuID = pop(); return Common::SharedPtr(new MenuPropExprNode(offset, Common::move(menuID), propertyID)); } break; case 0x03: // menu item property { auto menuID = pop(); auto itemID = pop(); return Common::SharedPtr(new MenuItemPropExprNode(offset, Common::move(menuID), Common::move(itemID), propertyID)); } break; case 0x04: // sound property { auto soundID = pop(); return Common::SharedPtr(new SoundPropExprNode(offset, Common::move(soundID), propertyID)); } break; case 0x05: // resource property - unused? return Common::SharedPtr(new CommentNode(offset, "ERROR: Resource property")); case 0x06: // sprite property { auto spriteID = pop(); return Common::SharedPtr(new SpritePropExprNode(offset, Common::move(spriteID), propertyID)); } break; case 0x07: // animation property return Common::SharedPtr(new TheExprNode(offset, StandardNames::animationPropertyNames[propertyID])); case 0x08: // animation 2 property if (propertyID == 0x02 && script->version >= 500) { // the number of castMembers supports castLib selection from Director 5.0 auto castLib = pop(); if (!(castLib->type == kLiteralNode && castLib->getValue()->type == kDatumInt && castLib->getValue()->toInt() == 0)) { auto castLibNode = Common::SharedPtr(new MemberExprNode(offset, "castLib", castLib, nullptr)); return Common::SharedPtr(new ThePropExprNode(offset, castLibNode, StandardNames::animation2PropertyNames[propertyID])); } } return Common::SharedPtr(new TheExprNode(offset, StandardNames::animation2PropertyNames[propertyID])); case 0x09: // generic cast member case 0x0a: // chunk of cast member case 0x0b: // field case 0x0c: // chunk of field case 0x0d: // digital video case 0x0e: // bitmap case 0x0f: // sound case 0x10: // button case 0x11: // shape case 0x12: // movie case 0x13: // script case 0x14: // scriptText case 0x15: // chunk of scriptText { auto propName = StandardNames::memberPropertyNames[propertyID]; Common::SharedPtr castID; if (script->version >= 500) { castID = pop(); } auto memberID = pop(); Common::String prefix; if (propertyType == 0x0b || propertyType == 0x0c) { prefix = "field"; } else if (propertyType == 0x14 || propertyType == 0x15) { prefix = "script"; } else { prefix = (script->version >= 500) ? "member" : "cast"; } auto member = Common::SharedPtr(new MemberExprNode(offset, prefix, Common::move(memberID), Common::move(castID))); Common::SharedPtr entity; if (propertyType == 0x0a || propertyType == 0x0c || propertyType == 0x15) { entity = readChunkRef(offset, Common::move(member)); } else { entity = member; } return Common::SharedPtr(new ThePropExprNode(offset, Common::move(entity), propName)); } break; default: break; } return Common::SharedPtr(new CommentNode(offset, Common::String::format("ERROR: Unknown property type %d", propertyType))); } Common::SharedPtr Handler::readChunkRef(uint32 offset, Common::SharedPtr string) { auto lastLine = pop(); auto firstLine = pop(); auto lastItem = pop(); auto firstItem = pop(); auto lastWord = pop(); auto firstWord = pop(); auto lastChar = pop(); auto firstChar = pop(); if (!(firstLine->type == kLiteralNode && firstLine->getValue()->type == kDatumInt && firstLine->getValue()->toInt() == 0)) string = Common::SharedPtr(new ChunkExprNode(offset, kChunkLine, Common::move(firstLine), Common::move(lastLine), Common::move(string))); if (!(firstItem->type == kLiteralNode && firstItem->getValue()->type == kDatumInt && firstItem->getValue()->toInt() == 0)) string = Common::SharedPtr(new ChunkExprNode(offset, kChunkItem, Common::move(firstItem), Common::move(lastItem), Common::move(string))); if (!(firstWord->type == kLiteralNode && firstWord->getValue()->type == kDatumInt && firstWord->getValue()->toInt() == 0)) string = Common::SharedPtr(new ChunkExprNode(offset, kChunkWord, Common::move(firstWord), Common::move(lastWord), Common::move(string))); if (!(firstChar->type == kLiteralNode && firstChar->getValue()->type == kDatumInt && firstChar->getValue()->toInt() == 0)) string = Common::SharedPtr(new ChunkExprNode(offset, kChunkChar, Common::move(firstChar), Common::move(lastChar), Common::move(string))); return string; } void Handler::tagLoops() { // Tag any jmpifz which is a loop with the loop type // (kTagRepeatWhile, kTagRepeatWithIn, kTagRepeatWithTo, kTagRepeatWithDownTo). // Tag the instruction which `next repeat` jumps to with kTagNextRepeatTarget. // Tag any instructions which are internal loop logic with kTagSkip, so that // they will be skipped during translation. for (uint32 startIndex = 0; startIndex < bytecodeArray.size(); startIndex++) { // All loops begin with jmpifz... auto &jmpifz = bytecodeArray[startIndex]; if (jmpifz.opcode != kOpJmpIfZ) continue; // ...and end with endrepeat. uint32 jmpPos = jmpifz.pos + jmpifz.obj; uint32 endIndex = bytecodePosMap[jmpPos]; auto &endRepeat = bytecodeArray[endIndex - 1]; if (endRepeat.opcode != kOpEndRepeat || (endRepeat.pos - endRepeat.obj) > jmpifz.pos) continue; BytecodeTag loopType = identifyLoop(startIndex, endIndex); bytecodeArray[startIndex].tag = loopType; if (loopType == kTagRepeatWithIn) { for (uint32 i = startIndex - 7, end = startIndex - 1; i <= end; i++) bytecodeArray[i].tag = kTagSkip; for (uint32 i = startIndex + 1, end = startIndex + 5; i <= end; i++) bytecodeArray[i].tag = kTagSkip; bytecodeArray[endIndex - 3].tag = kTagNextRepeatTarget; // pushint8 1 bytecodeArray[endIndex - 3].ownerLoop = startIndex; bytecodeArray[endIndex - 2].tag = kTagSkip; // add bytecodeArray[endIndex - 1].tag = kTagSkip; // endrepeat bytecodeArray[endIndex - 1].ownerLoop = startIndex; bytecodeArray[endIndex].tag = kTagSkip; // pop 3 } else if (loopType == kTagRepeatWithTo || loopType == kTagRepeatWithDownTo) { uint32 conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj]; bytecodeArray[conditionStartIndex - 1].tag = kTagSkip; // set bytecodeArray[conditionStartIndex].tag = kTagSkip; // get bytecodeArray[startIndex - 1].tag = kTagSkip; // lteq / gteq bytecodeArray[endIndex - 5].tag = kTagNextRepeatTarget; // pushint8 1 / pushint8 -1 bytecodeArray[endIndex - 5].ownerLoop = startIndex; bytecodeArray[endIndex - 4].tag = kTagSkip; // get bytecodeArray[endIndex - 3].tag = kTagSkip; // add bytecodeArray[endIndex - 2].tag = kTagSkip; // set bytecodeArray[endIndex - 1].tag = kTagSkip; // endrepeat bytecodeArray[endIndex - 1].ownerLoop = startIndex; } else if (loopType == kTagRepeatWhile) { bytecodeArray[endIndex - 1].tag = kTagNextRepeatTarget; // endrepeat bytecodeArray[endIndex - 1].ownerLoop = startIndex; } } } bool Handler::isRepeatWithIn(uint32 startIndex, uint32 endIndex) { if (startIndex < 7 || startIndex > bytecodeArray.size() - 6) return false; if (!(bytecodeArray[startIndex - 7].opcode == kOpPeek && bytecodeArray[startIndex - 7].obj == 0)) return false; if (!(bytecodeArray[startIndex - 6].opcode == kOpPushArgList && bytecodeArray[startIndex - 6].obj == 1)) return false; if (!(bytecodeArray[startIndex - 5].opcode == kOpExtCall && getName(bytecodeArray[startIndex - 5].obj) == "count")) return false; if (!(bytecodeArray[startIndex - 4].opcode == kOpPushInt8 && bytecodeArray[startIndex - 4].obj == 1)) return false; if (!(bytecodeArray[startIndex - 3].opcode == kOpPeek && bytecodeArray[startIndex - 3].obj == 0)) return false; if (!(bytecodeArray[startIndex - 2].opcode == kOpPeek && bytecodeArray[startIndex - 2].obj == 2)) return false; if (!(bytecodeArray[startIndex - 1].opcode == kOpLtEq)) return false; // if (!(bytecodeArray[startIndex].opcode == kOpJmpIfZ)) // return false; if (!(bytecodeArray[startIndex + 1].opcode == kOpPeek && bytecodeArray[startIndex + 1].obj == 2)) return false; if (!(bytecodeArray[startIndex + 2].opcode == kOpPeek && bytecodeArray[startIndex + 2].obj == 1)) return false; if (!(bytecodeArray[startIndex + 3].opcode == kOpPushArgList && bytecodeArray[startIndex + 3].obj == 2)) return false; if (!(bytecodeArray[startIndex + 4].opcode == kOpExtCall && getName(bytecodeArray[startIndex + 4].obj) == "getAt")) return false; if (!(bytecodeArray[startIndex + 5].opcode == kOpSetGlobal || bytecodeArray[startIndex + 5].opcode == kOpSetProp || bytecodeArray[startIndex + 5].opcode == kOpSetParam || bytecodeArray[startIndex + 5].opcode == kOpSetLocal)) return false; if (endIndex < 3) return false; if (!(bytecodeArray[endIndex - 3].opcode == kOpPushInt8 && bytecodeArray[endIndex - 3].obj == 1)) return false; if (!(bytecodeArray[endIndex - 2].opcode == kOpAdd)) return false; // if (!(bytecodeArray[startIndex - 1].opcode == kOpEndRepeat)) // return false; if (!(bytecodeArray[endIndex].opcode == kOpPop && bytecodeArray[endIndex].obj == 3)) return false; return true; } BytecodeTag Handler::identifyLoop(uint32 startIndex, uint32 endIndex) { if (isRepeatWithIn(startIndex, endIndex)) return kTagRepeatWithIn; if (startIndex < 1) return kTagRepeatWhile; bool up; switch (bytecodeArray[startIndex - 1].opcode) { case kOpLtEq: up = true; break; case kOpGtEq: up = false; break; default: return kTagRepeatWhile; } auto &endRepeat = bytecodeArray[endIndex - 1]; uint32 conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj]; if (conditionStartIndex < 1) return kTagRepeatWhile; OpCode getOp; switch (bytecodeArray[conditionStartIndex - 1].opcode) { case kOpSetGlobal: getOp = kOpGetGlobal; break; case kOpSetGlobal2: getOp = kOpGetGlobal2; break; case kOpSetProp: getOp = kOpGetProp; break; case kOpSetParam: getOp = kOpGetParam; break; case kOpSetLocal: getOp = kOpGetLocal; break; default: return kTagRepeatWhile; } OpCode setOp = bytecodeArray[conditionStartIndex - 1].opcode; int32 varID = bytecodeArray[conditionStartIndex - 1].obj; if (!(bytecodeArray[conditionStartIndex].opcode == getOp && bytecodeArray[conditionStartIndex].obj == varID)) return kTagRepeatWhile; if (endIndex < 5) return kTagRepeatWhile; if (up) { if (!(bytecodeArray[endIndex - 5].opcode == kOpPushInt8 && bytecodeArray[endIndex - 5].obj == 1)) return kTagRepeatWhile; } else { if (!(bytecodeArray[endIndex - 5].opcode == kOpPushInt8 && bytecodeArray[endIndex - 5].obj == -1)) return kTagRepeatWhile; } if (!(bytecodeArray[endIndex - 4].opcode == getOp && bytecodeArray[endIndex - 4].obj == varID)) return kTagRepeatWhile; if (!(bytecodeArray[endIndex - 3].opcode == kOpAdd)) return kTagRepeatWhile; if (!(bytecodeArray[endIndex - 2].opcode == setOp && bytecodeArray[endIndex - 2].obj == varID)) return kTagRepeatWhile; return up ? kTagRepeatWithTo : kTagRepeatWithDownTo; } void Handler::parse() { tagLoops(); stack.clear(); uint32 i = 0; while (i < bytecodeArray.size()) { auto &bytecode = bytecodeArray[i]; uint32 pos = bytecode.pos; // exit last block if at end while (pos == ast.currentBlock->endPos) { auto exitedBlock = ast.currentBlock; auto ancestorStmt = ast.currentBlock->ancestorStatement(); ast.exitBlock(); if (ancestorStmt) { if (ancestorStmt->type == kIfStmtNode) { auto ifStatement = static_cast(ancestorStmt); if (ifStatement->hasElse && exitedBlock == ifStatement->block1.get()) { ast.enterBlock(ifStatement->block2.get()); } } else if (ancestorStmt->type == kCaseStmtNode) { auto caseStmt = static_cast(ancestorStmt); auto caseLabel = ast.currentBlock->currentCaseLabel; if (caseLabel) { if (caseLabel->expect == kCaseExpectOtherwise) { ast.currentBlock->currentCaseLabel = nullptr; caseStmt->addOtherwise(i); size_t otherwiseIndex = bytecodePosMap[caseStmt->potentialOtherwisePos]; bytecodeArray[otherwiseIndex].translation = Common::SharedPtr(caseStmt->otherwise); ast.enterBlock(caseStmt->otherwise->block.get()); } else if (caseLabel->expect == kCaseExpectEnd) { ast.currentBlock->currentCaseLabel = nullptr; } } } } } auto translateSize = translateBytecode(bytecode, i); i += translateSize; } } uint32 Handler::translateBytecode(Bytecode &bytecode, uint32 index) { if (bytecode.tag == kTagSkip || bytecode.tag == kTagNextRepeatTarget) { // This is internal loop logic. Skip it. return 1; } Common::SharedPtr translation = nullptr; BlockNode *nextBlock = nullptr; switch (bytecode.opcode) { case kOpRet: case kOpRetFactory: if (index == bytecodeArray.size() - 1) { ast.root->_endOffset = bytecode.pos; ast.currentBlock->_endOffset = bytecode.pos; return 1; // end of handler } translation = Common::SharedPtr(new ExitStmtNode(bytecode.pos)); break; case kOpPushZero: translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::SharedPtr(new Datum(0)))); break; case kOpMul: case kOpAdd: case kOpSub: case kOpDiv: case kOpMod: case kOpJoinStr: case kOpJoinPadStr: case kOpLt: case kOpLtEq: case kOpNtEq: case kOpEq: case kOpGt: case kOpGtEq: case kOpAnd: case kOpOr: case kOpContainsStr: case kOpContains0Str: { auto b = pop(); auto a = pop(); translation = Common::SharedPtr(new BinaryOpNode(bytecode.pos, bytecode.opcode, Common::move(a), Common::move(b))); } break; case kOpInv: { auto x = pop(); translation = Common::SharedPtr(new InverseOpNode(bytecode.pos, Common::move(x))); } break; case kOpNot: { auto x = pop(); translation = Common::SharedPtr(new NotOpNode(bytecode.pos, Common::move(x))); } break; case kOpGetChunk: { auto string = pop(); translation = readChunkRef(bytecode.pos, Common::move(string)); } break; case kOpHiliteChunk: { Common::SharedPtr castID; if (script->version >= 500) castID = pop(); auto fieldID = pop(); auto field = Common::SharedPtr(new MemberExprNode(bytecode.pos, "field", Common::move(fieldID), Common::move(castID))); auto chunk = readChunkRef(bytecode.pos, Common::move(field)); if (chunk->type == kCommentNode) { // error comment translation = chunk; } else { translation = Common::SharedPtr(new ChunkHiliteStmtNode(bytecode.pos, Common::move(chunk))); } } break; case kOpOntoSpr: { auto secondSprite = pop(); auto firstSprite = pop(); translation = Common::SharedPtr(new SpriteIntersectsExprNode(bytecode.pos, Common::move(firstSprite), Common::move(secondSprite))); } break; case kOpIntoSpr: { auto secondSprite = pop(); auto firstSprite = pop(); translation = Common::SharedPtr(new SpriteWithinExprNode(bytecode.pos, Common::move(firstSprite), Common::move(secondSprite))); } break; case kOpGetField: { Common::SharedPtr castID; if (script->version >= 500) castID = pop(); auto fieldID = pop(); translation = Common::SharedPtr(new MemberExprNode(bytecode.pos, "field", Common::move(fieldID), Common::move(castID))); } break; case kOpStartTell: { auto window = pop(); auto tellStmt = Common::SharedPtr(new TellStmtNode(bytecode.pos, Common::move(window))); translation = tellStmt; nextBlock = tellStmt->block.get(); } break; case kOpEndTell: { ast.currentBlock->_endOffset = bytecode.pos; ast.exitBlock(); return 1; } break; case kOpPushList: { auto list = pop(); list->getValue()->type = kDatumList; translation = list; } break; case kOpPushPropList: { auto list = pop(); list->getValue()->type = kDatumPropList; translation = list; } break; case kOpSwap: if (stack.size() >= 2) { SWAP(stack[stack.size() - 1], stack[stack.size() - 2]); } else { warning("kOpSwap: Stack too small!"); } return 1; case kOpPushInt8: case kOpPushInt16: case kOpPushInt32: { auto i = Common::SharedPtr(new Datum((int)bytecode.obj)); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(i))); } break; case kOpPushFloat32: { auto f = Common::SharedPtr(new Datum(*(float *)(&bytecode.obj))); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(f))); } break; case kOpPushArgListNoRet: { auto argCount = bytecode.obj; Common::Array> args; args.resize(argCount); while (argCount) { argCount--; args[argCount] = pop(); } auto argList = Common::SharedPtr(new Datum(kDatumArgListNoRet, args)); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(argList))); } break; case kOpPushArgList: { auto argCount = bytecode.obj; Common::Array> args; args.resize(argCount); while (argCount) { argCount--; args[argCount] = pop(); } auto argList = Common::SharedPtr(new Datum(kDatumArgList, args)); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(argList))); } break; case kOpPushCons: { int literalID = bytecode.obj / variableMultiplier(); if (-1 < literalID && (unsigned)literalID < script->literals.size()) { translation = Common::SharedPtr(new LiteralNode(bytecode.pos, script->literals[literalID].value)); } else { translation = Common::SharedPtr(new ErrorNode(bytecode.pos)); } break; } case kOpPushSymb: { auto sym = Common::SharedPtr(new Datum(kDatumSymbol, getName(bytecode.obj))); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(sym))); } break; case kOpPushVarRef: { auto ref = Common::SharedPtr(new Datum(kDatumVarRef, getName(bytecode.obj))); translation = Common::SharedPtr(new LiteralNode(bytecode.pos, Common::move(ref))); } break; case kOpGetGlobal: case kOpGetGlobal2: { auto name_ = getName(bytecode.obj); translation = Common::SharedPtr(new VarNode(bytecode.pos, name_)); } break; case kOpGetProp: translation = Common::SharedPtr(new VarNode(bytecode.pos, getName(bytecode.obj))); break; case kOpGetParam: translation = Common::SharedPtr(new VarNode(bytecode.pos, getArgumentName(bytecode.obj / variableMultiplier()))); break; case kOpGetLocal: translation = Common::SharedPtr(new VarNode(bytecode.pos, getLocalName(bytecode.obj / variableMultiplier()))); break; case kOpSetGlobal: case kOpSetGlobal2: { auto varName = getName(bytecode.obj); auto var = Common::SharedPtr(new VarNode(bytecode.pos, varName)); auto value = pop(); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(var), Common::move(value))); } break; case kOpSetProp: { auto var = Common::SharedPtr(new VarNode(bytecode.pos, getName(bytecode.obj))); auto value = pop(); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(var), Common::move(value))); } break; case kOpSetParam: { auto var = Common::SharedPtr(new VarNode(bytecode.pos, getArgumentName(bytecode.obj / variableMultiplier()))); auto value = pop(); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(var), Common::move(value))); } break; case kOpSetLocal: { auto var = Common::SharedPtr(new VarNode(bytecode.pos, getLocalName(bytecode.obj / variableMultiplier()))); auto value = pop(); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(var), Common::move(value))); } break; case kOpJmp: { uint32 targetPos = bytecode.pos + bytecode.obj; size_t targetIndex = bytecodePosMap[targetPos]; auto &targetBytecode = bytecodeArray[targetIndex]; auto ancestorLoop = ast.currentBlock->ancestorLoop(); if (ancestorLoop) { if (bytecodeArray[targetIndex - 1].opcode == kOpEndRepeat && bytecodeArray[targetIndex - 1].ownerLoop == ancestorLoop->startIndex) { translation = Common::SharedPtr(new ExitRepeatStmtNode(bytecode.pos)); break; } else if (bytecodeArray[targetIndex].tag == kTagNextRepeatTarget && bytecodeArray[targetIndex].ownerLoop == ancestorLoop->startIndex) { translation = Common::SharedPtr(new NextRepeatStmtNode(bytecode.pos)); break; } } auto &nextBytecode = bytecodeArray[index + 1]; auto ancestorStatement = ast.currentBlock->ancestorStatement(); if (ancestorStatement && nextBytecode.pos == ast.currentBlock->endPos) { if (ancestorStatement->type == kIfStmtNode) { auto ifStmt = static_cast(ancestorStatement); if (ast.currentBlock == ifStmt->block1.get()) { ifStmt->hasElse = true; ifStmt->block2->_startOffset = ifStmt->block1->_endOffset; ifStmt->block2->endPos = targetPos; ifStmt->block2->_endOffset = targetPos; ifStmt->_endOffset = targetPos; return 1; // if statement amended, nothing to push } } else if (ancestorStatement->type == kCaseStmtNode) { auto caseStmt = static_cast(ancestorStatement); caseStmt->potentialOtherwisePos = bytecode.pos; caseStmt->endPos = targetPos; caseStmt->_endOffset = targetPos; targetBytecode.tag = kTagEndCase; return 1; } } if (targetBytecode.opcode == kOpPop && targetBytecode.obj == 1) { // This is a case statement starting with 'otherwise' auto value = pop(); auto caseStmt = Common::SharedPtr(new CaseStmtNode(bytecode.pos, Common::move(value))); caseStmt->endPos = targetPos; caseStmt->_endOffset = targetPos; targetBytecode.tag = kTagEndCase; caseStmt->addOtherwise(bytecode.pos); translation = caseStmt; nextBlock = caseStmt->otherwise->block.get(); break; } translation = Common::SharedPtr(new CommentNode(bytecode.pos, "ERROR: Could not identify jmp")); } break; case kOpEndRepeat: // This should normally be tagged kTagSkip or kTagNextRepeatTarget and skipped. translation = Common::SharedPtr(new CommentNode(bytecode.pos, "ERROR: Stray endrepeat")); break; case kOpJmpIfZ: { uint32 endPos = bytecode.pos + bytecode.obj; uint32 endIndex = bytecodePosMap[endPos]; switch (bytecode.tag) { case kTagRepeatWhile: { auto condition = pop(); auto loop = Common::SharedPtr(new RepeatWhileStmtNode(bytecode.pos, Common::move(condition), bytecode.pos)); loop->block->endPos = endPos; loop->block->_endOffset = endPos; loop->_endOffset = endPos; translation = loop; nextBlock = loop->block.get(); } break; case kTagRepeatWithIn: { auto list = pop(); Common::String varName = getVarNameFromSet(bytecodeArray[index + 5]); auto loop = Common::SharedPtr(new RepeatWithInStmtNode(bytecode.pos, varName, Common::move(list), bytecode.pos)); loop->block->endPos = endPos; loop->block->_endOffset = endPos; loop->_endOffset = endPos; translation = loop; nextBlock = loop->block.get(); } break; case kTagRepeatWithTo: case kTagRepeatWithDownTo: { bool up = (bytecode.tag == kTagRepeatWithTo); auto end = pop(); auto start = pop(); auto endRepeat = bytecodeArray[endIndex - 1]; uint32 conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj]; Common::String varName = getVarNameFromSet(bytecodeArray[conditionStartIndex - 1]); auto loop = Common::SharedPtr(new RepeatWithToStmtNode(bytecode.pos, varName, Common::move(start), up, Common::move(end), bytecode.pos)); loop->block->endPos = endPos; loop->block->_endOffset = endPos; loop->_endOffset = endPos; translation = loop; nextBlock = loop->block.get(); } break; default: { auto condition = pop(); auto ifStmt = Common::SharedPtr(new IfStmtNode(bytecode.pos, Common::move(condition))); ifStmt->block1->endPos = endPos; ifStmt->block1->_endOffset = endPos; ifStmt->_endOffset = endPos; translation = ifStmt; nextBlock = ifStmt->block1.get(); } break; } } break; case kOpLocalCall: { auto argList = pop(); translation = Common::SharedPtr(new CallNode(bytecode.pos, script->handlers[bytecode.obj].name, Common::move(argList))); } break; case kOpExtCall: case kOpTellCall: { Common::String name_ = getName(bytecode.obj); auto argList = pop(); bool isStatement = (argList->getValue()->type == kDatumArgListNoRet); auto &rawArgList = argList->getValue()->l; size_t nargs = rawArgList.size(); if (isStatement && name_ == "sound" && nargs > 0 && rawArgList[0]->type == kLiteralNode && rawArgList[0]->getValue()->type == kDatumSymbol) { Common::String cmd = rawArgList[0]->getValue()->s; rawArgList.erase(rawArgList.begin()); translation = Common::SharedPtr(new SoundCmdStmtNode(bytecode.pos, cmd, Common::move(argList))); } else if (isStatement && name_ == "play" && nargs <= 2) { translation = Common::SharedPtr(new PlayCmdStmtNode(bytecode.pos, Common::move(argList))); } else { translation = Common::SharedPtr(new CallNode(bytecode.pos, name_, Common::move(argList))); } } break; case kOpObjCallV4: { auto object = readVar(bytecode.obj); auto argList = pop(); auto &rawArgList = argList->getValue()->l; if (rawArgList.size() > 0) { // first arg is a symbol // replace it with a variable rawArgList[0] = Common::SharedPtr(new VarNode(bytecode.pos, rawArgList[0]->getValue()->s)); } translation = Common::SharedPtr(new ObjCallV4Node(bytecode.pos, Common::move(object), Common::move(argList))); } break; case kOpPut: { PutType putType = static_cast((bytecode.obj >> 4) & 0xF); uint32 varType = bytecode.obj & 0xF; auto var = readVar(varType); auto val = pop(); translation = Common::SharedPtr(new PutStmtNode(bytecode.pos, putType, Common::move(var), Common::move(val))); } break; case kOpPutChunk: { PutType putType = static_cast((bytecode.obj >> 4) & 0xF); uint32 varType = bytecode.obj & 0xF; auto var = readVar(varType); auto chunk = readChunkRef(bytecode.pos, Common::move(var)); auto val = pop(); if (chunk->type == kCommentNode) { // error comment translation = chunk; } else { translation = Common::SharedPtr(new PutStmtNode(bytecode.pos, putType, Common::move(chunk), Common::move(val))); } } break; case kOpDeleteChunk: { auto var = readVar(bytecode.obj); auto chunk = readChunkRef(bytecode.pos, Common::move(var)); if (chunk->type == kCommentNode) { // error comment translation = chunk; } else { translation = Common::SharedPtr(new ChunkDeleteStmtNode(bytecode.pos, Common::move(chunk))); } } break; case kOpGet: { int propertyID = pop()->getValue()->toInt(); translation = readV4Property(bytecode.pos, bytecode.obj, propertyID); } break; case kOpSet: { int propertyID = pop()->getValue()->toInt(); auto value = pop(); if (bytecode.obj == 0x00 && 0x01 <= propertyID && propertyID <= 0x05 && value->getValue()->type == kDatumString) { // This is either a `set eventScript to "script"` or `when event then script` statement. // If the script starts with a space, it's probably a when statement. // If the script contains a line break, it's definitely a when statement. Common::String script_ = value->getValue()->s; if (script_.size() > 0 && (script_[0] == ' ' || script_.find('\r') != Common::String::npos)) { translation = Common::SharedPtr(new WhenStmtNode(bytecode.pos, propertyID, script_)); } } if (!translation) { auto prop = readV4Property(bytecode.pos, bytecode.obj, propertyID); if (prop->type == kCommentNode) { // error comment translation = prop; } else { translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(prop), Common::move(value), true)); } } } break; case kOpGetMovieProp: translation = Common::SharedPtr(new TheExprNode(bytecode.pos, getName(bytecode.obj))); break; case kOpSetMovieProp: { auto value = pop(); auto prop = Common::SharedPtr(new TheExprNode(bytecode.pos, getName(bytecode.obj))); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(prop), Common::move(value))); } break; case kOpGetObjProp: case kOpGetChainedProp: { auto object = pop(); translation = Common::SharedPtr(new ObjPropExprNode(bytecode.pos, Common::move(object), getName(bytecode.obj))); } break; case kOpSetObjProp: { auto value = pop(); auto object = pop(); auto prop = Common::SharedPtr(new ObjPropExprNode(bytecode.pos, Common::move(object), getName(bytecode.obj))); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(prop), Common::move(value))); } break; case kOpPeek: { // This op denotes the beginning of a 'repeat with ... in list' statement or a case in a cases statement. // In a 'repeat with ... in list' statement, this peeked value is the list. // In a cases statement, this is the switch expression. auto prevLabel = ast.currentBlock->currentCaseLabel; // This must be a case. Find the comparison against the switch expression. auto originalStackSize = stack.size(); uint32 currIndex = index + 1; Bytecode *currBytecode = &bytecodeArray[currIndex]; do { translateBytecode(*currBytecode, currIndex); currIndex += 1; currBytecode = &bytecodeArray[currIndex]; } while ( currIndex < bytecodeArray.size() && !(stack.size() == originalStackSize + 1 && (currBytecode->opcode == kOpEq || currBytecode->opcode == kOpNtEq)) ); if (currIndex >= bytecodeArray.size()) { bytecode.translation = Common::SharedPtr(new CommentNode(bytecode.pos, "ERROR: Expected eq or nteq!")); ast.addStatement(bytecode.translation); return currIndex - index + 1; } // If the comparison is <>, this is followed by another, equivalent case. // (e.g. this could be case1 in `case1, case2: statement`) bool notEq = (currBytecode->opcode == kOpNtEq); Common::SharedPtr caseValue = pop(); // This is the value the switch expression is compared against. currIndex += 1; currBytecode = &bytecodeArray[currIndex]; if (currIndex >= bytecodeArray.size() || currBytecode->opcode != kOpJmpIfZ) { bytecode.translation = Common::SharedPtr(new CommentNode(bytecode.pos, "ERROR: Expected jmpifz!")); ast.addStatement(bytecode.translation); return currIndex - index + 1; } auto &jmpifz = *currBytecode; auto jmpPos = jmpifz.pos + jmpifz.obj; size_t targetIndex = bytecodePosMap[jmpPos]; auto &targetBytecode = bytecodeArray[targetIndex]; auto &prevFromTarget = bytecodeArray[targetIndex - 1]; CaseExpect expect; if (notEq) { expect = kCaseExpectOr; // Expect an equivalent case after this one. } else if (targetBytecode.opcode == kOpPeek) { expect = kCaseExpectNext; // Expect a different case after this one. } else if (targetBytecode.opcode == kOpPop && targetBytecode.obj == 1 && (prevFromTarget.opcode != kOpJmp || prevFromTarget.pos + prevFromTarget.obj == targetBytecode.pos)) { expect = kCaseExpectEnd; // Expect the end of the switch statement. } else { expect = kCaseExpectOtherwise; // Expect an 'otherwise' block. } auto currLabel = Common::SharedPtr(new CaseLabelNode(bytecode.pos, Common::move(caseValue), expect)); jmpifz.translation = currLabel; ast.currentBlock->currentCaseLabel = currLabel.get(); if (!prevLabel) { auto peekedValue = pop(); auto caseStmt = Common::SharedPtr(new CaseStmtNode(bytecode.pos, Common::move(peekedValue))); caseStmt->firstLabel = currLabel; currLabel->parent = caseStmt.get(); bytecode.translation = caseStmt; ast.addStatement(caseStmt); } else if (prevLabel->expect == kCaseExpectOr) { prevLabel->nextOr = currLabel; currLabel->parent = prevLabel; } else if (prevLabel->expect == kCaseExpectNext) { prevLabel->nextLabel = currLabel; currLabel->parent = prevLabel; } // The block doesn't start until the after last equivalent case, // so don't create a block yet if we're expecting an equivalent case. if (currLabel->expect != kCaseExpectOr) { currLabel->block = Common::SharedPtr(new BlockNode(bytecode.pos)); currLabel->block->parent = currLabel.get(); currLabel->block->endPos = jmpPos; currLabel->block->_endOffset = jmpPos; currLabel->_endOffset = jmpPos; ast.enterBlock(currLabel->block.get()); } return currIndex - index + 1; } break; case kOpPop: { // Pop instructions in 'repeat with in' loops are tagged kTagSkip and skipped. if (bytecode.tag == kTagEndCase) { // We've already recognized this as the end of a case statement. // Attach an 'end case' node for the summary only. bytecode.translation = Common::SharedPtr(); return 1; } if (bytecode.obj == 1 && stack.size() == 1) { // We have an unused value on the stack, so this must be the end // of a case statement with no labels. auto value = pop(); translation = Common::SharedPtr(new CaseStmtNode(bytecode.pos, Common::move(value))); break; } // Otherwise, this pop instruction occurs before a 'return' within // a case statement. No translation needed. return 1; } break; case kOpTheBuiltin: { pop(); // empty arglist translation = Common::SharedPtr(new TheExprNode(bytecode.pos, getName(bytecode.obj))); } break; case kOpObjCall: { Common::String method = getName(bytecode.obj); auto argList = pop(); auto &rawArgList = argList->getValue()->l; size_t nargs = rawArgList.size(); if (method == "getAt" && nargs == 2) { // obj.getAt(i) => obj[i] auto obj = rawArgList[0]; auto prop = rawArgList[1]; translation = Common::SharedPtr(new ObjBracketExprNode(bytecode.pos, Common::move(obj), Common::move(prop))); } else if (method == "setAt" && nargs == 3) { // obj.setAt(i) => obj[i] = val auto obj = rawArgList[0]; auto prop = rawArgList[1]; auto val = rawArgList[2]; Common::SharedPtr propExpr = Common::SharedPtr(new ObjBracketExprNode(bytecode.pos, Common::move(obj), Common::move(prop))); translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(propExpr), Common::move(val))); } else if ((method == "getProp" || method == "getPropRef") && (nargs == 3 || nargs == 4) && rawArgList[1]->getValue()->type == kDatumSymbol) { // obj.getProp(#prop, i) => obj.prop[i] // obj.getProp(#prop, i, i2) => obj.prop[i..i2] auto obj = rawArgList[0]; Common::String propName = rawArgList[1]->getValue()->s; auto i = rawArgList[2]; auto i2 = (nargs == 4) ? rawArgList[3] : nullptr; translation = Common::SharedPtr(new ObjPropIndexExprNode(bytecode.pos, Common::move(obj), propName, Common::move(i), Common::move(i2))); } else if (method == "setProp" && (nargs == 4 || nargs == 5) && rawArgList[1]->getValue()->type == kDatumSymbol) { // obj.setProp(#prop, i, val) => obj.prop[i] = val // obj.setProp(#prop, i, i2, val) => obj.prop[i..i2] = val auto obj = rawArgList[0]; Common::String propName = rawArgList[1]->getValue()->s; auto i = rawArgList[2]; auto i2 = (nargs == 5) ? rawArgList[3] : nullptr; auto propExpr = Common::SharedPtr(new ObjPropIndexExprNode(bytecode.pos, Common::move(obj), propName, Common::move(i), Common::move(i2))); auto val = rawArgList[nargs - 1]; translation = Common::SharedPtr(new AssignmentStmtNode(bytecode.pos, Common::move(propExpr), Common::move(val))); } else if (method == "count" && nargs == 2 && rawArgList[1]->getValue()->type == kDatumSymbol) { // obj.count(#prop) => obj.prop.count auto obj = rawArgList[0]; Common::String propName = rawArgList[1]->getValue()->s; auto propExpr = Common::SharedPtr(new ObjPropExprNode(bytecode.pos, Common::move(obj), propName)); translation = Common::SharedPtr(new ObjPropExprNode(bytecode.pos, Common::move(propExpr), "count")); } else if ((method == "setContents" || method == "setContentsAfter" || method == "setContentsBefore") && nargs == 2) { // var.setContents(val) => put val into var // var.setContentsAfter(val) => put val after var // var.setContentsBefore(val) => put val before var PutType putType; if (method == "setContents") { putType = kPutInto; } else if (method == "setContentsAfter") { putType = kPutAfter; } else { putType = kPutBefore; } auto var = rawArgList[0]; auto val = rawArgList[1]; translation = Common::SharedPtr(new PutStmtNode(bytecode.pos, putType, Common::move(var), Common::move(val))); } else if (method == "hilite" && nargs == 1) { // chunk.hilite() => hilite chunk auto chunk = rawArgList[0]; translation = Common::SharedPtr(new ChunkHiliteStmtNode(bytecode.pos, chunk)); } else if (method == "delete" && nargs == 1) { // chunk.delete() => delete chunk auto chunk = rawArgList[0]; translation = Common::SharedPtr(new ChunkDeleteStmtNode(bytecode.pos, chunk)); } else { translation = Common::SharedPtr(new ObjCallNode(bytecode.pos, method, Common::move(argList))); } } break; case kOpPushChunkVarRef: translation = readVar(bytecode.obj); break; case kOpGetTopLevelProp: { auto name_ = getName(bytecode.obj); translation = Common::SharedPtr(new VarNode(bytecode.pos, name_)); } break; case kOpNewObj: { auto objType = getName(bytecode.obj); auto objArgs = pop(); translation = Common::SharedPtr(new NewObjNode(bytecode.pos, objType, Common::move(objArgs))); } break; default: { auto commentText = StandardNames::getOpcodeName(bytecode.opID); if (bytecode.opcode >= 0x40) commentText += Common::String::format(" %d", bytecode.obj); translation = Common::SharedPtr(new CommentNode(bytecode.pos, commentText)); stack.clear(); // Clear stack so later bytecode won't be too screwed up } } if (!translation) translation = Common::SharedPtr(new ErrorNode(bytecode.pos)); bytecode.translation = translation; if (translation->isExpression) { stack.push_back(Common::move(translation)); } else { ast.addStatement(Common::move(translation)); } if (nextBlock) ast.enterBlock(nextBlock); return 1; } Common::String posToString(int32 pos) { return Common::String::format("[%3d]", pos); } void Handler::writeBytecodeText(CodeWriterVisitor &code) const { bool isMethod = script->isFactory(); if (!isGenericEvent) { if (isMethod) { code.write("method "); } else { code.write("on "); } code.write(name); if (argumentNames.size() > 0) { code.write(" "); for (size_t i = 0; i < argumentNames.size(); i++) { if (i > 0) code.write(", "); code.write(argumentNames[i]); } } code.writeLine(); code.indent(); } for (auto &bytecode : bytecodeArray) { code.write(posToString(bytecode.pos)); code.write(" "); code.write(StandardNames::getOpcodeName(bytecode.opID)); switch (bytecode.opcode) { case kOpJmp: case kOpJmpIfZ: code.write(" "); code.write(posToString(bytecode.pos + bytecode.obj)); break; case kOpEndRepeat: code.write(" "); code.write(posToString(bytecode.pos - bytecode.obj)); break; case kOpPushFloat32: code.write(" "); code.write(Common::String::format("%g", (*(const float *)(&bytecode.obj)))); break; default: if (bytecode.opID > 0x40) { code.write(" "); code.write(Common::String::format("%d", bytecode.obj)); } break; } if (bytecode.translation) { code.write(" ..."); while (code.lineWidth() < 49) { code.write("."); } code.write(" "); if (bytecode.translation->isExpression) { code.write("<"); } bytecode.translation->accept(code); if (bytecode.translation->isExpression) { code.write(">"); } } code.writeLine(); } if (!isGenericEvent) { code.unindent(); if (!isMethod) { code.writeLine("end"); } } } } // namespace LingoDec