diff --git a/doc/script/operators.txt b/doc/script/operators.txt index 5f4965a7..7c121717 100644 --- a/doc/script/operators.txt +++ b/doc/script/operators.txt @@ -56,6 +56,11 @@ In a table: | @true@ @false@ @true@ @false@ @true@ | @true@ @true@ @true@ @true@ @false@ +The @and@ and @or@ operators use [[http://en.wikipedia.org/wiki/Short-circuit_evaluation|short-circuit evaluation]], which means that the second argument is only evaluated if the first argument does not suffice to determine the value of the expression. +For example +> true or card.field_that_does_not_exist +evaluates to @true@ instead of giving an error. + --Grouping and order-- Operators are ordered as usual, so diff --git a/src/script/context.cpp b/src/script/context.cpp index 88c601a9..3d2590de 100644 --- a/src/script/context.cpp +++ b/src/script/context.cpp @@ -74,6 +74,25 @@ ScriptValueP Context::eval(const Script& script, bool useScope) { } break; } + // Short-circuiting and/or = conditional jump without pop + case I_JUMP_SC_AND: { + bool condition = *stack.back(); + if (!condition) { + instr = &script.instructions[0] + i.data; + } else { + stack.pop_back(); + } + break; + } + case I_JUMP_SC_OR: { + bool condition = *stack.back(); + if (condition) { + instr = &script.instructions[0] + i.data; + } else { + stack.pop_back(); + } + break; + } // Get a variable case I_GET_VAR: { diff --git a/src/script/dependency.cpp b/src/script/dependency.cpp index 2a4b04d5..035697c3 100644 --- a/src/script/dependency.cpp +++ b/src/script/dependency.cpp @@ -207,8 +207,10 @@ ScriptValueP Context::dependencies(const Dependency& dep, const Script& script) break; } // Conditional jump - case I_JUMP_IF_NOT: { - stack.pop_back(); // condition + case I_JUMP_IF_NOT: case I_JUMP_SC_AND: case I_JUMP_SC_OR: { + if (i.instr == I_JUMP_IF_NOT) { + stack.pop_back(); // pop condition + } // create jump record Jump* jump = new Jump; jump->target = &script.instructions[i.data]; @@ -217,6 +219,9 @@ ScriptValueP Context::dependencies(const Dependency& dep, const Script& script) getBindings(scope, jump->bindings); jumps.push(jump); // just fall through for the case that the condition holds + if (i.instr != I_JUMP_IF_NOT) { + stack.pop_back(); // pop condition afterwards, so it is not poped when jump is taken + } break; } @@ -231,9 +236,11 @@ ScriptValueP Context::dependencies(const Dependency& dep, const Script& script) ScriptValueP& it = stack[stack.size() - 2]; // second element of stack ScriptValueP val = it->next(); if (val) { + // we have not been through the body it = dependency_dummy; // invalidate iterator, so we loop only once stack.push_back(val); } else { + // we have been through the body once already stack.erase(stack.end() - 2); // remove iterator instr = &script.instructions[i.data]; } diff --git a/src/script/parser.cpp b/src/script/parser.cpp index fbb6b5c0..c3d81409 100644 --- a/src/script/parser.cpp +++ b/src/script/parser.cpp @@ -663,15 +663,29 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc parseOper(input, script, PREC_SET, I_SET_VAR, instr.data); } else if (minPrec <= PREC_AND && token==_("orelse"))parseOper(input, script, PREC_ADD, I_BINARY, I_OR_ELSE); - else if (minPrec <= PREC_AND && token==_("and")) parseOper(input, script, PREC_CMP, I_BINARY, I_AND); + else if (minPrec <= PREC_AND && token==_("and")) { + // short-circuiting and: + // "XXX and YYY" + // becomes + // XXX + // I_JUMP_SC_AND after # if top==false then goto after else pop + // YYY + // after: + unsigned jmpSC = script.addInstruction(I_JUMP_SC_AND); + parseOper(input, script, PREC_CMP); + script.comeFrom(jmpSC); + } else if (minPrec <= PREC_AND && token==_("or" )) { Token t = input.peek(); if (t == _("else")) {// or else input.read(); // skip else // TODO: deprecate "or else" in favor of "orelse" - parseOper(input, script, PREC_ADD, I_BINARY, I_OR_ELSE); + parseOper(input, script, PREC_ADD, I_BINARY, I_OR_ELSE); } else { - parseOper(input, script, PREC_CMP, I_BINARY, I_OR); + // short-circuiting or + unsigned jmpSC = script.addInstruction(I_JUMP_SC_OR); + parseOper(input, script, PREC_CMP); + script.comeFrom(jmpSC); } } else if (minPrec <= PREC_AND && token==_("xor")) parseOper(input, script, PREC_CMP, I_BINARY, I_XOR); diff --git a/src/script/script.cpp b/src/script/script.cpp index 12582c14..1746ec3d 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -101,6 +101,8 @@ static const unsigned int INVALID_ADDRESS = 0x03FFFFFF; unsigned int Script::addInstruction(InstructionType t) { assert( t == I_JUMP || t == I_JUMP_IF_NOT + || t == I_JUMP_SC_AND + || t == I_JUMP_SC_OR || t == I_LOOP || t == I_LOOP_WITH_KEY || t == I_POP); @@ -134,6 +136,8 @@ void Script::addInstruction(InstructionType t, const String& s) { void Script::comeFrom(unsigned int pos) { assert( instructions.at(pos).instr == I_JUMP || instructions.at(pos).instr == I_JUMP_IF_NOT + || instructions.at(pos).instr == I_JUMP_SC_AND + || instructions.at(pos).instr == I_JUMP_SC_OR || instructions.at(pos).instr == I_LOOP || instructions.at(pos).instr == I_LOOP_WITH_KEY); assert( instructions.at(pos).data == INVALID_ADDRESS ); @@ -166,6 +170,8 @@ String Script::dumpInstr(unsigned int pos, Instruction i) const { case I_PUSH_CONST: ret += _("push"); break; case I_JUMP: ret += _("jump"); break; case I_JUMP_IF_NOT: ret += _("jnz"); break; + case I_JUMP_SC_AND: ret += _("jump sc and");break; + case I_JUMP_SC_OR: ret += _("jump sc or"); break; case I_GET_VAR: ret += _("get"); break; case I_SET_VAR: ret += _("set"); break; case I_MEMBER_C: ret += _("member_c"); break; @@ -221,7 +227,10 @@ String Script::dumpInstr(unsigned int pos, Instruction i) const { case I_PUSH_CONST: case I_MEMBER_C: // const ret += _("\t") + constants[i.data]->typeName(); break; - case I_JUMP: case I_JUMP_IF_NOT: case I_LOOP: case I_LOOP_WITH_KEY: case I_MAKE_OBJECT: case I_CALL: case I_CLOSURE: case I_DUP: // int + case I_JUMP: case I_JUMP_IF_NOT: case I_JUMP_SC_AND: case I_JUMP_SC_OR: + case I_LOOP: case I_LOOP_WITH_KEY: + case I_MAKE_OBJECT: + case I_CALL: case I_CLOSURE: case I_DUP: // int ret += String::Format(_("\t%d"), i.data); break; case I_GET_VAR: case I_SET_VAR: case I_NOP: // variable @@ -310,6 +319,9 @@ const Instruction* Script::backtraceSkip(const Instruction* instr, int to_skip) } case I_JUMP_IF_NOT: case I_LOOP: case I_LOOP_WITH_KEY: return nullptr; // give up + case I_JUMP_SC_AND: case I_JUMP_SC_OR: + // assume the fallthrough case, in which case we compared and poped the top of the stack + to_skip += 1; break; default: break; // nett stack effect 0 } diff --git a/src/script/script.hpp b/src/script/script.hpp index fbe5a869..63381827 100644 --- a/src/script/script.hpp +++ b/src/script/script.hpp @@ -26,6 +26,8 @@ enum InstructionType , I_PUSH_CONST = 1 ///< arg = const val : push a constant onto the stack , I_JUMP = 2 ///< arg = address : move the instruction pointer to the given position , I_JUMP_IF_NOT = 3 ///< arg = address : move the instruction pointer if the top of the stack is false +, I_JUMP_SC_AND = 19 ///< arg = address : (short-circuiting and) jump and don't pop if the top of the stack is false +, I_JUMP_SC_OR = 20 ///< arg = address : (short-circuiting or) jump and don't pop if the top of the stack is true // Variables , I_GET_VAR = 4 ///< arg = var : find a variable, push its value onto the stack, it is an error if the variable is not found , I_SET_VAR = 5 ///< arg = var : assign the top value from the stack to a variable (doesn't pop)