reflect: add support for linked lists
Linked lists are usually implemented as follows: type linkedList struct { next *linkedList data int // whatever } This caused a stack overflow while writing out the reflect run-time type information. This has now been fixed by splitting the allocation of a named type number from setting the underlying type in the sidetable.
Этот коммит содержится в:
родитель
0818a125c0
коммит
ea8e4079bc
7 изменённых файлов: 128 добавлений и 84 удалений
|
@ -92,7 +92,7 @@ func (c *Compiler) makeStructTypeFields(typ *types.Struct) llvm.Value {
|
||||||
for i := 0; i < typ.NumFields(); i++ {
|
for i := 0; i < typ.NumFields(); i++ {
|
||||||
fieldGlobalValue := c.getZeroValue(runtimeStructField)
|
fieldGlobalValue := c.getZeroValue(runtimeStructField)
|
||||||
fieldGlobalValue = llvm.ConstInsertValue(fieldGlobalValue, c.getTypeCode(typ.Field(i).Type()), []uint32{0})
|
fieldGlobalValue = llvm.ConstInsertValue(fieldGlobalValue, c.getTypeCode(typ.Field(i).Type()), []uint32{0})
|
||||||
fieldName := c.makeGlobalBytes([]byte(typ.Field(i).Name()), "reflect/types.structFieldName")
|
fieldName := c.makeGlobalArray([]byte(typ.Field(i).Name()), "reflect/types.structFieldName", c.ctx.Int8Type())
|
||||||
fieldName.SetLinkage(llvm.PrivateLinkage)
|
fieldName.SetLinkage(llvm.PrivateLinkage)
|
||||||
fieldName.SetUnnamedAddr(true)
|
fieldName.SetUnnamedAddr(true)
|
||||||
fieldName = llvm.ConstGEP(fieldName, []llvm.Value{
|
fieldName = llvm.ConstGEP(fieldName, []llvm.Value{
|
||||||
|
@ -101,7 +101,7 @@ func (c *Compiler) makeStructTypeFields(typ *types.Struct) llvm.Value {
|
||||||
})
|
})
|
||||||
fieldGlobalValue = llvm.ConstInsertValue(fieldGlobalValue, fieldName, []uint32{1})
|
fieldGlobalValue = llvm.ConstInsertValue(fieldGlobalValue, fieldName, []uint32{1})
|
||||||
if typ.Tag(i) != "" {
|
if typ.Tag(i) != "" {
|
||||||
fieldTag := c.makeGlobalBytes([]byte(typ.Tag(i)), "reflect/types.structFieldTag")
|
fieldTag := c.makeGlobalArray([]byte(typ.Tag(i)), "reflect/types.structFieldTag", c.ctx.Int8Type())
|
||||||
fieldTag = llvm.ConstGEP(fieldTag, []llvm.Value{
|
fieldTag = llvm.ConstGEP(fieldTag, []llvm.Value{
|
||||||
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
||||||
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package compiler
|
package compiler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"tinygo.org/x/go-llvm"
|
"tinygo.org/x/go-llvm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -153,24 +155,26 @@ func (c *Compiler) splitBasicBlock(afterInst llvm.Value, insertAfter llvm.BasicB
|
||||||
return newBlock
|
return newBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeGlobalBytes creates a new LLVM global with the given name and bytes as
|
// makeGlobalArray creates a new LLVM global with the given name and integers as
|
||||||
// contents, and returns the global.
|
// contents, and returns the global.
|
||||||
// Note that it is left with the default linkage etc., you should set
|
// Note that it is left with the default linkage etc., you should set
|
||||||
// linkage/constant/etc properties yourself.
|
// linkage/constant/etc properties yourself.
|
||||||
func (c *Compiler) makeGlobalBytes(buf []byte, name string) llvm.Value {
|
func (c *Compiler) makeGlobalArray(bufItf interface{}, name string, elementType llvm.Type) llvm.Value {
|
||||||
globalType := llvm.ArrayType(c.ctx.Int8Type(), len(buf))
|
buf := reflect.ValueOf(bufItf)
|
||||||
|
globalType := llvm.ArrayType(elementType, buf.Len())
|
||||||
global := llvm.AddGlobal(c.mod, globalType, name)
|
global := llvm.AddGlobal(c.mod, globalType, name)
|
||||||
value := llvm.Undef(globalType)
|
value := llvm.Undef(globalType)
|
||||||
for i, ch := range buf {
|
for i := 0; i < buf.Len(); i++ {
|
||||||
value = llvm.ConstInsertValue(value, llvm.ConstInt(c.ctx.Int8Type(), uint64(ch), false), []uint32{uint32(i)})
|
ch := buf.Index(i).Uint()
|
||||||
|
value = llvm.ConstInsertValue(value, llvm.ConstInt(elementType, ch, false), []uint32{uint32(i)})
|
||||||
}
|
}
|
||||||
global.SetInitializer(value)
|
global.SetInitializer(value)
|
||||||
return global
|
return global
|
||||||
}
|
}
|
||||||
|
|
||||||
// getGlobalBytes returns the byte slice contained in the i8 array of the
|
// getGlobalBytes returns the slice contained in the array of the provided
|
||||||
// provided global. It can recover the bytes originally created using
|
// global. It can recover the bytes originally created using makeGlobalArray, if
|
||||||
// makeGlobalBytes.
|
// makeGlobalArray was given a byte slice.
|
||||||
func getGlobalBytes(global llvm.Value) []byte {
|
func getGlobalBytes(global llvm.Value) []byte {
|
||||||
value := global.Initializer()
|
value := global.Initializer()
|
||||||
buf := make([]byte, value.Type().ArrayLength())
|
buf := make([]byte, value.Type().ArrayLength())
|
||||||
|
@ -180,12 +184,12 @@ func getGlobalBytes(global llvm.Value) []byte {
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceGlobalByteWithArray replaces a global i8 in the module with a byte
|
// replaceGlobalByteWithArray replaces a global integer type in the module with
|
||||||
// array, using a GEP to make the types match. It is a convenience function used
|
// an integer array, using a GEP to make the types match. It is a convenience
|
||||||
// for creating reflection sidetables, for example.
|
// function used for creating reflection sidetables, for example.
|
||||||
func (c *Compiler) replaceGlobalByteWithArray(name string, buf []byte) llvm.Value {
|
func (c *Compiler) replaceGlobalIntWithArray(name string, buf interface{}) llvm.Value {
|
||||||
global := c.makeGlobalBytes(buf, name+".tmp")
|
|
||||||
oldGlobal := c.mod.NamedGlobal(name)
|
oldGlobal := c.mod.NamedGlobal(name)
|
||||||
|
global := c.makeGlobalArray(buf, name+".tmp", oldGlobal.Type().ElementType())
|
||||||
gep := llvm.ConstGEP(global, []llvm.Value{
|
gep := llvm.ConstGEP(global, []llvm.Value{
|
||||||
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
||||||
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
||||||
|
|
|
@ -37,7 +37,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// A list of basic types and their numbers. This list should be kept in sync
|
// A list of basic types and their numbers. This list should be kept in sync
|
||||||
// with the list of Kind constants of type.go in the runtime package.
|
// with the list of Kind constants of type.go in the reflect package.
|
||||||
var basicTypes = map[string]int64{
|
var basicTypes = map[string]int64{
|
||||||
"bool": 1,
|
"bool": 1,
|
||||||
"int": 2,
|
"int": 2,
|
||||||
|
@ -59,6 +59,19 @@ var basicTypes = map[string]int64{
|
||||||
"unsafeptr": 18,
|
"unsafeptr": 18,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A list of non-basic types. Adding 19 to this number will give the Kind as
|
||||||
|
// used in src/reflect/types.go, and it must be kept in sync with that list.
|
||||||
|
var nonBasicTypes = map[string]int64{
|
||||||
|
"chan": 0,
|
||||||
|
"interface": 1,
|
||||||
|
"pointer": 2,
|
||||||
|
"slice": 3,
|
||||||
|
"array": 4,
|
||||||
|
"func": 5,
|
||||||
|
"map": 6,
|
||||||
|
"struct": 7,
|
||||||
|
}
|
||||||
|
|
||||||
// typeCodeAssignmentState keeps some global state around for type code
|
// typeCodeAssignmentState keeps some global state around for type code
|
||||||
// assignments, used to assign one unique type code to each Go type.
|
// assignments, used to assign one unique type code to each Go type.
|
||||||
type typeCodeAssignmentState struct {
|
type typeCodeAssignmentState struct {
|
||||||
|
@ -96,7 +109,7 @@ type typeCodeAssignmentState struct {
|
||||||
// Note that this byte buffer is not created when it is not needed
|
// Note that this byte buffer is not created when it is not needed
|
||||||
// (reflect.namedNonBasicTypesSidetable has no uses), see
|
// (reflect.namedNonBasicTypesSidetable has no uses), see
|
||||||
// needsNamedTypesSidetable.
|
// needsNamedTypesSidetable.
|
||||||
namedNonBasicTypesSidetable []byte
|
namedNonBasicTypesSidetable []uint64
|
||||||
|
|
||||||
// This indicates whether namedNonBasicTypesSidetable needs to be created at
|
// This indicates whether namedNonBasicTypesSidetable needs to be created at
|
||||||
// all. If it is false, namedNonBasicTypesSidetable will contain simple
|
// all. If it is false, namedNonBasicTypesSidetable will contain simple
|
||||||
|
@ -144,17 +157,17 @@ func (c *Compiler) assignTypeCodes(typeSlice typeInfoSlice) {
|
||||||
|
|
||||||
// Only create this sidetable when it is necessary.
|
// Only create this sidetable when it is necessary.
|
||||||
if state.needsNamedNonBasicTypesSidetable {
|
if state.needsNamedNonBasicTypesSidetable {
|
||||||
global := c.replaceGlobalByteWithArray("reflect.namedNonBasicTypesSidetable", state.namedNonBasicTypesSidetable)
|
global := c.replaceGlobalIntWithArray("reflect.namedNonBasicTypesSidetable", state.namedNonBasicTypesSidetable)
|
||||||
global.SetLinkage(llvm.InternalLinkage)
|
global.SetLinkage(llvm.InternalLinkage)
|
||||||
global.SetUnnamedAddr(true)
|
global.SetUnnamedAddr(true)
|
||||||
}
|
}
|
||||||
if state.needsStructTypesSidetable {
|
if state.needsStructTypesSidetable {
|
||||||
global := c.replaceGlobalByteWithArray("reflect.structTypesSidetable", state.structTypesSidetable)
|
global := c.replaceGlobalIntWithArray("reflect.structTypesSidetable", state.structTypesSidetable)
|
||||||
global.SetLinkage(llvm.InternalLinkage)
|
global.SetLinkage(llvm.InternalLinkage)
|
||||||
global.SetUnnamedAddr(true)
|
global.SetUnnamedAddr(true)
|
||||||
}
|
}
|
||||||
if state.needsStructNamesSidetable {
|
if state.needsStructNamesSidetable {
|
||||||
global := c.replaceGlobalByteWithArray("reflect.structNamesSidetable", state.structNamesSidetable)
|
global := c.replaceGlobalIntWithArray("reflect.structNamesSidetable", state.structNamesSidetable)
|
||||||
global.SetLinkage(llvm.InternalLinkage)
|
global.SetLinkage(llvm.InternalLinkage)
|
||||||
global.SetUnnamedAddr(true)
|
global.SetUnnamedAddr(true)
|
||||||
}
|
}
|
||||||
|
@ -194,49 +207,76 @@ func (state *typeCodeAssignmentState) getTypeCodeNum(typecode llvm.Value) *big.I
|
||||||
// (channel, interface, pointer, slice) just contain the bits of the
|
// (channel, interface, pointer, slice) just contain the bits of the
|
||||||
// wrapped type. Other types (like struct) need more fields and thus
|
// wrapped type. Other types (like struct) need more fields and thus
|
||||||
// cannot be encoded as a simple prefix.
|
// cannot be encoded as a simple prefix.
|
||||||
var num *big.Int
|
|
||||||
var classNumber int64
|
var classNumber int64
|
||||||
switch class {
|
if n, ok := nonBasicTypes[class]; ok {
|
||||||
case "chan":
|
classNumber = n
|
||||||
sub := llvm.ConstExtractValue(typecode.Initializer(), []uint32{0})
|
} else {
|
||||||
num = state.getTypeCodeNum(sub)
|
|
||||||
classNumber = 0
|
|
||||||
case "interface":
|
|
||||||
num = big.NewInt(int64(state.fallbackIndex))
|
|
||||||
state.fallbackIndex++
|
|
||||||
classNumber = 1
|
|
||||||
case "pointer":
|
|
||||||
sub := llvm.ConstExtractValue(typecode.Initializer(), []uint32{0})
|
|
||||||
num = state.getTypeCodeNum(sub)
|
|
||||||
classNumber = 2
|
|
||||||
case "slice":
|
|
||||||
sub := llvm.ConstExtractValue(typecode.Initializer(), []uint32{0})
|
|
||||||
num = state.getTypeCodeNum(sub)
|
|
||||||
classNumber = 3
|
|
||||||
case "array":
|
|
||||||
num = big.NewInt(int64(state.fallbackIndex))
|
|
||||||
state.fallbackIndex++
|
|
||||||
classNumber = 4
|
|
||||||
case "func":
|
|
||||||
num = big.NewInt(int64(state.fallbackIndex))
|
|
||||||
state.fallbackIndex++
|
|
||||||
classNumber = 5
|
|
||||||
case "map":
|
|
||||||
num = big.NewInt(int64(state.fallbackIndex))
|
|
||||||
state.fallbackIndex++
|
|
||||||
classNumber = 6
|
|
||||||
case "struct":
|
|
||||||
num = big.NewInt(int64(state.getStructTypeNum(typecode)))
|
|
||||||
classNumber = 7
|
|
||||||
default:
|
|
||||||
panic("unknown type kind: " + class)
|
panic("unknown type kind: " + class)
|
||||||
}
|
}
|
||||||
|
var num *big.Int
|
||||||
|
lowBits := (classNumber << 1) + 1 // the 5 low bits of the typecode
|
||||||
if name == "" {
|
if name == "" {
|
||||||
num.Lsh(num, 5).Or(num, big.NewInt((classNumber<<1)+1))
|
num = state.getNonBasicTypeCode(class, typecode)
|
||||||
} else {
|
} else {
|
||||||
num = big.NewInt(int64(state.getNonBasicNamedTypeNum(name, num))<<1 | 1)
|
// We must return a named type here. But first check whether it
|
||||||
num.Lsh(num, 4).Or(num, big.NewInt((classNumber<<1)+1))
|
// has already been defined.
|
||||||
|
if index, ok := state.namedNonBasicTypes[name]; ok {
|
||||||
|
num := big.NewInt(int64(index))
|
||||||
|
num.Lsh(num, 5).Or(num, big.NewInt((classNumber<<1)+1+(1<<4)))
|
||||||
|
return num
|
||||||
}
|
}
|
||||||
|
lowBits |= 1 << 4 // set the 'n' bit (see above)
|
||||||
|
if !state.needsNamedNonBasicTypesSidetable {
|
||||||
|
// Use simple small integers in this case, to make these numbers
|
||||||
|
// smaller.
|
||||||
|
index := len(state.namedNonBasicTypes) + 1
|
||||||
|
state.namedNonBasicTypes[name] = index
|
||||||
|
num = big.NewInt(int64(index))
|
||||||
|
} else {
|
||||||
|
// We need to store full type information.
|
||||||
|
// First allocate a number in the named non-basic type
|
||||||
|
// sidetable.
|
||||||
|
index := len(state.namedNonBasicTypesSidetable)
|
||||||
|
state.namedNonBasicTypesSidetable = append(state.namedNonBasicTypesSidetable, 0)
|
||||||
|
state.namedNonBasicTypes[name] = index
|
||||||
|
// Get the typecode of the underlying type (which could be the
|
||||||
|
// element type in the case of pointers, for example).
|
||||||
|
num = state.getNonBasicTypeCode(class, typecode)
|
||||||
|
if num.BitLen() > state.uintptrLen || !num.IsUint64() {
|
||||||
|
panic("cannot store value in sidetable")
|
||||||
|
}
|
||||||
|
// Now update the side table with the number we just
|
||||||
|
// determined. We need this multi-step approach to avoid stack
|
||||||
|
// overflow due to adding types recursively in the case of
|
||||||
|
// linked lists (a pointer which points to a struct that
|
||||||
|
// contains that same pointer).
|
||||||
|
state.namedNonBasicTypesSidetable[index] = num.Uint64()
|
||||||
|
num = big.NewInt(int64(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Concatenate the 'num' and 'lowBits' bitstrings.
|
||||||
|
num.Lsh(num, 5).Or(num, big.NewInt(lowBits))
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNonBasicTypeCode is used by getTypeCodeNum. It returns the upper bits of
|
||||||
|
// the type code used there in the type code.
|
||||||
|
func (state *typeCodeAssignmentState) getNonBasicTypeCode(class string, typecode llvm.Value) *big.Int {
|
||||||
|
switch class {
|
||||||
|
case "chan", "pointer", "slice":
|
||||||
|
// Prefix-style type kinds. The upper bits contain the element type.
|
||||||
|
sub := llvm.ConstExtractValue(typecode.Initializer(), []uint32{0})
|
||||||
|
return state.getTypeCodeNum(sub)
|
||||||
|
case "struct":
|
||||||
|
// More complicated type kind. The upper bits contain the index to the
|
||||||
|
// struct type in the struct types sidetable.
|
||||||
|
return big.NewInt(int64(state.getStructTypeNum(typecode)))
|
||||||
|
default:
|
||||||
|
// Type has not yet been implemented, so fall back by using a unique
|
||||||
|
// number.
|
||||||
|
num := big.NewInt(int64(state.fallbackIndex))
|
||||||
|
state.fallbackIndex++
|
||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,29 +312,6 @@ func (state *typeCodeAssignmentState) getBasicNamedTypeNum(name string) int {
|
||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNonBasicNamedTypeNum returns a number unique for this named type. It tries
|
|
||||||
// to return the smallest number possible to make encoding of this type code
|
|
||||||
// easier.
|
|
||||||
func (state *typeCodeAssignmentState) getNonBasicNamedTypeNum(name string, value *big.Int) int {
|
|
||||||
if num, ok := state.namedNonBasicTypes[name]; ok {
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
if !state.needsNamedNonBasicTypesSidetable {
|
|
||||||
// Use simple small integers in this case, to make these numbers
|
|
||||||
// smaller.
|
|
||||||
num := len(state.namedNonBasicTypes) + 1
|
|
||||||
state.namedNonBasicTypes[name] = num
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
num := len(state.namedNonBasicTypesSidetable)
|
|
||||||
if value.BitLen() > state.uintptrLen || !value.IsUint64() {
|
|
||||||
panic("cannot store value in sidetable")
|
|
||||||
}
|
|
||||||
state.namedNonBasicTypesSidetable = append(state.namedNonBasicTypesSidetable, makeVarint(value.Uint64())...)
|
|
||||||
state.namedNonBasicTypes[name] = num
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
|
|
||||||
// getStructTypeNum returns the struct type number, which is an index into
|
// getStructTypeNum returns the struct type number, which is an index into
|
||||||
// reflect.structTypesSidetable or an unique number for every struct if this
|
// reflect.structTypesSidetable or an unique number for every struct if this
|
||||||
// sidetable is not needed in the to-be-compiled program.
|
// sidetable is not needed in the to-be-compiled program.
|
||||||
|
|
|
@ -5,10 +5,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// This stores a varint for each named type. Named types are identified by their
|
// This stores a varint for each named type. Named types are identified by their
|
||||||
// name instead of by their type. The named types stored in this struct are the
|
// name instead of by their type. The named types stored in this struct are
|
||||||
// simpler non-basic types: pointer, struct, and channel.
|
// non-basic types: pointer, struct, and channel.
|
||||||
//go:extern reflect.namedNonBasicTypesSidetable
|
//go:extern reflect.namedNonBasicTypesSidetable
|
||||||
var namedNonBasicTypesSidetable byte
|
var namedNonBasicTypesSidetable uintptr
|
||||||
|
|
||||||
//go:extern reflect.structTypesSidetable
|
//go:extern reflect.structTypesSidetable
|
||||||
var structTypesSidetable byte
|
var structTypesSidetable byte
|
||||||
|
|
|
@ -163,7 +163,7 @@ func (t Type) stripPrefix() Type {
|
||||||
if (t>>4)%2 != 0 {
|
if (t>>4)%2 != 0 {
|
||||||
// This is a named type. The data is stored in a sidetable.
|
// This is a named type. The data is stored in a sidetable.
|
||||||
namedTypeNum := t >> 5
|
namedTypeNum := t >> 5
|
||||||
n, _ := readVarint(unsafe.Pointer(uintptr(unsafe.Pointer(&namedNonBasicTypesSidetable)) + uintptr(namedTypeNum)))
|
n := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&namedNonBasicTypesSidetable)) + uintptr(namedTypeNum)*unsafe.Sizeof(uintptr(0))))
|
||||||
return Type(n)
|
return Type(n)
|
||||||
}
|
}
|
||||||
// Not a named type, so the value is stored directly in the type code.
|
// Not a named type, so the value is stored directly in the type code.
|
||||||
|
|
7
testdata/reflect.go
предоставленный
7
testdata/reflect.go
предоставленный
|
@ -22,6 +22,10 @@ type (
|
||||||
buf []byte
|
buf []byte
|
||||||
Buf []byte
|
Buf []byte
|
||||||
}
|
}
|
||||||
|
linkedList struct {
|
||||||
|
next *linkedList `description:"chain"`
|
||||||
|
foo int
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -103,6 +107,9 @@ func main() {
|
||||||
c int8
|
c int8
|
||||||
}{42, 321, 123},
|
}{42, 321, 123},
|
||||||
mystruct{5, point{-5, 3}, struct{}{}, []byte{'G', 'o'}, []byte{'X'}},
|
mystruct{5, point{-5, 3}, struct{}{}, []byte{'G', 'o'}, []byte{'X'}},
|
||||||
|
&linkedList{
|
||||||
|
foo: 42,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
showValue(reflect.ValueOf(v), "")
|
showValue(reflect.ValueOf(v), "")
|
||||||
}
|
}
|
||||||
|
|
16
testdata/reflect.txt
предоставленный
16
testdata/reflect.txt
предоставленный
|
@ -293,6 +293,22 @@ reflect type: struct
|
||||||
indexing: 0
|
indexing: 0
|
||||||
reflect type: uint8 settable=true
|
reflect type: uint8 settable=true
|
||||||
uint: 88
|
uint: 88
|
||||||
|
reflect type: ptr
|
||||||
|
pointer: true struct
|
||||||
|
nil: false
|
||||||
|
reflect type: struct settable=true
|
||||||
|
struct: 2
|
||||||
|
field: 0 next
|
||||||
|
tag: description:"chain"
|
||||||
|
embedded: false
|
||||||
|
reflect type: ptr
|
||||||
|
pointer: false struct
|
||||||
|
nil: true
|
||||||
|
field: 1 foo
|
||||||
|
tag:
|
||||||
|
embedded: false
|
||||||
|
reflect type: int
|
||||||
|
int: 42
|
||||||
|
|
||||||
sizes:
|
sizes:
|
||||||
int8 1 8
|
int8 1 8
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче