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