From d2c4b3e8d3353430be9b649993eeeb0483e5f967 Mon Sep 17 00:00:00 2001 From: "Moises P. Sena" Date: Tue, 21 Nov 2023 15:11:02 -0300 Subject: [PATCH] core!: 1) improve buffer helper functions: `obstart`, `obend` and `flush`; 2) improve mixed `Text` type as safe string value; 3) improve custom non `Text` object write handler. --- builtin_types.go | 2 + builtins.go | 151 ++++++++++++++++++++++++++++++++++------- call.go | 6 ++ compiler.go | 57 ++++++++++------ compiler_test.go | 18 ++--- helpers.go | 16 +++++ objects.go | 21 ++++++ parser/node/expr.go | 16 ++++- parser/node/stmt.go | 11 ++- vm_caller.go | 4 +- vm_err_test.go | 12 ++-- vm_invoker.go | 16 +++-- vm_io.go | 52 +++++++++++++- vm_test.go | 160 ++++++++++++++++++++++++++++++++++++++------ 14 files changed, 444 insertions(+), 98 deletions(-) create mode 100644 helpers.go diff --git a/builtin_types.go b/builtin_types.go index 3b81996..3e8a2b4 100644 --- a/builtin_types.go +++ b/builtin_types.go @@ -72,6 +72,7 @@ var ( TFloat, TDecimal, TChar, + TText, TString, TBytes, TBuffer, @@ -133,6 +134,7 @@ func init() { TFloat = RegisterBuiltinType(BuiltinFloat, "float", Float(0), funcPf64RO(builtinFloatFunc)) TDecimal = RegisterBuiltinType(BuiltinDecimal, "decimal", Decimal{}, funcPOROe(builtinDecimalFunc)) TChar = RegisterBuiltinType(BuiltinChar, "char", Char(0), funcPOROe(builtinCharFunc)) + TText = RegisterBuiltinType(BuiltinText, "text", Text(""), builtinTextFunc) TString = RegisterBuiltinType(BuiltinString, "string", String(""), builtinStringFunc) TBytes = RegisterBuiltinType(BuiltinBytes, "bytes", Bytes{}, builtinBytesFunc) TBuffer = RegisterBuiltinType(BuiltinBuffer, "buffer", Buffer{}, builtinBufferFunc) diff --git a/builtins.go b/builtins.go index 9b28027..1f85478 100644 --- a/builtins.go +++ b/builtins.go @@ -37,6 +37,7 @@ const ( BuiltinFloat BuiltinDecimal BuiltinChar + BuiltinText BuiltinString BuiltinBytes BuiltinArray @@ -81,6 +82,9 @@ const ( BuiltinItems BuiltinVMPushWriter BuiltinVMPopWriter + BuiltinOBStart + BuiltinOBEnd + BuiltinFlush BuiltinIs BuiltinIsError @@ -186,6 +190,9 @@ var BuiltinsMap = map[string]BuiltinType{ "vmPushWriter": BuiltinVMPushWriter, "vmPopWriter": BuiltinVMPopWriter, + "obstart": BuiltinOBStart, + "obend": BuiltinOBEnd, + "flush": BuiltinFlush, "DISCARD_WRITER": BuiltinDiscardWriter, } @@ -248,10 +255,6 @@ var BuiltinObjects = map[BuiltinType]Object{ Name: "typeName", Value: funcPORO(builtinTypeNameFunc), }, - BuiltinWrite: &BuiltinFunction{ - Name: "write", - Value: builtinWriteFunc, - }, BuiltinPrint: &BuiltinFunction{ Name: "print", Value: builtinPrintFunc, @@ -372,6 +375,18 @@ var BuiltinObjects = map[BuiltinType]Object{ Name: "vmPopWriter", Value: builtinVMPopWriterFunc, }, + BuiltinOBStart: &BuiltinFunction{ + Name: "obstart", + Value: builtinOBStartFunc, + }, + BuiltinOBEnd: &BuiltinFunction{ + Name: "obend", + Value: builtinOBEndFunc, + }, + BuiltinFlush: &BuiltinFunction{ + Name: "flush", + Value: builtinFlushFunc, + }, BuiltinWrongNumArgumentsError: ErrWrongNumArguments, BuiltinInvalidOperatorError: ErrInvalidOperator, @@ -388,6 +403,10 @@ var BuiltinObjects = map[BuiltinType]Object{ } func init() { + BuiltinObjects[BuiltinWrite] = &BuiltinFunction{ + Name: "write", + Value: builtinWriteFunc, + } BuiltinObjects[BuiltinMap] = &BuiltinFunction{ Name: "map", Value: builtinMapFunc, @@ -945,6 +964,23 @@ func builtinCharFunc(arg Object) (Object, error) { ) } +func builtinTextFunc(c Call) (ret Object, err error) { + if err := c.Args.CheckLen(1); err != nil { + return Nil, err + } + + o := c.Args.Get(0) + + if ts, _ := o.(ToStringer); ts != nil { + var s String + s, err = ts.Stringer(c) + ret = Text(s) + } else { + ret = Text(o.ToString()) + } + return +} + func builtinStringFunc(c Call) (ret Object, err error) { if err := c.Args.CheckLen(1); err != nil { return Nil, err @@ -1067,6 +1103,31 @@ func builtinWriteFunc(c Call) (ret Object, err error) { w io.Writer = c.VM.StdOut total Int n int + write = func(w io.Writer, obj Object) (total Int, err error) { + var n int + switch t := obj.(type) { + case Text: + n, err = w.Write([]byte(t)) + case String: + n, err = w.Write([]byte(t)) + case Bytes: + n, err = w.Write(t) + case BytesConverter: + var b Bytes + if b, err = t.ToBytes(); err == nil { + n, err = w.Write(b) + } + case io.WriterTo: + var i64 int64 + i64, err = t.WriteTo(w) + total = Int(i64) + default: + n, err = fmt.Fprint(w, t) + } + total += Int(n) + return + } + convert CallerObject ) if err = c.Args.CheckMinLen(1); err != nil { @@ -1079,27 +1140,56 @@ func builtinWriteFunc(c Call) (ret Object, err error) { c.Args.Shift() } - c.Args.Walk(func(i int, arg Object) (continueLoop bool) { - switch t := arg.(type) { - case String: - n, err = w.Write([]byte(t)) - case Bytes: - n, err = w.Write(t) - case BytesConverter: - var b Bytes - if b, err = t.ToBytes(); err == nil { - n, err = w.Write(b) + if convertValue := c.NamedArgs.GetValueOrNil("convert"); convertValue != nil { + convert = convertValue.(CallerObject) + } + + if convert == nil { + c.Args.Walk(func(i int, arg Object) (continueLoop bool) { + switch t := arg.(type) { + case Text: + n, err = w.Write([]byte(t)) + total += Int(n) + default: + total, err = write(w, arg) } - case io.WriterTo: - var i64 int64 - i64, err = t.WriteTo(w) - total = Int(i64) - default: - n, err = fmt.Fprint(w, arg) + return err == nil + }) + } else { + var ( + convertCallArgs = Array{ + NewWriter(w), + &Function{ + Value: func(c Call) (_ Object, err error) { + var i Int + i, err = write(c.Args.MustGet(0).(Writer), c.Args.MustGet(1)) + return i, err + }, + }, + nil, + } + caller VMCaller + ) + if caller, err = NewInvoker(c.VM, convert).Caller(Args{convertCallArgs}, nil); err != nil { + return } - total += Int(n) - return err == nil - }) + + c.Args.Walk(func(i int, arg Object) (continueLoop bool) { + switch t := arg.(type) { + case Text: + n, err = w.Write([]byte(t)) + total += Int(n) + default: + var iO Object + convertCallArgs[2] = t + iO, err = caller.Call() + if i, ok := iO.(Int); ok { + total += i + } + } + return err == nil + }) + } return total, err } @@ -1591,8 +1681,19 @@ func builtinVMPushWriterFunc(c Call) (ret Object, err error) { } func builtinVMPopWriterFunc(c Call) (ret Object, err error) { - c.VM.StdOut.Pop() - return Nil, nil + return c.VM.StdOut.Pop(), nil +} + +func builtinOBStartFunc(c Call) (ret Object, err error) { + return builtinVMPushWriterFunc(Call{VM: c.VM, Args: Args{Array{&Buffer{}}}}) +} + +func builtinOBEndFunc(c Call) (ret Object, err error) { + return c.VM.StdOut.Pop(), nil +} + +func builtinFlushFunc(c Call) (Object, error) { + return c.VM.StdOut.Flush() } func builtinWrapFunc(c Call) (ret Object, err error) { diff --git a/call.go b/call.go index 30afb78..cd33955 100644 --- a/call.go +++ b/call.go @@ -189,6 +189,12 @@ func (o Args) Get(n int) (v Object) { return } +// MustGet returns the nth argument. If n is greater than the number of arguments, +// it returns the nth variadic argument or Nil. +func (o Args) MustGet(n int) Object { + return o.GetDefault(n, Nil) +} + func (o Args) GetIJ(n int) (i, j int, ok bool) { var ( at int diff --git a/compiler.go b/compiler.go index bb94cac..be0d8bf 100644 --- a/compiler.go +++ b/compiler.go @@ -71,21 +71,22 @@ type ( // CompilerOptions represents customizable options for Compile(). CompilerOptions struct { - ModuleMap *ModuleMap - ModulePath string - Constants []Object - SymbolTable *SymbolTable - Trace io.Writer - TraceParser bool - TraceCompiler bool - TraceOptimizer bool - OptimizerMaxCycle int - OptimizeConst bool - OptimizeExpr bool - Mixed bool - MixedWriteFunction node.Expr - moduleStore *moduleStore - constsCache map[Object]int + ModuleMap *ModuleMap + ModulePath string + Constants []Object + SymbolTable *SymbolTable + Trace io.Writer + TraceParser bool + TraceCompiler bool + TraceOptimizer bool + OptimizerMaxCycle int + OptimizeConst bool + OptimizeExpr bool + Mixed bool + MixedWriteFunction node.Expr + MixedExprToTextFunc node.Expr + moduleStore *moduleStore + constsCache map[Object]int } // CompilerError represents a compiler error. @@ -322,19 +323,28 @@ stmts: for z, s := range stmt[i:j] { switch t := s.(type) { case *node.TextStmt: - exprs[z] = &node.StringLit{Value: t.Literal} + exprs[z] = &node.TextLit{StringLit: node.StringLit{Value: t.Literal}} case *node.ExprToTextStmt: exprs[z] = t.Expr } } - wf := c.opts.MixedWriteFunction + var ( + wf = c.opts.MixedWriteFunction + na node.CallExprNamedArgs + ) if wf == nil { wf = &node.Ident{Name: "write"} } + if c.opts.MixedExprToTextFunc != nil { + na = *new(node.CallExprNamedArgs).AppendS("convert", c.opts.MixedExprToTextFunc) + } err = c.compileCallExpr(&node.CallExpr{ - Func: wf, - CallArgs: node.CallArgs{Args: node.CallExprArgs{Values: exprs}}, + Func: wf, + CallArgs: node.CallArgs{ + Args: node.CallExprArgs{Values: exprs}, + NamedArgs: na, + }, }) if err != nil { return @@ -413,6 +423,8 @@ func (c *Compiler) Compile(nd ast.Node) error { } case *node.StringLit: c.emit(nt, OpConstant, c.addConstant(String(nt.Value))) + case *node.TextLit: + c.emit(nt, OpConstant, c.addConstant(Text(nt.Value))) case *node.CharLit: c.emit(nt, OpConstant, c.addConstant(Char(nt.Value))) case *node.NilLit: @@ -483,12 +495,15 @@ func (c *Compiler) Compile(nd ast.Node) error { case *node.CondExpr: return c.compileCondExpr(nt) case *node.TextStmt: - return c.Compile(&node.StringLit{Value: nt.Literal}) + return c.Compile(&node.TextLit{StringLit: node.StringLit{Value: nt.Literal}}) case *node.EmptyStmt: case *node.ConfigStmt: if nt.Options.WriteFunc != nil { c.opts.MixedWriteFunction = nt.Options.WriteFunc } + if nt.Options.ExprToTextFunc != nil { + c.opts.MixedExprToTextFunc = nt.Options.ExprToTextFunc + } case nil: default: return c.errorf(nt, `%[1]T "%[1]v" not implemented`, nt) @@ -530,7 +545,7 @@ func (c *Compiler) addConstant(obj Object) (index int) { }() switch obj.(type) { - case Int, Uint, String, Bool, Float, Char, *NilType: + case Int, Uint, String, Text, Bool, Float, Char, *NilType: i, ok := c.constsCache[obj] if ok { index = i diff --git a/compiler_test.go b/compiler_test.go index 88f882e..ebeb08d 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -194,7 +194,7 @@ func TestCompiler_CompileIfNull(t *testing.T) { func TestCompiler_Mixed(t *testing.T) { expectCompileMixed(t, "# gad: writer=myfn\n#{- var myfn -} a", bytecode( - Array{String("a")}, + Array{Text("a")}, compFunc(concatInsts( makeInst(OpNull), makeInst(OpDefineLocal, 0), @@ -206,7 +206,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompileMixed(t, `a#{=1}c`, bytecode( - Array{String("a"), Int(1), String("c")}, + Array{Text("a"), Int(1), Text("c")}, compFunc(concatInsts( makeInst(OpGetBuiltin, int(BuiltinWrite)), makeInst(OpConstant, 0), @@ -218,7 +218,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompileMixed(t, `a#{=1}c#{x := 5}#{=x}`, bytecode( - Array{String("a"), Int(1), String("c"), Int(5)}, + Array{Text("a"), Int(1), Text("c"), Int(5)}, compFunc(concatInsts( makeInst(OpGetBuiltin, int(BuiltinWrite)), makeInst(OpConstant, 0), @@ -235,7 +235,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed, writer=myfn\n#{ var myfn -} a", bytecode( - Array{String("a")}, + Array{Text("a")}, compFunc(concatInsts( makeInst(OpNull), makeInst(OpDefineLocal, 0), @@ -247,7 +247,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed\n#{- a := begin} a #{end}", bytecode( - Array{String(" a ")}, + Array{Text(" a ")}, compFunc(concatInsts( makeInst(OpConstant, 0), makeInst(OpDefineLocal, 0), @@ -256,7 +256,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed\n#{- a := begin -} a #{- end}", bytecode( - Array{String("a")}, + Array{Text("a")}, compFunc(concatInsts( makeInst(OpConstant, 0), makeInst(OpDefineLocal, 0), @@ -265,7 +265,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed\n#{- a := begin -} a #{- end; return a}", bytecode( - Array{String("a")}, + Array{Text("a")}, compFunc(concatInsts( makeInst(OpConstant, 0), makeInst(OpDefineLocal, 0), @@ -275,7 +275,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed\n#{- a := begin -} a #{- end}#{return a}", bytecode( - Array{String("a")}, + Array{Text("a")}, compFunc(concatInsts( makeInst(OpConstant, 0), makeInst(OpDefineLocal, 0), @@ -285,7 +285,7 @@ func TestCompiler_Mixed(t *testing.T) { )) expectCompile(t, "# gad: mixed\n#{- a := begin -} a #{- end} b #{return a}", bytecode( - Array{String("a"), String(" b ")}, + Array{Text("a"), Text(" b ")}, compFunc(concatInsts( makeInst(OpConstant, 0), makeInst(OpDefineLocal, 0), diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..c0a1307 --- /dev/null +++ b/helpers.go @@ -0,0 +1,16 @@ +package gad + +func ExprToTextOverride(name string, f func(vm *VM, w Writer, old func(w Writer, expr Object) (n Int, err error), expr Object) (n Int, err error)) CallerObject { + return &Function{ + Name: name, + Value: func(c Call) (_ Object, err error) { + var n Int + n, err = f(c.VM, c.Args.MustGet(0).(Writer), func(w Writer, expr Object) (_ Int, err error) { + var n Object + n, err = c.Args.MustGet(1).(CallerObject).Call(Call{Args: Args{Array{w, expr}}}) + return n.(Int), err + }, c.Args.MustGet(2)) + return n, err + }, + } +} diff --git a/objects.go b/objects.go index cced008..6ff139b 100644 --- a/objects.go +++ b/objects.go @@ -162,6 +162,27 @@ func (o Bool) Format(s fmt.State, verb rune) { fmt.Fprintf(s, format, bool(o)) } +type Text string + +func (o Text) Type() ObjectType { + return TText +} + +func (o Text) ToString() string { + return string(o) +} + +func (o Text) IsFalsy() bool { + return len(o) == 0 +} + +func (o Text) Equal(right Object) bool { + if v, ok := right.(Text); ok { + return o == v + } + return false +} + // String represents string values and implements Object interface. type String string diff --git a/parser/node/expr.go b/parser/node/expr.go index dc21f91..c06ec0d 100644 --- a/parser/node/expr.go +++ b/parser/node/expr.go @@ -866,6 +866,10 @@ func (e *StringLit) String() string { return e.Literal } +type TextLit struct { + StringLit +} + // UnaryExpr represents an unary operator expression. type UnaryExpr struct { Expr Expr @@ -979,14 +983,22 @@ type CallExprNamedArgs struct { Var *NamedArgVarLit } -func (a *CallExprNamedArgs) Append(name NamedArgExpr, value Expr) { +func (a *CallExprNamedArgs) Append(name NamedArgExpr, value Expr) *CallExprNamedArgs { a.Names = append(a.Names, name) a.Values = append(a.Values, value) + return a +} + +func (a *CallExprNamedArgs) AppendS(name string, value Expr) *CallExprNamedArgs { + a.Names = append(a.Names, NamedArgExpr{Ident: &Ident{Name: name}}) + a.Values = append(a.Values, value) + return a } -func (a *CallExprNamedArgs) Prepend(name NamedArgExpr, value Expr) { +func (a *CallExprNamedArgs) Prepend(name NamedArgExpr, value Expr) *CallExprNamedArgs { a.Names = append([]NamedArgExpr{name}, a.Names...) a.Values = append([]Expr{value}, a.Values...) + return a } func (a *CallExprNamedArgs) Get(name NamedArgExpr) (index int, value Expr) { diff --git a/parser/node/stmt.go b/parser/node/stmt.go index 3aca9c5..2da4fb1 100644 --- a/parser/node/stmt.go +++ b/parser/node/stmt.go @@ -552,9 +552,10 @@ func (e *ExprToTextStmt) String() string { } type ConfigOptions struct { - Mixed bool - NoMixed bool - WriteFunc Expr + Mixed bool + NoMixed bool + WriteFunc Expr + ExprToTextFunc Expr } type ConfigStmt struct { @@ -599,6 +600,10 @@ func (c *ConfigStmt) ParseElements() { if k.Value != nil { c.Options.WriteFunc = k.Value } + case "expr_to_text": + if k.Value != nil { + c.Options.ExprToTextFunc = k.Value + } } } } diff --git a/vm_caller.go b/vm_caller.go index 20b1931..4fa815c 100644 --- a/vm_caller.go +++ b/vm_caller.go @@ -40,7 +40,7 @@ func (r *vmCompiledFuncCaller) Close() { type vmObjectCaller struct { vm *VM args Args - namedArgs *NamedArgs + namedArgs NamedArgs closed bool callee CallerObject } @@ -49,7 +49,7 @@ func (r *vmObjectCaller) Call() (ret Object, err error) { return r.callee.Call(Call{ VM: r.vm, Args: r.args, - NamedArgs: *r.namedArgs, + NamedArgs: r.namedArgs, }) } diff --git a/vm_err_test.go b/vm_err_test.go index efa9c55..debdb3c 100644 --- a/vm_err_test.go +++ b/vm_err_test.go @@ -733,7 +733,7 @@ func TestVMExamples(t *testing.T) { cleanupCall = 0 expectRun(t, ex1MainScript, newOpts(). - out(printWriter). + Out(printWriter). Module("module", ex1Module). Globals(Dict{ "DoCleanup": &Function{ @@ -790,7 +790,7 @@ func TestVMExamples(t *testing.T) { return mapEach(args, func(x) { return x*multiplier }) `, newOpts(). - out(printWriter). + Out(printWriter). Globals(Dict{"multiplier": Int(2)}). Args(Int(1), Int(2), Int(3), Int(4)), Array{Int(2), Int(4), Int(6), Int(8)}, @@ -827,16 +827,16 @@ func TestVMExamples(t *testing.T) { } ` var g IndexGetSetter = Dict{} - expectRun(t, scr, newOpts().out(printWriter).Globals(g).Args(Nil), Int(-1)) + expectRun(t, scr, newOpts().Out(printWriter).Globals(g).Args(Nil), Int(-1)) require.Equal(t, 1, len(g.(Dict))) require.Equal(t, True, g.(Dict)["notAnInt"]) g = Dict{} - expectRun(t, scr, newOpts().out(printWriter).Globals(g).Args(Int(0)), Int(-1)) + expectRun(t, scr, newOpts().Out(printWriter).Globals(g).Args(Int(0)), Int(-1)) require.Equal(t, 1, len(g.(Dict))) require.Equal(t, True, g.(Dict)["zeroDivision"]) - expectRun(t, scr, newOpts().out(printWriter).Args(Int(2)), Int(5)) + expectRun(t, scr, newOpts().Out(printWriter).Args(Int(2)), Int(5)) g = &SyncMap{Value: Dict{"stats": Dict{"fn1": Int(0), "fn2": Int(0)}}} expectRun(t, ` @@ -858,7 +858,7 @@ func TestVMExamples(t *testing.T) { stats.fn2++ /* ... */ } - `).out(printWriter).Globals(g).Skip2Pass(), Nil) + `).Out(printWriter).Globals(g).Skip2Pass(), Nil) require.Equal(t, Int(1), g.(*SyncMap).Value["stats"].(Dict)["fn1"]) require.Equal(t, Int(1), g.(*SyncMap).Value["stats"].(Dict)["fn2"]) } diff --git a/vm_invoker.go b/vm_invoker.go index 870ce00..2780496 100644 --- a/vm_invoker.go +++ b/vm_invoker.go @@ -124,11 +124,13 @@ func (inv *Invoker) Caller(args Args, namedArgs *NamedArgs) (VMCaller, error) { if callee == nil { return nil, ErrNotCallable.NewError(inv.callee.Type().Name()) } - - return &vmObjectCaller{ - vm: inv.vm, - args: args, - namedArgs: namedArgs, - callee: callee, - }, nil + caller := &vmObjectCaller{ + vm: inv.vm, + args: args, + callee: callee, + } + if namedArgs != nil { + caller.namedArgs = *namedArgs + } + return caller, nil } diff --git a/vm_io.go b/vm_io.go index 13bec9d..294d269 100644 --- a/vm_io.go +++ b/vm_io.go @@ -43,9 +43,59 @@ func (w *StackWriter) Push(sw io.Writer) { w.last++ } -func (w *StackWriter) Pop() { +func (w *StackWriter) Pop() Writer { + last := w.writers[w.last] w.writers = w.writers[:w.last] w.last-- + + switch t := last.(type) { + case Writer: + return t + default: + return NewWriter(t) + } +} + +func (w *StackWriter) Old() Writer { + if w.last == 0 { + return nil + } + switch t := w.writers[w.last-1].(type) { + case Writer: + return t + default: + return NewWriter(t) + } +} + +func (w *StackWriter) Current() Writer { + switch t := w.writers[w.last].(type) { + case Writer: + return t + default: + return NewWriter(t) + } +} + +func (w *StackWriter) Flush() (n Int, err error) { + if w.last == 0 { + return + } + old, cur := w.writers[w.last-1], w.writers[w.last] + + switch t := cur.(type) { + case io.WriterTo: + var n_ int64 + n_, err = t.WriteTo(old) + n = Int(n_) + case io.Reader: + var n_ int64 + n_, err = io.Copy(old, t) + n = Int(n_) + default: + err = ErrType.NewError("current writer in't io.Reader|io.WriterTo") + } + return } type StackReader struct { diff --git a/vm_test.go b/vm_test.go index 40f9002..29ae408 100644 --- a/vm_test.go +++ b/vm_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/gad-lang/gad/parser/node" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1386,39 +1387,39 @@ keyValueArray(keyValue("d",4))))`, var stdOut bytes.Buffer stdOut.Reset() - expectRun(t, `printf("test")`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `printf("test")`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test", stdOut.String()) stdOut.Reset() - expectRun(t, `printf("test %d", 1)`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `printf("test %d", 1)`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test 1", stdOut.String()) stdOut.Reset() - expectRun(t, `printf("test %d %d", 1, 2u)`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `printf("test %d %d", 1, 2u)`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test 1 2", stdOut.String()) stdOut.Reset() - expectRun(t, `println()`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `println()`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "\n", stdOut.String()) stdOut.Reset() - expectRun(t, `println("test")`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `println("test")`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test\n", stdOut.String()) stdOut.Reset() - expectRun(t, `println("test", 1)`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `println("test", 1)`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test 1\n", stdOut.String()) stdOut.Reset() - expectRun(t, `println("test", 1, 2u)`, newOpts().out(&stdOut).Skip2Pass(), Nil) + expectRun(t, `println("test", 1, 2u)`, newOpts().Out(&stdOut).Skip2Pass(), Nil) require.Equal(t, "test 1 2\n", stdOut.String()) expectRun(t, `return sprintf("test")`, - newOpts().out(&stdOut).Skip2Pass(), String("test")) + newOpts().Out(&stdOut).Skip2Pass(), String("test")) expectRun(t, `return sprintf("test %d", 1)`, - newOpts().out(&stdOut).Skip2Pass(), String("test 1")) + newOpts().Out(&stdOut).Skip2Pass(), String("test 1")) expectRun(t, `return sprintf("test %d %t", 1, true)`, - newOpts().out(&stdOut).Skip2Pass(), String("test 1 true")) + newOpts().Out(&stdOut).Skip2Pass(), String("test 1 true")) expectRun(t, `f := func(*args;**kwargs){ return [args, kwargs.dict] }; return wrap(f, 1, a=3)(2, b=4)`, nil, Array{Array{Int(1), Int(2)}, Dict{"a": Int(3), "b": Int(4)}}) @@ -2984,7 +2985,6 @@ func TestVMScopes(t *testing.T) { out = a } return out`, nil, Int(0)) - // shadowing function level expectRun(t, ` a := 5 @@ -3819,6 +3819,91 @@ return [ ) } +func TestVMMixedOutput(t *testing.T) { + expectRun(t, `# gad: mixed +#{obstart() -} +a +#{- = 2 -} +b +#{- return string(obend())} +`, + newOpts(), + String("a2b"), + ) + + expectRun(t, `# gad: mixed +#{obstart() -} +a +#{- obstart() -} +#{- = 2 -} +b +#{- flush(); obend() -} +#{- return string(obend())} +`, + newOpts(), + String("a2b"), + ) + exprToText := ExprToTextOverride( + "expr2text", + func(vm *VM, w Writer, old func(w Writer, expr Object) (n Int, err error), expr Object) (n Int, err error) { + var b strings.Builder + n, err = old(NewWriter(&b), expr) + w.Write([]byte(strings.ReplaceAll(b.String(), `"`, `\"`))) + return + }, + ) + + expectRun(t, ` +global expr2text +global value +obstart() + +# gad: mixed, expr_to_text=expr2text +{key:"#{= value}"} +#{- return string(obend())} +`, + newOpts().Globals(Dict{ + "value": String(`a"b`), + "expr2text": exprToText, + }), + String(`{key:"a\"b"}`), + ) + + expectRun(t, ` +#{ + global value + obstart() +-} +{key:"#{= value}"} +#{- return string(obend())} +`, + newOpts(). + Mixed(). + ExprToTextFunc("expr2text"). + Builtins(map[string]Object{ + "expr2text": exprToText, + }). + Globals(Dict{ + "value": String(`a"b`), + }), + String(`{key:"a\"b"}`), + ) + + expectRun(t, `#{global value-}{key:"#{= value}"}`, + newOpts(). + Mixed(). + Buffered(). + ExprToTextFunc("expr2text"). + Builtins(map[string]Object{ + "expr2text": exprToText, + }). + Globals(Dict{ + "value": String(`a"b`), + }), + String(`{key:"a\"b"}`), + ) +} + func TestVMReflectSlice(t *testing.T) { expectRun(t, `param s;return func(z, *x) { return append([], *x) }(100, *s)`, newOpts().Args(MustToObject([]int{4, 7})), @@ -3853,22 +3938,25 @@ func (*callerObject) Call(c Call) (Object, error) { var _ CallerObject = &callerObject{} type testopts struct { - globals IndexGetSetter - args []Object - namedArgs *NamedArgs - moduleMap *ModuleMap - skip2pass bool - isCompilerErr bool - noPanic bool - stdout Writer - builtins map[string]Object + globals IndexGetSetter + args []Object + namedArgs *NamedArgs + moduleMap *ModuleMap + skip2pass bool + isCompilerErr bool + noPanic bool + stdout Writer + builtins map[string]Object + exprToTextFunc string + mixed bool + buffered bool } func newOpts() *testopts { return &testopts{} } -func (t *testopts) out(w io.Writer) *testopts { +func (t *testopts) Out(w io.Writer) *testopts { t.stdout = NewWriter(w) return t } @@ -3936,6 +4024,21 @@ func (t *testopts) Module(name string, module any) *testopts { return t } +func (t *testopts) ExprToTextFunc(name string) *testopts { + t.exprToTextFunc = name + return t +} + +func (t *testopts) Mixed() *testopts { + t.mixed = true + return t +} + +func (t *testopts) Buffered() *testopts { + t.buffered = true + return t +} + func expectErrHas(t *testing.T, script string, opts *testopts, expectMsg string) { t.Helper() if expectMsg == "" { @@ -4098,6 +4201,12 @@ func expectRun(t *testing.T, script string, opts *testopts, expect Object) { } tC.opts.SymbolTable = NewSymbolTable(builtins) } + if opts.exprToTextFunc != "" { + tC.opts.MixedExprToTextFunc = &node.Ident{Name: opts.exprToTextFunc} + } + if opts.mixed { + tC.opts.Mixed = true + } gotBc, err := Compile([]byte(script), tC.opts) require.NoError(t, err) // create a copy of the bytecode before execution to test bytecode @@ -4122,13 +4231,20 @@ func expectRun(t *testing.T, script string, opts *testopts, expect Object) { if opts.namedArgs != nil { ropts.NamedArgs = opts.namedArgs.Copy().(*NamedArgs) } - if opts.stdout != nil { + var buf *bytes.Buffer + if opts.buffered { + buf = &bytes.Buffer{} + ropts.StdOut = buf + } else if opts.stdout != nil { ropts.StdOut = opts.stdout } got, err := vm.SetRecover(opts.noPanic).RunOpts(ropts) if !assert.NoErrorf(t, err, "Code:\n%s\n", script) { gotBc.Fprint(os.Stderr) } + if buf != nil && got == Nil { + got = String(buf.String()) + } if !reflect.DeepEqual(expect, got) { var buf bytes.Buffer gotBc.Fprint(&buf)