Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow I/O operations to have a level of indirection. #123

Merged
merged 3 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -1454,13 +1454,18 @@ func printFn(env *env.Environment, args []primitive.Primitive) primitive.Primiti
return primitive.ArityError()
}

ioHelper := env.GetIOConfig()

// one arg
if len(args) == 1 {
// expand
str := expandStr(args[0].ToString())

// show & return
fmt.Println(str)
// Write via our configuration object
// Linter complains about ignored return values here..
_, _ = ioHelper.STDOUT.Write([]byte(str))
_, _ = ioHelper.STDOUT.Write([]byte("\n"))

return primitive.String(str)
}

Expand All @@ -1481,7 +1486,12 @@ func printFn(env *env.Environment, args []primitive.Primitive) primitive.Primiti
}

out := fmt.Sprintf(frmt, parm...)
fmt.Println(out)

// Write via our configuration object
// Linter complains about ignored return values here..
_, _ = ioHelper.STDOUT.Write([]byte(out))
_, _ = ioHelper.STDOUT.Write([]byte("\n"))

return primitive.String(out)
}

Expand Down
37 changes: 34 additions & 3 deletions builtins/builtins_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package builtins

import (
"bytes"
"math"
"os"
"path/filepath"
Expand All @@ -9,6 +10,7 @@ import (
"testing"
"time"

"github.com/skx/yal/config"
"github.com/skx/yal/env"
"github.com/skx/yal/eval"
"github.com/skx/yal/primitive"
Expand All @@ -21,6 +23,10 @@ var ENV *env.Environment
// init ensures our environment pointer is up to date.
func init() {
ENV = env.New()

// Environment will have a config
ENV.SetIOConfig(config.DefaultIO())

}

func TestArch(t *testing.T) {
Expand Down Expand Up @@ -3046,6 +3052,31 @@ func TestPrint(t *testing.T) {
t.Fatalf("got string, but wrong one %v", e2)
}

// Preserve the current config
orig := ENV.GetIOConfig()

// Setup a new STDOUT handle, pointing to a buffer
cfg := config.New()
tmp := new(bytes.Buffer)
cfg.STDOUT = tmp
ENV.SetIOConfig(cfg)

// Print something
printFn(ENV, []primitive.Primitive{
primitive.String("Hello %s!"),
primitive.List{
primitive.String("world"),
primitive.Number(42),
},
})

// Restore our handles
ENV.SetIOConfig(orig)

// Our buffer should be good
if tmp.String() != "Hello (world 42)!\n" {
t.Fatalf("(print 'hello (list)!') failed, got '%s'", tmp.String())
}
}

// TestRandom tests (random)
Expand Down Expand Up @@ -3524,7 +3555,7 @@ func TestStringLt(t *testing.T) {
}
}

func TestTrig(t *testing.T ) {
func TestTrig(t *testing.T) {

funs := []primitive.GolangPrimitiveFn{
acosFn,
Expand All @@ -3538,9 +3569,9 @@ func TestTrig(t *testing.T ) {
tanhFn,
}

for _, fn := range(funs) {
for _, fn := range funs {

out := fn(nil, []primitive.Primitive{} )
out := fn(nil, []primitive.Primitive{})

// Will lead to an error
e, ok := out.(primitive.Error)
Expand Down
1 change: 0 additions & 1 deletion builtins/file_info_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ func getGID(info os.FileInfo) (int, error) {
return int(stat.Gid), nil
}


// getUID returns the owner of the file, from the extended information
// available after a stat - that is not portable to Windows though.
//
Expand Down
47 changes: 47 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Package config provides an I/O abstraction for our interpreter,
// allowing it to be embedded and used in places where STDIN and STDOUT
// are not necessarily terminal-based.
//
// All input-reading uses the level of indirection provided here, and
// similarly output goes via the writer we hold here.
//
// This abstraction allows a host program to setup a different pair of
// streams prior to initializing the interpreter.
package config

import (
"io"
"os"
)

// Config is a holder for configuration which is used for interfacing
// the interpreter with the outside world.
type Config struct {

// STDIN is an input-reader used for the (read) function, when
// called with no arguments.
STDIN io.Reader

// STDOUT is the writer which is used for "(print)".
STDOUT io.Writer
}

// New returns a new configuration object
func New() *Config {

e := &Config{}
return e
}

// DefaultIO returns a configuration which uses the default
// input and output streams - i.e. STDIN and STDOUT work as
// expected
func DefaultIO() *Config {
e := New()

// Setup default input/output streams
e.STDIN = os.Stdin
e.STDOUT = os.Stdout

return e
}
28 changes: 25 additions & 3 deletions env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
// or call-frames, you can create a nested environment via NewEnvironment.
package env

import (
"github.com/skx/yal/config"
)

// Environment holds our state
type Environment struct {

Expand All @@ -17,6 +21,10 @@ type Environment struct {

// values holds the actual values
values map[string]any

// ioconfig holds the interface to the outside world,
// which is used for I/O
ioconfig *config.Config
}

// Get retrieves a value from the environment.
Expand Down Expand Up @@ -59,16 +67,18 @@ func (env *Environment) Items() map[string]any {
// New creates a new environment, with no parent.
func New() *Environment {
return &Environment{
values: map[string]any{},
values: map[string]any{},
ioconfig: config.New(),
}
}

// NewEnvironment creates a new environment, which will use the specified
// parent environment for values in a higher level.
func NewEnvironment(parent *Environment) *Environment {
return &Environment{
parent: parent,
values: map[string]any{},
parent: parent,
values: map[string]any{},
ioconfig: parent.ioconfig,
}
}

Expand All @@ -87,3 +97,15 @@ func (env *Environment) SetOuter(key string, value any) {
env.parent.SetOuter(key, value)
}
}

// SetIOConfig updates the configuration object which is stored
// in our environment
func (env *Environment) SetIOConfig(cfg *config.Config) {
env.ioconfig = cfg
}

// GetIOConfig returns the configuration object which is stored in
// our environment.
func (env *Environment) GetIOConfig() *config.Config {
return env.ioconfig
}
14 changes: 0 additions & 14 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
package eval

import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"os"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -59,14 +57,6 @@ type Eval struct {
// The key is the name of the fake method, the value the name of
// the field to get/set
accessors map[string]string

// STDIN is an input-reader used for the (read) function, when
// called with no arguments.
STDIN *bufio.Reader

// STDOUT is the writer which we should use for "(print)", but we
// currently don't.
STDOUT *bufio.Writer
}

// New constructs a new lisp interpreter.
Expand All @@ -93,10 +83,6 @@ func New(src string) *Eval {
accessors: make(map[string]string),
}

// Setup default input/output streams
e.STDIN = bufio.NewReader(os.Stdin)
e.STDOUT = bufio.NewWriter(os.Stdout)

// Setup the default symbol-table (interned) entries.

// true
Expand Down
7 changes: 7 additions & 0 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/skx/yal/builtins"
"github.com/skx/yal/config"
"github.com/skx/yal/env"
"github.com/skx/yal/primitive"
"github.com/skx/yal/stdlib"
Expand All @@ -26,6 +27,9 @@ func TestAliased(t *testing.T) {
// With a new environment
env := env.New()

// Environment will have a config
env.SetIOConfig(config.DefaultIO())

// Populate the default primitives
builtins.PopulateEnvironment(env)

Expand Down Expand Up @@ -416,6 +420,9 @@ a
// With a new environment
env := env.New()

// Environment will have a config
env.SetIOConfig(config.DefaultIO())

// Populate the default primitives
builtins.PopulateEnvironment(env)

Expand Down
6 changes: 5 additions & 1 deletion eval/specials.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package eval

import (
"bufio"
"fmt"
"strings"

Expand Down Expand Up @@ -286,7 +287,10 @@ func (ev *Eval) evalSpecialForm(name string, args []primitive.Primitive, e *env.

// zero arguments: read from STDIN
if len(args) == 0 {
input, err := ev.STDIN.ReadString('\n')

ioHelper := e.GetIOConfig()
r := bufio.NewReader(ioHelper.STDIN)
input, err := r.ReadString('\n')
if err != nil {
return primitive.Error(
fmt.Sprintf("failed to read from STDIN %s", err)), true
Expand Down
4 changes: 4 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/skx/yal/builtins"
"github.com/skx/yal/config"
"github.com/skx/yal/env"
"github.com/skx/yal/eval"
"github.com/skx/yal/primitive"
Expand Down Expand Up @@ -188,6 +189,9 @@ func FuzzYAL(f *testing.F) {
// Create a new environment
environment := env.New()

// Environment will have a config
environment.SetIOConfig(config.DefaultIO())

// Populate the default primitives
builtins.PopulateEnvironment(environment)

Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/skx/yal/builtins"
"github.com/skx/yal/config"
"github.com/skx/yal/env"
"github.com/skx/yal/eval"
"github.com/skx/yal/primitive"
Expand All @@ -43,6 +44,9 @@ func create() {
// Create a new environment
ENV = env.New()

// Setup the I/O
ENV.SetIOConfig(config.DefaultIO())

// Populate the default primitives
builtins.PopulateEnvironment(ENV)

Expand Down