Compare commits
6 Commits
eda4f92900
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f836f279de | |||
| c2fc83b74b | |||
| 9aa6b9694b | |||
| bb5e9e42d9 | |||
| 1f3d64f97c | |||
| aca0dae7de |
323
fluxc/src/ast.rs
323
fluxc/src/ast.rs
@@ -1,5 +1,233 @@
|
|||||||
use crate::token::Span;
|
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>,
|
||||||
|
},
|
||||||
|
/// Unresolved integer type from a literal or an unannotated let-binding.
|
||||||
|
/// Compatible with every concrete integer type; defaults to `i32` in
|
||||||
|
/// error messages when no concrete type can be inferred.
|
||||||
|
UnboundInt,
|
||||||
|
// Error propagation sentinel
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
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() || matches!(self, Ty::UnboundInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// UnboundInt (from integer literal / unannotated let) promotes to
|
||||||
|
// any concrete integer type, and any concrete integer promotes to
|
||||||
|
// UnboundInt so it can receive a typed value.
|
||||||
|
(Ty::UnboundInt, t) if t.is_integer() => true,
|
||||||
|
(t, Ty::UnboundInt) if t.is_integer() => true,
|
||||||
|
// Unsigned widening: same unsigned category, rank strictly increases
|
||||||
|
(a, b) if a.is_unsigned() && b.is_unsigned() => {
|
||||||
|
a.rank().unwrap_or(0) < b.rank().unwrap_or(0)
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
// UnboundInt is resolved by the concrete type; concrete type wins.
|
||||||
|
// common(UnboundInt, UnboundInt) = UnboundInt.
|
||||||
|
match (a, b) {
|
||||||
|
(Ty::UnboundInt, _) if b.is_integer() => return Some(b.clone()),
|
||||||
|
(_, Ty::UnboundInt) if a.is_integer() => return Some(a.clone()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if b.promotes_to(a) {
|
||||||
|
return Some(a.clone());
|
||||||
|
}
|
||||||
|
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::UnboundInt => "{integer}".to_string(),
|
||||||
|
Ty::Error => "<error>".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Operators ──────────────────────────────────────────────────────────────────
|
// ── Operators ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -54,7 +282,7 @@ pub enum BinaryOp {
|
|||||||
Assign, // `=`
|
Assign, // `=`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
// ── Types (syntactic, from parser) ────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Type {
|
pub enum Type {
|
||||||
@@ -74,6 +302,8 @@ pub enum Type {
|
|||||||
// Other primitives
|
// Other primitives
|
||||||
Bool,
|
Bool,
|
||||||
Char,
|
Char,
|
||||||
|
// Unit type (explicit `-> ()`)
|
||||||
|
Unit,
|
||||||
// User-defined named type (e.g. a struct)
|
// User-defined named type (e.g. a struct)
|
||||||
Named(String, Span),
|
Named(String, Span),
|
||||||
// Typed pointer: `*type` (immutable) or `*mut type` (mutable)
|
// Typed pointer: `*type` (immutable) or `*mut type` (mutable)
|
||||||
@@ -89,28 +319,29 @@ pub enum Type {
|
|||||||
// ── Struct literal field ───────────────────────────────────────────────────────
|
// ── Struct literal field ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StructField {
|
pub struct StructField<P: Phase> {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub name_span: Span,
|
pub name_span: Span,
|
||||||
pub value: Expr,
|
pub value: Expr<P>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Expression ────────────────────────────────────────────────────────────────
|
// ── Expression ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Expr {
|
pub struct Expr<P: Phase> {
|
||||||
pub kind: ExprKind,
|
pub kind: ExprKind<P>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
|
pub ty: P::ExprExtra,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Expr {
|
impl Expr<Parsed> {
|
||||||
pub fn new(kind: ExprKind, span: Span) -> Self {
|
pub fn new(kind: ExprKind<Parsed>, span: Span) -> Self {
|
||||||
Self { kind, span }
|
Self { kind, span, ty: () }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ExprKind {
|
pub enum ExprKind<P: Phase> {
|
||||||
// Literals
|
// Literals
|
||||||
IntLit(String),
|
IntLit(String),
|
||||||
FloatLit(String),
|
FloatLit(String),
|
||||||
@@ -125,46 +356,46 @@ pub enum ExprKind {
|
|||||||
StructLit {
|
StructLit {
|
||||||
name: String,
|
name: String,
|
||||||
name_span: Span,
|
name_span: Span,
|
||||||
fields: Vec<StructField>,
|
fields: Vec<StructField<P>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Operators
|
// Operators
|
||||||
Unary {
|
Unary {
|
||||||
op: UnaryOp,
|
op: UnaryOp,
|
||||||
op_span: Span,
|
op_span: Span,
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr<P>>,
|
||||||
},
|
},
|
||||||
Binary {
|
Binary {
|
||||||
op: BinaryOp,
|
op: BinaryOp,
|
||||||
op_span: Span,
|
op_span: Span,
|
||||||
lhs: Box<Expr>,
|
lhs: Box<Expr<P>>,
|
||||||
rhs: Box<Expr>,
|
rhs: Box<Expr<P>>,
|
||||||
},
|
},
|
||||||
// Compound assignment: `lhs op= rhs` (expands to `lhs = lhs op rhs`)
|
// Compound assignment: `lhs op= rhs` (expands to `lhs = lhs op rhs`)
|
||||||
CompoundAssign {
|
CompoundAssign {
|
||||||
op: CompoundAssignOp,
|
op: CompoundAssignOp,
|
||||||
op_span: Span,
|
op_span: Span,
|
||||||
lhs: Box<Expr>,
|
lhs: Box<Expr<P>>,
|
||||||
rhs: Box<Expr>,
|
rhs: Box<Expr<P>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Postfix
|
// Postfix
|
||||||
Field {
|
Field {
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr<P>>,
|
||||||
field: String,
|
field: String,
|
||||||
field_span: Span,
|
field_span: Span,
|
||||||
},
|
},
|
||||||
Index {
|
Index {
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr<P>>,
|
||||||
index: Box<Expr>,
|
index: Box<Expr<P>>,
|
||||||
},
|
},
|
||||||
Call {
|
Call {
|
||||||
callee: Box<Expr>,
|
callee: Box<Expr<P>>,
|
||||||
args: Vec<Expr>,
|
args: Vec<Expr<P>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Parenthesised expression
|
// Parenthesised expression
|
||||||
Group(Box<Expr>),
|
Group(Box<Expr<P>>),
|
||||||
|
|
||||||
// Placeholder for parse errors — allows parsing to continue
|
// Placeholder for parse errors — allows parsing to continue
|
||||||
Error,
|
Error,
|
||||||
@@ -173,57 +404,57 @@ pub enum ExprKind {
|
|||||||
// ── Block ──────────────────────────────────────────────────────────────────────
|
// ── Block ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Block {
|
pub struct Block<P: Phase> {
|
||||||
pub stmts: Vec<Stmt>,
|
pub stmts: Vec<Stmt<P>>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Else branch ───────────────────────────────────────────────────────────────
|
// ── Else branch ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ElseBranch {
|
pub enum ElseBranch<P: Phase> {
|
||||||
If(Box<Stmt>), // `else if …`
|
If(Box<Stmt<P>>), // `else if …`
|
||||||
Block(Block), // `else { … }`
|
Block(Block<P>), // `else { … }`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Statement ─────────────────────────────────────────────────────────────────
|
// ── Statement ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Stmt {
|
pub struct Stmt<P: Phase> {
|
||||||
pub kind: StmtKind,
|
pub kind: StmtKind<P>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum StmtKind {
|
pub enum StmtKind<P: Phase> {
|
||||||
/// `let [mut] name [: type] [= expr] ;`
|
/// `let [mut] name [: type] [= expr] ;`
|
||||||
Let {
|
Let {
|
||||||
mutable: bool,
|
mutable: bool,
|
||||||
name: String,
|
name: String,
|
||||||
name_span: Span,
|
name_span: Span,
|
||||||
ty: Option<Type>,
|
ty: Option<Type>,
|
||||||
init: Option<Expr>,
|
init: Option<Expr<P>>,
|
||||||
},
|
},
|
||||||
/// `return [expr] ;`
|
/// `return [expr] ;`
|
||||||
Return(Option<Expr>),
|
Return(Option<Expr<P>>),
|
||||||
/// `if expr_ns block [else else_branch]`
|
/// `if expr_ns block [else else_branch]`
|
||||||
If {
|
If {
|
||||||
cond: Expr,
|
cond: Expr<P>,
|
||||||
then_block: Block,
|
then_block: Block<P>,
|
||||||
else_branch: Option<ElseBranch>,
|
else_branch: Option<ElseBranch<P>>,
|
||||||
},
|
},
|
||||||
/// `while expr_ns block`
|
/// `while expr_ns block`
|
||||||
While { cond: Expr, body: Block },
|
While { cond: Expr<P>, body: Block<P> },
|
||||||
/// `loop block`
|
/// `loop block`
|
||||||
Loop { body: Block },
|
Loop { body: Block<P> },
|
||||||
/// `break ;`
|
/// `break ;`
|
||||||
Break,
|
Break,
|
||||||
/// `continue ;`
|
/// `continue ;`
|
||||||
Continue,
|
Continue,
|
||||||
/// `{ stmts }`
|
/// `{ stmts }`
|
||||||
Block(Block),
|
Block(Block<P>),
|
||||||
/// `expr ;`
|
/// `expr ;`
|
||||||
Expr(Expr),
|
Expr(Expr<P>),
|
||||||
/// Error placeholder — emitted during recovery so the parent can continue.
|
/// Error placeholder — emitted during recovery so the parent can continue.
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
@@ -252,12 +483,12 @@ pub struct FieldDef {
|
|||||||
|
|
||||||
/// `fn name ( params ) [ -> type ] block`
|
/// `fn name ( params ) [ -> type ] block`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FuncDef {
|
pub struct FuncDef<P: Phase> {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub name_span: Span,
|
pub name_span: Span,
|
||||||
pub params: Vec<Param>,
|
pub params: Vec<Param>,
|
||||||
pub ret_ty: Option<Type>,
|
pub ret_ty: Option<Type>,
|
||||||
pub body: Block,
|
pub body: Block<P>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `struct name { fields }`
|
/// `struct name { fields }`
|
||||||
@@ -269,14 +500,14 @@ pub struct StructDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TopLevelDef {
|
pub struct TopLevelDef<P: Phase> {
|
||||||
pub kind: TopLevelDefKind,
|
pub kind: TopLevelDefKind<P>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TopLevelDefKind {
|
pub enum TopLevelDefKind<P: Phase> {
|
||||||
Func(FuncDef),
|
Func(FuncDef<P>),
|
||||||
Struct(StructDef),
|
Struct(StructDef),
|
||||||
/// Error placeholder for recovery.
|
/// Error placeholder for recovery.
|
||||||
Error,
|
Error,
|
||||||
@@ -284,7 +515,7 @@ pub enum TopLevelDefKind {
|
|||||||
|
|
||||||
/// The root of the AST — a sequence of top-level definitions.
|
/// The root of the AST — a sequence of top-level definitions.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Program {
|
pub struct Program<P: Phase> {
|
||||||
pub defs: Vec<TopLevelDef>,
|
pub defs: Vec<TopLevelDef<P>>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|||||||
189
fluxc/src/checker/env.rs
Normal file
189
fluxc/src/checker/env.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
use crate::ast::Ty;
|
||||||
|
use crate::token::Span;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
// ── StructTable ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct StructTable {
|
||||||
|
entries: HashMap<String, StructEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
749
fluxc/src/checker/expr.rs
Normal file
749
fluxc/src/checker/expr.rs
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
307
fluxc/src/checker/mod.rs
Normal file
307
fluxc/src/checker/mod.rs
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
pub mod env;
|
||||||
|
pub mod expr;
|
||||||
|
pub mod stmt;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
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};
|
||||||
|
|
||||||
|
// ── Check result ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The result of running the semantic checker. Carries both the diagnostics and
|
||||||
|
/// the resolved symbol tables so that downstream passes (e.g. codegen) can
|
||||||
|
/// reuse them without re-running the checker.
|
||||||
|
pub struct CheckResult {
|
||||||
|
pub errors: Vec<Diagnostic>,
|
||||||
|
pub sigma: StructTable,
|
||||||
|
pub phi: FuncTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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>, no_main: bool) -> CheckResult {
|
||||||
|
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 ────────────────────────────────────────────
|
||||||
|
if !no_main {
|
||||||
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckResult {
|
||||||
|
errors: checker.errors,
|
||||||
|
sigma: checker.sigma,
|
||||||
|
phi: checker.phi,
|
||||||
|
}
|
||||||
|
}
|
||||||
384
fluxc/src/checker/stmt.rs
Normal file
384
fluxc/src/checker/stmt.rs
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::Checker;
|
||||||
|
use super::env::TypeEnv;
|
||||||
|
use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty};
|
||||||
|
use crate::diagnostics::{Diagnostic, Label};
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
619
fluxc/src/checker/tests.rs
Normal file
619
fluxc/src/checker/tests.rs
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
/// Checker integration tests.
|
||||||
|
///
|
||||||
|
/// Each helper parses a source string and runs the full 4-pass checker,
|
||||||
|
/// returning the list of error messages (without ANSI codes) so tests can
|
||||||
|
/// assert on their content.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::checker;
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Run the checker on `src` and return all diagnostic messages.
|
||||||
|
fn errors(src: &str) -> Vec<String> {
|
||||||
|
let mut parser = Parser::new(src);
|
||||||
|
let program = parser.parse_program();
|
||||||
|
assert!(
|
||||||
|
parser.errors.is_empty(),
|
||||||
|
"parse errors: {:?}",
|
||||||
|
parser.errors
|
||||||
|
);
|
||||||
|
checker::check(&program, false)
|
||||||
|
.errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| d.message)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that checking `src` produces no errors.
|
||||||
|
fn ok(src: &str) {
|
||||||
|
let msgs = errors(src);
|
||||||
|
assert!(msgs.is_empty(), "unexpected errors: {:?}", msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that checking `src` produces at least one error whose message
|
||||||
|
/// contains `needle`.
|
||||||
|
fn err_contains(src: &str, needle: &str) {
|
||||||
|
let msgs = errors(src);
|
||||||
|
assert!(
|
||||||
|
msgs.iter().any(|m| m.contains(needle)),
|
||||||
|
"expected error containing {:?}, got: {:?}",
|
||||||
|
needle,
|
||||||
|
msgs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 4: entry point ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_main_is_error() {
|
||||||
|
err_contains("fn foo() { }", "no `main`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_with_params_is_error() {
|
||||||
|
err_contains("fn main(x: i32) { }", "no parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_bad_return_type_is_error() {
|
||||||
|
err_contains("fn main() -> bool { return true; }", "return `()` or `i32`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_unit_return_ok() {
|
||||||
|
ok("fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_i32_return_ok() {
|
||||||
|
ok("fn main() -> i32 { return 0; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 1: duplicate definitions ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_function_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn foo() { } fn foo() { } fn main() { }",
|
||||||
|
"duplicate function `foo`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: i32 } struct S { y: i32 } fn main() { }",
|
||||||
|
"duplicate struct `S`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_reserved() {
|
||||||
|
err_contains("struct string_view { } fn main() { }", "reserved built-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 2: struct fields + cycles ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_field_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: Unknown } fn main() { }",
|
||||||
|
"undefined type `Unknown`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_size_cycle_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct A { b: B } struct B { a: A } fn main() { }",
|
||||||
|
"infinite size",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_pointer_cycle_ok() {
|
||||||
|
// Pointer-to-self does NOT cause a size cycle
|
||||||
|
ok("struct Node { next: *Node, value: i32 } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Literals ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_literal_coerces_to_annotated_type() {
|
||||||
|
// Integer literals have type UnboundInt and coerce to any annotated integer.
|
||||||
|
ok("fn main() { let a: i32 = 42; let b: u64 = 0; let c: u8 = 255; let d: i8 = -1; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_literal_is_f64() {
|
||||||
|
ok("fn main() { let x: f64 = 3.14; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bool_literal() {
|
||||||
|
ok("fn main() { let x: bool = true; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn char_literal() {
|
||||||
|
ok("fn main() { let x: char = 'a'; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Integer literal inference ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn literal_coerces_to_any_integer_size() {
|
||||||
|
// UnboundInt coerces to all integer types.
|
||||||
|
ok("fn main() { let a: u8 = 1; let b: u16 = 1; let c: u32 = 1; let d: u64 = 1; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unannotated_let_infers_from_context() {
|
||||||
|
// `let x = 0` gets type UnboundInt; used as u8 in comparison.
|
||||||
|
ok("fn f(n: u8) -> u8 {
|
||||||
|
let mut x = 0;
|
||||||
|
while x < n { x += 1; }
|
||||||
|
return x;
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn literal_in_binary_op_with_concrete_type() {
|
||||||
|
// `n - 1` where n: u8 — literal 1 resolves to u8.
|
||||||
|
ok("fn f(n: u8) -> u8 { return n - 1; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn literal_in_comparison_with_concrete_type() {
|
||||||
|
// `n < 2` where n: u8 — literal 2 resolves to u8.
|
||||||
|
ok("fn f(n: u8) -> bool { return n < 2; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unannotated_literal_variable_as_return() {
|
||||||
|
// `let x = 0;` with no annotation; returned as u64.
|
||||||
|
ok("fn f() -> u64 { let x = 0; return x; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fibonacci_rec_typechecks() {
|
||||||
|
ok("fn fibonacci_rec(n: u8) -> u64 {
|
||||||
|
if n < 2 { return n; }
|
||||||
|
return fibonacci_rec(n - 1) + fibonacci_rec(n - 2);
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fibonacci_iter_typechecks() {
|
||||||
|
ok("fn fibonacci_iter(n: u8) -> u64 {
|
||||||
|
let mut counter = 0;
|
||||||
|
let mut a = 0;
|
||||||
|
let mut b = 1;
|
||||||
|
while counter < n {
|
||||||
|
let temp = a + b;
|
||||||
|
a = b;
|
||||||
|
b = temp;
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn f32_promotes_to_f64_in_let() {
|
||||||
|
ok("fn foo(v: f32) { let x: f64 = v; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_literal_does_not_coerce_to_float() {
|
||||||
|
err_contains("fn main() { let x: f64 = 1; }", "type mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsigned_to_signed_forbidden_in_let() {
|
||||||
|
err_contains(
|
||||||
|
"fn foo(v: u32) { let x: i32 = v; } fn main() { }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_category_forbidden_in_let() {
|
||||||
|
// Annotated-to-annotated cross-category still rejected.
|
||||||
|
err_contains(
|
||||||
|
"fn foo(v: u32) { let x: i32 = v; } fn main() { }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Type inference ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_type_inferred_from_init() {
|
||||||
|
// `let x = 1;` → inferred as i32; later usage as i32 should pass
|
||||||
|
ok("fn add(a: i32, b: i32) -> i32 { return a + b; }
|
||||||
|
fn main() { let x = 1; let y = add(x, 2); }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_no_type_no_init_is_error() {
|
||||||
|
err_contains("fn main() { let x; }", "cannot infer type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Definite assignment ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_before_init_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let x: i32; let y = x; }",
|
||||||
|
"uninitialized variable `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_via_assignment_is_ok() {
|
||||||
|
ok("fn main() { let mut x: i32; x = 5; let y = x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn definite_assign_both_branches() {
|
||||||
|
ok("fn f(b: bool) -> i32 {
|
||||||
|
let mut x: i32;
|
||||||
|
if b { x = 1; } else { x = 2; }
|
||||||
|
return x;
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn definite_assign_missing_else_branch_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(b: bool) -> i32 {
|
||||||
|
let x: i32;
|
||||||
|
if b { x = 1; }
|
||||||
|
return x;
|
||||||
|
} fn main() { }",
|
||||||
|
"uninitialized variable `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Undefined variable / function ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_variable_is_error() {
|
||||||
|
err_contains("fn main() { let y = x; }", "undefined variable `x`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_function_is_error() {
|
||||||
|
err_contains("fn main() { foo(); }", "undefined function `foo`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arithmetic ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_same_type_ok() {
|
||||||
|
ok("fn main() { let x: i32 = 1 + 2; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_cross_category_is_error() {
|
||||||
|
// float + int: not in same category
|
||||||
|
err_contains(
|
||||||
|
"fn f(a: f64, b: i32) -> f64 { return a + b; } fn main() { }",
|
||||||
|
"+",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_result_is_lhs_type() {
|
||||||
|
ok("fn f(n: u32) -> u32 { return n << 2; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_rhs_can_be_different_int() {
|
||||||
|
ok("fn f(n: u32, s: u8) -> u32 { return n >> s; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_non_integer_lhs_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: f64) -> f64 { return x << 1; } fn main() { }",
|
||||||
|
"integer LHS",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Comparison / logical ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparison_ok() {
|
||||||
|
ok("fn f(a: i32, b: i32) -> bool { return a < b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_ok() {
|
||||||
|
ok("fn f(a: bool, b: bool) -> bool { return a and b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_non_bool_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(a: i32, b: bool) -> bool { return a and b; } fn main() { }",
|
||||||
|
"`and`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unary operators ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neg_on_signed_ok() {
|
||||||
|
ok("fn f(x: i32) -> i32 { return -x; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neg_on_unsigned_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: u32) -> u32 { return -x; } fn main() { }",
|
||||||
|
"unary `-`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_on_bool_ok() {
|
||||||
|
ok("fn f(b: bool) -> bool { return !b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_on_int_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) -> bool { return !x; } fn main() { }",
|
||||||
|
"unary `!`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bitnot_on_integer_ok() {
|
||||||
|
ok("fn f(x: u32) -> u32 { return ~x; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pointers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_immutable_binding_gives_immutable_ptr() {
|
||||||
|
// taking address of immutable x gives *i32; assigning to *mut i32 should fail
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let x: i32 = 1; let p: *mut i32 = &x; }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_mutable_binding_gives_mut_ptr() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; let p: *mut i32 = &x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mut_ptr_coerces_to_immutable_ptr() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; let p: *i32 = &x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deref_typed_ptr_ok() {
|
||||||
|
ok("fn f(p: *i32) -> i32 { return *p; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deref_opaque_ptr_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(p: *opaque) { let x = *p; } fn main() { }",
|
||||||
|
"opaque pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_non_place_is_error() {
|
||||||
|
err_contains("fn main() { let p = &(1 + 2); }", "non-place");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Struct literals ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_ok() {
|
||||||
|
ok("struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p: Point = Point { x: 1, y: 2 }; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_missing_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p = Point { x: 1 }; }",
|
||||||
|
"missing field `y`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_unknown_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p = Point { x: 1, y: 2, z: 3 }; }",
|
||||||
|
"no field `z`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_wrong_field_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: bool }
|
||||||
|
fn main() { let s = S { x: 42 }; }",
|
||||||
|
"field `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_undefined_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let s = Nope { x: 1 }; }",
|
||||||
|
"undefined struct `Nope`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Field access ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_ok() {
|
||||||
|
ok("struct S { v: i32 }
|
||||||
|
fn f(s: S) -> i32 { return s.v; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_unknown_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { v: i32 }
|
||||||
|
fn f(s: S) -> i32 { return s.nope; } fn main() { }",
|
||||||
|
"no field `nope`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_on_non_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) -> i32 { return x.foo; } fn main() { }",
|
||||||
|
"non-struct",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Index expressions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_index_ok() {
|
||||||
|
ok("fn f(arr: [i32; 4]) -> i32 { return arr[0]; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_index_ok() {
|
||||||
|
ok("fn f(p: *i32) -> i32 { return p[0]; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_integer_index_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(arr: [i32; 4]) -> i32 { return arr[true]; } fn main() { }",
|
||||||
|
"index must be an integer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Function calls ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_wrong_arg_count_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn add(a: i32, b: i32) -> i32 { return a + b; }
|
||||||
|
fn main() { let x = add(1); }",
|
||||||
|
"expects 2 argument(s), got 1",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_wrong_arg_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) { }
|
||||||
|
fn main() { f(true); }",
|
||||||
|
"argument 1",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_with_promotion_ok() {
|
||||||
|
// Passing i32 where i64 expected — implicit widening
|
||||||
|
ok("fn f(x: i64) { }
|
||||||
|
fn main() { let v: i32 = 1; f(v); }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Return type checking ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_return_in_non_unit_fn_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f() -> i32 { let x = 1; } fn main() { }",
|
||||||
|
"must always return",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_wrong_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f() -> i32 { return true; } fn main() { }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_value_from_unit_fn_is_error() {
|
||||||
|
err_contains("fn f() { return 1; } fn main() { }", "returns `()`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn early_return_satisfies_all_paths() {
|
||||||
|
ok("fn f(b: bool) -> i32 {
|
||||||
|
if b { return 1; } else { return 2; }
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Assignment / mutation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_to_immutable_is_error() {
|
||||||
|
err_contains("fn main() { let x: i32 = 0; x = 1; }", "mutable place");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_to_mutable_ok() {
|
||||||
|
ok("fn main() { let mut x: i32 = 0; x = 1; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compound_assign_ok() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; x += 2; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compound_assign_immutable_is_error() {
|
||||||
|
err_contains("fn main() { let x: i32 = 1; x += 2; }", "mutable place");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── break / continue ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn break_in_loop_ok() {
|
||||||
|
ok("fn main() { loop { break; } }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn break_outside_loop_is_error() {
|
||||||
|
err_contains("fn main() { break; }", "outside of a loop");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn continue_in_while_ok() {
|
||||||
|
ok("fn main() { let mut i: i32 = 0; while i < 10 { i += 1; continue; } }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn continue_outside_loop_is_error() {
|
||||||
|
err_contains("fn main() { continue; }", "outside of a loop");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── built-in string_view ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_fields_accessible() {
|
||||||
|
ok("fn f(s: string_view) -> u64 { return s.size; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_data_is_ptr_char() {
|
||||||
|
ok("fn f(s: string_view) -> *char { return s.data; } fn main() { }");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,19 @@ pub fn print_help() {
|
|||||||
"-V".bold(),
|
"-V".bold(),
|
||||||
"--version".bold(),
|
"--version".bold(),
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
" {} Compile to object file (no `main` required, no linking)",
|
||||||
|
"-c".bold(),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {} Emit LLVM IR and stop (implies `-c`)",
|
||||||
|
"-S".bold(),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {} {} Write output to <file>",
|
||||||
|
"-o".bold(),
|
||||||
|
"<file>".bold(),
|
||||||
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("{}", "ARGS:".bold().yellow());
|
println!("{}", "ARGS:".bold().yellow());
|
||||||
println!(
|
println!(
|
||||||
@@ -57,12 +70,22 @@ pub fn io_error(path: &str, err: std::io::Error) -> ! {
|
|||||||
|
|
||||||
pub struct Opts {
|
pub struct Opts {
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
|
/// `-c`: compile to object file without requiring a `main` entry point.
|
||||||
|
pub no_main: bool,
|
||||||
|
/// `-S`: emit LLVM IR text and stop (implies `-c`).
|
||||||
|
pub emit_ir: bool,
|
||||||
|
/// `-o <file>`: write final output to this path.
|
||||||
|
pub output: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_args() -> Opts {
|
pub fn parse_args() -> Opts {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
let mut no_main = false;
|
||||||
|
let mut emit_ir = false;
|
||||||
|
let mut output: Option<String> = None;
|
||||||
|
let mut args = std::env::args().skip(1).peekable();
|
||||||
|
|
||||||
for arg in std::env::args().skip(1) {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
print_help();
|
print_help();
|
||||||
@@ -72,6 +95,12 @@ pub fn parse_args() -> Opts {
|
|||||||
print_version();
|
print_version();
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
}
|
}
|
||||||
|
"-c" => no_main = true,
|
||||||
|
"-S" => { emit_ir = true; no_main = true; }
|
||||||
|
"-o" => match args.next() {
|
||||||
|
Some(path) => output = Some(path),
|
||||||
|
None => fatal("option `-o` requires an argument"),
|
||||||
|
},
|
||||||
flag if flag.starts_with('-') => {
|
flag if flag.starts_with('-') => {
|
||||||
fatal(&format!("unknown option `{flag}`"));
|
fatal(&format!("unknown option `{flag}`"));
|
||||||
}
|
}
|
||||||
@@ -83,5 +112,5 @@ pub fn parse_args() -> Opts {
|
|||||||
fatal("no input files — at least one source file is required");
|
fatal("no input files — at least one source file is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
Opts { files }
|
Opts { files, no_main, emit_ir, output }
|
||||||
}
|
}
|
||||||
|
|||||||
1418
fluxc/src/codegen/emit.rs
Normal file
1418
fluxc/src/codegen/emit.rs
Normal file
File diff suppressed because it is too large
Load Diff
120
fluxc/src/codegen/mod.rs
Normal file
120
fluxc/src/codegen/mod.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
pub mod emit;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
|
use crate::ast::{self, Parsed};
|
||||||
|
use crate::checker::CheckResult;
|
||||||
|
use crate::cli::Opts;
|
||||||
|
|
||||||
|
// ── Entry point ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Compile a parsed + type-checked program.
|
||||||
|
///
|
||||||
|
/// Mode is controlled by `opts`:
|
||||||
|
///
|
||||||
|
/// | flags | output | pipeline |
|
||||||
|
/// |----------|---------------------|-----------------------|
|
||||||
|
/// | (none) | `<stem>` executable | emit → opt → llc → cc |
|
||||||
|
/// | `-c` | `<stem>.o` | emit → opt → llc |
|
||||||
|
/// | `-S` | `<stem>.ll` | emit only |
|
||||||
|
///
|
||||||
|
/// Tool overrides via environment variables:
|
||||||
|
/// `FLUXC_OPT` (default `opt`), `FLUXC_LLC` (default `llc`),
|
||||||
|
/// `FLUXC_CC` (default `cc`).
|
||||||
|
pub fn compile(
|
||||||
|
input_path: &str,
|
||||||
|
program: &ast::Program<Parsed>,
|
||||||
|
result: CheckResult,
|
||||||
|
opts: &Opts,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// ── Derive output path ────────────────────────────────────────────────────
|
||||||
|
let stem = Path::new(input_path)
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "out".to_string());
|
||||||
|
|
||||||
|
let final_output = opts.output.clone().unwrap_or_else(|| {
|
||||||
|
if opts.emit_ir {
|
||||||
|
format!("{stem}.ll")
|
||||||
|
} else if opts.no_main {
|
||||||
|
format!("{stem}.o")
|
||||||
|
} else {
|
||||||
|
stem.clone()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Step 1: emit LLVM IR ──────────────────────────────────────────────────
|
||||||
|
let ir = emit::emit_program(program, &result.sigma, &result.phi);
|
||||||
|
|
||||||
|
// `-S`: write IR directly to final output and stop.
|
||||||
|
if opts.emit_ir {
|
||||||
|
return fs::write(&final_output, &ir)
|
||||||
|
.map_err(|e| format!("cannot write {final_output}: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Temp paths (only used when compiling beyond IR) ───────────────────────
|
||||||
|
let tmp = env::temp_dir();
|
||||||
|
let raw_ll = tmp.join(format!("fluxc_{stem}.ll"));
|
||||||
|
let opt_ll = tmp.join(format!("fluxc_{stem}.opt.ll"));
|
||||||
|
let obj = tmp.join(format!("fluxc_{stem}.o"));
|
||||||
|
|
||||||
|
fs::write(&raw_ll, &ir)
|
||||||
|
.map_err(|e| format!("cannot write IR to {}: {e}", raw_ll.display()))?;
|
||||||
|
|
||||||
|
// ── Step 2: opt ───────────────────────────────────────────────────────────
|
||||||
|
let opt_bin = tool_path("FLUXC_OPT", "opt");
|
||||||
|
run(
|
||||||
|
&opt_bin,
|
||||||
|
&["-O2", raw_ll.to_str().unwrap(), "-S", "-o", opt_ll.to_str().unwrap()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// ── Step 3: llc ───────────────────────────────────────────────────────────
|
||||||
|
let llc_bin = tool_path("FLUXC_LLC", "llc");
|
||||||
|
run(
|
||||||
|
&llc_bin,
|
||||||
|
&[opt_ll.to_str().unwrap(), "-filetype=obj", "-o", obj.to_str().unwrap()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// ── Step 4: link (skipped in `-c` mode) ───────────────────────────────────
|
||||||
|
if opts.no_main {
|
||||||
|
fs::copy(&obj, &final_output)
|
||||||
|
.map_err(|e| format!("cannot write {final_output}: {e}"))?;
|
||||||
|
} else {
|
||||||
|
let cc_bin = tool_path("FLUXC_CC", "cc");
|
||||||
|
run(&cc_bin, &[obj.to_str().unwrap(), "-o", &final_output])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clean up temp files ───────────────────────────────────────────────────
|
||||||
|
let _ = fs::remove_file(&raw_ll);
|
||||||
|
let _ = fs::remove_file(&opt_ll);
|
||||||
|
let _ = fs::remove_file(&obj);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn tool_path(env_var: &str, default: &str) -> String {
|
||||||
|
env::var(env_var).unwrap_or_else(|_| default.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(bin: &str, args: &[&str]) -> Result<(), String> {
|
||||||
|
let output = Command::new(bin)
|
||||||
|
.args(args)
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("failed to run `{bin}`: {e}"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(format!(
|
||||||
|
"`{bin}` exited with {}\n{}",
|
||||||
|
output.status,
|
||||||
|
stderr.trim_end()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ use std::{fs, process};
|
|||||||
use crate::parser::Parser;
|
use crate::parser::Parser;
|
||||||
|
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
pub mod checker;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod codegen;
|
||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
pub mod lexer;
|
pub mod lexer;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
@@ -13,6 +15,10 @@ fn main() {
|
|||||||
let opts = cli::parse_args();
|
let opts = cli::parse_args();
|
||||||
let mut had_errors = false;
|
let mut had_errors = false;
|
||||||
|
|
||||||
|
// Collect (path, source, program, check_result) for all input files.
|
||||||
|
// We gate codegen on all files being error-free.
|
||||||
|
let mut compiled = Vec::new();
|
||||||
|
|
||||||
for path in &opts.files {
|
for path in &opts.files {
|
||||||
let content = fs::read_to_string(path).unwrap_or_else(|e| cli::io_error(path, e));
|
let content = fs::read_to_string(path).unwrap_or_else(|e| cli::io_error(path, e));
|
||||||
|
|
||||||
@@ -24,7 +30,26 @@ fn main() {
|
|||||||
had_errors = true;
|
had_errors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{program:#?}");
|
if parser.errors.is_empty() {
|
||||||
|
let result = checker::check(&program, opts.no_main);
|
||||||
|
for diag in &result.errors {
|
||||||
|
eprint!("{}", diag.render(&content, path));
|
||||||
|
had_errors = true;
|
||||||
|
}
|
||||||
|
compiled.push((path.clone(), program, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if had_errors {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All files are clean — run codegen.
|
||||||
|
for (path, program, result) in compiled {
|
||||||
|
if let Err(e) = codegen::compile(&path, &program, result, &opts) {
|
||||||
|
eprintln!("{}: {e}", "error".to_string());
|
||||||
|
had_errors = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if had_errors {
|
if had_errors {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
|
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
|
||||||
Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type,
|
Parsed, Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind,
|
||||||
UnaryOp,
|
Type, UnaryOp,
|
||||||
},
|
},
|
||||||
diagnostics::{Diagnostic, Label},
|
diagnostics::{Diagnostic, Label},
|
||||||
lexer::Lexer,
|
lexer::Lexer,
|
||||||
@@ -235,7 +235,10 @@ impl<'src> Parser<'src> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
Type::OpaquePointer { mutable }
|
Type::OpaquePointer { mutable }
|
||||||
} else {
|
} else {
|
||||||
Type::Pointer { mutable, pointee: Box::new(self.parse_type()) }
|
Type::Pointer {
|
||||||
|
mutable,
|
||||||
|
pointee: Box::new(self.parse_type()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a block: `{ stmt* }`.
|
/// Parse a block: `{ stmt* }`.
|
||||||
pub fn parse_block(&mut self) -> Block {
|
pub fn parse_block(&mut self) -> Block<Parsed> {
|
||||||
let open = self.expect(TokenKind::LCurly);
|
let open = self.expect(TokenKind::LCurly);
|
||||||
let mut stmts = Vec::new();
|
let mut stmts = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -288,7 +291,7 @@ impl<'src> Parser<'src> {
|
|||||||
/// - *Synchronization*: tokens that can never start a statement or
|
/// - *Synchronization*: tokens that can never start a statement or
|
||||||
/// expression trigger `synchronize()`, which skips forward until the
|
/// expression trigger `synchronize()`, which skips forward until the
|
||||||
/// next statement boundary to prevent cascading errors.
|
/// next statement boundary to prevent cascading errors.
|
||||||
pub fn parse_stmt(&mut self) -> Stmt {
|
pub fn parse_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let tok = self.current();
|
let tok = self.current();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
TokenKind::Let => self.parse_let_stmt(),
|
TokenKind::Let => self.parse_let_stmt(),
|
||||||
@@ -351,13 +354,13 @@ impl<'src> Parser<'src> {
|
|||||||
/// `allow_struct_literals` controls whether a bare `Ident { … }` is
|
/// `allow_struct_literals` controls whether a bare `Ident { … }` is
|
||||||
/// parsed as a struct literal. Pass `false` in `if`/`while` conditions
|
/// parsed as a struct literal. Pass `false` in `if`/`while` conditions
|
||||||
/// so that `{` is not consumed as a struct body.
|
/// so that `{` is not consumed as a struct body.
|
||||||
pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr {
|
pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr<Parsed> {
|
||||||
self.pratt(0, allow_struct_literals)
|
self.pratt(0, allow_struct_literals)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Statement helpers ─────────────────────────────────────────────────────
|
// ── Statement helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_let_stmt(&mut self) -> Stmt {
|
fn parse_let_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let start = self.advance(); // consume `let`
|
let start = self.advance(); // consume `let`
|
||||||
let mutable = if self.current().kind == TokenKind::Mut {
|
let mutable = if self.current().kind == TokenKind::Mut {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -391,7 +394,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_return_stmt(&mut self) -> Stmt {
|
fn parse_return_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `return`
|
let kw = self.advance(); // consume `return`
|
||||||
// LL(1): `;` → unit return; anything else → parse expression
|
// LL(1): `;` → unit return; anything else → parse expression
|
||||||
let value = if self.current().kind != TokenKind::Semicolon {
|
let value = if self.current().kind != TokenKind::Semicolon {
|
||||||
@@ -406,7 +409,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_if_stmt(&mut self) -> Stmt {
|
fn parse_if_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `if`
|
let kw = self.advance(); // consume `if`
|
||||||
// Condition: expr_ns (no struct literals at outermost level)
|
// Condition: expr_ns (no struct literals at outermost level)
|
||||||
let cond = self.parse_expr(false);
|
let cond = self.parse_expr(false);
|
||||||
@@ -437,7 +440,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_while_stmt(&mut self) -> Stmt {
|
fn parse_while_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `while`
|
let kw = self.advance(); // consume `while`
|
||||||
let cond = self.parse_expr(false); // no struct literals in condition
|
let cond = self.parse_expr(false); // no struct literals in condition
|
||||||
let body = self.parse_block();
|
let body = self.parse_block();
|
||||||
@@ -448,7 +451,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_loop_stmt(&mut self) -> Stmt {
|
fn parse_loop_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `loop`
|
let kw = self.advance(); // consume `loop`
|
||||||
let body = self.parse_block();
|
let body = self.parse_block();
|
||||||
let span = kw.span.cover(body.span);
|
let span = kw.span.cover(body.span);
|
||||||
@@ -458,7 +461,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expr_stmt(&mut self) -> Stmt {
|
fn parse_expr_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let expr = self.parse_expr(true);
|
let expr = self.parse_expr(true);
|
||||||
let semi = self.expect(TokenKind::Semicolon);
|
let semi = self.expect(TokenKind::Semicolon);
|
||||||
let span = expr.span.cover(semi.span);
|
let span = expr.span.cover(semi.span);
|
||||||
@@ -470,7 +473,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
// ── Pratt core ────────────────────────────────────────────────────────────
|
// ── Pratt core ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr {
|
fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr<Parsed> {
|
||||||
let mut lhs = self.parse_nud(allow_struct_lit);
|
let mut lhs = self.parse_nud(allow_struct_lit);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -504,7 +507,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
// ── Null denotation (prefix / primary) ───────────────────────────────────
|
// ── Null denotation (prefix / primary) ───────────────────────────────────
|
||||||
|
|
||||||
fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr {
|
fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr<Parsed> {
|
||||||
let tok = self.advance();
|
let tok = self.advance();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
// Literals
|
// Literals
|
||||||
@@ -558,11 +561,11 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
fn parse_led(
|
fn parse_led(
|
||||||
&mut self,
|
&mut self,
|
||||||
lhs: Expr,
|
lhs: Expr<Parsed>,
|
||||||
op_tok: Token<'src>,
|
op_tok: Token<'src>,
|
||||||
r_bp: u8,
|
r_bp: u8,
|
||||||
allow_struct_lit: bool,
|
allow_struct_lit: bool,
|
||||||
) -> Expr {
|
) -> Expr<Parsed> {
|
||||||
// Consume the operator token.
|
// Consume the operator token.
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
@@ -647,7 +650,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
/// Called after we have already parsed the leading `Ident` as `lhs` and
|
/// Called after we have already parsed the leading `Ident` as `lhs` and
|
||||||
/// the current token is `{`.
|
/// the current token is `{`.
|
||||||
fn parse_struct_lit(&mut self, name_expr: Expr) -> Expr {
|
fn parse_struct_lit(&mut self, name_expr: Expr<Parsed>) -> Expr<Parsed> {
|
||||||
let (name, name_span) = match name_expr.kind {
|
let (name, name_span) = match name_expr.kind {
|
||||||
ExprKind::Ident(ref s) => (s.clone(), name_expr.span),
|
ExprKind::Ident(ref s) => (s.clone(), name_expr.span),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -669,7 +672,7 @@ impl<'src> Parser<'src> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_field_list(&mut self) -> Vec<StructField> {
|
fn parse_struct_field_list(&mut self) -> Vec<StructField<Parsed>> {
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if matches!(self.current().kind, TokenKind::RCurly | TokenKind::Eof) {
|
if matches!(self.current().kind, TokenKind::RCurly | TokenKind::Eof) {
|
||||||
@@ -685,7 +688,7 @@ impl<'src> Parser<'src> {
|
|||||||
fields
|
fields
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_field(&mut self) -> StructField {
|
fn parse_struct_field(&mut self) -> StructField<Parsed> {
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::Colon);
|
self.expect(TokenKind::Colon);
|
||||||
// Struct literals allowed inside field values.
|
// Struct literals allowed inside field values.
|
||||||
@@ -702,7 +705,7 @@ impl<'src> Parser<'src> {
|
|||||||
// ── Top-level definitions ─────────────────────────────────────────────────
|
// ── Top-level definitions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Parse an entire source file as a `Program`.
|
/// Parse an entire source file as a `Program`.
|
||||||
pub fn parse_program(&mut self) -> Program {
|
pub fn parse_program(&mut self) -> Program<Parsed> {
|
||||||
let start = self.current().span;
|
let start = self.current().span;
|
||||||
let mut defs = Vec::new();
|
let mut defs = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -716,7 +719,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse one top-level definition (`fn` or `struct`).
|
/// Parse one top-level definition (`fn` or `struct`).
|
||||||
pub fn parse_top_level_def(&mut self) -> TopLevelDef {
|
pub fn parse_top_level_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let tok = self.current();
|
let tok = self.current();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
TokenKind::Fn => self.parse_func_def(),
|
TokenKind::Fn => self.parse_func_def(),
|
||||||
@@ -749,7 +752,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_func_def(&mut self) -> TopLevelDef {
|
fn parse_func_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let kw = self.advance(); // consume `fn`
|
let kw = self.advance(); // consume `fn`
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::LParen);
|
self.expect(TokenKind::LParen);
|
||||||
@@ -809,7 +812,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_def(&mut self) -> TopLevelDef {
|
fn parse_struct_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let kw = self.advance(); // consume `struct`
|
let kw = self.advance(); // consume `struct`
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::LCurly);
|
self.expect(TokenKind::LCurly);
|
||||||
@@ -855,7 +858,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
/// Parse `arg, arg, …` up to `)`. The opening `(` has already been
|
/// Parse `arg, arg, …` up to `)`. The opening `(` has already been
|
||||||
/// consumed by `parse_led`. Returns `(args, close_span)`.
|
/// consumed by `parse_led`. Returns `(args, close_span)`.
|
||||||
fn parse_arg_list(&mut self) -> (Vec<Expr>, Span) {
|
fn parse_arg_list(&mut self) -> (Vec<Expr<Parsed>>, Span) {
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if matches!(self.current().kind, TokenKind::RParen | TokenKind::Eof) {
|
if matches!(self.current().kind, TokenKind::RParen | TokenKind::Eof) {
|
||||||
@@ -879,21 +882,21 @@ impl<'src> Parser<'src> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ast::{ElseBranch, ExprKind, StmtKind, TopLevelDefKind, Type};
|
use crate::ast::{ElseBranch, ExprKind, Parsed, StmtKind, TopLevelDefKind, Type};
|
||||||
|
|
||||||
// ── Expression test helpers ───────────────────────────────────────────────
|
// ── Expression test helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse(src: &str) -> Expr {
|
fn parse(src: &str) -> Expr<Parsed> {
|
||||||
Parser::new(src).parse_expr(true)
|
Parser::new(src).parse_expr(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_no_struct(src: &str) -> Expr {
|
fn parse_no_struct(src: &str) -> Expr<Parsed> {
|
||||||
Parser::new(src).parse_expr(false)
|
Parser::new(src).parse_expr(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Statement test helpers ────────────────────────────────────────────────
|
// ── Statement test helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
fn stmt(src: &str) -> Stmt {
|
fn stmt(src: &str) -> Stmt<Parsed> {
|
||||||
Parser::new(src).parse_stmt()
|
Parser::new(src).parse_stmt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,7 +1226,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn type_nested_pointer() {
|
fn type_nested_pointer() {
|
||||||
// `**i32` → Pointer { Pointer { I32 } }
|
// `**i32` → Pointer { Pointer { I32 } }
|
||||||
assert!(matches!(parse_type_str("**i32"), Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
parse_type_str("**i32"),
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1444,7 +1450,7 @@ mod tests {
|
|||||||
|
|
||||||
// ── Function definition tests ─────────────────────────────────────────────
|
// ── Function definition tests ─────────────────────────────────────────────
|
||||||
|
|
||||||
fn top(src: &str) -> TopLevelDef {
|
fn top(src: &str) -> TopLevelDef<Parsed> {
|
||||||
Parser::new(src).parse_top_level_def()
|
Parser::new(src).parse_top_level_def()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,7 +1509,10 @@ mod tests {
|
|||||||
let d = top("fn foo(p: *i32) { }");
|
let d = top("fn foo(p: *i32) { }");
|
||||||
match &d.kind {
|
match &d.kind {
|
||||||
TopLevelDefKind::Func(f) => {
|
TopLevelDefKind::Func(f) => {
|
||||||
assert!(matches!(f.params[0].ty, Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
f.params[0].ty,
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => panic!("expected func def"),
|
_ => panic!("expected func def"),
|
||||||
}
|
}
|
||||||
@@ -1543,7 +1552,10 @@ mod tests {
|
|||||||
let d = top("struct Node { value: i32, next: *Node }");
|
let d = top("struct Node { value: i32, next: *Node }");
|
||||||
match &d.kind {
|
match &d.kind {
|
||||||
TopLevelDefKind::Struct(s) => {
|
TopLevelDefKind::Struct(s) => {
|
||||||
assert!(matches!(s.fields[1].ty, Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
s.fields[1].ty,
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => panic!("expected struct def"),
|
_ => panic!("expected struct def"),
|
||||||
}
|
}
|
||||||
@@ -1551,7 +1563,7 @@ mod tests {
|
|||||||
|
|
||||||
// ── Program tests ─────────────────────────────────────────────────────────
|
// ── Program tests ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn program(src: &str) -> Program {
|
fn program(src: &str) -> Program<Parsed> {
|
||||||
Parser::new(src).parse_program()
|
Parser::new(src).parse_program()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user