rib/type_checker/
mod.rs

1// Copyright 2024-2025 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub(crate) use check_instance_returns::*;
16pub(crate) use exhaustive_pattern_match::*;
17pub(crate) use invalid_expr::*;
18pub(crate) use invalid_math_expr::*;
19pub(crate) use missing_fields::*;
20pub use path::*;
21pub(crate) use type_mismatch::*;
22pub(crate) use unresolved_types::*;
23
24mod check_instance_returns;
25mod exhaustive_pattern_match;
26mod invalid_expr;
27mod invalid_math_expr;
28mod invalid_worker_name;
29mod missing_fields;
30mod path;
31mod type_check_in_function_calls;
32mod type_mismatch;
33mod unresolved_types;
34
35use crate::rib_compilation_error::RibCompilationError;
36use crate::type_checker::exhaustive_pattern_match::check_exhaustive_pattern_match;
37use crate::type_checker::invalid_expr::check_invalid_expr;
38use crate::type_checker::invalid_math_expr::check_invalid_math_expr;
39use crate::type_checker::invalid_worker_name::check_invalid_worker_name;
40use crate::type_checker::type_check_in_function_calls::check_type_error_in_function_calls;
41use crate::{Expr, FunctionTypeRegistry};
42
43pub fn type_check(
44    expr: &mut Expr,
45    function_type_registry: &FunctionTypeRegistry,
46) -> Result<(), RibCompilationError> {
47    check_type_error_in_function_calls(expr, function_type_registry)?;
48    check_unresolved_types(expr)?;
49    check_invalid_worker_name(expr)?;
50    check_invalid_expr(expr)?;
51    check_invalid_program_return(expr)?;
52    check_invalid_math_expr(expr)?;
53    check_exhaustive_pattern_match(expr, function_type_registry)?;
54    Ok(())
55}
56
57#[cfg(test)]
58mod type_check_tests {
59
60    mod type_mismatch_errors {
61        use test_r::test;
62
63        use crate::type_checker::type_check_tests::internal;
64        use crate::type_checker::type_check_tests::internal::strip_spaces;
65        use crate::{compile, Expr};
66
67        #[test]
68        fn test_type_mismatch_in_record_in_function_call1() {
69            let expr = r#"
70          let result = foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: "foo", c: [1, 2, 3], d: {da: 4}});
71          result
72        "#;
73
74            let expr = Expr::from_text(expr).unwrap();
75
76            let metadata = internal::get_metadata_with_record_input_params();
77
78            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
79
80            let expected = r#"
81            error in the following rib found at line 2, column 28
82            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: "foo", c: [1, 2, 3], d: {da: 4}}`
83            found within:
84            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: "foo", c: [1, 2, 3], d: {da: 4}})`
85            cause: type mismatch at path: `b`. expected u64
86            invalid argument to the function `foo`
87            "#;
88
89            assert_eq!(error_msg, strip_spaces(expected));
90        }
91
92        #[test]
93        fn test_type_mismatch_in_record_in_function_call2() {
94            let expr = r#"
95          let result = foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: ["foo", "bar"], d: {da: 4}});
96          result
97        "#;
98
99            let expr = Expr::from_text(expr).unwrap();
100
101            let metadata = internal::get_metadata_with_record_input_params();
102
103            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
104
105            let expected = r#"
106            error in the following rib found at line 2, column 28
107            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: ["foo", "bar"], d: {da: 4}}`
108            found within:
109            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: ["foo", "bar"], d: {da: 4}})`
110            cause: type mismatch at path: `c`. expected list<s32>
111            invalid argument to the function `foo`
112            "#;
113
114            assert_eq!(error_msg, strip_spaces(expected));
115        }
116
117        #[test]
118        fn test_type_mismatch_in_record_in_function_call3() {
119            let expr = r#"
120          let result = foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: "foo"}});
121          result
122        "#;
123
124            let expr = Expr::from_text(expr).unwrap();
125
126            let metadata = internal::get_metadata_with_record_input_params();
127
128            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
129
130            let expected = r#"
131            error in the following rib found at line 2, column 28
132            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: "foo"}}`
133            found within:
134            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: "foo"}})`
135            cause: type mismatch at path: `d.da`. expected s32
136            invalid argument to the function `foo`
137            "#;
138
139            assert_eq!(error_msg, strip_spaces(expected));
140        }
141
142        // Here the difference is, the shape itself is different losing the preciseness of the error.
143        // The best precise error
144        // is type-mismatch, however, here we get an ambiguity error. This can be improved,
145        // by not allowing accumulation of conflicting types into Exprs that are part of a function call
146        #[test]
147        fn test_type_mismatch_in_record_in_function_call4() {
148            let expr = r#"
149          let result = foo({a: {aa: 1, ab: 2, ac: (1, 2), ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: 1}});
150          result
151        "#;
152
153            let expr = Expr::from_text(expr).unwrap();
154
155            let metadata = internal::get_metadata_with_record_input_params();
156
157            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
158
159            let expected = r#"
160            error in the following rib found at line 2, column 51
161            `(1, 2)`
162            cause: The expression is wrongly used (directly or indirectly) elsewhere resulting in conflicting types: `list`, `tuple`
163            help: ensure this expression is only used in contexts that align with its actual type
164            "#;
165
166            assert_eq!(error_msg, strip_spaces(expected));
167        }
168
169        #[test]
170        fn test_type_mismatch_in_record_in_function_call5() {
171            let expr = r#"
172            let x = {a: "foo"};
173          let result = foo({a: {aa: 1, ab: 2, ac: x, ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: 1}});
174          result
175        "#;
176
177            let expr = Expr::from_text(expr).unwrap();
178
179            let metadata = internal::get_metadata_with_record_input_params();
180
181            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
182
183            let expected = r#"
184            error in the following rib found at line 2, column 21
185            `{a: "foo"}`
186            cause: The expression is wrongly used (directly or indirectly) elsewhere resulting in conflicting types: `list`, `record`
187            help: ensure this expression is only used in contexts that align with its actual type
188            "#;
189
190            assert_eq!(error_msg, strip_spaces(expected));
191        }
192
193        #[test]
194        fn test_type_mismatch_in_nested_record_in_function_call1() {
195            let expr = r#"
196          let result = foo({a: {aa: "foo", ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}});
197          result
198        "#;
199
200            let expr = Expr::from_text(expr).unwrap();
201
202            let metadata = internal::get_metadata_with_record_input_params();
203
204            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
205
206            let expected = r#"
207            error in the following rib found at line 2, column 28
208            `{a: {aa: "foo", ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}}`
209            found within:
210            `foo({a: {aa: "foo", ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}})`
211            cause: type mismatch at path: `a.aa`. expected s32
212            invalid argument to the function `foo`
213            "#;
214
215            assert_eq!(error_msg, strip_spaces(expected));
216        }
217
218        #[test]
219        fn test_type_mismatch_in_nested_record_in_function_call2() {
220            let expr = r#"
221          let result = foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}});
222          result
223        "#;
224
225            let expr = Expr::from_text(expr).unwrap();
226
227            let metadata = internal::get_metadata_with_record_input_params();
228
229            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
230
231            let expected = r#"
232            error in the following rib found at line 2, column 28
233            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}}`
234            found within:
235            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}})`
236            cause: type mismatch at path: `a.ad.ada`. expected s32
237            invalid argument to the function `foo`
238            "#;
239
240            assert_eq!(error_msg, strip_spaces(expected));
241        }
242
243        #[test]
244        fn test_type_mismatch_in_nested_record_in_function_call3() {
245            let expr = r#"
246            let bar = {a: {aa: 1, ab: 2, ac: 1, ad: {ada: 1}, ae:(1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}};
247            let result = foo(bar);
248            result
249        "#;
250
251            let expr = Expr::from_text(expr).unwrap();
252
253            let metadata = internal::get_metadata_with_record_input_params();
254
255            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
256
257            let expected = r#"
258            error in the following rib found at line 3, column 30
259            `bar`
260            found within:
261            `foo(bar)`
262            cause: type mismatch at path: `a.ac`. expected list<s32>
263            invalid argument to the function `foo`
264            "#;
265
266            assert_eq!(error_msg, strip_spaces(expected));
267        }
268
269        #[test]
270        fn test_type_mismatch_in_nested_record_in_function_call4() {
271            let expr = r#"
272          let result = foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, 2)}, b: 3, c: [1, 2, 3], d: {da: 4}});
273          result
274        "#;
275
276            let expr = Expr::from_text(expr).unwrap();
277
278            let metadata = internal::get_metadata_with_record_input_params();
279
280            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
281
282            let expected = r#"
283            error in the following rib found at line 2, column 28
284            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, 2)}, b: 3, c: [1, 2, 3], d: {da: 4}}`
285            found within:
286            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, 2)}, b: 3, c: [1, 2, 3], d: {da: 4}})`
287            cause: type mismatch at path: `a.ae`. expected string
288            invalid argument to the function `foo`
289            "#;
290
291            assert_eq!(error_msg, strip_spaces(expected));
292        }
293    }
294
295    mod internal {
296        use golem_wasm_ast::analysis::analysed_type::{list, record, s32, str, tuple, u64};
297        use golem_wasm_ast::analysis::{
298            AnalysedExport, AnalysedFunction, AnalysedFunctionParameter, AnalysedFunctionResult,
299            NameTypePair,
300        };
301
302        pub(crate) fn strip_spaces(input: &str) -> String {
303            let lines = input.lines();
304
305            let first_line = lines
306                .clone()
307                .find(|line| !line.trim().is_empty())
308                .unwrap_or("");
309            let margin_width = first_line.chars().take_while(|c| c.is_whitespace()).count();
310
311            let result = lines
312                .map(|line| {
313                    if line.trim().is_empty() {
314                        String::new()
315                    } else {
316                        line[margin_width..].to_string()
317                    }
318                })
319                .collect::<Vec<String>>()
320                .join("\n");
321
322            result.strip_prefix("\n").unwrap_or(&result).to_string()
323        }
324
325        pub(crate) fn get_metadata_with_record_input_params() -> Vec<AnalysedExport> {
326            let analysed_export = AnalysedExport::Function(AnalysedFunction {
327                name: "foo".to_string(),
328                parameters: vec![AnalysedFunctionParameter {
329                    name: "arg1".to_string(),
330                    typ: record(vec![
331                        NameTypePair {
332                            name: "a".to_string(),
333                            typ: record(vec![
334                                NameTypePair {
335                                    name: "aa".to_string(),
336                                    typ: s32(),
337                                },
338                                NameTypePair {
339                                    name: "ab".to_string(),
340                                    typ: s32(),
341                                },
342                                NameTypePair {
343                                    name: "ac".to_string(),
344                                    typ: list(s32()),
345                                },
346                                NameTypePair {
347                                    name: "ad".to_string(),
348                                    typ: record(vec![NameTypePair {
349                                        name: "ada".to_string(),
350                                        typ: s32(),
351                                    }]),
352                                },
353                                NameTypePair {
354                                    name: "ae".to_string(),
355                                    typ: tuple(vec![s32(), str()]),
356                                },
357                            ]),
358                        },
359                        NameTypePair {
360                            name: "b".to_string(),
361                            typ: u64(),
362                        },
363                        NameTypePair {
364                            name: "c".to_string(),
365                            typ: list(s32()),
366                        },
367                        NameTypePair {
368                            name: "d".to_string(),
369                            typ: record(vec![NameTypePair {
370                                name: "da".to_string(),
371                                typ: s32(),
372                            }]),
373                        },
374                    ]),
375                }],
376                results: vec![AnalysedFunctionResult {
377                    name: None,
378                    typ: str(),
379                }],
380            });
381
382            vec![analysed_export]
383        }
384    }
385}