diff --git a/eval/eval.go b/eval/eval.go index 46dde742..b092a639 100644 --- a/eval/eval.go +++ b/eval/eval.go @@ -2,6 +2,7 @@ package eval import ( "fmt" + "io" "math" "os" "strings" @@ -13,11 +14,13 @@ import ( ) type State struct { - env *object.Environment + env *object.Environment + Out io.Writer + NoLog bool // turn log() into print() (for EvalString) } func NewState() *State { - return &State{env: object.NewEnvironment()} + return &State{env: object.NewEnvironment(), Out: os.Stdout} } // Forward to env to count the number of bindings. Used mostly to know if there are any macros. @@ -213,10 +216,19 @@ func (s *State) evalBuiltin(node *ast.Builtin) object.Object { buf.WriteString(r.Inspect()) } } - if node.Type == token.PRINT { - os.Stdout.WriteString(buf.String()) + doLog := node.Type != token.PRINT + if s.NoLog && doLog { + doLog = false + buf.WriteRune('\n') // log() has a implicit newline when using log.Xxx, print() doesn't. + } + if doLog { + // Consider passing the arguments to log instead of making a string concatenation. + log.Printf("%s", buf.String()) } else { - log.Printf(buf.String()) + _, err := s.Out.Write([]byte(buf.String())) + if err != nil { + log.Warnf("print: %v", err) + } } return object.NULL case token.FIRST: diff --git a/repl/repl.go b/repl/repl.go index b4072c27..c4160ec5 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -51,8 +51,10 @@ func EvalAll(s, macroState *eval.State, in io.Reader, out io.Writer, options Opt func EvalString(what string) string { s := eval.NewState() macroState := eval.NewState() - out := strings.Builder{} - EvalOne(s, macroState, what, &out, Options{All: true, ShowEval: true, NoColor: true}) + out := &strings.Builder{} + s.Out = out + s.NoLog = true + EvalOne(s, macroState, what, out, Options{All: true, ShowEval: true, NoColor: true}) return out.String() } diff --git a/repl/repl_test.go b/repl/repl_test.go new file mode 100644 index 00000000..209418db --- /dev/null +++ b/repl/repl_test.go @@ -0,0 +1,30 @@ +package repl_test + +import ( + "testing" + + "grol.io/grol/repl" +) + +func TestEvalString(t *testing.T) { + s := ` +fact=func(n) { // function + log("called fact", n) // log (timestamped stderr output) + if (n<=1) { + return 1 + } + n*fact(n-1) +} +result = fact(5) +print("Factorial of 5 is", result, "\n") // print to stdout +result` + expected := `called fact 5 +called fact 4 +called fact 3 +called fact 2 +called fact 1 +Factorial of 5 is 120` + " \n120\n" // there is an extra space before \n that vscode wants to remove + if got := repl.EvalString(s); got != expected { + t.Errorf("EvalString() got\n---\n%s\n---want---\n%s\n---", got, expected) + } +}