beck_core/
backend.rs

1//! The seam between `Core` and something that can execute it.
2//!
3//! # Why this exists
4//!
5//! `docs/05-tier-lowering.md` §5.2 says the `Core → Target` seam is what lets a backend slot in
6//! later, and `docs/04-compiler-architecture.md` §4.8 names a differential test *between backends*
7//! as a thing the project will need. Neither is possible while the host calls a particular
8//! evaluator by name.
9//!
10//! Until this module existed, `beck-rt` constructed `Interp` directly in four places. That is not a
11//! narrow Phase 1 — it is a Phase 1 whose successor is a refactor rather than an addition. The trait
12//! below is the whole interface a host needs, so a native backend is a new crate that implements it
13//! and a line that chooses it, and the two can be run against each other on the same program.
14//!
15//! # The shape, and why it is this small
16//!
17//! A host needs exactly two things from an executor: turn a closed `Core` expression into a value
18//! (the fold's initial state), and turn one denoting a function into something callable
19//! (`validate`, the fold, the view). Everything else — environments, closures, fuel — is a detail
20//! of *how* a backend executes, and a tree-walker and a JIT do not agree on any of it.
21//!
22//! So [`Backend::function`] returns a [`Callable`] rather than a backend-specific handle. There is
23//! no `call(handle, args)` method to downcast through, and no `Value::Closure` in the interface —
24//! that variant is the tree-walker's representation and a compiled backend would not produce one.
25
26use std::sync::Arc;
27
28use beck_diag::Span;
29
30use crate::core::{Core, Value};
31
32/// A failure while executing `Core`.
33///
34/// Carries a span because a language server has to survive evaluating half-written code, and
35/// because "folding at seq 41 failed" is not an answer without a location.
36#[derive(Clone, Debug)]
37pub struct ExecError {
38    pub message: String,
39    pub span: Span,
40    /// The value a `raise` failed with and that value's type name, when this failure is one the
41    /// program *chose* rather than a fault.
42    ///
43    /// A raise is a row label (§3.2, [`crate::row::Effect::Raises`]) and `try:` matches on the
44    /// type name, so a raise that crossed this seam as a message alone would arrive at a handler
45    /// as something it could not catch — the failure would be real and the `Result` the program
46    /// was checked against unreachable. It is on the seam rather than inside a backend for the
47    /// reason every other thing here is: a stub that fails is a `beck-rt` facility and `beck-rt`
48    /// may not name a backend crate (`docs/19` §19.9).
49    pub raised: Option<Box<(Arc<str>, Value)>>,
50}
51
52impl ExecError {
53    pub fn new(message: impl Into<String>, span: Span) -> ExecError {
54        ExecError {
55            message: message.into(),
56            span,
57            raised: None,
58        }
59    }
60
61    /// A failure a program chose: `raise Declined(…)`, carrying the value and its type.
62    pub fn raise(ty: Arc<str>, value: Value, span: Span) -> ExecError {
63        ExecError {
64            message: format!("raised `{}`", value.display()),
65            span,
66            raised: Some(Box::new((ty, value))),
67        }
68    }
69
70    /// The type name this failure raised, if it raised at all.
71    pub fn raised_type(&self) -> Option<&str> {
72        self.raised.as_ref().map(|r| r.0.as_ref())
73    }
74}
75
76impl std::fmt::Display for ExecError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.write_str(&self.message)
79    }
80}
81
82impl std::error::Error for ExecError {}
83
84/// A function a host can call, however the backend made it.
85///
86/// `'static` and `Send + Sync` because the runtime calls the fold from a sequencer task and the
87/// view from a connection task, and a backend that cannot survive that is not a backend for this
88/// runtime.
89pub type Callable = Arc<dyn Fn(Vec<Value>) -> Result<Value, ExecError> + Send + Sync>;
90
91/// Something that answers a call *instead of* the definition it names.
92///
93/// This exists for exactly one caller, and the reason it is on the seam rather than inside a
94/// backend is `docs/21-tests-in-beck-and-proof.md` §21.3: "**A mock is not a stand-in for an
95/// object. It is a value for an effect.**" A stub is therefore not a program transformation the
96/// compiler can do once — the *complete list* of what got stubbed has to be reportable per test,
97/// with the arguments each stubbed call was passed, because §21.3 rule 4 makes verification a query
98/// over what happened rather than an expectation set in advance.
99///
100/// A backend that cannot offer this says so by returning `None` from [`Backend::intercepting`], and
101/// the harness reports that stubs are unavailable rather than running the test and lying about it.
102pub trait Interceptor: Send + Sync {
103    /// Called before a top-level definition named `name` is applied to `args`. Returning `Some`
104    /// replaces the call; returning `None` runs the real body.
105    ///
106    /// The answer is a `Result` because a definition's failure is one of its answers: a stub
107    /// stands in for the definition, so it stands in for the `raises(E)` its signature already
108    /// declares (`docs/22` §22.6). `Err` carrying an [`ExecError::raise`] unwinds exactly as the
109    /// real body's `raise` would, so the program's own `try:` catches it; `Err` without one is a
110    /// fault in the stub itself.
111    fn intercept(&self, name: &str, args: &[Value]) -> Option<Result<Value, ExecError>>;
112}
113
114/// A backend's running count of what it has executed, if it keeps one.
115///
116/// # Why the seam carries this at all
117///
118/// [`crate::engine::Work`] counts what the *engine* does — functions applied, arrangement entries
119/// moved, pointwise operators re-evaluated — and one application is one application whatever that
120/// application goes on to do. When a plan's per-element function is a whole page, the counters say
121/// three and the clock says tenfold, and the failure is silent and flatters the plan that hides the
122/// most: every shape gate over an opaque operator was blind to exactly the pessimisation an opaque
123/// operator can hide.
124///
125/// The count has to come from whatever executed the code, so it comes through here. It is a
126/// **count**, not a duration, for the reason every other number a gate asserts on is:
127/// [`docs/13`](../../../../../docs/13-testing.md) §13.7 says a shared runner cannot hold a timing
128/// gate honestly.
129///
130/// # What a step is, and what it is not
131///
132/// Deliberately unspecified across backends. The tree-walker's is its own evaluation budget — a
133/// node, plus a charge per element for a primitive whose work is proportional to a length the
134/// caller chose — so it is *comparable between two runs of the same backend* and means nothing
135/// between two backends. A gate that reads it is asking "did this plan do more work than that
136/// one", never "how long did it take".
137pub trait Steps: Send + Sync {
138    /// Steps this backend has executed since it was created, monotonically.
139    fn taken(&self) -> u64;
140}
141
142/// A way to execute `Core`.
143pub trait Backend: Send + Sync {
144    /// What to call this in a diagnostic or on a dashboard. Two backends running differentially
145    /// need to be distinguishable in the report that says they disagreed.
146    fn name(&self) -> &'static str;
147
148    /// Reduce a closed expression to a value — the fold's initial accumulator.
149    fn constant(&self, code: &Core) -> Result<Value, ExecError>;
150
151    /// Prepare an expression denoting a function for calling.
152    ///
153    /// Called once per role at startup, so a backend that compiles is free to do the expensive
154    /// thing here rather than on every event.
155    fn function(&self, code: &Core) -> Result<Callable, ExecError>;
156
157    /// The same program, executed with an [`Interceptor`] consulted at every call of a top-level
158    /// definition. `None` — the default — means this backend cannot do it.
159    ///
160    /// Defaulted rather than required because it is not part of *executing a program*: a backend
161    /// that only ever runs an application in production has no reason to carry it, and the seam
162    /// should not grow a method every host must implement to serve one command.
163    fn intercepting(&self, _by: Arc<dyn Interceptor>) -> Option<Arc<dyn Backend>> {
164        None
165    }
166
167    /// How much host stack a thread must have before it calls into this backend.
168    ///
169    /// Zero — the default — means "whatever the caller has", which is the honest answer for a
170    /// backend that compiles to a machine-code loop and never nests host frames on the program's
171    /// recursion. A tree-walker does nest, and needs to say so: `docs/27` §27.2 records what
172    /// leaving it unsaid cost, which was a `SIGSEGV` where a diagnostic belonged.
173    ///
174    /// It is part of the seam rather than of one crate because the *runtime* is what spawns
175    /// threads and the runtime may not name a backend crate (`docs/19` §19.9). Asking the backend
176    /// it was handed is how it finds out without one.
177    fn stack_bytes(&self) -> usize {
178        0
179    }
180
181    /// This backend's step counter, if it keeps one — see [`Steps`].
182    ///
183    /// `None` — the default — is the honest answer for a backend that compiles to machine code and
184    /// has nothing to count without instrumenting what it emitted. A caller that needs the number
185    /// says so by refusing rather than by reading a zero as "no work", which is the failure this
186    /// exists to end.
187    fn steps(&self) -> Option<Arc<dyn Steps>> {
188        None
189    }
190}