diff --git a/src/namedef.h b/src/namedef.h index 48f1a19434..33e61a2764 100644 --- a/src/namedef.h +++ b/src/namedef.h @@ -349,6 +349,7 @@ xx(VisibleStartAngle) xx(VisibleStartPitch) xx(VisibleEndAngle) xx(VisibleEndPitch) +xx(Format) // Various actor names which are used internally xx(MapSpot) diff --git a/src/scripting/codegeneration/codegen.cpp b/src/scripting/codegeneration/codegen.cpp index b2f9d40eb1..d89ba9ae7f 100644 --- a/src/scripting/codegeneration/codegen.cpp +++ b/src/scripting/codegeneration/codegen.cpp @@ -7115,6 +7115,14 @@ FxExpression *FxFunctionCall::Resolve(FCompileContext& ctx) } } + // [ZZ] string formatting function + if (MethodName == NAME_Format) + { + FxExpression *x = new FxFormat(ArgList, ScriptPosition); + delete this; + return x->Resolve(ctx); + } + int min, max, special; if (MethodName == NAME_ACS_NamedExecuteWithResult || MethodName == NAME_CallACS) { @@ -8341,6 +8349,85 @@ ExpEmit FxFlopFunctionCall::Emit(VMFunctionBuilder *build) } +//========================================================================== +// +// +// +//========================================================================== + +FxFormat::FxFormat(FArgumentList &args, const FScriptPosition &pos) + : FxExpression(EFX_FlopFunctionCall, pos) +{ + ArgList = std::move(args); +} + +//========================================================================== +// +// +// +//========================================================================== + +FxFormat::~FxFormat() +{ +} + +FxExpression *FxFormat::Resolve(FCompileContext& ctx) +{ + CHECKRESOLVED(); + + for (int i = 0; i < ArgList.Size(); i++) + { + ArgList[i] = ArgList[i]->Resolve(ctx); + if (ArgList[i] == nullptr) + { + delete this; + return nullptr; + } + + // first argument should be a string + if (!i && ArgList[i]->ValueType != TypeString) + { + ScriptPosition.Message(MSG_ERROR, "String was expected for format"); + delete this; + return nullptr; + } + + if (ArgList[i]->ValueType == TypeName || + ArgList[i]->ValueType == TypeSound) + { + FxExpression* x = new FxStringCast(ArgList[i]); + x = x->Resolve(ctx); + if (x == nullptr) + { + delete this; + return nullptr; + } + ArgList[i] = x; + } + } + + ValueType = TypeString; + return this; +} + +//========================================================================== +// +// +//========================================================================== + +ExpEmit FxFormat::Emit(VMFunctionBuilder *build) +{ + ExpEmit to = ExpEmit(build, REGT_STRING); + for (int i = 0; i < ArgList.Size(); i++) + { + EmitParameter(build, ArgList[i], ScriptPosition); + } + + build->Emit(OP_STRFMT, to.RegNum, ArgList.Size(), 0); + return to; +} + + //========================================================================== // // diff --git a/src/scripting/codegeneration/codegen.h b/src/scripting/codegeneration/codegen.h index a0a66a776d..29369ebc79 100644 --- a/src/scripting/codegeneration/codegen.h +++ b/src/scripting/codegeneration/codegen.h @@ -255,6 +255,7 @@ enum EFxType EFX_MemberFunctionCall, EFX_ActionSpecialCall, EFX_FlopFunctionCall, + EFX_FormatFunctionCall, EFX_VMFunctionCall, EFX_Sequence, EFX_CompoundStatement, @@ -1532,7 +1533,25 @@ public: //========================================================================== // -// FxFlopFunctionCall +// FxFormatFunctionCall +// +//========================================================================== + +class FxFormat : public FxExpression +{ + FArgumentList ArgList; + +public: + + FxFormat(FArgumentList &args, const FScriptPosition &pos); + ~FxFormat(); + FxExpression *Resolve(FCompileContext&); + ExpEmit Emit(VMFunctionBuilder *build); +}; + +//========================================================================== +// +// FxVectorBuiltin // //========================================================================== @@ -1551,7 +1570,7 @@ public: //========================================================================== // -// FxFlopFunctionCall +// FxGetClass // //========================================================================== @@ -1569,7 +1588,7 @@ public: //========================================================================== // -// FxFlopFunctionCall +// FxGetDefaultByType // //========================================================================== diff --git a/src/scripting/vm/vm.h b/src/scripting/vm/vm.h index 1035c2f8e5..2a76f8e2ea 100644 --- a/src/scripting/vm/vm.h +++ b/src/scripting/vm/vm.h @@ -199,6 +199,7 @@ enum EVMAbortException X_ARRAY_OUT_OF_BOUNDS, X_DIVISION_BY_ZERO, X_BAD_SELF, + X_FORMAT_ERROR }; class CVMAbortException : public CDoomError diff --git a/src/scripting/vm/vmexec.h b/src/scripting/vm/vmexec.h index 1d685c69cc..506cb3b4e0 100644 --- a/src/scripting/vm/vmexec.h +++ b/src/scripting/vm/vmexec.h @@ -862,6 +862,157 @@ 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 and char formats + 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; + } + + // int formats + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + case 'o': + { + 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/vmframe.cpp b/src/scripting/vm/vmframe.cpp index 09192cbfe1..3b97eb614f 100644 --- a/src/scripting/vm/vmframe.cpp +++ b/src/scripting/vm/vmframe.cpp @@ -549,6 +549,10 @@ CVMAbortException::CVMAbortException(EVMAbortException reason, const char *morei AppendMessage("invalid self pointer."); break; + case X_FORMAT_ERROR: + AppendMessage("string format failed."); + break; + default: { size_t len = strlen(m_Message); diff --git a/src/scripting/vm/vmops.h b/src/scripting/vm/vmops.h index 889706726c..2d76adb7e5 100644 --- a/src/scripting/vm/vmops.h +++ b/src/scripting/vm/vmops.h @@ -120,6 +120,7 @@ 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