Skip to main content

polydat_core/
convert.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Converting a value to a port type, the one way polydat does it
5//! (input_variance.md).
6//!
7//! A write into a kernel is never converted: a value that does not
8//! satisfy the input's declared type is refused. Where a host's values
9//! legitimately vary in type, the conversion is either the host's, made
10//! with [`to_port`] before the write, or a converter node in the
11//! program, which applies the same function when its input changes.
12//! Both use the boundary adapter catalog, so a value converts the same
13//! way on either side.
14
15use crate::ast::{NodeMeta, PolydatNode, Port, PortType, Slot, Value};
16
17/// Why a value could not be converted.
18#[derive(Debug, Clone, PartialEq)]
19pub struct ConvertError {
20    /// The value's type.
21    pub from: PortType,
22    /// The type asked for.
23    pub to: PortType,
24    /// What the conversion said, or that there is none.
25    pub reason: String,
26}
27
28impl std::fmt::Display for ConvertError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(
31            f,
32            "cannot convert {} to {}: {}",
33            self.from, self.to, self.reason
34        )
35    }
36}
37
38impl std::error::Error for ConvertError {}
39
40/// `value` as a value of type `to`, by the boundary adapter catalog.
41///
42/// A value that already satisfies `to` (its own type, or a carrier's
43/// bit-stuffed form) and `None` are returned unchanged. Otherwise the
44/// catalog's adapter from the value's type to `to` is applied; one that
45/// fails, such as a text that is not a number, is an error, as is a pair
46/// of types the catalog has no adapter for.
47pub fn to_port(value: Value, to: PortType) -> Result<Value, ConvertError> {
48    if to == PortType::Dyn || value.satisfies_slot(to) {
49        return Ok(value);
50    }
51    let from = value.port_type();
52    let Some(adapter) = crate::compile::assembly::boundary_adapter(from, to) else {
53        return Err(ConvertError {
54            from,
55            to,
56            reason: "no conversion between these types".into(),
57        });
58    };
59    // A failing adapter panics, as every node's failure does; caught
60    // here quietly and returned as the error it is.
61    let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
62    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
63        let mut outputs = vec![Value::None];
64        adapter.eval(&[value], &mut outputs);
65        outputs.remove(0)
66    }));
67    drop(capture);
68    match outcome {
69        Ok(converted) => Ok(converted),
70        Err(payload) => Err(ConvertError {
71            from,
72            to,
73            reason: crate::kernel::panic_payload_text(payload.as_ref()),
74        }),
75    }
76}
77
78/// The converter the compiler places in front of an input whose type
79/// may vary (input_variance.md ยง5): a `Dyn` input, the value as the
80/// host wrote it, and an output of the type its consumers read.
81pub(crate) struct InputConverter {
82    meta: NodeMeta,
83    input: String,
84    to: PortType,
85}
86
87impl InputConverter {
88    /// The converter of input `input` to `to`, named for both.
89    pub(crate) fn new(input: &str, to: PortType) -> Self {
90        Self {
91            meta: NodeMeta {
92                name: converter_name(input, to),
93                outs: vec![Port::new("output", to)],
94                ins: vec![Slot::Wire(Port::new("input", PortType::Dyn))],
95            },
96            input: input.to_string(),
97            to,
98        }
99    }
100}
101
102/// The node name of input `input`'s converter to `to`.
103pub(crate) fn converter_name(input: &str, to: PortType) -> String {
104    format!("__convert_{input}_{}", to.to_keyword())
105}
106
107/// Convert, or fail as the converter node, naming the input.
108fn convert_input(input: &str, value: Value, to: PortType) -> Value {
109    match to_port(value, to) {
110        Ok(v) => v,
111        Err(e) => panic!("input '{input}': {e}"),
112    }
113}
114
115impl PolydatNode for InputConverter {
116    fn meta(&self) -> &NodeMeta {
117        &self.meta
118    }
119
120    fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
121        outputs[0] = convert_input(&self.input, inputs[0].clone(), self.to);
122    }
123
124    /// The same conversion over slots: the input's pair names the value
125    /// the kernel stores for it, and the output is written by its type,
126    /// a scalar as its bits and a by-reference value into the step's
127    /// scratch.
128    fn compiled_slot(
129        &self,
130        _wire_types: &[PortType],
131        _engine: crate::compile::select::Engine,
132    ) -> Option<crate::ast::CompiledSlotKit> {
133        use crate::ast::SlotShape;
134        let (input, to) = (self.input.clone(), self.to);
135        Some(crate::ast::CompiledSlotKit {
136            scratch: to.scratch_elem().into_iter().collect(),
137            op: Box::new(
138                move |inputs: &[u64],
139                      outputs: &mut [u64],
140                      scratch: &mut [crate::ast::ScratchBuf]| {
141                    let value = crate::derive_support::read_poly(PortType::Dyn, inputs);
142                    let converted = convert_input(&input, value, to);
143                    crate::derive_support::write_poly(to, converted, scratch, outputs);
144                },
145            ),
146        })
147    }
148}