Skip to main content

hara_native/core/
native_result.rs

1use super::{
2    caught_error, map_entries, protocol_deref, protocol_deref_timeout, thrown_error, ExceptionInfo,
3    PromiseRejection, PromiseState, Value,
4};
5
6fn native_equal(left: &Value, right: &Value) -> bool {
7    left == right
8}
9use crate::lang::data::{Keyword, Map as PMap};
10use crate::lang::hash::{self as jh, JavaHash};
11use crate::lang::protocol::HashType;
12use std::cell::RefCell;
13use std::cmp::Ordering;
14use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::rc::Rc;
16use std::time::Duration;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub enum ResultStatus {
20    Success,
21    Error,
22}
23
24impl ResultStatus {
25    pub fn keyword(self) -> &'static str {
26        match self {
27            Self::Success => "success",
28            Self::Error => "error",
29        }
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct ResultValue {
35    pub status: ResultStatus,
36    pub data: Value,
37    pub error: Option<Rc<ExceptionInfo>>,
38    pub context: Value,
39}
40
41impl ResultValue {
42    pub fn success(data: Value, context: Value) -> Result<Self, String> {
43        Ok(Self {
44            status: ResultStatus::Success,
45            data,
46            error: None,
47            context: validate_context(context)?,
48        })
49    }
50
51    pub fn error(error: Value, context: Value) -> Result<Self, String> {
52        Ok(Self {
53            status: ResultStatus::Error,
54            data: Value::Nil,
55            error: Some(normalize_error(error)),
56            context: validate_context(context)?,
57        })
58    }
59
60    pub fn status_value(&self) -> Value {
61        Value::Keyword(Keyword::from(self.status.keyword()))
62    }
63
64    pub fn error_value(&self) -> Value {
65        self.error
66            .as_ref()
67            .map(|error| Value::ExceptionInfo(error.clone()))
68            .unwrap_or(Value::Nil)
69    }
70
71    pub fn is_success(&self) -> bool {
72        self.status == ResultStatus::Success
73    }
74
75    pub fn is_error(&self) -> bool {
76        self.status == ResultStatus::Error
77    }
78
79    pub fn is_timeout(&self) -> bool {
80        if !self.is_error() {
81            return false;
82        }
83        let Some(error) = &self.error else {
84            return false;
85        };
86        let code_key = Value::Keyword(Keyword::from("code"));
87        map_entries(error.data.as_ref()).is_some_and(|entries| {
88            entries.into_iter().any(|(key, value)| {
89                key == code_key
90                    && matches!(
91                        value,
92                        Value::Keyword(code) if code.as_str() == "result/timeout"
93                    )
94            })
95        })
96    }
97
98    pub fn with_context(&self, additional: Value) -> Result<Self, String> {
99        let additional = validate_context(additional)?;
100        let mut merged = PMap::new();
101        for (key, value) in map_entries(&self.context)
102            .expect("validated Result context")
103            .into_iter()
104            .chain(
105                map_entries(&additional)
106                    .expect("validated additional Result context")
107                    .into_iter(),
108            )
109        {
110            merged = merged.assoc_value(key, value);
111        }
112        let mut updated = self.clone();
113        updated.context = Value::Map(merged);
114        Ok(updated)
115    }
116
117    pub(crate) fn transport_context(&self) -> Value {
118        let display = Value::Keyword(Keyword::from("display"));
119        Value::Map(PMap::from_iter(
120            map_entries(&self.context)
121                .expect("validated Result context")
122                .into_iter()
123                .filter(|(key, _)| key != &display),
124        ))
125    }
126
127    pub(crate) fn deref_value(&self) -> Result<Value, String> {
128        match self.status {
129            ResultStatus::Success => Ok(self.data.clone()),
130            ResultStatus::Error => self
131                .error
132                .as_ref()
133                .map(|error| Err(thrown_error(Value::ExceptionInfo(error.clone()))))
134                .unwrap_or_else(|| Err("invalid Result/error without a native Error".into())),
135        }
136    }
137
138    pub fn display(&self) -> String {
139        format!(
140            "#hara/Result[{} {} {} {}]",
141            self.status_value().display(),
142            self.data.display(),
143            self.error_value().display(),
144            self.context.display()
145        )
146    }
147
148    pub fn compare(&self, other: &Self) -> Ordering {
149        self.status
150            .cmp(&other.status)
151            .then_with(|| self.data.cmp(&other.data))
152            .then_with(|| compare_error(self.error.as_deref(), other.error.as_deref()))
153    }
154
155    pub fn java_hash(&self, hash_type: HashType) -> i64 {
156        jh::compose_ordered(
157            "RESULT",
158            [
159                match self.status {
160                    ResultStatus::Success => 1,
161                    ResultStatus::Error => 2,
162                },
163                self.data.java_hash(hash_type),
164                self.error
165                    .as_deref()
166                    .map_or(0, |error| error_hash(error, hash_type)),
167            ],
168        )
169    }
170}
171
172const DEREF_UNSUPPORTED: &str = "IDeref/deref has no implementation for this value";
173const DEREF_TIMEOUT_UNSUPPORTED: &str =
174    "IDerefTimeout/deref-timeout expects a dereferenceable value, milliseconds, and timeout value";
175
176pub(super) fn synchronize_value(
177    value: Value,
178    timeout: Option<u64>,
179    context: Value,
180) -> Result<Value, String> {
181    let context = validate_context(context)?;
182    if let Value::Result(result) = value {
183        if map_entries(&context)
184            .expect("validated Result context")
185            .is_empty()
186        {
187            return Ok(Value::Result(result));
188        }
189        return Ok(Value::Result(Rc::new(result.with_context(context)?)));
190    }
191
192    let result = match value {
193        Value::Promise(promise) => synchronize_promise(promise, timeout, context)?,
194        value => match timeout {
195            Some(milliseconds) => synchronize_timed(value, milliseconds, context)?,
196            None => synchronize_untimed(value, context)?,
197        },
198    };
199    Ok(Value::Result(Rc::new(result)))
200}
201
202fn synchronize_untimed(value: Value, context: Value) -> Result<ResultValue, String> {
203    match protocol_deref(std::slice::from_ref(&value)) {
204        Ok(data) => ResultValue::success(data, context),
205        Err(error) if error == DEREF_UNSUPPORTED => ResultValue::success(value, context),
206        Err(error) => ResultValue::error(caught_error(&error), context),
207    }
208}
209
210fn synchronize_timed(
211    value: Value,
212    milliseconds: u64,
213    context: Value,
214) -> Result<ResultValue, String> {
215    let marker = Value::Array(Rc::new(RefCell::new(Vec::new())));
216    let milliseconds_value = Value::Number(i64::try_from(milliseconds).unwrap_or(i64::MAX));
217    match protocol_deref_timeout(&[value.clone(), milliseconds_value, marker.clone()]) {
218        Ok(resolved) if same_marker(&resolved, &marker) => {
219            timeout_result(milliseconds, context, None)
220        }
221        Ok(data) => ResultValue::success(data, context),
222        Err(error) if error == DEREF_TIMEOUT_UNSUPPORTED => {
223            if matches!(value, Value::Pointer(_)) {
224                timeout_unsupported_result(milliseconds, context)
225            } else {
226                ResultValue::success(value, context)
227            }
228        }
229        Err(error) => ResultValue::error(caught_error(&error), context),
230    }
231}
232
233fn synchronize_promise(
234    promise: super::Promise,
235    timeout: Option<u64>,
236    context: Value,
237) -> Result<ResultValue, String> {
238    let state = match timeout {
239        Some(milliseconds) => promise.wait_state_timeout(Duration::from_millis(milliseconds)),
240        None => promise.wait_state(),
241    };
242    match state {
243        PromiseState::Fulfilled(data) => ResultValue::success(data, context),
244        PromiseState::Rejected(error) => {
245            ResultValue::error(promise_rejection_value(error), context)
246        }
247        PromiseState::Pending => timeout_result(
248            timeout.expect("only timed Promise synchronization can remain pending"),
249            context,
250            Some(promise),
251        ),
252    }
253}
254
255fn promise_rejection_value(error: PromiseRejection) -> Value {
256    error.value()
257}
258
259fn timeout_result(
260    milliseconds: u64,
261    context: Value,
262    promise: Option<super::Promise>,
263) -> Result<ResultValue, String> {
264    let mut details = vec![
265        (
266            Value::Keyword(Keyword::from("result/timeout")),
267            Value::Number(i64::try_from(milliseconds).unwrap_or(i64::MAX)),
268        ),
269        (
270            Value::Keyword(Keyword::from("result/cancellation-requested")),
271            Value::Bool(promise.is_some()),
272        ),
273    ];
274
275    if let Some(promise) = promise {
276        match catch_unwind(AssertUnwindSafe(|| promise.cancel())) {
277            Ok(cancelled) => details.push((
278                Value::Keyword(Keyword::from("result/cancelled")),
279                Value::Bool(cancelled),
280            )),
281            Err(payload) => {
282                details.push((
283                    Value::Keyword(Keyword::from("result/cancelled")),
284                    Value::Bool(false),
285                ));
286                details.push((
287                    Value::Keyword(Keyword::from("result/cancellation-error")),
288                    Value::String(panic_message(payload)),
289                ));
290            }
291        }
292    }
293
294    ResultValue::error(
295        result_error(
296            "result/timeout",
297            "Result synchronization timed out",
298            milliseconds,
299        ),
300        context_with(context, details),
301    )
302}
303
304fn timeout_unsupported_result(milliseconds: u64, context: Value) -> Result<ResultValue, String> {
305    ResultValue::error(
306        result_error(
307            "result/timeout-unsupported",
308            "Timed synchronization is unsupported for this dereferenceable value",
309            milliseconds,
310        ),
311        context_with(
312            context,
313            [(
314                Value::Keyword(Keyword::from("result/timeout")),
315                Value::Number(i64::try_from(milliseconds).unwrap_or(i64::MAX)),
316            )],
317        ),
318    )
319}
320
321fn result_error(code: &str, message: &str, milliseconds: u64) -> Value {
322    Value::ExceptionInfo(Rc::new(ExceptionInfo {
323        message: message.into(),
324        data: Box::new(Value::Map(PMap::from_iter([
325            (
326                Value::Keyword(Keyword::from("code")),
327                Value::Keyword(Keyword::from(code)),
328            ),
329            (
330                Value::Keyword(Keyword::from("message")),
331                Value::String(message.into()),
332            ),
333            (
334                Value::Keyword(Keyword::from("timeout")),
335                Value::Number(i64::try_from(milliseconds).unwrap_or(i64::MAX)),
336            ),
337        ]))),
338        cause: None,
339        provenance: Rc::new(RefCell::new(Default::default())),
340    }))
341}
342
343fn context_with(context: Value, entries: impl IntoIterator<Item = (Value, Value)>) -> Value {
344    let mut merged = PMap::new();
345    for (key, value) in map_entries(&context).expect("validated Result context") {
346        merged = merged.assoc_value(key, value);
347    }
348    for (key, value) in entries {
349        merged = merged.assoc_value(key, value);
350    }
351    Value::Map(merged)
352}
353
354fn same_marker(left: &Value, right: &Value) -> bool {
355    matches!(
356        (left, right),
357        (Value::Array(left), Value::Array(right)) if Rc::ptr_eq(left, right)
358    )
359}
360
361fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
362    payload
363        .downcast_ref::<&str>()
364        .map(|message| (*message).to_owned())
365        .or_else(|| payload.downcast_ref::<String>().cloned())
366        .unwrap_or_else(|| "Promise cancellation panicked".into())
367}
368
369impl PartialEq for ResultValue {
370    fn eq(&self, other: &Self) -> bool {
371        self.status == other.status
372            && native_equal(&self.data, &other.data)
373            && error_equal(self.error.as_deref(), other.error.as_deref())
374    }
375}
376
377impl Eq for ResultValue {}
378
379fn validate_context(context: Value) -> Result<Value, String> {
380    map_entries(&context)
381        .is_some()
382        .then_some(context)
383        .ok_or_else(|| "Result context must be a map".into())
384}
385
386fn normalize_error(value: Value) -> Rc<ExceptionInfo> {
387    match value {
388        Value::ExceptionInfo(error) => error,
389        value => {
390            let message = match &value {
391                Value::String(text) => text.clone(),
392                _ => value.display(),
393            };
394            Rc::new(ExceptionInfo {
395                message,
396                data: Box::new(Value::Map(PMap::from_iter([(
397                    Value::Keyword(Keyword::from("error/value")),
398                    value,
399                )]))),
400                cause: None,
401                provenance: Rc::new(RefCell::new(Default::default())),
402            })
403        }
404    }
405}
406
407fn error_equal(left: Option<&ExceptionInfo>, right: Option<&ExceptionInfo>) -> bool {
408    match (left, right) {
409        (None, None) => true,
410        (Some(left), Some(right)) => {
411            left.message == right.message
412                && native_equal(&left.data, &right.data)
413                && match (&left.cause, &right.cause) {
414                    (None, None) => true,
415                    (Some(left), Some(right)) => native_equal(left, right),
416                    _ => false,
417                }
418        }
419        _ => false,
420    }
421}
422
423fn compare_error(left: Option<&ExceptionInfo>, right: Option<&ExceptionInfo>) -> Ordering {
424    match (left, right) {
425        (None, None) => Ordering::Equal,
426        (None, Some(_)) => Ordering::Less,
427        (Some(_), None) => Ordering::Greater,
428        (Some(left), Some(right)) => left
429            .message
430            .cmp(&right.message)
431            .then_with(|| left.data.cmp(&right.data))
432            .then_with(|| left.cause.cmp(&right.cause)),
433    }
434}
435
436fn error_hash(error: &ExceptionInfo, hash_type: HashType) -> i64 {
437    jh::compose_ordered(
438        "RESULT_ERROR",
439        [
440            jh::java_string_hash("hara/Error") as i64,
441            jh::java_string_hash(&error.message) as i64,
442            error.data.java_hash(hash_type),
443            error
444                .cause
445                .as_deref()
446                .map_or(0, |cause| cause.java_hash(hash_type)),
447        ],
448    )
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn context(key: &str, value: Value) -> Value {
456        Value::Map(PMap::from_iter([(
457            Value::Keyword(Keyword::from(key)),
458            value,
459        )]))
460    }
461
462    #[test]
463    fn native_result_equality_and_hash_ignore_context() {
464        let left = ResultValue::success(
465            Value::Number(42),
466            context("source", Value::String("left".into())),
467        )
468        .expect("left Result");
469        let right = ResultValue::success(
470            Value::Number(42),
471            context("source", Value::String("right".into())),
472        )
473        .expect("right Result");
474        assert_eq!(left, right);
475        assert_eq!(
476            left.java_hash(crate::lang::hash::DEFAULT_HASH),
477            right.java_hash(crate::lang::hash::DEFAULT_HASH)
478        );
479        assert_eq!(
480            left.deref_value().expect("success deref"),
481            Value::Number(42)
482        );
483    }
484
485    #[test]
486    fn native_result_context_merge_uses_supplied_keys() {
487        let result = ResultValue::success(
488            Value::Number(7),
489            Value::Map(PMap::from_iter([
490                (
491                    Value::Keyword(Keyword::from("source")),
492                    Value::String("left".into()),
493                ),
494                (Value::Keyword(Keyword::from("kept")), Value::Bool(true)),
495            ])),
496        )
497        .expect("Result");
498        let updated = result
499            .with_context(Value::Map(PMap::from_iter([
500                (
501                    Value::Keyword(Keyword::from("source")),
502                    Value::String("right".into()),
503                ),
504                (Value::Keyword(Keyword::from("added")), Value::Number(1)),
505            ])))
506            .expect("merged Result");
507        let source =
508            super::super::map_value(&updated.context, &Value::Keyword(Keyword::from("source")))
509                .expect("source context");
510        assert!(matches!(source, Value::String(value) if value.as_str() == "right"));
511        assert_eq!(result, updated);
512    }
513
514    #[test]
515    fn synchronize_raw_existing_and_nested_results() {
516        let raw = synchronize_value(Value::Number(42), None, Value::Map(PMap::new()))
517            .expect("raw synchronization");
518        let Value::Result(raw) = raw else {
519            panic!("expected Result");
520        };
521        assert!(raw.is_success());
522        assert_eq!(raw.data, Value::Number(42));
523
524        let existing = Rc::new(
525            ResultValue::success(
526                Value::Number(7),
527                context("source", Value::String("left".into())),
528            )
529            .expect("existing Result"),
530        );
531        let synchronized = synchronize_value(
532            Value::Result(existing.clone()),
533            None,
534            context("source", Value::String("right".into())),
535        )
536        .expect("existing synchronization");
537        let Value::Result(synchronized) = synchronized else {
538            panic!("expected Result");
539        };
540        assert_eq!(synchronized.as_ref(), existing.as_ref());
541        let source = super::super::map_value(
542            &synchronized.context,
543            &Value::Keyword(Keyword::from("source")),
544        )
545        .expect("source context");
546        assert!(matches!(source, Value::String(value) if value == "right"));
547
548        let promise = super::super::Promise::new();
549        promise.resolve(Value::Result(existing.clone()));
550        let wrapped = synchronize_value(Value::Promise(promise), None, Value::Map(PMap::new()))
551            .expect("nested synchronization");
552        let Value::Result(wrapped) = wrapped else {
553            panic!("expected Result");
554        };
555        assert!(matches!(
556            &wrapped.data,
557            Value::Result(value) if Rc::ptr_eq(value, &existing)
558        ));
559    }
560
561    #[test]
562    fn synchronize_captures_rejection_timeout_and_cancellation_failure() {
563        let error = Rc::new(ExceptionInfo {
564            message: "rejected".into(),
565            data: Box::new(context("code", Value::Keyword(Keyword::from("rejected")))),
566            cause: None,
567            provenance: Rc::new(RefCell::new(Default::default())),
568        });
569        let rejected = super::super::Promise::new();
570        rejected.reject_value(Value::ExceptionInfo(error.clone()));
571        let captured = synchronize_value(Value::Promise(rejected), None, Value::Map(PMap::new()))
572            .expect("rejection synchronization");
573        let Value::Result(captured) = captured else {
574            panic!("expected Result");
575        };
576        assert!(captured.is_error());
577        assert!(!captured.is_timeout());
578        assert!(matches!(
579            captured.error_value(),
580            Value::ExceptionInfo(value) if Rc::ptr_eq(&value, &error)
581        ));
582
583        let timed = super::super::Promise::new();
584        let timeout = synchronize_value(
585            Value::Promise(timed.clone()),
586            Some(0),
587            Value::Map(PMap::new()),
588        )
589        .expect("timeout synchronization");
590        let Value::Result(timeout) = timeout else {
591            panic!("expected Result");
592        };
593        assert!(timeout.is_error());
594        assert!(timeout.is_timeout());
595        let Value::ExceptionInfo(timeout_error) = timeout.error_value() else {
596            panic!("expected timeout Error");
597        };
598        let code = super::super::map_value(
599            timeout_error.data.as_ref(),
600            &Value::Keyword(Keyword::from("code")),
601        )
602        .expect("timeout code");
603        assert_eq!(code, &Value::Keyword(Keyword::from("result/timeout")));
604        assert!(matches!(timed.state(), PromiseState::Rejected(_)));
605
606        let cancellation_failure = super::super::Promise::new();
607        cancellation_failure.set_cancel_hook(Rc::new(|| panic!("cannot cancel")));
608        let timeout = synchronize_value(
609            Value::Promise(cancellation_failure),
610            Some(0),
611            Value::Map(PMap::new()),
612        )
613        .expect("cancellation failure synchronization");
614        let Value::Result(timeout) = timeout else {
615            panic!("expected Result");
616        };
617        assert!(super::super::map_value(
618            &timeout.context,
619            &Value::Keyword(Keyword::from("result/cancellation-error")),
620        )
621        .is_some());
622    }
623
624    #[test]
625    fn native_result_error_preserves_native_error_and_deref_throws() {
626        let error = Rc::new(ExceptionInfo {
627            message: "boom".into(),
628            data: Box::new(context("code", Value::Keyword(Keyword::from("boom")))),
629            cause: None,
630            provenance: Rc::new(RefCell::new(Default::default())),
631        });
632        let result =
633            ResultValue::error(Value::ExceptionInfo(error.clone()), Value::Map(PMap::new()))
634                .expect("error Result");
635        assert!(result.is_error());
636        let preserved = match result.error_value() {
637            Value::ExceptionInfo(preserved) => preserved,
638            other => panic!("expected native Error, got {}", other.display()),
639        };
640        assert_eq!(preserved.message, error.message);
641        assert_eq!(preserved.data.display(), error.data.display());
642        assert!(result.deref_value().is_err());
643        assert!(result.display().contains("#hara/Result[:error"));
644    }
645
646    #[test]
647    fn native_result_string_errors_keep_unquoted_messages() {
648        let result = ResultValue::error(Value::String("boom".into()), Value::Map(PMap::new()))
649            .expect("error Result");
650        let Value::ExceptionInfo(error) = result.error_value() else {
651            panic!("expected native Error");
652        };
653        assert_eq!(error.message, "boom");
654    }
655}