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        async fn test_inference_pattern_match_invalid() {
69            let expr = r#"
70          let x: option<u64> = some(1);
71          match x {
72            some(x) => x,
73            none => "none"
74          }
75        "#;
76
77            let expr = Expr::from_text(expr).unwrap();
78
79            let metadata = internal::get_metadata_with_record_input_params();
80
81            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
82
83            let expected = r#"
84            error in the following rib found at line 3, column 11
85            `match x {  some(x) => x, none => "none" } `
86            cause: cannot determine the type
87            invalid pattern match, conflicting types inferred. u64, string
88            help: try specifying the expected type explicitly
89            help: if the issue persists, please review the script for potential type inconsistencies
90            "#;
91
92            assert_eq!(error_msg, strip_spaces(expected));
93        }
94
95        #[test]
96        fn test_type_mismatch_in_record_in_function_call1() {
97            let expr = r#"
98          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}});
99          result
100        "#;
101
102            let expr = Expr::from_text(expr).unwrap();
103
104            let metadata = internal::get_metadata_with_record_input_params();
105
106            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
107
108            let expected = r#"
109            error in the following rib found at line 2, column 28
110            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: "foo", c: [1, 2, 3], d: {da: 4}}`
111            found within:
112            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: "foo", c: [1, 2, 3], d: {da: 4}})`
113            cause: type mismatch at path: `b`. expected u64
114            invalid argument to the function `foo`
115            "#;
116
117            assert_eq!(error_msg, strip_spaces(expected));
118        }
119
120        #[test]
121        fn test_type_mismatch_in_record_in_function_call2() {
122            let expr = r#"
123          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}});
124          result
125        "#;
126
127            let expr = Expr::from_text(expr).unwrap();
128
129            let metadata = internal::get_metadata_with_record_input_params();
130
131            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
132
133            let expected = r#"
134            error in the following rib found at line 2, column 28
135            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: ["foo", "bar"], d: {da: 4}}`
136            found within:
137            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: ["foo", "bar"], d: {da: 4}})`
138            cause: type mismatch at path: `c`. expected list<s32>
139            invalid argument to the function `foo`
140            "#;
141
142            assert_eq!(error_msg, strip_spaces(expected));
143        }
144
145        #[test]
146        fn test_type_mismatch_in_record_in_function_call3() {
147            let expr = r#"
148          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"}});
149          result
150        "#;
151
152            let expr = Expr::from_text(expr).unwrap();
153
154            let metadata = internal::get_metadata_with_record_input_params();
155
156            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
157
158            let expected = r#"
159            error in the following rib found at line 2, column 28
160            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: "foo"}}`
161            found within:
162            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: "foo"}})`
163            cause: type mismatch at path: `d.da`. expected s32
164            invalid argument to the function `foo`
165            "#;
166
167            assert_eq!(error_msg, strip_spaces(expected));
168        }
169
170        // Here the difference is, the shape itself is different losing the preciseness of the error.
171        // The best precise error
172        // is type-mismatch, however, here we get an ambiguity error. This can be improved,
173        // by not allowing accumulation of conflicting types into Exprs that are part of a function call
174        #[test]
175        fn test_type_mismatch_in_record_in_function_call4() {
176            let expr = r#"
177          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}});
178          result
179        "#;
180
181            let expr = Expr::from_text(expr).unwrap();
182
183            let metadata = internal::get_metadata_with_record_input_params();
184
185            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
186
187            let expected = r#"
188            error in the following rib found at line 2, column 51
189            `(1, 2)`
190            cause: ambiguous types: `list<number>`, `tuple<number, number>`
191            "#;
192
193            assert_eq!(error_msg, strip_spaces(expected));
194        }
195
196        #[test]
197        fn test_type_mismatch_in_record_in_function_call5() {
198            let expr = r#"
199            let x = {a: "foo"};
200          let result = foo({a: {aa: 1, ab: 2, ac: x, ad: {ada: 1}, ae: (1, "foo")}, b: 2, c: [1, 2], d: {da: 1}});
201          result
202        "#;
203
204            let expr = Expr::from_text(expr).unwrap();
205
206            let metadata = internal::get_metadata_with_record_input_params();
207
208            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
209
210            let expected = r#"
211            error in the following rib found at line 2, column 21
212            `{a: "foo"}`
213            cause: ambiguous types: `list<number>`, `record{a: str}`
214            "#;
215
216            assert_eq!(error_msg, strip_spaces(expected));
217        }
218
219        #[test]
220        fn test_type_mismatch_in_nested_record_in_function_call1() {
221            let expr = r#"
222          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}});
223          result
224        "#;
225
226            let expr = Expr::from_text(expr).unwrap();
227
228            let metadata = internal::get_metadata_with_record_input_params();
229
230            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
231
232            let expected = r#"
233            error in the following rib found at line 2, column 28
234            `{a: {aa: "foo", ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}}`
235            found within:
236            `foo({a: {aa: "foo", ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}})`
237            cause: type mismatch at path: `a.aa`. expected s32
238            invalid argument to the function `foo`
239            "#;
240
241            assert_eq!(error_msg, strip_spaces(expected));
242        }
243
244        #[test]
245        fn test_type_mismatch_in_nested_record_in_function_call2() {
246            let expr = r#"
247          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}});
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 2, column 28
259            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}}`
260            found within:
261            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: "1"}, ae: (1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}})`
262            cause: type mismatch at path: `a.ad.ada`. expected 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_call3() {
271            let expr = r#"
272            let bar = {a: {aa: 1, ab: 2, ac: 1, ad: {ada: 1}, ae:(1, "foo")}, b: 3, c: [1, 2, 3], d: {da: 4}};
273            let result = foo(bar);
274            result
275        "#;
276
277            let expr = Expr::from_text(expr).unwrap();
278
279            let metadata = internal::get_metadata_with_record_input_params();
280
281            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
282
283            let expected = r#"
284            error in the following rib found at line 3, column 30
285            `bar`
286            found within:
287            `foo(bar)`
288            cause: type mismatch at path: `a.ac`. expected list<s32>
289            invalid argument to the function `foo`
290            "#;
291
292            assert_eq!(error_msg, strip_spaces(expected));
293        }
294
295        #[test]
296        fn test_type_mismatch_in_nested_record_in_function_call4() {
297            let expr = r#"
298          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}});
299          result
300        "#;
301
302            let expr = Expr::from_text(expr).unwrap();
303
304            let metadata = internal::get_metadata_with_record_input_params();
305
306            let error_msg = compile(expr, &metadata).unwrap_err().to_string();
307
308            let expected = r#"
309            error in the following rib found at line 2, column 28
310            `{a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, 2)}, b: 3, c: [1, 2, 3], d: {da: 4}}`
311            found within:
312            `foo({a: {aa: 1, ab: 2, ac: [1, 2], ad: {ada: 1}, ae: (1, 2)}, b: 3, c: [1, 2, 3], d: {da: 4}})`
313            cause: type mismatch at path: `a.ae`. expected string
314            invalid argument to the function `foo`
315            "#;
316
317            assert_eq!(error_msg, strip_spaces(expected));
318        }
319    }
320
321    mod internal {
322        use golem_wasm_ast::analysis::analysed_type::{list, record, s32, str, tuple, u64};
323        use golem_wasm_ast::analysis::{
324            AnalysedExport, AnalysedFunction, AnalysedFunctionParameter, AnalysedFunctionResult,
325            NameTypePair,
326        };
327
328        pub(crate) fn strip_spaces(input: &str) -> String {
329            let lines = input.lines();
330
331            let first_line = lines
332                .clone()
333                .find(|line| !line.trim().is_empty())
334                .unwrap_or("");
335            let margin_width = first_line.chars().take_while(|c| c.is_whitespace()).count();
336
337            let result = lines
338                .map(|line| {
339                    if line.trim().is_empty() {
340                        String::new()
341                    } else {
342                        line[margin_width..].to_string()
343                    }
344                })
345                .collect::<Vec<String>>()
346                .join("\n");
347
348            result.strip_prefix("\n").unwrap_or(&result).to_string()
349        }
350
351        pub(crate) fn get_metadata_with_record_input_params() -> Vec<AnalysedExport> {
352            let analysed_export = AnalysedExport::Function(AnalysedFunction {
353                name: "foo".to_string(),
354                parameters: vec![AnalysedFunctionParameter {
355                    name: "arg1".to_string(),
356                    typ: record(vec![
357                        NameTypePair {
358                            name: "a".to_string(),
359                            typ: record(vec![
360                                NameTypePair {
361                                    name: "aa".to_string(),
362                                    typ: s32(),
363                                },
364                                NameTypePair {
365                                    name: "ab".to_string(),
366                                    typ: s32(),
367                                },
368                                NameTypePair {
369                                    name: "ac".to_string(),
370                                    typ: list(s32()),
371                                },
372                                NameTypePair {
373                                    name: "ad".to_string(),
374                                    typ: record(vec![NameTypePair {
375                                        name: "ada".to_string(),
376                                        typ: s32(),
377                                    }]),
378                                },
379                                NameTypePair {
380                                    name: "ae".to_string(),
381                                    typ: tuple(vec![s32(), str()]),
382                                },
383                            ]),
384                        },
385                        NameTypePair {
386                            name: "b".to_string(),
387                            typ: u64(),
388                        },
389                        NameTypePair {
390                            name: "c".to_string(),
391                            typ: list(s32()),
392                        },
393                        NameTypePair {
394                            name: "d".to_string(),
395                            typ: record(vec![NameTypePair {
396                                name: "da".to_string(),
397                                typ: s32(),
398                            }]),
399                        },
400                    ]),
401                }],
402                results: vec![AnalysedFunctionResult {
403                    name: None,
404                    typ: str(),
405                }],
406            });
407
408            vec![analysed_export]
409        }
410    }
411}