Skip to main content

byteflow/
natives.rs

1//! Standard native (FFI) table shipped with the facade.
2//!
3//! # Why Message make/unpack lives here (not as new opcodes)
4//!
5//! [`crate::Value::Message`] is already a first-class wire/register value
6//! (ABI v2). Building and tearing it apart in bytecode could be ISA ops
7//! (`MakeMessage`, `MsgSender`, …), but that would burn opcode space and
8//! force every embedder that never does request-reply to carry the
9//! dispatch cost. Natives keep the envelope **generic in the core** while
10//! the std table opts into the helpers — same pattern as `print` / `now_ms`
11//! (design notes §30-31): host Rust owns the operation, bytecode only
12//! picks a slot.
13//!
14//! # Stable indices are an embed contract
15//!
16//! Flash / separately-built `.bf` modules hard-code `CallNative` indices.
17//! Renumbering an existing slot is a **breaking** change. Keep
18//! [`std_native_map`] and [`std_native_table`] in lockstep; append only.
19//!
20//! | Index | Name | Role |
21//! |------:|------|------|
22//! | 0 | `print` | host [`crate::OutputSink`] (CLI uses stdout) |
23//! | 1 | `now_ms` | wall-clock millis as `Value::Int` |
24//! | 2 | `make_msg` | build [`crate::Message`]; no sender operand (stamped on `Send`) |
25//! | 3 | `msg_sender` | extract `sender` → `Value::Pid` (authenticated **after** delivery) |
26//! | 4 | `msg_request_id` | extract `request_id` → `Value::Int` |
27//! | 5 | `msg_tag` | extract `tag` → `Value::Int` |
28//! | 6 | `msg_payload` | extract `payload` → any [`crate::Value`] |
29//! | 7 | `msg_reply_cap` | extract `reply_cap` → `Value::Cap` (SEND grant) |
30//!
31//! # `CallNative` argument layout
32//!
33//! `CallNative ra, fb, nc` takes args from `r[a .. a+nc]` and writes the
34//! result back into `r[a]` — so a call **destroys** the first argument
35//! register. Samples that need to keep a `Message` while unpacking must
36//! `Move` it into the dest register first (see
37//! [`crate::samples::atomic_request_reply`]).
38
39use std::collections::HashMap;
40use std::sync::Arc;
41use std::time::{SystemTime, UNIX_EPOCH};
42
43use crate::bytecode::{Message, Value};
44use crate::output::{OutputSink, StdoutSink};
45use crate::vm::{expect_arg, expect_message, expect_u64, Fault, NativeTable};
46
47/// Stable `CallNative` indices for [`std_native_table`]. Bytecode embeds these
48/// once assembled — append only; never renumber.
49pub mod std_native {
50    pub const PRINT: u32 = 0;
51    pub const NOW_MS: u32 = 1;
52    pub const MAKE_MSG: u32 = 2;
53    pub const MSG_SENDER: u32 = 3;
54    pub const MSG_REQUEST_ID: u32 = 4;
55    pub const MSG_TAG: u32 = 5;
56    pub const MSG_PAYLOAD: u32 = 6;
57    pub const MSG_REPLY_CAP: u32 = 7;
58}
59
60/// Name → `CallNative` index for documentation / host-side lookups.
61///
62/// Prefer this (or [`std_natives`]) over hard-coding integers in host code
63/// so renames stay discoverable; bytecode itself still embeds the numeric
64/// slot once assembled.
65pub fn std_native_map() -> HashMap<String, u32> {
66    HashMap::from([
67        ("print".to_owned(), 0),
68        ("now_ms".to_owned(), 1),
69        ("make_msg".to_owned(), 2),
70        ("msg_sender".to_owned(), 3),
71        ("msg_request_id".to_owned(), 4),
72        ("msg_tag".to_owned(), 5),
73        ("msg_payload".to_owned(), 6),
74        ("msg_reply_cap".to_owned(), 7),
75    ])
76}
77
78/// Default FFI table with [`StdoutSink`]. Prefer [`std_native_table_with`]
79/// in embedders that must not touch stdout.
80pub fn std_native_table() -> Arc<NativeTable> {
81    std_native_table_with(Arc::new(StdoutSink))
82}
83
84/// Std natives with an explicit print sink (`NullSink` in tests / libraries).
85pub fn std_native_table_with(output: Arc<dyn OutputSink>) -> Arc<NativeTable> {
86    let built = NativeTable::builder()
87        .register("print", {
88            let output = Arc::clone(&output);
89            move |args| {
90                output.write(args);
91                Ok(Value::Unit)
92            }
93        })
94        .and_then(|b| {
95            b.register("now_ms", |_| {
96                let duration = SystemTime::now()
97                    .duration_since(UNIX_EPOCH)
98                    .map_err(|e| Fault::NativeError(format!("system clock error: {e}")))?;
99                let millis = i64::try_from(duration.as_millis())
100                    .map_err(|_| Fault::NativeError("system clock value exceeds i64".into()))?;
101                Ok(Value::Int(millis))
102            })
103        })
104        .and_then(|b| {
105            b.register("make_msg", |args| {
106                // 3-arg form: (request_id, tag, payload).
107                // 4-arg legacy: (sender, request_id, tag, payload) — the sender
108                // operand is discarded. Message.sender is always 0 here;
109                // only authenticate_outgoing_message / send() writes identity.
110                let (request_id, tag, payload) = if args.len() >= 4 {
111                    (
112                        expect_u64(args, 1, "make_msg")?,
113                        expect_u64(args, 2, "make_msg")?,
114                        expect_arg(args, 3, "make_msg")?.clone(),
115                    )
116                } else {
117                    (
118                        expect_u64(args, 0, "make_msg")?,
119                        expect_u64(args, 1, "make_msg")?,
120                        expect_arg(args, 2, "make_msg")?.clone(),
121                    )
122                };
123                let tag = u16::try_from(tag).map_err(|_| {
124                    Fault::NativeError(format!("make_msg: tag {tag} does not fit in u16"))
125                })?;
126                Ok(Value::Message(Message::new(0, request_id, tag, payload)))
127            })
128        })
129        .and_then(|b| {
130            b.register("msg_sender", |args| {
131                Ok(Value::Pid(expect_message(args, 0, "msg_sender")?.sender))
132            })
133        })
134        .and_then(|b| {
135            b.register("msg_request_id", |args| {
136                Ok(Value::Int(
137                    expect_message(args, 0, "msg_request_id")?.request_id as i64,
138                ))
139            })
140        })
141        .and_then(|b| {
142            b.register("msg_tag", |args| {
143                Ok(Value::Int(i64::from(expect_message(args, 0, "msg_tag")?.tag)))
144            })
145        })
146        .and_then(|b| {
147            b.register("msg_payload", |args| {
148                Ok(expect_message(args, 0, "msg_payload")?.payload.as_ref().clone())
149            })
150        })
151        .and_then(|b| {
152            b.register("msg_reply_cap", |args| {
153                Ok(Value::Cap(
154                    expect_message(args, 0, "msg_reply_cap")?.reply_cap,
155                ))
156            })
157        });
158    match built {
159        Ok(b) => b.build(),
160        Err(_) => NativeTable::empty(),
161    }
162}
163
164/// Build the default native table and its matching name map together.
165///
166/// Prefer this when the host both registers natives and resolves names to
167/// indices for `ChunkBuilder` — one source of truth, no drift between map
168/// and table.
169pub fn std_natives() -> (Arc<NativeTable>, HashMap<String, u32>) {
170    let table = std_native_table();
171    let map: HashMap<String, u32> = table
172        .names()
173        .filter_map(|n| table.index_of(n).map(|i| (n.to_string(), i)))
174        .collect();
175    (table, map)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn std_native_indices_are_stable() {
184        let map = std_native_map();
185        assert_eq!(map.get("print"), Some(&0));
186        assert_eq!(map.get("now_ms"), Some(&1));
187        assert_eq!(map.get("make_msg"), Some(&2));
188        assert_eq!(map.get("msg_sender"), Some(&3));
189        assert_eq!(map.get("msg_request_id"), Some(&4));
190        assert_eq!(map.get("msg_tag"), Some(&5));
191        assert_eq!(map.get("msg_payload"), Some(&6));
192        assert_eq!(map.get("msg_reply_cap"), Some(&7));
193    }
194
195    #[test]
196    fn std_native_table_matches_map() {
197        let (table, map) = std_natives();
198        assert_eq!(table.len(), 8);
199        for (name, idx) in &map {
200            assert_eq!(table.index_of(name), Some(*idx));
201        }
202    }
203
204    #[test]
205    fn now_ms_returns_non_negative_int() -> Result<(), Box<dyn std::error::Error>> {
206        let table = std_native_table();
207        let now_ms = table.get(1).ok_or("now_ms native")?;
208        let value = now_ms(&[])?;
209        assert!(matches!(value, Value::Int(ms) if ms >= 0));
210        Ok(())
211    }
212
213    use crate::bytecode::CapId;
214
215    #[test]
216    fn make_msg_and_unpack_round_trip() -> Result<(), Box<dyn std::error::Error>> {
217        let table = std_native_table();
218        let make = table.get(2).ok_or("make_msg")?;
219        let msg = make(&[
220            Value::Int(3),
221            Value::Int(7),
222            Value::Int(42),
223        ])?;
224        let expected = Message::new(0, 3, 7, 42);
225        assert_eq!(msg.as_message(), Some(&expected));
226        let sender = table.get(3).ok_or("msg_sender")?;
227        let req = table.get(4).ok_or("msg_request_id")?;
228        let tag = table.get(5).ok_or("msg_tag")?;
229        let payload = table.get(6).ok_or("msg_payload")?;
230        let cap = table.get(7).ok_or("msg_reply_cap")?;
231        assert_eq!(sender(std::slice::from_ref(&msg))?, Value::Pid(0));
232        assert_eq!(req(std::slice::from_ref(&msg))?, Value::Int(3));
233        assert_eq!(tag(std::slice::from_ref(&msg))?, Value::Int(7));
234        assert_eq!(payload(std::slice::from_ref(&msg))?, Value::Int(42));
235        assert_eq!(cap(std::slice::from_ref(&msg))?, Value::Cap(CapId::NONE));
236        Ok(())
237    }
238
239    #[test]
240    fn print_accepts_all_current_values() -> Result<(), Box<dyn std::error::Error>> {
241        let table = std_native_table();
242        let print = table.get(0).ok_or("print native")?;
243        let values = [
244            Value::Unit,
245            Value::Bool(true),
246            Value::Int(42),
247            Value::Float(1.5),
248            Value::Pid(7),
249            Value::Message(Message::new(1, 2, 3, 4)),
250            Value::Cap(CapId::from_raw(9)),
251            Value::str("hello"),
252            Value::bytes([1u8, 2, 3]),
253        ];
254        assert_eq!(print(&values)?, Value::Unit);
255        Ok(())
256    }
257
258    #[test]
259    fn print_with_no_args_still_returns_unit() -> Result<(), Box<dyn std::error::Error>> {
260        let table = std_native_table();
261        let print = table.get(0).ok_or("print native")?;
262        assert_eq!(print(&[])?, Value::Unit);
263        Ok(())
264    }
265}