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),
|
Struct(String),
|
||||||
// Internal function signature (not user-facing)
|
// Internal function signature (not user-facing)
|
||||||
FnSig { params: Vec<Ty>, ret: Box<Ty> },
|
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 propagation sentinel
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
@@ -68,7 +72,7 @@ impl Ty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_integer(&self) -> bool {
|
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 {
|
pub fn is_float(&self) -> bool {
|
||||||
@@ -99,6 +103,11 @@ impl Ty {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
match (self, target) {
|
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
|
// Unsigned widening: same unsigned category, rank strictly increases
|
||||||
(a, b) if a.is_unsigned() && b.is_unsigned() => {
|
(a, b) if a.is_unsigned() && b.is_unsigned() => {
|
||||||
a.rank().unwrap_or(0) < b.rank().unwrap_or(0)
|
a.rank().unwrap_or(0) < b.rank().unwrap_or(0)
|
||||||
@@ -132,6 +141,13 @@ impl Ty {
|
|||||||
if a.is_error() || b.is_error() {
|
if a.is_error() || b.is_error() {
|
||||||
return Some(Ty::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) {
|
if b.promotes_to(a) {
|
||||||
return Some(a.clone());
|
return Some(a.clone());
|
||||||
}
|
}
|
||||||
@@ -195,6 +211,7 @@ impl Ty {
|
|||||||
let ps: Vec<_> = params.iter().map(|p| p.display()).collect();
|
let ps: Vec<_> = params.iter().map(|p| p.display()).collect();
|
||||||
format!("fn({}) -> {}", ps.join(", "), ret.display())
|
format!("fn({}) -> {}", ps.join(", "), ret.display())
|
||||||
}
|
}
|
||||||
|
Ty::UnboundInt => "{integer}".to_string(),
|
||||||
Ty::Error => "<error>".to_string(),
|
Ty::Error => "<error>".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ impl Checker {
|
|||||||
assigned: &HashSet<String>,
|
assigned: &HashSet<String>,
|
||||||
) -> Ty {
|
) -> Ty {
|
||||||
match &expr.kind {
|
match &expr.kind {
|
||||||
// T-IntLit → i32
|
// T-IntLit → UnboundInt (resolved from context; defaults to i32)
|
||||||
ExprKind::IntLit(_) => Ty::I32,
|
ExprKind::IntLit(_) => Ty::UnboundInt,
|
||||||
|
|
||||||
// T-FloatLit → f64
|
// T-FloatLit → f64
|
||||||
ExprKind::FloatLit(_) => Ty::F64,
|
ExprKind::FloatLit(_) => Ty::F64,
|
||||||
@@ -338,6 +338,10 @@ impl Checker {
|
|||||||
) -> Ty {
|
) -> Ty {
|
||||||
match op {
|
match op {
|
||||||
UnaryOp::Neg => {
|
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() {
|
if !inner_ty.is_signed() && !inner_ty.is_float() {
|
||||||
self.emit(
|
self.emit(
|
||||||
Diagnostic::error(format!(
|
Diagnostic::error(format!(
|
||||||
@@ -501,7 +505,7 @@ impl Checker {
|
|||||||
Ty::Bool
|
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 => {
|
BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
|
||||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||||
let rhs_ty = self.check_expr(rhs, env, assigned);
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
||||||
@@ -524,7 +528,7 @@ impl Checker {
|
|||||||
Ty::Bool
|
Ty::Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bitwise: require matching integer types
|
// Bitwise: require integer types (UnboundInt is fine)
|
||||||
BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => {
|
BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => {
|
||||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||||
let rhs_ty = self.check_expr(rhs, 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 => {
|
BinaryOp::Shl | BinaryOp::Shr => {
|
||||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
||||||
let rhs_ty = self.check_expr(rhs, 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 {
|
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) {
|
match Ty::common(&lhs_ty, &rhs_ty) {
|
||||||
Some(Ty::Error) => Ty::Error,
|
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(),
|
Some(ref c) if c.is_numeric() => c.clone(),
|
||||||
_ => {
|
_ => {
|
||||||
let sym = match op {
|
let sym = match op {
|
||||||
|
|||||||
@@ -120,8 +120,9 @@ mod tests {
|
|||||||
// ── Literals ──────────────────────────────────────────────────────────────
|
// ── Literals ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn int_literal_is_i32() {
|
fn int_literal_coerces_to_annotated_type() {
|
||||||
ok("fn main() { let x: i32 = 42; }");
|
// 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]
|
#[test]
|
||||||
@@ -139,12 +140,64 @@ mod tests {
|
|||||||
ok("fn main() { let x: char = 'a'; }");
|
ok("fn main() { let x: char = 'a'; }");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Implicit promotion in let ──────────────────────────────────────────────
|
// ── Integer literal inference ──────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn i8_promotes_to_i32_in_let() {
|
fn literal_coerces_to_any_integer_size() {
|
||||||
// i32 literal (type i32) promotes to i64
|
// UnboundInt coerces to all integer types.
|
||||||
ok("fn main() { let x: i64 = 1; }");
|
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]
|
#[test]
|
||||||
@@ -153,7 +206,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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");
|
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 ────────────────────────────────────────────────────────
|
// ── Type inference ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user