123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- // Copyright 2014 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- // Package triegen implements a code generator for a trie for associating
- // unsigned integer values with UTF-8 encoded runes.
- //
- // Many of the go.text packages use tries for storing per-rune information. A
- // trie is especially useful if many of the runes have the same value. If this
- // is the case, many blocks can be expected to be shared allowing for
- // information on many runes to be stored in little space.
- //
- // As most of the lookups are done directly on []byte slices, the tries use the
- // UTF-8 bytes directly for the lookup. This saves a conversion from UTF-8 to
- // runes and contributes a little bit to better performance. It also naturally
- // provides a fast path for ASCII.
- //
- // Space is also an issue. There are many code points defined in Unicode and as
- // a result tables can get quite large. So every byte counts. The triegen
- // package automatically chooses the smallest integer values to represent the
- // tables. Compacters allow further compression of the trie by allowing for
- // alternative representations of individual trie blocks.
- //
- // triegen allows generating multiple tries as a single structure. This is
- // useful when, for example, one wants to generate tries for several languages
- // that have a lot of values in common. Some existing libraries for
- // internationalization store all per-language data as a dynamically loadable
- // chunk. The go.text packages are designed with the assumption that the user
- // typically wants to compile in support for all supported languages, in line
- // with the approach common to Go to create a single standalone binary. The
- // multi-root trie approach can give significant storage savings in this
- // scenario.
- //
- // triegen generates both tables and code. The code is optimized to use the
- // automatically chosen data types. The following code is generated for a Trie
- // or multiple Tries named "foo":
- // - type fooTrie
- // The trie type.
- //
- // - func newFooTrie(x int) *fooTrie
- // Trie constructor, where x is the index of the trie passed to Gen.
- //
- // - func (t *fooTrie) lookup(s []byte) (v uintX, sz int)
- // The lookup method, where uintX is automatically chosen.
- //
- // - func lookupString, lookupUnsafe and lookupStringUnsafe
- // Variants of the above.
- //
- // - var fooValues and fooIndex and any tables generated by Compacters.
- // The core trie data.
- //
- // - var fooTrieHandles
- // Indexes of starter blocks in case of multiple trie roots.
- //
- // It is recommended that users test the generated trie by checking the returned
- // value for every rune. Such exhaustive tests are possible as the number of
- // runes in Unicode is limited.
- package triegen // import "golang.org/x/text/internal/triegen"
- // TODO: Arguably, the internally optimized data types would not have to be
- // exposed in the generated API. We could also investigate not generating the
- // code, but using it through a package. We would have to investigate the impact
- // on performance of making such change, though. For packages like unicode/norm,
- // small changes like this could tank performance.
- import (
- "encoding/binary"
- "fmt"
- "hash/crc64"
- "io"
- "log"
- "unicode/utf8"
- )
- // builder builds a set of tries for associating values with runes. The set of
- // tries can share common index and value blocks.
- type builder struct {
- Name string
- // ValueType is the type of the trie values looked up.
- ValueType string
- // ValueSize is the byte size of the ValueType.
- ValueSize int
- // IndexType is the type of trie index values used for all UTF-8 bytes of
- // a rune except the last one.
- IndexType string
- // IndexSize is the byte size of the IndexType.
- IndexSize int
- // SourceType is used when generating the lookup functions. If the user
- // requests StringSupport, all lookup functions will be generated for
- // string input as well.
- SourceType string
- Trie []*Trie
- IndexBlocks []*node
- ValueBlocks [][]uint64
- Compactions []compaction
- Checksum uint64
- ASCIIBlock string
- StarterBlock string
- indexBlockIdx map[uint64]int
- valueBlockIdx map[uint64]nodeIndex
- asciiBlockIdx map[uint64]int
- // Stats are used to fill out the template.
- Stats struct {
- NValueEntries int
- NValueBytes int
- NIndexEntries int
- NIndexBytes int
- NHandleBytes int
- }
- err error
- }
- // A nodeIndex encodes the index of a node, which is defined by the compaction
- // which stores it and an index within the compaction. For internal nodes, the
- // compaction is always 0.
- type nodeIndex struct {
- compaction int
- index int
- }
- // compaction keeps track of stats used for the compaction.
- type compaction struct {
- c Compacter
- blocks []*node
- maxHandle uint32
- totalSize int
- // Used by template-based generator and thus exported.
- Cutoff uint32
- Offset uint32
- Handler string
- }
- func (b *builder) setError(err error) {
- if b.err == nil {
- b.err = err
- }
- }
- // An Option can be passed to Gen.
- type Option func(b *builder) error
- // Compact configures the trie generator to use the given Compacter.
- func Compact(c Compacter) Option {
- return func(b *builder) error {
- b.Compactions = append(b.Compactions, compaction{
- c: c,
- Handler: c.Handler() + "(n, b)"})
- return nil
- }
- }
- // Gen writes Go code for a shared trie lookup structure to w for the given
- // Tries. The generated trie type will be called nameTrie. newNameTrie(x) will
- // return the *nameTrie for tries[x]. A value can be looked up by using one of
- // the various lookup methods defined on nameTrie. It returns the table size of
- // the generated trie.
- func Gen(w io.Writer, name string, tries []*Trie, opts ...Option) (sz int, err error) {
- // The index contains two dummy blocks, followed by the zero block. The zero
- // block is at offset 0x80, so that the offset for the zero block for
- // continuation bytes is 0.
- b := &builder{
- Name: name,
- Trie: tries,
- IndexBlocks: []*node{{}, {}, {}},
- Compactions: []compaction{{
- Handler: name + "Values[n<<6+uint32(b)]",
- }},
- // The 0 key in indexBlockIdx and valueBlockIdx is the hash of the zero
- // block.
- indexBlockIdx: map[uint64]int{0: 0},
- valueBlockIdx: map[uint64]nodeIndex{0: {}},
- asciiBlockIdx: map[uint64]int{},
- }
- b.Compactions[0].c = (*simpleCompacter)(b)
- for _, f := range opts {
- if err := f(b); err != nil {
- return 0, err
- }
- }
- b.build()
- if b.err != nil {
- return 0, b.err
- }
- if err = b.print(w); err != nil {
- return 0, err
- }
- return b.Size(), nil
- }
- // A Trie represents a single root node of a trie. A builder may build several
- // overlapping tries at once.
- type Trie struct {
- root *node
- hiddenTrie
- }
- // hiddenTrie contains values we want to be visible to the template generator,
- // but hidden from the API documentation.
- type hiddenTrie struct {
- Name string
- Checksum uint64
- ASCIIIndex int
- StarterIndex int
- }
- // NewTrie returns a new trie root.
- func NewTrie(name string) *Trie {
- return &Trie{
- &node{
- children: make([]*node, blockSize),
- values: make([]uint64, utf8.RuneSelf),
- },
- hiddenTrie{Name: name},
- }
- }
- // Gen is a convenience wrapper around the Gen func passing t as the only trie
- // and uses the name passed to NewTrie. It returns the size of the generated
- // tables.
- func (t *Trie) Gen(w io.Writer, opts ...Option) (sz int, err error) {
- return Gen(w, t.Name, []*Trie{t}, opts...)
- }
- // node is a node of the intermediate trie structure.
- type node struct {
- // children holds this node's children. It is always of length 64.
- // A child node may be nil.
- children []*node
- // values contains the values of this node. If it is non-nil, this node is
- // either a root or leaf node:
- // For root nodes, len(values) == 128 and it maps the bytes in [0x00, 0x7F].
- // For leaf nodes, len(values) == 64 and it maps the bytes in [0x80, 0xBF].
- values []uint64
- index nodeIndex
- }
- // Insert associates value with the given rune. Insert will panic if a non-zero
- // value is passed for an invalid rune.
- func (t *Trie) Insert(r rune, value uint64) {
- if value == 0 {
- return
- }
- s := string(r)
- if []rune(s)[0] != r && value != 0 {
- // Note: The UCD tables will always assign what amounts to a zero value
- // to a surrogate. Allowing a zero value for an illegal rune allows
- // users to iterate over [0..MaxRune] without having to explicitly
- // exclude surrogates, which would be tedious.
- panic(fmt.Sprintf("triegen: non-zero value for invalid rune %U", r))
- }
- if len(s) == 1 {
- // It is a root node value (ASCII).
- t.root.values[s[0]] = value
- return
- }
- n := t.root
- for ; len(s) > 1; s = s[1:] {
- if n.children == nil {
- n.children = make([]*node, blockSize)
- }
- p := s[0] % blockSize
- c := n.children[p]
- if c == nil {
- c = &node{}
- n.children[p] = c
- }
- if len(s) > 2 && c.values != nil {
- log.Fatalf("triegen: insert(%U): found internal node with values", r)
- }
- n = c
- }
- if n.values == nil {
- n.values = make([]uint64, blockSize)
- }
- if n.children != nil {
- log.Fatalf("triegen: insert(%U): found leaf node that also has child nodes", r)
- }
- n.values[s[0]-0x80] = value
- }
- // Size returns the number of bytes the generated trie will take to store. It
- // needs to be exported as it is used in the templates.
- func (b *builder) Size() int {
- // Index blocks.
- sz := len(b.IndexBlocks) * blockSize * b.IndexSize
- // Skip the first compaction, which represents the normal value blocks, as
- // its totalSize does not account for the ASCII blocks, which are managed
- // separately.
- sz += len(b.ValueBlocks) * blockSize * b.ValueSize
- for _, c := range b.Compactions[1:] {
- sz += c.totalSize
- }
- // TODO: this computation does not account for the fixed overhead of a using
- // a compaction, either code or data. As for data, though, the typical
- // overhead of data is in the order of bytes (2 bytes for cases). Further,
- // the savings of using a compaction should anyway be substantial for it to
- // be worth it.
- // For multi-root tries, we also need to account for the handles.
- if len(b.Trie) > 1 {
- sz += 2 * b.IndexSize * len(b.Trie)
- }
- return sz
- }
- func (b *builder) build() {
- // Compute the sizes of the values.
- var vmax uint64
- for _, t := range b.Trie {
- vmax = maxValue(t.root, vmax)
- }
- b.ValueType, b.ValueSize = getIntType(vmax)
- // Compute all block allocations.
- // TODO: first compute the ASCII blocks for all tries and then the other
- // nodes. ASCII blocks are more restricted in placement, as they require two
- // blocks to be placed consecutively. Processing them first may improve
- // sharing (at least one zero block can be expected to be saved.)
- for _, t := range b.Trie {
- b.Checksum += b.buildTrie(t)
- }
- // Compute the offsets for all the Compacters.
- offset := uint32(0)
- for i := range b.Compactions {
- c := &b.Compactions[i]
- c.Offset = offset
- offset += c.maxHandle + 1
- c.Cutoff = offset
- }
- // Compute the sizes of indexes.
- // TODO: different byte positions could have different sizes. So far we have
- // not found a case where this is beneficial.
- imax := uint64(b.Compactions[len(b.Compactions)-1].Cutoff)
- for _, ib := range b.IndexBlocks {
- if x := uint64(ib.index.index); x > imax {
- imax = x
- }
- }
- b.IndexType, b.IndexSize = getIntType(imax)
- }
- func maxValue(n *node, max uint64) uint64 {
- if n == nil {
- return max
- }
- for _, c := range n.children {
- max = maxValue(c, max)
- }
- for _, v := range n.values {
- if max < v {
- max = v
- }
- }
- return max
- }
- func getIntType(v uint64) (string, int) {
- switch {
- case v < 1<<8:
- return "uint8", 1
- case v < 1<<16:
- return "uint16", 2
- case v < 1<<32:
- return "uint32", 4
- }
- return "uint64", 8
- }
- const (
- blockSize = 64
- // Subtract two blocks to offset 0x80, the first continuation byte.
- blockOffset = 2
- // Subtract three blocks to offset 0xC0, the first non-ASCII starter.
- rootBlockOffset = 3
- )
- var crcTable = crc64.MakeTable(crc64.ISO)
- func (b *builder) buildTrie(t *Trie) uint64 {
- n := t.root
- // Get the ASCII offset. For the first trie, the ASCII block will be at
- // position 0.
- hasher := crc64.New(crcTable)
- binary.Write(hasher, binary.BigEndian, n.values)
- hash := hasher.Sum64()
- v, ok := b.asciiBlockIdx[hash]
- if !ok {
- v = len(b.ValueBlocks)
- b.asciiBlockIdx[hash] = v
- b.ValueBlocks = append(b.ValueBlocks, n.values[:blockSize], n.values[blockSize:])
- if v == 0 {
- // Add the zero block at position 2 so that it will be assigned a
- // zero reference in the lookup blocks.
- // TODO: always do this? This would allow us to remove a check from
- // the trie lookup, but at the expense of extra space. Analyze
- // performance for unicode/norm.
- b.ValueBlocks = append(b.ValueBlocks, make([]uint64, blockSize))
- }
- }
- t.ASCIIIndex = v
- // Compute remaining offsets.
- t.Checksum = b.computeOffsets(n, true)
- // We already subtracted the normal blockOffset from the index. Subtract the
- // difference for starter bytes.
- t.StarterIndex = n.index.index - (rootBlockOffset - blockOffset)
- return t.Checksum
- }
- func (b *builder) computeOffsets(n *node, root bool) uint64 {
- // For the first trie, the root lookup block will be at position 3, which is
- // the offset for UTF-8 non-ASCII starter bytes.
- first := len(b.IndexBlocks) == rootBlockOffset
- if first {
- b.IndexBlocks = append(b.IndexBlocks, n)
- }
- // We special-case the cases where all values recursively are 0. This allows
- // for the use of a zero block to which all such values can be directed.
- hash := uint64(0)
- if n.children != nil || n.values != nil {
- hasher := crc64.New(crcTable)
- for _, c := range n.children {
- var v uint64
- if c != nil {
- v = b.computeOffsets(c, false)
- }
- binary.Write(hasher, binary.BigEndian, v)
- }
- binary.Write(hasher, binary.BigEndian, n.values)
- hash = hasher.Sum64()
- }
- if first {
- b.indexBlockIdx[hash] = rootBlockOffset - blockOffset
- }
- // Compacters don't apply to internal nodes.
- if n.children != nil {
- v, ok := b.indexBlockIdx[hash]
- if !ok {
- v = len(b.IndexBlocks) - blockOffset
- b.IndexBlocks = append(b.IndexBlocks, n)
- b.indexBlockIdx[hash] = v
- }
- n.index = nodeIndex{0, v}
- } else {
- h, ok := b.valueBlockIdx[hash]
- if !ok {
- bestI, bestSize := 0, blockSize*b.ValueSize
- for i, c := range b.Compactions[1:] {
- if sz, ok := c.c.Size(n.values); ok && bestSize > sz {
- bestI, bestSize = i+1, sz
- }
- }
- c := &b.Compactions[bestI]
- c.totalSize += bestSize
- v := c.c.Store(n.values)
- if c.maxHandle < v {
- c.maxHandle = v
- }
- h = nodeIndex{bestI, int(v)}
- b.valueBlockIdx[hash] = h
- }
- n.index = h
- }
- return hash
- }
|