logicaffeine_compile/semantics/failcode.rs
1//! The failure codes a compiled module reports, each mapped onto its sentence in `logicaffeine_data::failure`,
2//! the one place every tier's failure wording lives.
3//!
4//! A runtime failure comes in two shapes. Most carry a message the compiler already wrote and
5//! interned (`Op::FailWith { msg: ConstIdx }`), so the wasm backend hands that Text straight to
6//! the host. But an INLINE check — a bounds test, a division guard — has no constant: the
7//! tree-walker builds its message while running, from a value that does not exist until then
8//! (`Index 9 out of bounds`). There is nothing to hand over.
9//!
10//! So those report a CODE and the offending VALUE, and the host renders the sentence.
11//!
12//! ★ WHICH PUTS THE WORDING IN A HOST AS WELL, AND THAT IS THE WHOLE DANGER. The browser saying
13//! something subtly different from the reference tier is exactly the drift every lock in this
14//! campaign exists to prevent. So [`message`] is what the interpreter formats with, what the Node
15//! host is generated from ([`js_cases`]), and what `runtime.js` is checked against
16//! (`fail_code_lock` reads its cases and holds them to this function). A new code that the host
17//! does not render fails that lock rather than reaching a visitor as a blank page.
18//!
19//! The codes are APPEND-ONLY for the same reason the host-import list is: the emitter is compiled
20//! into a module and the host ships separately, so renumbering silently re-labels every failure a
21//! previously-built module reports.
22
23use std::cell::RefCell;
24use std::rc::Rc;
25
26use logicaffeine_base::BigInt;
27
28use crate::interpreter::{ListRepr, RuntimeValue};
29
30/// Why a compiled module stopped, for the failures whose message needs a runtime value.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(i32)]
33pub enum FailCode {
34 /// `item i of xs` where `i` names no element. The value is the index the program asked for.
35 IndexOutOfBounds = 1,
36 /// An integer division or modulo by zero. The value is unused.
37 DivisionByZero = 2,
38 /// `text_from_bytes(bytes)` where the bytes are not valid UTF-8. The value is unused: the
39 /// reference tier appends the standard library's own description of the fault, which a
40 /// compiled module has no way to reproduce, so the module reports the sentence they share.
41 InvalidUtf8 = 3,
42 /// The allocator could not grow linear memory far enough for the next allocation. The value
43 /// is unused: there is nothing more specific to report than the shape of the failure itself.
44 OutOfMemory = 4,
45 /// `item i of xs` on a list, where `i` is an integer past a machine word. The value is unused: the
46 /// reference tier names the index's type, not its digits.
47 ListIndexTooWide = 5,
48 /// `item i of t` on a Text, where `i` is an integer past a machine word. The value is unused.
49 TextIndexTooWide = 6,
50 /// `Set item i of xs to v`, where `i` is an integer past a machine word. The value is unused.
51 ListStoreIndexTooWide = 7,
52 /// An integer past a machine word reaching a place the module holds a machine word: an element, a
53 /// field, a builtin's argument. The value is unused.
54 IntTooWide = 8,
55}
56
57impl FailCode {
58 /// The wire value the module reports. Append-only.
59 pub fn code(self) -> i32 {
60 self as i32
61 }
62}
63
64/// Every code, in wire order — so a lock can enumerate them rather than trusting a hand-written list.
65pub const ALL: &[FailCode] = &[
66 FailCode::IndexOutOfBounds,
67 FailCode::DivisionByZero,
68 FailCode::InvalidUtf8,
69 FailCode::OutOfMemory,
70 FailCode::ListIndexTooWide,
71 FailCode::TextIndexTooWide,
72 FailCode::ListStoreIndexTooWide,
73 FailCode::IntTooWide,
74];
75
76/// Every code's sentence as JavaScript: one `case` arm per code assigning `target`, then a `default` arm,
77/// with the offending value spliced in from the variable `value` (a JS string) and an unknown code from the
78/// variable `code`. Rendered from [`message`] itself, so a host written out as JavaScript cannot word a
79/// failure differently from the reference tier.
80pub fn js_cases(target: &str, value: &str, code: &str) -> String {
81 const VALUE: i64 = -7_919_000_000_000_000_007;
82 const UNKNOWN: i32 = -7919;
83 let js = |text: String, hole: &str, var: &str| {
84 let escaped = text
85 .replace('\\', "\\\\")
86 .replace('\'', "\\'")
87 .replace('\n', "\\n");
88 format!("'{}'", escaped.replace(hole, &format!("' + {var} + '")))
89 };
90 let mut arms: Vec<String> = ALL
91 .iter()
92 .map(|c| {
93 format!(
94 "case {}: {target} = {}; break;",
95 c.code(),
96 js(message(c.code(), VALUE), &VALUE.to_string(), value)
97 )
98 })
99 .collect();
100 arms.push(format!(
101 "default: {target} = {};",
102 js(message(UNKNOWN, 0), &UNKNOWN.to_string(), code)
103 ));
104 arms.join(" ")
105}
106
107/// The sentence a failure reports, from its code and the value that caused it.
108///
109/// This is the single definition: the interpreter formats with it, and the browser host is held
110/// to it. An unknown code is named rather than guessed — a module built after the host would
111/// otherwise report a plausible-looking wrong reason.
112pub fn message(code: i32, value: i64) -> String {
113 match code {
114 c if c == FailCode::IndexOutOfBounds.code() => {
115 logicaffeine_data::failure::index_out_of_bounds(value)
116 }
117 c if c == FailCode::DivisionByZero.code() => {
118 logicaffeine_data::failure::DIVISION_BY_ZERO.to_string()
119 }
120 c if c == FailCode::InvalidUtf8.code() => "text_from_bytes(): invalid UTF-8".to_string(),
121 c if c == FailCode::OutOfMemory.code() => "Out of memory".to_string(),
122 c if c == FailCode::ListIndexTooWide.code() => wide_index_refusal(empty_list()),
123 c if c == FailCode::TextIndexTooWide.code() => {
124 wide_index_refusal(RuntimeValue::Text(Rc::new(String::new())))
125 }
126 c if c == FailCode::ListStoreIndexTooWide.code() => {
127 crate::semantics::collections::index_set(
128 &empty_list(),
129 &past_the_word(),
130 RuntimeValue::Nothing,
131 )
132 .expect_err("a list refuses a store at an index past a machine word")
133 }
134 c if c == FailCode::IntTooWide.code() => {
135 logicaffeine_data::failure::INT_TOO_WIDE.to_string()
136 }
137 other => format!("the program failed with an unrecognized code {other}"),
138 }
139}
140
141/// An integer one past the largest machine word.
142fn past_the_word() -> RuntimeValue {
143 RuntimeValue::from_bigint(BigInt::from_i64(i64::MAX).add(&BigInt::from_i64(1)))
144}
145
146fn empty_list() -> RuntimeValue {
147 RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(Vec::new()))))
148}
149
150/// What the reference tier says when `collection` is indexed by an integer past a machine word: its own
151/// sentence, asked of the function it indexes with.
152fn wide_index_refusal(collection: RuntimeValue) -> String {
153 crate::semantics::collections::index_get(&collection, &past_the_word())
154 .expect_err("a collection refuses an index past a machine word")
155}