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}