beck_prim/
lib.rs

1//! The runtime library: the primitives a compiled program links against.
2//!
3//! # What this is
4//!
5//! A handful of Beck's primitives are neither arithmetic nor a shape the code generators can lay
6//! out: a digest is somebody's table, base64 is somebody's grammar, case mapping is Unicode's
7//! table, and `str_to_int` has to agree with Rust's own parser about every input that is not a
8//! number. Both native backends refused all of them, and the refusal said so in as many words.
9//!
10//! There were three ways to answer that, and only one of them is fast **and** exact:
11//!
12//! * **Ask the host.** The four `nondet` primitives do
13//!   ([`beck_llvm::Upcall`](../beck_llvm/emit/enum.Upcall.html)), because a clock and an id source
14//!   are questions only the process outside can answer. A digest is not a question — it is a
15//!   function of its argument — and a pipe round trip per call would make the compiled program
16//!   *slower* than the tree-walker at the thing it exists to be faster at.
17//! * **Emit the algorithm.** Hex and base64 would compile; BLAKE3 and a Unicode case table would
18//!   be a second implementation of somebody else's specification, sitting beside the one the
19//!   evaluator already calls and agreeing with it only as far as it had been tested.
20//! * **Link the implementation.** This crate. The compiled program calls the **same Rust
21//!   functions** the evaluator calls, so the differential between the backends is not a claim
22//!   about two implementations agreeing; there is one.
23//!
24//! So this crate is the standard library's host half, in one place: [`digest`] and [`time`] moved
25//! here from `beck-core` and `beck-eval` rather than being copied, [`text`] is where the three
26//! primitives that are a `str::` method live, and the evaluator calls all of it.
27//!
28//! # The two exports, and why there is no pointer in them
29//!
30//! `abi` — the module, behind the feature of the same name — is the C entry points, and the
31//! interesting property is what does **not** cross them.
32//! The workspace forbids `unsafe_code`, and a runtime library that took `(*const u8, usize)` and
33//! made a slice out of it would need an `unsafe` block in the first line of every primitive. So
34//! the arena is turned around: this crate **owns** the compiled program's heap ([`arena`]), the
35//! program is handed its base once at startup, and every call after that carries **offsets**. An
36//! offset is an `i64`, reading one is an indexing operation on a `Vec<u8>` this crate holds, and a
37//! bad offset is a panic rather than a fault — so there is no raw pointer dereference here, no
38//! `unsafe` block, and nothing for `docs/43-threat-model.md`'s structural claim to give up.
39//!
40//! `adr/0026` had already made a value in that arena an **offset and not a pointer**, so that the
41//! whole heap could cross a pipe as bytes. This crate is the second thing that property buys.
42//!
43//! # The protocol
44//!
45//! `beck_prim`, and a mark rather than a return value:
46//!
47//! 1. The caller passes the arena's high-water mark and up to three argument words.
48//! 2. This library allocates what its answer needs, starting at that mark.
49//! 3. It writes a **two-word outcome record** — a [`Status`] and a word — immediately *above*
50//!    everything it allocated, and answers with that offset.
51//! 4. The caller stores the answer as its new mark and reads the record from it.
52//!
53//! The record sitting above the mark is what makes a call cost no arena at all beyond its answer:
54//! it is scratch, live until the next allocation, which is exactly as long as the caller needs it.
55//! An arena with no room is `-1`, which the caller turns into its own heap-exhausted trap with the
56//! span it already has.
57//!
58//! There is deliberately **no error path for a bad offset**. A caller here is a code generator in
59//! this workspace, not a program, and a defensive answer would be a second contract to keep in
60//! agreement with the first.
61//!
62//! # The second entry point, which carries no offsets at all
63//!
64//! [`math`] is here for a different reason from the rest of this crate. A digest is linked because
65//! emitting it would be a second implementation of somebody else's table; a sine is linked because
66//! **there is no answer to ask the host for** — IEEE 754 pins `sqrt` to one correctly-rounded
67//! result and pins neither `sin` nor `cos`, so a fold that reaches the platform's libm replays to
68//! a different state on a platform with a different one.
69//!
70//! A function from a double to a double needs no heap, so `beck_prim_f64` takes the argument
71//! itself and answers the result. Routing it through the mark protocol above would cost a lock, a
72//! bounds check and an outcome record per call, for a primitive whose whole input is 64 bits — and
73//! the arena's first paragraph, which is what buys all of that, has nothing to buy here.
74
75#[cfg(feature = "abi")]
76pub mod abi;
77pub mod arena;
78pub mod digest;
79pub mod math;
80pub mod text;
81pub mod time;
82
83/// What the first word of an outcome record says.
84///
85/// Three cases rather than two, because `str_to_int` answers `Option[Int]` and an `Option` is laid
86/// out by the code generator that asked: this library says *there is no value* and the emitter
87/// builds the `None` its own layout calls for. The same division is what makes a raise work — the
88/// library produces the message, the emitter builds the declared error value around it, because
89/// the type of that value is a fact about the program.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91#[repr(i64)]
92pub enum Status {
93    /// The second word is the answer: a scalar, or the offset of a `Str`.
94    Value = 0,
95    /// The second word is the offset of a `Str` saying why. The caller raises.
96    Raised = 1,
97    /// There is no value. The caller answers `None`.
98    Nothing = 2,
99}
100
101impl Status {
102    pub fn word(self) -> i64 {
103        self as i64
104    }
105}
106
107/// A primitive this library computes, and the code the two backends call it by.
108///
109/// The numbers are written out rather than derived from the order, because they are a protocol
110/// between a compiled program and a library it was linked against: a primitive removed from the
111/// middle of this list must not renumber the ones after it.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum Op {
114    Digest = 1,
115    DigestKeyed = 2,
116    DigestEq = 3,
117    HexEncode = 4,
118    HexDecode = 5,
119    Base64Encode = 6,
120    Base64Decode = 7,
121    UuidParse = 8,
122    UuidVersion = 9,
123    StrUpper = 10,
124    StrLower = 11,
125    StrToInt = 12,
126    StrReplace = 13,
127    TimeFormat = 14,
128    TimeParse = 15,
129}
130
131impl Op {
132    /// Every one of them, so a caller can build a table without repeating the list.
133    pub const ALL: [Op; 15] = [
134        Op::Digest,
135        Op::DigestKeyed,
136        Op::DigestEq,
137        Op::HexEncode,
138        Op::HexDecode,
139        Op::Base64Encode,
140        Op::Base64Decode,
141        Op::UuidParse,
142        Op::UuidVersion,
143        Op::StrUpper,
144        Op::StrLower,
145        Op::StrToInt,
146        Op::StrReplace,
147        Op::TimeFormat,
148        Op::TimeParse,
149    ];
150
151    pub fn code(self) -> i32 {
152        self as i32
153    }
154
155    pub fn from_code(code: i32) -> Option<Op> {
156        Op::ALL.into_iter().find(|o| o.code() == code)
157    }
158
159    /// How many argument words the call carries.
160    ///
161    /// A property of the primitive rather than of the call, for the reason `beck_llvm`'s upcall
162    /// arity is one: an arity read out of the call is an arity nobody checked.
163    pub fn arity(self) -> usize {
164        match self {
165            Op::StrReplace => 3,
166            Op::DigestKeyed | Op::DigestEq => 2,
167            _ => 1,
168        }
169    }
170
171    /// How many of the argument words are the offset of a `Str`.
172    ///
173    /// The text arguments come first and there is only one primitive here whose argument is not
174    /// text at all, so this is a count rather than a mask — and it is what stops the ABI from
175    /// reading a millisecond as an offset.
176    pub fn text_args(self) -> usize {
177        match self {
178            Op::TimeFormat => 0,
179            _ => self.arity(),
180        }
181    }
182
183    /// The name the Beck program wrote, which is also `beck_core::Prim::name`.
184    pub fn name(self) -> &'static str {
185        match self {
186            Op::Digest => "digest",
187            Op::DigestKeyed => "digest_keyed",
188            Op::DigestEq => "digest_eq",
189            Op::HexEncode => "hex_encode",
190            Op::HexDecode => "hex_decode",
191            Op::Base64Encode => "base64_encode",
192            Op::Base64Decode => "base64_decode",
193            Op::UuidParse => "uuid_parse",
194            Op::UuidVersion => "uuid_version",
195            Op::StrUpper => "str_upper",
196            Op::StrLower => "str_lower",
197            Op::StrToInt => "str_to_int",
198            Op::StrReplace => "str_replace",
199            Op::TimeFormat => "time_format",
200            Op::TimeParse => "time_parse",
201        }
202    }
203
204    /// The declared value a failure of this primitive raises.
205    ///
206    /// `None` for one that cannot fail. This library produces the *message* and nothing else: the
207    /// value around it is a declared type with a layout, and a layout belongs to whichever code
208    /// generator asked. So the shape is described here, where the failure is, and built there.
209    pub fn raises(self) -> Option<Raise> {
210        let (ty, variant, constants): (_, _, &'static [(&'static str, &'static str)]) = match self {
211            Op::HexDecode => ("EncodingError", "BadEncoding", &[("encoding", "hex")]),
212            Op::Base64Decode => ("EncodingError", "BadEncoding", &[("encoding", "base64")]),
213            Op::UuidParse | Op::UuidVersion => ("UuidError", "BadUuid", &[]),
214            Op::TimeParse => ("TimeError", "BadTime", &[]),
215            _ => return None,
216        };
217        Some(Raise {
218            ty,
219            variant,
220            constants,
221            why: "why",
222        })
223    }
224}
225
226/// The value a primitive's failure raises, described rather than built.
227///
228/// Every field of it is here: the declared type, its variant, the fields whose values are a
229/// constant of the primitive rather than of the input, and the one field the message goes in. A
230/// caller that filled these in from its own table would be a second place for the evaluator's
231/// `EncodingError.BadEncoding(encoding = "hex", …)` to be written down.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub struct Raise {
234    pub ty: &'static str,
235    pub variant: &'static str,
236    /// Fields the primitive fixes: `hex_decode` always raises with `encoding = "hex"`.
237    pub constants: &'static [(&'static str, &'static str)],
238    /// The field the message goes in.
239    pub why: &'static str,
240}
241
242/// What one call answers, before it is written into the arena.
243///
244/// Owned, and that is the point: the argument words are read out of the arena by reference and
245/// this is produced from them, so the borrow ends before anything is written back.
246#[derive(Clone, Debug, PartialEq, Eq)]
247pub enum Answer {
248    /// A scalar — an `Int`, or a `Bool` as `0` and `1`.
249    Word(i64),
250    /// Text, to be allocated as a `Str`.
251    Text(String),
252    /// `None`.
253    Nothing,
254    /// A failure, with the message the declared error value carries.
255    Raised(String),
256}
257
258/// Perform one primitive over text already read out of the arena.
259///
260/// Separated from the ABI so that it can be tested without an arena at all, and so that the one
261/// place a `Prim` becomes an answer is a `match` a reader can check against `beck-eval`'s.
262pub fn perform(op: Op, args: &[&str], words: &[i64]) -> Answer {
263    match op {
264        Op::Digest => Answer::Text(digest::of(args[0])),
265        Op::DigestKeyed => Answer::Text(digest::keyed(args[0], args[1])),
266        Op::DigestEq => Answer::Word(i64::from(digest::same(args[0], args[1]))),
267        Op::HexEncode => Answer::Text(digest::hex_encode(args[0])),
268        Op::HexDecode => answer(digest::hex_decode(args[0])),
269        Op::Base64Encode => Answer::Text(digest::base64_encode(args[0])),
270        Op::Base64Decode => answer(digest::base64_decode(args[0])),
271        Op::UuidParse => answer(digest::uuid_normalise(args[0])),
272        Op::UuidVersion => match digest::uuid_normalise(args[0]) {
273            Ok(canonical) => Answer::Word(digest::uuid_version(&canonical)),
274            Err(why) => Answer::Raised(why),
275        },
276        Op::StrUpper => Answer::Text(text::upper(args[0])),
277        Op::StrLower => Answer::Text(text::lower(args[0])),
278        Op::StrToInt => match text::to_int(args[0]) {
279            Some(n) => Answer::Word(n),
280            None => Answer::Nothing,
281        },
282        Op::StrReplace => Answer::Text(text::replace(args[0], args[1], args[2])),
283        Op::TimeFormat => Answer::Text(time::format(words[0])),
284        Op::TimeParse => match time::parse(args[0]) {
285            Ok(ms) => Answer::Word(ms),
286            Err(why) => Answer::Raised(why),
287        },
288    }
289}
290
291fn answer(r: Result<String, String>) -> Answer {
292    match r {
293        Ok(text) => Answer::Text(text),
294        Err(why) => Answer::Raised(why),
295    }
296}
297
298/// A primitive that is a function from a double to a double, and the code the backends call it by.
299///
300/// A separate vocabulary from [`Op`] rather than two more of its variants, because the two share
301/// no part of their protocol: an [`Op`] reads text out of the arena and writes an outcome record
302/// above the mark, and one of these takes a number and answers one. Merging them would give every
303/// arm of the arena protocol a case that cannot happen.
304///
305/// The numbers are written out for [`Op`]'s reason: they are a protocol between a compiled program
306/// and a library it was linked against.
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub enum FloatOp {
309    Sin = 1,
310    Cos = 2,
311}
312
313impl FloatOp {
314    pub const ALL: [FloatOp; 2] = [FloatOp::Sin, FloatOp::Cos];
315
316    pub fn code(self) -> i32 {
317        self as i32
318    }
319
320    pub fn from_code(code: i32) -> Option<FloatOp> {
321        FloatOp::ALL.into_iter().find(|o| o.code() == code)
322    }
323
324    /// The name the Beck program wrote, which is also `beck_core::Prim::name`.
325    pub fn name(self) -> &'static str {
326        match self {
327            FloatOp::Sin => "sin",
328            FloatOp::Cos => "cos",
329        }
330    }
331}
332
333/// Perform one primitive that is a function from a double to a double.
334///
335/// Separated from the ABI so that the evaluator reaches the same `match` a compiled program does,
336/// which is what makes the three-way differential a statement about one implementation rather than
337/// about three that agree.
338pub fn perform_f64(op: FloatOp, x: f64) -> f64 {
339    match op {
340        FloatOp::Sin => math::sin(x),
341        FloatOp::Cos => math::cos(x),
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn every_code_reads_back_as_the_primitive_that_wrote_it() {
351        for op in Op::ALL {
352            assert_eq!(Op::from_code(op.code()), Some(op), "{}", op.name());
353        }
354        assert_eq!(Op::from_code(0), None, "no primitive is zero");
355        assert_eq!(Op::from_code(99), None);
356        // A duplicate code would make two primitives one call.
357        let mut codes: Vec<i32> = Op::ALL.iter().map(|o| o.code()).collect();
358        codes.sort_unstable();
359        codes.dedup();
360        assert_eq!(codes.len(), Op::ALL.len(), "the codes are distinct");
361    }
362
363    #[test]
364    fn a_primitive_that_can_fail_names_what_it_raises() {
365        // The pairing is what the emitter builds a value from, so a primitive whose answer can be
366        // `Raised` and which names no type would be a raise with nothing to raise.
367        for (op, args) in [
368            (Op::HexDecode, "zz"),
369            (Op::Base64Decode, "!"),
370            (Op::UuidParse, "nope"),
371            (Op::UuidVersion, "nope"),
372            (Op::TimeParse, "nope"),
373        ] {
374            assert!(
375                matches!(perform(op, &[args], &[]), Answer::Raised(_)),
376                "{} should fail on {args:?}",
377                op.name()
378            );
379            assert!(op.raises().is_some(), "{} names no error type", op.name());
380        }
381    }
382}