Files
interpres/target.go
T
petrbalvin a7d0041259
Test / test (push) Successful in 1m50s
perf(decode): parse struct destinations without the value tree
Assisted-by: GLM 5.3 Flash
2026-09-22 15:59:03 +02:00

1219 lines
38 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"context"
"errors"
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"sync"
)
// errTargetFallback aborts a targeted parse and hands the document back to
// the ordinary tree path. It is the contract-keeping device of this file:
// every condition the tree path answers with a decode-stage error, an
// Unmarshaler hook, an embedded map filler or any other machinery the
// targeted skeleton does not model, ends here, and the caller reruns the
// document through the tree path, so the observable behaviour is the tree
// path's, exactly. A targeted parse either completes with the result the
// tree path would give, or it erases itself.
var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path")
// targetCache holds whether a destination type may take the targeted parse.
// One computed answer per type, the same trade-off structSchemaCache makes.
var targetCache sync.Map // reflect.Type -> bool
// mapStringAnyType is the map shape the tree builds for an any destination's
// tables, reused by the any-map element branch.
var mapStringAnyType = reflect.TypeFor[map[string]any]()
// typeTargetable reports whether decoding into the struct type t can use the
// targeted parse. The one structural ban is untagged embedded maps: their
// filler-key rule lives in the tree decode, and a targeted document that
// meets an unknown table would need a subtree of it. Everything else is safe
// to attempt, because the value layer is the ordinary decode and every
// mismatch falls back.
func typeTargetable(t reflect.Type) bool {
if t == nil || t.Kind() != reflect.Struct || t == orderedMapType {
return false
}
if v, ok := targetCache.Load(t); ok {
return v.(bool)
}
r := scanTargetable(t, make(map[reflect.Type]bool))
v, _ := targetCache.LoadOrStore(t, r)
return v.(bool)
}
func scanTargetable(t reflect.Type, seen map[reflect.Type]bool) bool {
if seen[t] {
return true
}
seen[t] = true
if len(cachedStructSchema(t).embedMaps) != 0 {
return false
}
for _, loc := range cachedStructSchema(t).byName {
ft := derefType(t.FieldByIndex(loc.index).Type)
if ft == orderedMapType {
return false
}
if ft.Kind() != reflect.Struct || isScalarStruct(ft) {
continue
}
// A struct field with a custom decode hook receives the whole parsed
// value from the tree decode; the targeted skeleton never builds that
// value for a table it enters directly, so the hook must win.
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
return false
}
if !scanTargetable(ft, seen) {
return false
}
}
return true
}
func derefType(t reflect.Type) reflect.Type {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t
}
// implementsDecodeHook reports whether t carries one of the custom decode
// interfaces the tree decode honours.
func implementsDecodeHook(t reflect.Type) bool {
return t.Implements(unmarshalerType) || t.Implements(ctxUnmarshalerType) || t.Implements(textUnmarshalerType)
}
// canTargetDecode reports whether the decoder can take the targeted path for
// the destination v: a non-nil pointer to a struct whose graph carries no
// embedded map filler, and that is not itself a custom decode hook (the tree
// decode hands a hook the whole parsed tree).
func canTargetDecode(v any) bool {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return false
}
et := rv.Type().Elem()
if et.Kind() != reflect.Struct || !typeTargetable(et) {
return false
}
return !implementsDecodeHook(et) && !implementsDecodeHook(reflect.PointerTo(et))
}
// targetTable is one open table of the targeted parse: the struct (or map)
// value its keys fill, the schema that resolves them (nil for a map or sink
// destination), the absolute path its errors wrap, and whether it collects
// the keys no field claims for the strict check. A sink is the destination
// an unknown subtree gets: its statements parse for the syntax and definition
// contracts, and its values are discarded.
type targetTable struct {
rv reflect.Value
schema *structSchema
path []string
sink bool
keys []string // the keys defined in the table, interned; struct
// tables keep theirs per destination address instead
strict bool
unknown string // strict: the smallest unclaimed key so far
resolvedSeen map[string]bool // the schema keys resolved so far, for required
}
// targetParser parses a document straight into a struct destination. It
// reuses the parser's scanner, grammar errors and definition maps, and the
// decoder's value assignment; its own work is the table skeleton a struct
// destination needs: which field does this header or key land in.
type targetParser struct {
*parser
d *decoder
root reflect.Value
tables []*targetTable // every opened table, in document order
rootT *targetTable
cur *targetTable
// arrayNext tracks how many elements of a fixed-size array the document
// has filled, per field address.
arrayNext map[uintptr]int
// opened registers every opened table by its path key, sinks included: a
// later header or dotted key meets the table the tree already built.
opened map[string]*targetTable
// keyAssigned records the fields a key statement assigned directly, per
// table address: an array-of-tables header over such a field is the
// tree's `not an array of tables` error, where a header over a
// header-built array appends.
keyAssignedKeys map[uintptr]map[string]bool
// tableKeysByAddr holds the defined keys of one struct destination, keyed
// by the value's address: the table object a dotted descent builds is
// transient, the destination is not.
tableKeysByAddr map[uintptr][]string
}
// markKeyAssigned records that a key statement assigned the field, and
// keyAssigned reports that state. Sinks keep no such bookkeeping.
func (tp *targetParser) markKeyAssigned(t *targetTable, key string) {
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return
}
addr := t.rv.Addr().Pointer()
if tp.keyAssignedKeys == nil {
tp.keyAssignedKeys = make(map[uintptr]map[string]bool, 8)
}
if tp.keyAssignedKeys[addr] == nil {
tp.keyAssignedKeys[addr] = make(map[string]bool, 8)
}
tp.keyAssignedKeys[addr][key] = true
}
func (tp *targetParser) keyAssigned(t *targetTable, key string) bool {
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return false
}
addr := t.rv.Addr().Pointer()
return tp.keyAssignedKeys[addr][key]
}
// tableHas reports whether key is already defined in the table. A struct
// table's keys are interned parser strings, so the linear scan compares
// against a handful of short keys, cheaper than hashing a per-table map.
// Struct tables keep their keys by destination address, because the table
// object a dotted descent builds is transient while the destination is not.
func (tp *targetParser) tableHas(t *targetTable, key string) bool {
switch {
case t.sink:
return slices.Contains(t.keys, key)
case t.schema == nil:
return t.rv.Kind() == reflect.Map && t.rv.MapIndex(reflect.ValueOf(key)).IsValid()
default:
return slices.Contains(tp.tableKeys(t), key)
}
}
// tableKeys returns the persistent keys slice of a struct table.
func (tp *targetParser) tableKeys(t *targetTable) []string {
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return nil
}
addr := t.rv.Addr().Pointer()
if tp.tableKeysByAddr == nil {
tp.tableKeysByAddr = make(map[uintptr][]string, 8)
}
return tp.tableKeysByAddr[addr]
}
// tableMark records the key as defined.
func (tp *targetParser) tableMark(t *targetTable, key string) {
switch {
case t.sink:
t.keys = append(t.keys, key)
case t.schema == nil:
default:
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return
}
addr := t.rv.Addr().Pointer()
if tp.tableKeysByAddr == nil {
tp.tableKeysByAddr = make(map[uintptr][]string, 8)
}
tp.tableKeysByAddr[addr] = append(tp.tableKeysByAddr[addr], key)
}
}
// resolvedHas reports whether the resolved schema key has been seen, the
// check a required tag runs: the duplicate bookkeeping tracks the key as the
// document wrote it, the required bookkeeping the key as the schema
// resolved it.
func (t *targetTable) resolvedHas(key string) bool {
return t.resolvedSeen[key]
}
func (t *targetTable) markResolved(key string) {
if t.schema == nil || len(t.schema.required) == 0 {
return
}
if t.resolvedSeen == nil {
t.resolvedSeen = make(map[string]bool, 8)
}
t.resolvedSeen[key] = true
}
// recordStrictUnknown remembers the key no field claims when strict decoding
// is on: the smallest one is reported, the tree decode's own choice.
func (t *targetTable) recordStrictUnknown(key string) {
if !t.strict {
return
}
if t.unknown == "" || key < t.unknown {
t.unknown = key
}
}
// schemaRef hands out the pointer form the target tables hold. The schema
// is immutable once published, so sharing one copy is safe.
func schemaRef(t reflect.Type) *structSchema {
s := cachedStructSchema(t)
return &s
}
// parseIntoTargeted runs the targeted parse of data into v. It returns
// errTargetFallback when the document or the destination needs the tree
// path, and any parse error the tree path would return.
func parseIntoTargeted(ctx context.Context, data []byte, d *decoder, useNumber bool, maxDepth int, v any) error {
if maxDepth <= 0 {
maxDepth = maxNestingDepth
}
rv := reflect.ValueOf(v)
tp := &targetParser{
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
d: d,
root: rv.Elem(),
arrayNext: make(map[uintptr]int, 4),
keyAssignedKeys: make(map[uintptr]map[string]bool, 8),
opened: make(map[string]*targetTable, 8),
}
tp.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown}
tp.tables = append(tp.tables, tp.rootT)
tp.cur = tp.rootT
if err := tp.run(); err != nil {
return err
}
return tp.reportDeferred()
}
// run walks the statements; the cadence and the end conditions mirror the
// tree parser's loop.
func (tp *targetParser) run() error {
p := tp.parser
for i := 0; ; i++ {
if i%ctxCheckInterval == 0 {
if err := p.checkCtx(); err != nil {
return err
}
}
if err := p.skipBlank(); err != nil {
return err
}
p.pending = nil
if p.eof() {
break
}
c := p.peek()
switch {
case c == '[':
if err := tp.parseHeader(); err != nil {
return err
}
default:
if err := tp.parseKeyStatement(); err != nil {
return err
}
}
if err := p.expectLineEnd(); err != nil {
return err
}
}
return nil
}
// reportDeferred raises the decode-stage findings in the tree decode's
// order: the root table first, then the opened tables in document order. The
// tree decode reports them after a full parse, so a later parse error always
// won; here the parse has already completed.
func (tp *targetParser) reportDeferred() error {
for _, t := range append([]*targetTable{tp.rootT}, tp.tables...) {
if t.sink {
continue
}
if t.strict && t.unknown != "" {
return tp.wrapTableErr(t, fmt.Errorf("interpres: unknown field %q for %s", t.unknown, t.rv.Type()))
}
if t.schema != nil {
for _, key := range t.schema.required {
if !t.resolvedHas(key) {
return tp.wrapTableErr(t, fmt.Errorf("interpres: missing required key %q", key))
}
}
}
}
return nil
}
// wrapTableErr wraps a table's finding the way the tree decode wraps it: the
// root speaks for itself, a nested table gains its path.
func (tp *targetParser) wrapTableErr(t *targetTable, err error) error {
if len(t.path) == 0 {
return err
}
return &DecodeError{Path: Path(slices.Clone(t.path)), Err: err}
}
// --- headers ---------------------------------------------------------------
// parseHeader parses a [table] or [[array of tables]] header and makes it the
// current table. The definition checks and their messages are the tree
// parser's.
func (tp *targetParser) parseHeader() error {
p := tp.parser
array := false
p.pos++ // consume '['
if !p.eof() && p.peek() == '[' {
array = true
p.pos++
}
first, rest, err := p.parseKeyPath()
if err != nil {
return err
}
p.skipInline()
if p.eof() || p.peek() != ']' {
return p.errf("expected ']' to close table header")
}
p.pos++
if array {
if p.eof() || p.peek() != ']' {
return p.errf("expected ']]' to close array-of-tables header")
}
p.pos++
}
key := p.keyBuf[:1]
key[0] = first
if len(rest) > 0 {
key = append([]string{first}, rest...)
}
if array {
return tp.appendArrayTable(key)
}
pk := pathKey(key)
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
return p.errf("table %q is defined more than once", strings.Join(key, "."))
}
p.markHeader(pk)
tbl, err := tp.openTablePath(key)
if err != nil {
return err
}
if !tbl.sink {
tp.tables = append(tp.tables, tbl)
}
tp.cur = tbl
return nil
}
// openTablePath walks the header path from the root and returns the table it
// names. The frozen checks are the tree parser's; a segment no field claims
// opens a sink, and a segment whose destination cannot be a table falls
// back, because the tree decode answers with its own type error.
func (tp *targetParser) openTablePath(key []string) (*targetTable, error) {
parent := tp.rootT
for i, k := range key[:len(key)-1] {
if tp.frozenAt(key[:i+1]) {
return nil, tp.errf("cannot extend inline table %q", strings.Join(key[:i+1], "."))
}
child, err := tp.descendOne(parent, k, key[:i+1])
if err != nil {
return nil, err
}
tp.tableMark(parent, k)
if !child.sink {
tp.tables = append(tp.tables, child)
}
parent = child
}
leaf := key[len(key)-1]
if tp.frozenAt(key) {
return nil, tp.errf("cannot extend inline table %q", strings.Join(key, "."))
}
tbl, err := tp.descendOne(parent, leaf, key)
if err != nil {
return nil, err
}
tp.tableMark(parent, leaf)
return tbl, nil
}
// frozenAt reports whether the path was frozen as an inline table.
func (tp *targetParser) frozenAt(path []string) bool {
return tp.parser.frozen[pathKey(path)]
}
// elementPath extends a table's path with an array-of-tables element's
// key and bracketed index, the path the tree decode wraps an element's
// errors with.
func elementPath(base []string, key string, index int) []string {
out := make([]string, 0, len(base)+2)
out = append(out, base...)
out = append(out, key, "["+strconv.Itoa(index)+"]")
return out
}
// descendOne enters the table one header segment names inside parent.
func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string) (*targetTable, error) {
if parent.sink {
return parent, nil
}
if parent.schema == nil {
// A map destination: the entry must be (or become) a table.
if parent.rv.Kind() != reflect.Map {
return nil, errTargetFallback
}
elemT := parent.rv.Type().Elem()
if elemT.Kind() != reflect.Map || elemT.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() {
return &targetTable{rv: existing.Elem(), path: abs}, nil
}
next := reflect.MakeMap(elemT)
parent.rv.SetMapIndex(reflect.ValueOf(key), next)
return &targetTable{rv: next, path: abs}, nil
}
resolved := key
loc, ok := parent.schema.byName[key]
if !ok {
resolved = strings.ToLower(key)
loc, ok = parent.schema.byName[resolved]
}
if !ok {
parent.recordStrictUnknown(key)
if opened, ok := tp.opened[pathKey(abs)]; ok {
return opened, nil
}
if tp.tableHas(parent, key) {
return nil, tp.errf("key %q is not a table", key)
}
sink := &targetTable{sink: true, path: abs}
tp.opened[pathKey(abs)] = sink
return sink, nil
}
parent.markResolved(resolved)
fv, err := fieldByIndex(parent.rv, loc.index)
if err != nil {
return nil, errTargetFallback
}
return tp.openValueTable(fv, parent, key, abs, parent.strict)
}
// openValueTable opens a table scope over a placed field value, allocating a
// nil pointer on the way. The rules mirror the tree decode's own type
// decisions: a struct enters, a map enters (allocated when nil), an array of
// tables enters its last element, and anything else is a type mismatch the
// tree decode reports, so it falls back. A scalar field the table keys
// already define is the tree's `key is not a table` error, checked against
// parent, the table the key belongs to.
func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, key string, abs []string, strict bool) (*targetTable, error) {
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
if !fv.CanSet() {
return nil, errTargetFallback
}
fv.Set(reflect.New(fv.Type().Elem()))
}
fv = fv.Elem()
}
switch fv.Kind() {
case reflect.Struct:
if isScalarStruct(fv.Type()) {
return nil, errTargetFallback
}
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: abs, strict: strict}, nil
case reflect.Map:
if fv.Type().Key().Kind() != reflect.String {
return nil, errTargetFallback
}
if fv.IsNil() {
fv.Set(reflect.MakeMap(fv.Type()))
}
return &targetTable{rv: fv, path: abs}, nil
case reflect.Slice:
if fv.Len() == 0 {
// No [[header]] ever filled it, so the tree holds a map here and
// its decode raises the type mismatch.
return nil, errTargetFallback
}
et := derefType(fv.Type().Elem())
if et.Kind() != reflect.Struct || isScalarStruct(et) {
return nil, errTargetFallback
}
return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil
}
if tp.tableHas(parent, key) {
return nil, tp.errf("key %q is not a table", key)
}
return nil, errTargetFallback
}
// appendArrayTable appends a new element to the array of tables the leaf
// names and makes it the current table.
func (tp *targetParser) appendArrayTable(key []string) error {
parent := tp.rootT
for i, k := range key[:len(key)-1] {
if tp.frozenAt(key[:i+1]) {
return tp.errf("cannot extend inline table %q", strings.Join(key[:i+1], "."))
}
child, err := tp.descendOne(parent, k, key[:i+1])
if err != nil {
return err
}
tp.tableMark(parent, k)
parent = child
}
leaf := key[len(key)-1]
if tp.frozenAt(key) {
return tp.errf("cannot extend inline table %q", strings.Join(key, "."))
}
pk := pathKey(key)
if tp.parser.dotted[pk] || tp.parser.headers[pk] {
return tp.errf("key %q is not an array of tables", leaf)
}
tp.parser.markArray(pk)
elem, err := tp.appendElement(parent, leaf, key)
if err != nil {
return err
}
tp.tableMark(parent, leaf)
if !elem.sink {
tp.tables = append(tp.tables, elem)
}
tp.cur = elem
return nil
}
// appendElement appends one element to the array the leaf names in parent
// and returns its table. A leaf no field claims sinks; a field whose array
// element kind cannot be a table falls back, the tree decode owning the type
// error.
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
if parent.sink {
return parent, nil
}
if parent.schema == nil {
if parent.rv.Kind() != reflect.Map {
return nil, errTargetFallback
}
elemT := parent.rv.Type().Elem()
gk := reflect.ValueOf(leaf)
var arr reflect.Value
if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() {
if existing.Elem().Kind() != reflect.Slice {
return nil, tp.errf("key %q is not an array of tables", leaf)
}
arr = existing.Elem()
}
var elem reflect.Value
switch {
case elemT.Kind() == reflect.Slice:
et := elemT.Elem()
if et.Kind() != reflect.Map || et.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
if !arr.IsValid() {
arr = reflect.MakeSlice(elemT, 0, 4)
}
elem = reflect.MakeMap(et)
case elemT.Kind() == reflect.Interface:
// An any-map entry is built exactly as the tree builds it: a
// []map[string]any slice of entry maps.
if !arr.IsValid() {
arr = reflect.ValueOf([]map[string]any{})
}
elem = reflect.MakeMap(mapStringAnyType)
default:
return nil, errTargetFallback
}
grown := reflect.Append(arr, elem)
parent.rv.SetMapIndex(gk, grown)
return &targetTable{rv: grown.Index(grown.Len() - 1), path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
}
resolved := leaf
loc, ok := parent.schema.byName[leaf]
if !ok {
resolved = strings.ToLower(leaf)
loc, ok = parent.schema.byName[resolved]
}
if !ok {
parent.recordStrictUnknown(leaf)
if opened, ok := tp.opened[pathKey(parent.path)]; ok {
return opened, nil
}
if tp.tableHas(parent, leaf) {
return nil, tp.errf("key %q is not an array of tables", leaf)
}
sink := &targetTable{sink: true, path: parent.path}
tp.opened[pathKey(parent.path)] = sink
return sink, nil
}
parent.markResolved(resolved)
if tp.keyAssigned(parent, resolved) {
// A key statement already assigned the field its own value; the tree
// holds a value array there and its header append is the
// `not an array of tables` parse error.
return nil, tp.errf("key %q is not an array of tables", leaf)
}
fv, err := fieldByIndex(parent.rv, loc.index)
if err != nil {
return nil, errTargetFallback
}
if fv.Kind() == reflect.Array {
// A fixed-size array fills position by position; one element too many
// is the length mismatch the tree decode reports.
et := derefType(fv.Type().Elem())
switch et.Kind() {
case reflect.Struct:
if isScalarStruct(et) {
return nil, errTargetFallback
}
addr := fv.Addr().Pointer()
n := tp.arrayNext[addr]
if n >= fv.Len() {
return nil, errTargetFallback
}
tp.arrayNext[addr] = n + 1
return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: parent.path, strict: parent.strict}, nil
case reflect.Map:
if et.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
addr := fv.Addr().Pointer()
n := tp.arrayNext[addr]
if n >= fv.Len() {
return nil, errTargetFallback
}
tp.arrayNext[addr] = n + 1
elem := reflect.MakeMap(et)
fv.Index(n).Set(elem)
return &targetTable{rv: elem, path: parent.path}, nil
}
return nil, errTargetFallback
}
if fv.Kind() != reflect.Slice {
return nil, tp.errf("key %q is not an array of tables", leaf)
}
et := derefType(fv.Type().Elem())
switch et.Kind() {
case reflect.Struct:
if isScalarStruct(et) {
return nil, errTargetFallback
}
grown := reflect.Append(fv, reflect.New(et).Elem())
fv.Set(grown)
return &targetTable{rv: grown.Index(grown.Len() - 1), schema: schemaRef(et), path: elementPath(parent.path, leaf, grown.Len()-1), strict: parent.strict}, nil
case reflect.Map:
if et.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
grown := reflect.Append(fv, reflect.MakeMap(et))
fv.Set(grown)
return &targetTable{rv: grown.Index(grown.Len() - 1), path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
}
return nil, errTargetFallback
}
// --- keys ------------------------------------------------------------------
// parseKeyStatement parses one `key = value` statement into the current
// table, mirroring the tree parser's dotted descent and definition checks.
// The value parses after the descent here, straight into the destination
// where the destination is a plain scalar: the descent and the value scan
// are independent, so the only observable difference is which error a line
// with two faults reports.
func (tp *targetParser) parseKeyStatement() error {
p := tp.parser
first, rest, err := p.parseKeyPath()
if err != nil {
return err
}
p.skipInline()
if p.eof() || p.peek() != '=' {
return p.errf("expected '=' after key")
}
p.pos++
p.skipInline()
dest := tp.cur
leaf := first
var dst reflect.Value
var mapDst reflect.Value
var leafTable *targetTable
placed := false
if len(rest) == 0 {
dst, mapDst, leafTable, placed, err = tp.leafInTable(dest, first)
if err != nil {
return err
}
} else {
// A dotted key is the one shape whose bookkeeping needs the statement
// path (the segment freeze and definition checks), so only here does
// the path slice get built. A plain key at the root has no path, and
// a plain key inside a table needs none either.
abs := make([]string, 0, len(dest.path)+len(rest)+1)
abs = append(abs, dest.path...)
abs = append(abs, first)
abs = append(abs, rest...)
dst, mapDst, leafTable, placed, err = tp.descendDotted(dest, first, rest, abs)
if err != nil {
return err
}
leaf = rest[len(rest)-1]
}
if !placed {
// A sink or an unknown key: the value parses for the syntax contract
// and is dropped, but the key still takes the duplicate check, the way
// the tree's maps record every key they receive. The sink's flat key
// set tracks the full statement path, because a dotted key inside a
// sink lands in a sub-table of its own in the tree, not beside the
// leaf name.
val, verr := p.parseValue()
if verr != nil {
return verr
}
dupKey := first
if len(dest.path) > 0 || len(rest) > 0 {
full := make([]string, 0, len(dest.path)+len(rest)+1)
full = append(full, dest.path...)
full = append(full, first)
full = append(full, rest...)
dupKey = pathKey(full)
}
if tp.tableHas(leafTable, dupKey) {
return p.errf("duplicate key %q", leaf)
}
tp.tableMark(leafTable, dupKey)
if m, isMap := val.(map[string]any); isMap {
full := make([]string, 0, len(dest.path)+len(leaf)+1)
full = append(full, dest.path...)
full = append(full, leaf)
p.freezeInline(full, m)
}
return nil
}
if tp.tableHas(leafTable, leaf) {
return p.errf("duplicate key %q", leaf)
}
tp.tableMark(leafTable, leaf)
if dst.Kind() == reflect.Slice || dst.Kind() == reflect.Array {
// Only a field a later [[header]] could append to needs the
// key-assigned record; everything else never checks it.
et := derefType(dst.Type().Elem())
if et.Kind() == reflect.Struct && !isScalarStruct(et) || et.Kind() == reflect.Map {
tp.markKeyAssigned(leafTable, leaf)
}
}
val, err := tp.parseValueInto(dst)
if err != nil {
return err
}
if mapDst.IsValid() {
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
}
if m, isMap := val.(map[string]any); isMap {
// An inline table freezes its paths; the abs slice is built for it
// alone, after the parse proved one is needed.
abs := make([]string, 0, len(dest.path)+len(leaf)+1)
abs = append(abs, dest.path...)
abs = append(abs, leaf)
p.freezeInline(abs, m)
}
return nil
}
// parseValueInto parses the value at the cursor straight into the
// destination and returns the boxed value the freeze bookkeeping may need
// (non-nil only for inline tables and other composites). Scalars are written
// into the destination without the boxing the tree layer requires.
func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
p := tp.parser
if p.eof() {
return nil, p.errf("expected a value")
}
start := p.pos
if dstHasDecodeHook(dst) {
// The custom hooks take the boxed value the tree layer produces.
v, err := p.parseValue()
if err != nil {
return nil, err
}
if err := tp.d.assign(v, dst); err != nil {
return nil, errTargetFallback
}
return v, nil
}
switch c := p.peek(); {
case c == '"' || c == '\'':
if dst.Kind() == reflect.String {
var s string
var err error
if c == '"' {
s, err = p.parseBasicString()
} else {
s, err = p.parseLiteralString()
}
if err != nil {
return nil, err
}
dst.SetString(s)
return nil, nil
}
case c == 't' || c == 'f':
b, ok := tp.scanBool()
if !ok {
return nil, p.errf("invalid value")
}
if dst.Kind() == reflect.Bool {
dst.SetBool(b)
return nil, nil
}
p.pos = start
case (c >= '0' && c <= '9') || c == '+' || c == '-':
// The plain-digit fast path parses the common integer without a
// token copy; anything else takes the token route, where the strict
// number rules live.
if dstNumericKind(dst) && dst.Kind() != reflect.Float32 && dst.Kind() != reflect.Float64 {
if n, ok := tp.tryFastInt(); ok {
if err := setInt(dst, n); err != nil {
return nil, errTargetFallback
}
return nil, nil
}
}
tok := tp.scanNumberToken()
if dtv, dtok := parseDateTime(tok); dtok {
if err := tp.d.assign(dtv, dst); err != nil {
return nil, errTargetFallback
}
return dtv, nil
}
if dstNumericKind(dst) {
fallback, syntaxErr := numberIntoReflect(dst, tok)
if syntaxErr != nil {
return nil, p.errf("%s", syntaxErr)
}
if fallback {
return nil, errTargetFallback
}
return nil, nil
}
p.pos = start
}
v, err := p.parseValue()
if err != nil {
return nil, err
}
if err := tp.d.assign(v, dst); err != nil {
return nil, errTargetFallback
}
return v, nil
}
// dstHasDecodeHook reports whether the destination carries one of the custom
// decode interfaces, whose hooks need the boxed value the tree layer makes.
func dstHasDecodeHook(dst reflect.Value) bool {
if _, ok := unmarshalerOf(dst); ok {
return true
}
if _, ok := ctxUnmarshalerOf(dst); ok {
return true
}
if _, ok := textUnmarshalerOf(dst); ok {
return true
}
return false
}
// scanBool consumes true or false and returns the value, reporting whether
// the token was a boolean at all.
func (tp *targetParser) scanBool() (bool, bool) {
if tp.parser.match("true") {
return true, true
}
if tp.parser.match("false") {
return false, true
}
return false, false
}
// tryFastInt parses a run of plain decimal digits at the cursor into an
// int64 without materialising the token, reporting whether the token was
// one. A leading zero, an underscore, or any trailing character that is not
// a token terminator hands the token back to the strict number rules.
func (tp *targetParser) tryFastInt() (int64, bool) {
p := tp.parser
i := p.pos
if i >= len(p.src) {
return 0, false
}
if p.src[i] == '+' || p.src[i] == '-' {
return 0, false
}
if p.src[i] == '0' && i+1 < len(p.src) && p.src[i+1] >= '0' && p.src[i+1] <= '9' {
return 0, false
}
var n int64
digits := 0
for i < len(p.src) && p.src[i] >= '0' && p.src[i] <= '9' {
if digits >= 18 {
return 0, false
}
n = n*10 + int64(p.src[i]-'0')
i++
digits++
}
if digits == 0 {
return 0, false
}
if i < len(p.src) {
switch p.src[i] {
case ' ', '\t', '\n', '\r', ',', ']', '}', '#':
default:
return 0, false
}
}
p.pos = i
return n, true
}
// scanNumberToken scans a bare number (or date-time) token, including the
// space-separated date and time forms, and returns it as text.
func (tp *targetParser) scanNumberToken() string {
p := tp.parser
start := p.pos
p.scanBareToken()
tok := string(p.src[start:p.pos])
if isDateToken(tok) && !p.eof() && p.peek() == ' ' {
if next, ok := p.peekAt(1); ok && next >= '0' && next <= '9' {
p.pos++ // consume the separating space
timeStart := p.pos
p.scanBareToken()
tok = tok + " " + string(p.src[timeStart:p.pos])
}
}
return tok
}
// dstNumericKind reports whether the destination takes a parsed number.
func dstNumericKind(dst reflect.Value) bool {
switch dst.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
}
return false
}
// numberIntoReflect parses a number token straight into a numeric
// destination, with the strict validation the tree parser applies. The
// fallback flag reports a destination-level overflow, the tree decode's
// own error; syntaxErr is the token-level error the parser reports.
func numberIntoReflect(dst reflect.Value, tok string) (fallback bool, syntaxErr error) {
v, err := decodeNumber(tok)
if err != nil {
return false, err
}
switch n := v.(type) {
case int64:
if err := setInt(dst, n); err != nil {
return true, nil
}
return false, nil
case float64:
if err := setFloat(dst, n); err != nil {
return true, nil
}
return false, nil
}
return true, nil
}
// descendDotted walks the dotted segments first..rest[:len(rest)-1] and
// returns the leaf destination: a struct field, or a map entry to set after
// the value assigns. A segment no field claims sinks the rest of the dotted
// key. The per-segment checks and their messages are the tree parser's
// descendKey: an existing value that is not a table is the `not a table`
// error, a missing one is created where the tree creates it and falls back
// where the tree's creation meets a decode-stage type error.
func (tp *targetParser) descendDotted(dest *targetTable, first string, rest []string, abs []string) (dst reflect.Value, mapDst reflect.Value, leafTable *targetTable, placed bool, err error) {
leafTable = dest
if dest.sink {
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
}
tbl := dest
segments := append([]string{first}, rest[:len(rest)-1]...)
for i, seg := range segments {
segAbs := abs[:len(dest.path)+i+1]
if tp.frozenAt(segAbs) {
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("cannot extend inline table %q", strings.Join(segAbs, "."))
}
if tp.parser.headers[pathKey(segAbs)] {
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("cannot extend table %q with a dotted key", strings.Join(segAbs, "."))
}
tp.parser.markDotted(pathKey(segAbs))
child, err := tp.dottedEnter(tbl, seg, segAbs)
if err != nil {
return reflect.Value{}, reflect.Value{}, leafTable, false, err
}
tp.tableMark(tbl, seg)
tbl = child
if tbl.sink {
leafTable = tbl
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
}
}
leaf := rest[len(rest)-1]
leafTable = tbl
if tp.tableHas(tbl, leaf) {
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", leaf)
}
if tbl.schema == nil {
if tbl.rv.Kind() != reflect.Map {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
}
elem := reflect.New(tbl.rv.Type().Elem()).Elem()
return elem, tbl.rv, leafTable, true, nil
}
resolved := leaf
loc, found := tbl.schema.byName[leaf]
if !found {
resolved = strings.ToLower(leaf)
loc, found = tbl.schema.byName[resolved]
}
if !found {
tbl.recordStrictUnknown(leaf)
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
}
tbl.markResolved(resolved)
fv, ferr := fieldByIndex(tbl.rv, loc.index)
if ferr != nil {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
}
return fv, reflect.Value{}, leafTable, true, nil
}
// dottedEnter enters one dotted segment inside tbl. It differs from the
// header descent in the slice and assigned-scalar cases, which the tree's
// descendKey answers with `key is not a table`.
func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []string) (*targetTable, error) {
if tbl.sink {
return tbl, nil
}
if tbl.schema == nil {
if tbl.rv.Kind() != reflect.Map {
return nil, errTargetFallback
}
elemT := tbl.rv.Type().Elem()
if elemT.Kind() != reflect.Map || elemT.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
ev := existing.Elem()
if ev.Kind() != reflect.Map {
return nil, tp.errf("key %q is not a table", seg)
}
return &targetTable{rv: ev, path: segAbs}, nil
}
next := reflect.MakeMap(elemT)
tbl.rv.SetMapIndex(reflect.ValueOf(seg), next)
return &targetTable{rv: next, path: segAbs}, nil
}
resolved := seg
loc, ok := tbl.schema.byName[seg]
if !ok {
resolved = strings.ToLower(seg)
loc, ok = tbl.schema.byName[resolved]
}
if !ok {
tbl.recordStrictUnknown(seg)
if opened, ok := tp.opened[pathKey(segAbs)]; ok {
return opened, nil
}
if tp.tableHas(tbl, seg) {
return nil, tp.errf("key %q is not a table", seg)
}
sink := &targetTable{sink: true, path: segAbs}
tp.opened[pathKey(segAbs)] = sink
return sink, nil
}
fv, ferr := fieldByIndex(tbl.rv, loc.index)
if ferr != nil {
return nil, errTargetFallback
}
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
if !fv.CanSet() {
return nil, errTargetFallback
}
fv.Set(reflect.New(fv.Type().Elem()))
}
fv = fv.Elem()
}
switch fv.Kind() {
case reflect.Struct:
if isScalarStruct(fv.Type()) {
if tp.tableHas(tbl, seg) {
return nil, tp.errf("key %q is not a table", seg)
}
return nil, errTargetFallback
}
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: segAbs, strict: tbl.strict}, nil
case reflect.Map:
if fv.Type().Key().Kind() != reflect.String {
return nil, errTargetFallback
}
if fv.IsNil() {
fv.Set(reflect.MakeMap(fv.Type()))
}
return &targetTable{rv: fv, path: segAbs}, nil
}
if tp.tableHas(tbl, seg) {
return nil, tp.errf("key %q is not a table", seg)
}
return nil, errTargetFallback
}
// leafInTable resolves a plain key in the table.
func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.Value, mapDst reflect.Value, leafTable *targetTable, placed bool, err error) {
leafTable = dest
if dest.sink {
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
}
if dest.schema == nil {
if dest.rv.Kind() != reflect.Map {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
}
if dest.rv.MapIndex(reflect.ValueOf(key)).IsValid() {
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key)
}
elem := reflect.New(dest.rv.Type().Elem()).Elem()
return elem, dest.rv, leafTable, true, nil
}
resolved := key
loc, ok := dest.schema.byName[key]
if !ok {
resolved = strings.ToLower(key)
loc, ok = dest.schema.byName[resolved]
}
if !ok {
dest.recordStrictUnknown(key)
return reflect.Value{}, reflect.Value{}, leafTable, false, nil
}
dest.markResolved(resolved)
fv, ferr := fieldByIndex(dest.rv, loc.index)
if ferr != nil {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
}
return fv, reflect.Value{}, leafTable, true, nil
}