Skip to main content

konjure_lang/
methods.rs

1//! Checked signatures for native operations owned by their receiver types.
2//!
3//! These declarations deliberately exclude the implicit receiver.  The checker
4//! supplies that receiver when it resolves a method expression, while the
5//! runtime can dispatch [`operation`] using the same canonical name.
6
7use crate::Type;
8
9/// A native operation after method resolution has selected its receiver.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum Operation {
12    /// Count a receiver's elements, bytes, or scalar values.
13    Len,
14    /// Encode UTF-8 text.
15    Utf8,
16    /// Decode UTF-8 bytes.
17    Decode,
18    /// Append one value to a list.
19    Append,
20    /// Read one list, text, byte, or tensor element.
21    Get,
22    /// Return tensor dimensions.
23    Shape,
24    /// Reinterpret a tensor with a supplied shape.
25    Reshape,
26    /// Reorder tensor axes.
27    Transpose,
28    /// Sum tensor elements.
29    Sum,
30    /// Multiply two tensors.
31    Matmul,
32    /// Convert numeric scalar or tensor data to an explicit dtype.
33    Convert,
34    /// Compute the named numeric operation.
35    Numeric(&'static str),
36    /// Creates or transforms an owned procedural mesh.
37    Mesh,
38}
39
40/// Returns the runtime operation corresponding to a receiver-owned method.
41pub fn operation(name: &str) -> Option<Operation> {
42    Some(match name {
43        "len" => Operation::Len,
44        "utf8" => Operation::Utf8,
45        "decode" => Operation::Decode,
46        "append" => Operation::Append,
47        "get" => Operation::Get,
48        "shape" => Operation::Shape,
49        "reshape" => Operation::Reshape,
50        "transpose" => Operation::Transpose,
51        "sum" => Operation::Sum,
52        "matmul" => Operation::Matmul,
53        "convert" => Operation::Convert,
54        "sin" => Operation::Numeric("sin"),
55        "cos" => Operation::Numeric("cos"),
56        "sqrt" => Operation::Numeric("sqrt"),
57        "abs" => Operation::Numeric("abs"),
58        "floor" => Operation::Numeric("floor"),
59        "ceil" => Operation::Numeric("ceil"),
60        "min" => Operation::Numeric("min"),
61        "max" => Operation::Numeric("max"),
62        "clamp" => Operation::Numeric("clamp"),
63        "pow" => Operation::Numeric("pow"),
64        "sphere" | "torus" | "cone" | "grid" | "lathe" | "suzanne" | "translated" | "scaled"
65        | "rotated" | "mapped" | "decode_json" => Operation::Mesh,
66        _ => return None,
67    })
68}
69
70/// Names which are no longer global functions because their receiver owns them.
71pub fn is_owned_global(name: &str) -> bool {
72    matches!(
73        name,
74        "len"
75            | "decode"
76            | "utf8"
77            | "shape"
78            | "reshape"
79            | "transpose"
80            | "sum"
81            | "convert"
82            | "matmul"
83            | "append"
84            | "sin"
85            | "cos"
86            | "sqrt"
87            | "abs"
88            | "floor"
89            | "ceil"
90            | "min"
91            | "max"
92            | "clamp"
93            | "pow"
94            | "sphere"
95            | "torus"
96            | "cone"
97            | "grid"
98            | "lathe"
99            | "suzanne"
100            | "translated"
101            | "scaled"
102            | "rotated"
103            | "mapped"
104            | "decode_json"
105    )
106}
107
108/// Returns every applicable method signature, excluding the implicit receiver.
109///
110/// Multiple signatures are possible only for `Tensor[T].transpose`; callers
111/// select the one whose explicit parameter count matches the authored call.
112pub fn signatures(receiver: &Type, name: &str, type_arg: Option<&Type>) -> Vec<Type> {
113    let number = Type::named("f64");
114    let text = Type::named("Str");
115    let binary = Type::named("Bin");
116    let error = Type::named("DataError");
117    let list_number = Type::List(Box::new(number.clone()));
118    let result = |value| applied("Res", vec![value, error.clone()]);
119    let option = |value| applied("Opt", vec![value]);
120
121    match receiver {
122        Type::Named(value) if value == "Str" => match name {
123            "len" => vec![Type::function(vec![], number)],
124            "utf8" => vec![Type::function(vec![], binary)],
125            "get" => vec![Type::function(vec![Type::named("f64")], option(text))],
126            _ => vec![],
127        },
128        Type::Named(value) if value == "Bin" => match name {
129            "len" => vec![Type::function(vec![], number)],
130            "decode" => vec![Type::function(vec![], result(text))],
131            "get" => vec![Type::function(
132                vec![Type::named("f64")],
133                option(Type::named("u8")),
134            )],
135            _ => vec![],
136        },
137        Type::List(element) => match name {
138            "len" => vec![Type::function(vec![], number)],
139            "append" => vec![Type::function(
140                vec![element.as_ref().clone()],
141                Type::List(element.clone()),
142            )],
143            "get" => vec![Type::function(
144                vec![Type::named("f64")],
145                option(element.as_ref().clone()),
146            )],
147            _ => vec![],
148        },
149        Type::Tensor(element) => match name {
150            "len" => vec![Type::function(vec![], number)],
151            "shape" => vec![Type::function(vec![], list_number.clone())],
152            "reshape" => vec![Type::function(
153                vec![list_number.clone()],
154                result(Type::Tensor(element.clone())),
155            )],
156            "transpose" => vec![
157                Type::function(vec![], result(Type::Tensor(element.clone()))),
158                Type::function(
159                    vec![list_number.clone()],
160                    result(Type::Tensor(element.clone())),
161                ),
162            ],
163            "sum" => vec![Type::function(vec![], result(element.as_ref().clone()))],
164            "matmul" => vec![Type::function(
165                vec![Type::Tensor(element.clone())],
166                result(Type::Tensor(element.clone())),
167            )],
168            "get" => vec![Type::function(
169                vec![list_number],
170                option(element.as_ref().clone()),
171            )],
172            "convert" if is_dtype(type_arg) => vec![Type::function(
173                vec![],
174                result(Type::Tensor(Box::new(
175                    type_arg.expect("checked above").clone(),
176                ))),
177            )],
178            _ => vec![],
179        },
180        Type::Named(value) if value == "f64" => match name {
181            "sin" | "cos" | "sqrt" | "abs" | "floor" | "ceil" => {
182                vec![Type::function(vec![], result(number.clone()))]
183            }
184            "min" | "max" | "pow" => vec![Type::function(vec![number.clone()], result(number))],
185            "clamp" => vec![Type::function(
186                vec![number.clone(), number.clone()],
187                result(number),
188            )],
189            "convert" if is_dtype(type_arg) => vec![Type::function(
190                vec![],
191                result(type_arg.expect("checked above").clone()),
192            )],
193            _ => vec![],
194        },
195        Type::Named(value) if value == "Mesh" => {
196            let mesh = Type::named("Mesh");
197            let vec3 = Type::named("Vec3");
198            let number = Type::named("f64");
199            let mesh_result = result(mesh.clone());
200            match name {
201                "sphere" => vec![Type::function(
202                    vec![number.clone(), number.clone(), number.clone()],
203                    mesh_result,
204                )],
205                "torus" => vec![Type::function(
206                    vec![
207                        number.clone(),
208                        number.clone(),
209                        number.clone(),
210                        number.clone(),
211                    ],
212                    mesh_result,
213                )],
214                "cone" => vec![Type::function(
215                    vec![number.clone(), number.clone(), number.clone()],
216                    mesh_result,
217                )],
218                "grid" => vec![Type::function(
219                    vec![
220                        number.clone(),
221                        number.clone(),
222                        number.clone(),
223                        number.clone(),
224                    ],
225                    mesh_result,
226                )],
227                "lathe" => vec![Type::function(
228                    vec![Type::Tensor(Box::new(Type::named("f32"))), number.clone()],
229                    mesh_result,
230                )],
231                "suzanne" => vec![Type::function(vec![], mesh_result)],
232                "translated" | "scaled" | "rotated" => {
233                    vec![Type::function(vec![vec3], mesh_result)]
234                }
235                "mapped" => vec![Type::function(
236                    vec![Type::function(vec![vec3], result(Type::named("Vec3")))],
237                    mesh_result,
238                )],
239                "decode_json" => vec![Type::function(
240                    vec![Type::List(Box::new(Type::named("Str")))],
241                    mesh_result,
242                )],
243                _ => vec![],
244            }
245        }
246        Type::Named(_) if is_dtype(Some(receiver)) && name == "convert" && is_dtype(type_arg) => {
247            vec![Type::function(
248                vec![],
249                result(type_arg.expect("checked above").clone()),
250            )]
251        }
252        _ => vec![],
253    }
254}
255
256/// Returns the only applicable signature, excluding the implicit receiver.
257///
258/// Use [`signatures`] when choosing an overload such as `transpose` by arity.
259pub fn signature(receiver: &Type, name: &str, type_arg: Option<&Type>) -> Option<Type> {
260    let mut signatures = signatures(receiver, name, type_arg).into_iter();
261    let signature = signatures.next()?;
262    signatures.next().is_none().then_some(signature)
263}
264
265fn applied(name: &str, arguments: Vec<Type>) -> Type {
266    Type::Applied {
267        name: name.into(),
268        arguments,
269    }
270}
271
272fn is_dtype(ty: Option<&Type>) -> bool {
273    ty.is_some_and(|ty| crate::data::dtype(ty).is_some())
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn data_methods_have_receiver_specialized_results() {
282        let tensor = Type::Tensor(Box::new(Type::named("f32")));
283        assert_eq!(
284            signature(&Type::named("Bin"), "decode", None),
285            Some(Type::function(
286                vec![],
287                Type::Applied {
288                    name: "Res".into(),
289                    arguments: vec![Type::named("Str"), Type::named("DataError")],
290                },
291            ))
292        );
293        assert_eq!(
294            signature(&Type::List(Box::new(Type::named("u8"))), "append", None),
295            Some(Type::function(
296                vec![Type::named("u8")],
297                Type::List(Box::new(Type::named("u8"))),
298            ))
299        );
300        assert_eq!(
301            signature(&tensor, "convert", Some(&Type::named("u16"))),
302            Some(Type::function(
303                vec![],
304                Type::Applied {
305                    name: "Res".into(),
306                    arguments: vec![
307                        Type::Tensor(Box::new(Type::named("u16"))),
308                        Type::named("DataError"),
309                    ],
310                },
311            ))
312        );
313    }
314
315    #[test]
316    fn transpose_keeps_its_two_arity_overloads_explicit() {
317        let tensor = Type::Tensor(Box::new(Type::named("f64")));
318        assert_eq!(signature(&tensor, "transpose", None), None);
319        assert_eq!(signatures(&tensor, "transpose", None).len(), 2);
320    }
321
322    #[test]
323    fn owned_globals_are_not_left_in_the_function_namespace() {
324        for name in [
325            "len",
326            "decode",
327            "utf8",
328            "shape",
329            "reshape",
330            "transpose",
331            "sum",
332            "convert",
333            "matmul",
334            "append",
335        ] {
336            assert!(is_owned_global(name));
337            assert!(operation(name).is_some());
338        }
339        assert!(!is_owned_global("bytes"));
340        assert_eq!(operation("bytes"), None);
341    }
342}