Skip to main content

sim_lib_numbers_signal/
runtime.rs

1//! Loadable Lisp-facing transform callable.
2
3use std::{any::Any, sync::Arc};
4
5use sim_kernel::{
6    AbiVersion, Args, Callable, ClassRef, Cx, DefaultFactory, Dependency, Error, Export, Expr,
7    Factory, Lib, LibManifest, LibTarget, Linker, NumberLiteral, Object, RawArgs, Result, Symbol,
8    Value, Version, force_list_to_vec,
9};
10
11use crate::{
12    DctType, Direction, DstType, Normalization, SignalBuffer, SignalError, SignalView,
13    SpectrumPacking, TransformKind, TransformPlan,
14    runtime_convolution_callable::{
15        load_operations as load_convolution_operations,
16        operation_symbols as convolution_operation_symbols,
17    },
18    runtime_spectral_callable::{load_spectral_operations, spectral_symbols},
19    transform,
20};
21
22/// Symbol of the Lisp-facing transform operation (`signal/transform`).
23pub fn signal_transform_symbol() -> Symbol {
24    Symbol::qualified("signal", "transform")
25}
26
27#[derive(Clone)]
28struct SignalTransformFunction;
29
30impl Object for SignalTransformFunction {
31    fn display(&self, _cx: &mut Cx) -> Result<String> {
32        Ok(format!("#<function {}>", signal_transform_symbol()))
33    }
34
35    fn as_any(&self) -> &dyn Any {
36        self
37    }
38}
39
40impl sim_kernel::ObjectCompat for SignalTransformFunction {
41    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
42        if let Some(value) = cx
43            .registry()
44            .class_by_symbol(&Symbol::qualified("core", "Function"))
45        {
46            return Ok(value.clone());
47        }
48        DefaultFactory.class_stub(
49            sim_kernel::CORE_FUNCTION_CLASS_ID,
50            Symbol::qualified("core", "Function"),
51        )
52    }
53
54    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
55        Ok(Expr::Symbol(signal_transform_symbol()))
56    }
57
58    fn as_callable(&self) -> Option<&dyn Callable> {
59        Some(self)
60    }
61}
62
63impl Callable for SignalTransformFunction {
64    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
65        call_signal_transform(cx, args)
66    }
67
68    fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
69        let values = args.into_exprs();
70        let [kind, direction, normalization, packing, len, input] = values.as_slice() else {
71            return Err(argument_error());
72        };
73        let options = ParsedOptions {
74            kind: option_expr(kind, "kind")?,
75            direction: option_expr(direction, "direction")?,
76            normalization: option_expr(normalization, "normalization")?,
77            packing: option_expr(packing, "packing")?,
78        };
79        let len = cx.eval_expr(len.clone())?;
80        let input = cx.eval_expr(input.clone())?;
81        execute(cx, options, &len, &input)
82    }
83}
84
85/// Calls `signal/transform` with evaluated symbol options, a length, and a
86/// real list or list of complex `(real imag)` pairs.
87pub fn call_signal_transform(cx: &mut Cx, args: Args) -> Result<Value> {
88    let values = args.into_vec();
89    let [kind, direction, normalization, packing, len, input] = values.as_slice() else {
90        return Err(argument_error());
91    };
92    let options = ParsedOptions {
93        kind: option_value(cx, kind, "kind")?,
94        direction: option_value(cx, direction, "direction")?,
95        normalization: option_value(cx, normalization, "normalization")?,
96        packing: option_value(cx, packing, "packing")?,
97    };
98    execute(cx, options, len, input)
99}
100
101struct ParsedOptions {
102    kind: String,
103    direction: String,
104    normalization: String,
105    packing: String,
106}
107
108fn execute(cx: &mut Cx, options: ParsedOptions, len: &Value, input: &Value) -> Result<Value> {
109    let kind = parse_kind(&options.kind)?;
110    let direction = parse_direction(&options.direction)?;
111    let normalization = parse_normalization(&options.normalization)?;
112    let packing = parse_packing(&options.packing)?;
113    let len = value_to_usize(cx, len, "len")?;
114    let mut plan = TransformPlan::new(kind, len);
115    plan.direction = direction;
116    plan.normalization = normalization;
117    plan.packing = packing;
118
119    let output = match (kind, direction) {
120        (TransformKind::Dft | TransformKind::Fft, _)
121        | (TransformKind::RealFft, Direction::Inverse) => {
122            let input = value_to_complex_list(cx, input)?;
123            transform(&plan, SignalView::Complex(&input))
124        }
125        (TransformKind::RealFft | TransformKind::Dct(_) | TransformKind::Dst(_), _) => {
126            let input = value_to_real_list(cx, input)?;
127            transform(&plan, SignalView::Real(&input))
128        }
129    }
130    .map_err(signal_error_to_kernel)?;
131    buffer_to_value(cx, output)
132}
133
134/// Loadable runtime library exporting [`signal_transform_symbol`].
135pub struct SignalNumbersLib;
136
137impl SignalNumbersLib {
138    /// Creates the stateless signal-transform library.
139    pub fn new() -> Self {
140        Self
141    }
142}
143
144impl Default for SignalNumbersLib {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl Lib for SignalNumbersLib {
151    fn manifest(&self) -> LibManifest {
152        LibManifest {
153            id: Symbol::qualified("numbers", "signal"),
154            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
155            abi: AbiVersion { major: 0, minor: 1 },
156            target: LibTarget::HostRegistered,
157            requires: Vec::<Dependency>::new(),
158            capabilities: Vec::new(),
159            exports: std::iter::once(signal_transform_symbol())
160                .chain(convolution_operation_symbols())
161                .chain(spectral_symbols())
162                .map(|symbol| Export::Function {
163                    symbol,
164                    function_id: None,
165                })
166                .collect(),
167        }
168    }
169
170    fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
171        linker.function_value(
172            signal_transform_symbol(),
173            DefaultFactory.opaque(Arc::new(SignalTransformFunction))?,
174        )?;
175        load_convolution_operations(linker)?;
176        load_spectral_operations(linker)?;
177        Ok(())
178    }
179}
180
181fn parse_kind(name: &str) -> Result<TransformKind> {
182    match name {
183        "dft" => Ok(TransformKind::Dft),
184        "fft" => Ok(TransformKind::Fft),
185        "rfft" => Ok(TransformKind::RealFft),
186        "dct-i" => Ok(TransformKind::Dct(DctType::I)),
187        "dct-ii" => Ok(TransformKind::Dct(DctType::II)),
188        "dct-iii" => Ok(TransformKind::Dct(DctType::III)),
189        "dct-iv" => Ok(TransformKind::Dct(DctType::IV)),
190        "dst-i" => Ok(TransformKind::Dst(DstType::I)),
191        "dst-ii" => Ok(TransformKind::Dst(DstType::II)),
192        "dst-iii" => Ok(TransformKind::Dst(DstType::III)),
193        "dst-iv" => Ok(TransformKind::Dst(DstType::IV)),
194        _ => Err(Error::Eval(format!(
195            "signal/transform unsupported kind {name}"
196        ))),
197    }
198}
199
200fn parse_direction(name: &str) -> Result<Direction> {
201    match name {
202        "forward" => Ok(Direction::Forward),
203        "inverse" => Ok(Direction::Inverse),
204        _ => Err(Error::Eval(format!(
205            "signal/transform direction must be forward or inverse, got {name}"
206        ))),
207    }
208}
209
210fn parse_normalization(name: &str) -> Result<Normalization> {
211    match name {
212        "none" => Ok(Normalization::None),
213        "forward" => Ok(Normalization::Forward),
214        "inverse" => Ok(Normalization::Inverse),
215        "orthonormal" => Ok(Normalization::Orthonormal),
216        _ => Err(Error::Eval(format!(
217            "signal/transform unsupported normalization {name}"
218        ))),
219    }
220}
221
222fn parse_packing(name: &str) -> Result<SpectrumPacking> {
223    match name {
224        "full" => Ok(SpectrumPacking::Full),
225        "hermitian-half" => Ok(SpectrumPacking::HermitianHalf),
226        _ => Err(Error::Eval(format!(
227            "signal/transform packing must be full or hermitian-half, got {name}"
228        ))),
229    }
230}
231
232fn option_expr(expr: &Expr, name: &str) -> Result<String> {
233    let Expr::Symbol(symbol) = expr else {
234        return Err(Error::Eval(format!(
235            "signal/transform {name} must be an unquoted option symbol"
236        )));
237    };
238    Ok(symbol.as_qualified_str().to_owned())
239}
240
241fn option_value(cx: &mut Cx, value: &Value, name: &str) -> Result<String> {
242    let Expr::Symbol(symbol) = value.object().as_expr(cx)? else {
243        return Err(Error::Eval(format!(
244            "signal/transform {name} must be a symbol"
245        )));
246    };
247    Ok(symbol.as_qualified_str().to_owned())
248}
249
250fn value_to_usize(cx: &mut Cx, value: &Value, name: &str) -> Result<usize> {
251    let literal = value_to_number(cx, value, name)?;
252    literal.canonical.parse::<usize>().map_err(|_| {
253        Error::Eval(format!(
254            "signal/transform {name} must be a non-negative integer"
255        ))
256    })
257}
258
259fn value_to_real_list(cx: &mut Cx, value: &Value) -> Result<Vec<f64>> {
260    value_to_list(cx, value, "input")?
261        .iter()
262        .enumerate()
263        .map(|(index, value)| {
264            value_to_number(cx, value, &format!("input[{index}]"))?
265                .canonical
266                .parse::<f64>()
267                .map_err(|_| {
268                    Error::Eval(format!(
269                        "signal/transform input[{index}] must be an f64-compatible number"
270                    ))
271                })
272        })
273        .collect()
274}
275
276fn value_to_complex_list(cx: &mut Cx, value: &Value) -> Result<Vec<(f64, f64)>> {
277    value_to_list(cx, value, "input")?
278        .iter()
279        .enumerate()
280        .map(|(index, value)| {
281            let pair = value_to_list(cx, value, &format!("input[{index}]"))?;
282            let [real, imag] = pair.as_slice() else {
283                return Err(Error::Eval(format!(
284                    "signal/transform input[{index}] must contain real and imaginary components"
285                )));
286            };
287            Ok((
288                value_to_number(cx, real, &format!("input[{index}].real"))?
289                    .canonical
290                    .parse::<f64>()
291                    .map_err(|_| Error::Eval("complex real component must be f64".to_owned()))?,
292                value_to_number(cx, imag, &format!("input[{index}].imag"))?
293                    .canonical
294                    .parse::<f64>()
295                    .map_err(|_| {
296                        Error::Eval("complex imaginary component must be f64".to_owned())
297                    })?,
298            ))
299        })
300        .collect()
301}
302
303fn value_to_number(cx: &mut Cx, value: &Value, name: &str) -> Result<NumberLiteral> {
304    value
305        .object()
306        .as_number_value()
307        .ok_or(Error::TypeMismatch {
308            expected: "number",
309            found: "non-number",
310        })?
311        .number_literal(cx)?
312        .ok_or_else(|| Error::Eval(format!("signal/transform {name} has no numeric literal")))
313}
314
315fn value_to_list(cx: &mut Cx, value: &Value, name: &str) -> Result<Vec<Value>> {
316    let list = value.object().as_list().ok_or(Error::TypeMismatch {
317        expected: "list",
318        found: "non-list",
319    })?;
320    force_list_to_vec(cx, list, &format!("signal/transform {name}"))
321}
322
323fn buffer_to_value(cx: &mut Cx, output: SignalBuffer) -> Result<Value> {
324    match output {
325        SignalBuffer::Real(values) => {
326            let values = values
327                .as_slice()
328                .iter()
329                .map(|value| f64_value(cx, *value))
330                .collect::<Result<Vec<_>>>()?;
331            cx.factory().list(values)
332        }
333        SignalBuffer::Complex(values) => {
334            let values = values
335                .as_slice()
336                .iter()
337                .map(|(real, imag)| {
338                    let real = f64_value(cx, *real)?;
339                    let imag = f64_value(cx, *imag)?;
340                    cx.factory().list(vec![real, imag])
341                })
342                .collect::<Result<Vec<_>>>()?;
343            cx.factory().list(values)
344        }
345    }
346}
347
348fn f64_value(cx: &mut Cx, value: f64) -> Result<Value> {
349    let canonical = if value == 0.0 {
350        "0".to_owned()
351    } else {
352        value.to_string()
353    };
354    cx.factory()
355        .number_literal(Symbol::qualified("numbers", "f64"), canonical)
356}
357
358fn signal_error_to_kernel(error: SignalError) -> Error {
359    Error::Eval(format!("signal/transform: {error}"))
360}
361
362fn argument_error() -> Error {
363    Error::Eval(
364        "signal/transform expects kind, direction, normalization, packing, len, and input"
365            .to_owned(),
366    )
367}