Feat: add UnboundInt type for integer literal inference
Integer literals now produce `Ty::UnboundInt` instead of a hardcoded `i32`. This flexible type promotes to/from any concrete integer, so literals resolve naturally in context (e.g. `n < 2` when `n: u8` works without an explicit cast). `common(UnboundInt, T)` returns `T` when `T` is a concrete integer, so binary ops adopt the concrete operand's type. Includes 8 new tests covering literal coercion, fibonacci-style patterns, and negative cases (literals still don't coerce to float types). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,10 @@ pub enum Ty {
|
||||
Struct(String),
|
||||
// Internal function signature (not user-facing)
|
||||
FnSig { params: Vec<Ty>, ret: Box<Ty> },
|
||||
/// Unresolved integer type from a literal or an unannotated let-binding.
|
||||
/// Compatible with every concrete integer type; defaults to `i32` in
|
||||
/// error messages when no concrete type can be inferred.
|
||||
UnboundInt,
|
||||
// Error propagation sentinel
|
||||
Error,
|
||||
}
|
||||
@@ -68,7 +72,7 @@ impl Ty {
|
||||
}
|
||||
|
||||
pub fn is_integer(&self) -> bool {
|
||||
self.is_unsigned() || self.is_signed()
|
||||
self.is_unsigned() || self.is_signed() || matches!(self, Ty::UnboundInt)
|
||||
}
|
||||
|
||||
pub fn is_float(&self) -> bool {
|
||||
@@ -99,6 +103,11 @@ impl Ty {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
// UnboundInt (from integer literal / unannotated let) promotes to
|
||||
// any concrete integer type, and any concrete integer promotes to
|
||||
// UnboundInt so it can receive a typed value.
|
||||
(Ty::UnboundInt, t) if t.is_integer() => true,
|
||||
(t, Ty::UnboundInt) if t.is_integer() => true,
|
||||
// Unsigned widening: same unsigned category, rank strictly increases
|
||||
(a, b) if a.is_unsigned() && b.is_unsigned() => {
|
||||
a.rank().unwrap_or(0) < b.rank().unwrap_or(0)
|
||||
@@ -132,6 +141,13 @@ impl Ty {
|
||||
if a.is_error() || b.is_error() {
|
||||
return Some(Ty::Error);
|
||||
}
|
||||
// UnboundInt is resolved by the concrete type; concrete type wins.
|
||||
// common(UnboundInt, UnboundInt) = UnboundInt.
|
||||
match (a, b) {
|
||||
(Ty::UnboundInt, _) if b.is_integer() => return Some(b.clone()),
|
||||
(_, Ty::UnboundInt) if a.is_integer() => return Some(a.clone()),
|
||||
_ => {}
|
||||
}
|
||||
if b.promotes_to(a) {
|
||||
return Some(a.clone());
|
||||
}
|
||||
@@ -195,6 +211,7 @@ impl Ty {
|
||||
let ps: Vec<_> = params.iter().map(|p| p.display()).collect();
|
||||
format!("fn({}) -> {}", ps.join(", "), ret.display())
|
||||
}
|
||||
Ty::UnboundInt => "{integer}".to_string(),
|
||||
Ty::Error => "<error>".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ impl Checker {
|
||||
assigned: &HashSet<String>,
|
||||
) -> Ty {
|
||||
match &expr.kind {
|
||||
// T-IntLit → i32
|
||||
ExprKind::IntLit(_) => Ty::I32,
|
||||
// T-IntLit → UnboundInt (resolved from context; defaults to i32)
|
||||
ExprKind::IntLit(_) => Ty::UnboundInt,
|
||||
|
||||
// T-FloatLit → f64
|
||||
ExprKind::FloatLit(_) => Ty::F64,
|
||||
@@ -338,6 +338,10 @@ impl Checker {
|
||||
) -> Ty {
|
||||
match op {
|
||||
UnaryOp::Neg => {
|
||||
// UnboundInt is a literal-origin integer; negating keeps it unbound.
|
||||
if matches!(inner_ty, Ty::UnboundInt) {
|
||||
return Ty::UnboundInt;
|
||||
}
|
||||
if !inner_ty.is_signed() && !inner_ty.is_float() {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
@@ -501,7 +505,7 @@ impl Checker {
|
||||
Ty::Bool
|
||||
}
|
||||
|
||||
// Ordering: numeric or char
|
||||
// Ordering: numeric or char (UnboundInt resolves to the other operand's type)
|
||||
BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
|
||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||
let rhs_ty = self.check_expr(rhs, env, assigned);
|
||||
@@ -524,7 +528,7 @@ impl Checker {
|
||||
Ty::Bool
|
||||
}
|
||||
|
||||
// Bitwise: require matching integer types
|
||||
// Bitwise: require integer types (UnboundInt is fine)
|
||||
BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => {
|
||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||
let rhs_ty = self.check_expr(rhs, env, assigned);
|
||||
@@ -547,7 +551,9 @@ impl Checker {
|
||||
}
|
||||
}
|
||||
|
||||
// Shift: LHS integer, RHS any integer; result type = LHS type
|
||||
// Shift: LHS integer, RHS any integer; result type = LHS type.
|
||||
// If LHS is UnboundInt and RHS is concrete, result is still UnboundInt
|
||||
// (it will be resolved when the result is used in context).
|
||||
BinaryOp::Shl | BinaryOp::Shr => {
|
||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||
let rhs_ty = self.check_expr(rhs, env, assigned);
|
||||
@@ -625,6 +631,8 @@ impl Checker {
|
||||
fn check_arith_or_shift(&mut self, lhs_ty: Ty, rhs_ty: Ty, op: BinaryOp, op_span: Span) -> Ty {
|
||||
match Ty::common(&lhs_ty, &rhs_ty) {
|
||||
Some(Ty::Error) => Ty::Error,
|
||||
// UnboundInt + UnboundInt → still UnboundInt (resolved downstream)
|
||||
Some(Ty::UnboundInt) => Ty::UnboundInt,
|
||||
Some(ref c) if c.is_numeric() => c.clone(),
|
||||
_ => {
|
||||
let sym = match op {
|
||||
|
||||
@@ -120,8 +120,9 @@ mod tests {
|
||||
// ── Literals ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn int_literal_is_i32() {
|
||||
ok("fn main() { let x: i32 = 42; }");
|
||||
fn int_literal_coerces_to_annotated_type() {
|
||||
// Integer literals have type UnboundInt and coerce to any annotated integer.
|
||||
ok("fn main() { let a: i32 = 42; let b: u64 = 0; let c: u8 = 255; let d: i8 = -1; }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -139,12 +140,64 @@ mod tests {
|
||||
ok("fn main() { let x: char = 'a'; }");
|
||||
}
|
||||
|
||||
// ── Implicit promotion in let ──────────────────────────────────────────────
|
||||
// ── Integer literal inference ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn i8_promotes_to_i32_in_let() {
|
||||
// i32 literal (type i32) promotes to i64
|
||||
ok("fn main() { let x: i64 = 1; }");
|
||||
fn literal_coerces_to_any_integer_size() {
|
||||
// UnboundInt coerces to all integer types.
|
||||
ok("fn main() { let a: u8 = 1; let b: u16 = 1; let c: u32 = 1; let d: u64 = 1; }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unannotated_let_infers_from_context() {
|
||||
// `let x = 0` gets type UnboundInt; used as u8 in comparison.
|
||||
ok("fn f(n: u8) -> u8 {
|
||||
let mut x = 0;
|
||||
while x < n { x += 1; }
|
||||
return x;
|
||||
} fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_in_binary_op_with_concrete_type() {
|
||||
// `n - 1` where n: u8 — literal 1 resolves to u8.
|
||||
ok("fn f(n: u8) -> u8 { return n - 1; } fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_in_comparison_with_concrete_type() {
|
||||
// `n < 2` where n: u8 — literal 2 resolves to u8.
|
||||
ok("fn f(n: u8) -> bool { return n < 2; } fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unannotated_literal_variable_as_return() {
|
||||
// `let x = 0;` with no annotation; returned as u64.
|
||||
ok("fn f() -> u64 { let x = 0; return x; } fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fibonacci_rec_typechecks() {
|
||||
ok("fn fibonacci_rec(n: u8) -> u64 {
|
||||
if n < 2 { return n; }
|
||||
return fibonacci_rec(n - 1) + fibonacci_rec(n - 2);
|
||||
} fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fibonacci_iter_typechecks() {
|
||||
ok("fn fibonacci_iter(n: u8) -> u64 {
|
||||
let mut counter = 0;
|
||||
let mut a = 0;
|
||||
let mut b = 1;
|
||||
while counter < n {
|
||||
let temp = a + b;
|
||||
a = b;
|
||||
b = temp;
|
||||
counter += 1;
|
||||
}
|
||||
return a;
|
||||
} fn main() { }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -153,7 +206,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_category_forbidden_in_let() {
|
||||
fn int_literal_does_not_coerce_to_float() {
|
||||
err_contains("fn main() { let x: f64 = 1; }", "type mismatch");
|
||||
}
|
||||
|
||||
@@ -165,6 +218,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_category_forbidden_in_let() {
|
||||
// Annotated-to-annotated cross-category still rejected.
|
||||
err_contains(
|
||||
"fn foo(v: u32) { let x: i32 = v; } fn main() { }",
|
||||
"type mismatch",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Type inference ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user