generated code size: union initialization produces 15x bloat for data-heavy projects
Summary
ccgo-generated Go code is overwhelmingly data initialization (99%+) due to verbose union initialization in initializerUnion. For tree-sitter grammars this produces Go files that are 1.5-2x the C source, mostly from repetitive inline struct literals.
Real-world example: tree-sitter-python
| Metric | Value |
|---|---|
| C source lines | 130,440 |
| Generated Go lines | 243,736 |
Data portion (before init()) |
242,872 (99.6%) |
Code portion (after init()) |
864 (0.4%) |
Four static arrays account for 91% of the file:
| Array | Elements | Type | Lines |
|---|---|---|---|
_ts_small_parse_table |
118,009 | uint16 |
117,908 |
_ts_parse_actions |
5,042 | TTSParseActionEntry (union) |
73,934 |
_ts_parse_table |
— | — | 18,313 |
_ts_lex_modes |
— | — | 12,202 |
The LR parse table is the dominant consumer regardless of language — every tree-sitter grammar has one.
Root cause
initializerUnion (v4/lib/init.go:378) generates a full inline struct definition + field assignments + unsafe.Pointer wrapper for every single union element:
// Each union element: 15+ lines
3: *(*TTSParseActionEntry)(unsafe.Pointer(&struct {
_ [0]byte
f struct {
Fcount Tuint8_t
Freusable uint8
}
_ [6]byte
}{f: struct {
Fcount Tuint8_t
Freusable uint8
}{
Fcount: uint8(1),
Freusable: libc.BoolUint8(true1 != 0),
}})),For TTSParseActionEntry (4 bytes actual), this expands to 15+ lines per element — a 15:1 bloat ratio.
Proposed solutions
Phase 1: Compact union byte literals (minimal ccgo change)
Modify initializerUnion to emit a compact byte array instead of inline struct:
// Before (15 lines):
*(*TTSParseActionEntry)(unsafe.Pointer(&struct{_ [0]byte; f struct{...}; _ [6]byte}{...}))
// After (1 line):
*(*TTSParseActionEntry)(unsafe.Pointer(&[4]byte{0x01, 0x00, 0x00, 0x00}))Since ccgo already knows all type sizes and field offsets, computing the exact byte representation is straightforward. This alone would reduce tree-sitter-python.go from 243K → ~58K lines (76% reduction).
Phase 2: -compact-data flag with //go:embed (larger feature)
For arrays above a size threshold, write data as binary companion files and embed at compile time:
// Instead of 242K lines of data:
//go:embed _ts_parse_actions.bin
var _ts_parse_actions_data []byte
var _ts_parse_actions = (*[5042]TTSParseActionEntry)(unsafe.Pointer(&_ts_parse_actions_data[0]))This would reduce the Go file to ~15K lines (94% reduction) and also speed up compilation by removing massive literal initialization from the Go compiler's workload.
Phase 1 implementation sketch
The key insight is that initializerUnion already produces correct values — it just produces them in a verbose form. Converting to byte literals needs:
- A helper that walks the union's active member and writes each field's value as bytes at the correct offset
- Use
binary.LittleEndian/binary.BigEndianper target ABI for multi-byte fields - Handle nested structs/arrays recursively
Questions for maintainer
- Would you accept a Phase 1 PR (compact union byte literals)?
- Is
-compact-datawith//go:embedsomething you'd want in ccgo itself, or would it be better as a post-processing tool? - Are there ABI considerations (endianness, alignment) that the byte-literal approach must handle that the current struct-literal approach implicitly gets right?
Discovered while converting tree-sitter grammars to cgo-free Go with ccgo: tree-sitter, tree-sitter-c, tree-sitter-python. MR !24 (merged) fixed the blocking exprUintptr bug; this issue addresses the next scalability concern.