AST Tweaks (#551)

* Use pointers instead of copying around ast.Node

Node is a 56B struct that is constantly in the hot path. Passing nodes
around by copy had a cost that started to add up. This change replaces
them by pointers. Using unsafe pointer arithmetic and converting
sibling/child indexes to relative offsets, it removes the need to carry
around a pointer to the root of the tree. This saves 8B per Node. This
space will be used to store an extra []byte slice to provide contextual
error handling on all nodes, including the ones whose data is different
than the raw input (for example: strings with escaped characters), while
staying under the size of a cache line.

* Remove conditional

* Add Raw to track range in data for parsed values

* Simplify reference tracking
This commit is contained in:
Thomas Pelletier
2021-06-03 21:48:51 -04:00
committed by GitHub
parent f3bb20ea79
commit 618f0181ac
13 changed files with 239 additions and 165 deletions
+16 -16
View File
@@ -156,12 +156,12 @@ Execution time speedup compared to other Go TOML libraries:
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter</td><td>1.9x</td><td>1.9x</td></tr>
<tr><td>Marshal/ReferenceFile/map</td><td>1.7x</td><td>1.9x</td></tr>
<tr><td>Marshal/ReferenceFile/struct</td><td>2.7x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter</td><td>2.9x</td><td>2.4x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map</td><td>3.1x</td><td>3.0x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct</td><td>5.5x</td><td>5.8x</td></tr>
<tr><td>Marshal/HugoFrontMatter</td><td>2.0x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/map</td><td>1.8x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/struct</td><td>2.7x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter</td><td>3.0x</td><td>2.6x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map</td><td>3.0x</td><td>3.1x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct</td><td>5.9x</td><td>6.6x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
@@ -174,16 +174,16 @@ provided for completeness.</p>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map</td><td>1.8x</td><td>2.4x</td></tr>
<tr><td>Marshal/SimpleDocument/struct</td><td>2.7x</td><td>3.5x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map</td><td>4.3x</td><td>2.4x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct</td><td>5.8x</td><td>3.3x</td></tr>
<tr><td>UnmarshalDataset/example</td><td>3.1x</td><td>2.2x</td></tr>
<tr><td>UnmarshalDataset/code</td><td>1.8x</td><td>2.1x</td></tr>
<tr><td>UnmarshalDataset/twitter</td><td>2.7x</td><td>1.9x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog</td><td>1.8x</td><td>1.2x</td></tr>
<tr><td>UnmarshalDataset/config</td><td>3.4x</td><td>2.8x</td></tr>
<tr><td>[Geo mean]</td><td>2.8x</td><td>2.5x</td></tr>
<tr><td>Marshal/SimpleDocument/map</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/SimpleDocument/struct</td><td>2.6x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct</td><td>6.3x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example</td><td>3.5x</td><td>2.4x</td></tr>
<tr><td>UnmarshalDataset/code</td><td>2.2x</td><td>2.8x</td></tr>
<tr><td>UnmarshalDataset/twitter</td><td>2.8x</td><td>2.1x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog</td><td>2.3x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config</td><td>4.2x</td><td>3.2x</td></tr>
<tr><td>[Geo mean]</td><td>3.0x</td><td>2.7x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// DecodeError represents an error encountered during the parsing or decoding
@@ -105,7 +105,7 @@ func (e *DecodeError) Key() Key {
// highlight can be freely deallocated.
//nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
offset := unsafe.SubsliceOffset(document, de.highlight)
offset := danger.SubsliceOffset(document, de.highlight)
errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
+37 -29
View File
@@ -2,6 +2,9 @@ package ast
import (
"fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
// Iterator starts uninitialized, you need to call Next() first.
@@ -14,7 +17,7 @@ import (
// }
type Iterator struct {
started bool
node Node
node *Node
}
// Next moves the iterator forward and returns true if points to a node, false
@@ -31,11 +34,11 @@ func (c *Iterator) Next() bool {
// IsLast returns true if the current node of the iterator is the last one.
// Subsequent call to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next <= 0
return c.node.next == 0
}
// Node returns a copy of the node pointed at by the iterator.
func (c *Iterator) Node() Node {
func (c *Iterator) Node() *Node {
return c.node
}
@@ -50,14 +53,13 @@ type Root struct {
func (r *Root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = r.nodes[0]
it.node = &r.nodes[0]
}
return it
}
func (r *Root) at(idx int) Node {
// TODO: unsafe to point to the node directly
return r.nodes[idx]
func (r *Root) at(idx Reference) *Node {
return &r.nodes[idx]
}
// Arrays have one child per element in the array.
@@ -69,42 +71,48 @@ func (r *Root) at(idx int) Node {
// children []Node
type Node struct {
Kind Kind
Data []byte // Raw bytes from the input
Raw Range // Raw bytes from the input.
Data []byte // Node value (could be either allocated or referencing the input).
// next idx (in the root array). 0 if last of the collection.
next int
// child idx (in the root array). 0 if no child.
child int
// pointer to the root array
root *Root
// References to other nodes, as offsets in the backing array from this
// node. References can go backward, so those can be negative.
next int // 0 if last element
child int // 0 if no child
}
type Range struct {
Offset uint32
Length uint32
}
// Next returns a copy of the next node, or an invalid Node if there is no
// next node.
func (n Node) Next() Node {
if n.next <= 0 {
return noNode
func (n *Node) Next() *Node {
if n.next == 0 {
return nil
}
return n.root.at(n.next)
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.next))
}
// Child returns a copy of the first child node of this node. Other children
// can be accessed calling Next on the first child.
// Returns an invalid Node if there is none.
func (n Node) Child() Node {
if n.child <= 0 {
return noNode
func (n *Node) Child() *Node {
if n.child == 0 {
return nil
}
return n.root.at(n.child)
ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.child))
}
// Valid returns true if the node's kind is set (not to Invalid).
func (n Node) Valid() bool {
return n.Kind != Invalid
func (n *Node) Valid() bool {
return n != nil
}
var noNode = Node{}
// Key returns the child nodes making the Key on a supported node. Panics
// otherwise.
// They are guaranteed to be all be of the Kind Key. A simple key would return
@@ -127,13 +135,13 @@ func (n *Node) Key() Iterator {
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil.
// Panics if not called on a KeyValue node, or if the Children are malformed.
func (n Node) Value() Node {
assertKind(KeyValue, n)
func (n *Node) Value() *Node {
assertKind(KeyValue, *n)
return n.Child()
}
// Children returns an iterator over a node's children.
func (n Node) Children() Iterator {
func (n *Node) Children() Iterator {
return Iterator{node: n.Child()}
}
+11 -20
View File
@@ -1,12 +1,11 @@
package ast
type Reference struct {
idx int
set bool
}
type Reference int
const InvalidReference Reference = -1
func (r Reference) Valid() bool {
return r.set
return r != InvalidReference
}
type Builder struct {
@@ -18,8 +17,8 @@ func (b *Builder) Tree() *Root {
return &b.tree
}
func (b *Builder) NodeAt(ref Reference) Node {
return b.tree.at(ref.idx)
func (b *Builder) NodeAt(ref Reference) *Node {
return b.tree.at(ref)
}
func (b *Builder) Reset() {
@@ -28,33 +27,25 @@ func (b *Builder) Reset() {
}
func (b *Builder) Push(n Node) Reference {
n.root = &b.tree
b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
return Reference{
idx: b.lastIdx,
set: true,
}
return Reference(b.lastIdx)
}
func (b *Builder) PushAndChain(n Node) Reference {
n.root = &b.tree
newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx
b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
}
b.lastIdx = newIdx
return Reference{
idx: b.lastIdx,
set: true,
}
return Reference(b.lastIdx)
}
func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent.idx].child = child.idx
b.tree.nodes[parent].child = int(child) - int(parent)
}
func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from.idx].next = to.idx
b.tree.nodes[from].next = int(to) - int(from)
}
@@ -1,4 +1,4 @@
package unsafe
package danger
import (
"fmt"
@@ -57,3 +57,9 @@ func BytesRange(start []byte, end []byte) []byte {
return start[:l]
}
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
// TODO: replace with unsafe.Add when Go 1.17 is released
// https://github.com/golang/go/issues/40481
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
}
@@ -1,15 +1,16 @@
package unsafe_test
package danger_test
import (
"testing"
"unsafe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
)
func TestUnsafeSubsliceOffsetValid(t *testing.T) {
func TestSubsliceOffsetValid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
@@ -28,13 +29,13 @@ func TestUnsafeSubsliceOffsetValid(t *testing.T) {
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
offset := unsafe.SubsliceOffset(d, s)
offset := danger.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset)
})
}
}
func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
func TestSubsliceOffsetInvalid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
@@ -72,13 +73,22 @@ func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
require.Panics(t, func() {
unsafe.SubsliceOffset(d, s)
danger.SubsliceOffset(d, s)
})
})
}
}
func TestUnsafeBytesRange(t *testing.T) {
func TestStride(t *testing.T) {
a := []byte{1, 2, 3, 4}
x := &a[1]
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
require.Equal(t, &a[2], n)
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
require.Equal(t, &a[0], n)
}
func TestBytesRange(t *testing.T) {
type fn = func() ([]byte, []byte)
examples := []struct {
desc string
@@ -157,10 +167,10 @@ func TestUnsafeBytesRange(t *testing.T) {
start, end := e.test()
if e.expected == nil {
require.Panics(t, func() {
unsafe.BytesRange(start, end)
danger.BytesRange(start, end)
})
} else {
res := unsafe.BytesRange(start, end)
res := danger.BytesRange(start, end)
require.Equal(t, e.expected, res)
}
})
+4 -4
View File
@@ -11,19 +11,19 @@ type KeyTracker struct {
}
// UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node ast.Node) {
func (t *KeyTracker) UpdateTable(node *ast.Node) {
t.reset()
t.Push(node)
}
// UpdateArrayTable sets the state of the tracker with the AST array table node.
func (t *KeyTracker) UpdateArrayTable(node ast.Node) {
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
t.reset()
t.Push(node)
}
// Push the given key on the stack.
func (t *KeyTracker) Push(node ast.Node) {
func (t *KeyTracker) Push(node *ast.Node) {
it := node.Key()
for it.Next() {
t.k = append(t.k, string(it.Node().Data))
@@ -31,7 +31,7 @@ func (t *KeyTracker) Push(node ast.Node) {
}
// Pop key from stack.
func (t *KeyTracker) Pop(node ast.Node) {
func (t *KeyTracker) Pop(node *ast.Node) {
it := node.Key()
for it.Next() {
t.k = t.k[:len(t.k)-1]
+4 -4
View File
@@ -104,7 +104,7 @@ func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit
// CheckExpression takes a top-level node and checks that it does not contain keys
// that have been seen in previous calls, and validates that types are consistent.
func (s *SeenTracker) CheckExpression(node ast.Node) error {
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
if s.entries == nil {
// s.entries = make([]entry, 0, 8)
// Skip ID = 0 to remove the confusion between nodes whose parent has
@@ -125,7 +125,7 @@ func (s *SeenTracker) CheckExpression(node ast.Node) error {
}
}
func (s *SeenTracker) checkTable(node ast.Node) error {
func (s *SeenTracker) checkTable(node *ast.Node) error {
it := node.Key()
parentIdx := -1
@@ -169,7 +169,7 @@ func (s *SeenTracker) checkTable(node ast.Node) error {
return nil
}
func (s *SeenTracker) checkArrayTable(node ast.Node) error {
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
it := node.Key()
parentIdx := -1
@@ -207,7 +207,7 @@ func (s *SeenTracker) checkArrayTable(node ast.Node) error {
return nil
}
func (s *SeenTracker) checkKeyValue(node ast.Node) error {
func (s *SeenTracker) checkKeyValue(node *ast.Node) error {
it := node.Key()
parentIdx := s.currentIdx
+62 -44
View File
@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/danger"
)
type parser struct {
@@ -16,9 +17,20 @@ type parser struct {
first bool
}
func (p *parser) Range(b []byte) ast.Range {
return ast.Range{
Offset: uint32(danger.SubsliceOffset(p.data, b)),
Length: uint32(len(b)),
}
}
func (p *parser) Raw(raw ast.Range) []byte {
return p.data[raw.Offset : raw.Offset+raw.Length]
}
func (p *parser) Reset(b []byte) {
p.builder.Reset()
p.ref = ast.Reference{}
p.ref = ast.InvalidReference
p.data = b
p.left = b
p.err = nil
@@ -32,7 +44,7 @@ func (p *parser) NextExpression() bool {
}
p.builder.Reset()
p.ref = ast.Reference{}
p.ref = ast.InvalidReference
for {
if len(p.left) == 0 || p.err != nil {
@@ -61,7 +73,7 @@ func (p *parser) NextExpression() bool {
}
}
func (p *parser) Expression() ast.Node {
func (p *parser) Expression() *ast.Node {
return p.builder.NodeAt(p.ref)
}
@@ -86,7 +98,7 @@ func (p *parser) parseExpression(b []byte) (ast.Reference, []byte, error) {
// expression = ws [ comment ]
// expression =/ ws keyval ws [ comment ]
// expression =/ ws table ws [ comment ]
var ref ast.Reference
ref := ast.InvalidReference
b = p.parseWhitespace(b)
@@ -197,7 +209,7 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
key, b, err := p.parseKey(b)
if err != nil {
return ast.Reference{}, nil, err
return ast.InvalidReference, nil, err
}
// keyval-sep = ws %x3D ws ; =
@@ -205,12 +217,12 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
b = p.parseWhitespace(b)
if len(b) == 0 {
return ast.Reference{}, nil, newDecodeError(b, "expected = after a key, but the document ends there")
return ast.InvalidReference, nil, newDecodeError(b, "expected = after a key, but the document ends there")
}
b, err = expect('=', b)
if err != nil {
return ast.Reference{}, nil, err
return ast.InvalidReference, nil, err
}
b = p.parseWhitespace(b)
@@ -229,7 +241,7 @@ func (p *parser) parseKeyval(b []byte) (ast.Reference, []byte, error) {
//nolint:cyclop,funlen
func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
// val = string / boolean / array / inline-table / date-time / float / integer
var ref ast.Reference
ref := ast.InvalidReference
if len(b) == 0 {
return ref, nil, newDecodeError(b, "expected value, not eof")
@@ -240,32 +252,36 @@ func (p *parser) parseVal(b []byte) (ast.Reference, []byte, error) {
switch c {
case '"':
var raw []byte
var v []byte
if scanFollowsMultilineBasicStringDelimiter(b) {
v, b, err = p.parseMultilineBasicString(b)
raw, v, b, err = p.parseMultilineBasicString(b)
} else {
v, b, err = p.parseBasicString(b)
raw, v, b, err = p.parseBasicString(b)
}
if err == nil {
ref = p.builder.Push(ast.Node{
Kind: ast.String,
Raw: p.Range(raw),
Data: v,
})
}
return ref, b, err
case '\'':
var raw []byte
var v []byte
if scanFollowsMultilineLiteralStringDelimiter(b) {
v, b, err = p.parseMultilineLiteralString(b)
raw, v, b, err = p.parseMultilineLiteralString(b)
} else {
v, b, err = p.parseLiteralString(b)
raw, v, b, err = p.parseLiteralString(b)
}
if err == nil {
ref = p.builder.Push(ast.Node{
Kind: ast.String,
Raw: p.Range(raw),
Data: v,
})
}
@@ -310,13 +326,13 @@ func atmost(b []byte, n int) []byte {
return b[:n]
}
func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, error) {
func (p *parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
v, rest, err := scanLiteralString(b)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
return v[1 : len(v)-1], rest, nil
return v, v[1 : len(v)-1], rest, nil
}
func (p *parser) parseInlineTable(b []byte) (ast.Reference, []byte, error) {
@@ -476,10 +492,10 @@ func (p *parser) parseOptionalWhitespaceCommentNewline(b []byte) ([]byte, error)
return b, nil
}
func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, error) {
func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, []byte, error) {
token, rest, err := scanMultilineLiteralString(b)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
i := 3
@@ -491,11 +507,11 @@ func (p *parser) parseMultilineLiteralString(b []byte) ([]byte, []byte, error) {
i += 2
}
return token[i : len(token)-3], rest, err
return token, token[i : len(token)-3], rest, err
}
//nolint:funlen,gocognit,cyclop
func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, error) {
// ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
// ml-basic-string-delim
// ml-basic-string-delim = 3quotation-mark
@@ -508,7 +524,7 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
// mlb-escaped-nl = escape ws newline *( wschar / newline )
token, rest, err := scanMultilineBasicString(b)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
i := 3
@@ -529,7 +545,7 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
}
}
if i == endIdx {
return token[startIdx:endIdx], rest, nil
return token, token[startIdx:endIdx], rest, nil
}
var builder bytes.Buffer
@@ -579,7 +595,7 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
case 'u':
x, err := hexToString(atmost(token[i+1:], 4), 4)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
builder.WriteString(x)
@@ -587,20 +603,20 @@ func (p *parser) parseMultilineBasicString(b []byte) ([]byte, []byte, error) {
case 'U':
x, err := hexToString(atmost(token[i+1:], 8), 8)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
builder.WriteString(x)
i += 8
default:
return nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
}
} else {
builder.WriteByte(c)
}
}
return builder.Bytes(), rest, nil
return token, builder.Bytes(), rest, nil
}
func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
@@ -612,13 +628,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
// dotted-key = simple-key 1*( dot-sep simple-key )
//
// dot-sep = ws %x2E ws ; . Period
key, b, err := p.parseSimpleKey(b)
raw, key, b, err := p.parseSimpleKey(b)
if err != nil {
return ast.Reference{}, nil, err
return ast.InvalidReference, nil, err
}
ref := p.builder.Push(ast.Node{
Kind: ast.Key,
Raw: p.Range(raw),
Data: key,
})
@@ -627,13 +644,14 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
if len(b) > 0 && b[0] == '.' {
b = p.parseWhitespace(b[1:])
key, b, err = p.parseSimpleKey(b)
raw, key, b, err = p.parseSimpleKey(b)
if err != nil {
return ref, nil, err
}
p.builder.PushAndChain(ast.Node{
Kind: ast.Key,
Raw: p.Range(raw),
Data: key,
})
} else {
@@ -644,12 +662,12 @@ func (p *parser) parseKey(b []byte) (ast.Reference, []byte, error) {
return ref, b, nil
}
func (p *parser) parseSimpleKey(b []byte) (key, rest []byte, err error) {
func (p *parser) parseSimpleKey(b []byte) (raw, key, rest []byte, err error) {
// simple-key = quoted-key / unquoted-key
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
// quoted-key = basic-string / literal-string
if len(b) == 0 {
return nil, nil, newDecodeError(b, "key is incomplete")
return nil, nil, nil, newDecodeError(b, "key is incomplete")
}
switch {
@@ -659,14 +677,14 @@ func (p *parser) parseSimpleKey(b []byte) (key, rest []byte, err error) {
return p.parseBasicString(b)
case isUnquotedKeyChar(b[0]):
key, rest = scanUnquotedKey(b)
return key, rest, nil
return key, key, rest, nil
default:
return nil, nil, newDecodeError(b[0:1], "invalid character at start of key: %c", b[0])
return nil, nil, nil, newDecodeError(b[0:1], "invalid character at start of key: %c", b[0])
}
}
//nolint:funlen,cyclop
func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
func (p *parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
// basic-string = quotation-mark *basic-char quotation-mark
// quotation-mark = %x22 ; "
// basic-char = basic-unescaped / escaped
@@ -683,7 +701,7 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
// escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
token, rest, err := scanBasicString(b)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// fast path
@@ -696,7 +714,7 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
}
}
if i == endIdx {
return token[startIdx:endIdx], rest, nil
return token, token[startIdx:endIdx], rest, nil
}
var builder bytes.Buffer
@@ -726,7 +744,7 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
case 'u':
x, err := hexToString(token[i+1:len(token)-1], 4)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
builder.WriteString(x)
@@ -734,20 +752,20 @@ func (p *parser) parseBasicString(b []byte) ([]byte, []byte, error) {
case 'U':
x, err := hexToString(token[i+1:len(token)-1], 8)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
builder.WriteString(x)
i += 8
default:
return nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
return nil, nil, nil, newDecodeError(token[i:i+1], "invalid escaped character %#U", c)
}
} else {
builder.WriteByte(c)
}
}
return builder.Bytes(), rest, nil
return token, builder.Bytes(), rest, nil
}
func hexToString(b []byte, length int) (string, error) {
@@ -780,7 +798,7 @@ func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, err
switch b[0] {
case 'i':
if !scanFollowsInf(b) {
return ast.Reference{}, nil, newDecodeError(atmost(b, 3), "expected 'inf'")
return ast.InvalidReference, nil, newDecodeError(atmost(b, 3), "expected 'inf'")
}
return p.builder.Push(ast.Node{
@@ -789,7 +807,7 @@ func (p *parser) parseIntOrFloatOrDateTime(b []byte) (ast.Reference, []byte, err
}), b[3:], nil
case 'n':
if !scanFollowsNan(b) {
return ast.Reference{}, nil, newDecodeError(atmost(b, 3), "expected 'nan'")
return ast.InvalidReference, nil, newDecodeError(atmost(b, 3), "expected 'nan'")
}
return p.builder.Push(ast.Node{
@@ -945,7 +963,7 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
}), b[i+3:], nil
}
return ast.Reference{}, nil, newDecodeError(b[i:i+1], "unexpected character 'i' while scanning for a number")
return ast.InvalidReference, nil, newDecodeError(b[i:i+1], "unexpected character 'i' while scanning for a number")
}
if c == 'n' {
@@ -956,14 +974,14 @@ func (p *parser) scanIntOrFloat(b []byte) (ast.Reference, []byte, error) {
}), b[i+3:], nil
}
return ast.Reference{}, nil, newDecodeError(b[i:i+1], "unexpected character 'n' while scanning for a number")
return ast.InvalidReference, nil, newDecodeError(b[i:i+1], "unexpected character 'n' while scanning for a number")
}
break
}
if i == 0 {
return ast.Reference{}, b, newDecodeError(b, "incomplete number")
return ast.InvalidReference, b, newDecodeError(b, "incomplete number")
}
kind := ast.Integer
+1 -5
View File
@@ -9,7 +9,6 @@ import (
//nolint:funlen
func TestParser_AST_Numbers(t *testing.T) {
examples := []struct {
desc string
input string
@@ -136,7 +135,6 @@ func TestParser_AST_Numbers(t *testing.T) {
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := parser{}
p.Reset([]byte(`A = ` + e.input))
p.NextExpression()
@@ -167,7 +165,7 @@ type (
}
)
func compareNode(t *testing.T, e astNode, n ast.Node) {
func compareNode(t *testing.T, e astNode, n *ast.Node) {
t.Helper()
require.Equal(t, e.Kind, n.Kind)
require.Equal(t, e.Data, n.Data)
@@ -199,7 +197,6 @@ func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
//nolint:funlen
func TestParser_AST(t *testing.T) {
examples := []struct {
desc string
input string
@@ -338,7 +335,6 @@ func TestParser_AST(t *testing.T) {
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := parser{}
p.Reset([]byte(e.input))
p.NextExpression()
+9 -9
View File
@@ -2,8 +2,8 @@ package toml
import (
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
type strict struct {
@@ -15,7 +15,7 @@ type strict struct {
missing []decodeError
}
func (s *strict) EnterTable(node ast.Node) {
func (s *strict) EnterTable(node *ast.Node) {
if !s.Enabled {
return
}
@@ -23,7 +23,7 @@ func (s *strict) EnterTable(node ast.Node) {
s.key.UpdateTable(node)
}
func (s *strict) EnterArrayTable(node ast.Node) {
func (s *strict) EnterArrayTable(node *ast.Node) {
if !s.Enabled {
return
}
@@ -31,7 +31,7 @@ func (s *strict) EnterArrayTable(node ast.Node) {
s.key.UpdateArrayTable(node)
}
func (s *strict) EnterKeyValue(node ast.Node) {
func (s *strict) EnterKeyValue(node *ast.Node) {
if !s.Enabled {
return
}
@@ -39,7 +39,7 @@ func (s *strict) EnterKeyValue(node ast.Node) {
s.key.Push(node)
}
func (s *strict) ExitKeyValue(node ast.Node) {
func (s *strict) ExitKeyValue(node *ast.Node) {
if !s.Enabled {
return
}
@@ -47,7 +47,7 @@ func (s *strict) ExitKeyValue(node ast.Node) {
s.key.Pop(node)
}
func (s *strict) MissingTable(node ast.Node) {
func (s *strict) MissingTable(node *ast.Node) {
if !s.Enabled {
return
}
@@ -59,7 +59,7 @@ func (s *strict) MissingTable(node ast.Node) {
})
}
func (s *strict) MissingField(node ast.Node) {
func (s *strict) MissingField(node *ast.Node) {
if !s.Enabled {
return
}
@@ -88,7 +88,7 @@ func (s *strict) Error(doc []byte) error {
return err
}
func keyLocation(node ast.Node) []byte {
func keyLocation(node *ast.Node) []byte {
k := node.Key()
hasOne := k.Next()
@@ -103,5 +103,5 @@ func keyLocation(node ast.Node) []byte {
end = k.Node().Data
}
return unsafe.BytesRange(start, end)
return danger.BytesRange(start, end)
}
+19 -22
View File
@@ -106,7 +106,6 @@ func (d *Decoder) Decode(v interface{}) error {
type decoder struct {
// Which parser instance in use for this decoding session.
// TODO: Think about removing later.
p *parser
// Flag indicating that the current expression is stashed.
@@ -132,7 +131,7 @@ type decoder struct {
strict strict
}
func (d *decoder) expr() ast.Node {
func (d *decoder) expr() *ast.Node {
return d.p.Expression()
}
@@ -208,7 +207,7 @@ Rules for the unmarshal code:
- An "object" is either a struct or a map.
*/
func (d *decoder) handleRootExpression(expr ast.Node, v reflect.Value) error {
func (d *decoder) handleRootExpression(expr *ast.Node, v reflect.Value) error {
var x reflect.Value
var err error
@@ -533,7 +532,7 @@ func (d *decoder) handleTablePart(key ast.Iterator, v reflect.Value) (reflect.Va
return d.handleKeyPart(key, v, d.handleTable, makeMapStringInterface)
}
func tryTextUnmarshaler(node ast.Node, v reflect.Value) (bool, error) {
func (d *decoder) tryTextUnmarshaler(node *ast.Node, v reflect.Value) (bool, error) {
if v.Kind() != reflect.Struct {
return false, nil
}
@@ -547,9 +546,7 @@ func tryTextUnmarshaler(node ast.Node, v reflect.Value) (bool, error) {
if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) {
err := v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data)
if err != nil {
return false, fmt.Errorf("toml: error calling UnmarshalText: %w", err)
// TODO: same as above
// return false, newDecodeError(node.Data, "error calling UnmarshalText: %w", err)
return false, newDecodeError(d.p.Raw(node.Raw), "error calling UnmarshalText: %w", err)
}
return true, nil
@@ -558,12 +555,12 @@ func tryTextUnmarshaler(node ast.Node, v reflect.Value) (bool, error) {
return false, nil
}
func (d *decoder) handleValue(value ast.Node, v reflect.Value) error {
func (d *decoder) handleValue(value *ast.Node, v reflect.Value) error {
for v.Kind() == reflect.Ptr {
v = initAndDereferencePointer(v)
}
ok, err := tryTextUnmarshaler(value, v)
ok, err := d.tryTextUnmarshaler(value, v)
if ok || err != nil {
return err
}
@@ -592,7 +589,7 @@ func (d *decoder) handleValue(value ast.Node, v reflect.Value) error {
}
}
func (d *decoder) unmarshalArray(array ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalArray(array *ast.Node, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
if v.IsNil() {
@@ -663,7 +660,7 @@ func (d *decoder) unmarshalArray(array ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalInlineTable(itable ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalInlineTable(itable *ast.Node, v reflect.Value) error {
// Make sure v is an initialized object.
switch v.Kind() {
case reflect.Map:
@@ -699,7 +696,7 @@ func (d *decoder) unmarshalInlineTable(itable ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalDateTime(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalDateTime(value *ast.Node, v reflect.Value) error {
dt, err := parseDateTime(value.Data)
if err != nil {
return err
@@ -709,7 +706,7 @@ func (d *decoder) unmarshalDateTime(value ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalLocalDate(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalLocalDate(value *ast.Node, v reflect.Value) error {
ld, err := parseLocalDate(value.Data)
if err != nil {
return err
@@ -727,7 +724,7 @@ func (d *decoder) unmarshalLocalDate(value ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalLocalDateTime(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalLocalDateTime(value *ast.Node, v reflect.Value) error {
ldt, rest, err := parseLocalDateTime(value.Data)
if err != nil {
return err
@@ -749,7 +746,7 @@ func (d *decoder) unmarshalLocalDateTime(value ast.Node, v reflect.Value) error
return nil
}
func (d *decoder) unmarshalBool(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalBool(value *ast.Node, v reflect.Value) error {
b := value.Data[0] == 't'
switch v.Kind() {
@@ -764,7 +761,7 @@ func (d *decoder) unmarshalBool(value ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalFloat(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalFloat(value *ast.Node, v reflect.Value) error {
f, err := parseFloat(value.Data)
if err != nil {
return err
@@ -787,7 +784,7 @@ func (d *decoder) unmarshalFloat(value ast.Node, v reflect.Value) error {
return nil
}
func (d *decoder) unmarshalInteger(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalInteger(value *ast.Node, v reflect.Value) error {
const (
maxInt = int64(^uint(0) >> 1)
minInt = -maxInt - 1
@@ -865,7 +862,7 @@ func (d *decoder) unmarshalInteger(value ast.Node, v reflect.Value) error {
return err
}
func (d *decoder) unmarshalString(value ast.Node, v reflect.Value) error {
func (d *decoder) unmarshalString(value *ast.Node, v reflect.Value) error {
var err error
switch v.Kind() {
@@ -874,13 +871,13 @@ func (d *decoder) unmarshalString(value ast.Node, v reflect.Value) error {
case reflect.Interface:
v.Set(reflect.ValueOf(string(value.Data)))
default:
err = fmt.Errorf("toml: cannot store TOML string into a Go %s", v.Kind())
err = newDecodeError(d.p.Raw(value.Raw), "cannot store TOML string into a Go %s", v.Kind())
}
return err
}
func (d *decoder) handleKeyValue(expr ast.Node, v reflect.Value) (reflect.Value, error) {
func (d *decoder) handleKeyValue(expr *ast.Node, v reflect.Value) (reflect.Value, error) {
d.strict.EnterKeyValue(expr)
v, err := d.handleKeyValueInner(expr.Key(), expr.Value(), v)
@@ -894,7 +891,7 @@ func (d *decoder) handleKeyValue(expr ast.Node, v reflect.Value) (reflect.Value,
return v, err
}
func (d *decoder) handleKeyValueInner(key ast.Iterator, value ast.Node, v reflect.Value) (reflect.Value, error) {
func (d *decoder) handleKeyValueInner(key ast.Iterator, value *ast.Node, v reflect.Value) (reflect.Value, error) {
if key.Next() {
// Still scoping the key
return d.handleKeyValuePart(key, value, v)
@@ -904,7 +901,7 @@ func (d *decoder) handleKeyValueInner(key ast.Iterator, value ast.Node, v reflec
return reflect.Value{}, d.handleValue(value, v)
}
func (d *decoder) handleKeyValuePart(key ast.Iterator, value ast.Node, v reflect.Value) (reflect.Value, error) {
func (d *decoder) handleKeyValuePart(key ast.Iterator, value *ast.Node, v reflect.Value) (reflect.Value, error) {
// contains the replacement for v
var rv reflect.Value
+48
View File
@@ -287,6 +287,54 @@ func TestUnmarshal(t *testing.T) {
}
},
},
{
desc: "local datetime into time.Time",
input: `a = 1979-05-27T00:32:00`,
gen: func() test {
type doc struct {
A time.Time
}
return test{
target: &doc{},
expected: &doc{
A: time.Date(1979, 5, 27, 0, 32, 0, 0, time.Local),
},
}
},
},
{
desc: "local datetime into interface",
input: `a = 1979-05-27T00:32:00`,
gen: func() test {
type doc struct {
A interface{}
}
return test{
target: &doc{},
expected: &doc{
A: toml.LocalDateTimeOf(time.Date(1979, 5, 27, 0, 32, 0, 0, time.Local)),
},
}
},
},
{
desc: "local date into interface",
input: `a = 1979-05-27`,
gen: func() test {
type doc struct {
A interface{}
}
return test{
target: &doc{},
expected: &doc{
A: toml.LocalDateOf(time.Date(1979, 5, 27, 0, 32, 0, 0, time.Local)),
},
}
},
},
{
desc: "issue 475 - space between dots in key",
input: `fruit. color = "yellow"