perf(encode): write scalars and scalar arrays without boxing
Test / test (push) Successful in 1m34s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 01:46:26 +02:00
parent ebaca18093
commit 80b2bc6e0f
4 changed files with 354 additions and 28 deletions
+6
View File
@@ -188,6 +188,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
**Performance** **Performance**
- Marshal writes plain scalars and typed scalar arrays straight from their
reflect cells instead of boxing them into interface values first, and skips
the per-element resolution for arrays that can never take the `[[header]]`
form. The representative document now costs 130 allocations per call
instead of 141, the long array-of-tables document 55 915 instead of
63 660, with byte-identical output.
- Parsing is faster than in 1.1.0 while carrying the new document layer: - Parsing is faster than in 1.1.0 while carrying the new document layer:
the suite's representative document decodes at about 79 MB/s with 104 the suite's representative document decodes at about 79 MB/s with 104
allocations per call, and the long array-of-tables document at about allocations per call, and the long array-of-tables document at about
+47
View File
@@ -567,3 +567,50 @@ type Item struct {
} }
} }
} }
func TestRunPlainJSONShapes(t *testing.T) {
var stdout, stderr bytes.Buffer
in := strings.NewReader("when = 1979-05-27T07:32:00-07:00\nd = 1979-05-27\nt = 07:32:00\nwall = 1979-05-27T07:32:00\n" +
"items = [1, \"two\"]\n\n[[tables]]\nx = true\n")
code := Run([]string{"-json"}, in, &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{
"\"when\": \"1979-05-27T07:32-07:00\"",
"\"d\": \"1979-05-27\"",
"\"t\": \"07:32\"",
"\"wall\": \"1979-05-27T07:32\"",
"\"items\": [",
"\"x\": true",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q:\n%s", want, out)
}
}
}
func TestInferStructScalarShapes(t *testing.T) {
var stdout, stderr bytes.Buffer
in := strings.NewReader("f = 1.5\nb = true\nd = 1979-05-27\nldt = 1979-05-27T07:32:00\nlt = 07:32:00\nnums = [1, 2, 3]\nmixed = [1, \"a\"]\nempty = []\n")
code := Run([]string{"-struct"}, in, &stdout, &stderr)
if code != 0 {
t.Fatalf("Run returned %d, want 0; stderr = %q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{
"F float64",
"B bool",
"D interpres.LocalDate",
"Ldt interpres.LocalDateTime",
"Lt interpres.LocalTime",
"Nums []int64",
"Mixed []any",
"Empty []any",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q:\n%s", want, out)
}
}
}
+291 -28
View File
@@ -303,8 +303,9 @@ const (
type entry struct { type entry struct {
kind entryKind kind entryKind
key string key string
val any // entryScalar val any // entryScalar, boxed
doc *tomlDoc // entryTable rv reflect.Value // entryScalar, the value the walk kept unboxed
doc *tomlDoc // entryTable
docs []*tomlDoc docs []*tomlDoc
// inline forces a table entry to emit as `key = {…}`; it is set by the // inline forces a table entry to emit as `key = {…}`; it is set by the
@@ -359,6 +360,12 @@ func (d *tomlDoc) addScalar(key string, val any) {
d.entries = append(d.entries, entry{kind: entryScalar, key: key, val: val}) d.entries = append(d.entries, entry{kind: entryScalar, key: key, val: val})
} }
// addScalarReflect adds a scalar whose value stays in its reflect cell until
// emission writes it, the path that keeps plain scalars from being boxed.
func (d *tomlDoc) addScalarReflect(key string, v reflect.Value) {
d.entries = append(d.entries, entry{kind: entryScalar, key: key, rv: v})
}
func (d *tomlDoc) addTable(key string, sub *tomlDoc) *entry { func (d *tomlDoc) addTable(key string, sub *tomlDoc) *entry {
e := entry{kind: entryTable, key: key, doc: sub} e := entry{kind: entryTable, key: key, doc: sub}
d.entries = append(d.entries, e) d.entries = append(d.entries, e)
@@ -712,6 +719,19 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInl
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
return addArrayValue(doc, name, v, path, forceInline) return addArrayValue(doc, name, v, path, forceInline)
default: default:
// A plain scalar stays in its reflect cell until emission, which
// writes it without the boxing Interface() would cost. The kinds the
// boxed rules rewrite (duration, Number, the date-time structs) take
// the boxed path as before.
if t := v.Type(); t != durationType && t != numberType {
switch v.Kind() {
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
doc.addScalarReflect(name, v)
return nil
}
}
val, err := normaliseValue(v) val, err := normaliseValue(v)
if err != nil { if err != nil {
return fmt.Errorf("interpres: %s: %w", path.key(name), err) return fmt.Errorf("interpres: %s: %w", path.key(name), err)
@@ -762,39 +782,92 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, for
// names them. // names them.
apath := path.key(name) apath := path.key(name)
// Every element is resolved through MarshalTOML first, so an element that // The per-element resolution runs only where the array could still take
// renders itself as a scalar, a table or a value array is classified by // the [[header]] form: an interface element can never (its [[form]] would
// what it produces rather than by its Go kind, and its method runs once. // re-parse as []map[string]any), and a plain scalar element can only when
elems := make([]reflect.Value, n) // a Marshaler rewrites it into a table. Everywhere else the array is a
for i := range n { // value array and the elements are written straight from the slice.
if i%ctxCheckInterval == 0 { elem := v.Type().Elem()
if err := doc.checkCtx(); err != nil { maybeTables := true
return err if elem.Kind() == reflect.Interface {
maybeTables = false
} else {
switch elem.Kind() {
case reflect.Struct, reflect.Map, reflect.Pointer:
default:
f := encTypeFlags(elem)
if f&encFlagMarshaler == 0 && f&encFlagAddrMarshaler == 0 {
maybeTables = false
} }
} }
ev, err := resolveElement(v.Index(i), apath.elem(i)) }
if err != nil {
return err // A typed array of plain scalars takes the direct emission path: no
// per-element slice, no boxing, and no error the boxed rules could raise.
// Everything wider, []any first among it, keeps the boxed path, because
// its element errors carry the element's path.
directValueArray := false
switch elem.Kind() {
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
f := encTypeFlags(elem)
if elem != durationType && elem != numberType &&
f&(encFlagMarshaler|encFlagAddrMarshaler|encFlagTextMarshaler|encFlagAddrTextMarshaler) == 0 {
directValueArray = true
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
f := encTypeFlags(elem)
if f&(encFlagMarshaler|encFlagAddrMarshaler|encFlagTextMarshaler|encFlagAddrTextMarshaler) == 0 {
directValueArray = true
// The one error the boxed path raises for these is the int64
// overflow, checked here so the direct path keeps the contract.
for i := range n {
if u := v.Index(i).Uint(); u > math.MaxInt64 {
return &EncodeError{Path: apath.elem(i).segments(), Err: fmt.Errorf("unsigned value %d overflows int64", u)}
}
}
}
}
var elems []reflect.Value
if maybeTables {
// Every element is resolved through MarshalTOML first, so an element
// that renders itself as a scalar, a table or a value array is
// classified by what it produces rather than by its Go kind, and its
// method runs once.
elems = make([]reflect.Value, n)
for i := range n {
if i%ctxCheckInterval == 0 {
if err := doc.checkCtx(); err != nil {
return err
}
}
ev, err := resolveElement(v.Index(i), apath.elem(i))
if err != nil {
return err
}
elems[i] = ev
} }
elems[i] = ev
} }
// An array keeps the [[header]] form only when every element is a table. // An array keeps the [[header]] form only when every element is a table.
// TOML lets one array mix tables with scalars, and that mix renders as a // TOML lets one array mix tables with scalars, and that mix renders as a
// value array with the table elements written inline. // value array with the table elements written inline.
allTables := true allTables := maybeTables
for _, ev := range elems { if allTables {
if !ev.IsValid() || !isTableElementValue(ev) { for _, ev := range elems {
if !ev.IsValid() || !isTableElementValue(ev) {
allTables = false
break
}
}
// A []any of tables is what Parse produces for a value array of inline
// tables; the [[header]] form would re-parse as []map[string]any and so
// change the value's Go type across a round-trip. The header form is
// reserved for typed table slices.
if v.Type().Elem().Kind() == reflect.Interface {
allTables = false allTables = false
break
} }
}
// A []any of tables is what Parse produces for a value array of inline
// tables; the [[header]] form would re-parse as []map[string]any and so
// change the value's Go type across a round-trip. The header form is
// reserved for typed table slices.
if v.Type().Elem().Kind() == reflect.Interface {
allTables = false
} }
// A `,inline` tag on an array of tables asks for a form that would change // A `,inline` tag on an array of tables asks for a form that would change
// the value's Go type on re-parse, so the error is the honest answer. // the value's Go type on re-parse, so the error is the honest answer.
@@ -839,15 +912,24 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, for
return nil return nil
} }
if directValueArray {
doc.addScalarReflect(name, v)
return nil
}
// Value array. Table elements normalise to map[string]any and the emitter // Value array. Table elements normalise to map[string]any and the emitter
// writes them as inline tables. // writes them as inline tables.
items := make([]any, n) items := make([]any, n)
for i, ev := range elems { for i := range n {
if i%ctxCheckInterval == 0 { if i%ctxCheckInterval == 0 {
if err := doc.checkCtx(); err != nil { if err := doc.checkCtx(); err != nil {
return err return err
} }
} }
ev := v.Index(i)
if elems != nil {
ev = elems[i]
}
val, err := normaliseValue(ev) val, err := normaliseValue(ev)
if err != nil { if err != nil {
return &EncodeError{Path: apath.elem(i).segments(), Err: err} return &EncodeError{Path: apath.elem(i).segments(), Err: err}
@@ -858,6 +940,149 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, for
return nil return nil
} }
// writeArrayValue writes a value array from its reflect value, its elements
// written one by one, each falling back to the boxed path only where the
// boxed rules rewrite it.
func (e *encoder) writeArrayValue(v reflect.Value, depth int) error {
if atDepthLimit(depth) {
return errDepthLimit()
}
e.buf.WriteByte('[')
for i := range v.Len() {
if i > 0 {
e.buf.WriteString(", ")
}
if err := e.writeArrayElem(v.Index(i), depth+1); err != nil {
return err
}
}
e.buf.WriteByte(']')
return nil
}
// writeArrayElem writes one element of a value array. The kinds the boxed
// rules rewrite are handed to normaliseValue and the boxed writer; the rest
// write directly, including nested arrays and inline tables.
func (e *encoder) writeArrayElem(v reflect.Value, depth int) error {
if v.Kind() == reflect.Interface {
if v.IsNil() {
return fmt.Errorf("interpres: cannot encode nil value")
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Slice, reflect.Array:
return e.writeArrayValue(v, depth)
case reflect.Map:
return e.writeInlineMapFromReflect(v, depth)
}
if _, isMarshaler := marshalerOf(v); isMarshaler {
return e.writeNormalisedElem(v, depth)
}
if _, isText, err := textValue(v); err != nil || isText {
if err != nil {
return err
}
return e.writeNormalisedElem(v, depth)
}
switch v.Kind() {
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
if t := v.Type(); t != durationType && t != numberType {
return e.writeScalarValue(v)
}
}
return e.writeNormalisedElem(v, depth)
}
// writeNormalisedElem normalises one element through the boxed rules and
// writes the result.
func (e *encoder) writeNormalisedElem(v reflect.Value, depth int) error {
val, err := normaliseValueAt(v, depth)
if err != nil {
return err
}
return e.writeValue(val)
}
// writeInlineMapFromReflect renders a map from its reflect value as an inline
// table, the shape the boxed path gives the table elements of a value array:
// sorted keys, single line when it fits, across lines when it does not.
func (e *encoder) writeInlineMapFromReflect(v reflect.Value, depth int) error {
if e.limit >= noInlineBreak {
return e.writeInlineMapFlatReflect(v, depth)
}
flat := e.flat()
err := flat.writeInlineMapFlatReflect(v, depth)
if err != nil {
flat.release()
return err
}
fits := e.column()+flat.buf.Len() <= e.limit
if fits {
e.buf.Write(flat.buf.Bytes())
}
flat.release()
if fits {
return nil
}
return e.writeInlineMapMultilineReflect(v, depth)
}
// writeInlineMapFlatReflect renders the single-line form.
func (e *encoder) writeInlineMapFlatReflect(v reflect.Value, depth int) error {
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("map key must be string, got %s", v.Type().Key())
}
keys := make([]string, 0, v.Len())
for _, k := range v.MapKeys() {
keys = append(keys, k.String())
}
slices.Sort(keys)
e.buf.WriteByte('{')
for i, k := range keys {
if i > 0 {
e.buf.WriteString(", ")
}
if err := e.writeKey(k); err != nil {
return err
}
e.buf.WriteString(" = ")
if err := e.writeArrayElem(v.MapIndex(reflect.ValueOf(k)), depth+1); err != nil {
return err
}
}
e.buf.WriteByte('}')
return nil
}
// writeInlineMapMultilineReflect renders the across-lines form.
func (e *encoder) writeInlineMapMultilineReflect(v reflect.Value, depth int) error {
keys := make([]string, 0, v.Len())
for _, k := range v.MapKeys() {
keys = append(keys, k.String())
}
slices.Sort(keys)
e.buf.WriteString("{\n")
e.inlineDepth++
for _, k := range keys {
e.writeInlineIndent()
if err := e.writeKey(k); err != nil {
return err
}
e.buf.WriteString(" = ")
if err := e.writeArrayElem(v.MapIndex(reflect.ValueOf(k)), depth+1); err != nil {
return err
}
e.buf.WriteString(",\n")
}
e.inlineDepth--
e.writeInlineIndent()
e.buf.WriteByte('}')
return nil
}
// marshalerOf finds the Marshaler a value carries: on the value itself, or on // marshalerOf finds the Marshaler a value carries: on the value itself, or on
// its address, so a pointer-receiver MarshalTOML is found on an addressable // its address, so a pointer-receiver MarshalTOML is found on an addressable
// struct field or slice element, exactly as textMarshalerOf finds MarshalText. // struct field or slice element, exactly as textMarshalerOf finds MarshalText.
@@ -1296,13 +1521,51 @@ func (e *encoder) writeKV(ent *entry) error {
return err return err
} }
e.buf.WriteString(" = ") e.buf.WriteString(" = ")
if err := e.writeValue(ent.val); err != nil { if err := e.writeEntryValue(ent); err != nil {
return err return err
} }
e.buf.WriteByte('\n') e.buf.WriteByte('\n')
return nil return nil
} }
// writeEntryValue writes a scalar entry's value: from the reflect cell the
// walk kept when it can be written unboxed, from the boxed value otherwise.
func (e *encoder) writeEntryValue(ent *entry) error {
if ent.rv.IsValid() {
switch ent.rv.Kind() {
case reflect.Slice, reflect.Array:
return e.writeArrayValue(ent.rv, 0)
}
return e.writeScalarValue(ent.rv)
}
return e.writeValue(ent.val)
}
// writeScalarValue writes a scalar from its reflect value, boxing only the
// kinds the boxed writer handles specially.
func (e *encoder) writeScalarValue(v reflect.Value) error {
switch v.Kind() {
case reflect.String:
return e.writeStringVal(v.String())
case reflect.Bool:
e.buf.WriteString(strconv.FormatBool(v.Bool()))
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
e.buf.WriteString(strconv.FormatInt(v.Int(), 10))
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u := v.Uint()
if u > math.MaxInt64 {
return fmt.Errorf("unsigned value %d overflows int64", u)
}
e.buf.WriteString(strconv.FormatUint(u, 10))
return nil
case reflect.Float32, reflect.Float64:
return e.writeFloat(v.Float())
}
return e.writeValue(v.Interface())
}
func (e *encoder) writeKeyPath(path []string) error { func (e *encoder) writeKeyPath(path []string) error {
for i, p := range path { for i, p := range path {
if i > 0 { if i > 0 {
@@ -1573,7 +1836,7 @@ func (e *encoder) writeInlineDocEntry(ent entry) error {
case entryArray: case entryArray:
return errInlineArrayOfTables return errInlineArrayOfTables
default: default:
return e.writeValue(ent.val) return e.writeEntryValue(&ent)
} }
} }
+10
View File
@@ -885,3 +885,13 @@ name = "b"
} }
}) })
} }
func TestParseCRLFDocument(t *testing.T) {
tree, err := ParseMap([]byte("a = 1\r\nb = 2\r\n[t]\r\nc = \"x\"\r\n"))
if err != nil {
t.Fatal(err)
}
if tree["a"] != int64(1) || tree["b"] != int64(2) {
t.Errorf("tree = %v", tree)
}
}