diff --git a/src/namedef.h b/src/namedef.h index 33e61a276..aa7969193 100644 --- a/src/namedef.h +++ b/src/namedef.h @@ -700,6 +700,7 @@ xx(BuiltinFindSingleNameState) xx(BuiltinHandleRuntimeState) xx(BuiltinGetDefault) xx(BuiltinClassCast) +xx(BuiltinFormat) xx(Damage) // basic type names diff --git a/src/scripting/codegeneration/codegen.cpp b/src/scripting/codegeneration/codegen.cpp index 756f2083f..5e2878cac 100644 --- a/src/scripting/codegeneration/codegen.cpp +++ b/src/scripting/codegeneration/codegen.cpp @@ -8358,6 +8358,7 @@ ExpEmit FxFlopFunctionCall::Emit(VMFunctionBuilder *build) FxFormat::FxFormat(FArgumentList &args, const FScriptPosition &pos) : FxExpression(EFX_Format, pos) { + EmitTail = false; ArgList = std::move(args); } @@ -8371,6 +8372,24 @@ FxFormat::~FxFormat() { } +//========================================================================== +// +// +// +//========================================================================== + +PPrototype *FxFormat::ReturnProto() +{ + EmitTail = true; + return FxExpression::ReturnProto(); +} + +//========================================================================== +// +// +// +//========================================================================== + FxExpression *FxFormat::Resolve(FCompileContext& ctx) { CHECKRESOLVED(); @@ -8415,16 +8434,191 @@ FxExpression *FxFormat::Resolve(FCompileContext& ctx) // //========================================================================== -ExpEmit FxFormat::Emit(VMFunctionBuilder *build) +static int BuiltinFormat(VMValue *args, TArray &defaultparam, int numparam, VMReturn *ret, int numret) { - ExpEmit to = ExpEmit(build, REGT_STRING); - for (int i = 0; i < ArgList.Size(); i++) + assert(args[0].Type == REGT_STRING); + FString fmtstring = args[0].s().GetChars(); + + // note: we don't need a real printf format parser. + // enough to simply find the subtitution tokens and feed them to the real printf after checking types. + // https://en.wikipedia.org/wiki/Printf_format_string#Format_placeholder_specification + FString output; + bool in_fmt = false; + FString fmt_current; + int argnum = 1; + int argauto = 1; + // % = starts + // [0-9], -, +, \s, 0, #, . continue + // %, s, d, i, u, fF, eE, gG, xX, o, c, p, aA terminate + // various type flags are not supported. not like stuff like 'hh' modifier is to be used in the VM. + // the only combination that is parsed locally is %n$... + bool haveargnums = false; + for (int i = 0; i < fmtstring.Len(); i++) { - EmitParameter(build, ArgList[i], ScriptPosition); + char c = fmtstring[i]; + if (in_fmt) + { + if ((c >= '0' && c <= '9') || + c == '-' || c == '+' || (c == ' ' && fmt_current[fmt_current.Len() - 1] != ' ') || c == '#' || c == '.') + { + fmt_current += c; + } + else if (c == '$') // %number$format + { + if (!haveargnums && argauto > 1) + ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); + FString argnumstr = fmt_current.Mid(1); + if (!argnumstr.IsInt()) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for argument number, got '%s'.", argnumstr.GetChars()); + argnum = argnumstr.ToLong(); + if (argnum < 1 || argnum >= numparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format (tried to access argument %d, %d total).", argnum, numparam); + fmt_current = "%"; + haveargnums = true; + } + else + { + fmt_current += c; + + switch (c) + { + // string + case 's': + { + if (argnum < 0 && haveargnums) + ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); + in_fmt = false; + // fail if something was found, but it's not a string + if (argnum >= numparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); + if (args[argnum].Type != REGT_STRING) ThrowAbortException(X_FORMAT_ERROR, "Expected a string for format %s.", fmt_current.GetChars()); + // append + output.AppendFormat(fmt_current.GetChars(), args[argnum].s().GetChars()); + if (!haveargnums) argnum = ++argauto; + else argnum = -1; + break; + } + + // pointer + case 'p': + { + if (argnum < 0 && haveargnums) + ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); + in_fmt = false; + // fail if something was found, but it's not a string + if (argnum >= numparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); + if (args[argnum].Type != REGT_POINTER) ThrowAbortException(X_FORMAT_ERROR, "Expected a pointer for format %s.", fmt_current.GetChars()); + // append + output.AppendFormat(fmt_current.GetChars(), args[argnum].a); + if (!haveargnums) argnum = ++argauto; + else argnum = -1; + break; + } + + // int formats (including char) + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + case 'o': + case 'c': + { + if (argnum < 0 && haveargnums) + ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); + in_fmt = false; + // fail if something was found, but it's not an int + if (argnum >= numparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); + if (args[argnum].Type != REGT_INT && + args[argnum].Type != REGT_FLOAT) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for format %s.", fmt_current.GetChars()); + // append + output.AppendFormat(fmt_current.GetChars(), args[argnum].ToInt()); + if (!haveargnums) argnum = ++argauto; + else argnum = -1; + break; + } + + // double formats + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + case 'a': + case 'A': + { + if (argnum < 0 && haveargnums) + ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); + in_fmt = false; + // fail if something was found, but it's not a float + if (argnum >= numparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); + if (args[argnum].Type != REGT_INT && + args[argnum].Type != REGT_FLOAT) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for format %s.", fmt_current.GetChars()); + // append + output.AppendFormat(fmt_current.GetChars(), args[argnum].ToDouble()); + if (!haveargnums) argnum = ++argauto; + else argnum = -1; + break; + } + + default: + // invalid character + output += fmt_current; + in_fmt = false; + break; + } + } + } + else + { + if (c == '%') + { + if (i + 1 < fmtstring.Len() && fmtstring[i + 1] == '%') + { + output += '%'; + i++; + } + else + { + in_fmt = true; + fmt_current = "%"; + } + } + else + { + output += c; + } + } } - build->Emit(OP_STRFMT, to.RegNum, ArgList.Size(), 0); - return to; + ACTION_RETURN_STRING(output); +} + +ExpEmit FxFormat::Emit(VMFunctionBuilder *build) +{ + // Call DecoRandom to generate a random number. + VMFunction *callfunc; + PSymbol *sym = FindBuiltinFunction(NAME_BuiltinFormat, BuiltinFormat); + + assert(sym->IsKindOf(RUNTIME_CLASS(PSymbolVMFunction))); + assert(((PSymbolVMFunction *)sym)->Function != nullptr); + callfunc = ((PSymbolVMFunction *)sym)->Function; + + if (build->FramePointer.Fixed) EmitTail = false; // do not tail call if the stack is in use + int opcode = (EmitTail ? OP_TAIL_K : OP_CALL_K); + + for (int i = 0; i < ArgList.Size(); i++) + EmitParameter(build, ArgList[i], ScriptPosition); + build->Emit(opcode, build->GetConstantAddress(callfunc, ATAG_OBJECT), ArgList.Size(), 1); + + if (EmitTail) + { + ExpEmit call; + call.Final = true; + return call; + } + + ExpEmit out(build, REGT_STRING); + build->Emit(OP_RESULT, 0, REGT_STRING, out.RegNum); + return out; } diff --git a/src/scripting/codegeneration/codegen.h b/src/scripting/codegeneration/codegen.h index 97fdf198e..ed4c2f97c 100644 --- a/src/scripting/codegeneration/codegen.h +++ b/src/scripting/codegeneration/codegen.h @@ -1533,19 +1533,21 @@ public: //========================================================================== // -// FxFormatFunctionCall +// FxFormat // //========================================================================== class FxFormat : public FxExpression { FArgumentList ArgList; + bool EmitTail; public: FxFormat(FArgumentList &args, const FScriptPosition &pos); ~FxFormat(); FxExpression *Resolve(FCompileContext&); + PPrototype *ReturnProto(); ExpEmit Emit(VMFunctionBuilder *build); }; diff --git a/src/scripting/vm/vmexec.h b/src/scripting/vm/vmexec.h index 43c1fec36..1d685c69c 100644 --- a/src/scripting/vm/vmexec.h +++ b/src/scripting/vm/vmexec.h @@ -862,174 +862,6 @@ begin: } NEXTOP; - OP(STRFMT) : - { - ASSERTS(a); - assert(B <= f->NumParam); - int countparam = B; - assert(countparam >= 1); - VMValue* args = reg.param + f->NumParam - B; - assert(args[0].Type == REGT_STRING); - FString fmtstring = args[0].s().GetChars(); - - // note: we don't need a real printf format parser. - // enough to simply find the subtitution tokens and feed them to the real printf after checking types. - // https://en.wikipedia.org/wiki/Printf_format_string#Format_placeholder_specification - FString output; - bool in_fmt = false; - FString fmt_current; - int argnum = 1; - int argauto = 1; - // % = starts - // [0-9], -, +, \s, 0, #, . continue - // %, s, d, i, u, fF, eE, gG, xX, o, c, p, aA terminate - // various type flags are not supported. not like stuff like 'hh' modifier is to be used in the VM. - // the only combination that is parsed locally is %n$... - bool haveargnums = false; - for (int i = 0; i < fmtstring.Len(); i++) - { - char c = fmtstring[i]; - if (in_fmt) - { - if ((c >= '0' && c <= '9') || - c == '-' || c == '+' || (c == ' ' && fmt_current[fmt_current.Len() - 1] != ' ') || c == '#' || c == '.') - { - fmt_current += c; - } - else if (c == '$') // %number$format - { - if (!haveargnums && argauto > 1) - ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); - FString argnumstr = fmt_current.Mid(1); - if (!argnumstr.IsInt()) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for argument number, got '%s'.", argnumstr.GetChars()); - argnum = argnumstr.ToLong(); - if (argnum < 1 || argnum >= countparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format (tried to access argument %d, %d total).", argnum, countparam); - fmt_current = "%"; - haveargnums = true; - } - else - { - fmt_current += c; - - switch (c) - { - // string - case 's': - { - if (argnum < 0 && haveargnums) - ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); - in_fmt = false; - // fail if something was found, but it's not a string - if (argnum >= countparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); - if (args[argnum].Type != REGT_STRING) ThrowAbortException(X_FORMAT_ERROR, "Expected a string for format %s.", fmt_current.GetChars()); - // append - output.AppendFormat(fmt_current.GetChars(), args[argnum].s().GetChars()); - if (!haveargnums) argnum = ++argauto; - else argnum = -1; - break; - } - - // pointer - case 'p': - { - if (argnum < 0 && haveargnums) - ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); - in_fmt = false; - // fail if something was found, but it's not a string - if (argnum >= countparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); - if (args[argnum].Type != REGT_POINTER) ThrowAbortException(X_FORMAT_ERROR, "Expected a pointer for format %s.", fmt_current.GetChars()); - // append - output.AppendFormat(fmt_current.GetChars(), args[argnum].a); - if (!haveargnums) argnum = ++argauto; - else argnum = -1; - break; - } - - // int formats (including char) - case 'd': - case 'i': - case 'u': - case 'x': - case 'X': - case 'o': - case 'c': - { - if (argnum < 0 && haveargnums) - ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); - in_fmt = false; - // fail if something was found, but it's not an int - if (argnum >= countparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); - if (args[argnum].Type != REGT_INT && - args[argnum].Type != REGT_FLOAT) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for format %s.", fmt_current.GetChars()); - // append - output.AppendFormat(fmt_current.GetChars(), args[argnum].ToInt()); - if (!haveargnums) argnum = ++argauto; - else argnum = -1; - break; - } - - // double formats - case 'f': - case 'F': - case 'e': - case 'E': - case 'g': - case 'G': - case 'a': - case 'A': - { - if (argnum < 0 && haveargnums) - ThrowAbortException(X_FORMAT_ERROR, "Cannot mix explicit and implicit arguments."); - in_fmt = false; - // fail if something was found, but it's not a float - if (argnum >= countparam) ThrowAbortException(X_FORMAT_ERROR, "Not enough arguments for format."); - if (args[argnum].Type != REGT_INT && - args[argnum].Type != REGT_FLOAT) ThrowAbortException(X_FORMAT_ERROR, "Expected a numeric value for format %s.", fmt_current.GetChars()); - // append - output.AppendFormat(fmt_current.GetChars(), args[argnum].ToDouble()); - if (!haveargnums) argnum = ++argauto; - else argnum = -1; - break; - } - - default: - // invalid character - output += fmt_current; - in_fmt = false; - break; - } - } - } - else - { - if (c == '%') - { - if (i + 1 < fmtstring.Len() && fmtstring[i + 1] == '%') - { - output += '%'; - i++; - } - else - { - in_fmt = true; - fmt_current = "%"; - } - } - else - { - output += c; - } - } - } - - // finalize parameters - for (b = B; b != 0; --b) - reg.param[--f->NumParam].~VMValue(); - - reg.s[a] = output; - } - NEXTOP; - OP(SLL_RR): ASSERTD(a); ASSERTD(B); ASSERTD(C); reg.d[a] = reg.d[B] << reg.d[C]; diff --git a/src/scripting/vm/vmops.h b/src/scripting/vm/vmops.h index 2d76adb7e..889706726 100644 --- a/src/scripting/vm/vmops.h +++ b/src/scripting/vm/vmops.h @@ -120,7 +120,6 @@ xx(BOUND_R, bound, RIRI, NOP, 0, 0), // if rA >= rB, throw exception xx(CONCAT, concat, RSRSRS, NOP, 0, 0), // sA = sB..sC xx(LENS, lens, RIRS, NOP, 0, 0), // dA = sB.Length xx(CMPS, cmps, I8RXRX, NOP, 0, 0), // if ((skB op skC) != (A & 1)) then pc++ -xx(STRFMT, strfmt, RIRIRI, NOP, 0, 0), // Integer math. xx(SLL_RR, sll, RIRIRI, NOP, 0, 0), // dA = dkB << diC