Skip to content

Commit 686818d

Browse files
authored
Merge pull request #1373 from harehare/chore/mq-check-integration
Integrate mq-check crate and type checker options
2 parents 140c929 + 7227114 commit 686818d

File tree

26 files changed

+1018
-171
lines changed

26 files changed

+1018
-171
lines changed

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
],
1515
"outFiles": ["${workspaceFolder}/editors/vscode/dist/**/*.js"],
1616
"env": {
17-
"_MQ_DEBUG_BIN": "${workspaceFolder}/target/debug/mq"
17+
"_MQ_DEBUG_BIN": "${workspaceFolder}/target/debug/mq-lsp"
1818
}
1919
},
2020
{
2121
"type": "lldb",
2222
"request": "launch",
2323
"name": "Rust Debug Launch",
24-
"program": "${workspaceRoot}/target/debug/${workspaceRootFolderName}",
24+
"program": "${workspaceRoot}/target/debug/mq-lsp",
2525
"args": [".h", "${workspaceRoot}/README.md"],
2626
"cwd": "${workspaceRoot}/target/debug/",
2727
"sourceLanguages": ["rust"]

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ resolver = "3"
2020

2121
[workspace.dependencies]
2222
# Internal crates
23+
mq-check = {path = "crates/mq-check", version = "0.5.17"}
2324
mq-dap = {path = "crates/mq-dap", version = "0.5.17"}
2425
mq-formatter = {path = "crates/mq-formatter", version = "0.5.17"}
2526
mq-hir = {path = "crates/mq-hir", version = "0.5.17"}

crates/mq-check/src/builtin.rs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ fn register_arithmetic(ctx: &mut InferenceContext) {
7979
register_binary(ctx, "+", Type::String, Type::String, Type::String);
8080
register_binary(ctx, "add", Type::String, Type::String, Type::String);
8181

82-
// Addition: string + number -> string (coercion)
83-
register_binary(ctx, "+", Type::String, Type::Number, Type::String);
84-
register_binary(ctx, "add", Type::String, Type::Number, Type::String);
85-
86-
// Addition: [a] + [a] -> [a] (array concatenation)
8782
for name in ["+", "add"] {
83+
// Addition: string + number -> string (coercion)
84+
register_binary(ctx, name, Type::String, Type::Number, Type::String);
85+
register_binary(ctx, name, Type::Number, Type::String, Type::String);
86+
87+
// Addition: [a] + [a] -> [a] (array concatenation)
8888
let a = ctx.fresh_var();
8989
register_binary(
9090
ctx,
@@ -1034,26 +1034,21 @@ mod tests {
10341034
}
10351035

10361036
#[rstest]
1037-
#[case::add_numbers("1 + 2", true)]
1038-
#[case::add_strings("\"hello\" + \"world\"", true)]
1039-
#[case::add_string_number("\"value: \" + 42", true)] // String + number coercion
1040-
#[case::add_arrays("[1, 2] + [3, 4]", true)] // Array concatenation
1041-
#[case::add_mixed("1 + \"world\"", false)] // Should fail: type mismatch
1042-
#[case::sub("10 - 5", true)]
1043-
#[case::mul("3 * 4", true)]
1044-
#[case::mul_array_repeat("[1, 2] * 3", true)] // Array repetition
1045-
#[case::div("10 / 2", true)]
1046-
#[case::mod_op("10 % 3", true)]
1047-
#[case::pow("2 ^ 8", true)]
1048-
fn test_arithmetic_operators(#[case] code: &str, #[case] should_succeed: bool) {
1037+
#[case::add_numbers("1 + 2")]
1038+
#[case::add_number_string("1 + \"string\"")]
1039+
#[case::add_strings("\"hello\" + \"world\"")]
1040+
#[case::add_string_number("\"value: \" + 42")] // String + number coercion
1041+
#[case::add_arrays("[1, 2] + [3, 4]")] // Array concatenation
1042+
#[case::add_mixed("1 + \"world\"")] // Number + string coercion
1043+
#[case::sub("10 - 5")]
1044+
#[case::mul("3 * 4")]
1045+
#[case::mul_array_repeat("[1, 2] * 3")] // Array repetition
1046+
#[case::div("10 / 2")]
1047+
#[case::mod_op("10 % 3")]
1048+
#[case::pow("2 ^ 8")]
1049+
fn test_arithmetic_operators(#[case] code: &str) {
10491050
let result = check_types(code);
1050-
assert_eq!(
1051-
result.is_empty(),
1052-
should_succeed,
1053-
"Code: {}\nResult: {:?}",
1054-
code,
1055-
result
1056-
);
1051+
assert!(result.is_empty(), "Code: {}\nResult: {:?}", code, result);
10571052
}
10581053

10591054
#[rstest]
@@ -1408,7 +1403,6 @@ mod tests {
14081403

14091404
// Type Error Cases
14101405
#[rstest]
1411-
#[case::add_number_string("1 + \"string\"", false)] // number + string is invalid
14121406
#[case::add_bool_number("true + 1", false)] // bool + number is invalid
14131407
#[case::add_bool_string("true + \"s\"", false)] // bool + string is invalid
14141408
#[case::sub_strings("\"a\" - \"b\"", false)] // strings cannot be subtracted

crates/mq-check/src/infer.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Type inference context and engine.
22
3-
use crate::TypeError;
43
use crate::constraint::Constraint;
54
use crate::types::{Substitution, Type, TypeScheme, TypeVarContext, TypeVarId};
5+
use crate::{TypeEnv, TypeError};
66
use mq_hir::SymbolId;
77
use rustc_hash::FxHashMap;
88
use smol_str::SmolStr;
@@ -511,8 +511,8 @@ impl InferenceContext {
511511
}
512512

513513
/// Finalizes inference and returns symbol type schemes
514-
pub fn finalize(self) -> FxHashMap<SymbolId, TypeScheme> {
515-
let mut result = FxHashMap::default();
514+
pub fn finalize(self) -> TypeEnv {
515+
let mut result = TypeEnv::default();
516516

517517
for (symbol, ty) in &self.symbol_types {
518518
let resolved = self.resolve_type(ty);

crates/mq-check/src/lib.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,43 @@ use types::TypeScheme;
4040
/// Result type for type checking operations
4141
pub type Result<T> = std::result::Result<T, TypeError>;
4242

43+
/// Type environment mapping symbol IDs to their inferred type schemes
44+
#[derive(Debug, Clone, Default)]
45+
pub struct TypeEnv(FxHashMap<SymbolId, TypeScheme>);
46+
47+
impl TypeEnv {
48+
pub fn insert(&mut self, symbol_id: SymbolId, scheme: TypeScheme) {
49+
self.0.insert(symbol_id, scheme);
50+
}
51+
52+
pub fn get(&self, symbol_id: &SymbolId) -> Option<&TypeScheme> {
53+
self.0.get(symbol_id)
54+
}
55+
56+
pub fn get_all(&self) -> &FxHashMap<SymbolId, TypeScheme> {
57+
&self.0
58+
}
59+
60+
pub fn len(&self) -> usize {
61+
self.0.len()
62+
}
63+
64+
pub fn is_empty(&self) -> bool {
65+
self.0.is_empty()
66+
}
67+
}
68+
69+
impl<'a> IntoIterator for &'a TypeEnv {
70+
type Item = (&'a SymbolId, &'a TypeScheme);
71+
type IntoIter = std::collections::hash_map::Iter<'a, SymbolId, TypeScheme>;
72+
73+
fn into_iter(self) -> Self::IntoIter {
74+
self.0.iter()
75+
}
76+
}
77+
4378
/// Type checking errors
44-
#[derive(Debug, Error, Diagnostic)]
79+
#[derive(Debug, Error, Clone, Diagnostic)]
4580
#[allow(unused_assignments)]
4681
pub enum TypeError {
4782
#[error("Type mismatch: expected {expected}, found {found}")]
@@ -160,7 +195,7 @@ pub(crate) fn walk_ancestors(
160195
}
161196

162197
/// Options for configuring the type checker behavior
163-
#[derive(Debug, Clone, Default)]
198+
#[derive(Debug, Clone, Copy, Default)]
164199
pub struct TypeCheckerOptions {
165200
/// When true, arrays must contain elements of a single type.
166201
/// Heterogeneous arrays like `[1, "hello"]` will produce a type error.
@@ -175,7 +210,7 @@ pub struct TypeCheckerOptions {
175210
/// Provides type inference and checking capabilities based on HIR information.
176211
pub struct TypeChecker {
177212
/// Symbol type mappings
178-
symbol_types: FxHashMap<SymbolId, TypeScheme>,
213+
symbol_types: TypeEnv,
179214
/// Type checker options
180215
options: TypeCheckerOptions,
181216
}
@@ -184,15 +219,15 @@ impl TypeChecker {
184219
/// Creates a new type checker with default options
185220
pub fn new() -> Self {
186221
Self {
187-
symbol_types: FxHashMap::default(),
222+
symbol_types: TypeEnv::default(),
188223
options: TypeCheckerOptions::default(),
189224
}
190225
}
191226

192227
/// Creates a new type checker with the given options
193228
pub fn with_options(options: TypeCheckerOptions) -> Self {
194229
Self {
195-
symbol_types: FxHashMap::default(),
230+
symbol_types: TypeEnv::default(),
196231
options,
197232
}
198233
}
@@ -887,9 +922,14 @@ impl TypeChecker {
887922
}
888923

889924
/// Gets all symbol types
890-
pub fn symbol_types(&self) -> &FxHashMap<SymbolId, TypeScheme> {
925+
pub fn symbol_types(&self) -> &TypeEnv {
891926
&self.symbol_types
892927
}
928+
929+
/// Gets the type scheme for a specific symbol.
930+
pub fn symbol_type(&self, symbol_id: SymbolId) -> Option<&TypeScheme> {
931+
self.symbol_types.get(&symbol_id)
932+
}
893933
}
894934

895935
impl Default for TypeChecker {

crates/mq-check/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ fn run() -> io::Result<()> {
103103
show_types: cli.show_types,
104104
label: label.as_deref(),
105105
no_builtins: cli.no_builtins,
106-
type_checker_options: tc_options.clone(),
106+
type_checker_options: tc_options,
107107
};
108108
let had_errors = check_file(&mut w, &code, source_url, &opts)?;
109109
if had_errors {
@@ -208,7 +208,7 @@ fn check_type(
208208
show_types: bool,
209209
options: &TypeCheckerOptions,
210210
) -> io::Result<bool> {
211-
let mut checker = TypeChecker::with_options(options.clone());
211+
let mut checker = TypeChecker::with_options(*options);
212212
let mut errors = checker.check(hir);
213213

214214
errors.sort_by_key(|a| a.location());

crates/mq-check/tests/error_location_test.rs

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -39,37 +39,3 @@ fn test_homogeneous_array_no_error() {
3939
let errors = check_types(code);
4040
assert!(errors.is_empty(), "Homogeneous array should have no errors");
4141
}
42-
43-
#[test]
44-
fn test_binary_op_type_error_location() {
45-
let code = r#"1 + "hello""#;
46-
let errors = check_types(code);
47-
48-
assert!(!errors.is_empty(), "Expected type error for number + string");
49-
}
50-
51-
#[test]
52-
fn test_error_message_readability() {
53-
// Test that error messages are human-readable
54-
let code = r#"1 + "hello""#;
55-
let errors = check_types(code);
56-
57-
assert!(!errors.is_empty(), "Expected type error");
58-
59-
let error_msg = format!("{}", errors[0]);
60-
// The error message should be informative
61-
assert!(
62-
error_msg.contains("mismatch") || error_msg.contains("type") || error_msg.contains("unify"),
63-
"Error message should mention type mismatch: {}",
64-
error_msg
65-
);
66-
}
67-
68-
#[test]
69-
fn test_multiple_errors_show_locations() {
70-
// Code with type error in binary operation
71-
let code = r#"1 + "two""#;
72-
let errors = check_types(code);
73-
74-
assert!(!errors.is_empty(), "Expected at least one type error");
75-
}

crates/mq-check/tests/integration_test.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ fn test_try_catch() {
333333
#[rstest]
334334
#[case::add_strings(r#"def add(x, y): x + y; | add("hello", "world")"#, true)]
335335
#[case::add_numbers("def add(x, y): x + y; | add(1, 2)", true)]
336-
#[case::add_mixed_types(r#"def add(x, y): x + y; | add(1, "hello")"#, false)]
336+
#[case::add_mixed_types(r#"def add(x, y): x + y; | add(1, "hello")"#, true)]
337337
#[case::add_mixed_types_reversed(r#"def add(x, y): x + y; | add("hello", 1)"#, true)] // string+any->string overload matches
338338
#[case::string_concat_in_fn(r#"def greet(name): "hello " + name; | greet("world")"#, true)]
339339
#[case::unary_negation("-42", true)]
@@ -415,8 +415,8 @@ fn test_macro_with_multiple_params() {
415415
// User-Defined Function Type Checking
416416

417417
#[rstest]
418-
#[case::arg_type_mismatch(r#"def add(x, y): x + y; | add(1, "hello")"#, false)]
419-
#[case::return_type_propagation(r#"def get_num(): 42; | get_num() + "hello""#, false)]
418+
#[case::arg_type_mismatch(r#"def add(x, y): x + y; | add(1, "hello")"#, true)]
419+
#[case::return_type_propagation(r#"def get_num(): 42; | get_num() + "hello""#, true)]
420420
#[case::chained_calls(r#"def double(x): x + x; | def negate(x): 0 - x; | double(negate(1))"#, true)]
421421
#[case::string_plus_number(r#"def greet(): "hello"; | greet() + 1"#, true)]
422422
#[case::string_minus_number(r#"def greet(): "hello"; | greet() - 1"#, false)]
@@ -447,7 +447,7 @@ fn test_user_function_type_checking(#[case] code: &str, #[case] should_succeed:
447447
#[case::number_mul_string(r#"3 * "x""#, false, "number * string")]
448448
#[case::string_minus_number(r#""abc" - 1"#, false, "string - number")]
449449
#[case::string_div_number(r#""abc" / 2"#, false, "string / number")]
450-
#[case::number_add_string(r#"1 + "world""#, false, "number + string")]
450+
#[case::number_add_string(r#"1 + "world""#, true, "number + string")]
451451
#[case::string_mul_string(r#""a" * "b""#, false, "string * string")]
452452
#[case::string_div_string(r#""a" / "b""#, false, "string / string")]
453453
#[case::string_minus_string(r#""a" - "b""#, false, "string - string")]
@@ -524,7 +524,7 @@ fn test_equality_op_type_errors(#[case] code: &str, #[case] should_succeed: bool
524524
#[case::let_num_minus_string(r#"let x = 1 | x - "str""#, false, "let number binding minus string")]
525525
#[case::let_num_mul_string(r#"let x = 1 | x * "str""#, false, "let number binding times string")]
526526
#[case::let_num_div_string(r#"let x = 1 | x / "str""#, false, "let number binding div string")]
527-
#[case::let_num_plus_string(r#"let x = 1 | x + "str""#, false, "let number binding plus string")]
527+
#[case::let_num_plus_string(r#"let x = 1 | x + "str""#, true, "let number binding plus string")]
528528
#[case::let_string_minus_num(r#"let x = "hello" | x - 1"#, false, "let string binding minus number")]
529529
#[case::let_string_div_num(r#"let x = "hello" | x / 2"#, false, "let string binding div number")]
530530
#[case::let_string_mul_string(r#"let x = "hello" | x * "world""#, false, "let string binding times string")]

crates/mq-check/tests/type_errors_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ fn check_types(code: &str) -> Vec<TypeError> {
2222

2323
#[test]
2424
fn test_binary_op_type_mismatch() {
25-
let result = check_types(r#"1 + "string""#);
25+
let result = check_types(r#"1 + true"#);
2626
println!("Binary op type mismatch: {:?}", result);
27-
assert!(!result.is_empty(), "Expected type error for number + string");
27+
assert!(!result.is_empty(), "Expected type error for number + boolean");
2828
}
2929

3030
#[test]

0 commit comments

Comments
 (0)