Skip to main content

geam_core/runtime/error/
host.rs

1use crate::host::HostFailure;
2use crate::plan::{FunctionType, HostCallSite, SourceContext};
3use camino::Utf8PathBuf;
4use ecow::EcoString;
5use miette::NamedSource;
6use std::fmt;
7
8#[derive(Debug, Clone)]
9pub struct HostError {
10    package: EcoString,
11    module: EcoString,
12    function: EcoString,
13    signature: FunctionType,
14    failure: HostFailure,
15    location: HostLocation,
16    source: Option<Box<NamedSource<String>>>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum HostLocation {
21    Resolved {
22        site: HostCallSite,
23        path: Utf8PathBuf,
24        line: usize,
25    },
26    Site(HostCallSite),
27    Host {
28        caller: HostOrigin,
29    },
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct HostOrigin {
34    package: EcoString,
35    module: EcoString,
36    function: EcoString,
37    signature: FunctionType,
38}
39
40#[derive(Clone)]
41pub(crate) enum HostCallOrigin {
42    Entry,
43    Source(HostCallSite),
44    Host(HostOrigin),
45}
46
47impl HostError {
48    pub(crate) fn new(
49        package: EcoString,
50        module: EcoString,
51        function: EcoString,
52        signature: FunctionType,
53        failure: HostFailure,
54        site: HostCallSite,
55        source_context: Option<&SourceContext>,
56    ) -> Self {
57        let location = HostLocation::from_context(site, source_context);
58        let source = source_context
59            .map(SourceContext::named_source)
60            .map(Box::new);
61        Self {
62            package,
63            module,
64            function,
65            signature,
66            failure,
67            location,
68            source,
69        }
70    }
71
72    pub(crate) fn new_from_host(
73        package: EcoString,
74        module: EcoString,
75        function: EcoString,
76        signature: FunctionType,
77        failure: HostFailure,
78        caller: HostOrigin,
79    ) -> Self {
80        Self {
81            package,
82            module,
83            function,
84            signature,
85            failure,
86            location: HostLocation::Host { caller },
87            source: None,
88        }
89    }
90
91    pub fn package(&self) -> &EcoString {
92        &self.package
93    }
94
95    pub fn module(&self) -> &EcoString {
96        &self.module
97    }
98
99    pub fn function(&self) -> &EcoString {
100        &self.function
101    }
102
103    pub fn signature(&self) -> &FunctionType {
104        &self.signature
105    }
106
107    pub fn failure(&self) -> &HostFailure {
108        &self.failure
109    }
110
111    pub fn location(&self) -> &HostLocation {
112        &self.location
113    }
114
115    pub(in crate::runtime::error) fn source(&self) -> Option<&NamedSource<String>> {
116        self.source.as_deref()
117    }
118
119    pub(in crate::runtime::error) fn primary_label(&self) -> String {
120        format!(
121            "host function {}::{}.{} failed",
122            self.package, self.module, self.function,
123        )
124    }
125}
126
127impl HostLocation {
128    pub fn site(&self) -> Option<&HostCallSite> {
129        match self {
130            Self::Resolved { site, .. } | Self::Site(site) => Some(site),
131            Self::Host { .. } => None,
132        }
133    }
134
135    pub fn path(&self) -> Option<&Utf8PathBuf> {
136        match self {
137            Self::Resolved { path, .. } => Some(path),
138            Self::Site(_) | Self::Host { .. } => None,
139        }
140    }
141
142    pub fn line(&self) -> Option<usize> {
143        match self {
144            Self::Resolved { line, .. } => Some(*line),
145            Self::Site(_) | Self::Host { .. } => None,
146        }
147    }
148
149    pub fn caller(&self) -> Option<&HostOrigin> {
150        match self {
151            Self::Host { caller } => Some(caller),
152            Self::Resolved { .. } | Self::Site(_) => None,
153        }
154    }
155
156    fn from_context(site: HostCallSite, context: Option<&SourceContext>) -> Self {
157        match context {
158            Some(context) => {
159                let line = context
160                    .source()
161                    .as_bytes()
162                    .iter()
163                    .take(site.span().start())
164                    .filter(|byte| **byte == b'\n')
165                    .count()
166                    + 1;
167                Self::Resolved {
168                    site,
169                    path: context.path().clone(),
170                    line,
171                }
172            }
173            None => Self::Site(site),
174        }
175    }
176}
177
178impl HostOrigin {
179    fn new(
180        package: EcoString,
181        module: EcoString,
182        function: EcoString,
183        signature: FunctionType,
184    ) -> Self {
185        Self {
186            package,
187            module,
188            function,
189            signature,
190        }
191    }
192
193    pub fn package(&self) -> &EcoString {
194        &self.package
195    }
196
197    pub fn module(&self) -> &EcoString {
198        &self.module
199    }
200
201    pub fn function(&self) -> &EcoString {
202        &self.function
203    }
204
205    pub fn signature(&self) -> &FunctionType {
206        &self.signature
207    }
208}
209
210impl HostCallOrigin {
211    pub(crate) fn source(site: HostCallSite) -> Self {
212        Self::Source(site)
213    }
214
215    pub(crate) fn host(function: &crate::plan::execution::host::HostedFunctionMetadata) -> Self {
216        Self::Host(HostOrigin::new(
217            function.package().clone(),
218            function.module().clone(),
219            function.name().clone(),
220            function.signature().clone(),
221        ))
222    }
223
224    pub(crate) fn into_source_site(
225        self,
226        declaration: &HostCallSite,
227    ) -> Result<HostCallSite, HostOrigin> {
228        match self {
229            Self::Entry => Ok(declaration.clone()),
230            Self::Source(site) => Ok(site),
231            Self::Host(caller) => Err(caller),
232        }
233    }
234}
235
236impl PartialEq for HostError {
237    fn eq(&self, other: &Self) -> bool {
238        self.package == other.package
239            && self.module == other.module
240            && self.function == other.function
241            && self.signature == other.signature
242            && self.failure == other.failure
243            && self.location == other.location
244            && named_source_eq(self.source(), other.source())
245    }
246}
247
248impl Eq for HostError {}
249
250fn named_source_eq(
251    left: Option<&NamedSource<String>>,
252    right: Option<&NamedSource<String>>,
253) -> bool {
254    left.map(|source| (source.name(), source.inner()))
255        == right.map(|source| (source.name(), source.inner()))
256}
257
258impl fmt::Display for HostError {
259    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(
261            formatter,
262            "host function {}::{}.{} failed: {}",
263            self.package, self.module, self.function, self.failure,
264        )
265    }
266}
267
268impl std::error::Error for HostError {}
269
270#[cfg(test)]
271mod tests {
272    use super::{HostCallOrigin, HostError, HostLocation};
273    use crate::host::HostFailure;
274    use crate::plan::{FunctionType, HostCallSite, SourceContext, SourceSpan, ValueType};
275
276    #[test]
277    fn host_error_owns_identity_signature_failure_and_resolved_location() {
278        let source = SourceContext::new("src/main.gleam", "pub fn main() {\n  math.add(1, 2)\n}");
279        let site = HostCallSite::new("main".into(), "main".into(), SourceSpan::new(18, 32));
280        let error = HostError::new(
281            "host_support".into(),
282            "host/math".into(),
283            "add".into(),
284            FunctionType::new(vec![ValueType::Int, ValueType::Int], ValueType::Int),
285            HostFailure::new("unavailable"),
286            site.clone(),
287            Some(&source),
288        );
289
290        assert_eq!(error.package(), "host_support");
291        assert_eq!(error.module(), "host/math");
292        assert_eq!(error.function(), "add");
293        assert_eq!(
294            error.signature(),
295            &FunctionType::new(vec![ValueType::Int, ValueType::Int], ValueType::Int),
296        );
297        assert_eq!(error.failure().message(), "unavailable");
298        assert_eq!(error.location().site(), Some(&site));
299        assert_eq!(error.location().caller(), None);
300        assert_eq!(error.location().path(), Some(source.path()));
301        assert_eq!(error.location().line(), Some(2));
302        assert_eq!(
303            error.to_string(),
304            "host function host_support::host/math.add failed: unavailable",
305        );
306    }
307
308    #[test]
309    fn source_less_host_error_retains_site_only_location() {
310        let site = HostCallSite::new("main".into(), "main".into(), SourceSpan::new(4, 8));
311        let error = HostError::new(
312            "host_support".into(),
313            "host/math".into(),
314            "add".into(),
315            FunctionType::new(Vec::new(), ValueType::Int),
316            HostFailure::new("unavailable"),
317            site.clone(),
318            None,
319        );
320
321        assert_eq!(error.location(), &HostLocation::Site(site.clone()));
322        assert_eq!(error.location().site(), Some(&site));
323        assert_eq!(error.location().caller(), None);
324        assert_eq!(error.location().path(), None);
325        assert_eq!(error.location().line(), None);
326    }
327
328    #[test]
329    fn host_call_origin_uses_the_declaration_or_exact_source_site() {
330        let declaration =
331            HostCallSite::new("host/math".into(), "add".into(), SourceSpan::new(2, 5));
332        let source = HostCallSite::new("main".into(), "main".into(), SourceSpan::new(20, 34));
333
334        assert_eq!(
335            HostCallOrigin::Entry.into_source_site(&declaration),
336            Ok(declaration.clone()),
337        );
338        assert_eq!(
339            HostCallOrigin::source(source.clone()).into_source_site(&declaration),
340            Ok(source),
341        );
342    }
343
344    #[test]
345    fn host_origin_location_preserves_the_invoking_host_identity() {
346        let caller = super::HostOrigin::new(
347            "application".into(),
348            "host/outer".into(),
349            "apply".into(),
350            FunctionType::new(vec![ValueType::Int], ValueType::Int),
351        );
352        let error = HostError::new_from_host(
353            "application".into(),
354            "host/inner".into(),
355            "increment".into(),
356            FunctionType::new(vec![ValueType::Int], ValueType::Int),
357            HostFailure::new("unavailable"),
358            caller.clone(),
359        );
360
361        assert_eq!(
362            error.location(),
363            &HostLocation::Host {
364                caller: caller.clone(),
365            },
366        );
367        assert_eq!(error.location().site(), None);
368        assert_eq!(error.location().path(), None);
369        assert_eq!(error.location().line(), None);
370        assert_eq!(error.location().caller(), Some(&caller));
371        assert_eq!(caller.package(), "application");
372        assert_eq!(caller.module(), "host/outer");
373        assert_eq!(caller.function(), "apply");
374        assert_eq!(
375            caller.signature(),
376            &FunctionType::new(vec![ValueType::Int], ValueType::Int),
377        );
378        let declaration = HostCallSite::new(
379            "host/inner".into(),
380            "increment".into(),
381            SourceSpan::new(0, 0),
382        );
383        assert_eq!(
384            HostCallOrigin::Host(caller.clone()).into_source_site(&declaration),
385            Err(caller),
386        );
387    }
388
389    #[test]
390    fn host_error_equality_includes_owned_source_context() {
391        let site = HostCallSite::new("main".into(), "main".into(), SourceSpan::new(18, 32));
392        let source = SourceContext::new("src/main.gleam", "pub fn main() {\n  fail()\n}");
393        let different_source =
394            SourceContext::new("src/other.gleam", "pub fn main() {\n  fail()\n}");
395        let resolved = HostError::new(
396            "host_support".into(),
397            "host/math".into(),
398            "fail".into(),
399            FunctionType::new(Vec::new(), ValueType::Int),
400            HostFailure::new("unavailable"),
401            site.clone(),
402            Some(&source),
403        );
404        let different = HostError::new(
405            "host_support".into(),
406            "host/math".into(),
407            "fail".into(),
408            FunctionType::new(Vec::new(), ValueType::Int),
409            HostFailure::new("unavailable"),
410            site.clone(),
411            Some(&different_source),
412        );
413        let site_only = HostError::new(
414            "host_support".into(),
415            "host/math".into(),
416            "fail".into(),
417            FunctionType::new(Vec::new(), ValueType::Int),
418            HostFailure::new("unavailable"),
419            site,
420            None,
421        );
422
423        assert_eq!(resolved, resolved.clone());
424        assert_ne!(resolved, different);
425        assert_eq!(site_only, site_only.clone());
426        assert_ne!(resolved, site_only);
427        assert_ne!(site_only, resolved);
428    }
429}