Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The following customizations have been made to adapt the original `ichiban/prolo
- Removed support for trigonometric functions (`sin`, `cos`, `tan`, `asin`, `acos`, `atan`).
- Introduced VM hooks for enhanced Prolog execution control.
- Added support for the `Dict` term.
- Added support for `read_write` mode for bidirectional file I/O, enabling half-duplex transactional devices in the host's VFS.
- `halt/0` and `halt/1` are forbidden and will throw an error.

## License
Expand Down
1 change: 1 addition & 0 deletions engine/atom.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ var (
atomPrologFlag = NewAtom("prolog_flag")
atomQuoted = NewAtom("quoted")
atomRead = NewAtom("read")
atomReadWrite = NewAtom("read_write")
atomReadOption = NewAtom("read_option")
atomRem = NewAtom("rem")
atomReposition = NewAtom("reposition")
Expand Down
15 changes: 10 additions & 5 deletions engine/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ func SetInput(vm *VM, streamOrAlias Term, k Cont, env *Env) *Promise {
return Error(err)
}

if s.mode != ioModeRead {
if s.mode != ioModeRead && s.mode != ioModeReadWrite {
return Error(permissionError(operationInput, permissionTypeStream, streamOrAlias, env))
}

Expand All @@ -1208,7 +1208,7 @@ func SetOutput(vm *VM, streamOrAlias Term, k Cont, env *Env) *Promise {
return Error(err)
}

if s.mode != ioModeWrite && s.mode != ioModeAppend {
if s.mode != ioModeWrite && s.mode != ioModeAppend && s.mode != ioModeReadWrite {
return Error(permissionError(operationOutput, permissionTypeStream, streamOrAlias, env))
}

Expand Down Expand Up @@ -1254,9 +1254,10 @@ func Open(vm *VM, sourceSink, mode, stream, options Term, k Cont, env *Env) *Pro
case Atom:
var ok bool
streamMode, ok = map[Atom]ioMode{
atomRead: ioModeRead,
atomWrite: ioModeWrite,
atomAppend: ioModeAppend,
atomRead: ioModeRead,
atomWrite: ioModeWrite,
atomAppend: ioModeAppend,
atomReadWrite: ioModeReadWrite,
}[m]
if !ok {
return Error(domainError(validDomainIOMode, m, env))
Expand All @@ -1275,6 +1276,10 @@ func Open(vm *VM, sourceSink, mode, stream, options Term, k Cont, env *Env) *Pro
if s.mode == ioModeRead {
s.source = f
_ = s.initRead()
} else if s.mode == ioModeReadWrite {
s.source = f
s.sink = f
_ = s.initRead()
} else {
s.sink = f
}
Expand Down
143 changes: 143 additions & 0 deletions engine/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3364,6 +3364,7 @@ func TestSetInput(t *testing.T) {
foo, bar := NewAtom("foo"), NewAtom("bar")
input := Stream{mode: ioModeRead, alias: foo}
output := Stream{mode: ioModeAppend}
readWrite := Stream{mode: ioModeReadWrite, source: os.Stdin, sink: os.Stdout}
stream := NewVariable()

var vm VM
Expand All @@ -3378,6 +3379,7 @@ func TestSetInput(t *testing.T) {
}{
{title: "stream", streamOrAlias: &input, ok: true, input: &input},
{title: "alias", streamOrAlias: foo, ok: true, input: &input},
{title: "read-write stream", streamOrAlias: &readWrite, ok: true, input: &readWrite},

// 8.11.3.3 Errors
{title: "a", streamOrAlias: stream, err: InstantiationError(nil)},
Expand All @@ -3400,6 +3402,7 @@ func TestSetOutput(t *testing.T) {
foo, bar := NewAtom("foo"), NewAtom("bar")
input := Stream{mode: ioModeRead}
output := Stream{mode: ioModeAppend, alias: foo}
readWrite := Stream{mode: ioModeReadWrite, source: os.Stdin, sink: os.Stdout}
stream := NewVariable()

var vm VM
Expand All @@ -3414,6 +3417,7 @@ func TestSetOutput(t *testing.T) {
}{
{title: "stream", streamOrAlias: &output, ok: true, output: &output},
{title: "alias", streamOrAlias: foo, ok: true, output: &output},
{title: "read-write stream", streamOrAlias: &readWrite, ok: true, output: &readWrite},

// 8.11.4.3 Errors
{title: "a", streamOrAlias: stream, err: InstantiationError(nil)},
Expand Down Expand Up @@ -3684,6 +3688,102 @@ func TestOpen(t *testing.T) {
assert.True(t, ok)
})

t.Run("read_write", func(t *testing.T) {
f, err := os.CreateTemp("", "open_test_read_write")
assert.NoError(t, err)
defer func() {
assert.NoError(t, os.Remove(f.Name()))
}()

_, err = fmt.Fprintf(f, "initial content\n")
assert.NoError(t, err)
assert.NoError(t, f.Close())

t.Run("can read and write", func(t *testing.T) {
v := NewVariable()
ok, err := Open(&vm, NewAtom(f.Name()), atomReadWrite, v, List(), func(env *Env) *Promise {
ref, ok := env.lookup(v)
assert.True(t, ok)
s, ok := ref.(*Stream)
assert.True(t, ok)

// Should be able to read
assert.NoError(t, s.initRead())
b := make([]byte, 7)
n, err := s.source.(io.Reader).Read(b)
assert.NoError(t, err)
assert.Equal(t, 7, n)
assert.Equal(t, "initial", string(b))

// Should be able to write
_, err = fmt.Fprintf(s.sink, " + new")
assert.NoError(t, err)

return Bool(true)
}, nil).Force(context.Background())
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("stream has both input and output properties", func(t *testing.T) {
v := NewVariable()
ok, err := Open(&vm, NewAtom(f.Name()), atomReadWrite, v, List(), func(env *Env) *Promise {
ref, ok := env.lookup(v)
assert.True(t, ok)
s, ok := ref.(*Stream)
assert.True(t, ok)

ps := s.properties()
hasInput := false
hasOutput := false
for _, p := range ps {
if atom, ok := p.(Atom); ok {
if atom == atomInput {
hasInput = true
}
if atom == atomOutput {
hasOutput = true
}
}
}
assert.True(t, hasInput)
assert.True(t, hasOutput)

return Bool(true)
}, nil).Force(context.Background())
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("can be used with both SetInput and SetOutput", func(t *testing.T) {
v := NewVariable()
ok, err := Open(&vm, NewAtom(f.Name()), atomReadWrite, v, List(), func(env *Env) *Promise {
ref, ok := env.lookup(v)
assert.True(t, ok)
s, ok := ref.(*Stream)
assert.True(t, ok)

// Should work as input stream
ok2, err2 := SetInput(&vm, s, func(_ *Env) *Promise {
return Bool(true)
}, nil).Force(context.Background())
assert.NoError(t, err2)
assert.True(t, ok2)

// Should work as output stream
ok3, err3 := SetOutput(&vm, s, func(_ *Env) *Promise {
return Bool(true)
}, nil).Force(context.Background())
assert.NoError(t, err3)
assert.True(t, ok3)

return Bool(true)
}, nil).Force(context.Background())
assert.NoError(t, err)
assert.True(t, ok)
})
})

t.Run("append", func(t *testing.T) {
f, err := os.CreateTemp("", "open_test_append")
assert.NoError(t, err)
Expand Down Expand Up @@ -6398,6 +6498,34 @@ func TestSetStreamPosition(t *testing.T) {
assert.False(t, ok)
})

t.Run("position is not an integer", func(t *testing.T) {
tests := []struct {
name string
position Term
}{
{name: "atom", position: NewAtom("foo")},
{name: "float", position: newFloatFromFloat64Must(3.14)},
{name: "compound", position: NewAtom("foo").Apply(Integer(1), Integer(2))},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open("testdata/empty.txt")
assert.NoError(t, err)
defer func() {
assert.NoError(t, f.Close())
}()

s := &Stream{source: f, mode: ioModeRead, reposition: true}

var vm VM
ok, err := SetStreamPosition(&vm, s, tt.position, Success, nil).Force(context.Background())
assert.Equal(t, typeError(validTypeInteger, tt.position, nil), err)
assert.False(t, ok)
})
}
})

t.Run("streamOrAlias is neither a variable nor a stream term or alias", func(t *testing.T) {
var vm VM
ok, err := SetStreamPosition(&vm, Integer(2), Integer(0), Success, nil).Force(context.Background())
Expand Down Expand Up @@ -6426,6 +6554,21 @@ func TestSetStreamPosition(t *testing.T) {
assert.Equal(t, permissionError(operationReposition, permissionTypeStream, s, env), err)
assert.False(t, ok)
})

t.Run("reposition false with direct stream", func(t *testing.T) {
f, err := os.Open("testdata/empty.txt")
assert.NoError(t, err)
defer func() {
assert.NoError(t, f.Close())
}()

s := &Stream{source: f, mode: ioModeRead, reposition: false}

var vm VM
ok, err := SetStreamPosition(&vm, s, Integer(0), Success, nil).Force(context.Background())
assert.Equal(t, permissionError(operationReposition, permissionTypeStream, s, nil), err)
assert.False(t, ok)
})
}

func TestCharConversion(t *testing.T) {
Expand Down
21 changes: 13 additions & 8 deletions engine/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func (s *Stream) Flush() error {
Sync() error
}

if s.mode != ioModeWrite && s.mode != ioModeAppend {
if s.mode != ioModeWrite && s.mode != ioModeAppend && s.mode != ioModeReadWrite {
return errWrongIOMode
}

Expand Down Expand Up @@ -304,7 +304,7 @@ func (s *Stream) Close() error {
}

func (s *Stream) initRead() error {
if s.mode != ioModeRead {
if s.mode != ioModeRead && s.mode != ioModeReadWrite {
return errWrongIOMode
}

Expand All @@ -325,7 +325,7 @@ func (s *Stream) initRead() error {
}

func (s *Stream) reset() {
if s.mode != ioModeRead {
if s.mode != ioModeRead && s.mode != ioModeReadWrite {
return
}

Expand Down Expand Up @@ -379,6 +379,8 @@ func (s *Stream) properties() []Term {
ps = append(ps, atomInput)
case ioModeWrite, ioModeAppend:
ps = append(ps, atomOutput)
case ioModeReadWrite:
ps = append(ps, atomInput, atomOutput)
}

if s.alias != "" {
Expand All @@ -403,7 +405,7 @@ func (s *Stream) properties() []Term {
}

func (s *Stream) textWriter() (textWriter, error) {
if s.mode != ioModeWrite && s.mode != ioModeAppend {
if s.mode != ioModeWrite && s.mode != ioModeAppend && s.mode != ioModeReadWrite {
return textWriter{}, errWrongIOMode
}

Expand All @@ -415,7 +417,7 @@ func (s *Stream) textWriter() (textWriter, error) {
}

func (s *Stream) binaryWriter() (binaryWriter, error) {
if s.mode != ioModeWrite && s.mode != ioModeAppend {
if s.mode != ioModeWrite && s.mode != ioModeAppend && s.mode != ioModeReadWrite {
return binaryWriter{}, errWrongIOMode
}

Expand Down Expand Up @@ -463,13 +465,16 @@ const (
ioModeWrite = ioMode(os.O_CREATE | os.O_WRONLY)
// ioModeAppend means you can append to the stream.
ioModeAppend = ioMode(os.O_APPEND) | ioModeWrite
// ioModeReadWrite means you can both read from and write to an existing stream.
ioModeReadWrite = ioMode(os.O_RDWR)
)

func (m ioMode) Term() Term {
return [...]Term{
ioModeRead: atomRead,
ioModeWrite: atomWrite,
ioModeAppend: atomAppend,
ioModeRead: atomRead,
ioModeWrite: atomWrite,
ioModeAppend: atomAppend,
ioModeReadWrite: atomReadWrite,
}[m]
}

Expand Down
Loading