Skip to content

Commit 94054b9

Browse files
hearticalquickwritereader
authored andcommitted
feat: implicit array encoding (ADR 002)
ADR 002 - Length-Agnostic Implicit Array Encoding: - Arrays detected when payload > 8 bytes - First byte indicates element size (1,2,4,8) - Element count calculated as (payloadSize - 1) / elementSize - Support int8/int16/int32/int64 and float32/float64 arrays - Add PutAccess.AddIntegerArray/AddFloatArray - Add PackInt64Array/PackFloat64Array high-level functions Closes ADR-001
1 parent bf9e015 commit 94054b9

3 files changed

Lines changed: 220 additions & 13 deletions

File tree

access/generic_decode.go

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,44 @@ import (
99
)
1010

1111
// DecodePrimitive interprets a primitive payload directly using type tag and width.
12-
// It returns a Go value (int, float, string, []byte as string, bool, nil).
13-
func DecodePrimitive(typ typetags.Type, buf []byte) (interface{}, error) {
12+
// It returns a Go value (int, float, string, []byte, bool, nil, or array).
13+
// For integer and floating-point types, if payload size > 8 bytes, it decodes as an array.
14+
func DecodePrimitive(typ typetags.Type, buf []byte) (any, error) {
1415
size := len(buf)
1516

1617
switch typ {
1718
case typetags.TypeInteger:
18-
switch size {
19-
case 0:
19+
switch {
20+
case size == 0:
2021
return nil, nil
21-
case 1:
22+
case size == 1:
2223
return int8(buf[0]), nil
23-
case 2:
24+
case size == 2:
2425
return int16(binary.LittleEndian.Uint16(buf)), nil
25-
case 4:
26+
case size == 4:
2627
return int32(binary.LittleEndian.Uint32(buf)), nil
27-
case 8:
28+
case size == 8:
2829
return int64(binary.LittleEndian.Uint64(buf)), nil
30+
case size > typetags.MaxScalarSize:
31+
// Array mode: payload > 8 bytes
32+
return decodeIntegerArrayPayload(buf)
2933
default:
3034
return nil, fmt.Errorf("DecodePrimitive: unsupported integer size %d", size)
3135
}
3236

3337
case typetags.TypeFloating:
34-
switch size {
35-
case 0:
38+
switch {
39+
case size == 0:
3640
return nil, nil
37-
case 4:
41+
case size == 4:
3842
bits := binary.LittleEndian.Uint32(buf)
3943
return math.Float32frombits(bits), nil
40-
case 8:
44+
case size == 8:
4145
bits := binary.LittleEndian.Uint64(buf)
4246
return math.Float64frombits(bits), nil
47+
case size > typetags.MaxScalarSize:
48+
// Array mode: payload > 8 bytes
49+
return decodeFloatArrayPayload(buf)
4350
default:
4451
return nil, fmt.Errorf("DecodePrimitive: unsupported float size %d", size)
4552
}
@@ -61,6 +68,32 @@ func DecodePrimitive(typ typetags.Type, buf []byte) (interface{}, error) {
6168
}
6269
}
6370

71+
// decodeIntegerArrayPayload decodes an integer array from the payload
72+
func decodeIntegerArrayPayload(payload []byte) (any, error) {
73+
elementSize, ok := typetags.ArrayElementSize(payload)
74+
if !ok {
75+
return nil, fmt.Errorf("invalid array element size: %d", payload[0])
76+
}
77+
78+
count := typetags.ArrayElementCount(len(payload), elementSize)
79+
data := payload[1:] // Skip the element size indicator
80+
81+
return decodeIntegerArray(data, elementSize, count)
82+
}
83+
84+
// decodeFloatArrayPayload decodes a floating-point array from the payload
85+
func decodeFloatArrayPayload(payload []byte) (any, error) {
86+
elementSize, ok := typetags.ArrayElementSize(payload)
87+
if !ok {
88+
return nil, fmt.Errorf("invalid array element size: %d", payload[0])
89+
}
90+
91+
count := typetags.ArrayElementCount(len(payload), elementSize)
92+
data := payload[1:] // Skip the element size indicator
93+
94+
return decodeFloatArray(data, elementSize, count)
95+
}
96+
6497
// DecodeTupleGeneric: decode a []any from the current position in a SeqGetAccess.
6598
// If root is true, the caller already consumed the tuple header.
6699
// If ordered is true, maps inside the tuple are decoded as *typetags.OrderedMapAny.
@@ -328,3 +361,50 @@ func DecodeOrdered(buf []byte) (any, error) {
328361
}
329362
return vals, nil
330363
}
364+
365+
// decodeIntegerArray decodes an integer array
366+
func decodeIntegerArray(data []byte, elementSize, count int) (any, error) {
367+
result := make([]int64, count)
368+
369+
for i := range count {
370+
offset := i * elementSize
371+
switch elementSize {
372+
case 1:
373+
result[i] = int64(data[offset])
374+
case 2:
375+
result[i] = int64(binary.LittleEndian.Uint16(data[offset:]))
376+
case 4:
377+
result[i] = int64(binary.LittleEndian.Uint32(data[offset:]))
378+
case 8:
379+
result[i] = int64(binary.LittleEndian.Uint64(data[offset:]))
380+
default:
381+
return nil, fmt.Errorf("unsupported element size: %d", elementSize)
382+
}
383+
}
384+
385+
return result, nil
386+
}
387+
388+
// decodeFloatArray decodes a floating-point array
389+
func decodeFloatArray(data []byte, elementSize, count int) (any, error) {
390+
if elementSize != 4 && elementSize != 8 {
391+
return nil, fmt.Errorf("unsupported float element size: %d", elementSize)
392+
}
393+
394+
if elementSize == 4 {
395+
result := make([]float32, count)
396+
for i := 0; i < count; i++ {
397+
bits := binary.LittleEndian.Uint32(data[i*4:])
398+
result[i] = math.Float32frombits(bits)
399+
}
400+
return result, nil
401+
}
402+
403+
// elementSize == 8
404+
result := make([]float64, count)
405+
for i := range count {
406+
bits := binary.LittleEndian.Uint64(data[i*8:])
407+
result[i] = math.Float64frombits(bits)
408+
}
409+
return result, nil
410+
}

access/put.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,3 +734,90 @@ func (p *PutAccess) AddNumericString(val string) error {
734734
}
735735
return fmt.Errorf("AddNumericString: unsupported numeric string %q", val)
736736
}
737+
738+
// AddIntegerArray adds an integer array
739+
func (p *PutAccess) AddIntegerArray(values []int64) {
740+
if len(values) == 0 {
741+
p.AddNull(nil)
742+
return
743+
}
744+
745+
// Determine the minimal element size that can store all values
746+
elementSize := determineIntegerSize(values)
747+
748+
// Create buffer: [elementSize] + [values...]
749+
buf := make([]byte, 1+len(values)*elementSize)
750+
buf[0] = byte(elementSize)
751+
752+
// Encode values
753+
encodeIntegers(buf[1:], values, elementSize)
754+
755+
p.offsets = binary.LittleEndian.AppendUint16(p.offsets,
756+
typetags.EncodeHeader(p.position, typetags.TypeInteger))
757+
p.buf = append(p.buf, buf...)
758+
p.position = len(p.buf)
759+
}
760+
761+
// AddFloatArray adds a floating-point array
762+
func (p *PutAccess) AddFloatArray(values []float64) {
763+
if len(values) == 0 {
764+
p.AddNull(nil)
765+
return
766+
}
767+
768+
// Always use float64 (8 bytes)
769+
elementSize := 8
770+
buf := make([]byte, 1+len(values)*elementSize)
771+
buf[0] = byte(elementSize)
772+
773+
for i, v := range values {
774+
bits := math.Float64bits(v)
775+
binary.LittleEndian.PutUint64(buf[1+i*elementSize:], bits)
776+
}
777+
778+
p.offsets = binary.LittleEndian.AppendUint16(p.offsets,
779+
typetags.EncodeHeader(p.position, typetags.TypeFloating))
780+
p.buf = append(p.buf, buf...)
781+
p.position = len(p.buf)
782+
}
783+
784+
// determineIntegerSize determines the minimal element size that can store all integers
785+
func determineIntegerSize(values []int64) int {
786+
maxVal := int64(0)
787+
minVal := int64(0)
788+
for _, v := range values {
789+
if v > maxVal {
790+
maxVal = v
791+
}
792+
if v < minVal {
793+
minVal = v
794+
}
795+
}
796+
797+
switch {
798+
case minVal >= math.MinInt8 && maxVal <= math.MaxInt8:
799+
return 1
800+
case minVal >= math.MinInt16 && maxVal <= math.MaxInt16:
801+
return 2
802+
case minVal >= math.MinInt32 && maxVal <= math.MaxInt32:
803+
return 4
804+
default:
805+
return 8
806+
}
807+
}
808+
809+
// encodeIntegers encodes integers into the buffer with the given element size
810+
func encodeIntegers(buf []byte, values []int64, elementSize int) {
811+
for i, v := range values {
812+
switch elementSize {
813+
case 1:
814+
buf[i] = byte(v)
815+
case 2:
816+
binary.LittleEndian.PutUint16(buf[i*2:], uint16(v))
817+
case 4:
818+
binary.LittleEndian.PutUint32(buf[i*4:], uint32(v))
819+
case 8:
820+
binary.LittleEndian.PutUint64(buf[i*8:], uint64(v))
821+
}
822+
}
823+
}

typetags/types.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package typetags
33
// Type is a 3-bit tag encoded into a uint16 header
44
type Type uint16
55

6+
// MaxScalarSize is the maximum size of a scalar value (8 bytes)
7+
const MaxScalarSize = 8
8+
69
const (
710
TypeInvalid Type = 0
811
TypeEnd Type = 0
@@ -19,7 +22,44 @@ const (
1922
TypeMap Type = 7
2023
)
2124

22-
// String returns the human-readable name of the type
25+
// Extended container constants
26+
const (
27+
ExtendedContainerValueSize = 4 // 4 bytes continuation
28+
EndOfChain = 0xFFFFFFFF
29+
)
30+
31+
// ExtendedContainerValue represents the 4-byte management block for extended containers
32+
type ExtendedContainerValue struct {
33+
Continuation uint32 // Absolute 32-bit offset to next segment (or EndOfChain)
34+
// SelfOffset uint32 // Absolute 32-bit address for validation
35+
}
36+
37+
// IsArray determines whether the payload is an array
38+
func IsArray(payloadSize int) bool {
39+
return payloadSize > MaxScalarSize
40+
}
41+
42+
// ArrayElementSize returns the element size from the first byte of the payload
43+
func ArrayElementSize(payload []byte) (int, bool) {
44+
if len(payload) == 0 {
45+
return 0, false
46+
}
47+
switch payload[0] {
48+
case 1, 2, 4, 8:
49+
return int(payload[0]), true
50+
default:
51+
return 0, false
52+
}
53+
}
54+
55+
// ArrayElementCount calculates the number of elements in the array
56+
func ArrayElementCount(payloadSize, elementSize int) int {
57+
if elementSize <= 0 {
58+
return 0
59+
}
60+
return (payloadSize - 1) / elementSize
61+
}
62+
2363
func (t Type) String() string {
2464
switch t {
2565
case TypeInteger:

0 commit comments

Comments
 (0)