soft3/tape/spec/1-wire-format.md

TAPE — Wire Format

Frame structure

Every TAPE frame has the following layout:

 0        1        2…           N…
 ┌────────┬────────┬─────────────┬────────────────┐
 │ marker │  type  │  size       │  data          │
 └────────┴────────┴─────────────┴────────────────┘
 0x1F      1 byte   1–10 bytes    N bytes

marker

The marker byte is 0x1F (ASCII Unit Separator). It was chosen because:

  • It never appears in valid UTF-8 encoded text (multibyte lead bytes are 0xC0–0xFF)
  • It is not in the Nox ISA instruction range (0x01–0x1E)
  • It is not a printable ASCII character (0x20–0x7E)
  • It is not used by any standard terminal control sequence

A decoder scanning for the start of a frame MUST look for 0x1F. Bytes before the first 0x1F in a stream MUST be discarded without error (allows plain-text preambles and mixed streams).

type

One byte identifying the particle kind of this frame. The meaning of each type value is defined by the active dialect (see 3-catalog-protocol.md). The value 0x2A (*) is reserved by TAPE for stream control (see 2-stream-control.md). A decoder that receives an unknown type MUST skip the frame (advance past the data) rather than failing.

size

Unsigned data length encoded as LEB128 (Little-Endian Base 128):

  • Each byte contributes 7 bits of the value, LSB first.
  • The high bit of each byte is 1 if more bytes follow, 0 for the last byte.
  • Maximum encoded value: 2^63 − 1 (10 bytes maximum).
  • A decoder that reaches 10 bytes without a terminating byte MUST treat the frame as malformed and skip forward to the next 0x1F.

Examples:

Value Bytes
0 00
1 01
127 7F
128 80 01
300 AC 02
16383 FF 7F
16384 80 80 01

data

Exactly N bytes as given by size. The data is opaque to the framing layer — its interpretation depends on the type byte and the active dialect. For composite particle types (component, table, struct), the data is itself a sequence of valid TAPE frames that can be decoded with the same parser.

Encoding algorithm

fn encode(type: u8, data: &[u8]) -> Vec<u8> {
    let mut out = vec![0x1F, type];
    encode_leb128(data.len() as u64, &mut out);
    out.extend_from_slice(data);
    out
}

Decoding algorithm

fn decode_next(buf: &[u8], pos: &mut usize) -> Option<Frame> {
    // scan for marker
    let start = buf[*pos..].iter().position(|&b| b == 0x1F)?;
    *pos += start;
    if buf.len() - *pos < 3 { return None; }  // need marker + type + size(min 1)
    let frame_type = buf[*pos + 1];
    *pos += 2;
    let (data_len, new_pos) = decode_leb128(buf, *pos)?;
    *pos = new_pos;
    if buf.len() - *pos < data_len { return None; }  // incomplete
    let data = &buf[*pos .. *pos + data_len];
    *pos += data_len;
    Some(Frame { frame_type, data })
}

A decoder MUST handle:

  • Multiple frames concatenated end-to-end in a single buffer
  • Frames split across multiple feed() calls (incremental / streaming)
  • Unknown type: skip (advance past data) without error
  • Truncated frame at end of buffer: return Pending, not an error

Minimum overhead

Data size Total frame bytes
0 3 (marker + type + 0x00)
1–127 4
128–16383 5
16384–2097151 6

A 100-byte particle costs 4 bytes of overhead (4%).

Graph