Skip to content

✨ feat: add typechecker crate#850

Merged
harehare merged 64 commits intomainfrom
feat/add-typechecker-crate
Feb 28, 2026
Merged

✨ feat: add typechecker crate#850
harehare merged 64 commits intomainfrom
feat/add-typechecker-crate

Conversation

@harehare
Copy link
Copy Markdown
Owner

@harehare harehare commented Nov 15, 2025

No description provided.

@harehare harehare force-pushed the feat/add-typechecker-crate branch from 53ab26c to 9ffd4ee Compare November 23, 2025 13:39
@harehare harehare force-pushed the feat/add-typechecker-crate branch from 9ffd4ee to f1eb752 Compare December 13, 2025 12:49
@harehare harehare force-pushed the feat/add-typechecker-crate branch from f1eb752 to 02334e8 Compare February 9, 2026 13:43
Copilot AI review requested due to automatic review settings February 9, 2026 13:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-typechecker crate (types, constraint generation, unification, builtin signatures, and public TypeChecker API).
  • 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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 9, 2026

Merging this PR will not alter performance

✅ 29 untouched benchmarks


Comparing feat/add-typechecker-crate (e9abb63) with main (c776692)

Open in CodSpeed

@harehare harehare closed this Feb 15, 2026
@harehare harehare reopened this Feb 15, 2026
@harehare harehare force-pushed the feat/add-typechecker-crate branch from 02334e8 to db1b2b1 Compare February 15, 2026 08:37
@harehare harehare changed the title ✨ feat(typechecker): add Hindley-Milner type inference crate ✨ feat(typechecker): add typechecker crate Feb 15, 2026
@harehare harehare changed the title ✨ feat(typechecker): add typechecker crate ✨ feat: add typechecker crate Feb 15, 2026
Copilot AI review requested due to automatic review settings February 15, 2026 08:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 15 comments.

Copilot AI review requested due to automatic review settings February 15, 2026 10:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 8 comments.

Copilot AI review requested due to automatic review settings February 15, 2026 11:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.

Copilot AI review requested due to automatic review settings February 15, 2026 14:21
@harehare harehare force-pushed the feat/add-typechecker-crate branch from 538c9cb to 4e5cbf1 Compare February 15, 2026 14:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

@harehare harehare marked this pull request as ready for review February 15, 2026 14:47
Copilot AI review requested due to automatic review settings February 16, 2026 00:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.

Copilot AI and others added 18 commits February 28, 2026 13:36
…der function type checking and enhance lambda type validation in integration tests
…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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 19 changed files in this pull request and generated 1 comment.

Comment on lines +215 to +225
// `&&` 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));
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...).

Suggested change
// `&&` 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()));

Copilot uses AI. Check for mistakes.
@harehare harehare force-pushed the feat/add-typechecker-crate branch from d209665 to 4d22d1d Compare February 28, 2026 06:53
Copilot AI review requested due to automatic review settings February 28, 2026 12:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 21 changed files in this pull request and generated 3 comments.

Comment on lines +119 to +143
// 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)
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +448 to +460
// 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)));
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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()));

Copilot uses AI. Check for mistakes.
@harehare harehare merged commit ac9e417 into main Feb 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants