Added canonical sexps sans parsing
This commit is contained in:
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)
|
||||
}))
|
||||
}
|
Reference in New Issue
Block a user