Added canonical sexps sans parsing
This commit is contained in:
parent
33f732639f
commit
8261a876d1
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
.vscode
|
||||
*.fail
|
32
csexp/example_test.go
Normal file
32
csexp/example_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package csexp_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.codemonkeysoftware.net/b/peachy-go/csexp"
|
||||
)
|
||||
|
||||
func ExampleSexp_WriteTo() {
|
||||
var sexp csexp.Sexp = csexp.List{
|
||||
csexp.Atom("Hello,"),
|
||||
csexp.List{
|
||||
csexp.Atom("Mr."),
|
||||
csexp.Atom("Rivest."),
|
||||
},
|
||||
}
|
||||
sexp.WriteTo(os.Stdout)
|
||||
// Output: (6:Hello,(3:Mr.7:Rivest.))
|
||||
}
|
||||
|
||||
func ExampleSexp_String() {
|
||||
var sexp csexp.Sexp = csexp.List{
|
||||
csexp.Atom("Hello,"),
|
||||
csexp.List{
|
||||
csexp.Atom("Mr."),
|
||||
csexp.Atom("Rivest."),
|
||||
},
|
||||
}
|
||||
fmt.Print(sexp.String())
|
||||
// Output: (6:Hello,(3:Mr.7:Rivest.))
|
||||
}
|
36
csexp/gen/gen.go
Normal file
36
csexp/gen/gen.go
Normal file
@ -0,0 +1,36 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"git.codemonkeysoftware.net/b/peachy-go/csexp"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
type T rapid.TB
|
||||
|
||||
func Atom() *rapid.Generator[csexp.Atom] {
|
||||
return rapid.Custom(func(t *rapid.T) csexp.Atom {
|
||||
return csexp.Atom(rapid.SliceOf(rapid.Byte()).Draw(t, "atom"))
|
||||
})
|
||||
}
|
||||
|
||||
func List() *rapid.Generator[csexp.List] {
|
||||
return rapid.Custom(func(t *rapid.T) csexp.List {
|
||||
var s []csexp.Sexp = rapid.SliceOfN(rapid.Deferred(Sexp), 0, 5).Draw(t, "s")
|
||||
return csexp.List(s)
|
||||
})
|
||||
}
|
||||
|
||||
func AsSexp[T csexp.Sexp](value T) csexp.Sexp {
|
||||
return csexp.Sexp(value)
|
||||
}
|
||||
|
||||
func Sexp() *rapid.Generator[csexp.Sexp] {
|
||||
return rapid.Custom(func(t *rapid.T) csexp.Sexp {
|
||||
choice := rapid.Uint8Range(0, 10).Draw(t, "choice")
|
||||
// If we don't weight it enough towards atoms, it likes to overflow the stack.
|
||||
if choice < 7 {
|
||||
return rapid.Map(Atom(), AsSexp).Draw(t, "atom-sexp")
|
||||
}
|
||||
return rapid.Map(rapid.Deferred(List), AsSexp).Draw(t, "list-sexp")
|
||||
})
|
||||
}
|
87
csexp/sexp.go
Normal file
87
csexp/sexp.go
Normal file
@ -0,0 +1,87 @@
|
||||
package csexp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
|
||||
"git.codemonkeysoftware.net/b/peachy-go/shortcircuit"
|
||||
)
|
||||
|
||||
type Sexp interface {
|
||||
isSexp()
|
||||
WriteTo(w io.Writer) (n int64, err error)
|
||||
String() string
|
||||
Equal(Sexp) bool
|
||||
Clone() Sexp
|
||||
}
|
||||
|
||||
type Atom []byte
|
||||
|
||||
func (a Atom) isSexp() {}
|
||||
|
||||
func (a Atom) WriteTo(w io.Writer) (int64, error) {
|
||||
if scw, ok := w.(*shortcircuit.Writer); ok && scw.Failed() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n, err := fmt.Fprintf(w, "%d:%s", len(a), []byte(a))
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
func (a Atom) String() string {
|
||||
var buf bytes.Buffer
|
||||
a.WriteTo(&buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (a Atom) Equal(s Sexp) bool {
|
||||
a2, ok := s.(Atom)
|
||||
return ok && bytes.Equal([]byte(a), []byte(a2))
|
||||
}
|
||||
|
||||
func (a Atom) Clone() Sexp {
|
||||
return Atom(bytes.Clone([]byte(a)))
|
||||
}
|
||||
|
||||
type List []Sexp
|
||||
|
||||
func (l List) isSexp() {}
|
||||
|
||||
func (l List) WriteTo(w io.Writer) (int64, error) {
|
||||
scw := shortcircuit.EnsureWriter(w)
|
||||
if scw.Failed() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
io.WriteString(scw, "(")
|
||||
for _, child := range l {
|
||||
child.WriteTo(scw)
|
||||
}
|
||||
io.WriteString(scw, ")")
|
||||
return scw.Status()
|
||||
}
|
||||
|
||||
func (l List) String() string {
|
||||
var buf bytes.Buffer
|
||||
l.WriteTo(&buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (l List) Equal(s Sexp) bool {
|
||||
l2, ok := s.(List)
|
||||
return ok && slices.EqualFunc(l, l2, Sexp.Equal)
|
||||
}
|
||||
|
||||
func (l List) Clone() Sexp {
|
||||
l2 := make(List, len(l))
|
||||
for i, child := range l {
|
||||
l2[i] = child.Clone()
|
||||
}
|
||||
return l2
|
||||
}
|
||||
|
||||
func Parse(data []byte) (Sexp, error) {
|
||||
return nil, nil
|
||||
}
|
98
csexp/sexp_test.go
Normal file
98
csexp/sexp_test.go
Normal file
@ -0,0 +1,98 @@
|
||||
package csexp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"git.codemonkeysoftware.net/b/peachy-go/csexp"
|
||||
"git.codemonkeysoftware.net/b/peachy-go/csexp/gen"
|
||||
"github.com/shoenig/test/must"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
func TestWriteToAndStringEquivalent(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
sexp := gen.Sexp().Draw(t, "sexp")
|
||||
stringed := sexp.String()
|
||||
var buf bytes.Buffer
|
||||
n, err := sexp.WriteTo(&buf)
|
||||
must.NoError(t, err)
|
||||
written := buf.String()
|
||||
must.EqOp(t, written, stringed)
|
||||
must.EqOp(t, int64(len(stringed)), n)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringAndParseEqual(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
sexp := gen.Sexp().Draw(t, "sexp")
|
||||
str := sexp.String()
|
||||
parsed, err := csexp.Parse([]byte(str))
|
||||
must.NoError(t, err)
|
||||
must.Equal(t, sexp, parsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCloneEqual(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
sexp := gen.Sexp().Draw(t, "sexp")
|
||||
cloned := sexp.Clone()
|
||||
must.Equal(t, sexp, cloned)
|
||||
})
|
||||
}
|
||||
|
||||
func MustNotBeEqual(t rapid.TB, s1, s2 csexp.Sexp) {
|
||||
e1 := s1.Equal(s2)
|
||||
e2 := s2.Equal(s1)
|
||||
if e1 == !e2 {
|
||||
t.Logf("expected Sexp.Equal to be commutative, but got different results")
|
||||
}
|
||||
if e1 || e2 {
|
||||
t.Fatalf("expected sexps not to be equal\ns1: %q\ns2: %q", string(s1.String()), string(s2.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotEqual(t *testing.T) {
|
||||
t.Run("append to atom", rapid.MakeCheck(func(t *rapid.T) {
|
||||
a1 := gen.Atom().Draw(t, "a1")
|
||||
|
||||
a2 := a1.Clone().(csexp.Atom)
|
||||
a2 = append(a2, 'x')
|
||||
|
||||
MustNotBeEqual(t, a1, a2)
|
||||
}))
|
||||
t.Run("change atom byte", rapid.MakeCheck(func(t *rapid.T) {
|
||||
nonEmpty := func(a csexp.Atom) bool { return len(a) >= 1 }
|
||||
a1 := gen.Atom().Filter(nonEmpty).Draw(t, "a1")
|
||||
|
||||
a2 := a1.Clone().(csexp.Atom)
|
||||
a2[0] = a2[0] + 1
|
||||
|
||||
MustNotBeEqual(t, a1, a2)
|
||||
}))
|
||||
t.Run("append to list", rapid.MakeCheck(func(t *rapid.T) {
|
||||
l1 := gen.List().Draw(t, "l1")
|
||||
|
||||
extraElement := gen.Sexp().Draw(t, "extraElement")
|
||||
l2 := l1.Clone().(csexp.List)
|
||||
l2 = append(l2, extraElement)
|
||||
|
||||
MustNotBeEqual(t, l1, l2)
|
||||
}))
|
||||
t.Run("change list element", rapid.MakeCheck(func(t *rapid.T) {
|
||||
nonEmpty := func(l csexp.List) bool { return len(l) >= 1 }
|
||||
l1 := gen.List().Filter(nonEmpty).Draw(t, "l1")
|
||||
|
||||
isDifferent := func(s csexp.Sexp) bool { return !s.Equal(l1[0]) }
|
||||
newElement := gen.Sexp().Filter(isDifferent).Draw(t, "newElement")
|
||||
l2 := l1.Clone().(csexp.List)
|
||||
l2[0] = newElement
|
||||
|
||||
MustNotBeEqual(t, l1, l2)
|
||||
}))
|
||||
t.Run("list and atom", rapid.MakeCheck(func(t *rapid.T) {
|
||||
atom := gen.Atom().Draw(t, "atom")
|
||||
list := gen.List().Draw(t, "list")
|
||||
MustNotBeEqual(t, atom, list)
|
||||
}))
|
||||
}
|
5
go.mod
5
go.mod
@ -4,7 +4,8 @@ go 1.22.1
|
||||
|
||||
require (
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/shoenig/test v1.8.2
|
||||
github.com/shoenig/test v1.9.0
|
||||
pgregory.net/rapid v1.1.0
|
||||
zombiezen.com/go/sqlite v1.3.0
|
||||
)
|
||||
|
||||
@ -21,5 +22,3 @@ require (
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.29.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/shoenig/test v1.8.2 => ../shoenig-test
|
||||
|
4
go.sum
4
go.sum
@ -12,6 +12,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/shoenig/test v1.9.0 h1:PWtSP8Ty2N0F+Ndh4p0a8SOofFmTEIX/nYh/c3vRCbo=
|
||||
github.com/shoenig/test v1.9.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
@ -26,5 +28,7 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
|
||||
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||
pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
|
||||
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
|
||||
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=
|
||||
|
79
shortcircuit/shortcircuit.go
Normal file
79
shortcircuit/shortcircuit.go
Normal file
@ -0,0 +1,79 @@
|
||||
package shortcircuit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Error indicates that a write was not attempted because
|
||||
// an earlier write failed due to Err.
|
||||
type Error struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (a Error) Error() string {
|
||||
return fmt.Sprintf("writer already failed: %v", a.Err)
|
||||
}
|
||||
|
||||
// Unwrap returns the error from the earlier failed write.
|
||||
func (e Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Is returns true if target is an Error.
|
||||
func (e Error) Is(target error) bool {
|
||||
_, ok := target.(Error)
|
||||
return ok
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
err error
|
||||
n int64
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func EnsureWriter(w io.Writer) *Writer {
|
||||
scw, ok := w.(*Writer)
|
||||
if !ok {
|
||||
scw = NewWriter(w)
|
||||
}
|
||||
return scw
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
// Write attempts to write p to the underlying io.Writer if no earlier call to
|
||||
// Write has failed. It returns the number of bytes written and an error if
|
||||
// the write failed. If an earlier call did fail, no bytes will be written and
|
||||
// err will be an Error containing the error from the failed call.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
if w.Failed() {
|
||||
return 0, Error{Err: w.err}
|
||||
}
|
||||
n, err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Status returns the total bytes written to the underlying writer,
|
||||
// and any error resulting from a call to Write.
|
||||
func (w *Writer) Status() (written int64, err error) {
|
||||
return w.n, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) Failed() bool {
|
||||
return w.err != nil
|
||||
}
|
||||
|
||||
// ClearError makes this Writer forget any earlier error from Write, so that
|
||||
// future calls to Write will attempt to write to the underlying io.Writer.
|
||||
func (s *Writer) ClearError() {
|
||||
s.err = nil
|
||||
}
|
127
shortcircuit/shortcircuit_test.go
Normal file
127
shortcircuit/shortcircuit_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package shortcircuit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.codemonkeysoftware.net/b/peachy-go/shortcircuit"
|
||||
"github.com/shoenig/test"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
var ErrFakeFailure = errors.New("operation successfully failed")
|
||||
|
||||
type FailingWriter struct {
|
||||
fail bool
|
||||
failAfter int
|
||||
writeCalls int
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Fail() {
|
||||
f.FailAfter(0)
|
||||
}
|
||||
|
||||
func (f *FailingWriter) FailAfter(nBytes int) {
|
||||
f.failAfter = nBytes
|
||||
f.fail = true
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Succeed() {
|
||||
f.fail = false
|
||||
}
|
||||
|
||||
func (f *FailingWriter) Write(p []byte) (int, error) {
|
||||
f.writeCalls++
|
||||
if !f.fail {
|
||||
return len(p), nil
|
||||
}
|
||||
if f.failAfter > len(p) {
|
||||
f.failAfter -= len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
n := f.failAfter
|
||||
f.failAfter = 0
|
||||
return n, ErrFakeFailure
|
||||
}
|
||||
|
||||
func (f *FailingWriter) WriteCalls() int {
|
||||
return f.writeCalls
|
||||
}
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
t.Run("writes multiple times and counts total bytes written", func(t *testing.T) {
|
||||
inputs := [][]byte{
|
||||
[]byte("abcdefghi"),
|
||||
[]byte("jklmnopqr"),
|
||||
[]byte("stuvwxyz"),
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
w := shortcircuit.NewWriter(&buf)
|
||||
for _, b := range inputs {
|
||||
n, err := w.Write(slices.Clone(b))
|
||||
must.NoError(t, err)
|
||||
must.False(t, w.Failed())
|
||||
must.EqOp(t, len(b), n, must.Sprint("expected Write to return length of byte slice"))
|
||||
}
|
||||
allInput := slices.Concat(inputs...)
|
||||
test.EqFunc(t, allInput, buf.Bytes(), bytes.Equal, test.Sprintf("expected written bytes to equal original bytes"))
|
||||
n, err := w.Status()
|
||||
test.EqOp(t, int64(len(allInput)), n, test.Sprint("expected total bytes written to equal total length of inputs"))
|
||||
test.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fails without writing if error was already returned, but tries again after clearing error", func(t *testing.T) {
|
||||
w := new(FailingWriter)
|
||||
sc := shortcircuit.NewWriter(w)
|
||||
b := []byte("abcdefghi")
|
||||
expectedTotal := int64(len(b))
|
||||
|
||||
// Succeed
|
||||
n, err := sc.Write(b)
|
||||
must.EqOp(t, len(b), n)
|
||||
must.NoError(t, err)
|
||||
must.EqOp(t, 1, w.WriteCalls())
|
||||
must.False(t, sc.Failed())
|
||||
|
||||
// Fail
|
||||
const limit = 3
|
||||
w.FailAfter(limit)
|
||||
expectedTotal += limit
|
||||
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, limit, n)
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.True(t, sc.Failed())
|
||||
must.EqOp(t, 2, w.WriteCalls())
|
||||
total, err := sc.Status()
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
|
||||
// Fail again
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, 0, n)
|
||||
must.ErrorIs(t, err, shortcircuit.Error{})
|
||||
must.ErrorIs(t, err, ErrFakeFailure)
|
||||
must.True(t, sc.Failed())
|
||||
must.EqOp(t, 2, w.WriteCalls())
|
||||
total, err = sc.Status()
|
||||
must.EqOp(t, ErrFakeFailure, err)
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
|
||||
// Clear and succeed
|
||||
w.Succeed()
|
||||
sc.ClearError()
|
||||
expectedTotal += int64(len(b))
|
||||
|
||||
n, err = sc.Write(b)
|
||||
must.EqOp(t, len(b), n)
|
||||
must.NoError(t, err)
|
||||
must.False(t, sc.Failed())
|
||||
must.EqOp(t, 3, w.WriteCalls())
|
||||
total, err = sc.Status()
|
||||
must.EqOp(t, expectedTotal, total)
|
||||
must.NoError(t, err)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user