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>
750 lines
29 KiB
Rust
750 lines
29 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use super::Checker;
|
|
use super::env::TypeEnv;
|
|
use crate::ast::{self, BinaryOp, CompoundAssignOp, ExprKind, Parsed, Ty, UnaryOp};
|
|
use crate::diagnostics::{Diagnostic, Label};
|
|
use crate::token::Span;
|
|
|
|
impl Checker {
|
|
/// Type-check `expr` in environment `env` with definite-assignment set `assigned`.
|
|
/// Returns the resolved type. Does NOT mutate `env` or `assigned`.
|
|
pub fn check_expr(
|
|
&mut self,
|
|
expr: &ast::Expr<Parsed>,
|
|
env: &TypeEnv,
|
|
assigned: &HashSet<String>,
|
|
) -> Ty {
|
|
match &expr.kind {
|
|
// T-IntLit → UnboundInt (resolved from context; defaults to i32)
|
|
ExprKind::IntLit(_) => Ty::UnboundInt,
|
|
|
|
// T-FloatLit → f64
|
|
ExprKind::FloatLit(_) => Ty::F64,
|
|
|
|
// T-StringLit → *char
|
|
ExprKind::StringLit(_) => Ty::Ptr {
|
|
mutable: false,
|
|
pointee: Box::new(Ty::Char),
|
|
},
|
|
|
|
// T-CharLit → char
|
|
ExprKind::CharLit(_) => Ty::Char,
|
|
|
|
// T-Bool → bool
|
|
ExprKind::Bool(_) => Ty::Bool,
|
|
|
|
// T-Ident
|
|
ExprKind::Ident(name) => match env.lookup(name) {
|
|
Some((ty, _)) => {
|
|
if !assigned.contains(name) {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"use of potentially uninitialized variable `{name}`"
|
|
))
|
|
.with_label(Label::primary(expr.span)),
|
|
);
|
|
Ty::Error
|
|
} else {
|
|
ty.clone()
|
|
}
|
|
}
|
|
None => {
|
|
self.emit(
|
|
Diagnostic::error(format!("undefined variable `{name}`"))
|
|
.with_label(Label::primary(expr.span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
},
|
|
|
|
// T-StructLit
|
|
ExprKind::StructLit {
|
|
name,
|
|
name_span,
|
|
fields,
|
|
} => {
|
|
if !self.sigma.contains(name) {
|
|
self.emit(
|
|
Diagnostic::error(format!("undefined struct `{name}`"))
|
|
.with_label(Label::primary(*name_span)),
|
|
);
|
|
// Still check field expressions for further errors
|
|
for sf in fields {
|
|
self.check_expr(&sf.value, env, assigned);
|
|
}
|
|
return Ty::Error;
|
|
}
|
|
|
|
let expected: Vec<(String, Ty)> = self
|
|
.sigma
|
|
.fields(name)
|
|
.unwrap_or(&[])
|
|
.iter()
|
|
.map(|f| (f.name.clone(), f.ty.clone()))
|
|
.collect();
|
|
|
|
let mut provided: HashSet<String> = HashSet::new();
|
|
for sf in fields {
|
|
let val_ty = self.check_expr(&sf.value, env, assigned);
|
|
if provided.contains(&sf.name) {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"field `{}` specified more than once",
|
|
sf.name
|
|
))
|
|
.with_label(Label::primary(sf.name_span)),
|
|
);
|
|
continue;
|
|
}
|
|
provided.insert(sf.name.clone());
|
|
match expected.iter().find(|(n, _)| *n == sf.name) {
|
|
Some((_, exp_ty)) => {
|
|
if !val_ty.is_error()
|
|
&& !exp_ty.is_error()
|
|
&& !val_ty.promotes_to(exp_ty)
|
|
{
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"field `{}`: expected `{}`, got `{}`",
|
|
sf.name,
|
|
exp_ty.display(),
|
|
val_ty.display()
|
|
))
|
|
.with_label(Label::primary(sf.value.span)),
|
|
);
|
|
}
|
|
}
|
|
None => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"struct `{name}` has no field `{}`",
|
|
sf.name
|
|
))
|
|
.with_label(Label::primary(sf.name_span)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (fname, _) in &expected {
|
|
if !provided.contains(fname) {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"missing field `{fname}` in struct literal `{name}`"
|
|
))
|
|
.with_label(Label::primary(*name_span)),
|
|
);
|
|
}
|
|
}
|
|
|
|
Ty::Struct(name.clone())
|
|
}
|
|
|
|
// T-Unary
|
|
ExprKind::Unary {
|
|
op,
|
|
op_span,
|
|
expr: inner,
|
|
} => {
|
|
let inner_ty = self.check_expr(inner, env, assigned);
|
|
if inner_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
self.check_unary(*op, *op_span, inner, inner_ty, env)
|
|
}
|
|
|
|
// T-Binary
|
|
ExprKind::Binary {
|
|
op,
|
|
op_span,
|
|
lhs,
|
|
rhs,
|
|
} => self.check_binary(*op, *op_span, lhs, rhs, env, assigned),
|
|
|
|
// T-CompoundAssign: lhs op= rhs (expands to lhs = lhs op rhs)
|
|
ExprKind::CompoundAssign {
|
|
op,
|
|
op_span,
|
|
lhs,
|
|
rhs,
|
|
} => {
|
|
if !self.is_mutable_place(lhs, env) {
|
|
self.emit(
|
|
Diagnostic::error(
|
|
"left-hand side of compound assignment must be a mutable place",
|
|
)
|
|
.with_label(Label::primary(lhs.span)),
|
|
);
|
|
}
|
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
|
if !lhs_ty.is_error() && !rhs_ty.is_error() {
|
|
let bin_op = compound_to_binary(*op);
|
|
self.check_arith_or_shift(lhs_ty, rhs_ty, bin_op, *op_span);
|
|
}
|
|
Ty::Unit
|
|
}
|
|
|
|
// T-Field
|
|
ExprKind::Field {
|
|
expr: inner,
|
|
field,
|
|
field_span,
|
|
} => {
|
|
let inner_ty = self.check_expr(inner, env, assigned);
|
|
if inner_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
let struct_name = match &inner_ty {
|
|
Ty::Struct(n) => n.clone(),
|
|
_ => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"field access on non-struct type `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(*field_span)),
|
|
);
|
|
return Ty::Error;
|
|
}
|
|
};
|
|
match self.sigma.field_ty(&struct_name, field) {
|
|
Some(ty) => ty.clone(),
|
|
None => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"struct `{struct_name}` has no field `{field}`"
|
|
))
|
|
.with_label(Label::primary(*field_span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
}
|
|
}
|
|
|
|
// T-Index
|
|
ExprKind::Index { expr: inner, index } => {
|
|
let inner_ty = self.check_expr(inner, env, assigned);
|
|
let idx_ty = self.check_expr(index, env, assigned);
|
|
if inner_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
if !idx_ty.is_error() && !idx_ty.is_integer() {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"index must be an integer type, got `{}`",
|
|
idx_ty.display()
|
|
))
|
|
.with_label(Label::primary(index.span)),
|
|
);
|
|
}
|
|
match inner_ty {
|
|
Ty::Array { elem, .. } => *elem,
|
|
Ty::Ptr { pointee, .. } => *pointee,
|
|
_ => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"indexing requires array or pointer, got `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(inner.span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
}
|
|
}
|
|
|
|
// T-Call
|
|
ExprKind::Call { callee, args } => {
|
|
let func_name = match &callee.kind {
|
|
ExprKind::Ident(name) => name.clone(),
|
|
_ => {
|
|
self.emit(
|
|
Diagnostic::error("callee must be a function name")
|
|
.with_label(Label::primary(callee.span)),
|
|
);
|
|
for arg in args {
|
|
self.check_expr(arg, env, assigned);
|
|
}
|
|
return Ty::Error;
|
|
}
|
|
};
|
|
|
|
let (param_tys, ret_ty) = match self.phi.get(&func_name) {
|
|
Some(entry) => {
|
|
let pts: Vec<Ty> = entry.params.iter().map(|p| p.ty.clone()).collect();
|
|
let ret = entry.ret.clone();
|
|
(pts, ret)
|
|
}
|
|
None => {
|
|
self.emit(
|
|
Diagnostic::error(format!("undefined function `{func_name}`"))
|
|
.with_label(Label::primary(callee.span)),
|
|
);
|
|
for arg in args {
|
|
self.check_expr(arg, env, assigned);
|
|
}
|
|
return Ty::Error;
|
|
}
|
|
};
|
|
|
|
if args.len() != param_tys.len() {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"`{func_name}` expects {} argument(s), got {}",
|
|
param_tys.len(),
|
|
args.len()
|
|
))
|
|
.with_label(Label::primary(callee.span)),
|
|
);
|
|
}
|
|
|
|
for (i, arg) in args.iter().enumerate() {
|
|
let arg_ty = self.check_expr(arg, env, assigned);
|
|
if let Some(exp) = param_tys.get(i) {
|
|
if !arg_ty.is_error() && !exp.is_error() && !arg_ty.promotes_to(exp) {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"argument {}: expected `{}`, got `{}`",
|
|
i + 1,
|
|
exp.display(),
|
|
arg_ty.display()
|
|
))
|
|
.with_label(Label::primary(arg.span)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
ret_ty
|
|
}
|
|
|
|
ExprKind::Group(inner) => self.check_expr(inner, env, assigned),
|
|
|
|
ExprKind::Error => Ty::Error,
|
|
}
|
|
}
|
|
|
|
// ── Unary helper ──────────────────────────────────────────────────────────
|
|
|
|
fn check_unary(
|
|
&mut self,
|
|
op: UnaryOp,
|
|
op_span: Span,
|
|
inner: &ast::Expr<Parsed>,
|
|
inner_ty: Ty,
|
|
env: &TypeEnv,
|
|
) -> 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!(
|
|
"unary `-` requires a signed integer or float, got `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
} else {
|
|
inner_ty
|
|
}
|
|
}
|
|
UnaryOp::Not => {
|
|
if inner_ty != Ty::Bool {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"unary `!` requires `bool`, got `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
} else {
|
|
Ty::Bool
|
|
}
|
|
}
|
|
UnaryOp::BitNot => {
|
|
if !inner_ty.is_integer() {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"unary `~` requires an integer type, got `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
} else {
|
|
inner_ty
|
|
}
|
|
}
|
|
UnaryOp::Deref => match &inner_ty {
|
|
Ty::Ptr { pointee, .. } => *pointee.clone(),
|
|
Ty::OpaquePtr { .. } => {
|
|
self.emit(
|
|
Diagnostic::error("cannot dereference an opaque pointer")
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
_ => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"unary `*` requires a pointer, got `{}`",
|
|
inner_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
},
|
|
UnaryOp::AddrOf => {
|
|
if !self.is_place(inner, env) {
|
|
self.emit(
|
|
Diagnostic::error("cannot take address of a non-place expression")
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
return Ty::Error;
|
|
}
|
|
let mutable = self.is_mutable_place(inner, env);
|
|
Ty::Ptr {
|
|
mutable,
|
|
pointee: Box::new(inner_ty),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Binary helper ─────────────────────────────────────────────────────────
|
|
|
|
fn check_binary(
|
|
&mut self,
|
|
op: BinaryOp,
|
|
op_span: Span,
|
|
lhs: &ast::Expr<Parsed>,
|
|
rhs: &ast::Expr<Parsed>,
|
|
env: &TypeEnv,
|
|
assigned: &HashSet<String>,
|
|
) -> Ty {
|
|
match op {
|
|
// T-Assign
|
|
BinaryOp::Assign => {
|
|
if !self.is_mutable_place(lhs, env) {
|
|
self.emit(
|
|
Diagnostic::error("left-hand side of `=` must be a mutable place")
|
|
.with_label(Label::primary(lhs.span)),
|
|
);
|
|
}
|
|
// For a bare identifier on the LHS we only need its declared
|
|
// type, not a read — don't emit "uninitialized" for the target
|
|
// of the assignment itself.
|
|
let lhs_ty = self.lhs_ty(lhs, env, assigned);
|
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
|
if !lhs_ty.is_error() && !rhs_ty.is_error() && !rhs_ty.promotes_to(&lhs_ty) {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"type mismatch: expected `{}`, got `{}`",
|
|
lhs_ty.display(),
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(rhs.span)),
|
|
);
|
|
}
|
|
Ty::Unit
|
|
}
|
|
|
|
// Logical
|
|
BinaryOp::Or | BinaryOp::And => {
|
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
|
let kw = if op == BinaryOp::Or { "or" } else { "and" };
|
|
if !lhs_ty.is_error() && lhs_ty != Ty::Bool {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"`{kw}` requires `bool` operands, left side is `{}`",
|
|
lhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(lhs.span)),
|
|
);
|
|
}
|
|
if !rhs_ty.is_error() && rhs_ty != Ty::Bool {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"`{kw}` requires `bool` operands, right side is `{}`",
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(rhs.span)),
|
|
);
|
|
}
|
|
Ty::Bool
|
|
}
|
|
|
|
// Equality: compatible types OR pointer-compatible
|
|
BinaryOp::Eq | BinaryOp::Ne => {
|
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
|
if !lhs_ty.is_error() && !rhs_ty.is_error() {
|
|
let ok = Ty::common(&lhs_ty, &rhs_ty).is_some()
|
|
|| Ty::ptr_common(&lhs_ty, &rhs_ty).is_some();
|
|
if !ok {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"cannot compare `{}` with `{}`",
|
|
lhs_ty.display(),
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
}
|
|
}
|
|
Ty::Bool
|
|
}
|
|
|
|
// 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);
|
|
if !lhs_ty.is_error() && !rhs_ty.is_error() {
|
|
let ok = match Ty::common(&lhs_ty, &rhs_ty) {
|
|
Some(ref c) => c.is_numeric() || *c == Ty::Char,
|
|
None => false,
|
|
};
|
|
if !ok {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"cannot order `{}` and `{}`",
|
|
lhs_ty.display(),
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
}
|
|
}
|
|
Ty::Bool
|
|
}
|
|
|
|
// 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);
|
|
if lhs_ty.is_error() || rhs_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
match Ty::common(&lhs_ty, &rhs_ty) {
|
|
Some(ref c) if c.is_integer() => c.clone(),
|
|
_ => {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"bitwise operator requires integer operands, got `{}` and `{}`",
|
|
lhs_ty.display(),
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
if lhs_ty.is_error() || rhs_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
if !lhs_ty.is_integer() {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"shift requires an integer LHS, got `{}`",
|
|
lhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(lhs.span)),
|
|
);
|
|
return Ty::Error;
|
|
}
|
|
if !rhs_ty.is_integer() {
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"shift amount must be an integer, got `{}`",
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(rhs.span)),
|
|
);
|
|
return Ty::Error;
|
|
}
|
|
lhs_ty
|
|
}
|
|
|
|
// Arithmetic
|
|
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
|
|
let lhs_ty = self.check_expr(lhs, env, assigned);
|
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
|
if lhs_ty.is_error() || rhs_ty.is_error() {
|
|
return Ty::Error;
|
|
}
|
|
self.check_arith_or_shift(lhs_ty, rhs_ty, op, op_span)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the declared type of `expr` as a write-side place, without
|
|
/// checking that it has been initialised first. For a bare identifier
|
|
/// this avoids a spurious "uninitialized" diagnostic on the target of
|
|
/// an assignment like `x = 5` when `x` is being given its first value.
|
|
///
|
|
/// For all other place forms (deref, field, index) the sub-expression IS
|
|
/// read, so the normal `check_expr` path (with assigned checking) is used.
|
|
fn lhs_ty(
|
|
&mut self,
|
|
expr: &ast::Expr<Parsed>,
|
|
env: &TypeEnv,
|
|
assigned: &HashSet<String>,
|
|
) -> Ty {
|
|
match &expr.kind {
|
|
ExprKind::Ident(name) => match env.lookup(name) {
|
|
Some((ty, _)) => ty.clone(),
|
|
None => {
|
|
self.emit(
|
|
Diagnostic::error(format!("undefined variable `{name}`"))
|
|
.with_label(Label::primary(expr.span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
},
|
|
ExprKind::Group(inner) => self.lhs_ty(inner, env, assigned),
|
|
// All other place forms (deref, field, index) involve a read of
|
|
// the sub-expression — use the full check.
|
|
_ => self.check_expr(expr, env, assigned),
|
|
}
|
|
}
|
|
|
|
/// Check that `lhs_ty op rhs_ty` is a valid arithmetic expression;
|
|
/// returns the result type.
|
|
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 {
|
|
BinaryOp::Add => "+",
|
|
BinaryOp::Sub => "-",
|
|
BinaryOp::Mul => "*",
|
|
BinaryOp::Div => "/",
|
|
BinaryOp::Rem => "%",
|
|
_ => "op",
|
|
};
|
|
self.emit(
|
|
Diagnostic::error(format!(
|
|
"`{sym}` requires numeric operands, got `{}` and `{}`",
|
|
lhs_ty.display(),
|
|
rhs_ty.display()
|
|
))
|
|
.with_label(Label::primary(op_span)),
|
|
);
|
|
Ty::Error
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Place predicates ──────────────────────────────────────────────────────
|
|
|
|
/// Returns true if `expr` is a syntactic place (lvalue).
|
|
pub fn is_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
|
match &expr.kind {
|
|
ExprKind::Ident(_) => true,
|
|
ExprKind::Unary {
|
|
op: UnaryOp::Deref, ..
|
|
} => true,
|
|
ExprKind::Field { expr: inner, .. } => self.is_place(inner, env),
|
|
ExprKind::Index { expr: inner, .. } => self.is_place(inner, env),
|
|
ExprKind::Group(inner) => self.is_place(inner, env),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns true if `expr` is a mutable place.
|
|
///
|
|
/// Rules (§6.3):
|
|
/// - `x` is mutable iff `x` is declared `mut`.
|
|
/// - `*e` is mutable iff `e` has type `*mut T`.
|
|
/// - `e.f` / `e[i]` is mutable iff `e` is mutable.
|
|
pub fn is_mutable_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
|
match &expr.kind {
|
|
ExprKind::Ident(name) => env.lookup(name).map(|(_, m)| m).unwrap_or(false),
|
|
ExprKind::Unary {
|
|
op: UnaryOp::Deref,
|
|
expr: inner,
|
|
..
|
|
} => {
|
|
// Mutable iff inner's type is *mut T.
|
|
// We resolve the type of inner from env without emitting errors.
|
|
matches!(
|
|
self.peek_ty(inner, env),
|
|
Some(Ty::Ptr { mutable: true, .. })
|
|
)
|
|
}
|
|
ExprKind::Field { expr: inner, .. } => self.is_mutable_place(inner, env),
|
|
ExprKind::Index { expr: inner, .. } => self.is_mutable_place(inner, env),
|
|
ExprKind::Group(inner) => self.is_mutable_place(inner, env),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Lightweight type inference for place mutability — does NOT emit diagnostics.
|
|
fn peek_ty(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> Option<Ty> {
|
|
match &expr.kind {
|
|
ExprKind::Ident(name) => env.lookup(name).map(|(ty, _)| ty.clone()),
|
|
ExprKind::Group(inner) => self.peek_ty(inner, env),
|
|
ExprKind::Unary {
|
|
op: UnaryOp::Deref,
|
|
expr: inner,
|
|
..
|
|
} => match self.peek_ty(inner, env)? {
|
|
Ty::Ptr { pointee, .. } => Some(*pointee),
|
|
_ => None,
|
|
},
|
|
ExprKind::Field {
|
|
expr: inner, field, ..
|
|
} => {
|
|
let Ty::Struct(sname) = self.peek_ty(inner, env)? else {
|
|
return None;
|
|
};
|
|
self.sigma.field_ty(&sname, field).cloned()
|
|
}
|
|
ExprKind::Index { expr: inner, .. } => match self.peek_ty(inner, env)? {
|
|
Ty::Array { elem, .. } => Some(*elem),
|
|
Ty::Ptr { pointee, .. } => Some(*pointee),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
fn compound_to_binary(op: CompoundAssignOp) -> BinaryOp {
|
|
match op {
|
|
CompoundAssignOp::Add => BinaryOp::Add,
|
|
CompoundAssignOp::Sub => BinaryOp::Sub,
|
|
CompoundAssignOp::Mul => BinaryOp::Mul,
|
|
CompoundAssignOp::Div => BinaryOp::Div,
|
|
CompoundAssignOp::Rem => BinaryOp::Rem,
|
|
CompoundAssignOp::BitAnd => BinaryOp::BitAnd,
|
|
CompoundAssignOp::BitOr => BinaryOp::BitOr,
|
|
CompoundAssignOp::BitXor => BinaryOp::BitXor,
|
|
CompoundAssignOp::Shl => BinaryOp::Shl,
|
|
CompoundAssignOp::Shr => BinaryOp::Shr,
|
|
}
|
|
}
|