Skip to main content

logicaffeine_compile/codegen_sva/
onboard.rs

1//! The compiler as the board's own CPU runs it: LOGOS source → register bytecode → the fabric engine's wire
2//! image, through exactly the path the laptop's board hosts take ([`super::engine_build`]), with the VM's golden
3//! proven against the engine model before a byte of the image exists. The board runs a
4//! `riscv64gc-unknown-linux-musl` build of this same code; `onboard_compiler_lock` holds its every answer
5//! byte-identical to the host build's.
6
7use super::chip_protocol::{
8    ChipValue, FrameError, Record, ResultFrame, FRAME_END, FRAME_END_CALL_STACK, FRAME_SYNC, REC_CALL, REC_SHOW,
9    TAG_BOOL, TAG_FLOAT, TAG_INT, TAG_TEXT, TAG_WORD32,
10};
11use super::engine_build::{fits_caps, lower, lower_main, Lifter, Profile};
12use super::wprog_engine_rtl::{
13    render_show, wprog_config_image_unchecked, WShow, WPE_HW_DEPTH, WPE_HW_HEAP, WPE_HW_MAXARG, WPE_HW_MAXBLK,
14    WPE_HW_MAXCELL, WPE_HW_MAXK, WPE_HW_MAXLIV, WPE_HW_MAXWR, WPE_HW_NREG,
15};
16use crate::vm::{binisa, Compiler};
17
18/// What the engine is asked to run: the program's `## Main`, or one of its functions on arguments.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum Run {
21    Main,
22    Call { func: String, args: Vec<i64> },
23}
24
25/// The VM's answer the engine must reproduce: a Main program's output, one line per `Show`, or a function's value.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Golden {
28    Lines(Vec<String>),
29    Answer(i64),
30}
31
32/// One program compiled for the fabric engine.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Compiled {
35    /// The register bytecode the lifter lowered, as the portable binary ISA frame ([`binisa::encode_program`]).
36    pub bytecode: Vec<u8>,
37    /// The bytes the engine loads: the lowered program's image followed by its arguments.
38    pub wire: Vec<u8>,
39    /// Which lowering produced the image.
40    pub lifter: Lifter,
41    /// What the VM answers, which the engine model was proven to answer before the image was emitted.
42    pub golden: Golden,
43}
44
45/// The register bytecode of `src`, compiled the way the fabric lifters compile it, as a binary ISA frame.
46pub fn register_bytecode(src: &str) -> Result<Vec<u8>, String> {
47    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
48        let (stmts, _types, _policies) = parsed?;
49        let program = Compiler::compile(stmts, interner)?;
50        Ok(binisa::encode_program(&program, interner))
51    })
52}
53
54/// Compile `src` for the engine: lower `run` and prove it ≡ VM ([`lower_main`] / [`lower`]), check the image
55/// against the engine `profile` and the flashed image's `caps` (`NAME=N,…`, DEPTH and HEAP measured on the exact
56/// arguments), then emit the wire image and the bytecode it was lowered from. A refusal is the sentence a person
57/// reads on the screen.
58pub fn compile(src: &str, run: &Run, profile: Profile, caps: &str) -> Result<Compiled, String> {
59    let (cfg, lifter, golden, params) = match run {
60        Run::Main => {
61            let (cfg, lines) = lower_main(src)?;
62            (cfg, Lifter::Main, Golden::Lines(lines), Vec::new())
63        }
64        Run::Call { func, args } => {
65            let (cfg, lifter, answer) = lower(src, func, args)?;
66            (cfg, lifter, Golden::Answer(answer), args.clone())
67        }
68    };
69    profile
70        .fits(&cfg)
71        .map_err(|e| format!("does not fit the {} engine: {e}", profile.name()))?;
72    fits_caps(&cfg, &params, caps).map_err(|e| format!("does not fit the flashed image: {e}"))?;
73    let wire = wprog_config_image_unchecked(&cfg).wire_bytes(&params);
74    let bytecode = register_bytecode(src)?;
75    Ok(Compiled {
76        bytecode,
77        wire,
78        lifter,
79        golden,
80    })
81}
82
83/// One program of a library: its title, its source file, and what the engine runs of it.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct Entry {
86    pub title: String,
87    pub program: String,
88    pub run: Run,
89}
90
91/// The entries of a library file (`benchmarks/silicon/programs/terminal_library.txt`'s format): one per line,
92/// `title | program file | function | arguments`, a `## Main` program with neither function nor arguments; `#`
93/// starts a comment line.
94pub fn library(text: &str) -> Result<Vec<Entry>, String> {
95    let mut entries = Vec::new();
96    for (n, line) in text.lines().enumerate() {
97        if line.trim().is_empty() || line.trim_start().starts_with('#') {
98            continue;
99        }
100        let fields: Vec<&str> = line.split('|').map(str::trim).collect();
101        let [title, program, func, args] = fields[..] else {
102            return Err(format!("library line {}: want `title | program | function | arguments`", n + 1));
103        };
104        let run = if func.is_empty() {
105            if !args.is_empty() {
106                return Err(format!("library line {}: a Main program takes no arguments", n + 1));
107            }
108            Run::Main
109        } else {
110            let args = args
111                .split(',')
112                .map(str::trim)
113                .filter(|a| !a.is_empty())
114                .map(|a| a.parse::<i64>().map_err(|_| format!("library line {}: bad argument {a:?}", n + 1)))
115                .collect::<Result<Vec<_>, _>>()?;
116            Run::Call { func: func.to_string(), args }
117        };
118        entries.push(Entry { title: title.to_string(), program: program.to_string(), run });
119    }
120    Ok(entries)
121}
122
123/// The caps of Board B's hardware engine, written the way a build recipe writes them.
124pub fn hw_caps() -> String {
125    format!(
126        "NREG={WPE_HW_NREG},MAXBLK={WPE_HW_MAXBLK},MAXCELL={WPE_HW_MAXCELL},MAXWR={WPE_HW_MAXWR},\
127         MAXARG={WPE_HW_MAXARG},MAXLIV={WPE_HW_MAXLIV},MAXK={WPE_HW_MAXK},HEAP={WPE_HW_HEAP},DEPTH={WPE_HW_DEPTH}"
128    )
129}
130
131/// A compile's report, one `key=value` line per fact: `lifter=`, then `golden=` (a function's value) or
132/// `golden_json=` (a Main program's lines), then `bytecode=` and `wire=` in lowercase hex. A refusal is the single
133/// line `error=` followed by the sentence.
134pub fn report(result: &Result<Compiled, String>) -> String {
135    match result {
136        Ok(c) => {
137            let golden = match &c.golden {
138                Golden::Answer(v) => format!("golden={v}"),
139                Golden::Lines(lines) => format!(
140                    "golden_json={}",
141                    serde_json::to_string(lines).expect("lines serialize")
142                ),
143            };
144            format!(
145                "lifter={}\n{golden}\nbytecode={}\nwire={}\n",
146                c.lifter.name(),
147                hex(&c.bytecode),
148                hex(&c.wire)
149            )
150        }
151        Err(e) => format!("error={}\n", e.replace('\n', " ")),
152    }
153}
154
155fn hex(bytes: &[u8]) -> String {
156    let mut s = String::with_capacity(bytes.len() * 2);
157    for b in bytes {
158        s.push_str(&format!("{b:02x}"));
159    }
160    s
161}
162
163/// How an engine run ended, read from its result frame.
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165pub enum Ended {
166    /// The program finished: its result and the engine cycles it took.
167    Finished { result: i64, cycles: u32 },
168    /// The call stack overflowed the engine's DEPTH: the result bytes are no answer.
169    CallStack { cycles: u32 },
170    /// The heap outgrew the engine's HEAP: no answer either.
171    Heap { cycles: u32 },
172}
173
174/// The engine's reply so far: every `Show` it sent, each rendered as the VM prints it, and how the run ended once
175/// the result frame is whole.
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct Reply {
178    pub shows: Vec<String>,
179    pub ended: Option<Ended>,
180}
181
182/// One shown value as the VM prints it — through the engine model's own rendering, so the board and the laptop can
183/// never print a value two ways.
184pub fn shown(v: &ChipValue) -> String {
185    let (tag, value, heap) = match v {
186        ChipValue::Int(i) => (TAG_INT, *i, Vec::new()),
187        ChipValue::Float(bits) => (TAG_FLOAT, *bits as i64, Vec::new()),
188        ChipValue::Word32(w) => (TAG_WORD32, i64::from(*w), Vec::new()),
189        ChipValue::Bool(b) => (TAG_BOOL, i64::from(*b), Vec::new()),
190        ChipValue::Text(t) => {
191            let mut heap = vec![t.len() as i64];
192            heap.extend(t.bytes().map(i64::from));
193            (TAG_TEXT, 0, heap)
194        }
195    };
196    render_show(WShow { tag, value }, &heap)
197}
198
199/// Decode what the engine has sent of one run — its bytes arrive in pieces, so a reply cut short is not an error,
200/// only not over yet. Bytes before the first record or frame (the engine's power-on byte) are skipped. A program
201/// that calls the host is refused by name: the board answers no host calls.
202pub fn engine_reply(bytes: &[u8]) -> Result<Reply, String> {
203    let mut reply = Reply { shows: Vec::new(), ended: None };
204    let mut at = bytes.iter().position(|&b| b == REC_SHOW || b == REC_CALL || b == FRAME_SYNC).unwrap_or(bytes.len());
205    while at < bytes.len() && bytes[at] != FRAME_SYNC {
206        match Record::decode(&bytes[at..]) {
207            Ok((Record::Show(v), n)) => {
208                reply.shows.push(shown(&v));
209                at += n;
210            }
211            Ok((Record::Call { call, .. }, _)) => {
212                return Err(format!("the program calls the host ({}), which the board does not answer", call.name()))
213            }
214            Ok((Record::Reply(_), _)) => return Err("the engine sent a reply record".to_string()),
215            Err(FrameError::Short) => return Ok(reply),
216            Err(FrameError::Malformed(why)) => return Err(format!("the engine sent no record: {why}")),
217        }
218    }
219    if at < bytes.len() {
220        match ResultFrame::decode(&bytes[at..]) {
221            Ok((f, _)) => {
222                reply.ended = Some(match f.end {
223                    FRAME_END => Ended::Finished { result: f.result, cycles: f.cycles },
224                    FRAME_END_CALL_STACK => Ended::CallStack { cycles: f.cycles },
225                    _ => Ended::Heap { cycles: f.cycles },
226                })
227            }
228            Err(FrameError::Short) => {}
229            Err(FrameError::Malformed(why)) => return Err(format!("the engine's result frame is damaged: {why}")),
230        }
231    }
232    Ok(reply)
233}
234
235/// FABRIC == VM: a finished run whose shows are the VM's lines (a Main program) or whose result is the VM's answer
236/// (a function). An overflowed run never agrees.
237pub fn agrees(golden: &Golden, reply: &Reply) -> bool {
238    match (golden, reply.ended) {
239        (Golden::Lines(lines), Some(Ended::Finished { .. })) => &reply.shows == lines,
240        (Golden::Answer(v), Some(Ended::Finished { result, .. })) => result == *v,
241        _ => false,
242    }
243}