Skip to main content

logicaffeine_compile/codegen_sva/
chip_protocol.rs

1//! The chips' byte protocol on the UART, defined once.
2//!
3//! The whole-program engine talks to its host over one byte channel: it takes the program image, streams a
4//! record for every `Show` as the program runs, sends a request and waits for the reply when the program reaches
5//! the outside world (a host call), and ends with a result frame. Every byte value in that exchange is defined
6//! here and nowhere else. The engine model, the renderer and the host bridge use these constants, and
7//! `scripts/logos-chip.py` reads the same table as JSON from the `chip_host` example ([`table_json`]).
8//!
9//! | record | direction | bytes |
10//! |---|---|---|
11//! | program image | host → chip | the config memories and the params, then [`IMAGE_GO`] |
12//! | show | chip → host | [`REC_SHOW`], a value |
13//! | call | chip → host | [`REC_CALL`], the [`HostCall`] code, the argument count, that many values |
14//! | reply | host → chip | [`REC_REPLY`], a value |
15//! | result frame | chip → host | [`FRAME_SYNC`], result `[63:0]`, cycles `[31:0]`, one end byte |
16//!
17//! A program that pre-loads its heap sends [`IMAGE_HEAP`] and the pre-load between the params and [`IMAGE_GO`].
18//! A value is its tag, then 8 bytes most significant first, except a Text: a 4-byte length, most significant
19//! first, and that many UTF-8 bytes. The end byte of the result frame is [`FRAME_END`] for a finished run,
20//! [`FRAME_END_CALL_STACK`] when the call stack overflowed and [`FRAME_END_HEAP`] when the heap did. Neither
21//! overflow's result bytes are an answer.
22
23/// The program image's heap pre-load segment follows.
24pub const IMAGE_HEAP: u8 = 0x48;
25/// The program image is complete; the engine starts the run.
26pub const IMAGE_GO: u8 = 0x5A;
27
28/// A `Show` record.
29pub const REC_SHOW: u8 = 0xA7;
30/// A host call's request.
31pub const REC_CALL: u8 = 0xA8;
32/// The host's reply to a host call.
33pub const REC_REPLY: u8 = 0xA9;
34
35/// The first byte of the result frame.
36pub const FRAME_SYNC: u8 = 0xA5;
37/// The result frame of a run that finished.
38pub const FRAME_END: u8 = 0x5A;
39/// The result frame of a run whose call stack overflowed the engine's frame stack.
40pub const FRAME_END_CALL_STACK: u8 = 0xE1;
41/// The result frame of a run whose heap overflowed the engine's heap.
42pub const FRAME_END_HEAP: u8 = 0xE2;
43
44/// A signed 64-bit Int.
45pub const TAG_INT: u8 = 0;
46/// A Real: its IEEE-754 bit pattern, rendered with the language's `fmt_f64`.
47pub const TAG_FLOAT: u8 = 1;
48/// A Text. On the engine, the shown value is a heap pointer to a char-list `[len, byte0..]`; on the wire, the
49/// length and the bytes.
50pub const TAG_TEXT: u8 = 2;
51/// A `Word32`, rendered unsigned.
52pub const TAG_WORD32: u8 = 3;
53/// A Bool, carried as 0 or 1.
54pub const TAG_BOOL: u8 = 4;
55
56/// Every value tag, in code order.
57pub const TAGS: [u8; 5] = [TAG_INT, TAG_FLOAT, TAG_TEXT, TAG_WORD32, TAG_BOOL];
58
59/// The program's operations that reach the outside world through the host. The host services each with the one
60/// relay client (`logicaffeine_system::relay`); the chip never frames the relay protocol itself.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
62#[repr(u8)]
63pub enum HostCall {
64    /// `Listen on <topic>`.
65    Listen = 1,
66    /// `Connect to <address>`.
67    Connect = 2,
68    /// `Send <value> to <peer>`.
69    Send = 3,
70    /// `Await response from <peer> into <name>`.
71    Await = 4,
72    /// `Stream <values> to <peer>`.
73    Stream = 5,
74    /// `Await stream from <peer> into <name>`.
75    AwaitStream = 6,
76    /// `Sync <value> on <topic>`.
77    Sync = 7,
78}
79
80impl HostCall {
81    /// Every host call, in code order.
82    pub const ALL: [HostCall; 7] = [
83        HostCall::Listen,
84        HostCall::Connect,
85        HostCall::Send,
86        HostCall::Await,
87        HostCall::Stream,
88        HostCall::AwaitStream,
89        HostCall::Sync,
90    ];
91
92    /// The byte a call record carries.
93    pub fn code(self) -> u8 {
94        self as u8
95    }
96
97    /// The call a record's byte names.
98    pub fn from_code(code: u8) -> Option<HostCall> {
99        HostCall::ALL.into_iter().find(|c| c.code() == code)
100    }
101
102    /// The call's name in the table.
103    pub fn name(self) -> &'static str {
104        match self {
105            HostCall::Listen => "listen",
106            HostCall::Connect => "connect",
107            HostCall::Send => "send",
108            HostCall::Await => "await",
109            HostCall::Stream => "stream",
110            HostCall::AwaitStream => "await_stream",
111            HostCall::Sync => "sync",
112        }
113    }
114}
115
116/// A value as the protocol carries it.
117#[derive(Clone, Debug, PartialEq)]
118pub enum ChipValue {
119    Int(i64),
120    /// The bit pattern, so every NaN payload and the sign of zero survive the trip.
121    Float(u64),
122    Text(String),
123    Word32(u32),
124    Bool(bool),
125}
126
127impl ChipValue {
128    /// The value's tag.
129    pub fn tag(&self) -> u8 {
130        match self {
131            ChipValue::Int(_) => TAG_INT,
132            ChipValue::Float(_) => TAG_FLOAT,
133            ChipValue::Text(_) => TAG_TEXT,
134            ChipValue::Word32(_) => TAG_WORD32,
135            ChipValue::Bool(_) => TAG_BOOL,
136        }
137    }
138
139    /// Append the value's framing: the tag, then the payload.
140    pub fn encode(&self, out: &mut Vec<u8>) {
141        out.push(self.tag());
142        match self {
143            ChipValue::Int(v) => out.extend_from_slice(&v.to_be_bytes()),
144            ChipValue::Float(bits) => out.extend_from_slice(&bits.to_be_bytes()),
145            ChipValue::Word32(w) => out.extend_from_slice(&u64::from(*w).to_be_bytes()),
146            ChipValue::Bool(b) => out.extend_from_slice(&u64::from(*b).to_be_bytes()),
147            ChipValue::Text(s) => {
148                out.extend_from_slice(&(s.len() as u32).to_be_bytes());
149                out.extend_from_slice(s.as_bytes());
150            }
151        }
152    }
153
154    /// Decode one value from the front of `bytes`, returning it and the bytes it took.
155    pub fn decode(bytes: &[u8]) -> Result<(ChipValue, usize), FrameError> {
156        let tag = *bytes.first().ok_or(FrameError::Short)?;
157        let word = |at: usize| -> Result<u64, FrameError> {
158            let b: [u8; 8] = bytes
159                .get(at..at + 8)
160                .ok_or(FrameError::Short)?
161                .try_into()
162                .expect("8 bytes");
163            Ok(u64::from_be_bytes(b))
164        };
165        match tag {
166            TAG_INT => Ok((ChipValue::Int(word(1)? as i64), 9)),
167            TAG_FLOAT => Ok((ChipValue::Float(word(1)?), 9)),
168            TAG_WORD32 => {
169                let w = word(1)?;
170                let w = u32::try_from(w)
171                    .map_err(|_| FrameError::Malformed(format!("a Word32 of {w:#x}")))?;
172                Ok((ChipValue::Word32(w), 9))
173            }
174            TAG_BOOL => match word(1)? {
175                0 => Ok((ChipValue::Bool(false), 9)),
176                1 => Ok((ChipValue::Bool(true), 9)),
177                w => Err(FrameError::Malformed(format!("a Bool of {w}"))),
178            },
179            TAG_TEXT => {
180                let len: [u8; 4] = bytes
181                    .get(1..5)
182                    .ok_or(FrameError::Short)?
183                    .try_into()
184                    .expect("4 bytes");
185                let len = u32::from_be_bytes(len) as usize;
186                let text = bytes.get(5..5 + len).ok_or(FrameError::Short)?;
187                let text = String::from_utf8(text.to_vec())
188                    .map_err(|_| FrameError::Malformed("a Text that is not UTF-8".to_string()))?;
189                Ok((ChipValue::Text(text), 5 + len))
190            }
191            t => Err(FrameError::Malformed(format!("value tag {t}"))),
192        }
193    }
194}
195
196/// One record of the exchange.
197#[derive(Clone, Debug, PartialEq)]
198pub enum Record {
199    Show(ChipValue),
200    Call {
201        call: HostCall,
202        args: Vec<ChipValue>,
203    },
204    Reply(ChipValue),
205}
206
207/// Why bytes are not a record.
208#[derive(Clone, Debug, PartialEq, Eq)]
209pub enum FrameError {
210    /// The bytes end before the record does.
211    Short,
212    /// The bytes are not a record: an unknown marker, tag or call, or a payload out of its type's range.
213    Malformed(String),
214}
215
216impl Record {
217    /// The record's bytes.
218    pub fn encode(&self) -> Vec<u8> {
219        let mut out = Vec::new();
220        match self {
221            Record::Show(v) => {
222                out.push(REC_SHOW);
223                v.encode(&mut out);
224            }
225            Record::Call { call, args } => {
226                out.push(REC_CALL);
227                out.push(call.code());
228                out.push(
229                    u8::try_from(args.len()).expect("a host call carries at most 255 arguments"),
230                );
231                for a in args {
232                    a.encode(&mut out);
233                }
234            }
235            Record::Reply(v) => {
236                out.push(REC_REPLY);
237                v.encode(&mut out);
238            }
239        }
240        out
241    }
242
243    /// Decode one record from the front of `bytes`, returning it and the bytes it took.
244    pub fn decode(bytes: &[u8]) -> Result<(Record, usize), FrameError> {
245        let marker = *bytes.first().ok_or(FrameError::Short)?;
246        match marker {
247            REC_SHOW => {
248                let (v, n) = ChipValue::decode(&bytes[1..])?;
249                Ok((Record::Show(v), 1 + n))
250            }
251            REC_REPLY => {
252                let (v, n) = ChipValue::decode(&bytes[1..])?;
253                Ok((Record::Reply(v), 1 + n))
254            }
255            REC_CALL => {
256                let code = *bytes.get(1).ok_or(FrameError::Short)?;
257                let call = HostCall::from_code(code)
258                    .ok_or_else(|| FrameError::Malformed(format!("host call {code}")))?;
259                let argc = *bytes.get(2).ok_or(FrameError::Short)? as usize;
260                let mut at = 3;
261                let mut args = Vec::with_capacity(argc);
262                for _ in 0..argc {
263                    let (v, n) = ChipValue::decode(&bytes[at..])?;
264                    args.push(v);
265                    at += n;
266                }
267                Ok((Record::Call { call, args }, at))
268            }
269            m => Err(FrameError::Malformed(format!("record marker {m:#04x}"))),
270        }
271    }
272}
273
274/// The result frame that closes a run.
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub struct ResultFrame {
277    pub result: i64,
278    pub cycles: u32,
279    /// [`FRAME_END`], [`FRAME_END_CALL_STACK`] or [`FRAME_END_HEAP`].
280    pub end: u8,
281}
282
283impl ResultFrame {
284    /// The frame's bytes.
285    pub fn encode(&self) -> Vec<u8> {
286        let mut out = vec![FRAME_SYNC];
287        out.extend_from_slice(&self.result.to_be_bytes());
288        out.extend_from_slice(&self.cycles.to_be_bytes());
289        out.push(self.end);
290        out
291    }
292
293    /// Decode a frame from the front of `bytes`, returning it and the bytes it took.
294    pub fn decode(bytes: &[u8]) -> Result<(ResultFrame, usize), FrameError> {
295        let sync = *bytes.first().ok_or(FrameError::Short)?;
296        if sync != FRAME_SYNC {
297            return Err(FrameError::Malformed(format!("frame sync {sync:#04x}")));
298        }
299        let body = bytes.get(1..14).ok_or(FrameError::Short)?;
300        let result = i64::from_be_bytes(body[0..8].try_into().expect("8 bytes"));
301        let cycles = u32::from_be_bytes(body[8..12].try_into().expect("4 bytes"));
302        let end = body[12];
303        if ![FRAME_END, FRAME_END_CALL_STACK, FRAME_END_HEAP].contains(&end) {
304            return Err(FrameError::Malformed(format!("frame end {end:#04x}")));
305        }
306        Ok((
307            ResultFrame {
308                result,
309                cycles,
310                end,
311            },
312            14,
313        ))
314    }
315}
316
317/// The protocol bytes the engine's Verilog templates name, by the placeholder name they use.
318const VERILOG_BYTES: [(&str, u8); 8] = [
319    ("IMAGE_HEAP", IMAGE_HEAP),
320    ("IMAGE_GO", IMAGE_GO),
321    ("REC_SHOW", REC_SHOW),
322    ("REC_CALL", REC_CALL),
323    ("FRAME_SYNC", FRAME_SYNC),
324    ("FRAME_END", FRAME_END),
325    ("FRAME_END_CALL_STACK", FRAME_END_CALL_STACK),
326    ("FRAME_END_HEAP", FRAME_END_HEAP),
327];
328
329/// Fill a Verilog template's protocol bytes from this table: `__V_<NAME>__` becomes the byte as a Verilog literal
330/// (`8'hA7`), `__X_<NAME>__` the hex a comment quotes (`0xA7`). Panics if a placeholder names no protocol byte.
331pub fn fill_verilog(template: &str) -> String {
332    let mut out = template.to_string();
333    for (name, byte) in VERILOG_BYTES {
334        out = out.replace(&format!("__V_{name}__"), &format!("8'h{byte:02X}"));
335        out = out.replace(&format!("__X_{name}__"), &format!("0x{byte:02X}"));
336    }
337    if let Some(at) = out.find("__V_").or_else(|| out.find("__X_")) {
338        let name: String = out[at..]
339            .chars()
340            .take_while(|c| !c.is_whitespace())
341            .collect();
342        panic!("the Verilog template names a protocol byte chip_protocol does not define: {name}");
343    }
344    out
345}
346
347/// The whole table as JSON, for a host that is not Rust: `scripts/logos-chip.py` reads it from
348/// `chip_host --table` rather than restating a byte of it.
349pub fn table_json() -> String {
350    let tags = [
351        ("int", TAG_INT),
352        ("float", TAG_FLOAT),
353        ("text", TAG_TEXT),
354        ("word32", TAG_WORD32),
355        ("bool", TAG_BOOL),
356    ];
357    let tags: Vec<String> = tags.iter().map(|(n, v)| format!("\"{n}\": {v}")).collect();
358    let calls: Vec<String> = HostCall::ALL
359        .iter()
360        .map(|c| format!("\"{}\": {}", c.name(), c.code()))
361        .collect();
362    format!(
363        "{{\"image\": {{\"heap\": {IMAGE_HEAP}, \"go\": {IMAGE_GO}}}, \
364         \"records\": {{\"show\": {REC_SHOW}, \"call\": {REC_CALL}, \"reply\": {REC_REPLY}}}, \
365         \"frame\": {{\"sync\": {FRAME_SYNC}, \"end\": {FRAME_END}, \"end_call_stack\": {FRAME_END_CALL_STACK}, \
366         \"end_heap\": {FRAME_END_HEAP}}}, \"tags\": {{{}}}, \"calls\": {{{}}}}}",
367        tags.join(", "),
368        calls.join(", ")
369    )
370}