blockio.go raw
1 // Block-level serialization for demand-paged growth. Each block can be
2 // frozen to disk and thawed back independently. The node skeleton (id +
3 // neighbor pointers) is never evicted — only the payload (constraints,
4 // occupant, candidates, projection state) is serialized and stripped.
5 //
6 // Format: binary, one file per block. Fixed header + N node records.
7 // Strings are uint16 length-prefixed UTF-8. All integers little-endian.
8 package grow
9
10 import (
11 "bufio"
12 "encoding/binary"
13 "fmt"
14 "io"
15 "os"
16 "path/filepath"
17
18 "git.mleku.dev/mleku/dendrite/pkg/axiom"
19 "git.mleku.dev/mleku/dendrite/pkg/enzyme"
20 "git.mleku.dev/mleku/dendrite/pkg/ratio"
21 "git.mleku.dev/mleku/dendrite/pkg/state"
22 )
23
24 const (
25 blockMagic = 0x424C4B30 // "BLK0"
26 blockVersion = 1
27 )
28
29 // blockHeader is the fixed-size file header for a block snapshot.
30 type blockHeader struct {
31 Magic uint32
32 Version uint16
33 BlockID uint32
34 NodeCount uint32
35 }
36
37 // nodeFlags encodes boolean fields into a single byte.
38 const (
39 nodeFlagOccupied = 1 << iota // bit 0: has occupant
40 nodeFlagCandidates // bit 1: has candidates
41 nodeFlagCrossLayer // bit 2: cross-layer bond
42 )
43
44 // blockFilePath returns the path for a block's snapshot file.
45 func blockFilePath(dir string, blockID int) string {
46 return filepath.Join(dir, fmt.Sprintf("blk%06d.dat", blockID))
47 }
48
49 // freezeBlock serializes a resident block's node payloads to disk.
50 // The node skeleton (id, neighbors) is not written — it persists in memory.
51 // Caller must have exclusive access to the block.
52 func freezeBlock(b *Block, dir string) error {
53 path := blockFilePath(dir, b.ID)
54 f, err := os.Create(path)
55 if err != nil {
56 return fmt.Errorf("freezeBlock %d: %w", b.ID, err)
57 }
58 w := bufio.NewWriter(f)
59
60 hdr := blockHeader{
61 Magic: blockMagic,
62 Version: blockVersion,
63 BlockID: uint32(b.ID),
64 NodeCount: uint32(len(b.Nodes)),
65 }
66 if err := binary.Write(w, binary.LittleEndian, &hdr); err != nil {
67 f.Close()
68 return fmt.Errorf("freezeBlock %d header: %w", b.ID, err)
69 }
70
71 for _, n := range b.Nodes {
72 if err := writeNodePayload(w, n); err != nil {
73 f.Close()
74 return fmt.Errorf("freezeBlock %d node %d: %w", b.ID, n.ID(), err)
75 }
76 }
77
78 if err := w.Flush(); err != nil {
79 f.Close()
80 return err
81 }
82 return f.Close()
83 }
84
85 // thawBlock loads node payloads from disk and restores them onto the
86 // existing (stripped) node skeleton. The constraint factory reconstructs
87 // Constraint objects from tag strings.
88 // Caller must have exclusive access to the block.
89 func thawBlock(b *Block, dir string, cf func(string) axiom.Constraint) error {
90 path := blockFilePath(dir, b.ID)
91 f, err := os.Open(path)
92 if err != nil {
93 return fmt.Errorf("thawBlock %d: %w", b.ID, err)
94 }
95 defer f.Close()
96 r := bufio.NewReader(f)
97
98 var hdr blockHeader
99 if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil {
100 return fmt.Errorf("thawBlock %d header: %w", b.ID, err)
101 }
102 if hdr.Magic != blockMagic {
103 return fmt.Errorf("thawBlock %d: bad magic %08x", b.ID, hdr.Magic)
104 }
105 if hdr.Version != blockVersion {
106 return fmt.Errorf("thawBlock %d: unsupported version %d", b.ID, hdr.Version)
107 }
108 if int(hdr.NodeCount) != len(b.Nodes) {
109 return fmt.Errorf("thawBlock %d: node count mismatch: file=%d block=%d",
110 b.ID, hdr.NodeCount, len(b.Nodes))
111 }
112
113 for i, n := range b.Nodes {
114 if err := readNodePayload(r, n, cf); err != nil {
115 return fmt.Errorf("thawBlock %d node %d: %w", b.ID, i, err)
116 }
117 }
118 return nil
119 }
120
121 // writeNodePayload serializes one node's payload fields.
122 func writeNodePayload(w io.Writer, n interface {
123 Occupant() axiom.Element
124 Candidates() []axiom.Element
125 Constraints() []axiom.Constraint
126 Hexagram() state.Hexagram
127 LockIn() ratio.Ratio
128 BondCount() int
129 CrossLayer() bool
130 Permutation() uint8
131 ProjectionVertex() uint8
132 ProjectionKey() uint8
133 ProjectionPath() uint16
134 Age() uint8
135 }) error {
136 // Gather state via public accessors (locked, but no contention between rounds).
137 occ := n.Occupant()
138 cands := n.Candidates()
139 constraints := n.Constraints()
140 hex := n.Hexagram()
141 lockIn := n.LockIn()
142 bondCount := n.BondCount()
143 crossLayer := n.CrossLayer()
144 perm := n.Permutation()
145 projVertex := n.ProjectionVertex()
146 projKey := n.ProjectionKey()
147 projPath := n.ProjectionPath()
148 age := n.Age()
149
150 // Flags byte.
151 var flags uint8
152 if occ != nil {
153 flags |= nodeFlagOccupied
154 }
155 if len(cands) > 0 {
156 flags |= nodeFlagCandidates
157 }
158 if crossLayer {
159 flags |= nodeFlagCrossLayer
160 }
161
162 // Fixed fields: flags, hex, perm, projVertex, projKey, projPath, age,
163 // bondCount, lockIn (num + den).
164 fixed := []any{
165 flags,
166 uint8(hex),
167 perm,
168 projVertex,
169 projKey,
170 projPath,
171 age,
172 uint8(bondCount),
173 lockIn.Num,
174 lockIn.Denom,
175 }
176 for _, v := range fixed {
177 if err := binary.Write(w, binary.LittleEndian, v); err != nil {
178 return err
179 }
180 }
181
182 // Constraint tags.
183 if err := writeUint16(w, uint16(len(constraints))); err != nil {
184 return err
185 }
186 for _, c := range constraints {
187 if err := writeString(w, c.Tag()); err != nil {
188 return err
189 }
190 }
191
192 // Occupant.
193 if occ != nil {
194 if err := writeString(w, occ.Type()); err != nil {
195 return err
196 }
197 if err := writeString(w, fmt.Sprintf("%v", occ.Value())); err != nil {
198 return err
199 }
200 }
201
202 // Candidates.
203 if len(cands) > 0 {
204 if err := writeUint16(w, uint16(len(cands))); err != nil {
205 return err
206 }
207 for _, c := range cands {
208 if err := writeString(w, c.Type()); err != nil {
209 return err
210 }
211 if err := writeString(w, fmt.Sprintf("%v", c.Value())); err != nil {
212 return err
213 }
214 }
215 }
216
217 return nil
218 }
219
220 // readNodePayload deserializes one node's payload fields and restores them.
221 func readNodePayload(r io.Reader, n interface {
222 RestoreConstraintsUnsafe([]axiom.Constraint)
223 ForceOccupant(axiom.Element, int)
224 AddCandidate(axiom.Element) bool
225 RestoreAge(uint8)
226 SetPermutation(uint8)
227 SetProjection(uint8, uint8, uint16)
228 SetEnergy(bool)
229 SetOuterTrigram(state.Trigram)
230 }, cf func(string) axiom.Constraint) error {
231 var flags, hex, perm, projVertex, projKey, age, bondCount uint8
232 var projPath uint16
233 var lockInNum, lockInDen int64
234
235 for _, v := range []any{
236 &flags, &hex, &perm, &projVertex, &projKey, &projPath, &age,
237 &bondCount, &lockInNum, &lockInDen,
238 } {
239 if err := binary.Read(r, binary.LittleEndian, v); err != nil {
240 return err
241 }
242 }
243
244 // Constraints.
245 tagCount, err := readUint16(r)
246 if err != nil {
247 return err
248 }
249 constraints := make([]axiom.Constraint, tagCount)
250 for i := range constraints {
251 tag, err := readString(r)
252 if err != nil {
253 return err
254 }
255 constraints[i] = cf(tag)
256 }
257 n.RestoreConstraintsUnsafe(constraints)
258
259 // Occupant.
260 if flags&nodeFlagOccupied != 0 {
261 typ, err := readString(r)
262 if err != nil {
263 return err
264 }
265 val, err := readString(r)
266 if err != nil {
267 return err
268 }
269 n.ForceOccupant(enzyme.Elem(typ, val), int(bondCount))
270 }
271
272 // Candidates.
273 if flags&nodeFlagCandidates != 0 {
274 candCount, err := readUint16(r)
275 if err != nil {
276 return err
277 }
278 for range candCount {
279 typ, err := readString(r)
280 if err != nil {
281 return err
282 }
283 val, err := readString(r)
284 if err != nil {
285 return err
286 }
287 n.AddCandidate(enzyme.Elem(typ, val))
288 }
289 }
290
291 // Restore scalar state.
292 n.RestoreAge(age)
293 n.SetPermutation(perm)
294 n.SetProjection(projVertex, projKey, projPath)
295
296 // Restore hexagram.
297 h := state.Hexagram(hex)
298 n.SetEnergy(h.Inner().Energy())
299 n.SetOuterTrigram(h.Outer())
300
301 return nil
302 }
303
304 // writeString writes a uint16 length-prefixed UTF-8 string.
305 func writeString(w io.Writer, s string) error {
306 if len(s) > 0xFFFF {
307 s = s[:0xFFFF]
308 }
309 if err := writeUint16(w, uint16(len(s))); err != nil {
310 return err
311 }
312 _, err := io.WriteString(w, s)
313 return err
314 }
315
316 // readString reads a uint16 length-prefixed UTF-8 string.
317 func readString(r io.Reader) (string, error) {
318 n, err := readUint16(r)
319 if err != nil {
320 return "", err
321 }
322 if n == 0 {
323 return "", nil
324 }
325 buf := make([]byte, n)
326 _, err = io.ReadFull(r, buf)
327 return string(buf), err
328 }
329
330 // writeUint16 writes a little-endian uint16.
331 func writeUint16(w io.Writer, v uint16) error {
332 return binary.Write(w, binary.LittleEndian, v)
333 }
334
335 // readUint16 reads a little-endian uint16.
336 func readUint16(r io.Reader) (uint16, error) {
337 var v uint16
338 err := binary.Read(r, binary.LittleEndian, &v)
339 return v, err
340 }
341
342 // removeBlockFile deletes a block's snapshot from disk.
343 func removeBlockFile(dir string, blockID int) error {
344 return os.Remove(blockFilePath(dir, blockID))
345 }
346