Added canonical sexps sans parsing

This commit is contained in:
Brandon Dyck 2024-08-28 23:01:38 -06:00
parent 33f732639f
commit 8261a876d1
9 changed files with 466 additions and 3 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.vscode .vscode
*.fail

32
csexp/example_test.go Normal file
View 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
View 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
View 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
View 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
View File

@ -4,7 +4,8 @@ go 1.22.1
require ( require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c 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 zombiezen.com/go/sqlite v1.3.0
) )
@ -21,5 +22,3 @@ require (
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.29.1 // indirect modernc.org/sqlite v1.29.1 // indirect
) )
replace github.com/shoenig/test v1.8.2 => ../shoenig-test

4
go.sum
View File

@ -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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 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/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 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= 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 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=

View 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
}

View 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)
})
}