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:
277
fluxc/src/ast.rs
277
fluxc/src/ast.rs
@@ -1,5 +1,187 @@
|
||||
use crate::token::Span;
|
||||
|
||||
// ── Phase type-state ───────────────────────────────────────────────────────────
|
||||
|
||||
pub trait Phase {
|
||||
type ExprExtra: std::fmt::Debug + Clone;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parsed;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Typed;
|
||||
|
||||
impl Phase for Parsed {
|
||||
type ExprExtra = ();
|
||||
}
|
||||
|
||||
impl Phase for Typed {
|
||||
type ExprExtra = Ty;
|
||||
}
|
||||
|
||||
// ── Resolved type system ───────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ty {
|
||||
// Unsigned integers
|
||||
U8,
|
||||
U16,
|
||||
U32,
|
||||
U64,
|
||||
// Signed integers
|
||||
I8,
|
||||
I16,
|
||||
I32,
|
||||
I64,
|
||||
// Floating-point
|
||||
F32,
|
||||
F64,
|
||||
// Other primitives
|
||||
Bool,
|
||||
Char,
|
||||
Unit,
|
||||
// Pointer types
|
||||
Ptr { mutable: bool, pointee: Box<Ty> },
|
||||
OpaquePtr { mutable: bool },
|
||||
// Array type
|
||||
Array { elem: Box<Ty>, size: u64 },
|
||||
// User-defined struct
|
||||
Struct(String),
|
||||
// Internal function signature (not user-facing)
|
||||
FnSig { params: Vec<Ty>, ret: Box<Ty> },
|
||||
// Error propagation sentinel
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
pub fn is_error(&self) -> bool {
|
||||
matches!(self, Ty::Error)
|
||||
}
|
||||
|
||||
pub fn is_unsigned(&self) -> bool {
|
||||
matches!(self, Ty::U8 | Ty::U16 | Ty::U32 | Ty::U64)
|
||||
}
|
||||
|
||||
pub fn is_signed(&self) -> bool {
|
||||
matches!(self, Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64)
|
||||
}
|
||||
|
||||
pub fn is_integer(&self) -> bool {
|
||||
self.is_unsigned() || self.is_signed()
|
||||
}
|
||||
|
||||
pub fn is_float(&self) -> bool {
|
||||
matches!(self, Ty::F32 | Ty::F64)
|
||||
}
|
||||
|
||||
pub fn is_numeric(&self) -> bool {
|
||||
self.is_integer() || self.is_float()
|
||||
}
|
||||
|
||||
/// Rank within a category: U8/I8=1, U16/I16=2, U32/I32=3, U64/I64=4, F32=1, F64=2
|
||||
pub fn rank(&self) -> Option<u8> {
|
||||
match self {
|
||||
Ty::U8 | Ty::I8 => Some(1),
|
||||
Ty::U16 | Ty::I16 => Some(2),
|
||||
Ty::U32 | Ty::I32 => Some(3),
|
||||
Ty::U64 | Ty::I64 => Some(4),
|
||||
Ty::F32 => Some(1),
|
||||
Ty::F64 => Some(2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `self` implicitly promotes to `target` under §5.2 rules.
|
||||
pub fn promotes_to(&self, target: &Ty) -> bool {
|
||||
// Refl
|
||||
if self == target {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
// 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)
|
||||
}
|
||||
// Signed widening
|
||||
(a, b) if a.is_signed() && b.is_signed() => {
|
||||
a.rank().unwrap_or(0) < b.rank().unwrap_or(0)
|
||||
}
|
||||
// Float widening: F32 → F64
|
||||
(Ty::F32, Ty::F64) => true,
|
||||
// Char: char → U if U is unsigned and rank(U) >= 3 (u32 or u64)
|
||||
(Ty::Char, b) if b.is_unsigned() => b.rank().unwrap_or(0) >= 3,
|
||||
// Ptr-Coerce: *mut T promotes to *T (same pointee)
|
||||
(
|
||||
Ty::Ptr { mutable: true, pointee: pa },
|
||||
Ty::Ptr { mutable: false, pointee: pb },
|
||||
) => pa == pb,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Least upper bound under `promotes_to`.
|
||||
/// Returns `Some(T)` where T is the common type, or `None` if incompatible.
|
||||
pub fn common(a: &Ty, b: &Ty) -> Option<Ty> {
|
||||
if a.is_error() || b.is_error() {
|
||||
return Some(Ty::Error);
|
||||
}
|
||||
if b.promotes_to(a) {
|
||||
return Some(a.clone());
|
||||
}
|
||||
if a.promotes_to(b) {
|
||||
return Some(b.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// For pointer equality comparison.
|
||||
pub fn ptr_common(a: &Ty, b: &Ty) -> Option<Ty> {
|
||||
match (a, b) {
|
||||
(
|
||||
Ty::Ptr { mutable: ma, pointee: pa },
|
||||
Ty::Ptr { mutable: mb, pointee: pb },
|
||||
) if pa == pb => Some(Ty::Ptr {
|
||||
mutable: *ma && *mb,
|
||||
pointee: pa.clone(),
|
||||
}),
|
||||
(Ty::OpaquePtr { mutable: ma }, Ty::OpaquePtr { mutable: mb }) => {
|
||||
Some(Ty::OpaquePtr { mutable: *ma && *mb })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(&self) -> String {
|
||||
match self {
|
||||
Ty::U8 => "u8".to_string(),
|
||||
Ty::U16 => "u16".to_string(),
|
||||
Ty::U32 => "u32".to_string(),
|
||||
Ty::U64 => "u64".to_string(),
|
||||
Ty::I8 => "i8".to_string(),
|
||||
Ty::I16 => "i16".to_string(),
|
||||
Ty::I32 => "i32".to_string(),
|
||||
Ty::I64 => "i64".to_string(),
|
||||
Ty::F32 => "f32".to_string(),
|
||||
Ty::F64 => "f64".to_string(),
|
||||
Ty::Bool => "bool".to_string(),
|
||||
Ty::Char => "char".to_string(),
|
||||
Ty::Unit => "()".to_string(),
|
||||
Ty::Ptr { mutable: true, pointee } => format!("*mut {}", pointee.display()),
|
||||
Ty::Ptr { mutable: false, pointee } => format!("*{}", pointee.display()),
|
||||
Ty::OpaquePtr { mutable: true } => "*mut opaque".to_string(),
|
||||
Ty::OpaquePtr { mutable: false } => "*opaque".to_string(),
|
||||
Ty::Array { elem, size } => format!("[{}; {}]", elem.display(), size),
|
||||
Ty::Struct(name) => format!("struct {}", name),
|
||||
Ty::FnSig { params, ret } => {
|
||||
let ps: Vec<_> = params.iter().map(|p| p.display()).collect();
|
||||
format!("fn({}) -> {}", ps.join(", "), ret.display())
|
||||
}
|
||||
Ty::Error => "<error>".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Operators ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -54,7 +236,7 @@ pub enum BinaryOp {
|
||||
Assign, // `=`
|
||||
}
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
// ── Types (syntactic, from parser) ────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Type {
|
||||
@@ -74,6 +256,8 @@ pub enum Type {
|
||||
// Other primitives
|
||||
Bool,
|
||||
Char,
|
||||
// Unit type (explicit `-> ()`)
|
||||
Unit,
|
||||
// User-defined named type (e.g. a struct)
|
||||
Named(String, Span),
|
||||
// Typed pointer: `*type` (immutable) or `*mut type` (mutable)
|
||||
@@ -89,28 +273,29 @@ pub enum Type {
|
||||
// ── Struct literal field ───────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StructField {
|
||||
pub struct StructField<P: Phase> {
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
pub value: Expr,
|
||||
pub value: Expr<P>,
|
||||
}
|
||||
|
||||
// ── Expression ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Expr {
|
||||
pub kind: ExprKind,
|
||||
pub struct Expr<P: Phase> {
|
||||
pub kind: ExprKind<P>,
|
||||
pub span: Span,
|
||||
pub ty: P::ExprExtra,
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
pub fn new(kind: ExprKind, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
impl Expr<Parsed> {
|
||||
pub fn new(kind: ExprKind<Parsed>, span: Span) -> Self {
|
||||
Self { kind, span, ty: () }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExprKind {
|
||||
pub enum ExprKind<P: Phase> {
|
||||
// Literals
|
||||
IntLit(String),
|
||||
FloatLit(String),
|
||||
@@ -125,46 +310,46 @@ pub enum ExprKind {
|
||||
StructLit {
|
||||
name: String,
|
||||
name_span: Span,
|
||||
fields: Vec<StructField>,
|
||||
fields: Vec<StructField<P>>,
|
||||
},
|
||||
|
||||
// Operators
|
||||
Unary {
|
||||
op: UnaryOp,
|
||||
op_span: Span,
|
||||
expr: Box<Expr>,
|
||||
expr: Box<Expr<P>>,
|
||||
},
|
||||
Binary {
|
||||
op: BinaryOp,
|
||||
op_span: Span,
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
lhs: Box<Expr<P>>,
|
||||
rhs: Box<Expr<P>>,
|
||||
},
|
||||
// Compound assignment: `lhs op= rhs` (expands to `lhs = lhs op rhs`)
|
||||
CompoundAssign {
|
||||
op: CompoundAssignOp,
|
||||
op_span: Span,
|
||||
lhs: Box<Expr>,
|
||||
rhs: Box<Expr>,
|
||||
lhs: Box<Expr<P>>,
|
||||
rhs: Box<Expr<P>>,
|
||||
},
|
||||
|
||||
// Postfix
|
||||
Field {
|
||||
expr: Box<Expr>,
|
||||
expr: Box<Expr<P>>,
|
||||
field: String,
|
||||
field_span: Span,
|
||||
},
|
||||
Index {
|
||||
expr: Box<Expr>,
|
||||
index: Box<Expr>,
|
||||
expr: Box<Expr<P>>,
|
||||
index: Box<Expr<P>>,
|
||||
},
|
||||
Call {
|
||||
callee: Box<Expr>,
|
||||
args: Vec<Expr>,
|
||||
callee: Box<Expr<P>>,
|
||||
args: Vec<Expr<P>>,
|
||||
},
|
||||
|
||||
// Parenthesised expression
|
||||
Group(Box<Expr>),
|
||||
Group(Box<Expr<P>>),
|
||||
|
||||
// Placeholder for parse errors — allows parsing to continue
|
||||
Error,
|
||||
@@ -173,57 +358,57 @@ pub enum ExprKind {
|
||||
// ── Block ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Block {
|
||||
pub stmts: Vec<Stmt>,
|
||||
pub struct Block<P: Phase> {
|
||||
pub stmts: Vec<Stmt<P>>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
// ── Else branch ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ElseBranch {
|
||||
If(Box<Stmt>), // `else if …`
|
||||
Block(Block), // `else { … }`
|
||||
pub enum ElseBranch<P: Phase> {
|
||||
If(Box<Stmt<P>>), // `else if …`
|
||||
Block(Block<P>), // `else { … }`
|
||||
}
|
||||
|
||||
// ── Statement ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Stmt {
|
||||
pub kind: StmtKind,
|
||||
pub struct Stmt<P: Phase> {
|
||||
pub kind: StmtKind<P>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StmtKind {
|
||||
pub enum StmtKind<P: Phase> {
|
||||
/// `let [mut] name [: type] [= expr] ;`
|
||||
Let {
|
||||
mutable: bool,
|
||||
name: String,
|
||||
name_span: Span,
|
||||
ty: Option<Type>,
|
||||
init: Option<Expr>,
|
||||
init: Option<Expr<P>>,
|
||||
},
|
||||
/// `return [expr] ;`
|
||||
Return(Option<Expr>),
|
||||
Return(Option<Expr<P>>),
|
||||
/// `if expr_ns block [else else_branch]`
|
||||
If {
|
||||
cond: Expr,
|
||||
then_block: Block,
|
||||
else_branch: Option<ElseBranch>,
|
||||
cond: Expr<P>,
|
||||
then_block: Block<P>,
|
||||
else_branch: Option<ElseBranch<P>>,
|
||||
},
|
||||
/// `while expr_ns block`
|
||||
While { cond: Expr, body: Block },
|
||||
While { cond: Expr<P>, body: Block<P> },
|
||||
/// `loop block`
|
||||
Loop { body: Block },
|
||||
Loop { body: Block<P> },
|
||||
/// `break ;`
|
||||
Break,
|
||||
/// `continue ;`
|
||||
Continue,
|
||||
/// `{ stmts }`
|
||||
Block(Block),
|
||||
Block(Block<P>),
|
||||
/// `expr ;`
|
||||
Expr(Expr),
|
||||
Expr(Expr<P>),
|
||||
/// Error placeholder — emitted during recovery so the parent can continue.
|
||||
Error,
|
||||
}
|
||||
@@ -252,12 +437,12 @@ pub struct FieldDef {
|
||||
|
||||
/// `fn name ( params ) [ -> type ] block`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuncDef {
|
||||
pub struct FuncDef<P: Phase> {
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
pub params: Vec<Param>,
|
||||
pub ret_ty: Option<Type>,
|
||||
pub body: Block,
|
||||
pub body: Block<P>,
|
||||
}
|
||||
|
||||
/// `struct name { fields }`
|
||||
@@ -269,14 +454,14 @@ pub struct StructDef {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TopLevelDef {
|
||||
pub kind: TopLevelDefKind,
|
||||
pub struct TopLevelDef<P: Phase> {
|
||||
pub kind: TopLevelDefKind<P>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TopLevelDefKind {
|
||||
Func(FuncDef),
|
||||
pub enum TopLevelDefKind<P: Phase> {
|
||||
Func(FuncDef<P>),
|
||||
Struct(StructDef),
|
||||
/// Error placeholder for recovery.
|
||||
Error,
|
||||
@@ -284,7 +469,7 @@ pub enum TopLevelDefKind {
|
||||
|
||||
/// The root of the AST — a sequence of top-level definitions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Program {
|
||||
pub defs: Vec<TopLevelDef>,
|
||||
pub struct Program<P: Phase> {
|
||||
pub defs: Vec<TopLevelDef<P>>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
158
fluxc/src/checker/env.rs
Normal file
158
fluxc/src/checker/env.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use crate::ast::Ty;
|
||||
use crate::token::Span;
|
||||
|
||||
// ── StructTable ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct StructTable {
|
||||
entries: HashMap<String, StructEntry>,
|
||||
}
|
||||
|
||||
struct StructEntry {
|
||||
name_span: Span,
|
||||
fields: Vec<FieldEntry>,
|
||||
}
|
||||
|
||||
pub struct FieldEntry {
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
pub ty: Ty,
|
||||
pub ty_span: Span,
|
||||
}
|
||||
|
||||
impl StructTable {
|
||||
pub fn new() -> Self {
|
||||
Self { entries: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Insert a struct name; returns false if it was already present.
|
||||
pub fn insert_name(&mut self, name: &str, span: Span) -> bool {
|
||||
if self.entries.contains_key(name) {
|
||||
return false;
|
||||
}
|
||||
self.entries.insert(name.to_string(), StructEntry { name_span: span, fields: Vec::new() });
|
||||
true
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.entries.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn add_field(&mut self, struct_name: &str, field: FieldEntry) {
|
||||
if let Some(entry) = self.entries.get_mut(struct_name) {
|
||||
entry.fields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fields(&self, name: &str) -> Option<&[FieldEntry]> {
|
||||
self.entries.get(name).map(|e| e.fields.as_slice())
|
||||
}
|
||||
|
||||
pub fn name_span(&self, name: &str) -> Option<Span> {
|
||||
self.entries.get(name).map(|e| e.name_span)
|
||||
}
|
||||
|
||||
pub fn field_ty(&self, struct_name: &str, field_name: &str) -> Option<&Ty> {
|
||||
self.entries.get(struct_name)?.fields.iter().find(|f| f.name == field_name).map(|f| &f.ty)
|
||||
}
|
||||
|
||||
pub fn names_in_outer(&self, _saved: usize) -> HashSet<String> {
|
||||
self.entries.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn all_struct_names(&self) -> Vec<String> {
|
||||
self.entries.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn all_entries(&self) -> impl Iterator<Item = (&str, &StructEntry)> {
|
||||
self.entries.iter().map(|(k, v)| (k.as_str(), v))
|
||||
}
|
||||
}
|
||||
|
||||
// ── FuncTable ──────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct FuncTable {
|
||||
entries: HashMap<String, FuncEntry>,
|
||||
}
|
||||
|
||||
pub struct FuncEntry {
|
||||
pub name_span: Span,
|
||||
pub params: Vec<ParamEntry>,
|
||||
pub ret: Ty,
|
||||
}
|
||||
|
||||
pub struct ParamEntry {
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
pub ty: Ty,
|
||||
pub mutable: bool,
|
||||
}
|
||||
|
||||
impl FuncTable {
|
||||
pub fn new() -> Self {
|
||||
Self { entries: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Insert a function; returns false if the name was already present.
|
||||
pub fn insert(&mut self, name: &str, span: Span, params: Vec<ParamEntry>, ret: Ty) -> bool {
|
||||
if self.entries.contains_key(name) {
|
||||
return false;
|
||||
}
|
||||
self.entries.insert(name.to_string(), FuncEntry { name_span: span, params, ret });
|
||||
true
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&FuncEntry> {
|
||||
self.entries.get(name)
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.entries.contains_key(name)
|
||||
}
|
||||
}
|
||||
|
||||
// ── TypeEnv ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TypeEnv {
|
||||
bindings: Vec<Binding>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Binding {
|
||||
pub name: String,
|
||||
pub ty: Ty,
|
||||
pub mutable: bool,
|
||||
}
|
||||
|
||||
impl TypeEnv {
|
||||
pub fn new() -> Self {
|
||||
Self { bindings: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, name: String, ty: Ty, mutable: bool) {
|
||||
self.bindings.push(Binding { name, ty, mutable });
|
||||
}
|
||||
|
||||
/// Look up a binding; rightmost (most recent) binding wins.
|
||||
pub fn lookup(&self, name: &str) -> Option<(&Ty, bool)> {
|
||||
self.bindings.iter().rev().find(|b| b.name == name).map(|b| (&b.ty, b.mutable))
|
||||
}
|
||||
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.bindings.iter().any(|b| b.name == name)
|
||||
}
|
||||
|
||||
pub fn save(&self) -> usize {
|
||||
self.bindings.len()
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, saved: usize) {
|
||||
self.bindings.truncate(saved);
|
||||
}
|
||||
|
||||
/// Returns all binding names that were introduced after `saved`.
|
||||
pub fn names_in_outer(&self, saved: usize) -> HashSet<String> {
|
||||
self.bindings[saved..].iter().map(|b| b.name.clone()).collect()
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
276
fluxc/src/checker/mod.rs
Normal file
276
fluxc/src/checker/mod.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
pub mod env;
|
||||
pub mod expr;
|
||||
pub mod stmt;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::ast::{self, Parsed, Ty, Type};
|
||||
use crate::diagnostics::{Diagnostic, Label};
|
||||
use crate::token::Span;
|
||||
use env::{FieldEntry, FuncTable, ParamEntry, StructTable};
|
||||
|
||||
// ── Checker ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Checker {
|
||||
pub sigma: StructTable,
|
||||
pub phi: FuncTable,
|
||||
pub errors: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl Checker {
|
||||
fn new() -> Self {
|
||||
let mut sigma = StructTable::new();
|
||||
let phi = FuncTable::new();
|
||||
|
||||
// Pre-load `string_view` built-in (§3.1): { data: *char, size: u64 }
|
||||
sigma.insert_name("string_view", Span::new(0, 0));
|
||||
sigma.add_field(
|
||||
"string_view",
|
||||
FieldEntry {
|
||||
name: "data".to_string(),
|
||||
name_span: Span::new(0, 0),
|
||||
ty: Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) },
|
||||
ty_span: Span::new(0, 0),
|
||||
},
|
||||
);
|
||||
sigma.add_field(
|
||||
"string_view",
|
||||
FieldEntry {
|
||||
name: "size".to_string(),
|
||||
name_span: Span::new(0, 0),
|
||||
ty: Ty::U64,
|
||||
ty_span: Span::new(0, 0),
|
||||
},
|
||||
);
|
||||
|
||||
Self { sigma, phi, errors: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, diag: Diagnostic) {
|
||||
self.errors.push(diag);
|
||||
}
|
||||
|
||||
/// Resolve a syntactic `ast::Type` to a semantic `Ty`.
|
||||
pub fn resolve_type(&mut self, ty: &Type, span: Span) -> Ty {
|
||||
match ty {
|
||||
Type::U8 => Ty::U8,
|
||||
Type::U16 => Ty::U16,
|
||||
Type::U32 => Ty::U32,
|
||||
Type::U64 => Ty::U64,
|
||||
Type::I8 => Ty::I8,
|
||||
Type::I16 => Ty::I16,
|
||||
Type::I32 => Ty::I32,
|
||||
Type::I64 => Ty::I64,
|
||||
Type::F32 => Ty::F32,
|
||||
Type::F64 => Ty::F64,
|
||||
Type::Bool => Ty::Bool,
|
||||
Type::Char => Ty::Char,
|
||||
Type::Unit => Ty::Unit,
|
||||
Type::Named(name, name_span) => {
|
||||
if self.sigma.contains(name) {
|
||||
Ty::Struct(name.clone())
|
||||
} else {
|
||||
self.emit(
|
||||
Diagnostic::error(format!("undefined type `{name}`"))
|
||||
.with_label(Label::primary(*name_span)),
|
||||
);
|
||||
Ty::Error
|
||||
}
|
||||
}
|
||||
Type::Pointer { mutable, pointee } => {
|
||||
let inner = self.resolve_type(pointee, span);
|
||||
Ty::Ptr { mutable: *mutable, pointee: Box::new(inner) }
|
||||
}
|
||||
Type::OpaquePointer { mutable } => Ty::OpaquePtr { mutable: *mutable },
|
||||
Type::Array { elem, size } => {
|
||||
let elem_ty = self.resolve_type(elem, span);
|
||||
match size.parse::<u64>() {
|
||||
Ok(n) => Ty::Array { elem: Box::new(elem_ty), size: n },
|
||||
Err(_) => {
|
||||
self.emit(
|
||||
Diagnostic::error(format!("invalid array size `{size}`"))
|
||||
.with_label(Label::primary(span)),
|
||||
);
|
||||
Ty::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::Error => Ty::Error,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Size-cycle detection ───────────────────────────────────────────────────
|
||||
|
||||
fn has_size_cycle(
|
||||
&self,
|
||||
name: &str,
|
||||
gray: &mut HashSet<String>,
|
||||
black: &mut HashSet<String>,
|
||||
) -> bool {
|
||||
if black.contains(name) {
|
||||
return false;
|
||||
}
|
||||
if gray.contains(name) {
|
||||
return true;
|
||||
}
|
||||
gray.insert(name.to_string());
|
||||
|
||||
// Collect field types to avoid holding an immutable borrow on self.sigma
|
||||
// while recursing (which needs immutable access again).
|
||||
let field_tys: Vec<Ty> = self
|
||||
.sigma
|
||||
.fields(name)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|f| f.ty.clone())
|
||||
.collect();
|
||||
|
||||
for ty in &field_tys {
|
||||
if let Some(inner) = value_struct_name(ty) {
|
||||
if self.has_size_cycle(inner, gray, black) {
|
||||
gray.remove(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gray.remove(name);
|
||||
black.insert(name.to_string());
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the struct name embedded by-value in `ty` (if any).
|
||||
/// Pointer-to-struct is NOT by-value, so it does not cause a size cycle.
|
||||
fn value_struct_name(ty: &Ty) -> Option<&str> {
|
||||
match ty {
|
||||
Ty::Struct(name) => Some(name.as_str()),
|
||||
Ty::Array { elem, .. } => value_struct_name(elem),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn check(program: &ast::Program<Parsed>) -> Vec<Diagnostic> {
|
||||
let mut checker = Checker::new();
|
||||
|
||||
// ── Pass 1: collect struct names + function signatures ────────────────────
|
||||
for def in &program.defs {
|
||||
match &def.kind {
|
||||
ast::TopLevelDefKind::Struct(s) => {
|
||||
if s.name == "string_view" {
|
||||
checker.emit(
|
||||
Diagnostic::error("`string_view` is a reserved built-in name")
|
||||
.with_label(Label::primary(s.name_span)),
|
||||
);
|
||||
} else if !checker.sigma.insert_name(&s.name, s.name_span) {
|
||||
checker.emit(
|
||||
Diagnostic::error(format!("duplicate struct `{}`", s.name))
|
||||
.with_label(Label::primary(s.name_span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::TopLevelDefKind::Func(f) => {
|
||||
let params: Vec<ParamEntry> = f
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let ty = checker.resolve_type(&p.ty, p.name_span);
|
||||
ParamEntry {
|
||||
name: p.name.clone(),
|
||||
name_span: p.name_span,
|
||||
ty,
|
||||
mutable: p.mutable,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let ret = match &f.ret_ty {
|
||||
Some(t) => checker.resolve_type(t, f.name_span),
|
||||
None => Ty::Unit,
|
||||
};
|
||||
if !checker.phi.insert(&f.name, f.name_span, params, ret) {
|
||||
checker.emit(
|
||||
Diagnostic::error(format!("duplicate function `{}`", f.name))
|
||||
.with_label(Label::primary(f.name_span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::TopLevelDefKind::Error => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: resolve struct field types + check for size cycles ────────────
|
||||
for def in &program.defs {
|
||||
if let ast::TopLevelDefKind::Struct(s) = &def.kind {
|
||||
if s.name == "string_view" {
|
||||
continue; // built-in, already populated
|
||||
}
|
||||
for field in &s.fields {
|
||||
let ty = checker.resolve_type(&field.ty, field.name_span);
|
||||
checker.sigma.add_field(
|
||||
&s.name,
|
||||
FieldEntry {
|
||||
name: field.name.clone(),
|
||||
name_span: field.name_span,
|
||||
ty,
|
||||
ty_span: field.name_span,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let struct_names = checker.sigma.all_struct_names();
|
||||
let mut black = HashSet::new();
|
||||
for name in struct_names {
|
||||
let mut gray = HashSet::new();
|
||||
if checker.has_size_cycle(&name, &mut gray, &mut black) {
|
||||
if let Some(span) = checker.sigma.name_span(&name) {
|
||||
checker.emit(
|
||||
Diagnostic::error(format!(
|
||||
"struct `{name}` has infinite size (recursive by value)"
|
||||
))
|
||||
.with_label(Label::primary(span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 3: check function bodies ─────────────────────────────────────────
|
||||
for def in &program.defs {
|
||||
if let ast::TopLevelDefKind::Func(f) = &def.kind {
|
||||
checker.check_function(f);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 4: verify entry point ────────────────────────────────────────────
|
||||
let main_info = checker
|
||||
.phi
|
||||
.get("main")
|
||||
.map(|e| (e.name_span, e.params.len(), e.ret.clone()));
|
||||
match main_info {
|
||||
None => {
|
||||
checker.emit(
|
||||
Diagnostic::error("program has no `main` function")
|
||||
.with_label(Label::primary(program.span)),
|
||||
);
|
||||
}
|
||||
Some((name_span, param_count, ret)) => {
|
||||
if param_count != 0 {
|
||||
checker.emit(
|
||||
Diagnostic::error("`main` must take no parameters")
|
||||
.with_label(Label::primary(name_span)),
|
||||
);
|
||||
}
|
||||
if ret != Ty::Unit && ret != Ty::I32 && !ret.is_error() {
|
||||
checker.emit(
|
||||
Diagnostic::error("`main` must return `()` or `i32`")
|
||||
.with_label(Label::primary(name_span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker.errors
|
||||
}
|
||||
365
fluxc/src/checker/stmt.rs
Normal file
365
fluxc/src/checker/stmt.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty};
|
||||
use crate::diagnostics::{Diagnostic, Label};
|
||||
use super::env::TypeEnv;
|
||||
use super::Checker;
|
||||
|
||||
// ── Control flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Cf {
|
||||
Normal,
|
||||
Diverges,
|
||||
}
|
||||
|
||||
// ── Check context ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct CheckCtx {
|
||||
pub ret_ty: Ty,
|
||||
pub in_loop: bool,
|
||||
}
|
||||
|
||||
impl CheckCtx {
|
||||
fn loop_ctx(&self) -> CheckCtx {
|
||||
CheckCtx { ret_ty: self.ret_ty.clone(), in_loop: true }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Function entry ────────────────────────────────────────────────────────────
|
||||
|
||||
impl Checker {
|
||||
pub fn check_function(&mut self, f: &ast::FuncDef<Parsed>) {
|
||||
let (params_info, ret_ty) = {
|
||||
let entry = self.phi.get(&f.name).expect("function must be in phi after pass 1");
|
||||
let params: Vec<(String, Ty, bool)> = entry
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| (p.name.clone(), p.ty.clone(), p.mutable))
|
||||
.collect();
|
||||
(params, entry.ret.clone())
|
||||
};
|
||||
|
||||
let mut env = TypeEnv::new();
|
||||
let mut assigned: HashSet<String> = HashSet::new();
|
||||
|
||||
for (name, ty, mutable) in params_info {
|
||||
env.extend(name.clone(), ty, mutable);
|
||||
assigned.insert(name);
|
||||
}
|
||||
|
||||
let ctx = CheckCtx { ret_ty: ret_ty.clone(), in_loop: false };
|
||||
let cf = self.check_block(&f.body, &mut env, &mut assigned, &ctx);
|
||||
|
||||
// If the declared return type is non-unit and the body may not diverge,
|
||||
// emit a missing-return error.
|
||||
if ret_ty != Ty::Unit && !ret_ty.is_error() && cf != Cf::Diverges {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"function `{}` must always return a value of type `{}`",
|
||||
f.name,
|
||||
ret_ty.display()
|
||||
))
|
||||
.with_label(Label::primary(f.body.span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Block ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn check_block(
|
||||
&mut self,
|
||||
block: &ast::Block<Parsed>,
|
||||
env: &mut TypeEnv,
|
||||
assigned: &mut HashSet<String>,
|
||||
ctx: &CheckCtx,
|
||||
) -> Cf {
|
||||
let saved = env.save();
|
||||
let mut cf = Cf::Normal;
|
||||
|
||||
for stmt in &block.stmts {
|
||||
if cf == Cf::Diverges {
|
||||
self.emit(
|
||||
Diagnostic::warning("unreachable statement")
|
||||
.with_label(Label::primary(stmt.span)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
cf = self.check_stmt(stmt, env, assigned, ctx);
|
||||
}
|
||||
|
||||
// Remove block-local variables from assigned (they leave scope).
|
||||
for var in env.names_in_outer(saved) {
|
||||
assigned.remove(&var);
|
||||
}
|
||||
env.restore(saved);
|
||||
cf
|
||||
}
|
||||
|
||||
// ── Statement ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_stmt(
|
||||
&mut self,
|
||||
stmt: &ast::Stmt<Parsed>,
|
||||
env: &mut TypeEnv,
|
||||
assigned: &mut HashSet<String>,
|
||||
ctx: &CheckCtx,
|
||||
) -> Cf {
|
||||
match &stmt.kind {
|
||||
// T-Let
|
||||
ast::StmtKind::Let { mutable, name, name_span, ty, init } => {
|
||||
let ann_ty = ty.as_ref().map(|t| self.resolve_type(t, *name_span));
|
||||
|
||||
let init_ty = init.as_ref().map(|e| {
|
||||
let t = self.check_expr(e, env, assigned);
|
||||
// Propagate inner assignments (e.g. `let x = (y = 5);`)
|
||||
for var in collect_assigns(e) {
|
||||
assigned.insert(var);
|
||||
}
|
||||
t
|
||||
});
|
||||
|
||||
let init_span = init.as_ref().map(|e| e.span).unwrap_or(*name_span);
|
||||
let resolved = match (ann_ty, init_ty) {
|
||||
(Some(ann), Some(ref init_t)) => {
|
||||
if !init_t.is_error() && !ann.is_error() && !init_t.promotes_to(&ann) {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"type mismatch in `let`: expected `{}`, got `{}`",
|
||||
ann.display(),
|
||||
init_t.display()
|
||||
))
|
||||
.with_label(Label::primary(init_span)),
|
||||
);
|
||||
}
|
||||
ann
|
||||
}
|
||||
(Some(ann), None) => ann,
|
||||
(None, Some(init)) => init,
|
||||
(None, None) => {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"cannot infer type of `{name}`: add a type annotation or initialiser"
|
||||
))
|
||||
.with_label(Label::primary(*name_span)),
|
||||
);
|
||||
Ty::Error
|
||||
}
|
||||
};
|
||||
|
||||
env.extend(name.clone(), resolved, *mutable);
|
||||
if init.is_some() {
|
||||
assigned.insert(name.clone());
|
||||
}
|
||||
Cf::Normal
|
||||
}
|
||||
|
||||
// T-Return
|
||||
ast::StmtKind::Return(expr) => {
|
||||
match expr {
|
||||
Some(e) => {
|
||||
let ty = self.check_expr(e, env, assigned);
|
||||
if ctx.ret_ty == Ty::Unit {
|
||||
if !ty.is_error() && ty != Ty::Unit {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"this function returns `()`, cannot return `{}`",
|
||||
ty.display()
|
||||
))
|
||||
.with_label(Label::primary(e.span)),
|
||||
);
|
||||
}
|
||||
} else if !ty.is_error()
|
||||
&& !ctx.ret_ty.is_error()
|
||||
&& !ty.promotes_to(&ctx.ret_ty)
|
||||
{
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"type mismatch: expected `{}`, got `{}`",
|
||||
ctx.ret_ty.display(),
|
||||
ty.display()
|
||||
))
|
||||
.with_label(Label::primary(e.span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if ctx.ret_ty != Ty::Unit && !ctx.ret_ty.is_error() {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"missing return value: function must return `{}`",
|
||||
ctx.ret_ty.display()
|
||||
))
|
||||
.with_label(Label::primary(stmt.span)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Cf::Diverges
|
||||
}
|
||||
|
||||
// T-If
|
||||
ast::StmtKind::If { cond, then_block, else_branch } => {
|
||||
let cond_ty = self.check_expr(cond, env, assigned);
|
||||
if !cond_ty.is_error() && cond_ty != Ty::Bool {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"condition must be `bool`, got `{}`",
|
||||
cond_ty.display()
|
||||
))
|
||||
.with_label(Label::primary(cond.span)),
|
||||
);
|
||||
}
|
||||
// Propagate assigns from condition expression
|
||||
for var in collect_assigns(cond) {
|
||||
assigned.insert(var);
|
||||
}
|
||||
|
||||
let mut a_then = assigned.clone();
|
||||
let cf_then = self.check_block(then_block, env, &mut a_then, ctx);
|
||||
|
||||
let cf_else = match else_branch {
|
||||
Some(ElseBranch::Block(b)) => {
|
||||
let mut a_else = assigned.clone();
|
||||
let cf = self.check_block(b, env, &mut a_else, ctx);
|
||||
// A_out = A_then ∩ A_else (both branches definitely assign)
|
||||
*assigned = a_then.intersection(&a_else).cloned().collect();
|
||||
cf
|
||||
}
|
||||
Some(ElseBranch::If(if_stmt)) => {
|
||||
let mut a_else = assigned.clone();
|
||||
let cf = self.check_stmt(if_stmt, env, &mut a_else, ctx);
|
||||
*assigned = a_then.intersection(&a_else).cloned().collect();
|
||||
cf
|
||||
}
|
||||
None => {
|
||||
// No else: no new definite assignments (branch might not run)
|
||||
Cf::Normal
|
||||
}
|
||||
};
|
||||
|
||||
if cf_then == Cf::Diverges && cf_else == Cf::Diverges {
|
||||
Cf::Diverges
|
||||
} else {
|
||||
Cf::Normal
|
||||
}
|
||||
}
|
||||
|
||||
// T-While
|
||||
ast::StmtKind::While { cond, body } => {
|
||||
let cond_ty = self.check_expr(cond, env, assigned);
|
||||
if !cond_ty.is_error() && cond_ty != Ty::Bool {
|
||||
self.emit(
|
||||
Diagnostic::error(format!(
|
||||
"while condition must be `bool`, got `{}`",
|
||||
cond_ty.display()
|
||||
))
|
||||
.with_label(Label::primary(cond.span)),
|
||||
);
|
||||
}
|
||||
for var in collect_assigns(cond) {
|
||||
assigned.insert(var);
|
||||
}
|
||||
let loop_ctx = ctx.loop_ctx();
|
||||
let mut a_body = assigned.clone();
|
||||
self.check_block(body, env, &mut a_body, &loop_ctx);
|
||||
// Conservatively Normal: condition might be false on first iteration
|
||||
Cf::Normal
|
||||
}
|
||||
|
||||
// T-Loop
|
||||
ast::StmtKind::Loop { body } => {
|
||||
let loop_ctx = ctx.loop_ctx();
|
||||
let mut a_body = assigned.clone();
|
||||
self.check_block(body, env, &mut a_body, &loop_ctx);
|
||||
// Conservatively Normal: might break
|
||||
Cf::Normal
|
||||
}
|
||||
|
||||
ast::StmtKind::Break => {
|
||||
if !ctx.in_loop {
|
||||
self.emit(
|
||||
Diagnostic::error("`break` outside of a loop")
|
||||
.with_label(Label::primary(stmt.span)),
|
||||
);
|
||||
}
|
||||
Cf::Diverges
|
||||
}
|
||||
|
||||
ast::StmtKind::Continue => {
|
||||
if !ctx.in_loop {
|
||||
self.emit(
|
||||
Diagnostic::error("`continue` outside of a loop")
|
||||
.with_label(Label::primary(stmt.span)),
|
||||
);
|
||||
}
|
||||
Cf::Diverges
|
||||
}
|
||||
|
||||
ast::StmtKind::Block(block) => self.check_block(block, env, assigned, ctx),
|
||||
|
||||
// T-ExprStmt
|
||||
ast::StmtKind::Expr(expr) => {
|
||||
self.check_expr(expr, env, assigned);
|
||||
for var in collect_assigns(expr) {
|
||||
assigned.insert(var);
|
||||
}
|
||||
Cf::Normal
|
||||
}
|
||||
|
||||
ast::StmtKind::Error => Cf::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── assigns(e) — §8.5 ────────────────────────────────────────────────────────
|
||||
|
||||
/// Collect the set of variable names that are directly (re-)assigned as a
|
||||
/// side-effect of evaluating `expr`. Only simple `ident = …` assignments
|
||||
/// contribute; compound places (`s.f = …`) are excluded.
|
||||
pub fn collect_assigns(expr: &ast::Expr<Parsed>) -> HashSet<String> {
|
||||
let mut set = HashSet::new();
|
||||
collect_assigns_inner(expr, &mut set);
|
||||
set
|
||||
}
|
||||
|
||||
fn collect_assigns_inner(expr: &ast::Expr<Parsed>, set: &mut HashSet<String>) {
|
||||
match &expr.kind {
|
||||
ExprKind::Binary { op, lhs, rhs, .. } => {
|
||||
if *op == BinaryOp::Assign {
|
||||
collect_assigns_inner(rhs, set);
|
||||
if let ExprKind::Ident(name) = &lhs.kind {
|
||||
set.insert(name.clone());
|
||||
}
|
||||
} else {
|
||||
collect_assigns_inner(lhs, set);
|
||||
collect_assigns_inner(rhs, set);
|
||||
}
|
||||
}
|
||||
ExprKind::CompoundAssign { rhs, .. } => {
|
||||
// Compound assign requires lhs to already be assigned (read side),
|
||||
// so we only propagate from rhs.
|
||||
collect_assigns_inner(rhs, set);
|
||||
}
|
||||
ExprKind::StructLit { fields, .. } => {
|
||||
for f in fields {
|
||||
collect_assigns_inner(&f.value, set);
|
||||
}
|
||||
}
|
||||
ExprKind::Group(inner) => collect_assigns_inner(inner, set),
|
||||
ExprKind::Unary { expr, .. } => collect_assigns_inner(expr, set),
|
||||
ExprKind::Field { expr, .. } => collect_assigns_inner(expr, set),
|
||||
ExprKind::Index { expr, index } => {
|
||||
collect_assigns_inner(expr, set);
|
||||
collect_assigns_inner(index, set);
|
||||
}
|
||||
ExprKind::Call { callee, args } => {
|
||||
collect_assigns_inner(callee, set);
|
||||
for a in args {
|
||||
collect_assigns_inner(a, set);
|
||||
}
|
||||
}
|
||||
// Leaves (literals, ident, bool, error) don't assign anything
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use std::{fs, process};
|
||||
use crate::parser::Parser;
|
||||
|
||||
pub mod ast;
|
||||
pub mod checker;
|
||||
pub mod cli;
|
||||
pub mod diagnostics;
|
||||
pub mod lexer;
|
||||
@@ -24,7 +25,13 @@ fn main() {
|
||||
had_errors = true;
|
||||
}
|
||||
|
||||
println!("{program:#?}");
|
||||
if parser.errors.is_empty() {
|
||||
let sema_errors = checker::check(&program);
|
||||
for diag in &sema_errors {
|
||||
eprint!("{}", diag.render(&content, path));
|
||||
had_errors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if had_errors {
|
||||
|
||||
Reference in New Issue
Block a user