From 6988d254ad59f40cce350cf019a6fb0354570e27 Mon Sep 17 00:00:00 2001 From: Wojciech Kaczmarek Date: Tue, 30 Apr 2024 17:20:23 +0200 Subject: [PATCH] WIP encoding values --- Makefile | 5 +- encoding.go | 121 ++++++++++++++++++++++++++++++++++++++++++++- encoding_test.go | 111 +++++++++++++++++++++++++++++++++++++++++ testapi_test.go | 99 +++++++++++++++++++------------------ typecode.go | 13 +++++ typecode_string.go | 27 ++++++++++ 6 files changed, 325 insertions(+), 51 deletions(-) create mode 100644 encoding_test.go create mode 100644 typecode.go create mode 100644 typecode_string.go diff --git a/Makefile b/Makefile index 2e39d4c..57258da 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ default: test -generated = tokentype_string.go opcode_string.go testapi_test.go +generated = tokentype_string.go opcode_string.go typecode_string.go testapi_test.go bcl: stringer $(generated) go.mod go.sum *.go cmd/bcl/*.go go build ./cmd/bcl @@ -11,6 +11,9 @@ tokentype_string.go: token.go opcode_string.go: opcode.go go generate +typecode_string.go: typecode.go + go generate + testapi_test.go: test.py basic_test.go go generate && go fmt testapi_test.go diff --git a/encoding.go b/encoding.go index e844419..670773f 100644 --- a/encoding.go +++ b/encoding.go @@ -1,6 +1,12 @@ package bcl -import "github.com/mohae/uvarint" +import ( + stdbinary "encoding/binary" + "fmt" + "math" + + "github.com/mohae/uvarint" +) func u16ToBytes(p []byte, x uint16) { p[0] = byte(x >> 8 & 0xff) @@ -18,3 +24,116 @@ func uvarintToBytes(p []byte, x uint64) int { func uvarintFromBytes(p []byte) (uint64, int) { return uvarint.Decode(p) } + +func varintToBytes(p []byte, x int64) int { + return uvarintToBytes(p, i64ToU64(x)) +} + +func varintFromBytes(p []byte) (int64, int) { + u, n := uvarintFromBytes(p) + return u64ToI64(u), n +} + +func i64ToU64(x int64) uint64 { + if x < 0 { + return uint64(-x) ^ 0xffffffffffffffff + 1 + } + return uint64(x) +} + +func u64ToI64(x uint64) int64 { + if x > math.MaxInt64 { + return -(int64(x^0xffffffffffffffff) + 1) + } + return int64(x) +} + +func valueToBytes(p []byte, v value) (n int) { + p[0] = byte(typecodeOf(v)) + p = p[1:] // local copy + n++ + + switch x := v.(type) { + case int: + n += varintToBytes(p, int64(x)) + + case float64: + stdbinary.BigEndian.PutUint64(p, math.Float64bits(x)) + n += 8 + + case string: + i := uvarintToBytes(p, uint64(len(x))) + p = p[i:] + if len(p) < len(x) { + n += i + panic("no space ") + } + copy(p, []byte(x)) + n += i + len(x) + + case bool: + if x { + p[0] = 1 + } else { + p[0] = 0 + } + n++ + + default: + // whether it's nil or invalid type, don't emit the value + } + return n +} + +func valueFromBytes(p []byte) (value, int) { + switch c, p := typecode(p[0]), p[1:]; c { + case typeINT: + x, n := varintFromBytes(p) + return int(x), 1 + n + + case typeFLOAT: + return math.Float64frombits(stdbinary.BigEndian.Uint64(p)), 1 + 8 + + case typeSTR: + k, i := uvarintFromBytes(p) + return string(p[i : i+int(k)]), 1 + i + int(k) + + case typeBOOL: + return p[0] != 0, 1 + 1 + + case typeNIL: + return nil, 1 + + default: + panic(errInvalidType{p[0]}) + } +} + +func typecodeOf(v value) typecode { + switch v.(type) { + case int: + return typeINT + case float64: + return typeFLOAT + case string: + return typeSTR + case bool: + return typeBOOL + default: + if v == nil { + return typeNIL + } + panic(errInvalidValue{v}) + } +} + +type errInvalidType struct{ byte } +type errInvalidValue struct{ value } + +func (e errInvalidType) Error() string { + return fmt.Sprintf("invalid type: %q", e.byte) +} + +func (e errInvalidValue) Error() string { + return fmt.Sprintf("invalid value: %v", e.value) +} diff --git a/encoding_test.go b/encoding_test.go new file mode 100644 index 0000000..42a997c --- /dev/null +++ b/encoding_test.go @@ -0,0 +1,111 @@ +package bcl + +import ( + "math" + "testing" + "testing/quick" +) + +var qcConf = &quick.Config{MaxCount: 1000} + +func TestEncodingU16(t *testing.T) { + f := func(x uint16) bool { + var b [2]byte + var p []byte = b[:] + u16ToBytes(p, x) + return x == u16FromBytes(p) + } + if err := quick.Check(f, qcConf); err != nil { + t.Error(err) + } +} + +func TestEncodingUvarint(t *testing.T) { + f := func(x uint64) bool { + var b [9]byte + var p []byte = b[:] + n := uvarintToBytes(p, x) + y, m := uvarintFromBytes(p) + return n == m && x == y + } + if err := quick.Check(f, qcConf); err != nil { + t.Error(err) + } +} + +func TestEncodingVarint(t *testing.T) { + f := func(x int64) bool { + var b [9]byte + var p []byte = b[:] + n := varintToBytes(p, x) + y, m := varintFromBytes(p) + return n == m && x == y + } + if err := quick.Check(f, qcConf); err != nil { + t.Error(err) + } +} + +func testEncodingValue(t *testing.T, x value) { + t.Helper() + + var b [12]byte // for the longest string below + var p []byte = b[:] + + n := valueToBytes(p, x) + y, m := valueFromBytes(p) + + if n != m { + t.Errorf("x=%v: size mismatch n=%d m=%d", x, n, m) + } + if x != y { + t.Errorf("mismatch x=%v y=%v", x, y) + } +} + +func TestEncodingValuesTab(t *testing.T) { + tab := []value{ + nil, + 0, 1, 2, -1, -2, 10, 127, 128, -127, -128, + math.MaxInt64, math.MaxInt64 - 1, math.MinInt64, math.MinInt64 + 1, + 0.0, 0.5, -0.5, 1234.5, -1234.5, + math.MaxFloat64, -math.MaxFloat64, + true, false, + "", "foo", "1234567890", + } + for _, v := range tab { + testEncodingValue(t, v) + } +} + +func qcEncodingValue(x value) bool { + var b [11]byte + var p []byte = b[:] + + n := valueToBytes(p, x) + y, m := valueFromBytes(p) + + return n == m && x == y +} + +func qcEncodingString(s string) bool { + p := make([]byte, 1+9+len(s)) + + n := valueToBytes(p, s) + y, m := valueFromBytes(p) + + return n == m && s == y +} + +func TestEncodingValuesQC(t *testing.T) { + var ( + fInt = func(x int) bool { return qcEncodingValue(x) } + fFloat = func(x float64) bool { return qcEncodingValue(x) } + fString = func(x string) bool { return qcEncodingString(x) } + ) + for _, f := range []any{fInt, fFloat, fString} { + if err := quick.Check(f, qcConf); err != nil { + t.Error(err) + } + } +} diff --git a/testapi_test.go b/testapi_test.go index a79ac63..07bce7b 100644 --- a/testapi_test.go +++ b/testapi_test.go @@ -3,20 +3,20 @@ package bcl_test import ( - "bytes" - "strings" - "testing" + "bytes" + "strings" + "testing" - "github.com/wkhere/bcl" + "github.com/wkhere/bcl" ) func TestInterpretFromPy(t *testing.T) { - var tab = []struct { - name, input, output string - disasm bool - errWanted bool - errMatch string - }{ + var tab = []struct { + name, input, output string + disasm bool + errWanted bool + errMatch string + }{ {`0`, ``, "", false, false, ""}, {`0.1`, `eval "expr that is discarded"`, "", false, false, ""}, {`0.2`, `eval nil`, "", false, false, ""}, @@ -376,53 +376,54 @@ func TestInterpretFromPy(t *testing.T) { {`121.2`, `print 1/0.0`, "+Inf", false, false, ""}, {`122.1`, `print 2147483647-1`, "2147483646", false, false, ""}, {`122.2`, `print -2147483647+1`, "-2147483646", false, false, ""}, - } + {`122.1-64`, `print 9223372036854775807-1`, "9223372036854775806", false, false, ""}, + {`122.2-64`, `print -9223372036854775807+1`, "-9223372036854775806", false, false, ""}, + } - for _, tc := range tab { - tc := tc - t.Run(tc.name, func(t *testing.T) { - out := new(bytes.Buffer) - log := new(bytes.Buffer) + for _, tc := range tab { + tc := tc + t.Run(tc.name, func(t *testing.T) { + out := new(bytes.Buffer) + log := new(bytes.Buffer) - _, err := bcl.Interpret( - []byte(tc.input), - bcl.OptDisasm(tc.disasm), - bcl.OptOutput(out), bcl.OptLogger(log), - ) + _, err := bcl.Interpret( + []byte(tc.input), + bcl.OptDisasm(tc.disasm), + bcl.OptOutput(out), bcl.OptLogger(log), + ) - switch { - case err != nil && !tc.errWanted: - t.Errorf("unexpected error: %s", relevantError(err, log)) + switch { + case err != nil && !tc.errWanted: + t.Errorf("unexpected error: %s", relevantError(err, log)) - case err != nil && tc.errWanted: - rerr := relevantError(err, log) - if !strings.Contains(rerr, tc.errMatch) { - t.Errorf("error mismatch\nhave: %s\nwant matching: %s", - rerr, tc.errMatch, - ) - } + case err != nil && tc.errWanted: + rerr := relevantError(err, log) + if !strings.Contains(rerr, tc.errMatch) { + t.Errorf("error mismatch\nhave: %s\nwant matching: %s", + rerr, tc.errMatch, + ) + } - case err == nil && tc.errWanted && tc.errMatch == "": - t.Errorf("no error when expecting one") + case err == nil && tc.errWanted && tc.errMatch == "": + t.Errorf("no error when expecting one") - case err == nil && tc.errWanted && tc.errMatch != "": - t.Errorf("no error when expecting one matching: %s", tc.errMatch) + case err == nil && tc.errWanted && tc.errMatch != "": + t.Errorf("no error when expecting one matching: %s", tc.errMatch) - case err == nil && !tc.errWanted: - s := strings.TrimRight(out.String(), "\n") - if s != tc.output { - t.Errorf("mismatch:\nhave: %s\nwant: %s", s, tc.output) - } - } - }) - } + case err == nil && !tc.errWanted: + s := strings.TrimRight(out.String(), "\n") + if s != tc.output { + t.Errorf("mismatch:\nhave: %s\nwant: %s", s, tc.output) + } + } + }) + } } func relevantError(err error, buf *bytes.Buffer) string { - if s := err.Error(); strings.HasPrefix(s, "combined errors") { - return strings.TrimRight(buf.String(), "\n") - } else { - return s - } + if s := err.Error(); strings.HasPrefix(s, "combined errors") { + return strings.TrimRight(buf.String(), "\n") + } else { + return s + } } - diff --git a/typecode.go b/typecode.go new file mode 100644 index 0000000..56171c7 --- /dev/null +++ b/typecode.go @@ -0,0 +1,13 @@ +package bcl + +type typecode byte + +const ( + typeNIL typecode = iota + typeINT + typeFLOAT + typeSTR + typeBOOL +) + +//go:generate stringer -type typecode -trimprefix type diff --git a/typecode_string.go b/typecode_string.go new file mode 100644 index 0000000..cf35636 --- /dev/null +++ b/typecode_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type typecode -trimprefix type"; DO NOT EDIT. + +package bcl + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[typeNIL-0] + _ = x[typeINT-1] + _ = x[typeFLOAT-2] + _ = x[typeSTR-3] + _ = x[typeBOOL-4] +} + +const _typecode_name = "NILINTFLOATSTRBOOL" + +var _typecode_index = [...]uint8{0, 3, 6, 11, 14, 18} + +func (i typecode) String() string { + if i >= typecode(len(_typecode_index)-1) { + return "typecode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _typecode_name[_typecode_index[i]:_typecode_index[i+1]] +}