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}
41
42impl ExecError {
43 pub fn new(message: impl Into<String>, span: Span) -> ExecError {
44 ExecError {
45 message: message.into(),
46 span,
47 }
48 }
49}
50
51impl std::fmt::Display for ExecError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.write_str(&self.message)
54 }
55}
56
57impl std::error::Error for ExecError {}
58
59/// A function a host can call, however the backend made it.
60///
61/// `'static` and `Send + Sync` because the runtime calls the fold from a sequencer task and the
62/// view from a connection task, and a backend that cannot survive that is not a backend for this
63/// runtime.
64pub type Callable = Arc<dyn Fn(Vec<Value>) -> Result<Value, ExecError> + Send + Sync>;
65
66/// Something that answers a call *instead of* the definition it names.
67///
68/// This exists for exactly one caller, and the reason it is on the seam rather than inside a
69/// backend is `docs/21-tests-in-beck-and-proof.md` §21.3: "**A mock is not a stand-in for an
70/// object. It is a value for an effect.**" A stub is therefore not a program transformation the
71/// compiler can do once — the *complete list* of what got stubbed has to be reportable per test,
72/// with the arguments each stubbed call was passed, because §21.3 rule 4 makes verification a query
73/// over what happened rather than an expectation set in advance.
74///
75/// A backend that cannot offer this says so by returning `None` from [`Backend::intercepting`], and
76/// the harness reports that stubs are unavailable rather than running the test and lying about it.
77pub trait Interceptor: Send + Sync {
78 /// Called before a top-level definition named `name` is applied to `args`. Returning `Some`
79 /// replaces the call; returning `None` runs the real body.
80 fn intercept(&self, name: &str, args: &[Value]) -> Option<Value>;
81}
82
83/// A way to execute `Core`.
84pub trait Backend: Send + Sync {
85 /// What to call this in a diagnostic or on a dashboard. Two backends running differentially
86 /// need to be distinguishable in the report that says they disagreed.
87 fn name(&self) -> &'static str;
88
89 /// Reduce a closed expression to a value — the fold's initial accumulator.
90 fn constant(&self, code: &Core) -> Result<Value, ExecError>;
91
92 /// Prepare an expression denoting a function for calling.
93 ///
94 /// Called once per role at startup, so a backend that compiles is free to do the expensive
95 /// thing here rather than on every event.
96 fn function(&self, code: &Core) -> Result<Callable, ExecError>;
97
98 /// The same program, executed with an [`Interceptor`] consulted at every call of a top-level
99 /// definition. `None` — the default — means this backend cannot do it.
100 ///
101 /// Defaulted rather than required because it is not part of *executing a program*: a backend
102 /// that only ever runs an application in production has no reason to carry it, and the seam
103 /// should not grow a method every host must implement to serve one command.
104 fn intercepting(&self, _by: Arc<dyn Interceptor>) -> Option<Arc<dyn Backend>> {
105 None
106 }
107
108 /// How much host stack a thread must have before it calls into this backend.
109 ///
110 /// Zero — the default — means "whatever the caller has", which is the honest answer for a
111 /// backend that compiles to a machine-code loop and never nests host frames on the program's
112 /// recursion. A tree-walker does nest, and needs to say so: `docs/31` §31.3 records what
113 /// leaving it unsaid cost, which was a `SIGSEGV` where a diagnostic belonged.
114 ///
115 /// It is part of the seam rather than of one crate because the *runtime* is what spawns
116 /// threads and the runtime may not name a backend crate (`docs/19` §19.9). Asking the backend
117 /// it was handed is how it finds out without one.
118 fn stack_bytes(&self) -> usize {
119 0
120 }
121}