Skip to main content

logicaffeine_data/
failure.rs

1//! The sentences a program stops with, written once for every tier.
2//!
3//! This is the lowest crate every engine and every compiled program links, so a runtime failure's wording
4//! lives here and nowhere else: the interpreter and the VM report these strings, the generated Rust
5//! panics with them, and the hosts written in other languages are generated from them
6//! (`logicaffeine_compile::semantics::failcode`).
7//!
8//! A broken contract (`Require that c, otherwise r.`) reports its reason, which must be Text. Every engine
9//! refuses a non-Text reason with the same sentence, naming the LOGOS type it got, so the compiled tier
10//! routes the reason value through [`contract_reason`]. A type with no implementation here does not
11//! compile — never a report in the wrong words.
12
13/// A value that can stand after `otherwise`.
14pub trait ContractReason {
15    /// The report: the text itself for a Text reason, the refusal otherwise.
16    fn contract_reason(&self) -> String;
17}
18
19fn refusal(type_name: &str) -> String {
20    format!("The reason after `otherwise` must be Text, but it was {type_name}.")
21}
22
23impl ContractReason for String {
24    fn contract_reason(&self) -> String {
25        self.clone()
26    }
27}
28
29impl ContractReason for &str {
30    fn contract_reason(&self) -> String {
31        (*self).to_string()
32    }
33}
34
35impl ContractReason for i64 {
36    fn contract_reason(&self) -> String {
37        refusal("Int")
38    }
39}
40
41impl ContractReason for crate::types::LogosInt {
42    fn contract_reason(&self) -> String {
43        refusal("Int")
44    }
45}
46
47impl ContractReason for f64 {
48    fn contract_reason(&self) -> String {
49        refusal("Real")
50    }
51}
52
53impl ContractReason for bool {
54    fn contract_reason(&self) -> String {
55        refusal("Bool")
56    }
57}
58
59impl ContractReason for char {
60    fn contract_reason(&self) -> String {
61        refusal("Char")
62    }
63}
64
65/// The report a broken contract stops with.
66pub fn contract_reason<R: ContractReason + ?Sized>(reason: &R) -> String {
67    reason.contract_reason()
68}
69
70/// The reason an index outside a sequence or a text stops a program: the index as the program wrote it
71/// (1-based, or negative from the end).
72pub fn index_out_of_bounds(index: impl std::fmt::Display) -> String {
73    format!("Index {index} out of bounds")
74}
75
76/// The reason popping a list with no items stops a program: the list as the program writes it
77/// (`xs`, `b's items`).
78pub fn pop_from_empty(list: &str) -> String {
79    format!("Cannot pop from {list}: it is empty")
80}
81
82/// The stop for reading a field of a value that is not a record: a value of `type_name`.
83pub fn field_access_on(type_name: &str) -> String {
84    format!("Cannot access field on {type_name}")
85}
86
87/// The stop for setting a field of a value that is not a record.
88pub const SET_FIELD_ON_NON_RECORD: &str = "Cannot set field on non-struct value";
89
90/// The reason reading a key a map does not hold stops a program: the key as `Show` prints it.
91pub fn key_not_found(key: &(impl FailureText + ?Sized)) -> String {
92    format!("Key '{}' not found in map", key.failure_text())
93}
94
95/// The reason `text_from_bytes` stops a program whose bytes are not UTF-8: the decoder's own account of
96/// where they stop being it.
97pub fn invalid_utf8(error: impl std::fmt::Display) -> String {
98    format!("text_from_bytes(): invalid UTF-8: {error}")
99}
100
101// ── Quantities, money, modular arithmetic ─────────────────────────────────────────────────────────
102
103/// `verb` (`add`, `subtract`) applied to two quantities whose dimensions differ.
104pub fn quantities_differ(
105    verb: &str,
106    a: impl std::fmt::Display,
107    b: impl std::fmt::Display,
108) -> String {
109    format!("cannot {verb} quantities of different dimensions ({a} vs {b})")
110}
111
112/// Two quantities compared across dimensions.
113pub fn quantities_uncomparable(a: impl std::fmt::Display, b: impl std::fmt::Display) -> String {
114    format!("Cannot compare quantities of different dimensions ({a} vs {b})")
115}
116
117/// A quantity divided by a quantity whose magnitude is zero.
118pub const DIVIDE_BY_ZERO_QUANTITY: &str = "cannot divide by a zero quantity";
119
120/// A quantity divided by the number zero.
121pub const DIVIDE_QUANTITY_BY_ZERO: &str = "cannot divide a quantity by zero";
122
123/// A unit name no unit table holds.
124pub fn unknown_unit(name: &str) -> String {
125    format!("Unknown unit '{name}'")
126}
127
128/// `convert(q, unit)` to a unit of another dimension: the quantity's own unit and the one asked for.
129pub fn cannot_convert(from_unit: &str, to_unit: &str) -> String {
130    format!("cannot convert a {from_unit} quantity to '{to_unit}' — different dimension")
131}
132
133/// A currency code no currency table holds.
134pub fn unknown_currency(code: &str) -> String {
135    format!("Unknown currency '{code}'")
136}
137
138/// `verb` (`add`, `subtract`, `compare`) applied to money in two currencies.
139pub fn currencies_differ(verb: &str, a: &str, b: &str) -> String {
140    format!("cannot {verb} money of different currencies ({a} vs {b})")
141}
142
143/// Money divided by money whose amount is zero.
144pub const DIVIDE_MONEY_BY_ZERO_AMOUNT: &str = "cannot divide money by a zero amount";
145
146/// Money divided by the number zero.
147pub const DIVIDE_MONEY_BY_ZERO: &str = "cannot divide money by zero";
148
149/// A conversion between two currencies with no rate for either in scope.
150pub fn no_exchange_rate(from: &str, to: &str) -> String {
151    format!("no exchange rate for {from} or {to}")
152}
153
154/// A conversion with no rates in scope at all.
155pub const NO_EXCHANGE_RATES: &str = "no exchange rates in scope (set a rate first)";
156
157/// `modular(v, m)` with `m <= 0`.
158pub const MODULUS_NOT_POSITIVE: &str = "modular() requires a positive modulus";
159
160/// `op` (`add`, `subtract`, `multiply`, `divide`) applied to two residues of different moduli.
161pub fn modular_rings_differ(op: &str) -> String {
162    format!("cannot {op} values in different modular rings")
163}
164
165/// A modular division whose divisor shares a factor with the modulus.
166pub const NO_MODULAR_INVERSE: &str = "modular divisor has no inverse (not coprime to the modulus)";
167
168/// Text `decimal(…)` cannot read as a base-10 number.
169pub fn not_a_decimal(text: &str) -> String {
170    format!("Cannot parse '{text}' as Decimal")
171}
172
173/// `decimal(…)` given a value of another kind than Text, Int or Decimal, named as the engines name it.
174pub fn decimal_needs_text_or_int(type_name: &str) -> String {
175    format!("decimal() requires a Text or Int, got {type_name}")
176}
177
178// ── Identifiers, time, files ──────────────────────────────────────────────────────────────────────
179
180/// Text that is not a UUID.
181pub fn invalid_uuid(text: &str) -> String {
182    format!("invalid UUID '{text}'")
183}
184
185/// `uuid_from_bytes` given the wrong number of bytes.
186pub fn uuid_needs_16_bytes(got: usize) -> String {
187    format!("uuid_from_bytes() needs 16 bytes, got {got}")
188}
189
190/// Text that is not an RFC 3339 timestamp.
191pub fn not_a_timestamp(text: &str) -> String {
192    format!("Cannot parse '{text}' as an RFC 3339 timestamp")
193}
194
195/// A time zone name no zone table holds.
196pub fn unknown_time_zone(name: &str) -> String {
197    format!("Unknown time zone '{name}'")
198}
199
200/// `readBytes(path)` on a file that cannot be read, with the reason the system gave.
201pub fn cannot_read_bytes(path: &str, reason: impl std::fmt::Display) -> String {
202    format!("readBytes: cannot read '{path}': {reason}")
203}
204
205/// The value a map lookup found for `key`, or the stop [`key_not_found`] names.
206#[inline]
207pub fn found<V, K: FailureText + ?Sized>(value: Option<V>, key: &K) -> V {
208    match value {
209        Some(value) => value,
210        None => no_value_for(key),
211    }
212}
213
214#[cold]
215#[inline(never)]
216fn no_value_for<K: FailureText + ?Sized>(key: &K) -> ! {
217    panic!("{}", key_not_found(key))
218}
219
220/// The value an operation of the exact extension answered, or the stop its refusal names: the sentence
221/// the interpreter reports for the same operation on the same values.
222#[inline]
223pub fn answered<V>(answer: Result<V, String>) -> V {
224    match answer {
225        Ok(value) => value,
226        Err(why) => refused(why),
227    }
228}
229
230#[cold]
231#[inline(never)]
232fn refused(why: String) -> ! {
233    panic!("{why}")
234}
235
236/// The reason a program stops when its calls nest deeper than its stack holds: the interpreter and the VM at their
237/// call-depth limit, a compiled program at the end of its stack.
238pub const CALL_DEPTH_EXCEEDED: &str = "Stack overflow: maximum call depth exceeded";
239
240/// The reason an integer division by zero stops a program.
241pub const DIVISION_BY_ZERO: &str = "Division by zero";
242
243/// The reason an integer remainder by zero stops a program.
244pub const MODULO_BY_ZERO: &str = "Modulo by zero";
245
246/// The reason a compiled program stops when an integer past a machine word reaches a place it holds one.
247pub const INT_TOO_WIDE: &str = "integer value out of range for this int64 context";
248
249/// The reason `&` stops a program: an operand it has no bits for (a negative big integer, a big integer beside
250/// a value that is not a low mask `2^k - 1`, or a kind `&` does not combine).
251pub const BITAND_OPERANDS: &str = "`&` requires integer, boolean, or Set operands";
252
253/// The reason `|` stops a program: a big integer, or a kind `|` does not combine.
254pub const BITOR_OPERANDS: &str = "`|` requires integer, boolean, or Set operands";
255
256/// The reason `^` (`xor`) stops a program: a big integer, or a kind it does not combine.
257pub const BITXOR_OPERANDS: &str = "Bitwise XOR requires integer, boolean, or Set operands";
258
259/// The reason a left shift stops a program: an operand that is not a machine-word integer.
260pub const SHL_OPERANDS: &str = "Left shift requires integer operands";
261
262/// The reason a right shift stops a program: an operand that is not a machine-word integer.
263pub const SHR_OPERANDS: &str = "Right shift requires integer operands";
264
265/// A value as a failure sentence names it: exactly as `Show` prints it.
266pub trait FailureText {
267    fn failure_text(&self) -> String;
268}
269
270impl FailureText for i64 {
271    fn failure_text(&self) -> String {
272        self.to_string()
273    }
274}
275
276impl FailureText for String {
277    fn failure_text(&self) -> String {
278        self.clone()
279    }
280}
281
282impl FailureText for str {
283    fn failure_text(&self) -> String {
284        self.to_string()
285    }
286}
287
288impl FailureText for bool {
289    fn failure_text(&self) -> String {
290        if *self { "true" } else { "false" }.to_string()
291    }
292}
293
294impl FailureText for char {
295    fn failure_text(&self) -> String {
296        self.to_string()
297    }
298}
299
300impl FailureText for f64 {
301    fn failure_text(&self) -> String {
302        crate::fmt::fmt_f64(*self)
303    }
304}
305
306impl FailureText for crate::types::LogosInt {
307    fn failure_text(&self) -> String {
308        self.to_string()
309    }
310}
311
312impl<T: FailureText + ?Sized> FailureText for &T {
313    fn failure_text(&self) -> String {
314        (**self).failure_text()
315    }
316}
317
318impl<A: FailureText, B: FailureText> FailureText for (A, B) {
319    fn failure_text(&self) -> String {
320        format!("({}, {})", self.0.failure_text(), self.1.failure_text())
321    }
322}
323
324impl<A: FailureText, B: FailureText, C: FailureText> FailureText for (A, B, C) {
325    fn failure_text(&self) -> String {
326        format!(
327            "({}, {}, {})",
328            self.0.failure_text(),
329            self.1.failure_text(),
330            self.2.failure_text()
331        )
332    }
333}
334
335#[cfg(test)]
336#[path = "../../logicaffeine_tests/unit/logicaffeine_data/failure/tests.rs"]
337mod tests;