Feat: add type-state AST and semantic analysis pass
- Update ast.rs with Phase trait (Parsed/Typed), Ty enum, and generic AST nodes so the same tree works pre- and post-type-checking - Add checker/ module implementing the 4-pass semantic analyser from SEMANTICS.md: struct/function collection, field resolution + size-cycle detection, full expression/statement type checking, and entry-point validation - Wire checker into main; semantic errors are only run when the parse succeeds and are rendered with the same diagnostic machinery Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
677
fluxc/src/checker/expr.rs
Normal file
677
fluxc/src/checker/expr.rs
Normal file
@@ -0,0 +1,677 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::ast::{self, BinaryOp, CompoundAssignOp, ExprKind, Parsed, Ty, UnaryOp};
|
||||
use crate::diagnostics::{Diagnostic, Label};
|
||||
use crate::token::Span;
|
||||
use super::env::TypeEnv;
|
||||
use super::Checker;
|
||||
|
||||
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 → i32
|
||||
ExprKind::IntLit(_) => Ty::I32,
|
||||
|
||||
// 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 => {
|
||||
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)),
|
||||
);
|
||||
}
|
||||
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() && !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
|
||||
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 matching integer types
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user