Skip to main content

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}