diff --git a/ruleguard/quasigo/compile.go b/ruleguard/quasigo/compile.go index c5287eb2..51584305 100644 --- a/ruleguard/quasigo/compile.go +++ b/ruleguard/quasigo/compile.go @@ -517,7 +517,7 @@ func (cl *compiler) compileBuiltinCall(fn *ast.Ident, call *ast.CallExpr) { funcName = "PrintInt" } key := funcKey{qualifier: "builtin", name: funcName} - if !cl.compileNativeCall(key, nil, call.Args) { + if !cl.compileNativeCall(key, 0, nil, call.Args) { panic(cl.errorf(fn, "builtin.%s native func is not registered", funcName)) } @@ -548,13 +548,16 @@ func (cl *compiler) compileCallExpr(call *ast.CallExpr) { } else { key.qualifier = fn.Pkg().Path() } - - if !cl.compileNativeCall(key, expr, call.Args) { + variadic := 0 + if sig.Variadic() { + variadic = sig.Params().Len() - 1 + } + if !cl.compileNativeCall(key, variadic, expr, call.Args) { panic(cl.errorf(call.Fun, "can't compile a call to %s func", key)) } } -func (cl *compiler) compileNativeCall(key funcKey, expr ast.Expr, args []ast.Expr) bool { +func (cl *compiler) compileNativeCall(key funcKey, variadic int, expr ast.Expr, args []ast.Expr) bool { funcID, ok := cl.ctx.Env.nameToNativeFuncID[key] if !ok { return false @@ -572,9 +575,35 @@ func (cl *compiler) compileNativeCall(key funcKey, expr ast.Expr, args []ast.Exp } } } - for _, arg := range args { + + normalArgs := args + var variadicArgs []ast.Expr + if variadic != 0 { + normalArgs = args[:variadic] + variadicArgs = args[variadic:] + } + + for _, arg := range normalArgs { cl.compileExpr(arg) } + if variadic != 0 { + for _, arg := range variadicArgs { + cl.compileExpr(arg) + // int-typed values should appear in the interface{}-typed + // objects slice, so we get all variadic args placed in one place. + if typeIsInt(cl.ctx.Types.TypeOf(arg)) { + cl.emit(opConvIntToIface) + } + } + if len(variadicArgs) > 255 { + panic(cl.errorf(expr, "too many variadic args")) + } + // Even if len(variadicArgs) is 0, we still need to overwrite + // the old variadicLen value, so the variadic func is not confused + // by some unrelated value. + cl.emit8(opSetVariadicLen, len(variadicArgs)) + } + cl.emit16(opCallNative, int(funcID)) return true } diff --git a/ruleguard/quasigo/compile_test.go b/ruleguard/quasigo/compile_test.go index 5136b163..c4e89cc4 100644 --- a/ruleguard/quasigo/compile_test.go +++ b/ruleguard/quasigo/compile_test.go @@ -330,13 +330,34 @@ func TestCompile(t *testing.T) { ` SetLocal 0 # err`, ` SetIntLocal 1 # v`, ` PushIntLocal 1 # v`, - ` CallNative 3 # builtin.PrintInt`, + ` CallNative 4 # builtin.PrintInt`, ` PushLocal 0 # err`, ` IsNil`, - ` CallNative 4 # builtin.Print`, + ` CallNative 5 # builtin.Print`, ` PushLocal 0 # err`, ` ReturnTop`, }, + + `v := sprintf("no formatting"); return v`: { + ` PushConst 0 # value="no formatting"`, + ` SetVariadicLen 0`, + ` CallNative 3 # testpkg.sprintf`, + ` SetLocal 0 # v`, + ` PushLocal 0 # v`, + ` ReturnTop`, + }, + + `v := sprintf("%s:%d", "foo", 5); return v`: { + ` PushConst 0 # value="%s:%d"`, + ` PushConst 1 # value="foo"`, + ` PushIntConst 0 # value=5`, + ` ConvIntToIface`, + ` SetVariadicLen 2`, + ` CallNative 3 # testpkg.sprintf`, + ` SetLocal 0 # v`, + ` PushLocal 0 # v`, + ` ReturnTop`, + }, } makePackageSource := func(body string) string { @@ -348,6 +369,7 @@ func TestCompile(t *testing.T) { func imul(x, y int) int func idiv(x, y int) int func atoi(s string) (int, error) + func sprintf(format string, args ...interface{}) string ` } @@ -361,6 +383,9 @@ func TestCompile(t *testing.T) { env.AddNativeFunc(testPackage, "atoi", func(stack *quasigo.ValueStack) { panic("should not be called") }) + env.AddNativeFunc(testPackage, "sprintf", func(stack *quasigo.ValueStack) { + panic("should not be called") + }) env.AddNativeFunc("builtin", "PrintInt", func(stack *quasigo.ValueStack) { panic("should not be called") }) diff --git a/ruleguard/quasigo/disasm.go b/ruleguard/quasigo/disasm.go index 192cf071..8859dee9 100644 --- a/ruleguard/quasigo/disasm.go +++ b/ruleguard/quasigo/disasm.go @@ -47,6 +47,8 @@ func disasm(env *Env, fn *Func) string { index := int(code[pc+1]) arg = index comment = dbg.localNames[index] + case opSetVariadicLen: + arg = int(code[pc+1]) case opPushConst: arg = int(code[pc+1]) comment = fmt.Sprintf("value=%#v", fn.constants[code[pc+1]]) diff --git a/ruleguard/quasigo/eval.go b/ruleguard/quasigo/eval.go index 18ce5108..da16455f 100644 --- a/ruleguard/quasigo/eval.go +++ b/ruleguard/quasigo/eval.go @@ -99,6 +99,10 @@ func eval(env *EvalEnv, fn *Func, args []interface{}) CallResult { stack.PushInt(fn.intConstants[id]) pc += 2 + case opConvIntToIface: + stack.Push(stack.PopInt()) + pc++ + case opPushTrue: stack.Push(true) pc++ @@ -117,6 +121,9 @@ func eval(env *EvalEnv, fn *Func, args []interface{}) CallResult { case opReturn: return CallResult{} + case opSetVariadicLen: + stack.variadicLen = int(code[pc+1]) + pc += 2 case opCallNative: id := decode16(code, pc+1) fn := env.nativeFuncs[id].mappedFunc diff --git a/ruleguard/quasigo/eval_bench_test.go b/ruleguard/quasigo/eval_bench_test.go index 5850be79..d471cfaf 100644 --- a/ruleguard/quasigo/eval_bench_test.go +++ b/ruleguard/quasigo/eval_bench_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo" + "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qfmt" ) type benchTestCase struct { @@ -75,7 +76,22 @@ func TestNoAllocs(t *testing.T) { } func BenchmarkEval(b *testing.B) { - var tests []*benchTestCase + var tests = []*benchTestCase{ + { + `CallNativeVariadic0`, + `return fmt.Sprintf("no formatting")`, + }, + { + `CallNativeVariadic1`, + `return fmt.Sprintf("Hello, %s!", "world")`, + }, + + { + `CallNativeVariadic2`, + `return fmt.Sprintf("%s:%d", "foo.go", 105)`, + }, + } + tests = append(tests, benchmarksNoAlloc...) runBench := func(b *testing.B, env *quasigo.EvalEnv, fn *quasigo.Func) { @@ -98,6 +114,8 @@ func compileBenchFunc(t testing.TB, bodySrc string) (*quasigo.Env, *quasigo.Func makePackageSource := func(body string) string { return ` package test + import "fmt" + var _ = fmt.Sprintf func f() interface{} { ` + body + ` } @@ -111,6 +129,7 @@ func compileBenchFunc(t testing.TB, bodySrc string) (*quasigo.Env, *quasigo.Func x := stack.PopInt() stack.PushInt(x * y) }) + qfmt.ImportAll(env) src := makePackageSource(bodySrc) parsed, err := parseGoFile(src) if err != nil { diff --git a/ruleguard/quasigo/eval_test.go b/ruleguard/quasigo/eval_test.go index 30e25e4f..334ae102 100644 --- a/ruleguard/quasigo/eval_test.go +++ b/ruleguard/quasigo/eval_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/internal/evaltest" + "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qfmt" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrconv" "github.com/quasilyte/go-ruleguard/ruleguard/quasigo/stdlib/qstrings" ) @@ -276,6 +277,7 @@ func TestEvalFile(t *testing.T) { qstrings.ImportAll(env) qstrconv.ImportAll(env) + qfmt.ImportAll(env) var mainFunc *quasigo.Func for _, decl := range parsed.ast.Decls { diff --git a/ruleguard/quasigo/gen_opcodes.go b/ruleguard/quasigo/gen_opcodes.go index e9abcae6..6f8f26d5 100644 --- a/ruleguard/quasigo/gen_opcodes.go +++ b/ruleguard/quasigo/gen_opcodes.go @@ -1,3 +1,4 @@ +//go:build main // +build main package main @@ -25,6 +26,8 @@ var opcodePrototypes = []opcodeProto{ {"PushConst", "op constid:u8", "() -> (const)"}, {"PushIntConst", "op constid:u8", "() -> (const:int)"}, + {"ConvIntToIface", "op", "(value:int) -> (value)"}, + {"SetLocal", "op index:u8", "(value) -> ()"}, {"SetIntLocal", "op index:u8", "(value:int) -> ()"}, {"IncLocal", "op index:u8", stackUnchanged}, @@ -40,6 +43,7 @@ var opcodePrototypes = []opcodeProto{ {"JumpFalse", "op offset:i16", "(cond:bool) -> ()"}, {"JumpTrue", "op offset:i16", "(cond:bool) -> ()"}, + {"SetVariadicLen", "op len:u8", stackUnchanged}, {"CallNative", "op funcid:u16", "(args...) -> (results...)"}, {"IsNil", "op", "(value) -> (result:bool)"}, diff --git a/ruleguard/quasigo/opcode_string.go b/ruleguard/quasigo/opcode_string.go index f6d37d14..27dfc494 100644 --- a/ruleguard/quasigo/opcode_string.go +++ b/ruleguard/quasigo/opcode_string.go @@ -19,42 +19,44 @@ func _() { _ = x[opPushTrue-8] _ = x[opPushConst-9] _ = x[opPushIntConst-10] - _ = x[opSetLocal-11] - _ = x[opSetIntLocal-12] - _ = x[opIncLocal-13] - _ = x[opDecLocal-14] - _ = x[opReturnTop-15] - _ = x[opReturnIntTop-16] - _ = x[opReturnFalse-17] - _ = x[opReturnTrue-18] - _ = x[opReturn-19] - _ = x[opJump-20] - _ = x[opJumpFalse-21] - _ = x[opJumpTrue-22] - _ = x[opCallNative-23] - _ = x[opIsNil-24] - _ = x[opIsNotNil-25] - _ = x[opNot-26] - _ = x[opEqInt-27] - _ = x[opNotEqInt-28] - _ = x[opGtInt-29] - _ = x[opGtEqInt-30] - _ = x[opLtInt-31] - _ = x[opLtEqInt-32] - _ = x[opEqString-33] - _ = x[opNotEqString-34] - _ = x[opConcat-35] - _ = x[opAdd-36] - _ = x[opSub-37] - _ = x[opStringSlice-38] - _ = x[opStringSliceFrom-39] - _ = x[opStringSliceTo-40] - _ = x[opStringLen-41] + _ = x[opConvIntToIface-11] + _ = x[opSetLocal-12] + _ = x[opSetIntLocal-13] + _ = x[opIncLocal-14] + _ = x[opDecLocal-15] + _ = x[opReturnTop-16] + _ = x[opReturnIntTop-17] + _ = x[opReturnFalse-18] + _ = x[opReturnTrue-19] + _ = x[opReturn-20] + _ = x[opJump-21] + _ = x[opJumpFalse-22] + _ = x[opJumpTrue-23] + _ = x[opSetVariadicLen-24] + _ = x[opCallNative-25] + _ = x[opIsNil-26] + _ = x[opIsNotNil-27] + _ = x[opNot-28] + _ = x[opEqInt-29] + _ = x[opNotEqInt-30] + _ = x[opGtInt-31] + _ = x[opGtEqInt-32] + _ = x[opLtInt-33] + _ = x[opLtEqInt-34] + _ = x[opEqString-35] + _ = x[opNotEqString-36] + _ = x[opConcat-37] + _ = x[opAdd-38] + _ = x[opSub-39] + _ = x[opStringSlice-40] + _ = x[opStringSliceFrom-41] + _ = x[opStringSliceTo-42] + _ = x[opStringLen-43] } -const _opcode_name = "InvalidPopDupPushParamPushIntParamPushLocalPushIntLocalPushFalsePushTruePushConstPushIntConstSetLocalSetIntLocalIncLocalDecLocalReturnTopReturnIntTopReturnFalseReturnTrueReturnJumpJumpFalseJumpTrueCallNativeIsNilIsNotNilNotEqIntNotEqIntGtIntGtEqIntLtIntLtEqIntEqStringNotEqStringConcatAddSubStringSliceStringSliceFromStringSliceToStringLen" +const _opcode_name = "InvalidPopDupPushParamPushIntParamPushLocalPushIntLocalPushFalsePushTruePushConstPushIntConstConvIntToIfaceSetLocalSetIntLocalIncLocalDecLocalReturnTopReturnIntTopReturnFalseReturnTrueReturnJumpJumpFalseJumpTrueSetVariadicLenCallNativeIsNilIsNotNilNotEqIntNotEqIntGtIntGtEqIntLtIntLtEqIntEqStringNotEqStringConcatAddSubStringSliceStringSliceFromStringSliceToStringLen" -var _opcode_index = [...]uint16{0, 7, 10, 13, 22, 34, 43, 55, 64, 72, 81, 93, 101, 112, 120, 128, 137, 149, 160, 170, 176, 180, 189, 197, 207, 212, 220, 223, 228, 236, 241, 248, 253, 260, 268, 279, 285, 288, 291, 302, 317, 330, 339} +var _opcode_index = [...]uint16{0, 7, 10, 13, 22, 34, 43, 55, 64, 72, 81, 93, 107, 115, 126, 134, 142, 151, 163, 174, 184, 190, 194, 203, 211, 225, 235, 240, 248, 251, 256, 264, 269, 276, 281, 288, 296, 307, 313, 316, 319, 330, 345, 358, 367} func (i opcode) String() string { if i >= opcode(len(_opcode_index)-1) { diff --git a/ruleguard/quasigo/opcodes.gen.go b/ruleguard/quasigo/opcodes.gen.go index 61d13d7d..a12aecd5 100644 --- a/ruleguard/quasigo/opcodes.gen.go +++ b/ruleguard/quasigo/opcodes.gen.go @@ -48,129 +48,137 @@ const ( // Stack effect: () -> (const:int) opPushIntConst opcode = 10 - // Encoding: 0x0b index:u8 (width=2) - // Stack effect: (value) -> () - opSetLocal opcode = 11 + // Encoding: 0x0b (width=1) + // Stack effect: (value:int) -> (value) + opConvIntToIface opcode = 11 // Encoding: 0x0c index:u8 (width=2) - // Stack effect: (value:int) -> () - opSetIntLocal opcode = 12 + // Stack effect: (value) -> () + opSetLocal opcode = 12 // Encoding: 0x0d index:u8 (width=2) - // Stack effect: unchanged - opIncLocal opcode = 13 + // Stack effect: (value:int) -> () + opSetIntLocal opcode = 13 // Encoding: 0x0e index:u8 (width=2) // Stack effect: unchanged - opDecLocal opcode = 14 + opIncLocal opcode = 14 - // Encoding: 0x0f (width=1) - // Stack effect: (value) -> (value) - opReturnTop opcode = 15 + // Encoding: 0x0f index:u8 (width=2) + // Stack effect: unchanged + opDecLocal opcode = 15 // Encoding: 0x10 (width=1) // Stack effect: (value) -> (value) - opReturnIntTop opcode = 16 + opReturnTop opcode = 16 // Encoding: 0x11 (width=1) - // Stack effect: unchanged - opReturnFalse opcode = 17 + // Stack effect: (value) -> (value) + opReturnIntTop opcode = 17 // Encoding: 0x12 (width=1) // Stack effect: unchanged - opReturnTrue opcode = 18 + opReturnFalse opcode = 18 // Encoding: 0x13 (width=1) // Stack effect: unchanged - opReturn opcode = 19 + opReturnTrue opcode = 19 - // Encoding: 0x14 offset:i16 (width=3) + // Encoding: 0x14 (width=1) // Stack effect: unchanged - opJump opcode = 20 + opReturn opcode = 20 // Encoding: 0x15 offset:i16 (width=3) - // Stack effect: (cond:bool) -> () - opJumpFalse opcode = 21 + // Stack effect: unchanged + opJump opcode = 21 // Encoding: 0x16 offset:i16 (width=3) // Stack effect: (cond:bool) -> () - opJumpTrue opcode = 22 + opJumpFalse opcode = 22 - // Encoding: 0x17 funcid:u16 (width=3) - // Stack effect: (args...) -> (results...) - opCallNative opcode = 23 + // Encoding: 0x17 offset:i16 (width=3) + // Stack effect: (cond:bool) -> () + opJumpTrue opcode = 23 - // Encoding: 0x18 (width=1) - // Stack effect: (value) -> (result:bool) - opIsNil opcode = 24 + // Encoding: 0x18 len:u8 (width=2) + // Stack effect: unchanged + opSetVariadicLen opcode = 24 - // Encoding: 0x19 (width=1) - // Stack effect: (value) -> (result:bool) - opIsNotNil opcode = 25 + // Encoding: 0x19 funcid:u16 (width=3) + // Stack effect: (args...) -> (results...) + opCallNative opcode = 25 // Encoding: 0x1a (width=1) - // Stack effect: (value:bool) -> (result:bool) - opNot opcode = 26 + // Stack effect: (value) -> (result:bool) + opIsNil opcode = 26 // Encoding: 0x1b (width=1) - // Stack effect: (x:int y:int) -> (result:bool) - opEqInt opcode = 27 + // Stack effect: (value) -> (result:bool) + opIsNotNil opcode = 27 // Encoding: 0x1c (width=1) - // Stack effect: (x:int y:int) -> (result:bool) - opNotEqInt opcode = 28 + // Stack effect: (value:bool) -> (result:bool) + opNot opcode = 28 // Encoding: 0x1d (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opGtInt opcode = 29 + opEqInt opcode = 29 // Encoding: 0x1e (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opGtEqInt opcode = 30 + opNotEqInt opcode = 30 // Encoding: 0x1f (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opLtInt opcode = 31 + opGtInt opcode = 31 // Encoding: 0x20 (width=1) // Stack effect: (x:int y:int) -> (result:bool) - opLtEqInt opcode = 32 + opGtEqInt opcode = 32 // Encoding: 0x21 (width=1) - // Stack effect: (x:string y:string) -> (result:bool) - opEqString opcode = 33 + // Stack effect: (x:int y:int) -> (result:bool) + opLtInt opcode = 33 // Encoding: 0x22 (width=1) - // Stack effect: (x:string y:string) -> (result:bool) - opNotEqString opcode = 34 + // Stack effect: (x:int y:int) -> (result:bool) + opLtEqInt opcode = 34 // Encoding: 0x23 (width=1) - // Stack effect: (x:string y:string) -> (result:string) - opConcat opcode = 35 + // Stack effect: (x:string y:string) -> (result:bool) + opEqString opcode = 35 // Encoding: 0x24 (width=1) - // Stack effect: (x:int y:int) -> (result:int) - opAdd opcode = 36 + // Stack effect: (x:string y:string) -> (result:bool) + opNotEqString opcode = 36 // Encoding: 0x25 (width=1) - // Stack effect: (x:int y:int) -> (result:int) - opSub opcode = 37 + // Stack effect: (x:string y:string) -> (result:string) + opConcat opcode = 37 // Encoding: 0x26 (width=1) - // Stack effect: (s:string from:int to:int) -> (result:string) - opStringSlice opcode = 38 + // Stack effect: (x:int y:int) -> (result:int) + opAdd opcode = 38 // Encoding: 0x27 (width=1) - // Stack effect: (s:string from:int) -> (result:string) - opStringSliceFrom opcode = 39 + // Stack effect: (x:int y:int) -> (result:int) + opSub opcode = 39 // Encoding: 0x28 (width=1) - // Stack effect: (s:string to:int) -> (result:string) - opStringSliceTo opcode = 40 + // Stack effect: (s:string from:int to:int) -> (result:string) + opStringSlice opcode = 40 // Encoding: 0x29 (width=1) + // Stack effect: (s:string from:int) -> (result:string) + opStringSliceFrom opcode = 41 + + // Encoding: 0x2a (width=1) + // Stack effect: (s:string to:int) -> (result:string) + opStringSliceTo opcode = 42 + + // Encoding: 0x2b (width=1) // Stack effect: (s:string) -> (result:int) - opStringLen opcode = 41 + opStringLen opcode = 43 ) type opcodeInfo struct { @@ -190,6 +198,7 @@ var opcodeInfoTable = [256]opcodeInfo{ opPushTrue: {width: 1}, opPushConst: {width: 2}, opPushIntConst: {width: 2}, + opConvIntToIface: {width: 1}, opSetLocal: {width: 2}, opSetIntLocal: {width: 2}, opIncLocal: {width: 2}, @@ -202,6 +211,7 @@ var opcodeInfoTable = [256]opcodeInfo{ opJump: {width: 3}, opJumpFalse: {width: 3}, opJumpTrue: {width: 3}, + opSetVariadicLen: {width: 2}, opCallNative: {width: 3}, opIsNil: {width: 1}, opIsNotNil: {width: 1}, diff --git a/ruleguard/quasigo/quasigo.go b/ruleguard/quasigo/quasigo.go index 7d457538..0f66bde2 100644 --- a/ruleguard/quasigo/quasigo.go +++ b/ruleguard/quasigo/quasigo.go @@ -138,8 +138,9 @@ type Func struct { // If int was pushed with PushInt(), it should be retrieved by PopInt(). // It's a bad idea to do a Push() and then PopInt() and vice-versa. type ValueStack struct { - objects []interface{} - ints []int + objects []interface{} + ints []int + variadicLen int } // Pop removes the top stack element and returns it. @@ -157,6 +158,19 @@ func (s *ValueStack) PopInt() int { return x } +// PopVariadic removes the `...` argument and returns it as a slice. +// +// Slice elements are in the order they were passed to the function, +// for example, a call Sprintf("%s:%d", filename, line) returns +// the slice []interface{filename, line}. +func (s *ValueStack) PopVariadic() []interface{} { + to := len(s.objects) + from := to - s.variadicLen + xs := s.objects[from:to] + s.objects = s.objects[:from] + return xs +} + // Push adds x to the stack. // Important: for int-typed values, use PushInt. func (s *ValueStack) Push(x interface{}) { s.objects = append(s.objects, x) } diff --git a/ruleguard/quasigo/stdlib/qfmt/qfmt.go b/ruleguard/quasigo/stdlib/qfmt/qfmt.go new file mode 100644 index 00000000..249ac256 --- /dev/null +++ b/ruleguard/quasigo/stdlib/qfmt/qfmt.go @@ -0,0 +1,17 @@ +package qfmt + +import ( + "fmt" + + "github.com/quasilyte/go-ruleguard/ruleguard/quasigo" +) + +func ImportAll(env *quasigo.Env) { + env.AddNativeFunc(`fmt`, `Sprintf`, Sprintf) +} + +func Sprintf(stack *quasigo.ValueStack) { + args := stack.PopVariadic() + format := stack.Pop().(string) + stack.Push(fmt.Sprintf(format, args...)) +} diff --git a/ruleguard/quasigo/testdata/variadiccall/main.go b/ruleguard/quasigo/testdata/variadiccall/main.go new file mode 100644 index 00000000..239ae9b9 --- /dev/null +++ b/ruleguard/quasigo/testdata/variadiccall/main.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + println(fmt.Sprintf("%s:%d", "hello", 10)) + + formatString := "hello, %s!" + println(fmt.Sprintf(formatString, "world")) + + println(fmt.Sprintf("no formatting args")) +}