Conversation
53ab26c to
9ffd4ee
Compare
9ffd4ee to
f1eb752
Compare
f1eb752 to
02334e8
Compare
There was a problem hiding this comment.
Pull request overview
Introduces a new mq-typechecker crate implementing Hindley–Milner-style static type checking/type inference for mq, plus initial tests and workspace integration.
Changes:
- Added
mq-typecheckercrate (types, constraint generation, unification, builtin signatures, and publicTypeCheckerAPI). - Added extensive test suite (integration, builtin typing via
rstest, error location, and debug-oriented tests). - Wired the crate into the workspace; adjusted HIR call-argument parenting for improved symbol structure.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/mq-typechecker/src/lib.rs | Public API + builtin type signatures and diagnostics types |
| crates/mq-typechecker/src/types.rs | Core type/type-scheme representation + basic matching utilities |
| crates/mq-typechecker/src/infer.rs | Inference context, substitutions, and overload resolution |
| crates/mq-typechecker/src/constraint.rs | HIR-to-constraints generation (operators, calls, collections, control flow) |
| crates/mq-typechecker/src/unify.rs | Constraint solving + unification/occurs check + span conversion |
| crates/mq-typechecker/tests/integration_test.rs | Broad end-to-end success/error cases for the typechecker |
| crates/mq-typechecker/tests/builtin_test.rs | Parameterized builtin/operator typing tests via rstest |
| crates/mq-typechecker/tests/type_errors_test.rs | Focused tests for known/expected type error detection |
| crates/mq-typechecker/tests/error_location_test.rs | Tests intended to validate span/error readability behavior |
| crates/mq-typechecker/tests/debug_builtin.rs | Debug-only HIR inspection test for builtins |
| crates/mq-typechecker/tests/debug_abs_test.rs | Debug-only typechecking inspection test for abs(42) |
| crates/mq-typechecker/README.md | Crate-level documentation and usage examples |
| crates/mq-typechecker/Cargo.toml | New crate manifest + dependencies/dev-dependencies |
| crates/mq-typechecker/.gitignore | Ignores generated docs dir for the new crate |
| crates/mq-lang/modules/table.mq | Modified module code/comments (currently appears corrupted) |
| crates/mq-hir/src/hir.rs | Ensures call arguments are parented under the call symbol node |
| Cargo.toml | Adds crates/mq-typechecker to the workspace members |
02334e8 to
db1b2b1
Compare
538c9cb to
4e5cbf1
Compare
…rt comparator Co-authored-by: harehare <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: harehare <[email protected]>
…hance error location tests
Co-authored-by: Copilot <[email protected]>
…e type checking tests
…der function type checking and enhance lambda type validation in integration tests
…a and function definitions
…hance error location tests
…n in debug functions
…ns with generic and None propagation support
…orting - Refactored constraint generation to use a pre-built children index for efficient symbol traversal. - Added single-pass symbol categorization to replace multiple iterations. - Improved error reporting with new report_no_matching_overload method. - Updated function signatures to use the new index. - Changed Substitution to use FxHashMap for performance. Related files: constraint.rs, infer.rs, lib.rs, types.rs
| // `&&` and `||` can accept any types (e.g., `none || "default"`, `is_array(x) && len(x)`). | ||
| for name in ["&&", "||", "and", "or"] { | ||
| let (a, b) = (ctx.fresh_var(), ctx.fresh_var()); | ||
| register_binary(ctx, name, Type::Var(a), Type::Var(b), Type::Var(b)); | ||
| } | ||
|
|
||
| // Variadic logical: or/and with 3-6 boolean arguments | ||
| for n in 3..=6 { | ||
| let params = vec![Type::Bool; n]; | ||
| for name in ["or", "and"] { | ||
| ctx.register_builtin(name, Type::function(params.clone(), Type::Bool)); |
There was a problem hiding this comment.
The generic overload for and/or/&&/|| is registered as (a, b) -> b, but mq’s runtime semantics can also produce false (and for || it can return either operand depending on truthiness). This will cause incorrect inferred types (e.g., x = "a" || 0 can evaluate to a string, but the checker will force x to type like 0). Consider making the return type a union that includes bool plus the possible operand types, and ensure the signatures reflect the actual variadic behavior of and(...)/or(...).
| // `&&` and `||` can accept any types (e.g., `none || "default"`, `is_array(x) && len(x)`). | |
| for name in ["&&", "||", "and", "or"] { | |
| let (a, b) = (ctx.fresh_var(), ctx.fresh_var()); | |
| register_binary(ctx, name, Type::Var(a), Type::Var(b), Type::Var(b)); | |
| } | |
| // Variadic logical: or/and with 3-6 boolean arguments | |
| for n in 3..=6 { | |
| let params = vec![Type::Bool; n]; | |
| for name in ["or", "and"] { | |
| ctx.register_builtin(name, Type::function(params.clone(), Type::Bool)); | |
| // `&&` and `||` (and their word forms) can accept any types and may return: | |
| // - `false`, or | |
| // - either operand, depending on truthiness. | |
| // | |
| // We approximate this with a generic overload: | |
| // forall a b. (a, b) -> bool | a | b | |
| for name in ["&&", "||", "and", "or"] { | |
| let (a, b) = (ctx.fresh_var(), ctx.fresh_var()); | |
| let lhs = Type::Var(a); | |
| let rhs = Type::Var(b); | |
| let result = Type::union(vec![Type::Bool, lhs.clone(), rhs.clone()]); | |
| register_binary(ctx, name, lhs, rhs, result); | |
| } | |
| // Variadic logical: and/or with 3-6 arguments. | |
| // Runtime semantics: may return `false` or any of the operands based on truthiness. | |
| // We model this as: | |
| // forall a1..an. (a1, ..., an) -> bool | a1 | ... | an | |
| for n in 3..=6 { | |
| let type_vars: Vec<_> = (0..n).map(|_| ctx.fresh_var()).collect(); | |
| let params: Vec<Type> = type_vars.iter().map(|v| Type::Var(*v)).collect(); | |
| let mut result_members: Vec<Type> = vec![Type::Bool]; | |
| result_members.extend(type_vars.iter().map(|v| Type::Var(*v))); | |
| let result = Type::union(result_members); | |
| for name in ["or", "and"] { | |
| ctx.register_builtin(name, Type::function(params.clone(), result.clone())); |
… tests for mixed type operations
… type error tests
d209665 to
4d22d1d
Compare
… variable reassignment tests
| // Union types: a union can unify with a type if any of its members can unify with it | ||
| (Type::Union(types), other) | (other, Type::Union(types)) => { | ||
| // Check if the other type matches any member of the union | ||
| let matches_any = types.iter().any(|t| { | ||
| // Try to check if types can match without adding errors | ||
| match (t, other) { | ||
| (t1, t2) if std::mem::discriminant(t1) == std::mem::discriminant(t2) => true, | ||
| (Type::Var(_), _) | (_, Type::Var(_)) => true, | ||
| _ => false, | ||
| } | ||
| }); | ||
|
|
||
| if !matches_any { | ||
| // No member of the union can unify with the other type - report error | ||
| let resolved_t1 = ctx.resolve_type(t1); | ||
| let resolved_t2 = ctx.resolve_type(t2); | ||
| ctx.add_error(TypeError::Mismatch { | ||
| expected: resolved_t1.display_renumbered(), | ||
| found: resolved_t2.display_renumbered(), | ||
| span: range.as_ref().map(range_to_span), | ||
| location: range.as_ref().map(|r| (r.start.line, r.start.column)), | ||
| }); | ||
| } | ||
| // If at least one member matches, allow it (union type semantics) | ||
| } |
There was a problem hiding this comment.
Union unification currently treats two types as compatible if their enum discriminants match (e.g. any array<T> matches any array<U>), which can let obviously-incompatible union members slip through without errors. This can silently accept invalid programs (and will also mis-handle nested structures). Consider using the existing structural compatibility check (Type::can_match) against the resolved types, or perform a non-allocating structural comparison that checks element/key/value/params recursively instead of only the top-level variant.
| // keys: {k: v} -> [k] | ||
| let (k, v) = (ctx.fresh_var(), ctx.fresh_var()); | ||
| register_unary( | ||
| ctx, | ||
| "keys", | ||
| Type::dict(Type::Var(k), Type::Var(v)), | ||
| Type::array(Type::Var(k)), | ||
| ); | ||
|
|
||
| // Generic fallback: keys/values/entries accept any type (for dynamically typed code | ||
| // where the argument type is determined by runtime guards like `is_dict`) | ||
| let (a, b) = (ctx.fresh_var(), ctx.fresh_var()); | ||
| register_unary(ctx, "keys", Type::Var(a), Type::array(Type::Var(b))); |
There was a problem hiding this comment.
keys is typed as {k: v} -> [k], but at runtime it always returns an array of strings (RuntimeValue::String(k.as_str())). The current signature will infer incorrect key types and can mask errors in downstream code; consider returning [string] (and keep the none -> none propagation overload).
| // keys: {k: v} -> [k] | |
| let (k, v) = (ctx.fresh_var(), ctx.fresh_var()); | |
| register_unary( | |
| ctx, | |
| "keys", | |
| Type::dict(Type::Var(k), Type::Var(v)), | |
| Type::array(Type::Var(k)), | |
| ); | |
| // Generic fallback: keys/values/entries accept any type (for dynamically typed code | |
| // where the argument type is determined by runtime guards like `is_dict`) | |
| let (a, b) = (ctx.fresh_var(), ctx.fresh_var()); | |
| register_unary(ctx, "keys", Type::Var(a), Type::array(Type::Var(b))); | |
| // keys: {k: v} -> [string] | |
| let (k, v) = (ctx.fresh_var(), ctx.fresh_var()); | |
| register_unary( | |
| ctx, | |
| "keys", | |
| Type::dict(Type::Var(k), Type::Var(v)), | |
| Type::array(Type::string()), | |
| ); | |
| // Generic fallback: keys/values/entries accept any type (for dynamically typed code | |
| // where the argument type is determined by runtime guards like `is_dict`) | |
| let (a, _b) = (ctx.fresh_var(), ctx.fresh_var()); | |
| register_unary(ctx, "keys", Type::Var(a), Type::array(Type::string())); |
No description provided.