// Copyright (c) 2026 Petr BalvĂ­n (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. // // One difference the two paths cannot share: a parse error or a cancellation // deep in the document leaves the statements before it already written into // the destination, where the tree path, which parses the whole document // before it decodes any of it, writes nothing. The value layer shares this // with encoding/json, whose Unmarshal also leaves a partial destination // behind a mid-document failure; a destination that must stay untouched on // error is a destination the caller resets. 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 structural bans are the shapes whose tree behaviour // the skeleton cannot model: untagged embedded maps, an OrderedMap anywhere a // table opens, and a custom decode hook on any table the parse would enter // directly (a struct field, a map field, or the element of a table slice), // because the tree hands a hook the whole parsed value. 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 { if !scanTargetableField(derefType(t.FieldByIndex(loc.index).Type), seen) { return false } } return true } // scanTargetableField reports whether one field's type is safe for the // targeted skeleton to fill directly. func scanTargetableField(ft reflect.Type, seen map[reflect.Type]bool) bool { switch ft.Kind() { case reflect.Struct: if ft == orderedMapType { return false } if isScalarStruct(ft) { return true } // A struct the parse enters directly never builds the whole value // the tree hands a hook, so the hook must win. if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) { return false } return scanTargetable(ft, seen) case reflect.Map: return !implementsDecodeHook(ft) && !implementsDecodeHook(reflect.PointerTo(ft)) case reflect.Slice, reflect.Array: et := derefType(ft.Elem()) if et == orderedMapType { return false } if et.Kind() == reflect.Struct && !isScalarStruct(et) { return scanTargetableField(et, seen) } return true } 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), and the absolute path its errors wrap. A sink is the // destination an unknown subtree gets: its statements parse for the syntax // and definition contracts, and its values are discarded. The strict and // required findings live in the parser's per-address store, not here, // because the table objects a dotted descent builds are transient while the // destination is not. type targetTable struct { rv reflect.Value schema *structSchema path []string sink bool // strict is the strict-decode setting the table was opened with, carried // into the per-address strict state on first sight. strict bool // arrayElem marks a sink created as the element of an unknown array of // tables: a dotted key may not enter it, the tree's own rule for an // array, while a [sub-table] header may, through the last element. arrayElem bool keys []string // the keys defined in a sink, as full path keys } // strictState is the strict and required bookkeeping of one struct // destination, keyed by the value's address. type strictState struct { path []string typ reflect.Type schema *structSchema strict bool unknown string // strict: the smallest unclaimed key so far resolved map[string]bool } // arrayFill tracks how many elements of one fixed-size array the document // has filled, with what the length mismatch the tree decode reports needs: // the array's type and its field's path. type arrayFill struct { next int typ reflect.Type path []string } // 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 // arrayFills counts the elements of each fixed-size array the document // has filled, per field address, in the order the arrays were met. arrayFills map[uintptr]*arrayFill fillOrder []*arrayFill // appendedHere records the slice fields this document's [[headers]] have // filled: a slice the caller prefilled is replaced by the tree decode, // not appended to, so the first header over one falls back. appendedHere map[uintptr]bool // 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 // 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 // mapKeysByAddr holds the keys the document has defined in one map // destination, keyed the same way: the destination map the caller // prefilled is not the parser's state, and a key it holds is not the // duplicate a key the document repeats is. mapKeysByAddr map[uintptr]map[string]bool // strictByAddr holds each destination's strict and required findings, // with strictOrder keeping the document order they first appeared in. strictByAddr map[uintptr]*strictState strictOrder []uintptr } // 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, and map tables keep // theirs there too, because the table object a dotted descent builds is // transient while the destination is not, and the destination map's own // contents are the caller's, not the document's. func (tp *targetParser) tableHas(t *targetTable, key string) bool { switch { case t.sink: return slices.Contains(t.keys, key) case t.schema == nil: if t.rv.Kind() != reflect.Map || !t.rv.CanAddr() { return false } return tp.mapKeysByAddr[t.rv.Addr().Pointer()][key] 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: if !t.rv.CanAddr() || t.rv.Kind() != reflect.Map { return } addr := t.rv.Addr().Pointer() if tp.mapKeysByAddr == nil { tp.mapKeysByAddr = make(map[uintptr]map[string]bool, 8) } if tp.mapKeysByAddr[addr] == nil { tp.mapKeysByAddr[addr] = make(map[string]bool, 8) } tp.mapKeysByAddr[addr][key] = true 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) } } // strictState returns the strict and required bookkeeping of the struct // destination t fills, registering it on first sight so a finding recorded // on a transient table survives the table. func (tp *targetParser) strictState(t *targetTable) *strictState { addr := t.rv.Addr().Pointer() if st, ok := tp.strictByAddr[addr]; ok { return st } st := &strictState{ path: slices.Clone(t.path), typ: t.rv.Type(), schema: t.schema, strict: t.strict, resolved: make(map[string]bool, 8), } tp.strictByAddr[addr] = st tp.strictOrder = append(tp.strictOrder, addr) return st } // markResolved records that the document resolved the schema key, 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 (tp *targetParser) markResolved(t *targetTable, key string) { if t.schema == nil || len(t.schema.required) == 0 { return } if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct { return } tp.strictState(t).resolved[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 (tp *targetParser) recordStrictUnknown(t *targetTable, key string) { if !t.strict || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct { return } st := tp.strictState(t) if st.unknown == "" || key < st.unknown { st.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(), arrayFills: make(map[uintptr]*arrayFill, 4), appendedHere: make(map[uintptr]bool, 4), opened: make(map[string]*targetTable, 8), strictByAddr: make(map[uintptr]*strictState, 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. A fixed-size array the document under-filled is the length mismatch // the tree decode raises, and it comes first. Then the unknown keys, before // the required ones, because the tree decode meets an unknown key while it // assigns and checks a table's required keys only once the whole table has // been; within each class the order is the order the destinations first // appeared in, the document's own order. func (tp *targetParser) reportDeferred() error { for _, f := range tp.fillOrder { if f.next != f.typ.Len() { return wrapTablePath(f.path, fmt.Errorf("interpres: cannot assign %d elements to %s", f.next, f.typ)) } } for _, addr := range tp.strictOrder { if st := tp.strictByAddr[addr]; st.strict && st.unknown != "" { return wrapTablePath(st.path, fmt.Errorf("interpres: unknown field %q for %s", st.unknown, st.typ)) } } for _, addr := range tp.strictOrder { st := tp.strictByAddr[addr] if st.schema == nil { continue } for _, key := range st.schema.required { if !st.resolved[key] { return wrapTablePath(st.path, fmt.Errorf("interpres: missing required key %q", key)) } } } return nil } // wrapTablePath wraps a table's finding the way the tree decode wraps it: the // root speaks for itself, a nested table gains its path. func wrapTablePath(path []string, err error) error { if len(path) == 0 { return err } return &DecodeError{Path: Path(slices.Clone(path)), Err: err} } // arrayFillFor returns the fill record of the fixed-size array fv, keyed by // its address, created with its field's path on first sight. func (tp *targetParser) arrayFillFor(fv reflect.Value, path []string) *arrayFill { addr := fv.Addr().Pointer() if f, ok := tp.arrayFills[addr]; ok { return f } f := &arrayFill{typ: fv.Type(), path: path} tp.arrayFills[addr] = f tp.fillOrder = append(tp.fillOrder, f) return f } // --- 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, path: slices.Clone(abs)}, nil } next := reflect.MakeMap(elemT) parent.rv.SetMapIndex(reflect.ValueOf(key), next) return &targetTable{rv: next, path: slices.Clone(abs)}, nil } resolved := key loc, ok := parent.schema.byName[key] if !ok { resolved = strings.ToLower(key) loc, ok = parent.schema.byName[resolved] } if !ok { tp.recordStrictUnknown(parent, 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: slices.Clone(abs)} tp.opened[pathKey(abs)] = sink return sink, nil } tp.markResolved(parent, 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 filled element, a slice enters the last element of // an array this document's [[headers]] built (a prefilled slice is a table // the tree decode rejects, so it falls back), and anything else is a type // mismatch the tree decode reports, so it falls back too. 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: slices.Clone(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: slices.Clone(abs)}, nil case reflect.Slice: if !tp.appendedHere[fv.Addr().Pointer()] { // No [[header]] of this document filled it, so the tree holds a // map here, whose decode raises the type mismatch; a prefilled // slice is the tree's replacement case, not a table to enter. 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 case reflect.Array: fill := tp.arrayFillFor(fv, append(slices.Clone(parent.path), key)) if fill.next == 0 { return nil, errTargetFallback } et := derefType(fv.Type().Elem()) if et.Kind() == reflect.Struct { if isScalarStruct(et) { return nil, errTargetFallback } return &targetTable{rv: fv.Index(fill.next - 1), schema: schemaRef(et), path: elementPath(abs, key, fill.next-1), strict: strict}, nil } if et.Kind() == reflect.Map && et.Key().Kind() == reflect.String { return &targetTable{rv: fv.Index(fill.next - 1), path: elementPath(abs, key, fill.next-1)}, nil } return nil, errTargetFallback } 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) } // A leaf the document already defined as a value or a table is the tree // parser's own parse error, and a leaf an earlier [[header]] defined // opens a new element; the arrays map, read before this header marks it, // is what tells the two apart. A sink parent holds no destination state // worth consulting. if !parent.sink && !tp.parser.arrays[pk] && tp.tableHas(parent, leaf) { return tp.errf("key %q is not an array of tables", leaf) } // A new element starts a fresh scope, exactly as the tree parser's own // header does: sub-headers, inline freezes and nested arrays from the // previous element no longer apply. tp.parser.resetScopeUnder(key) tp.parser.markArray(pk) elem, err := tp.appendElement(parent, leaf, key) if err != nil { return err } if elem.sink { // A sink the element scope reuses ([[a.b]] over an unknown a, the // parent sink) starts the new element with no keys, the way a known // array's element does. elem.keys = nil } 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 fresh namespace per // element; 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() { // A map[string]any destination boxes its arrays in the // interface; a typed map hands the slice itself. if existing.Kind() == reflect.Interface { existing = existing.Elem() } if existing.Kind() != reflect.Slice { return nil, tp.errf("key %q is not an array of tables", leaf) } arr = existing } 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 { tp.recordStrictUnknown(parent, leaf) // Every element is a fresh namespace, the way a known array's is, // registered under the header's path so a [sub-table] header reaches // the last element, the tree's rule for a header under an array of // tables; a dotted key skips it, the tree's rule for an array. sink := &targetTable{sink: true, arrayElem: true, path: slices.Clone(key)} tp.opened[pathKey(key)] = sink return sink, nil } tp.markResolved(parent, resolved) fv, err := fieldByIndex(parent.rv, loc.index) if err != nil { return nil, errTargetFallback } fieldPath := append(slices.Clone(parent.path), leaf) 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, and one too few is // the same mismatch, checked when the parse completes. fill := tp.arrayFillFor(fv, fieldPath) et := derefType(fv.Type().Elem()) switch { case et.Kind() == reflect.Struct && !isScalarStruct(et): if fill.next >= fv.Len() { return nil, errTargetFallback } n := fill.next fill.next = n + 1 return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: elementPath(parent.path, leaf, n), strict: parent.strict}, nil case et.Kind() == reflect.Map && et.Key().Kind() == reflect.String: if fill.next >= fv.Len() { return nil, errTargetFallback } n := fill.next fill.next = n + 1 elem := reflect.MakeMap(et) fv.Index(n).Set(elem) return &targetTable{rv: elem, path: elementPath(parent.path, leaf, n)}, nil } return nil, errTargetFallback } if fv.Kind() != reflect.Slice { if tp.tableHas(parent, leaf) { return nil, tp.errf("key %q is not an array of tables", leaf) } // The tree builds an array here without asking the destination, and // its decode answers with the type mismatch; the fallback keeps the // message the tree path gives. return nil, errTargetFallback } addr := fv.Addr().Pointer() if !tp.appendedHere[addr] { if fv.Len() > 0 { // A slice the caller prefilled is replaced by the tree decode, // not appended to; the fallback runs the document the tree's way. return nil, errTargetFallback } tp.appendedHere[addr] = true } et := derefType(fv.Type().Elem()) switch et.Kind() { case reflect.Struct: if isScalarStruct(et) { return nil, errTargetFallback } // A pointer element is appended as the allocated pointer and filled // through its pointee, so []*T takes the same path []T does. The // element the table fills is the slice's own: the value a New built // stands apart from the backing array. var el, appended reflect.Value if fv.Type().Elem().Kind() == reflect.Pointer { p := reflect.New(et) el, appended = p.Elem(), p } else { el = reflect.New(et).Elem() appended = el } grown := reflect.Append(fv, appended) fv.Set(grown) if fv.Type().Elem().Kind() != reflect.Pointer { el = grown.Index(grown.Len() - 1) } return &targetTable{rv: el, 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 } m := reflect.MakeMap(et) var appended reflect.Value = m if fv.Type().Elem().Kind() == reflect.Pointer { p := reflect.New(et) p.Elem().Set(m) appended = p } grown := reflect.Append(fv, appended) fv.Set(grown) return &targetTable{rv: m, 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 } full := append([]string{first}, rest...) if len(dest.path) > 0 { full = append(slices.Clone(dest.path), full...) } if tp.tableHas(leafTable, pathKey(full)) { return p.errf("duplicate key %q", leaf) } tp.tableMark(leafTable, pathKey(full)) if m, isMap := val.(map[string]any); isMap { // An inline table freezes the whole path the statement wrote, // intermediate segments included, so no later header or dotted // key can extend it at any depth. p.freezeInline(full, m) } return nil } if tp.tableHas(leafTable, leaf) { return p.errf("duplicate key %q", leaf) } tp.tableMark(leafTable, leaf) val, err := tp.parseValueInto(dst) if err != nil { if errors.Is(err, errTargetFallback) { return err } if _, isSyntax := errors.AsType[*SyntaxError](err); !isSyntax { // A hook's own failure, which the fallback must not rerun: it // returns wrapped the way the tree decode wraps a field's. return newDecodeError(leaf, err) } return err } if mapDst.IsValid() { mapDst.SetMapIndex(reflect.ValueOf(leaf), dst) } if m, isMap := val.(map[string]any); isMap { // An inline table freezes the whole path the statement wrote, // intermediate segments included; the slice is built for it alone, // after the parse proved one is needed. abs := make([]string, 0, len(dest.path)+len(rest)+1) abs = append(abs, dest.path...) abs = append(abs, first) abs = append(abs, rest...) 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 { // The hook has run; falling back would run it a second time on // tree path, so its error returns as the tree path's own, for // the caller to wrap the way the tree decode wraps a field's. return nil, err } 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 hasHighByte(tok) && invalidUTF8Offset(tok) >= 0 { // The token route the targeted parse takes validates UTF-8 the // way the tree scanner does, on the byte that does not decode. return nil, p.errf("invalid UTF-8 in value at byte offset %d", start+invalidUTF8Offset(tok)) } dtv, isDT, dterr := parseDateTime(tok) if dterr != nil { return nil, p.errf("%s", dterr) } if isDT { 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 { tp.recordStrictUnknown(tbl, leaf) return reflect.Value{}, reflect.Value{}, leafTable, false, nil } tp.markResolved(tbl, 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() { if existing.Kind() != reflect.Map { return nil, tp.errf("key %q is not a table", seg) } return &targetTable{rv: existing, path: slices.Clone(segAbs)}, nil } next := reflect.MakeMap(elemT) tbl.rv.SetMapIndex(reflect.ValueOf(seg), next) return &targetTable{rv: next, path: slices.Clone(segAbs)}, nil } resolved := seg loc, ok := tbl.schema.byName[seg] if !ok { resolved = strings.ToLower(seg) loc, ok = tbl.schema.byName[resolved] } if !ok { tp.recordStrictUnknown(tbl, seg) // A sink an array element created is entered by a [sub-table] // header, through the last element, but never by a dotted key: the // tree's descendKey rejects an array where tableAt follows it. if opened, ok := tp.opened[pathKey(segAbs)]; ok && !opened.arrayElem { return opened, nil } if tp.tableHas(tbl, seg) { return nil, tp.errf("key %q is not a table", seg) } sink := &targetTable{sink: true, path: slices.Clone(segAbs)} tp.opened[pathKey(segAbs)] = sink return sink, nil } tp.markResolved(tbl, resolved) 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: slices.Clone(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: slices.Clone(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 tp.tableHas(dest, key) { // The duplicate check reads the keys the document defined, not // the destination map's own contents, which are the caller's. 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 { tp.recordStrictUnknown(dest, key) return reflect.Value{}, reflect.Value{}, leafTable, false, nil } tp.markResolved(dest, 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 }